From 985f6dac03a98808873fbb7e17145690924c7830 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Sat, 14 Feb 2026 16:16:59 +0100 Subject: [PATCH] Harden transient scope lifecycle Improve GameCenter daemon handling of systemd transient scopes. - Watch monitored-launch scopes early and avoid dropping Launching sessions when the unit disappears before PIDs are attached. - Stop leaked scopes on AttachProcessesToUnit failures. - Make Stop() resilient for recovered sessions by falling back to TERM/KILL on scope PIDs when StopUnit fails unexpectedly. - Only recover scopes that match the expected A-La-Karte description. --- src/gamecenter/gamecenterdaemon.cpp | 71 ++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/gamecenter/gamecenterdaemon.cpp b/src/gamecenter/gamecenterdaemon.cpp index 933001c..b3678dd 100644 --- a/src/gamecenter/gamecenterdaemon.cpp +++ b/src/gamecenter/gamecenterdaemon.cpp @@ -803,6 +803,18 @@ void GameCenterDaemon::handleSystemdUnitRemoved(const QString &unitName, const Q return; } + if (sit.value().scanner) { + if (!sit.value().unitPath.path().isEmpty()) { + const QString unitPathKey = sit.value().unitPath.path(); + m_unitPathToSessionId.remove(unitPathKey); + if (QObject *watcher = m_unitPathWatchers.take(unitPathKey)) { + watcher->deleteLater(); + } + } + sit.value().unitPath = {}; + return; + } + unwatchSystemdUnit(sit.value().unitName, sit.value().unitPath); removeSessionInternal(sessionId, sit.value().stopping ? QStringLiteral("Stopped") : QStringLiteral("Exited")); } @@ -833,6 +845,25 @@ void GameCenterDaemon::handleSystemdUnitPropertiesChanged(const QDBusObjectPath } const QString activeState = activeStateV.toString(); + if (sit.value().scanner) { + 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(); + } + } else { + const QString unitPathKey = unitPath.path(); + m_unitPathToSessionId.remove(unitPathKey); + if (QObject *watcher = m_unitPathWatchers.take(unitPathKey)) { + watcher->deleteLater(); + } + sit.value().unitPath = {}; + } + + Q_EMIT SessionChanged(sessionToVariantMap(sit.value(), sessionState(sit.value()))); + return; + } + if (activeState == QLatin1String("active") || activeState == QLatin1String("activating") || activeState == QLatin1String("deactivating")) { const QList pids = m_systemd.scopePids(unitPath); if (!pids.isEmpty()) { @@ -1495,6 +1526,8 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q m_sessions[sessionId] = session; + watchSystemdUnit(sessionId, session.unitName, session.unitPath); + connect(bootstrap, qOverload(&QProcess::finished), this, [this, sessionId](int, QProcess::ExitStatus) { const auto it = m_sessions.find(sessionId); if (it == m_sessions.end()) { @@ -1609,6 +1642,11 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList {QStringLiteral("error"), attachReply.error().message()}, }; + if (!it.value().unitName.isEmpty()) { + m_systemd.stopUnit(it.value().unitName); + unwatchSystemdUnit(it.value().unitName, it.value().unitPath); + } + QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed")); if (it.value().process) { it.value().process->deleteLater(); @@ -1780,6 +1818,32 @@ void GameCenterDaemon::Stop(const QString &sessionId) return; } + QDBusObjectPath unitPath = it.value().unitPath; + if (unitPath.path().isEmpty()) { + const QDBusReply getUnitReply = m_systemd.getUnit(it.value().unitName); + if (getUnitReply.isValid()) { + unitPath = getUnitReply.value(); + it.value().unitPath = unitPath; + watchSystemdUnit(sessionId, it.value().unitName, unitPath); + } + } + + QList pids; + if (!unitPath.path().isEmpty()) { + pids = m_systemd.scopePids(unitPath); + } + if (pids.isEmpty() && it.value().mainPid > 0) { + pids = {it.value().mainPid}; + } + if (!pids.isEmpty()) { + terminatePids(pids); + QTimer::singleShot(5000, this, [pids]() { + killPids(pids); + }); + Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping"))); + 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]() { @@ -1886,7 +1950,8 @@ void GameCenterDaemon::recoverExistingSessions() static const QString prefix = QStringLiteral("alakarte-game-"); static const QString suffix = QStringLiteral(".scope"); - static const QString descPrefix = QStringLiteral("A-La-Karte game "); + static const QString descBase = QStringLiteral("A-La-Karte game"); + static const QString descPrefix = descBase + QLatin1Char(' '); const SystemdUnitInfoList units = reply.value(); for (const SystemdUnitInfo &unit : units) { @@ -1897,6 +1962,10 @@ void GameCenterDaemon::recoverExistingSessions() continue; } + if (unit.description != descBase && !unit.description.startsWith(descPrefix)) { + continue; + } + const QString sessionId = unit.name.mid(prefix.length(), unit.name.length() - prefix.length() - suffix.length()); if (sessionId.isEmpty() || m_sessions.contains(sessionId)) { continue;