From fbd93113846e061f5a8c7933a1c859104bcf81d3 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Thu, 12 Feb 2026 14:34:56 +0100 Subject: [PATCH] gamecenter: systemd scopes + Stop reliability --- src/gamecenter/CMakeLists.txt | 33 +--- src/gamecenter/gamecenterdaemon.cpp | 243 +++++++++++++++++++++----- src/gamecenter/gamecenterdaemon.h | 17 +- src/gamecenter/main.cpp | 26 +-- src/gamecenter/processscanner.cpp | 145 ++++++++++++--- src/gamecenter/processscanner.h | 10 ++ src/gamecenter/systemdusermanager.cpp | 11 ++ src/gamecenter/systemdusermanager.h | 2 + 8 files changed, 364 insertions(+), 123 deletions(-) diff --git a/src/gamecenter/CMakeLists.txt b/src/gamecenter/CMakeLists.txt index 3b53024..c95eb66 100644 --- a/src/gamecenter/CMakeLists.txt +++ b/src/gamecenter/CMakeLists.txt @@ -13,7 +13,10 @@ add_executable(alakarte_gamecenter target_link_libraries(alakarte_gamecenter PRIVATE Qt6::Core + Qt6::Concurrent Qt6::DBus + KF6::CoreAddons + KF6::DBusAddons ) set_target_properties(alakarte_gamecenter PROPERTIES @@ -24,9 +27,6 @@ install(TARGETS alakarte_gamecenter ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus") file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd") -file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus-system") -file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd-system") -file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus-policy") configure_file(dbus/org.kde.GameCenter1.service.in "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service" @@ -38,21 +38,6 @@ configure_file(systemd/org.kde.GameCenter1.service.in @ONLY ) -configure_file(dbus/org.kde.GameCenter1.system.service.in - "${CMAKE_CURRENT_BINARY_DIR}/dbus-system/org.kde.GameCenter1.service" - @ONLY -) - -configure_file(systemd/org.kde.GameCenter1.system.service.in - "${CMAKE_CURRENT_BINARY_DIR}/systemd-system/org.kde.GameCenter1.service" - @ONLY -) - -configure_file(dbus/org.kde.GameCenter1.conf.in - "${CMAKE_CURRENT_BINARY_DIR}/dbus-policy/org.kde.GameCenter1.conf" - @ONLY -) - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service" DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR} ) @@ -60,15 +45,3 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service" install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service" DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR} ) - -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus-system/org.kde.GameCenter1.service" - DESTINATION ${KDE_INSTALL_DBUSSYSTEMSERVICEDIR} -) - -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd-system/org.kde.GameCenter1.service" - DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR} -) - -install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus-policy/org.kde.GameCenter1.conf" - DESTINATION ${KDE_INSTALL_SYSCONFDIR}/dbus-1/system.d -) diff --git a/src/gamecenter/gamecenterdaemon.cpp b/src/gamecenter/gamecenterdaemon.cpp index c450fbd..162fd76 100644 --- a/src/gamecenter/gamecenterdaemon.cpp +++ b/src/gamecenter/gamecenterdaemon.cpp @@ -25,6 +25,14 @@ #include +#include +#include + +#ifdef ALAKARTE_HAVE_KAUTH +#include +#include +#endif + #include static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1"); @@ -43,6 +51,26 @@ static QString ensureScopeUnitName(const QString &unitName) namespace { +static void terminatePids(const QList &pids) +{ + for (uint pid : pids) { + if (pid == 0) { + continue; + } + ::kill(static_cast(pid), SIGTERM); + } +} + +static void killPids(const QList &pids) +{ + for (uint pid : pids) { + if (pid == 0) { + continue; + } + ::kill(static_cast(pid), SIGKILL); + } +} + static QVariant unwrapDbusVariant(QVariant v) { if (v.canConvert()) { @@ -236,6 +264,8 @@ private: } // namespace +#if 0 + static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1"); static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1"); static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1"); @@ -675,6 +705,8 @@ bool GameCenterSystemProxy::init() return d->init(); } +#endif + void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath) { if (unitName.isEmpty()) { @@ -910,6 +942,18 @@ QVariantMap GameCenterDaemon::GetCapabilities() const QVariantMap caps; caps.insert(QStringLiteral("supportsSystemd"), m_systemd.isAvailable()); caps.insert(QStringLiteral("supportsSystemBus"), false); +#ifdef ALAKARTE_HAVE_KAUTH + bool supportsPowerProfiles = false; + { + QDBusConnection sysBus = QDBusConnection::systemBus(); + if (sysBus.isConnected() && sysBus.interface()) { + supportsPowerProfiles = sysBus.interface()->isServiceRegistered(QStringLiteral("net.hadess.PowerProfiles")); + } + } + caps.insert(QStringLiteral("supportsPowerProfiles"), supportsPowerProfiles); +#else + caps.insert(QStringLiteral("supportsPowerProfiles"), false); +#endif const bool systemdAvailable = m_systemd.isAvailable(); const bool steamAvailable = !QStandardPaths::findExecutable(QStringLiteral("steam")).isEmpty(); const bool lutrisAvailable = !QStandardPaths::findExecutable(QStringLiteral("lutris")).isEmpty(); @@ -948,12 +992,44 @@ void GameCenterDaemon::SetPolicy(const QVariantMap &policy) } m_maxConcurrent = v.toInt(); } + + if (policy.contains(QStringLiteral("powerProfile"))) { + QVariant v = policy.value(QStringLiteral("powerProfile")); + if (v.canConvert()) { + v = v.value().variant(); + } + + const QString nextProfile = v.toString(); + if (!nextProfile.isEmpty() && nextProfile != m_powerProfile) { +#ifdef ALAKARTE_HAVE_KAUTH + const QString previousProfile = m_powerProfile; + QVariantMap args; + args.insert(QStringLiteral("profile"), nextProfile); + + KAuth::Action action(QStringLiteral("org.kde.alakarte.gamecenter.helper.setpowerprofile")); + action.setHelperId(QStringLiteral("org.kde.alakarte.gamecenter.helper")); + action.setArguments(args); + + KAuth::ExecuteJob *job = action.execute(); + if (job) { + if (job->exec()) { + m_powerProfile = nextProfile; + } else { + m_powerProfile = previousProfile; + } + } +#else + m_powerProfile = nextProfile; +#endif + } + } } QVariantMap GameCenterDaemon::GetPolicy() const { return { {QStringLiteral("maxConcurrent"), m_maxConcurrent}, + {QStringLiteral("powerProfile"), m_powerProfile}, }; } @@ -1020,7 +1096,10 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec) v = v.value().variant(); } if (v.canConvert()) { - return v.toMap(); + const QVariantMap asMap = v.toMap(); + if (!asMap.isEmpty()) { + return asMap; + } } if (v.canConvert()) { const QDBusArgument arg = v.value(); @@ -1033,9 +1112,24 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec) return converted; } - QVariantMap asMap = qdbus_cast(arg); - if (!asMap.isEmpty()) { - return asMap; + QVariantMap converted; + { + QDBusArgument mapArg = arg; + mapArg.beginMap(); + while (!mapArg.atEnd()) { + mapArg.beginMapEntry(); + QString key; + QDBusVariant value; + mapArg >> key >> value; + mapArg.endMapEntry(); + if (!key.isEmpty()) { + converted.insert(key, value.variant()); + } + } + mapArg.endMap(); + } + if (!converted.isEmpty()) { + return converted; } } return QVariantMap{}; @@ -1238,10 +1332,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q matcher = [appId]() -> QList { // Primary: look for SteamAppId= in environ - QList results = ProcessScanner::findByEnvironment(QStringLiteral("SteamAppId"), appId); - if (results.isEmpty()) { - results = ProcessScanner::findByEnvironment(QStringLiteral("SteamGameId"), appId); - } + QList results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId); // Filter out the steam client itself QList filtered; for (const auto &m : std::as_const(results)) { @@ -1330,6 +1421,34 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q } session.process = bootstrap; + + const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId)); + const QString description = session.gameId.isEmpty() ? QStringLiteral("A-La-Karte game") : QStringLiteral("A-La-Karte game %1").arg(session.gameId); + + const QDBusReply scopeReply = m_systemd.startTransientScope(unitName, {static_cast(bootstrap->processId())}, description); + if (!scopeReply.isValid()) { + const QVariantMap ctx = { + {QStringLiteral("gameId"), gameId}, + {QStringLiteral("provider"), provider}, + {QStringLiteral("command"), command}, + {QStringLiteral("unit"), unitName}, + {QStringLiteral("error"), scopeReply.error().message()}, + }; + bootstrap->kill(); + bootstrap->deleteLater(); + QVariantMap finalState = sessionToVariantMap(session, QStringLiteral("Failed")); + Q_EMIT SessionRemoved(sessionId, finalState); + m_sessions.remove(sessionId); + failLaunch(QStringLiteral("failed to create transient scope"), ctx); + return {}; + } + + session.unitName = unitName; + const QDBusReply getUnitReply = m_systemd.getUnit(unitName); + if (getUnitReply.isValid()) { + session.unitPath = getUnitReply.value(); + } + m_sessions[sessionId] = session; connect(bootstrap, qOverload(&QProcess::finished), this, [this, sessionId](int, QProcess::ExitStatus) { @@ -1373,6 +1492,11 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q QVariantMap finalState = sessionToVariantMap(it.value(), stopping ? QStringLiteral("Stopped") : QStringLiteral("Failed")); + if (!it.value().unitName.isEmpty()) { + m_systemd.stopUnit(it.value().unitName); + unwatchSystemdUnit(it.value().unitName, it.value().unitPath); + } + if (it.value().scanner) { it.value().scanner->cancel(); it.value().scanner->deleteLater(); @@ -1408,27 +1532,49 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList it.value().mainPid = pids.first(); - const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId)); + const QString unitName = it.value().unitName.isEmpty() ? ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId)) : it.value().unitName; const QString description = it.value().gameId.isEmpty() ? QStringLiteral("A-La-Karte game") : QStringLiteral("A-La-Karte game %1").arg(it.value().gameId); - const QDBusReply reply = m_systemd.startTransientScope(unitName, pids, description); - if (!reply.isValid()) { - const QVariantMap ctx = { - {QStringLiteral("gameId"), it.value().gameId}, - {QStringLiteral("provider"), it.value().provider}, - {QStringLiteral("unit"), unitName}, - {QStringLiteral("error"), reply.error().message()}, - }; + QDBusReply attachReply = m_systemd.attachProcessesToUnit(unitName, pids); + if (!attachReply.isValid()) { + if (attachReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) { + const QDBusReply startReply = m_systemd.startTransientScope(unitName, pids, description); + if (!startReply.isValid()) { + const QVariantMap ctx = { + {QStringLiteral("gameId"), it.value().gameId}, + {QStringLiteral("provider"), it.value().provider}, + {QStringLiteral("unit"), unitName}, + {QStringLiteral("error"), startReply.error().message()}, + }; - QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed")); - if (it.value().process) { - it.value().process->deleteLater(); + QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed")); + if (it.value().process) { + it.value().process->deleteLater(); + } + Q_EMIT SessionRemoved(sessionId, finalState); + m_sessions.erase(it); + + failLaunch(QStringLiteral("failed to create transient scope for monitored game"), ctx); + return; + } + } else { + const QVariantMap ctx = { + {QStringLiteral("gameId"), it.value().gameId}, + {QStringLiteral("provider"), it.value().provider}, + {QStringLiteral("unit"), unitName}, + {QStringLiteral("error"), attachReply.error().message()}, + }; + + QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed")); + if (it.value().process) { + it.value().process->deleteLater(); + } + Q_EMIT SessionRemoved(sessionId, finalState); + m_sessions.erase(it); + + failLaunch(QStringLiteral("failed to attach monitored game to scope"), ctx); + return; } - Q_EMIT SessionRemoved(sessionId, finalState); - m_sessions.erase(it); - - failLaunch(QStringLiteral("failed to create transient scope for monitored game"), ctx); - return; } it.value().unitName = unitName; @@ -1446,11 +1592,25 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList } if (it.value().stopping) { - m_systemd.stopUnit(it.value().unitName); - QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped")); - Q_EMIT SessionRemoved(sessionId, finalState); + const QDBusReply stopReply = m_systemd.stopUnit(it.value().unitName); + if (stopReply.isValid()) { + Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping"))); + return; + } + + if (stopReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) { + unwatchSystemdUnit(unitName, it.value().unitPath); + removeSessionInternal(sessionId, QStringLiteral("Stopped")); + return; + } + + terminatePids(pids); + QTimer::singleShot(5000, this, [pids]() { + killPids(pids); + }); + unwatchSystemdUnit(unitName, it.value().unitPath); - m_sessions.erase(it); + removeSessionInternal(sessionId, QStringLiteral("Stopped")); return; } @@ -1534,12 +1694,6 @@ void GameCenterDaemon::Stop(const QString &sessionId) if (it.value().scanner) { Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping"))); - if (it.value().scanner) { - it.value().scanner->cancel(); - it.value().scanner->deleteLater(); - it.value().scanner = nullptr; - } - QPointer proc = it.value().process; it.value().process = nullptr; if (proc) { @@ -1556,9 +1710,10 @@ void GameCenterDaemon::Stop(const QString &sessionId) } } - QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped")); - Q_EMIT SessionRemoved(sessionId, finalState); - m_sessions.erase(it); + if (!it.value().unitName.isEmpty()) { + m_systemd.stopUnit(it.value().unitName); + } + return; } @@ -1575,6 +1730,12 @@ void GameCenterDaemon::Stop(const QString &sessionId) const QDBusReply reply = m_systemd.stopUnit(it.value().unitName); if (!reply.isValid()) { + if (reply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) { + unwatchSystemdUnit(it.value().unitName, it.value().unitPath); + removeSessionInternal(sessionId, QStringLiteral("Stopped")); + return; + } + if (it.value().process && it.value().process->state() != QProcess::NotRunning) { it.value().process->terminate(); QTimer::singleShot(5000, it.value().process, [process = it.value().process]() { @@ -1617,12 +1778,12 @@ QString GameCenterDaemon::sessionState(const Session &session) if (session.stopping) { return QStringLiteral("Stopping"); } - if (!session.unitName.isEmpty()) { - return QStringLiteral("Running"); - } if (session.scanner) { return QStringLiteral("Launching"); } + if (!session.unitName.isEmpty()) { + return QStringLiteral("Running"); + } if (session.process) { return session.process->state() == QProcess::Running ? QStringLiteral("Running") : QStringLiteral("Exited"); } diff --git a/src/gamecenter/gamecenterdaemon.h b/src/gamecenter/gamecenterdaemon.h index 4ddf944..0dd493f 100644 --- a/src/gamecenter/gamecenterdaemon.h +++ b/src/gamecenter/gamecenterdaemon.h @@ -92,23 +92,8 @@ private: SystemdUserManager m_systemd; QHash m_sessions; int m_maxConcurrent = 0; + QString m_powerProfile; QHash m_unitPathToSessionId; QHash m_unitNameToSessionId; QHash m_unitPathWatchers; }; - -class GameCenterSystemProxyPrivate; - -class GameCenterSystemProxy : public QObject -{ - Q_OBJECT - -public: - explicit GameCenterSystemProxy(QObject *parent = nullptr); - ~GameCenterSystemProxy() override; - - bool init(); - -private: - std::unique_ptr d; -}; diff --git a/src/gamecenter/main.cpp b/src/gamecenter/main.cpp index d307dc2..717bd6f 100644 --- a/src/gamecenter/main.cpp +++ b/src/gamecenter/main.cpp @@ -5,31 +5,37 @@ #include #include +#include +#include + #include "gamecenterdaemon.h" +#include + int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); + app.setOrganizationDomain(QStringLiteral("kde.org")); + app.setApplicationName(QStringLiteral("alakarte_gamecenter")); + + KDBusService service(KDBusService::Unique); QCommandLineParser parser; parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center")); parser.addHelpOption(); - parser.addOption(QCommandLineOption(QStringLiteral("system"), QStringLiteral("Run as system bus proxy"))); parser.process(app); - if (parser.isSet(QStringLiteral("system"))) { - GameCenterSystemProxy proxy; - if (!proxy.init()) { - return 1; - } - - return app.exec(); - } - GameCenterDaemon daemon; if (!daemon.init()) { return 1; } + KSignalHandler::self()->watchSignal(SIGTERM); + KSignalHandler::self()->watchSignal(SIGINT); + QObject::connect(KSignalHandler::self(), &KSignalHandler::signalReceived, &app, [&app](int signal) { + Q_UNUSED(signal); + app.quit(); + }); + return app.exec(); } diff --git a/src/gamecenter/processscanner.cpp b/src/gamecenter/processscanner.cpp index 4f9d2d2..e039c4c 100644 --- a/src/gamecenter/processscanner.cpp +++ b/src/gamecenter/processscanner.cpp @@ -6,6 +6,7 @@ #include #include #include +#include ProcessScanner::ProcessScanner(QObject *parent) : QObject(parent) @@ -15,21 +16,61 @@ ProcessScanner::ProcessScanner(QObject *parent) connect(&m_deadline, &QTimer::timeout, this, [this]() { m_timer.stop(); + m_matcher = nullptr; + ++m_generation; + m_scanInFlight = false; Q_EMIT timedOut(); }); connect(&m_timer, &QTimer::timeout, this, [this]() { - if (!m_matcher) { - m_timer.stop(); + startScan(); + }); +} + +void ProcessScanner::startScan() +{ + if (!m_matcher) { + m_timer.stop(); + return; + } + + if (m_scanInFlight && m_scanGeneration == m_generation) { + return; + } + + const quint64 gen = m_generation; + m_scanInFlight = true; + m_scanGeneration = gen; + + const std::function()> matcher = m_matcher; + auto *watcher = new QFutureWatcher>(this); + + connect(watcher, &QFutureWatcher>::finished, this, [this, watcher, gen]() { + const QList results = watcher->result(); + watcher->deleteLater(); + + if (m_scanGeneration == gen) { + m_scanInFlight = false; + } + + if (gen != m_generation || !m_matcher) { return; } - const QList results = m_matcher(); + if (!results.isEmpty()) { m_timer.stop(); m_deadline.stop(); + m_matcher = nullptr; Q_EMIT found(results); } }); + + watcher->setFuture(QtConcurrent::run([matcher]() { + if (!matcher) { + return QList{}; + } + return matcher(); + })); } static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536) @@ -56,6 +97,28 @@ static QByteArray readEnviron(const QString &pidDir) return readProcFile(pidDir + QStringLiteral("/environ")); } +static bool containsNullSeparatedEntry(const QByteArray &blob, const QByteArray &needle) +{ + if (blob.isEmpty() || needle.isEmpty()) { + return false; + } + + int pos = -1; + while (true) { + pos = blob.indexOf(needle, pos + 1); + if (pos < 0) { + return false; + } + + const bool startOk = (pos == 0) || (blob.at(pos - 1) == '\0'); + const int endPos = pos + needle.size(); + const bool endOk = (endPos == blob.size()) || (blob.at(endPos) == '\0'); + if (startOk && endOk) { + return true; + } + } +} + static QList listPids() { QList pids; @@ -84,22 +147,7 @@ QList ProcessScanner::findByEnvironment(const QString &ke continue; } - // environ entries are separated by null bytes - bool found = false; - int start = 0; - while (start < env.size()) { - int end = env.indexOf('\0', start); - if (end < 0) { - end = env.size(); - } - if (env.mid(start, end - start) == needle) { - found = true; - break; - } - start = end + 1; - } - - if (found) { + if (containsNullSeparatedEntry(env, needle)) { Match m; m.pid = pid; m.exe = readExeLink(pidDir); @@ -111,6 +159,54 @@ QList ProcessScanner::findByEnvironment(const QString &ke return matches; } +QList ProcessScanner::findByAnyEnvironment(const QStringList &keys, const QString &value) +{ + QList matches; + if (keys.isEmpty()) { + return matches; + } + + QList needles; + needles.reserve(keys.size()); + for (const QString &key : keys) { + if (!key.isEmpty()) { + needles.push_back((key + QLatin1Char('=') + value).toUtf8()); + } + } + if (needles.isEmpty()) { + return matches; + } + + const QList pids = listPids(); + for (uint pid : pids) { + const QString pidDir = QStringLiteral("/proc/%1").arg(pid); + const QByteArray env = readEnviron(pidDir); + if (env.isEmpty()) { + continue; + } + + bool found = false; + for (const QByteArray &needle : needles) { + if (containsNullSeparatedEntry(env, needle)) { + found = true; + break; + } + } + if (!found) { + continue; + } + + Match m; + m.pid = pid; + m.exe = readExeLink(pidDir); + const QByteArray cmd = readCmdline(pidDir); + m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed(); + matches.push_back(m); + } + + return matches; +} + QList ProcessScanner::findByCmdline(const QString &substring) { QList matches; @@ -164,15 +260,10 @@ void ProcessScanner::pollUntilFound(std::function()> matcher, int i cancel(); m_matcher = std::move(matcher); - // Try once immediately - const QList immediate = m_matcher(); - if (!immediate.isEmpty()) { - Q_EMIT found(immediate); - return; - } - m_deadline.start(timeoutMs); m_timer.start(intervalMs); + + startScan(); } void ProcessScanner::cancel() @@ -180,4 +271,6 @@ void ProcessScanner::cancel() m_timer.stop(); m_deadline.stop(); m_matcher = nullptr; + ++m_generation; + m_scanInFlight = false; } diff --git a/src/gamecenter/processscanner.h b/src/gamecenter/processscanner.h index e5b94d3..2f57b2b 100644 --- a/src/gamecenter/processscanner.h +++ b/src/gamecenter/processscanner.h @@ -3,10 +3,12 @@ #pragma once +#include #include #include #include #include +#include #include class ProcessScanner : public QObject @@ -25,6 +27,9 @@ public: // Find PIDs whose /proc//environ contains key=value static QList findByEnvironment(const QString &key, const QString &value); + // Find PIDs whose /proc//environ contains any of key=value for the provided keys + static QList findByAnyEnvironment(const QStringList &keys, const QString &value); + // Find PIDs whose /proc//cmdline contains the substring static QList findByCmdline(const QString &substring); @@ -42,7 +47,12 @@ Q_SIGNALS: void timedOut(); private: + void startScan(); + QTimer m_timer; QTimer m_deadline; std::function()> m_matcher; + quint64 m_generation = 0; + bool m_scanInFlight = false; + quint64 m_scanGeneration = 0; }; diff --git a/src/gamecenter/systemdusermanager.cpp b/src/gamecenter/systemdusermanager.cpp index ce3846e..847166e 100644 --- a/src/gamecenter/systemdusermanager.cpp +++ b/src/gamecenter/systemdusermanager.cpp @@ -130,6 +130,17 @@ QDBusReply SystemdUserManager::stopUnit(const QString &unitName return QDBusReply(reply); } +QDBusReply SystemdUserManager::attachProcessesToUnit(const QString &unitName, const QList &pids, const QString &subcgroup) +{ + QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"), + QStringLiteral("/org/freedesktop/systemd1"), + QStringLiteral("org.freedesktop.systemd1.Manager"), + QDBusConnection::sessionBus()); + + const QDBusMessage reply = manager.call(QStringLiteral("AttachProcessesToUnit"), unitName, subcgroup, QVariant::fromValue(pids)); + return QDBusReply(reply); +} + QDBusReply SystemdUserManager::getUnit(const QString &unitName) { QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"), diff --git a/src/gamecenter/systemdusermanager.h b/src/gamecenter/systemdusermanager.h index 62318eb..5f153bb 100644 --- a/src/gamecenter/systemdusermanager.h +++ b/src/gamecenter/systemdusermanager.h @@ -59,6 +59,8 @@ public: QDBusReply stopUnit(const QString &unitName, const QString &mode = QStringLiteral("replace")); + QDBusReply attachProcessesToUnit(const QString &unitName, const QList &pids, const QString &subcgroup = QString()); + QDBusReply getUnit(const QString &unitName); QDBusReply listUnits();