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.
This commit is contained in:
Marco Allegretti 2026-02-14 16:16:59 +01:00
parent d56b91dbd0
commit 985f6dac03

View file

@ -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<uint> 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<uint> 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<int, QProcess::ExitStatus>(&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<QDBusObjectPath> getUnitReply = m_systemd.getUnit(it.value().unitName);
if (getUnitReply.isValid()) {
unitPath = getUnitReply.value();
it.value().unitPath = unitPath;
watchSystemdUnit(sessionId, it.value().unitName, unitPath);
}
}
QList<uint> 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;