Add Game Center daemon target

Build and install the Game Center daemon as a DBus-activated\nuser service. This introduces the new src/gamecenter target and\nwires it into the main build.
This commit is contained in:
Marco Allegretti 2026-02-06 14:01:35 +01:00
parent 55d5a70656
commit 017073c162
11 changed files with 1811 additions and 0 deletions

View file

@ -137,3 +137,5 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
) )
install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
add_subdirectory(gamecenter)

View file

@ -0,0 +1,44 @@
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
add_executable(alakarte_gamecenter
main.cpp
gamecenterdaemon.cpp
gamecenterdaemon.h
processscanner.cpp
processscanner.h
systemdusermanager.cpp
systemdusermanager.h
)
target_link_libraries(alakarte_gamecenter PRIVATE
Qt6::Core
Qt6::DBus
)
set_target_properties(alakarte_gamecenter PROPERTIES
OUTPUT_NAME "alakarte-gamecenter"
)
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")
configure_file(dbus/org.kde.GameCenter1.service.in
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
@ONLY
)
configure_file(systemd/org.kde.GameCenter1.service.in
"${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service"
@ONLY
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service"
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
)

View file

@ -0,0 +1,4 @@
[D-BUS Service]
Name=org.kde.GameCenter1
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-gamecenter
SystemdService=org.kde.GameCenter1.service

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#pragma once
#include <QDBusObjectPath>
#include <QDateTime>
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QProcess>
#include <QVariantMap>
#include "processscanner.h"
#include "systemdusermanager.h"
class GameCenterDaemon : public QObject
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.GameCenter1")
public:
explicit GameCenterDaemon(QObject *parent = nullptr);
bool init();
public Q_SLOTS:
QString Ping() const;
void Version(uint &major, uint &minor) const;
QVariantMap GetCapabilities() const;
QVariantList ListSessions() const;
QVariantMap GetSession(const QString &sessionId) const;
QString Launch(const QVariantMap &launchSpec);
void Stop(const QString &sessionId);
void StopByGameId(const QString &gameId);
void SetPolicy(const QVariantMap &policy);
QVariantMap GetPolicy() const;
Q_SIGNALS:
void SessionAdded(const QVariantMap &session);
void SessionChanged(const QVariantMap &session);
void SessionRemoved(const QString &sessionId, const QVariantMap &finalState);
void LaunchFailed(const QVariantMap &error);
private:
struct Session {
QString sessionId;
QString gameId;
QString displayName;
QString unitName;
QDBusObjectPath unitPath;
QString provider;
QDateTime startTime;
QPointer<QProcess> process;
QPointer<ProcessScanner> scanner;
uint mainPid = 0;
bool stopping = false;
bool hasExitInfo = false;
int exitCode = 0;
int exitStatus = 0;
};
QVariantMap sessionToVariantMap(const Session &session, const QString &state) const;
static QString sessionState(const Session &session);
void failLaunch(const QString &reason, const QVariantMap &context = {});
void recoverExistingSessions();
void setupSystemdSubscriptions();
void watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath);
void unwatchSystemdUnit(const QString &unitName, const QDBusObjectPath &unitPath);
void handleSystemdUnitPropertiesChanged(const QDBusObjectPath &unitPath, const QVariantMap &changedProperties);
void removeSessionInternal(const QString &sessionId, const QString &finalState);
private Q_SLOTS:
void handleSystemdUnitNew(const QString &unitName, const QDBusObjectPath &unitPath);
void handleSystemdUnitRemoved(const QString &unitName, const QDBusObjectPath &unitPath);
private:
QString launchDirect(const QVariantMap &launchSpec);
QString launchMonitored(const QVariantMap &launchSpec, const QString &provider);
void attachPidsToSession(const QString &sessionId, const QList<uint> &pids);
static QString detectProvider(const QVariantMap &launchSpec);
static QString extractSteamAppId(const QString &command);
static QString extractLutrisId(const QString &command);
SystemdUserManager m_systemd;
QHash<QString, Session> m_sessions;
int m_maxConcurrent = 0;
QHash<QString, QString> m_unitPathToSessionId;
QHash<QString, QString> m_unitNameToSessionId;
QHash<QString, QObject *> m_unitPathWatchers;
};

18
src/gamecenter/main.cpp Normal file
View file

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCoreApplication>
#include "gamecenterdaemon.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
GameCenterDaemon daemon;
if (!daemon.init()) {
return 1;
}
return app.exec();
}

View file

@ -0,0 +1,183 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include "processscanner.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
ProcessScanner::ProcessScanner(QObject *parent)
: QObject(parent)
{
m_timer.setSingleShot(false);
m_deadline.setSingleShot(true);
connect(&m_deadline, &QTimer::timeout, this, [this]() {
m_timer.stop();
Q_EMIT timedOut();
});
connect(&m_timer, &QTimer::timeout, this, [this]() {
if (!m_matcher) {
m_timer.stop();
return;
}
const QList<Match> results = m_matcher();
if (!results.isEmpty()) {
m_timer.stop();
m_deadline.stop();
Q_EMIT found(results);
}
});
}
static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
{
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
return {};
}
return f.read(maxSize);
}
static QString readExeLink(const QString &pidDir)
{
return QFileInfo(pidDir + QStringLiteral("/exe")).symLinkTarget();
}
static QByteArray readCmdline(const QString &pidDir)
{
return readProcFile(pidDir + QStringLiteral("/cmdline"));
}
static QByteArray readEnviron(const QString &pidDir)
{
return readProcFile(pidDir + QStringLiteral("/environ"));
}
static QList<uint> listPids()
{
QList<uint> pids;
const QDir proc(QStringLiteral("/proc"));
const QStringList entries = proc.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &entry : entries) {
bool ok = false;
const uint pid = entry.toUInt(&ok);
if (ok && pid > 1) {
pids.push_back(pid);
}
}
return pids;
}
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;
}
// 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) {
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;
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;
}
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;
}
void ProcessScanner::pollUntilFound(std::function<QList<Match>()> matcher, int intervalMs, int timeoutMs)
{
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);
}
void ProcessScanner::cancel()
{
m_timer.stop();
m_deadline.stop();
m_matcher = nullptr;
}

View file

@ -0,0 +1,48 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#pragma once
#include <QList>
#include <QObject>
#include <QString>
#include <QTimer>
#include <functional>
class ProcessScanner : public QObject
{
Q_OBJECT
public:
explicit ProcessScanner(QObject *parent = nullptr);
struct Match {
uint pid = 0;
QString exe;
QString cmdline;
};
// Find PIDs whose /proc/<pid>/environ contains key=value
static QList<Match> findByEnvironment(const QString &key, const QString &value);
// Find PIDs whose /proc/<pid>/cmdline contains the substring
static QList<Match> findByCmdline(const QString &substring);
// Find PIDs whose /proc/<pid>/exe resolves to a path under the given directory
static QList<Match> findByExePath(const QString &dirPrefix);
// Async poll: calls matcher repeatedly until it returns non-empty or timeout.
// Emits found() with matching PIDs, or timedOut() on failure.
void pollUntilFound(std::function<QList<Match>()> matcher, int intervalMs = 500, int timeoutMs = 15000);
void cancel();
Q_SIGNALS:
void found(const QList<ProcessScanner::Match> &matches);
void timedOut();
private:
QTimer m_timer;
QTimer m_deadline;
std::function<QList<Match>()> m_matcher;
};

View file

@ -0,0 +1,9 @@
[Unit]
Description=A-La-Karte Game Center
PartOf=graphical-session.target
[Service]
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-gamecenter
Type=dbus
BusName=org.kde.GameCenter1
Restart=no

View file

@ -0,0 +1,183 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include "systemdusermanager.h"
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QDBusMetaType>
#include <QDBusVariant>
static QDBusArgument &operator<<(QDBusArgument &argument, const SystemdProperty &prop)
{
argument.beginStructure();
argument << prop.name << QDBusVariant(prop.value);
argument.endStructure();
return argument;
}
static const QDBusArgument &operator>>(const QDBusArgument &argument, SystemdProperty &prop)
{
QDBusVariant variant;
argument.beginStructure();
argument >> prop.name >> variant;
argument.endStructure();
prop.value = variant.variant();
return argument;
}
static QDBusArgument &operator<<(QDBusArgument &argument, const SystemdAuxUnit &unit)
{
argument.beginStructure();
argument << unit.name << unit.properties;
argument.endStructure();
return argument;
}
static const QDBusArgument &operator>>(const QDBusArgument &argument, SystemdAuxUnit &unit)
{
argument.beginStructure();
argument >> unit.name >> unit.properties;
argument.endStructure();
return argument;
}
static QDBusArgument &operator<<(QDBusArgument &argument, const SystemdUnitInfo &info)
{
argument.beginStructure();
argument << info.name << info.description << info.loadState << info.activeState << info.subState << info.following << info.objectPath << info.jobId
<< info.jobType << info.jobPath;
argument.endStructure();
return argument;
}
static const QDBusArgument &operator>>(const QDBusArgument &argument, SystemdUnitInfo &info)
{
argument.beginStructure();
argument >> info.name >> info.description >> info.loadState >> info.activeState >> info.subState >> info.following >> info.objectPath >> info.jobId
>> info.jobType >> info.jobPath;
argument.endStructure();
return argument;
}
SystemdUserManager::SystemdUserManager(QObject *parent)
: QObject(parent)
{
ensureTypesRegistered();
}
void SystemdUserManager::ensureTypesRegistered()
{
static bool registered = false;
if (registered) {
return;
}
qDBusRegisterMetaType<SystemdProperty>();
qDBusRegisterMetaType<SystemdProperties>();
qDBusRegisterMetaType<SystemdAuxUnit>();
qDBusRegisterMetaType<SystemdAuxUnits>();
qDBusRegisterMetaType<SystemdUnitInfo>();
qDBusRegisterMetaType<SystemdUnitInfoList>();
registered = true;
}
bool SystemdUserManager::isAvailable() const
{
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QDBusConnection::sessionBus());
return manager.isValid();
}
QDBusReply<QDBusObjectPath> SystemdUserManager::startTransientScope(const QString &unitName, const QList<uint> &pids, const QString &description)
{
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QDBusConnection::sessionBus());
SystemdProperties props;
props.push_back({QStringLiteral("Description"), description});
props.push_back({QStringLiteral("PIDs"), QVariant::fromValue(pids)});
props.push_back({QStringLiteral("CollectMode"), QStringLiteral("inactive-or-failed")});
props.push_back({QStringLiteral("KillMode"), QStringLiteral("control-group")});
props.push_back({QStringLiteral("SendSIGKILL"), true});
props.push_back({QStringLiteral("TimeoutStopUSec"), static_cast<qulonglong>(10) * 1000 * 1000});
const SystemdAuxUnits aux;
const QDBusMessage reply =
manager.call(QStringLiteral("StartTransientUnit"), unitName, QStringLiteral("replace"), QVariant::fromValue(props), QVariant::fromValue(aux));
return QDBusReply<QDBusObjectPath>(reply);
}
QDBusReply<QDBusObjectPath> SystemdUserManager::stopUnit(const QString &unitName, const QString &mode)
{
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QDBusConnection::sessionBus());
const QDBusMessage reply = manager.call(QStringLiteral("StopUnit"), unitName, mode);
return QDBusReply<QDBusObjectPath>(reply);
}
QDBusReply<QDBusObjectPath> SystemdUserManager::getUnit(const QString &unitName)
{
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QDBusConnection::sessionBus());
const QDBusMessage reply = manager.call(QStringLiteral("GetUnit"), unitName);
return QDBusReply<QDBusObjectPath>(reply);
}
QDBusReply<SystemdUnitInfoList> SystemdUserManager::listUnits()
{
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
QDBusConnection::sessionBus());
const QDBusMessage reply = manager.call(QStringLiteral("ListUnits"));
return QDBusReply<SystemdUnitInfoList>(reply);
}
QList<uint> SystemdUserManager::scopePids(const QDBusObjectPath &unitPath)
{
QDBusInterface unit(QStringLiteral("org.freedesktop.systemd1"),
unitPath.path(),
QStringLiteral("org.freedesktop.systemd1.Unit"),
QDBusConnection::sessionBus());
const QDBusMessage reply = unit.call(QStringLiteral("GetProcesses"));
if (reply.type() != QDBusMessage::ReplyMessage || reply.arguments().isEmpty()) {
return {};
}
QList<uint> pids;
const QDBusArgument arg = reply.arguments().first().value<QDBusArgument>();
arg.beginArray();
while (!arg.atEnd()) {
arg.beginStructure();
QString cgroupPath;
uint pid = 0;
QString cmdline;
arg >> cgroupPath >> pid >> cmdline;
arg.endStructure();
if (pid > 0) {
pids.push_back(pid);
}
}
arg.endArray();
return pids;
}

View file

@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#pragma once
#include <QDBusError>
#include <QDBusObjectPath>
#include <QDBusReply>
#include <QList>
#include <QObject>
#include <QString>
#include <QVariant>
struct SystemdProperty {
QString name;
QVariant value;
};
Q_DECLARE_METATYPE(SystemdProperty)
using SystemdProperties = QList<SystemdProperty>;
Q_DECLARE_METATYPE(SystemdProperties)
struct SystemdAuxUnit {
QString name;
SystemdProperties properties;
};
Q_DECLARE_METATYPE(SystemdAuxUnit)
using SystemdAuxUnits = QList<SystemdAuxUnit>;
Q_DECLARE_METATYPE(SystemdAuxUnits)
struct SystemdUnitInfo {
QString name;
QString description;
QString loadState;
QString activeState;
QString subState;
QString following;
QDBusObjectPath objectPath;
uint jobId = 0;
QString jobType;
QDBusObjectPath jobPath;
};
Q_DECLARE_METATYPE(SystemdUnitInfo)
using SystemdUnitInfoList = QList<SystemdUnitInfo>;
Q_DECLARE_METATYPE(SystemdUnitInfoList)
class SystemdUserManager : public QObject
{
Q_OBJECT
public:
explicit SystemdUserManager(QObject *parent = nullptr);
bool isAvailable() const;
QDBusReply<QDBusObjectPath> startTransientScope(const QString &unitName, const QList<uint> &pids, const QString &description);
QDBusReply<QDBusObjectPath> stopUnit(const QString &unitName, const QString &mode = QStringLiteral("replace"));
QDBusReply<QDBusObjectPath> getUnit(const QString &unitName);
QDBusReply<SystemdUnitInfoList> listUnits();
QList<uint> scopePids(const QDBusObjectPath &unitPath);
private:
void ensureTypesRegistered();
};