gamecenter: systemd scopes + Stop reliability

This commit is contained in:
Marco Allegretti 2026-02-12 14:34:56 +01:00
parent c7956eed8b
commit fbd9311384
8 changed files with 364 additions and 123 deletions

View file

@ -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
)

View file

@ -25,6 +25,14 @@
#include <QSet>
#include <signal.h>
#include <sys/types.h>
#ifdef ALAKARTE_HAVE_KAUTH
#include <KAuth/Action>
#include <KAuth/ExecuteJob>
#endif
#include <utility>
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<uint> &pids)
{
for (uint pid : pids) {
if (pid == 0) {
continue;
}
::kill(static_cast<pid_t>(pid), SIGTERM);
}
}
static void killPids(const QList<uint> &pids)
{
for (uint pid : pids) {
if (pid == 0) {
continue;
}
::kill(static_cast<pid_t>(pid), SIGKILL);
}
}
static QVariant unwrapDbusVariant(QVariant v)
{
if (v.canConvert<QDBusVariant>()) {
@ -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<QDBusVariant>()) {
v = v.value<QDBusVariant>().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<QDBusVariant>().variant();
}
if (v.canConvert<QVariantMap>()) {
return v.toMap();
const QVariantMap asMap = v.toMap();
if (!asMap.isEmpty()) {
return asMap;
}
}
if (v.canConvert<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>();
@ -1033,9 +1112,24 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
return converted;
}
QVariantMap asMap = qdbus_cast<QVariantMap>(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<ProcessScanner::Match> {
// Primary: look for SteamAppId=<appId> in environ
QList<ProcessScanner::Match> results = ProcessScanner::findByEnvironment(QStringLiteral("SteamAppId"), appId);
if (results.isEmpty()) {
results = ProcessScanner::findByEnvironment(QStringLiteral("SteamGameId"), appId);
}
QList<ProcessScanner::Match> results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId);
// Filter out the steam client itself
QList<ProcessScanner::Match> 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<QDBusObjectPath> scopeReply = m_systemd.startTransientScope(unitName, {static_cast<uint>(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<QDBusObjectPath> getUnitReply = m_systemd.getUnit(unitName);
if (getUnitReply.isValid()) {
session.unitPath = getUnitReply.value();
}
m_sessions[sessionId] = session;
connect(bootstrap, qOverload<int, QProcess::ExitStatus>(&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<QDBusObjectPath> 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<void> attachReply = m_systemd.attachProcessesToUnit(unitName, pids);
if (!attachReply.isValid()) {
if (attachReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
const QDBusReply<QDBusObjectPath> 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<QDBusObjectPath> 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<QProcess> 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<QDBusObjectPath> 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");
}

View file

@ -92,23 +92,8 @@ private:
SystemdUserManager m_systemd;
QHash<QString, Session> m_sessions;
int m_maxConcurrent = 0;
QString m_powerProfile;
QHash<QString, QString> m_unitPathToSessionId;
QHash<QString, QString> m_unitNameToSessionId;
QHash<QString, QObject *> m_unitPathWatchers;
};
class GameCenterSystemProxyPrivate;
class GameCenterSystemProxy : public QObject
{
Q_OBJECT
public:
explicit GameCenterSystemProxy(QObject *parent = nullptr);
~GameCenterSystemProxy() override;
bool init();
private:
std::unique_ptr<GameCenterSystemProxyPrivate> d;
};

View file

@ -5,31 +5,37 @@
#include <QCommandLineParser>
#include <QCoreApplication>
#include <KDBusService>
#include <KSignalHandler>
#include "gamecenterdaemon.h"
#include <signal.h>
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();
}

View file

@ -6,6 +6,7 @@
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QtConcurrentRun>
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<QList<Match>()> matcher = m_matcher;
auto *watcher = new QFutureWatcher<QList<Match>>(this);
connect(watcher, &QFutureWatcher<QList<Match>>::finished, this, [this, watcher, gen]() {
const QList<Match> results = watcher->result();
watcher->deleteLater();
if (m_scanGeneration == gen) {
m_scanInFlight = false;
}
if (gen != m_generation || !m_matcher) {
return;
}
const QList<Match> 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<Match>{};
}
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<uint> listPids()
{
QList<uint> pids;
@ -84,22 +147,7 @@ QList<ProcessScanner::Match> 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::Match> ProcessScanner::findByEnvironment(const QString &ke
return matches;
}
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;
}
QList<ProcessScanner::Match> ProcessScanner::findByCmdline(const QString &substring)
{
QList<Match> matches;
@ -164,15 +260,10 @@ void ProcessScanner::pollUntilFound(std::function<QList<Match>()> matcher, int i
cancel();
m_matcher = std::move(matcher);
// Try once immediately
const QList<Match> 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;
}

View file

@ -3,10 +3,12 @@
#pragma once
#include <QFutureWatcher>
#include <QList>
#include <QObject>
#include <QString>
#include <QTimer>
#include <QtGlobal>
#include <functional>
class ProcessScanner : public QObject
@ -25,6 +27,9 @@ public:
// Find PIDs whose /proc/<pid>/environ contains key=value
static QList<Match> findByEnvironment(const QString &key, const QString &value);
// Find PIDs whose /proc/<pid>/environ contains any of key=value for the provided keys
static QList<Match> findByAnyEnvironment(const QStringList &keys, const QString &value);
// Find PIDs whose /proc/<pid>/cmdline contains the substring
static QList<Match> findByCmdline(const QString &substring);
@ -42,7 +47,12 @@ Q_SIGNALS:
void timedOut();
private:
void startScan();
QTimer m_timer;
QTimer m_deadline;
std::function<QList<Match>()> m_matcher;
quint64 m_generation = 0;
bool m_scanInFlight = false;
quint64 m_scanGeneration = 0;
};

View file

@ -130,6 +130,17 @@ QDBusReply<QDBusObjectPath> SystemdUserManager::stopUnit(const QString &unitName
return QDBusReply<QDBusObjectPath>(reply);
}
QDBusReply<void> SystemdUserManager::attachProcessesToUnit(const QString &unitName, const QList<uint> &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<void>(reply);
}
QDBusReply<QDBusObjectPath> SystemdUserManager::getUnit(const QString &unitName)
{
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),

View file

@ -59,6 +59,8 @@ public:
QDBusReply<QDBusObjectPath> stopUnit(const QString &unitName, const QString &mode = QStringLiteral("replace"));
QDBusReply<void> attachProcessesToUnit(const QString &unitName, const QList<uint> &pids, const QString &subcgroup = QString());
QDBusReply<QDBusObjectPath> getUnit(const QString &unitName);
QDBusReply<SystemdUnitInfoList> listUnits();