mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
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.
This commit is contained in:
parent
7200ad179c
commit
f03eb95b52
3 changed files with 225 additions and 122 deletions
|
|
@ -1402,7 +1402,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
|
|||
// Determine bootstrap command and scanner matcher
|
||||
QString bootstrapProgram;
|
||||
QStringList bootstrapArgs;
|
||||
std::function<QList<ProcessScanner::Match>()> 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<ProcessScanner::Match> {
|
||||
// Primary: look for SteamAppId=<appId> in environ
|
||||
QList<ProcessScanner::Match> results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId);
|
||||
matcher = [appId](const std::atomic_bool &cancelled) -> QList<ProcessScanner::Match> {
|
||||
ProcessScanner::ScanOptions opts;
|
||||
opts.envKeys = {QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")};
|
||||
opts.envValue = appId;
|
||||
opts.preferEnvironmentMatches = true;
|
||||
|
||||
QList<ProcessScanner::Match> results = ProcessScanner::scan(opts, cancelled);
|
||||
// Filter out the steam client itself
|
||||
QList<ProcessScanner::Match> 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<ProcessScanner::Match> {
|
||||
QList<ProcessScanner::Match> 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::Match> {
|
||||
ProcessScanner::ScanOptions opts;
|
||||
opts.envKeys = {QStringLiteral("LUTRIS_GAME_SLUG")};
|
||||
opts.envValue = lutrisId;
|
||||
opts.cmdlineSubstring = lutrisId;
|
||||
opts.preferEnvironmentMatches = true;
|
||||
|
||||
QList<ProcessScanner::Match> 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<ProcessScanner::Match> 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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,19 @@
|
|||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QLoggingCategory>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
#include <utility>
|
||||
|
||||
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<uint> 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<QList<Match>()> matcher = m_matcher;
|
||||
const Matcher matcher = m_matcher;
|
||||
const std::shared_ptr<std::atomic_bool> cancelToken = m_cancelToken;
|
||||
auto *watcher = new QFutureWatcher<QList<Match>>(this);
|
||||
|
||||
connect(watcher, &QFutureWatcher<QList<Match>>::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<Match>{};
|
||||
}
|
||||
return matcher();
|
||||
return matcher(cancelled);
|
||||
}));
|
||||
}
|
||||
|
||||
QList<ProcessScanner::Match> ProcessScanner::scan(const ScanOptions &options)
|
||||
{
|
||||
static const std::atomic_bool neverCancelled{false};
|
||||
return scan(options, neverCancelled);
|
||||
}
|
||||
|
||||
QList<ProcessScanner::Match> ProcessScanner::scan(const ScanOptions &options, const std::atomic_bool &cancelled)
|
||||
{
|
||||
QList<Match> matches;
|
||||
|
||||
QList<QByteArray> 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<uint> 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<Match> 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<uint> listPids()
|
|||
|
||||
QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &key, const QString &value)
|
||||
{
|
||||
QList<Match> matches;
|
||||
const QByteArray needle = (key + QLatin1Char('=') + value).toUtf8();
|
||||
|
||||
const QList<uint> 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::Match> ProcessScanner::findByAnyEnvironment(const QStringList &keys, const QString &value)
|
||||
{
|
||||
QList<Match> matches;
|
||||
if (keys.isEmpty()) {
|
||||
return matches;
|
||||
}
|
||||
|
||||
QList<QByteArray> 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<uint> 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::Match> ProcessScanner::findByCmdline(const QString &substring)
|
||||
{
|
||||
QList<Match> matches;
|
||||
const QByteArray needle = substring.toUtf8();
|
||||
|
||||
const QList<uint> 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::Match> ProcessScanner::findByExePath(const QString &dirPrefix)
|
||||
{
|
||||
QList<Match> matches;
|
||||
|
||||
const QList<uint> 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<QList<Match>()> matcher, int intervalMs, int timeoutMs)
|
||||
void ProcessScanner::pollUntilFound(Matcher matcher, int intervalMs, int timeoutMs)
|
||||
{
|
||||
cancel();
|
||||
m_cancelToken = std::make_shared<std::atomic_bool>(false);
|
||||
m_matcher = std::move(matcher);
|
||||
|
||||
if (!m_matcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_deadline.start(timeoutMs);
|
||||
m_timer.start(intervalMs);
|
||||
|
||||
startScan();
|
||||
}
|
||||
|
||||
void ProcessScanner::pollUntilFound(std::function<QList<Match>()> matcher, int intervalMs, int timeoutMs)
|
||||
{
|
||||
pollUntilFound(
|
||||
[matcher = std::move(matcher)](const std::atomic_bool &cancelled) {
|
||||
if (cancelled.load()) {
|
||||
return QList<Match>{};
|
||||
}
|
||||
if (!matcher) {
|
||||
return QList<Match>{};
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,16 @@
|
|||
|
||||
#include <QFutureWatcher>
|
||||
#include <QList>
|
||||
#include <QLoggingCategory>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QtGlobal>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
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<QList<Match>(const std::atomic_bool &cancelled)>;
|
||||
|
||||
static QList<Match> scan(const ScanOptions &options);
|
||||
static QList<Match> scan(const ScanOptions &options, const std::atomic_bool &cancelled);
|
||||
|
||||
// Find PIDs whose /proc/<pid>/environ contains key=value
|
||||
static QList<Match> 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<QList<Match>()> matcher, int intervalMs = 500, int timeoutMs = 15000);
|
||||
|
||||
void cancel();
|
||||
|
|
@ -51,7 +73,8 @@ private:
|
|||
|
||||
QTimer m_timer;
|
||||
QTimer m_deadline;
|
||||
std::function<QList<Match>()> m_matcher;
|
||||
Matcher m_matcher;
|
||||
std::shared_ptr<std::atomic_bool> m_cancelToken;
|
||||
quint64 m_generation = 0;
|
||||
bool m_scanInFlight = false;
|
||||
quint64 m_scanGeneration = 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue