From f03eb95b52836bb3819fc545258512231820bfbc Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Sat, 14 Feb 2026 17:38:28 +0100 Subject: [PATCH] gamecenter: refactor ProcessScanner for cancellable single-pass scans Add ProcessScanner::scan(ScanOptions) for combined env/cmdline/exe matching, extend Match with match-source flags, and make pollUntilFound() cancellation-aware. Update monitored launch matchers to use the new API. --- src/gamecenter/gamecenterdaemon.cpp | 37 +++- src/gamecenter/processscanner.cpp | 285 +++++++++++++++++----------- src/gamecenter/processscanner.h | 25 ++- 3 files changed, 225 insertions(+), 122 deletions(-) diff --git a/src/gamecenter/gamecenterdaemon.cpp b/src/gamecenter/gamecenterdaemon.cpp index 5f688d5..8145a85 100644 --- a/src/gamecenter/gamecenterdaemon.cpp +++ b/src/gamecenter/gamecenterdaemon.cpp @@ -1402,7 +1402,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q // Determine bootstrap command and scanner matcher QString bootstrapProgram; QStringList bootstrapArgs; - std::function()> matcher; + ProcessScanner::Matcher matcher; if (provider == QLatin1String("steam")) { const QString appId = extractSteamAppId(command); @@ -1414,9 +1414,13 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q bootstrapProgram = QStringLiteral("steam"); bootstrapArgs = {QStringLiteral("-silent"), QStringLiteral("-applaunch"), appId}; - matcher = [appId]() -> QList { - // Primary: look for SteamAppId= in environ - QList results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId); + matcher = [appId](const std::atomic_bool &cancelled) -> QList { + ProcessScanner::ScanOptions opts; + opts.envKeys = {QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}; + opts.envValue = appId; + opts.preferEnvironmentMatches = true; + + QList results = ProcessScanner::scan(opts, cancelled); // Filter out the steam client itself QList filtered; for (const auto &m : std::as_const(results)) { @@ -1438,11 +1442,25 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q bootstrapArgs = {QStringLiteral("lutris:rungameid/%1").arg(lutrisId)}; // For Lutris, look for child processes with the game slug or lutris game env - matcher = [lutrisId]() -> QList { - QList results = ProcessScanner::findByEnvironment(QStringLiteral("LUTRIS_GAME_SLUG"), lutrisId); - if (results.isEmpty()) { - results = ProcessScanner::findByCmdline(lutrisId); - // Filter out lutris itself + matcher = [lutrisId](const std::atomic_bool &cancelled) -> QList { + ProcessScanner::ScanOptions opts; + opts.envKeys = {QStringLiteral("LUTRIS_GAME_SLUG")}; + opts.envValue = lutrisId; + opts.cmdlineSubstring = lutrisId; + opts.preferEnvironmentMatches = true; + + QList results = ProcessScanner::scan(opts, cancelled); + + bool anyEnvMatch = false; + for (const auto &m : std::as_const(results)) { + if (m.envMatched) { + anyEnvMatch = true; + break; + } + } + + if (!anyEnvMatch) { + // Filter out lutris itself (only for cmdline fallback) QList filtered; for (const auto &m : std::as_const(results)) { if (!m.exe.endsWith(QLatin1String("/lutris")) && !m.exe.endsWith(QLatin1String("/python3"))) { @@ -1453,6 +1471,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q results = filtered; } } + return results; }; } diff --git a/src/gamecenter/processscanner.cpp b/src/gamecenter/processscanner.cpp index e039c4c..a1be4a6 100644 --- a/src/gamecenter/processscanner.cpp +++ b/src/gamecenter/processscanner.cpp @@ -6,8 +6,19 @@ #include #include #include +#include #include +#include + +Q_LOGGING_CATEGORY(lcProcessScanner, "org.kde.alakarte.processscanner") + +static QString readExeLink(const QString &pidDir); +static QByteArray readCmdline(const QString &pidDir); +static QByteArray readEnviron(const QString &pidDir); +static bool containsNullSeparatedEntry(const QByteArray &blob, const QByteArray &needle); +static QList listPids(); + ProcessScanner::ProcessScanner(QObject *parent) : QObject(parent) { @@ -16,7 +27,11 @@ ProcessScanner::ProcessScanner(QObject *parent) connect(&m_deadline, &QTimer::timeout, this, [this]() { m_timer.stop(); - m_matcher = nullptr; + if (m_cancelToken) { + m_cancelToken->store(true); + } + m_cancelToken.reset(); + m_matcher = {}; ++m_generation; m_scanInFlight = false; Q_EMIT timedOut(); @@ -42,7 +57,8 @@ void ProcessScanner::startScan() m_scanInFlight = true; m_scanGeneration = gen; - const std::function()> matcher = m_matcher; + const Matcher matcher = m_matcher; + const std::shared_ptr cancelToken = m_cancelToken; auto *watcher = new QFutureWatcher>(this); connect(watcher, &QFutureWatcher>::finished, this, [this, watcher, gen]() { @@ -60,19 +76,130 @@ void ProcessScanner::startScan() if (!results.isEmpty()) { m_timer.stop(); m_deadline.stop(); - m_matcher = nullptr; + if (m_cancelToken) { + m_cancelToken->store(true); + } + m_cancelToken.reset(); + m_matcher = {}; Q_EMIT found(results); } }); - watcher->setFuture(QtConcurrent::run([matcher]() { + watcher->setFuture(QtConcurrent::run([matcher, cancelToken]() { + static const std::atomic_bool neverCancelled{false}; + const std::atomic_bool &cancelled = cancelToken ? *cancelToken : neverCancelled; if (!matcher) { return QList{}; } - return matcher(); + return matcher(cancelled); })); } +QList ProcessScanner::scan(const ScanOptions &options) +{ + static const std::atomic_bool neverCancelled{false}; + return scan(options, neverCancelled); +} + +QList ProcessScanner::scan(const ScanOptions &options, const std::atomic_bool &cancelled) +{ + QList matches; + + QList envNeedles; + if (!options.envKeys.isEmpty() && !options.envValue.isEmpty()) { + envNeedles.reserve(options.envKeys.size()); + for (const QString &key : options.envKeys) { + if (!key.isEmpty()) { + envNeedles.push_back((key + QLatin1Char('=') + options.envValue).toUtf8()); + } + } + } + + const QByteArray cmdNeedle = options.cmdlineSubstring.isEmpty() ? QByteArray{} : options.cmdlineSubstring.toUtf8(); + const bool checkExePrefix = !options.exePathPrefix.isEmpty(); + + const QList pids = listPids(); + for (uint pid : pids) { + if (cancelled.load()) { + break; + } + + const QString pidDir = QStringLiteral("/proc/%1").arg(pid); + + bool envMatched = false; + if (!envNeedles.isEmpty()) { + const QByteArray env = readEnviron(pidDir); + if (!env.isEmpty()) { + for (const QByteArray &needle : envNeedles) { + if (containsNullSeparatedEntry(env, needle)) { + envMatched = true; + break; + } + } + } + } + + bool cmdlineMatched = false; + QByteArray cmd; + if (!cmdNeedle.isEmpty()) { + cmd = readCmdline(pidDir); + cmdlineMatched = !cmd.isEmpty() && cmd.contains(cmdNeedle); + } + + bool exeMatched = false; + QString exe; + if (checkExePrefix) { + exe = readExeLink(pidDir); + exeMatched = !exe.isEmpty() && exe.startsWith(options.exePathPrefix); + } + + if (!envMatched && !cmdlineMatched && !exeMatched) { + continue; + } + + Match m; + m.pid = pid; + m.envMatched = envMatched; + m.cmdlineMatched = cmdlineMatched; + m.exeMatched = exeMatched; + + if (exe.isEmpty()) { + exe = readExeLink(pidDir); + } + m.exe = exe; + + if (cmd.isEmpty()) { + cmd = readCmdline(pidDir); + } + m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed(); + + matches.push_back(m); + } + + if (options.preferEnvironmentMatches) { + bool anyEnvMatch = false; + for (const Match &m : std::as_const(matches)) { + if (m.envMatched) { + anyEnvMatch = true; + break; + } + } + + if (anyEnvMatch) { + QList filtered; + filtered.reserve(matches.size()); + for (const Match &m : std::as_const(matches)) { + if (m.envMatched) { + filtered.push_back(m); + } + } + return filtered; + } + } + + return matches; +} + static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536) { QFile f(path); @@ -136,141 +263,75 @@ static QList listPids() QList ProcessScanner::findByEnvironment(const QString &key, const QString &value) { - QList matches; - const QByteArray needle = (key + QLatin1Char('=') + value).toUtf8(); - - 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; - } - - if (containsNullSeparatedEntry(env, needle)) { - 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; + ScanOptions opts; + opts.envKeys = {key}; + opts.envValue = value; + return scan(opts); } 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; + ScanOptions opts; + opts.envKeys = keys; + opts.envValue = value; + return scan(opts); } QList ProcessScanner::findByCmdline(const QString &substring) { - QList matches; - const QByteArray needle = substring.toUtf8(); - - const QList pids = listPids(); - for (uint pid : pids) { - const QString pidDir = QStringLiteral("/proc/%1").arg(pid); - const QByteArray cmd = readCmdline(pidDir); - if (cmd.isEmpty()) { - continue; - } - - // cmdline has null-separated args; search the whole blob - if (cmd.contains(needle)) { - Match m; - m.pid = pid; - m.exe = readExeLink(pidDir); - m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed(); - matches.push_back(m); - } - } - return matches; + ScanOptions opts; + opts.cmdlineSubstring = substring; + return scan(opts); } QList ProcessScanner::findByExePath(const QString &dirPrefix) { - QList matches; - - const QList pids = listPids(); - for (uint pid : pids) { - const QString pidDir = QStringLiteral("/proc/%1").arg(pid); - const QString exe = readExeLink(pidDir); - if (exe.isEmpty()) { - continue; - } - if (exe.startsWith(dirPrefix)) { - Match m; - m.pid = pid; - m.exe = exe; - const QByteArray cmd = readCmdline(pidDir); - m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed(); - matches.push_back(m); - } - } - return matches; + ScanOptions opts; + opts.exePathPrefix = dirPrefix; + return scan(opts); } -void ProcessScanner::pollUntilFound(std::function()> matcher, int intervalMs, int timeoutMs) +void ProcessScanner::pollUntilFound(Matcher matcher, int intervalMs, int timeoutMs) { cancel(); + m_cancelToken = std::make_shared(false); m_matcher = std::move(matcher); + if (!m_matcher) { + return; + } + m_deadline.start(timeoutMs); m_timer.start(intervalMs); startScan(); } +void ProcessScanner::pollUntilFound(std::function()> matcher, int intervalMs, int timeoutMs) +{ + pollUntilFound( + [matcher = std::move(matcher)](const std::atomic_bool &cancelled) { + if (cancelled.load()) { + return QList{}; + } + if (!matcher) { + return QList{}; + } + return matcher(); + }, + intervalMs, + timeoutMs); +} + void ProcessScanner::cancel() { m_timer.stop(); m_deadline.stop(); - m_matcher = nullptr; + if (m_cancelToken) { + m_cancelToken->store(true); + } + m_cancelToken.reset(); + m_matcher = {}; ++m_generation; m_scanInFlight = false; } diff --git a/src/gamecenter/processscanner.h b/src/gamecenter/processscanner.h index 2f57b2b..17240fc 100644 --- a/src/gamecenter/processscanner.h +++ b/src/gamecenter/processscanner.h @@ -5,11 +5,16 @@ #include #include +#include #include #include #include #include +#include #include +#include + +Q_DECLARE_LOGGING_CATEGORY(lcProcessScanner) class ProcessScanner : public QObject { @@ -18,12 +23,28 @@ class ProcessScanner : public QObject public: explicit ProcessScanner(QObject *parent = nullptr); + struct ScanOptions { + QStringList envKeys; + QString envValue; + QString cmdlineSubstring; + QString exePathPrefix; + bool preferEnvironmentMatches = false; + }; + struct Match { uint pid = 0; QString exe; QString cmdline; + bool envMatched = false; + bool cmdlineMatched = false; + bool exeMatched = false; }; + using Matcher = std::function(const std::atomic_bool &cancelled)>; + + static QList scan(const ScanOptions &options); + static QList scan(const ScanOptions &options, const std::atomic_bool &cancelled); + // Find PIDs whose /proc//environ contains key=value static QList findByEnvironment(const QString &key, const QString &value); @@ -38,6 +59,7 @@ public: // Async poll: calls matcher repeatedly until it returns non-empty or timeout. // Emits found() with matching PIDs, or timedOut() on failure. + void pollUntilFound(Matcher matcher, int intervalMs = 500, int timeoutMs = 15000); void pollUntilFound(std::function()> matcher, int intervalMs = 500, int timeoutMs = 15000); void cancel(); @@ -51,7 +73,8 @@ private: QTimer m_timer; QTimer m_deadline; - std::function()> m_matcher; + Matcher m_matcher; + std::shared_ptr m_cancelToken; quint64 m_generation = 0; bool m_scanInFlight = false; quint64 m_scanGeneration = 0;