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 target_link_libraries(alakarte_gamecenter PRIVATE
Qt6::Core Qt6::Core
Qt6::Concurrent
Qt6::DBus Qt6::DBus
KF6::CoreAddons
KF6::DBusAddons
) )
set_target_properties(alakarte_gamecenter PROPERTIES 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}/dbus")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd") 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 configure_file(dbus/org.kde.GameCenter1.service.in
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service" "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
@ -38,21 +38,6 @@ configure_file(systemd/org.kde.GameCenter1.service.in
@ONLY @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" install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR} 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" install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service"
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR} 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 <QSet>
#include <signal.h>
#include <sys/types.h>
#ifdef ALAKARTE_HAVE_KAUTH
#include <KAuth/Action>
#include <KAuth/ExecuteJob>
#endif
#include <utility> #include <utility>
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1"); static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
@ -43,6 +51,26 @@ static QString ensureScopeUnitName(const QString &unitName)
namespace 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) static QVariant unwrapDbusVariant(QVariant v)
{ {
if (v.canConvert<QDBusVariant>()) { if (v.canConvert<QDBusVariant>()) {
@ -236,6 +264,8 @@ private:
} // namespace } // namespace
#if 0
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1"); static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1"); static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1"); static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
@ -675,6 +705,8 @@ bool GameCenterSystemProxy::init()
return d->init(); return d->init();
} }
#endif
void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath) void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath)
{ {
if (unitName.isEmpty()) { if (unitName.isEmpty()) {
@ -910,6 +942,18 @@ QVariantMap GameCenterDaemon::GetCapabilities() const
QVariantMap caps; QVariantMap caps;
caps.insert(QStringLiteral("supportsSystemd"), m_systemd.isAvailable()); caps.insert(QStringLiteral("supportsSystemd"), m_systemd.isAvailable());
caps.insert(QStringLiteral("supportsSystemBus"), false); 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 systemdAvailable = m_systemd.isAvailable();
const bool steamAvailable = !QStandardPaths::findExecutable(QStringLiteral("steam")).isEmpty(); const bool steamAvailable = !QStandardPaths::findExecutable(QStringLiteral("steam")).isEmpty();
const bool lutrisAvailable = !QStandardPaths::findExecutable(QStringLiteral("lutris")).isEmpty(); const bool lutrisAvailable = !QStandardPaths::findExecutable(QStringLiteral("lutris")).isEmpty();
@ -948,12 +992,44 @@ void GameCenterDaemon::SetPolicy(const QVariantMap &policy)
} }
m_maxConcurrent = v.toInt(); 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 QVariantMap GameCenterDaemon::GetPolicy() const
{ {
return { return {
{QStringLiteral("maxConcurrent"), m_maxConcurrent}, {QStringLiteral("maxConcurrent"), m_maxConcurrent},
{QStringLiteral("powerProfile"), m_powerProfile},
}; };
} }
@ -1020,7 +1096,10 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
v = v.value<QDBusVariant>().variant(); v = v.value<QDBusVariant>().variant();
} }
if (v.canConvert<QVariantMap>()) { if (v.canConvert<QVariantMap>()) {
return v.toMap(); const QVariantMap asMap = v.toMap();
if (!asMap.isEmpty()) {
return asMap;
}
} }
if (v.canConvert<QDBusArgument>()) { if (v.canConvert<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>(); const QDBusArgument arg = v.value<QDBusArgument>();
@ -1033,9 +1112,24 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
return converted; return converted;
} }
QVariantMap asMap = qdbus_cast<QVariantMap>(arg); QVariantMap converted;
if (!asMap.isEmpty()) { {
return asMap; 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{}; return QVariantMap{};
@ -1238,10 +1332,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
matcher = [appId]() -> QList<ProcessScanner::Match> { matcher = [appId]() -> QList<ProcessScanner::Match> {
// Primary: look for SteamAppId=<appId> in environ // Primary: look for SteamAppId=<appId> in environ
QList<ProcessScanner::Match> results = ProcessScanner::findByEnvironment(QStringLiteral("SteamAppId"), appId); QList<ProcessScanner::Match> results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId);
if (results.isEmpty()) {
results = ProcessScanner::findByEnvironment(QStringLiteral("SteamGameId"), appId);
}
// Filter out the steam client itself // Filter out the steam client itself
QList<ProcessScanner::Match> filtered; QList<ProcessScanner::Match> filtered;
for (const auto &m : std::as_const(results)) { for (const auto &m : std::as_const(results)) {
@ -1330,6 +1421,34 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
} }
session.process = bootstrap; 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; m_sessions[sessionId] = session;
connect(bootstrap, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this, sessionId](int, QProcess::ExitStatus) { 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")); 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) { if (it.value().scanner) {
it.value().scanner->cancel(); it.value().scanner->cancel();
it.value().scanner->deleteLater(); it.value().scanner->deleteLater();
@ -1408,16 +1532,19 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
it.value().mainPid = pids.first(); 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 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); QDBusReply<void> attachReply = m_systemd.attachProcessesToUnit(unitName, pids);
if (!reply.isValid()) { 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 = { const QVariantMap ctx = {
{QStringLiteral("gameId"), it.value().gameId}, {QStringLiteral("gameId"), it.value().gameId},
{QStringLiteral("provider"), it.value().provider}, {QStringLiteral("provider"), it.value().provider},
{QStringLiteral("unit"), unitName}, {QStringLiteral("unit"), unitName},
{QStringLiteral("error"), reply.error().message()}, {QStringLiteral("error"), startReply.error().message()},
}; };
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed")); QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed"));
@ -1430,6 +1557,25 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
failLaunch(QStringLiteral("failed to create transient scope for monitored game"), ctx); failLaunch(QStringLiteral("failed to create transient scope for monitored game"), ctx);
return; 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;
}
}
it.value().unitName = unitName; it.value().unitName = unitName;
@ -1446,11 +1592,25 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
} }
if (it.value().stopping) { if (it.value().stopping) {
m_systemd.stopUnit(it.value().unitName); const QDBusReply<QDBusObjectPath> stopReply = m_systemd.stopUnit(it.value().unitName);
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped")); if (stopReply.isValid()) {
Q_EMIT SessionRemoved(sessionId, finalState); Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping")));
return;
}
if (stopReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
unwatchSystemdUnit(unitName, it.value().unitPath); unwatchSystemdUnit(unitName, it.value().unitPath);
m_sessions.erase(it); removeSessionInternal(sessionId, QStringLiteral("Stopped"));
return;
}
terminatePids(pids);
QTimer::singleShot(5000, this, [pids]() {
killPids(pids);
});
unwatchSystemdUnit(unitName, it.value().unitPath);
removeSessionInternal(sessionId, QStringLiteral("Stopped"));
return; return;
} }
@ -1534,12 +1694,6 @@ void GameCenterDaemon::Stop(const QString &sessionId)
if (it.value().scanner) { if (it.value().scanner) {
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping"))); 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; QPointer<QProcess> proc = it.value().process;
it.value().process = nullptr; it.value().process = nullptr;
if (proc) { if (proc) {
@ -1556,9 +1710,10 @@ void GameCenterDaemon::Stop(const QString &sessionId)
} }
} }
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped")); if (!it.value().unitName.isEmpty()) {
Q_EMIT SessionRemoved(sessionId, finalState); m_systemd.stopUnit(it.value().unitName);
m_sessions.erase(it); }
return; return;
} }
@ -1575,6 +1730,12 @@ void GameCenterDaemon::Stop(const QString &sessionId)
const QDBusReply<QDBusObjectPath> reply = m_systemd.stopUnit(it.value().unitName); const QDBusReply<QDBusObjectPath> reply = m_systemd.stopUnit(it.value().unitName);
if (!reply.isValid()) { 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) { if (it.value().process && it.value().process->state() != QProcess::NotRunning) {
it.value().process->terminate(); it.value().process->terminate();
QTimer::singleShot(5000, it.value().process, [process = it.value().process]() { QTimer::singleShot(5000, it.value().process, [process = it.value().process]() {
@ -1617,12 +1778,12 @@ QString GameCenterDaemon::sessionState(const Session &session)
if (session.stopping) { if (session.stopping) {
return QStringLiteral("Stopping"); return QStringLiteral("Stopping");
} }
if (!session.unitName.isEmpty()) {
return QStringLiteral("Running");
}
if (session.scanner) { if (session.scanner) {
return QStringLiteral("Launching"); return QStringLiteral("Launching");
} }
if (!session.unitName.isEmpty()) {
return QStringLiteral("Running");
}
if (session.process) { if (session.process) {
return session.process->state() == QProcess::Running ? QStringLiteral("Running") : QStringLiteral("Exited"); return session.process->state() == QProcess::Running ? QStringLiteral("Running") : QStringLiteral("Exited");
} }

View file

@ -92,23 +92,8 @@ private:
SystemdUserManager m_systemd; SystemdUserManager m_systemd;
QHash<QString, Session> m_sessions; QHash<QString, Session> m_sessions;
int m_maxConcurrent = 0; int m_maxConcurrent = 0;
QString m_powerProfile;
QHash<QString, QString> m_unitPathToSessionId; QHash<QString, QString> m_unitPathToSessionId;
QHash<QString, QString> m_unitNameToSessionId; QHash<QString, QString> m_unitNameToSessionId;
QHash<QString, QObject *> m_unitPathWatchers; 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 <QCommandLineParser>
#include <QCoreApplication> #include <QCoreApplication>
#include <KDBusService>
#include <KSignalHandler>
#include "gamecenterdaemon.h" #include "gamecenterdaemon.h"
#include <signal.h>
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
QCoreApplication app(argc, argv); QCoreApplication app(argc, argv);
app.setOrganizationDomain(QStringLiteral("kde.org"));
app.setApplicationName(QStringLiteral("alakarte_gamecenter"));
KDBusService service(KDBusService::Unique);
QCommandLineParser parser; QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center")); parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center"));
parser.addHelpOption(); parser.addHelpOption();
parser.addOption(QCommandLineOption(QStringLiteral("system"), QStringLiteral("Run as system bus proxy")));
parser.process(app); parser.process(app);
if (parser.isSet(QStringLiteral("system"))) {
GameCenterSystemProxy proxy;
if (!proxy.init()) {
return 1;
}
return app.exec();
}
GameCenterDaemon daemon; GameCenterDaemon daemon;
if (!daemon.init()) { if (!daemon.init()) {
return 1; 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(); return app.exec();
} }

View file

@ -6,6 +6,7 @@
#include <QDir> #include <QDir>
#include <QFile> #include <QFile>
#include <QFileInfo> #include <QFileInfo>
#include <QtConcurrentRun>
ProcessScanner::ProcessScanner(QObject *parent) ProcessScanner::ProcessScanner(QObject *parent)
: QObject(parent) : QObject(parent)
@ -15,21 +16,61 @@ ProcessScanner::ProcessScanner(QObject *parent)
connect(&m_deadline, &QTimer::timeout, this, [this]() { connect(&m_deadline, &QTimer::timeout, this, [this]() {
m_timer.stop(); m_timer.stop();
m_matcher = nullptr;
++m_generation;
m_scanInFlight = false;
Q_EMIT timedOut(); Q_EMIT timedOut();
}); });
connect(&m_timer, &QTimer::timeout, this, [this]() { connect(&m_timer, &QTimer::timeout, this, [this]() {
startScan();
});
}
void ProcessScanner::startScan()
{
if (!m_matcher) { if (!m_matcher) {
m_timer.stop(); m_timer.stop();
return; return;
} }
const QList<Match> results = m_matcher();
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;
}
if (!results.isEmpty()) { if (!results.isEmpty()) {
m_timer.stop(); m_timer.stop();
m_deadline.stop(); m_deadline.stop();
m_matcher = nullptr;
Q_EMIT found(results); 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) static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
@ -56,6 +97,28 @@ static QByteArray readEnviron(const QString &pidDir)
return readProcFile(pidDir + QStringLiteral("/environ")); 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() static QList<uint> listPids()
{ {
QList<uint> pids; QList<uint> pids;
@ -84,22 +147,7 @@ QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &ke
continue; continue;
} }
// environ entries are separated by null bytes if (containsNullSeparatedEntry(env, needle)) {
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) {
Match m; Match m;
m.pid = pid; m.pid = pid;
m.exe = readExeLink(pidDir); m.exe = readExeLink(pidDir);
@ -111,6 +159,54 @@ QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &ke
return matches; 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<ProcessScanner::Match> ProcessScanner::findByCmdline(const QString &substring)
{ {
QList<Match> matches; QList<Match> matches;
@ -164,15 +260,10 @@ void ProcessScanner::pollUntilFound(std::function<QList<Match>()> matcher, int i
cancel(); cancel();
m_matcher = std::move(matcher); 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_deadline.start(timeoutMs);
m_timer.start(intervalMs); m_timer.start(intervalMs);
startScan();
} }
void ProcessScanner::cancel() void ProcessScanner::cancel()
@ -180,4 +271,6 @@ void ProcessScanner::cancel()
m_timer.stop(); m_timer.stop();
m_deadline.stop(); m_deadline.stop();
m_matcher = nullptr; m_matcher = nullptr;
++m_generation;
m_scanInFlight = false;
} }

View file

@ -3,10 +3,12 @@
#pragma once #pragma once
#include <QFutureWatcher>
#include <QList> #include <QList>
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QTimer> #include <QTimer>
#include <QtGlobal>
#include <functional> #include <functional>
class ProcessScanner : public QObject class ProcessScanner : public QObject
@ -25,6 +27,9 @@ public:
// Find PIDs whose /proc/<pid>/environ contains key=value // Find PIDs whose /proc/<pid>/environ contains key=value
static QList<Match> findByEnvironment(const QString &key, const QString &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 // Find PIDs whose /proc/<pid>/cmdline contains the substring
static QList<Match> findByCmdline(const QString &substring); static QList<Match> findByCmdline(const QString &substring);
@ -42,7 +47,12 @@ Q_SIGNALS:
void timedOut(); void timedOut();
private: private:
void startScan();
QTimer m_timer; QTimer m_timer;
QTimer m_deadline; QTimer m_deadline;
std::function<QList<Match>()> m_matcher; 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); 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) QDBusReply<QDBusObjectPath> SystemdUserManager::getUnit(const QString &unitName)
{ {
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"), 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<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<QDBusObjectPath> getUnit(const QString &unitName);
QDBusReply<SystemdUnitInfoList> listUnits(); QDBusReply<SystemdUnitInfoList> listUnits();