// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2026 A-La-Karte Contributors #include "gamecenterdaemon.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef ALAKARTE_HAVE_KAUTH #include #include #endif #include static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1"); static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1"); static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1"); static QString ensureScopeUnitName(const QString &unitName) { if (unitName.endsWith(QLatin1String(".scope"))) { return unitName; } return unitName + QStringLiteral(".scope"); } namespace { static void terminatePids(const QList &pids) { for (uint pid : pids) { if (pid == 0) { continue; } ::kill(static_cast(pid), SIGTERM); } } static void killPids(const QList &pids) { for (uint pid : pids) { if (pid == 0) { continue; } ::kill(static_cast(pid), SIGKILL); } } static QVariant unwrapDbusVariant(QVariant v) { if (v.canConvert()) { v = v.value().variant(); } return v; } static QVariant normalizeForDbus(QVariant v) { v = unwrapDbusVariant(v); if (v.userType() == QMetaType::QChar) { return QString(v.toChar()); } if (v.userType() == QMetaType::QString || v.userType() == QMetaType::QStringList) { return v; } if (v.canConvert()) { const QDBusArgument arg = v.value(); const QMap asStringMap = qdbus_cast>(arg); if (!asStringMap.isEmpty()) { QVariantMap converted; for (auto it = asStringMap.constBegin(); it != asStringMap.constEnd(); ++it) { converted.insert(it.key(), it.value()); } return normalizeForDbus(converted); } const QVariantMap asMap = qdbus_cast(arg); if (!asMap.isEmpty()) { return normalizeForDbus(asMap); } const QStringList asStringList = qdbus_cast(arg); if (!asStringList.isEmpty()) { return asStringList; } } if (v.canConvert()) { QVariantMap m = v.toMap(); for (auto it = m.begin(); it != m.end(); ++it) { it.value() = normalizeForDbus(it.value()); } return m; } if (v.userType() == QMetaType::QVariantList) { QVariantList list = v.toList(); for (QVariant &item : list) { item = normalizeForDbus(item); } return list; } return v; } static QVariantMap normalizeVariantMapForDbus(const QVariantMap &map) { QVariantMap out; for (auto it = map.constBegin(); it != map.constEnd(); ++it) { out.insert(it.key(), normalizeForDbus(it.value())); } return out; } static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &out) { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected() || !bus.interface()) { return false; } if (!bus.interface()->isServiceRegistered(kRunnerService)) { bus.interface()->startService(kRunnerService); } QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, bus); if (!iface.isValid()) { if (bus.interface()->startService(kRunnerService).isValid()) { QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus); if (retryIface.isValid()) { retryIface.setTimeout(2000); const QDBusReply retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec); if (retryReply.isValid()) { out = normalizeVariantMapForDbus(retryReply.value()); return true; } } } return false; } iface.setTimeout(2000); const QDBusReply reply = iface.call(QStringLiteral("ResolveLaunch"), spec); if (!reply.isValid()) { if (reply.error().type() == QDBusError::ServiceUnknown) { bus.interface()->startService(kRunnerService); QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus); if (!retryIface.isValid()) { return false; } retryIface.setTimeout(2000); const QDBusReply retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec); if (!retryReply.isValid()) { return false; } out = normalizeVariantMapForDbus(retryReply.value()); return true; } return false; } out = normalizeVariantMapForDbus(reply.value()); return true; } static QString mapStringValue(const QVariantMap &map, const QStringList &keys) { for (const QString &k : keys) { const auto it = map.constFind(k); if (it == map.constEnd()) { continue; } return unwrapDbusVariant(*it).toString(); } return {}; } static QStringList variantToStringList(QVariant v) { v = unwrapDbusVariant(v); if (v.canConvert()) { return v.toStringList(); } if (v.canConvert()) { const QVariantList list = v.toList(); QStringList out; out.reserve(list.size()); for (const QVariant &item : list) { out.push_back(unwrapDbusVariant(item).toString()); } return out; } if (v.canConvert()) { const QDBusArgument arg = v.value(); const QStringList out = qdbus_cast(arg); if (!out.isEmpty()) { return out; } } const QString s = v.toString(); if (!s.trimmed().isEmpty()) { return QProcess::splitCommand(s); } return {}; } class UnitPropertiesWatcher : public QObject { Q_OBJECT public: explicit UnitPropertiesWatcher(const QDBusObjectPath &path, QObject *parent = nullptr) : QObject(parent) , m_path(path) { } QDBusObjectPath path() const { return m_path; } public Q_SLOTS: void onPropertiesChanged(const QString &interfaceName, const QVariantMap &changedProperties, const QStringList &invalidated) { Q_UNUSED(invalidated) if (interfaceName != QLatin1String("org.freedesktop.systemd1.Unit")) { return; } Q_EMIT propertiesChanged(m_path, changedProperties); } Q_SIGNALS: void propertiesChanged(const QDBusObjectPath &path, const QVariantMap &changedProperties); private: QDBusObjectPath m_path; }; } // 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"); namespace { static QString gameCenterIntrospectionXml() { return QStringLiteral( "" "" " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " " ""); } } // namespace class GameCenterSystemProxyPrivate; class GameCenterProxyVirtualObject : public QDBusVirtualObject { Q_OBJECT public: explicit GameCenterProxyVirtualObject(GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr) : QDBusVirtualObject(parent) , m_proxy(proxy) { } QString introspect(const QString &path) const override; bool handleMessage(const QDBusMessage &message, const QDBusConnection &connection) override; private: GameCenterSystemProxyPrivate *m_proxy = nullptr; }; class BackendSignalForwarder : public QObject { Q_OBJECT public: explicit BackendSignalForwarder(uint uid, GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr) : QObject(parent) , m_uid(uid) , m_proxy(proxy) { } public Q_SLOTS: void onSessionAdded(const QVariantMap &session); void onSessionChanged(const QVariantMap &session); void onSessionRemoved(const QString &sessionId, const QVariantMap &finalState); void onLaunchFailed(const QVariantMap &error); private: uint m_uid = 0; GameCenterSystemProxyPrivate *m_proxy = nullptr; }; struct BackendBusEntry { BackendBusEntry(const QDBusConnection &bus, BackendSignalForwarder *forwarder) : bus(bus) , forwarder(forwarder) { } QDBusConnection bus; BackendSignalForwarder *forwarder = nullptr; }; class GameCenterSystemProxyPrivate : public QObject { Q_OBJECT public: explicit GameCenterSystemProxyPrivate(QObject *parent = nullptr) : QObject(parent) { } bool init(); bool handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection); void forwardSignalToUid(uint uid, const QString &member, const QList &args); private Q_SLOTS: void handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner); private: bool uidForSystemSender(const QString &senderUniqueName, uint &uid) const; void rememberClient(const QString &senderUniqueName, uint uid); void forgetClient(const QString &senderUniqueName); BackendBusEntry *ensureBackend(uint uid); QDBusConnection m_systemBus = QDBusConnection::systemBus(); std::unique_ptr m_virtualObject; QHash m_backends; QHash m_clientUid; QHash> m_uidClients; }; QString GameCenterProxyVirtualObject::introspect(const QString &path) const { if (path == kGameCenterPath) { return gameCenterIntrospectionXml(); } return {}; } bool GameCenterProxyVirtualObject::handleMessage(const QDBusMessage &message, const QDBusConnection &connection) { if (!m_proxy) { return false; } return m_proxy->handleSystemMessage(message, connection); } void BackendSignalForwarder::onSessionAdded(const QVariantMap &session) { if (m_proxy) { m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionAdded"), {session}); } } void BackendSignalForwarder::onSessionChanged(const QVariantMap &session) { if (m_proxy) { m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionChanged"), {session}); } } void BackendSignalForwarder::onSessionRemoved(const QString &sessionId, const QVariantMap &finalState) { if (m_proxy) { m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionRemoved"), {sessionId, finalState}); } } void BackendSignalForwarder::onLaunchFailed(const QVariantMap &error) { if (m_proxy) { m_proxy->forwardSignalToUid(m_uid, QStringLiteral("LaunchFailed"), {error}); } } bool GameCenterSystemProxyPrivate::init() { if (!m_systemBus.isConnected()) { return false; } if (!m_systemBus.registerService(kGameCenterService)) { return false; } m_virtualObject = std::make_unique(this); if (!m_systemBus.registerVirtualObject(kGameCenterPath, m_virtualObject.get(), QDBusConnection::SingleNode)) { return false; } if (m_systemBus.interface()) { connect(m_systemBus.interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &GameCenterSystemProxyPrivate::handleServiceOwnerChanged); } return true; } bool GameCenterSystemProxyPrivate::uidForSystemSender(const QString &senderUniqueName, uint &uid) const { if (!m_systemBus.interface()) { return false; } const QDBusReply uidReply = m_systemBus.interface()->serviceUid(senderUniqueName); if (!uidReply.isValid()) { return false; } uid = uidReply.value(); return true; } void GameCenterSystemProxyPrivate::rememberClient(const QString &senderUniqueName, uint uid) { const auto it = m_clientUid.constFind(senderUniqueName); if (it != m_clientUid.constEnd()) { if (it.value() == uid) { return; } forgetClient(senderUniqueName); } m_clientUid.insert(senderUniqueName, uid); m_uidClients[uid].insert(senderUniqueName); } void GameCenterSystemProxyPrivate::forgetClient(const QString &senderUniqueName) { const auto it = m_clientUid.find(senderUniqueName); if (it == m_clientUid.end()) { return; } const uint uid = it.value(); m_clientUid.erase(it); auto clientsIt = m_uidClients.find(uid); if (clientsIt != m_uidClients.end()) { clientsIt.value().remove(senderUniqueName); if (clientsIt.value().isEmpty()) { m_uidClients.erase(clientsIt); } } } void GameCenterSystemProxyPrivate::handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner) { Q_UNUSED(oldOwner) if (!newOwner.isEmpty()) { return; } if (!name.startsWith(QLatin1Char(':'))) { return; } forgetClient(name); } BackendBusEntry *GameCenterSystemProxyPrivate::ensureBackend(uint uid) { const auto it = m_backends.find(uid); if (it != m_backends.end()) { return &it.value(); } const QString address = QStringLiteral("unix:path=/run/user/%1/bus").arg(uid); const QString connectionName = QStringLiteral("gamecenter-user-%1").arg(uid); QDBusConnection backend = QDBusConnection::connectToBus(address, connectionName); if (!backend.isConnected()) { return nullptr; } auto *forwarder = new BackendSignalForwarder(uid, this, this); BackendBusEntry entry(backend, forwarder); backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionAdded"), forwarder, SLOT(onSessionAdded(QVariantMap))); backend .connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionChanged"), forwarder, SLOT(onSessionChanged(QVariantMap))); backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionRemoved"), forwarder, SLOT(onSessionRemoved(QString, QVariantMap))); backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("LaunchFailed"), forwarder, SLOT(onLaunchFailed(QVariantMap))); auto inserted = m_backends.insert(uid, entry); return &inserted.value(); } void GameCenterSystemProxyPrivate::forwardSignalToUid(uint uid, const QString &member, const QList &args) { if (!m_systemBus.isConnected()) { return; } const auto clientsIt = m_uidClients.constFind(uid); if (clientsIt == m_uidClients.constEnd()) { return; } const QSet clients = clientsIt.value(); for (const QString &client : clients) { if (client.isEmpty()) { continue; } QDBusMessage signal = QDBusMessage::createTargetedSignal(client, kGameCenterPath, kGameCenterInterface, member); signal.setArguments(args); m_systemBus.send(signal); } } bool GameCenterSystemProxyPrivate::handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection) { if (message.type() != QDBusMessage::MethodCallMessage) { return false; } if (message.interface() == QLatin1String("org.freedesktop.DBus.Introspectable") && message.member() == QLatin1String("Introspect")) { if (!message.isReplyRequired()) { return true; } connection.send(message.createReply(gameCenterIntrospectionXml())); return true; } const QString sender = message.service(); if (sender.isEmpty()) { if (message.isReplyRequired()) { connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("missing sender"))); } return true; } uint uid = 0; if (!uidForSystemSender(sender, uid)) { if (message.isReplyRequired()) { connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("failed to determine caller uid"))); } return true; } rememberClient(sender, uid); BackendBusEntry *backend = ensureBackend(uid); if (!backend) { if (message.isReplyRequired()) { connection.send(message.createErrorReply(QDBusError::ServiceUnknown, QStringLiteral("user bus backend unavailable"))); } return true; } QDBusMessage forward = QDBusMessage::createMethodCall(kGameCenterService, message.path(), message.interface(), message.member()); forward.setArguments(message.arguments()); forward.setAutoStartService(true); const QDBusMessage backendReply = backend->bus.call(forward, QDBus::Block, -1); if (!message.isReplyRequired()) { return true; } if (backendReply.type() == QDBusMessage::ErrorMessage) { connection.send(message.createErrorReply(backendReply.errorName(), backendReply.errorMessage())); return true; } if (backendReply.type() != QDBusMessage::ReplyMessage) { connection.send(message.createErrorReply(QDBusError::Failed, QStringLiteral("backend did not reply"))); return true; } QDBusMessage out = message.createReply(backendReply.arguments()); if (message.interface() == kGameCenterInterface && message.member() == QLatin1String("GetCapabilities")) { if (!out.arguments().isEmpty()) { QVariant capsV = unwrapDbusVariant(out.arguments().first()); QVariantMap caps; if (capsV.canConvert()) { const QDBusArgument arg = capsV.value(); caps = qdbus_cast(arg); } else if (capsV.canConvert()) { caps = capsV.toMap(); } if (!caps.isEmpty()) { caps.insert(QStringLiteral("supportsSystemBus"), true); out.setArguments({caps}); } } } connection.send(out); return true; } GameCenterSystemProxy::GameCenterSystemProxy(QObject *parent) : QObject(parent) , d(std::make_unique()) { } GameCenterSystemProxy::~GameCenterSystemProxy() = default; bool GameCenterSystemProxy::init() { return d->init(); } #endif void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath) { if (unitName.isEmpty()) { return; } m_unitNameToSessionId.insert(unitName, sessionId); if (unitPath.path().isEmpty()) { return; } const QString unitPathKey = unitPath.path(); m_unitPathToSessionId.insert(unitPathKey, sessionId); if (m_unitPathWatchers.contains(unitPathKey)) { return; } auto *watcher = new UnitPropertiesWatcher(unitPath, this); m_unitPathWatchers.insert(unitPathKey, watcher); QDBusConnection bus = QDBusConnection::sessionBus(); bus.connect(QStringLiteral("org.freedesktop.systemd1"), unitPath.path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged"), watcher, SLOT(onPropertiesChanged(QString, QVariantMap, QStringList))); connect(watcher, &UnitPropertiesWatcher::propertiesChanged, this, &GameCenterDaemon::handleSystemdUnitPropertiesChanged); } void GameCenterDaemon::unwatchSystemdUnit(const QString &unitName, const QDBusObjectPath &unitPath) { if (!unitName.isEmpty()) { m_unitNameToSessionId.remove(unitName); } if (!unitPath.path().isEmpty()) { const QString unitPathKey = unitPath.path(); m_unitPathToSessionId.remove(unitPathKey); if (QObject *watcher = m_unitPathWatchers.take(unitPathKey)) { watcher->deleteLater(); } } } void GameCenterDaemon::handleSystemdUnitNew(const QString &unitName, const QDBusObjectPath &unitPath) { const auto it = m_unitNameToSessionId.constFind(unitName); if (it == m_unitNameToSessionId.constEnd()) { return; } const QString sessionId = it.value(); auto sit = m_sessions.find(sessionId); if (sit == m_sessions.end()) { return; } if (sit.value().unitPath.path().isEmpty() && !unitPath.path().isEmpty()) { sit.value().unitPath = unitPath; watchSystemdUnit(sessionId, unitName, unitPath); } } void GameCenterDaemon::handleSystemdUnitRemoved(const QString &unitName, const QDBusObjectPath &unitPath) { QString sessionId; if (!unitPath.path().isEmpty()) { const auto it = m_unitPathToSessionId.constFind(unitPath.path()); if (it != m_unitPathToSessionId.constEnd()) { sessionId = it.value(); } } if (sessionId.isEmpty() && !unitName.isEmpty()) { const auto it = m_unitNameToSessionId.constFind(unitName); if (it != m_unitNameToSessionId.constEnd()) { sessionId = it.value(); } } if (sessionId.isEmpty()) { return; } auto sit = m_sessions.find(sessionId); if (sit == m_sessions.end()) { unwatchSystemdUnit(unitName, unitPath); return; } unwatchSystemdUnit(sit.value().unitName, sit.value().unitPath); removeSessionInternal(sessionId, sit.value().stopping ? QStringLiteral("Stopped") : QStringLiteral("Exited")); } void GameCenterDaemon::handleSystemdUnitPropertiesChanged(const QDBusObjectPath &unitPath, const QVariantMap &changedProperties) { if (unitPath.path().isEmpty()) { return; } const auto sidIt = m_unitPathToSessionId.constFind(unitPath.path()); if (sidIt == m_unitPathToSessionId.constEnd()) { return; } const QString sessionId = sidIt.value(); auto sit = m_sessions.find(sessionId); if (sit == m_sessions.end()) { return; } QVariant activeStateV = changedProperties.value(QStringLiteral("ActiveState")); if (activeStateV.canConvert()) { activeStateV = activeStateV.value().variant(); } if (!activeStateV.isValid()) { return; } const QString activeState = activeStateV.toString(); if (activeState == QLatin1String("active") || activeState == QLatin1String("activating") || activeState == QLatin1String("deactivating")) { const QList pids = m_systemd.scopePids(unitPath); if (!pids.isEmpty()) { sit.value().mainPid = pids.first(); } Q_EMIT SessionChanged(sessionToVariantMap(sit.value(), sessionState(sit.value()))); return; } const QString finalState = sit.value().stopping ? QStringLiteral("Stopped") : (activeState == QLatin1String("failed") ? QStringLiteral("Failed") : QStringLiteral("Exited")); unwatchSystemdUnit(sit.value().unitName, sit.value().unitPath); removeSessionInternal(sessionId, finalState); } void GameCenterDaemon::removeSessionInternal(const QString &sessionId, const QString &finalState) { auto it = m_sessions.find(sessionId); if (it == m_sessions.end()) { return; } QVariantMap finalMap = sessionToVariantMap(it.value(), finalState); if (it.value().hasExitInfo) { finalMap.insert(QStringLiteral("exitCode"), it.value().exitCode); finalMap.insert(QStringLiteral("exitStatus"), it.value().exitStatus); } if (it.value().process) { it.value().process->deleteLater(); it.value().process = nullptr; } if (it.value().scanner) { it.value().scanner->cancel(); it.value().scanner->deleteLater(); it.value().scanner = nullptr; } Q_EMIT SessionRemoved(sessionId, finalMap); m_sessions.erase(it); } GameCenterDaemon::GameCenterDaemon(QObject *parent) : QObject(parent) { } bool GameCenterDaemon::init() { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { return false; } if (!bus.registerService(QStringLiteral("org.kde.GameCenter1"))) { return false; } if (!bus.registerObject(QStringLiteral("/org/kde/GameCenter1"), this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) { return false; } setupSystemdSubscriptions(); recoverExistingSessions(); return true; } void GameCenterDaemon::setupSystemdSubscriptions() { if (!m_systemd.isAvailable()) { return; } QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { return; } bus.connect(QStringLiteral("org.freedesktop.systemd1"), QStringLiteral("/org/freedesktop/systemd1"), QStringLiteral("org.freedesktop.systemd1.Manager"), QStringLiteral("UnitNew"), this, SLOT(handleSystemdUnitNew(QString, QDBusObjectPath))); bus.connect(QStringLiteral("org.freedesktop.systemd1"), QStringLiteral("/org/freedesktop/systemd1"), QStringLiteral("org.freedesktop.systemd1.Manager"), QStringLiteral("UnitRemoved"), this, SLOT(handleSystemdUnitRemoved(QString, QDBusObjectPath))); } QString GameCenterDaemon::Ping() const { return QStringLiteral("ok"); } void GameCenterDaemon::Version(uint &major, uint &minor) const { major = 1; minor = 0; } 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(); caps.insert(QStringLiteral("supportsSteamAttach"), systemdAvailable && steamAvailable); caps.insert(QStringLiteral("supportsLutrisAttach"), systemdAvailable && lutrisAvailable); return caps; } QVariantList GameCenterDaemon::ListSessions() const { QVariantList sessions; sessions.reserve(m_sessions.size()); for (const auto &session : m_sessions) { sessions.push_back(sessionToVariantMap(session, sessionState(session))); } return sessions; } QVariantMap GameCenterDaemon::GetSession(const QString &sessionId) const { const auto it = m_sessions.constFind(sessionId); if (it == m_sessions.constEnd()) { return {}; } return sessionToVariantMap(it.value(), sessionState(it.value())); } void GameCenterDaemon::SetPolicy(const QVariantMap &policy) { if (policy.contains(QStringLiteral("maxConcurrent"))) { QVariant v = policy.value(QStringLiteral("maxConcurrent")); if (v.canConvert()) { v = v.value().variant(); } m_maxConcurrent = v.toInt(); } if (policy.contains(QStringLiteral("powerProfile"))) { QVariant v = policy.value(QStringLiteral("powerProfile")); if (v.canConvert()) { v = v.value().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}, }; } QString GameCenterDaemon::Launch(const QVariantMap &launchSpec) { if (!m_systemd.isAvailable()) { failLaunch(QStringLiteral("systemd user manager is not available"), launchSpec); return {}; } if (m_maxConcurrent > 0 && m_sessions.size() >= m_maxConcurrent) { QVariantMap ctx = launchSpec; ctx.insert(QStringLiteral("maxConcurrent"), m_maxConcurrent); ctx.insert(QStringLiteral("activeSessions"), m_sessions.size()); failLaunch(QStringLiteral("max concurrent session limit reached"), ctx); return {}; } const QString provider = detectProvider(launchSpec); if (provider == QLatin1String("steam") || provider == QLatin1String("lutris")) { return launchMonitored(launchSpec, provider); } return launchDirect(launchSpec); } QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec) { const QString gameId = mapStringValue(launchSpec, {QStringLiteral("gameId"), QStringLiteral("id")}); const QString displayName = mapStringValue(launchSpec, {QStringLiteral("displayName"), QStringLiteral("name"), QStringLiteral("title")}); const QString workingDirectory = mapStringValue(launchSpec, {QStringLiteral("workingDirectory"), QStringLiteral("cwd"), QStringLiteral("workingDir")}); const QString command = mapStringValue(launchSpec, {QStringLiteral("command"), QStringLiteral("launchCommand")}); QString program = mapStringValue(launchSpec, {QStringLiteral("program"), QStringLiteral("executable"), QStringLiteral("exe")}); QStringList args; if (launchSpec.contains(QStringLiteral("args"))) { args = variantToStringList(launchSpec.value(QStringLiteral("args"))); } else if (launchSpec.contains(QStringLiteral("argv"))) { args = variantToStringList(launchSpec.value(QStringLiteral("argv"))); } else if (launchSpec.contains(QStringLiteral("arguments"))) { args = variantToStringList(launchSpec.value(QStringLiteral("arguments"))); } if (program.trimmed().isEmpty()) { if (command.trimmed().isEmpty()) { failLaunch(QStringLiteral("launchSpec.command or launchSpec.program is required"), launchSpec); return {}; } const QStringList argv = QProcess::splitCommand(command); if (argv.isEmpty()) { failLaunch(QStringLiteral("failed to parse command"), launchSpec); return {}; } program = argv.first(); args = argv.mid(1); } QVariantMap envOverrides; auto extractVariantMap = [](QVariant v) { if (v.canConvert()) { v = v.value().variant(); } if (v.canConvert()) { const QVariantMap asMap = v.toMap(); if (!asMap.isEmpty()) { return asMap; } } if (v.canConvert()) { const QDBusArgument arg = v.value(); const QMap asStringMap = qdbus_cast>(arg); if (!asStringMap.isEmpty()) { QVariantMap converted; for (auto it = asStringMap.constBegin(); it != asStringMap.constEnd(); ++it) { converted.insert(it.key(), it.value()); } return converted; } 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{}; }; if (launchSpec.contains(QStringLiteral("env"))) { envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("env"))); } else if (launchSpec.contains(QStringLiteral("environment"))) { envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("environment"))); } else if (launchSpec.contains(QStringLiteral("envOverrides"))) { envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("envOverrides"))); } const QString runnerId = mapStringValue(launchSpec, {QStringLiteral("runnerId")}); const QString runner = mapStringValue(launchSpec, {QStringLiteral("runner")}); const QString runnerPath = mapStringValue(launchSpec, {QStringLiteral("runnerPath")}); const QString prefixPath = mapStringValue(launchSpec, {QStringLiteral("prefixPath")}); const QString requestedProgram = mapStringValue(launchSpec, {QStringLiteral("requestedProgram")}); QStringList requestedArgs; if (launchSpec.contains(QStringLiteral("requestedArgs"))) { requestedArgs = variantToStringList(launchSpec.value(QStringLiteral("requestedArgs"))); } QVariantMap requestedEnvOverrides; if (launchSpec.contains(QStringLiteral("requestedEnvOverrides"))) { requestedEnvOverrides = extractVariantMap(launchSpec.value(QStringLiteral("requestedEnvOverrides"))); } const bool wantsRunnerResolution = !runnerId.isEmpty() || runner == QLatin1String("wine") || runner == QLatin1String("proton"); if (wantsRunnerResolution) { const QString baseProgram = requestedProgram.isEmpty() ? program : requestedProgram; const QStringList baseArgs = requestedArgs.isEmpty() ? args : requestedArgs; const QVariantMap baseEnv = requestedEnvOverrides.isEmpty() ? envOverrides : requestedEnvOverrides; const QVariantMap resolveSpec = { {QStringLiteral("runnerId"), runnerId}, {QStringLiteral("runner"), runner}, {QStringLiteral("runnerPath"), runnerPath}, {QStringLiteral("gameId"), gameId}, {QStringLiteral("prefixPath"), prefixPath}, {QStringLiteral("program"), baseProgram}, {QStringLiteral("args"), baseArgs}, {QStringLiteral("envOverrides"), baseEnv}, }; QVariantMap resolved; if (tryResolveWithRunnerManager(resolveSpec, resolved)) { if (!resolved.value(QStringLiteral("ok")).toBool()) { QVariantMap ctx = launchSpec; ctx.insert(QStringLiteral("runnerResolution"), resolved); failLaunch(resolved.value(QStringLiteral("error")).toString(), ctx); return {}; } program = mapStringValue(resolved, {QStringLiteral("finalProgram")}); if (resolved.contains(QStringLiteral("finalArgs"))) { args = variantToStringList(resolved.value(QStringLiteral("finalArgs"))); } if (resolved.contains(QStringLiteral("effectiveEnv"))) { envOverrides = extractVariantMap(resolved.value(QStringLiteral("effectiveEnv"))); } const QString resolvedPrefixPath = mapStringValue(resolved, {QStringLiteral("resolvedPrefixPath")}); if (!resolvedPrefixPath.isEmpty()) { QDir().mkpath(resolvedPrefixPath); } } else if (!runnerId.isEmpty()) { QVariantMap ctx = launchSpec; ctx.insert(QStringLiteral("runnerResolution"), QVariantMap{{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("runner service unavailable")}}); failLaunch(QStringLiteral("runner service is not available"), ctx); return {}; } } const QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces); const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId)); auto *process = new QProcess(this); if (!workingDirectory.isEmpty()) { process->setWorkingDirectory(workingDirectory); } if (!envOverrides.isEmpty()) { QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); for (auto it = envOverrides.constBegin(); it != envOverrides.constEnd(); ++it) { const QString key = it.key(); if (key.isEmpty() || key.contains(QLatin1Char('='))) { continue; } QVariant value = it.value(); if (value.canConvert()) { value = value.value().variant(); } env.insert(key, value.toString()); } process->setProcessEnvironment(env); } process->start(program, args); if (!process->waitForStarted()) { const QVariantMap ctx = { {QStringLiteral("command"), command}, {QStringLiteral("program"), program}, {QStringLiteral("args"), args}, {QStringLiteral("gameId"), gameId}, {QStringLiteral("workingDirectory"), workingDirectory}, {QStringLiteral("envOverrides"), envOverrides}, {QStringLiteral("error"), process->errorString()}, }; process->deleteLater(); failLaunch(QStringLiteral("failed to start process"), ctx); return {}; } const QList pids = {static_cast(process->processId())}; const QString description = gameId.isEmpty() ? QStringLiteral("A-La-Karte game") : QStringLiteral("A-La-Karte game %1").arg(gameId); const QDBusReply startReply = m_systemd.startTransientScope(unitName, pids, description); if (!startReply.isValid()) { const QVariantMap ctx = { {QStringLiteral("command"), command}, {QStringLiteral("program"), program}, {QStringLiteral("args"), args}, {QStringLiteral("gameId"), gameId}, {QStringLiteral("workingDirectory"), workingDirectory}, {QStringLiteral("envOverrides"), envOverrides}, {QStringLiteral("unit"), unitName}, {QStringLiteral("error"), startReply.error().message()}, }; process->kill(); process->deleteLater(); failLaunch(QStringLiteral("failed to create transient scope"), ctx); return {}; } QDBusObjectPath unitPath; const QDBusReply getUnitReply = m_systemd.getUnit(unitName); if (getUnitReply.isValid()) { unitPath = getUnitReply.value(); } Session session; session.sessionId = sessionId; session.gameId = gameId; session.displayName = displayName; session.unitName = unitName; session.unitPath = unitPath; session.provider = QStringLiteral("manual"); session.startTime = QDateTime::currentDateTimeUtc(); session.process = process; session.mainPid = static_cast(process->processId()); m_sessions.insert(sessionId, session); watchSystemdUnit(sessionId, unitName, unitPath); Q_EMIT SessionAdded(sessionToVariantMap(session, QStringLiteral("Running"))); connect(process, qOverload(&QProcess::finished), this, [this, sessionId](int exitCode, QProcess::ExitStatus exitStatus) { const auto it = m_sessions.find(sessionId); if (it == m_sessions.end()) { return; } it.value().hasExitInfo = true; it.value().exitCode = exitCode; it.value().exitStatus = static_cast(exitStatus); if (it.value().process) { it.value().process->deleteLater(); it.value().process = nullptr; } }); return sessionId; } QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const QString &provider) { const QString gameId = mapStringValue(launchSpec, {QStringLiteral("gameId"), QStringLiteral("id")}); const QString displayName = mapStringValue(launchSpec, {QStringLiteral("displayName"), QStringLiteral("name"), QStringLiteral("title")}); const QString command = mapStringValue(launchSpec, {QStringLiteral("command"), QStringLiteral("launchCommand")}); const QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces); // Determine bootstrap command and scanner matcher QString bootstrapProgram; QStringList bootstrapArgs; std::function()> matcher; if (provider == QLatin1String("steam")) { const QString appId = extractSteamAppId(command); if (appId.isEmpty()) { failLaunch(QStringLiteral("could not extract Steam appId from command"), launchSpec); return {}; } 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); // Filter out the steam client itself QList filtered; for (const auto &m : std::as_const(results)) { if (!m.exe.contains(QLatin1String("steam")) || m.exe.contains(QLatin1String("proton"))) { filtered.push_back(m); } } return filtered.isEmpty() ? results : filtered; }; } else if (provider == QLatin1String("lutris")) { const QString lutrisId = extractLutrisId(command); if (lutrisId.isEmpty()) { failLaunch(QStringLiteral("could not extract Lutris ID from command"), launchSpec); return {}; } bootstrapProgram = QStringLiteral("lutris"); 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 QList filtered; for (const auto &m : std::as_const(results)) { if (!m.exe.endsWith(QLatin1String("/lutris")) && !m.exe.endsWith(QLatin1String("/python3"))) { filtered.push_back(m); } } if (!filtered.isEmpty()) { results = filtered; } } return results; }; } if (bootstrapProgram.isEmpty() || !matcher) { QVariantMap ctx = launchSpec; ctx.insert(QStringLiteral("provider"), provider); failLaunch(QStringLiteral("unsupported monitored provider"), ctx); return {}; } if (QStandardPaths::findExecutable(bootstrapProgram).isEmpty()) { const QVariantMap ctx = { {QStringLiteral("gameId"), gameId}, {QStringLiteral("provider"), provider}, {QStringLiteral("program"), bootstrapProgram}, }; failLaunch(QStringLiteral("bootstrap executable not found"), ctx); return {}; } // Create session in Launching state Session session; session.sessionId = sessionId; session.gameId = gameId; session.displayName = displayName; session.provider = provider; session.startTime = QDateTime::currentDateTimeUtc(); m_sessions.insert(sessionId, session); Q_EMIT SessionAdded(sessionToVariantMap(session, QStringLiteral("Launching"))); // Start bootstrap process (e.g. steam -applaunch or lutris ...) auto *bootstrap = new QProcess(this); bootstrap->start(bootstrapProgram, bootstrapArgs); if (!bootstrap->waitForStarted(5000)) { const QVariantMap ctx = { {QStringLiteral("gameId"), gameId}, {QStringLiteral("provider"), provider}, {QStringLiteral("command"), command}, {QStringLiteral("error"), bootstrap->errorString()}, }; bootstrap->deleteLater(); QVariantMap finalState = sessionToVariantMap(session, QStringLiteral("Failed")); Q_EMIT SessionRemoved(sessionId, finalState); m_sessions.remove(sessionId); failLaunch(QStringLiteral("failed to start bootstrap process"), ctx); return {}; } 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 scopeReply = m_systemd.startTransientScope(unitName, {static_cast(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 getUnitReply = m_systemd.getUnit(unitName); if (getUnitReply.isValid()) { session.unitPath = getUnitReply.value(); } m_sessions[sessionId] = session; connect(bootstrap, qOverload(&QProcess::finished), this, [this, sessionId](int, QProcess::ExitStatus) { const auto it = m_sessions.find(sessionId); if (it == m_sessions.end()) { return; } if (it.value().scanner || it.value().stopping) { return; } if (it.value().process) { it.value().process->deleteLater(); it.value().process = nullptr; } }); // Start polling for game PID auto *scanner = new ProcessScanner(this); m_sessions[sessionId].scanner = scanner; connect(scanner, &ProcessScanner::found, this, [this, sessionId](const QList &matches) { QList pids; pids.reserve(matches.size()); for (const auto &m : matches) { pids.push_back(m.pid); } attachPidsToSession(sessionId, pids); }); connect(scanner, &ProcessScanner::timedOut, this, [this, sessionId, launchSpec]() { auto it = m_sessions.find(sessionId); if (it == m_sessions.end()) { return; } const bool stopping = it.value().stopping; const QVariantMap ctx = { {QStringLiteral("gameId"), it.value().gameId}, {QStringLiteral("provider"), it.value().provider}, }; 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(); } if (it.value().process) { it.value().process->deleteLater(); } Q_EMIT SessionRemoved(sessionId, finalState); m_sessions.erase(it); if (!stopping) { failLaunch(QStringLiteral("timed out waiting for game process"), ctx); } }); scanner->pollUntilFound(std::move(matcher), 500, 20000); return sessionId; } void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList &pids) { auto it = m_sessions.find(sessionId); if (it == m_sessions.end() || pids.isEmpty()) { return; } if (it.value().scanner) { it.value().scanner->cancel(); it.value().scanner->deleteLater(); it.value().scanner = nullptr; } it.value().mainPid = pids.first(); 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); QDBusReply attachReply = m_systemd.attachProcessesToUnit(unitName, pids); if (!attachReply.isValid()) { if (attachReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) { const QDBusReply 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(); } 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; } } it.value().unitName = unitName; const QDBusReply getUnitReply = m_systemd.getUnit(unitName); if (getUnitReply.isValid()) { it.value().unitPath = getUnitReply.value(); } watchSystemdUnit(sessionId, unitName, it.value().unitPath); if (it.value().process) { it.value().process->deleteLater(); it.value().process = nullptr; } if (it.value().stopping) { const QDBusReply 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); removeSessionInternal(sessionId, QStringLiteral("Stopped")); return; } Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Running"))); } QString GameCenterDaemon::detectProvider(const QVariantMap &launchSpec) { // Explicit provider hint takes priority QString explicit_provider; { QVariant v = launchSpec.value(QStringLiteral("provider")); if (v.canConvert()) { v = v.value().variant(); } explicit_provider = v.toString().toLower(); } if (!explicit_provider.isEmpty()) { return explicit_provider; } // Auto-detect from command const QString command = mapStringValue(launchSpec, {QStringLiteral("command"), QStringLiteral("launchCommand")}); if (command.contains(QLatin1String("steam://rungameid/")) || command.contains(QLatin1String("steam -applaunch"))) { return QStringLiteral("steam"); } if (command.startsWith(QLatin1String("lutris ")) || command.contains(QLatin1String("lutris:rungame"))) { return QStringLiteral("lutris"); } return QStringLiteral("manual"); } QString GameCenterDaemon::extractSteamAppId(const QString &command) { // steam://rungameid/ static const QRegularExpression steamUrlRe(QStringLiteral("steam://rungameid/(\\d+)")); QRegularExpressionMatch match = steamUrlRe.match(command); if (match.hasMatch()) { return match.captured(1); } // steam -applaunch static const QRegularExpression applaunchRe(QStringLiteral("-applaunch\\s+(\\d+)")); match = applaunchRe.match(command); if (match.hasMatch()) { return match.captured(1); } return {}; } QString GameCenterDaemon::extractLutrisId(const QString &command) { // lutris:rungameid/ or lutris:rungame/ static const QRegularExpression lutrisRe(QStringLiteral("lutris:rungame(?:id)?/([^\\s]+)")); QRegularExpressionMatch match = lutrisRe.match(command); if (match.hasMatch()) { return match.captured(1); } // "lutris lutris:rungameid/" form static const QRegularExpression lutrisCmdRe(QStringLiteral("lutris\\s+lutris:rungame(?:id)?/([^\\s]+)")); match = lutrisCmdRe.match(command); if (match.hasMatch()) { return match.captured(1); } return {}; } void GameCenterDaemon::Stop(const QString &sessionId) { auto it = m_sessions.find(sessionId); if (it == m_sessions.end()) { return; } it.value().stopping = true; if (it.value().scanner) { Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping"))); QPointer proc = it.value().process; it.value().process = nullptr; if (proc) { connect(proc, qOverload(&QProcess::finished), proc, &QObject::deleteLater); if (proc->state() != QProcess::NotRunning) { proc->terminate(); QTimer::singleShot(5000, proc, [proc]() { if (proc && proc->state() != QProcess::NotRunning) { proc->kill(); } }); } else { proc->deleteLater(); } } if (!it.value().unitName.isEmpty()) { m_systemd.stopUnit(it.value().unitName); } return; } if (it.value().unitName.isEmpty()) { QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped")); if (it.value().process) { it.value().process->kill(); it.value().process->deleteLater(); } Q_EMIT SessionRemoved(sessionId, finalState); m_sessions.erase(it); return; } const QDBusReply 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]() { if (process && process->state() != QProcess::NotRunning) { process->kill(); } }); Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping"))); } else { Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Error"))); } return; } Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping"))); } void GameCenterDaemon::StopByGameId(const QString &gameId) { if (gameId.isEmpty()) { return; } QStringList sessionsToStop; sessionsToStop.reserve(m_sessions.size()); for (const auto &session : m_sessions) { if (session.gameId == gameId) { sessionsToStop.push_back(session.sessionId); } } for (const QString &sessionId : sessionsToStop) { Stop(sessionId); } } QString GameCenterDaemon::sessionState(const Session &session) { if (session.stopping) { return QStringLiteral("Stopping"); } 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"); } return QStringLiteral("Unknown"); } QVariantMap GameCenterDaemon::sessionToVariantMap(const Session &session, const QString &state) const { QVariantMap map; map.insert(QStringLiteral("sessionId"), session.sessionId); map.insert(QStringLiteral("gameId"), session.gameId); if (!session.displayName.isEmpty()) { map.insert(QStringLiteral("displayName"), session.displayName); } map.insert(QStringLiteral("unit"), session.unitName); map.insert(QStringLiteral("unitName"), session.unitName); map.insert(QStringLiteral("state"), state); if (!session.provider.isEmpty()) { map.insert(QStringLiteral("provider"), session.provider); } if (session.startTime.isValid()) { map.insert(QStringLiteral("startTime"), session.startTime.toString(Qt::ISODate)); } if (session.process && session.process->processId() > 0) { map.insert(QStringLiteral("pid"), static_cast(session.process->processId())); } else if (session.mainPid > 0) { map.insert(QStringLiteral("pid"), static_cast(session.mainPid)); } return map; } void GameCenterDaemon::failLaunch(const QString &reason, const QVariantMap &context) { QVariantMap error; error.insert(QStringLiteral("error"), reason); if (!context.isEmpty()) { error.insert(QStringLiteral("context"), normalizeVariantMapForDbus(context)); const QString gameId = mapStringValue(context, {QStringLiteral("gameId"), QStringLiteral("id")}); if (!gameId.isEmpty()) { error.insert(QStringLiteral("gameId"), gameId); } } Q_EMIT LaunchFailed(error); } void GameCenterDaemon::recoverExistingSessions() { if (!m_systemd.isAvailable()) { return; } const QDBusReply reply = m_systemd.listUnits(); if (!reply.isValid()) { return; } static const QString prefix = QStringLiteral("alakarte-game-"); static const QString suffix = QStringLiteral(".scope"); static const QString descPrefix = QStringLiteral("A-La-Karte game "); const SystemdUnitInfoList units = reply.value(); for (const SystemdUnitInfo &unit : units) { if (!unit.name.startsWith(prefix) || !unit.name.endsWith(suffix)) { continue; } if (unit.activeState != QLatin1String("active")) { continue; } const QString sessionId = unit.name.mid(prefix.length(), unit.name.length() - prefix.length() - suffix.length()); if (sessionId.isEmpty() || m_sessions.contains(sessionId)) { continue; } QString gameId; if (unit.description.startsWith(descPrefix)) { gameId = unit.description.mid(descPrefix.length()); } Session session; session.sessionId = sessionId; session.gameId = gameId; session.unitName = unit.name; session.unitPath = unit.objectPath; const QList pids = m_systemd.scopePids(unit.objectPath); if (!pids.isEmpty()) { session.mainPid = pids.first(); } m_sessions.insert(sessionId, session); watchSystemdUnit(sessionId, session.unitName, session.unitPath); Q_EMIT SessionAdded(sessionToVariantMap(session, QStringLiteral("Running"))); } } #include "gamecenterdaemon.moc"