Fix update discovery and isolate fingerprint installer permissions
This commit is contained in:
@@ -17,6 +17,8 @@ include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||
|
||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Core Quick)
|
||||
find_package(KF6 6.0 REQUIRED COMPONENTS Auth Config CoreAddons I18n KCMUtils)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(RPM REQUIRED IMPORTED_TARGET rpm)
|
||||
|
||||
add_subdirectory(src)
|
||||
if(BUILD_TESTING)
|
||||
|
||||
@@ -14,6 +14,11 @@ installation to a narrowly scoped KAuth helper. The helper accepts only valid
|
||||
package names that independently resolve to that capability in the
|
||||
`fedora-tools` repository.
|
||||
|
||||
Update detection compares repository and installed versions using RPM's version
|
||||
ordering, without requiring access to DNF's privileged transaction state. DNF
|
||||
still checks dependencies when installing an update. The repository file sets a
|
||||
five-minute metadata expiry so Discover's PackageKit backend checks it regularly.
|
||||
|
||||
Installed tools can also be configured or removed from the module. Package
|
||||
operations run one at a time; other actions remain disabled until DNF finishes.
|
||||
The module currently configures task-group shortcut behaviour, touchpad hold-tap
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: fedora-tools-settings
|
||||
Version: 0.1.0
|
||||
Release: 13%{?dist}
|
||||
Release: 14%{?dist}
|
||||
Summary: Plasma System Settings module for Fedora Tools
|
||||
|
||||
License: MIT
|
||||
@@ -18,6 +18,7 @@ BuildRequires: kf6-kconfig-devel
|
||||
BuildRequires: kf6-ki18n-devel
|
||||
BuildRequires: kf6-kirigami-devel
|
||||
BuildRequires: qt6-qtdeclarative-devel
|
||||
BuildRequires: rpm-devel
|
||||
|
||||
Requires: dnf5
|
||||
Requires: kf6-kauth
|
||||
@@ -74,6 +75,10 @@ done
|
||||
%{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy
|
||||
|
||||
%changelog
|
||||
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-14
|
||||
- Detect newer packages without reading privileged DNF state
|
||||
- Report package query failures instead of silently hiding updates
|
||||
|
||||
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-13
|
||||
- Share settings validation and report failed configuration writes
|
||||
- Simplify ordering and source packaging
|
||||
|
||||
@@ -10,6 +10,7 @@ kcmutils_add_qml_kcm(kcm_fedora_tools
|
||||
toolmodel.h
|
||||
)
|
||||
target_link_libraries(kcm_fedora_tools PRIVATE
|
||||
PkgConfig::RPM
|
||||
Qt6::Core
|
||||
Qt6::Quick
|
||||
KF6::AuthCore
|
||||
|
||||
@@ -142,7 +142,6 @@ void FedoraToolsKcm::refresh(bool refreshMetadata)
|
||||
m_refreshMetadata = refreshMetadata;
|
||||
m_installedPackages.clear();
|
||||
m_availablePackages.clear();
|
||||
m_updates.clear();
|
||||
setMessage({});
|
||||
setBusy(true);
|
||||
|
||||
@@ -316,8 +315,12 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
|
||||
const QByteArray output = m_query.readAllStandardOutput();
|
||||
const QByteArray errorOutput = m_query.readAllStandardError();
|
||||
|
||||
if (exitStatus != QProcess::NormalExit) {
|
||||
setMessage(i18n("Package discovery terminated unexpectedly."), true);
|
||||
// rpm -q reports missing packages on stdout with exit code 1. That is a
|
||||
// normal discovery result; stderr or a failed repository query is not.
|
||||
const QString details = QString::fromUtf8(errorOutput).trimmed();
|
||||
if (exitStatus != QProcess::NormalExit
|
||||
|| (exitCode != 0 && (m_stage == QueryStage::Available || !details.isEmpty() || exitCode != 1))) {
|
||||
setMessage(details.isEmpty() ? i18n("Package discovery failed (%1).", m_query.program()) : details, true);
|
||||
m_stage = QueryStage::Idle;
|
||||
setPackageOperation({}, {});
|
||||
setBusy(false);
|
||||
@@ -342,43 +345,12 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
|
||||
}
|
||||
|
||||
if (m_stage == QueryStage::Available) {
|
||||
if (exitCode != 0) {
|
||||
m_tools.setPackages(m_installedPackages, {});
|
||||
const QString details = QString::fromUtf8(errorOutput).trimmed();
|
||||
setMessage(details.isEmpty() ? i18n("The Fedora Tools repository is unavailable.") : details, true);
|
||||
updateFingerprintStatus();
|
||||
m_stage = QueryStage::Idle;
|
||||
setPackageOperation({}, {});
|
||||
setBusy(false);
|
||||
return;
|
||||
}
|
||||
|
||||
m_availablePackages = parsePackageRecords(output);
|
||||
if (m_availablePackages.isEmpty()) {
|
||||
finishRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
startQuery(QStringLiteral("/usr/bin/dnf5"),
|
||||
{QStringLiteral("--repo=fedora-tools"),
|
||||
QStringLiteral("-q"),
|
||||
QStringLiteral("repoquery"),
|
||||
QStringLiteral("--available"),
|
||||
QStringLiteral("--upgrades"),
|
||||
QStringLiteral("--whatprovides=fedora-tools-tool"),
|
||||
QStringLiteral("--queryformat"),
|
||||
packageFormat},
|
||||
QueryStage::Updates);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_stage == QueryStage::Updates) {
|
||||
if (exitCode == 0) {
|
||||
for (const PackageRecord &package : parsePackageRecords(output)) {
|
||||
m_updates.insert(package.name);
|
||||
}
|
||||
}
|
||||
|
||||
QStringList packageNames;
|
||||
for (const PackageRecord &package : std::as_const(m_availablePackages)) {
|
||||
packageNames.append(package.name);
|
||||
@@ -407,7 +379,7 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
|
||||
|
||||
void FedoraToolsKcm::finishRefresh()
|
||||
{
|
||||
m_tools.setPackages(m_installedPackages, m_availablePackages, m_updates);
|
||||
m_tools.setPackages(m_installedPackages, m_availablePackages);
|
||||
updateFingerprintStatus();
|
||||
m_stage = QueryStage::Idle;
|
||||
setPackageOperation({}, {});
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <KQuickConfigModule>
|
||||
|
||||
#include <QProcess>
|
||||
#include <QSet>
|
||||
#include <QUrl>
|
||||
|
||||
class KJob;
|
||||
@@ -75,7 +74,6 @@ private:
|
||||
Idle,
|
||||
InstalledProviders,
|
||||
Available,
|
||||
Updates,
|
||||
KnownInstalled,
|
||||
};
|
||||
|
||||
@@ -95,7 +93,6 @@ private:
|
||||
QueryStage m_stage = QueryStage::Idle;
|
||||
QList<PackageRecord> m_installedPackages;
|
||||
QList<PackageRecord> m_availablePackages;
|
||||
QSet<QString> m_updates;
|
||||
bool m_refreshMetadata = false;
|
||||
bool m_busy = false;
|
||||
bool m_error = false;
|
||||
|
||||
@@ -10,12 +10,26 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QStandardPaths>
|
||||
#include <rpm/rpmver.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool newerVersion(const QString &available, const QString &installed)
|
||||
{
|
||||
const auto candidate = rpmverParse(available.toUtf8().constData());
|
||||
const auto current = rpmverParse(installed.toUtf8().constData());
|
||||
if (!candidate || !current) {
|
||||
qWarning() << "Cannot compare RPM versions:" << available << installed;
|
||||
}
|
||||
const bool newer = candidate && current && rpmverCmp(candidate, current) > 0;
|
||||
rpmverFree(candidate);
|
||||
rpmverFree(current);
|
||||
return newer;
|
||||
}
|
||||
|
||||
QStringList readConfigurationCommand(const QString &packageName)
|
||||
{
|
||||
const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
|
||||
@@ -119,8 +133,7 @@ QHash<int, QByteArray> ToolModel::roleNames() const
|
||||
}
|
||||
|
||||
void ToolModel::setPackages(const QList<PackageRecord> &installed,
|
||||
const QList<PackageRecord> &available,
|
||||
const QSet<QString> &updates)
|
||||
const QList<PackageRecord> &available)
|
||||
{
|
||||
QMap<QString, Tool> tools;
|
||||
|
||||
@@ -147,7 +160,8 @@ void ToolModel::setPackages(const QList<PackageRecord> &installed,
|
||||
}
|
||||
tool.compatibilityError = incompatible;
|
||||
tool.available = incompatible.isEmpty();
|
||||
tool.updateAvailable = tool.available && tool.installed && updates.contains(package.name);
|
||||
tool.updateAvailable = tool.available && tool.installed
|
||||
&& newerVersion(package.version, tool.version);
|
||||
}
|
||||
|
||||
beginResetModel();
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "packageutils.h"
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
|
||||
struct Tool
|
||||
@@ -48,8 +47,7 @@ public:
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
void setPackages(const QList<PackageRecord> &installed,
|
||||
const QList<PackageRecord> &available,
|
||||
const QSet<QString> &updates = {});
|
||||
const QList<PackageRecord> &available);
|
||||
bool mayInstall(const QString &packageName) const;
|
||||
bool mayRemove(const QString &packageName) const;
|
||||
QStringList configurationCommand(const QString &packageName) const;
|
||||
|
||||
@@ -14,7 +14,7 @@ add_executable(test-toolmodel
|
||||
../src/packageutils.cpp
|
||||
)
|
||||
target_include_directories(test-toolmodel PRIVATE ../src)
|
||||
target_link_libraries(test-toolmodel PRIVATE Qt6::Core Qt6::Test)
|
||||
target_link_libraries(test-toolmodel PRIVATE Qt6::Core Qt6::Test PkgConfig::RPM)
|
||||
add_test(NAME toolmodel COMMAND test-toolmodel)
|
||||
|
||||
add_executable(test-qmlresource test-qmlresource.cpp)
|
||||
|
||||
@@ -12,6 +12,38 @@ class ToolModelTest : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void comparesRpmVersions_data()
|
||||
{
|
||||
QTest::addColumn<QString>("installed");
|
||||
QTest::addColumn<QString>("available");
|
||||
QTest::addColumn<bool>("update");
|
||||
QTest::newRow("numeric release") << QStringLiteral("0.1.0-9.fc44") << QStringLiteral("0.1.0-11.fc44") << true;
|
||||
QTest::newRow("same version") << QStringLiteral("0.1.0-11.fc44") << QStringLiteral("0.1.0-11.fc44") << false;
|
||||
QTest::newRow("locally newer") << QStringLiteral("0.1.0-25.fc44") << QStringLiteral("0.1.0-23.fc44") << false;
|
||||
QTest::newRow("epoch wins") << QStringLiteral("2.0-1") << QStringLiteral("1:1.0-1") << true;
|
||||
QTest::newRow("installed epoch wins") << QStringLiteral("1:1.0-1") << QStringLiteral("2.0-1") << false;
|
||||
QTest::newRow("implicit zero epoch") << QStringLiteral("0:1.0-1") << QStringLiteral("1.0-1") << false;
|
||||
QTest::newRow("prerelease") << QStringLiteral("1.0~rc1-1") << QStringLiteral("1.0-1") << true;
|
||||
QTest::newRow("snapshot") << QStringLiteral("1.0-1") << QStringLiteral("1.0^git1-1") << true;
|
||||
QTest::newRow("newer than snapshot") << QStringLiteral("1.0^git1-1") << QStringLiteral("1.0.1-1") << true;
|
||||
}
|
||||
|
||||
void comparesRpmVersions()
|
||||
{
|
||||
QFETCH(QString, installed);
|
||||
QFETCH(QString, available);
|
||||
QFETCH(bool, update);
|
||||
ToolModel model;
|
||||
// Match an older noarch install against its native-architecture update.
|
||||
// No DNF transaction state or privileged files are needed to compare EVRs.
|
||||
const PackageRecord current{QStringLiteral("touchpad-hold-tap"), installed, QStringLiteral("noarch"), QStringLiteral("Gesture")};
|
||||
const PackageRecord candidate{current.name, available, QStringLiteral("x86_64"), current.summary};
|
||||
model.setPackages({current}, {candidate});
|
||||
QCOMPARE(model.data(model.index(0), ToolModel::UpdateAvailableRole).toBool(), update);
|
||||
model.setPackages({}, {candidate});
|
||||
QVERIFY(!model.data(model.index(0), ToolModel::UpdateAvailableRole).toBool());
|
||||
}
|
||||
|
||||
void incompatibleFrameworkIsOnlyListedWhenInstalled() {
|
||||
if (packageCompatibilityError(QStringLiteral("framework-laptop-tools")).isEmpty()) QSKIP("Requires a non-Framework test host");
|
||||
const PackageRecord package{QStringLiteral("framework-laptop-tools"), QStringLiteral("1"), QStringLiteral("x86_64"), QStringLiteral("Hardware controls")};
|
||||
@@ -55,7 +87,7 @@ private Q_SLOTS:
|
||||
const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-5.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
|
||||
const PackageRecord available{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-6.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
|
||||
|
||||
model.setPackages({installed}, {available}, {QStringLiteral("touchpad-hold-tap")});
|
||||
model.setPackages({installed}, {available});
|
||||
|
||||
QCOMPARE(model.rowCount(), 1);
|
||||
const QModelIndex index = model.index(0);
|
||||
@@ -67,7 +99,7 @@ private Q_SLOTS:
|
||||
QCOMPARE(model.mayRemove(QStringLiteral("missing-tool")), false);
|
||||
}
|
||||
|
||||
void doesNotGuessUpdatesFromDifferentVersionStrings()
|
||||
void doesNotOfferDowngrades()
|
||||
{
|
||||
ToolModel model;
|
||||
const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("0.1.0-7.fc44"), QStringLiteral("noarch"), QStringLiteral("Gesture")};
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
name=Small tools for Fedora
|
||||
baseurl=https://git.ajpanton.se/api/packages/ajp_anton/rpm/fedora/$releasever
|
||||
enabled=1
|
||||
metadata_expire=300
|
||||
gpgcheck=1
|
||||
gpgkey=https://git.ajpanton.se/api/packages/ajp_anton/rpm/repository.key
|
||||
|
||||
@@ -36,6 +36,12 @@ require_fedora() {
|
||||
[[ ${ID:-} == fedora ]] || die "this workaround only supports Fedora Linux"
|
||||
}
|
||||
|
||||
run_dnf() (
|
||||
# The private download's umask must not make DNF's system state root-only.
|
||||
umask 022
|
||||
dnf5 "$@"
|
||||
)
|
||||
|
||||
cleanup() {
|
||||
[[ -n $temporary_dir ]] || return
|
||||
rm -f -- "$temporary_dir/kscreenlocker.rpm"
|
||||
@@ -233,7 +239,7 @@ enable_workaround() {
|
||||
|
||||
dnf_args=(install --assumeyes)
|
||||
(( force )) && dnf_args+=(--allow-downgrade)
|
||||
dnf5 "${dnf_args[@]}" "$candidate"
|
||||
run_dnf "${dnf_args[@]}" "$candidate"
|
||||
|
||||
is_workaround_package -q kscreenlocker ||
|
||||
die "DNF completed without enabling the workaround"
|
||||
@@ -264,7 +270,7 @@ disable_workaround() {
|
||||
printf 'This will replace the workaround with Fedora\x27s current KScreenLocker package.\n'
|
||||
confirm
|
||||
|
||||
dnf5 \
|
||||
run_dnf \
|
||||
--repo=fedora,updates \
|
||||
--refresh \
|
||||
distro-sync \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: plasma-fingerprint-workaround
|
||||
Version: 0.1.0
|
||||
Release: 5%{?dist}
|
||||
Release: 6%{?dist}
|
||||
Summary: Opt-in patched KScreenLocker for fingerprint recovery after suspend
|
||||
|
||||
License: MIT
|
||||
@@ -51,6 +51,9 @@ install -D -m 0644 %{SOURCE3} \
|
||||
%{_datadir}/plasma-fingerprint-workaround/payload.conf
|
||||
|
||||
%changelog
|
||||
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-6
|
||||
- Keep private-download permissions from leaking into DNF's system state
|
||||
|
||||
* Sat Sep 12 2026 fedora-tools contributors - 0.1.0-5
|
||||
- Simplify status queries and extend invalid-command tests
|
||||
|
||||
|
||||
@@ -29,4 +29,23 @@ source "$payload_config"
|
||||
[[ $payload_url == https://*/*.rpm ]]
|
||||
[[ $payload_sha256 =~ ^[[:xdigit:]]{64}$ ]]
|
||||
|
||||
# Load the functions through the harmless help command, then replace DNF with
|
||||
# a test function. Never invoke the package manager in this test.
|
||||
(
|
||||
source "$controller" --help >/dev/null
|
||||
dnf5() {
|
||||
[[ $(umask) == 0022 && $1 == test-argument ]]
|
||||
}
|
||||
umask 077
|
||||
run_dnf test-argument
|
||||
[[ $(umask) == 0077 ]]
|
||||
dnf5() { return 23; }
|
||||
if run_dnf test-argument; then
|
||||
printf 'DNF failure was not propagated.\n' >&2
|
||||
exit 1
|
||||
else
|
||||
[[ $? == 23 ]]
|
||||
fi
|
||||
)
|
||||
|
||||
printf 'Controller tests passed.\n'
|
||||
|
||||
Reference in New Issue
Block a user