// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2026 A-La-Karte Contributors #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { static QString takeArgValue(const QStringList &args, const QString &key) { const int idx = args.indexOf(key); if (idx < 0) { return {}; } if (idx + 1 >= args.size()) { return {}; } return args.at(idx + 1); } static bool waitForService(QDBusConnection &bus, const QString &service, int timeoutMs) { if (!bus.isConnected() || !bus.interface()) { return false; } QElapsedTimer t; t.start(); while (t.elapsed() < timeoutMs) { if (bus.interface()->isServiceRegistered(service)) { return true; } QTest::qWait(50); } return false; } static QVariant unwrapDbusVariant(QVariant v) { if (v.canConvert()) { v = v.value().variant(); } return v; } static QVariantMap unwrapVariantMap(QVariant v) { v = unwrapDbusVariant(v); if (v.canConvert()) { const QDBusArgument arg = v.value(); return qdbus_cast(arg); } if (v.canConvert()) { return v.toMap(); } return {}; } static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536) { QFile f(path); if (!f.open(QIODevice::ReadOnly)) { return {}; } return f.read(maxSize); } 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 pidsWithEnvironEntry(const QByteArray &needle) { QList out; 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) { continue; } const QByteArray env = readProcFile(QStringLiteral("/proc/%1/environ").arg(pid)); if (containsNullSeparatedEntry(env, needle)) { out.push_back(pid); } } return out; } static bool waitForAnyEnvironEntry(const QByteArray &needle, int timeoutMs) { QElapsedTimer t; t.start(); while (t.elapsed() < timeoutMs) { if (!pidsWithEnvironEntry(needle).isEmpty()) { return true; } QTest::qWait(50); } return !pidsWithEnvironEntry(needle).isEmpty(); } static bool waitForNoEnvironEntry(const QByteArray &needle, int timeoutMs) { QElapsedTimer t; t.start(); while (t.elapsed() < timeoutMs) { if (pidsWithEnvironEntry(needle).isEmpty()) { return true; } QTest::qWait(150); } return pidsWithEnvironEntry(needle).isEmpty(); } static void killPids(const QList &pids) { for (uint pid : pids) { if (pid > 1) { ::kill(static_cast(pid), SIGKILL); } } } static void stopProcess(QProcess &p) { if (p.state() == QProcess::NotRunning) { return; } p.terminate(); if (!p.waitForFinished(3000)) { p.kill(); p.waitForFinished(3000); } } static bool waitForUnitNotActive(const QString &unitName, int timeoutMs) { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { return false; } QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"), QStringLiteral("/org/freedesktop/systemd1"), QStringLiteral("org.freedesktop.systemd1.Manager"), bus); if (!manager.isValid()) { return false; } QElapsedTimer t; t.start(); while (t.elapsed() < timeoutMs) { const QDBusReply unitReply = manager.call(QStringLiteral("GetUnit"), unitName); if (!unitReply.isValid()) { if (unitReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) { return true; } QTest::qWait(100); continue; } QDBusInterface props(QStringLiteral("org.freedesktop.systemd1"), unitReply.value().path(), QStringLiteral("org.freedesktop.DBus.Properties"), bus); if (!props.isValid()) { return true; } const QDBusReply stateReply = props.call(QStringLiteral("Get"), QStringLiteral("org.freedesktop.systemd1.Unit"), QStringLiteral("ActiveState")); if (!stateReply.isValid()) { return true; } QVariant v = stateReply.value(); if (v.canConvert()) { v = v.value().variant(); } const QString state = v.toString(); if (state != QLatin1String("active") && state != QLatin1String("activating") && state != QLatin1String("deactivating")) { return true; } QTest::qWait(100); } const QDBusReply unitReply = manager.call(QStringLiteral("GetUnit"), unitName); if (!unitReply.isValid() && unitReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) { return true; } return false; } } class StopLaunchingRegressionTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void stopWhileLaunchingSteam(); void stopByGameIdWhileLaunchingSteam(); void stopWhileLaunchingLutris(); void stopByGameIdWhileLaunchingLutris(); void stopDirectLaunch(); void stopByGameIdDirectLaunch(); void recoverSessionsAfterDaemonRestart(); private: QString m_gamecenterPath; QProcess m_gamecenter; QString m_testXdgBase; QTemporaryDir m_stubBin; QString m_markerValue; }; void StopLaunchingRegressionTest::initTestCase() { m_gamecenterPath = qApp->property("alakarte_test_gamecenter").toString(); if (m_gamecenterPath.isEmpty()) { const QStringList args = QCoreApplication::arguments(); m_gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter")); } QVERIFY2(!m_gamecenterPath.isEmpty(), "--gamecenter is required"); QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected() || !bus.interface()) { QSKIP("session bus not available"); } QDBusInterface systemd(QStringLiteral("org.freedesktop.systemd1"), QStringLiteral("/org/freedesktop/systemd1"), QStringLiteral("org.freedesktop.systemd1.Manager"), bus); if (!systemd.isValid()) { QSKIP("systemd user manager not available on session bus"); } if (bus.interface()->isServiceRegistered(QStringLiteral("org.kde.GameCenter1"))) { QSKIP("org.kde.GameCenter1 already registered on the session bus"); } if (!m_stubBin.isValid()) { QSKIP("failed to create temporary directory for stub executables"); } m_markerValue = QUuid::createUuid().toString(QUuid::WithoutBraces); const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8(); killPids(pidsWithEnvironEntry(markerNeedle)); m_testXdgBase = QDir::tempPath() + QStringLiteral("/alakarte-stop-launching-test-xdg-") + QUuid::createUuid().toString(QUuid::WithoutBraces); QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/data"))); QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/config"))); QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/cache"))); const QString steamPath = m_stubBin.path() + QStringLiteral("/steam"); { QFile steam(steamPath); QVERIFY(steam.open(QIODevice::WriteOnly | QIODevice::Truncate)); const QByteArray script = "#!/bin/sh\n" "appid=\"\"\n" "while [ $# -gt 0 ]; do\n" " if [ \"$1\" = \"-applaunch\" ] && [ $# -ge 2 ]; then appid=\"$2\"; break; fi\n" " shift\n" "done\n" "if [ -z \"$appid\" ]; then appid=\"123456\"; fi\n" "marker=\"${ALAKARTE_TEST_MARKER_VALUE}\"\n" "nohup sh -c \"sleep 1; env SteamAppId=$appid SteamGameId=$appid ALAKARTE_TEST_MARKER=$marker /bin/sleep 60\" >/dev/null 2>&1 &\n" "exit 0\n"; QVERIFY(steam.write(script) == script.size()); } QVERIFY(QFile::setPermissions(steamPath, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | QFileDevice::ReadGroup | QFileDevice::ExeGroup | QFileDevice::ReadOther | QFileDevice::ExeOther)); const QString lutrisPath = m_stubBin.path() + QStringLiteral("/lutris"); { QFile lutris(lutrisPath); QVERIFY(lutris.open(QIODevice::WriteOnly | QIODevice::Truncate)); const QByteArray script = "#!/bin/sh\n" "id=\"\"\n" "if [ $# -ge 1 ]; then\n" " id=\"$1\"\n" "fi\n" "case \"$id\" in\n" " lutris:rungameid/*) id=\"${id##*/}\" ;;\n" " lutris:rungame/*) id=\"${id##*/}\" ;;\n" "esac\n" "if [ -z \"$id\" ]; then id=\"test-lutris\"; fi\n" "marker=\"${ALAKARTE_TEST_MARKER_VALUE}\"\n" "nohup sh -c \"sleep 1; env LUTRIS_GAME_SLUG=$id ALAKARTE_TEST_MARKER=$marker /bin/sleep 60\" >/dev/null 2>&1 &\n" "exit 0\n"; QVERIFY(lutris.write(script) == script.size()); } QVERIFY(QFile::setPermissions(lutrisPath, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | QFileDevice::ReadGroup | QFileDevice::ExeGroup | QFileDevice::ReadOther | QFileDevice::ExeOther)); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); env.insert(QStringLiteral("PATH"), m_stubBin.path() + QStringLiteral(":") + env.value(QStringLiteral("PATH"))); env.insert(QStringLiteral("ALAKARTE_TEST_MARKER_VALUE"), m_markerValue); env.insert(QStringLiteral("XDG_DATA_HOME"), m_testXdgBase + QStringLiteral("/data")); env.insert(QStringLiteral("XDG_CONFIG_HOME"), m_testXdgBase + QStringLiteral("/config")); env.insert(QStringLiteral("XDG_CACHE_HOME"), m_testXdgBase + QStringLiteral("/cache")); m_gamecenter.setProcessEnvironment(env); m_gamecenter.setProgram(m_gamecenterPath); m_gamecenter.setArguments({}); m_gamecenter.setProcessChannelMode(QProcess::MergedChannels); m_gamecenter.start(); QVERIFY(m_gamecenter.waitForStarted(5000)); QVERIFY2(waitForService(bus, QStringLiteral("org.kde.GameCenter1"), 5000), "GameCenter1 service did not appear on the session bus"); } void StopLaunchingRegressionTest::cleanupTestCase() { stopProcess(m_gamecenter); if (!m_markerValue.isEmpty()) { const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8(); killPids(pidsWithEnvironEntry(markerNeedle)); m_markerValue.clear(); } if (!m_testXdgBase.isEmpty()) { QDir(m_testXdgBase).removeRecursively(); m_testXdgBase.clear(); } } void StopLaunchingRegressionTest::recoverSessionsAfterDaemonRestart() { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { QSKIP("session bus not available"); } QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus); QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid"); const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8(); killPids(pidsWithEnvironEntry(markerNeedle)); const QString envExe = QStandardPaths::findExecutable(QStringLiteral("env")); if (envExe.isEmpty()) { QSKIP("env executable not found"); } const QString gameId = QStringLiteral("test-reattach-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces)); QVariantMap spec; spec.insert(QStringLiteral("provider"), QStringLiteral("manual")); spec.insert(QStringLiteral("program"), envExe); spec.insert(QStringLiteral("args"), QStringList{QStringLiteral("ALAKARTE_TEST_MARKER=%1").arg(m_markerValue), QStringLiteral("/bin/sleep"), QStringLiteral("60")}); spec.insert(QStringLiteral("gameId"), gameId); const QDBusReply launchReply = iface.call(QStringLiteral("Launch"), spec); QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message())); const QString sessionId = launchReply.value(); QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId"); const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId); QVERIFY2(waitForAnyEnvironEntry(markerNeedle, 5000), "direct launch marker process did not appear"); m_gamecenter.kill(); QVERIFY(m_gamecenter.waitForFinished(5000)); QVERIFY2(waitForNoEnvironEntry(markerNeedle, 1000) == false, "marker process unexpectedly disappeared after daemon kill"); m_gamecenter.start(); QVERIFY(m_gamecenter.waitForStarted(5000)); QVERIFY2(waitForService(bus, QStringLiteral("org.kde.GameCenter1"), 5000), "GameCenter1 service did not re-appear after restart"); QDBusInterface iface2(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus); QVERIFY2(iface2.isValid(), "GameCenter1 DBus interface not valid after restart"); const QDBusReply listReply = iface2.call(QStringLiteral("ListSessions")); QVERIFY2(listReply.isValid(), qPrintable(listReply.error().message())); bool found = false; for (const QVariant &v : listReply.value()) { const QVariantMap m = unwrapVariantMap(v); if (m.value(QStringLiteral("sessionId")).toString() == sessionId) { found = true; QCOMPARE(m.value(QStringLiteral("gameId")).toString(), gameId); QCOMPARE(m.value(QStringLiteral("state")).toString(), QStringLiteral("Running")); break; } } QVERIFY2(found, "expected recovered session not found in ListSessions() after restart"); const QDBusReply stopReply = iface2.call(QStringLiteral("Stop"), sessionId); QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message())); const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000); if (!cleaned) { killPids(pidsWithEnvironEntry(markerNeedle)); } QVERIFY2(cleaned, "marker process still alive after Stop() on recovered session"); QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() on recovered session"); } void StopLaunchingRegressionTest::stopWhileLaunchingSteam() { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { QSKIP("session bus not available"); } QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus); QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid"); const QString appId = QStringLiteral("987654"); QVariantMap spec; spec.insert(QStringLiteral("provider"), QStringLiteral("steam")); spec.insert(QStringLiteral("command"), QStringLiteral("steam -applaunch %1").arg(appId)); spec.insert(QStringLiteral("gameId"), QStringLiteral("test-steam-%1").arg(appId)); QDBusReply launchReply = iface.call(QStringLiteral("Launch"), spec); QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message())); const QString sessionId = launchReply.value(); QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId"); const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId); QDBusReply stopReply = iface.call(QStringLiteral("Stop"), sessionId); QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message())); const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8(); waitForAnyEnvironEntry(markerNeedle, 5000); const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000); if (!cleaned) { const QList pids = pidsWithEnvironEntry(markerNeedle); killPids(pids); } QVERIFY2(cleaned, "marker process still alive after Stop() during Launching"); QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() during Launching"); } void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingSteam() { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { QSKIP("session bus not available"); } QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus); QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid"); const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8(); killPids(pidsWithEnvironEntry(markerNeedle)); const QString appId = QStringLiteral("876543"); const QString gameId = QStringLiteral("test-steam-stop-by-gameid-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces)); QVariantMap spec; spec.insert(QStringLiteral("provider"), QStringLiteral("steam")); spec.insert(QStringLiteral("command"), QStringLiteral("steam -applaunch %1").arg(appId)); spec.insert(QStringLiteral("gameId"), gameId); QDBusReply launchReply = iface.call(QStringLiteral("Launch"), spec); QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message())); const QString sessionId = launchReply.value(); QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId"); const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId); QDBusReply stopReply = iface.call(QStringLiteral("StopByGameId"), gameId); QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message())); waitForAnyEnvironEntry(markerNeedle, 5000); const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000); if (!cleaned) { const QList pids = pidsWithEnvironEntry(markerNeedle); killPids(pids); } QVERIFY2(cleaned, "marker process still alive after StopByGameId() during Launching"); QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after StopByGameId() during Launching"); } void StopLaunchingRegressionTest::stopWhileLaunchingLutris() { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { QSKIP("session bus not available"); } QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus); QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid"); const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8(); killPids(pidsWithEnvironEntry(markerNeedle)); const QString lutrisId = QStringLiteral("rungameid/%1").arg(QStringLiteral("lutris-test-1")); QVariantMap spec; spec.insert(QStringLiteral("provider"), QStringLiteral("lutris")); spec.insert(QStringLiteral("command"), QStringLiteral("lutris lutris:%1").arg(lutrisId)); spec.insert(QStringLiteral("gameId"), QStringLiteral("test-lutris-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces))); QDBusReply launchReply = iface.call(QStringLiteral("Launch"), spec); QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message())); const QString sessionId = launchReply.value(); QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId"); const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId); QDBusReply stopReply = iface.call(QStringLiteral("Stop"), sessionId); QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message())); waitForAnyEnvironEntry(markerNeedle, 5000); const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000); if (!cleaned) { killPids(pidsWithEnvironEntry(markerNeedle)); } QVERIFY2(cleaned, "marker process still alive after Stop() during Launching (lutris)"); QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() during Launching (lutris)"); } void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingLutris() { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { QSKIP("session bus not available"); } QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus); QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid"); const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8(); killPids(pidsWithEnvironEntry(markerNeedle)); const QString gameId = QStringLiteral("test-lutris-stop-by-gameid-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces)); QVariantMap spec; spec.insert(QStringLiteral("provider"), QStringLiteral("lutris")); spec.insert(QStringLiteral("command"), QStringLiteral("lutris lutris:rungameid/%1").arg(QStringLiteral("lutris-test-2"))); spec.insert(QStringLiteral("gameId"), gameId); QDBusReply launchReply = iface.call(QStringLiteral("Launch"), spec); QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message())); const QString sessionId = launchReply.value(); QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId"); const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId); QDBusReply stopReply = iface.call(QStringLiteral("StopByGameId"), gameId); QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message())); waitForAnyEnvironEntry(markerNeedle, 5000); const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000); if (!cleaned) { killPids(pidsWithEnvironEntry(markerNeedle)); } QVERIFY2(cleaned, "marker process still alive after StopByGameId() during Launching (lutris)"); QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after StopByGameId() during Launching (lutris)"); } void StopLaunchingRegressionTest::stopDirectLaunch() { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { QSKIP("session bus not available"); } QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus); QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid"); const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8(); killPids(pidsWithEnvironEntry(markerNeedle)); const QString gameId = QStringLiteral("test-direct-stop-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces)); const QString envExe = QStandardPaths::findExecutable(QStringLiteral("env")); if (envExe.isEmpty()) { QSKIP("env executable not found"); } QVariantMap spec; spec.insert(QStringLiteral("provider"), QStringLiteral("manual")); spec.insert(QStringLiteral("program"), envExe); spec.insert(QStringLiteral("args"), QStringList{QStringLiteral("ALAKARTE_TEST_MARKER=%1").arg(m_markerValue), QStringLiteral("/bin/sleep"), QStringLiteral("60")}); spec.insert(QStringLiteral("gameId"), gameId); QDBusReply launchReply = iface.call(QStringLiteral("Launch"), spec); QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message())); const QString sessionId = launchReply.value(); QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId"); const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId); QVERIFY2(waitForAnyEnvironEntry(markerNeedle, 5000), "direct launch marker process did not appear"); QDBusReply stopReply = iface.call(QStringLiteral("Stop"), sessionId); QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message())); const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000); if (!cleaned) { killPids(pidsWithEnvironEntry(markerNeedle)); } QVERIFY2(cleaned, "marker process still alive after Stop() (direct launch)"); QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() (direct launch)"); } void StopLaunchingRegressionTest::stopByGameIdDirectLaunch() { QDBusConnection bus = QDBusConnection::sessionBus(); if (!bus.isConnected()) { QSKIP("session bus not available"); } QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus); QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid"); const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8(); killPids(pidsWithEnvironEntry(markerNeedle)); const QString gameId = QStringLiteral("test-direct-stop-by-gameid-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces)); const QString envExe = QStandardPaths::findExecutable(QStringLiteral("env")); if (envExe.isEmpty()) { QSKIP("env executable not found"); } QVariantMap spec; spec.insert(QStringLiteral("provider"), QStringLiteral("manual")); spec.insert(QStringLiteral("program"), envExe); spec.insert(QStringLiteral("args"), QStringList{QStringLiteral("ALAKARTE_TEST_MARKER=%1").arg(m_markerValue), QStringLiteral("/bin/sleep"), QStringLiteral("60")}); spec.insert(QStringLiteral("gameId"), gameId); QDBusReply launchReply = iface.call(QStringLiteral("Launch"), spec); QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message())); const QString sessionId = launchReply.value(); QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId"); const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId); QVERIFY2(waitForAnyEnvironEntry(markerNeedle, 5000), "direct launch marker process did not appear"); QDBusReply stopReply = iface.call(QStringLiteral("StopByGameId"), gameId); QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message())); const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000); if (!cleaned) { killPids(pidsWithEnvironEntry(markerNeedle)); } QVERIFY2(cleaned, "marker process still alive after StopByGameId() (direct launch)"); QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after StopByGameId() (direct launch)"); } int main(int argc, char **argv) { QCoreApplication app(argc, argv); const QStringList args = app.arguments(); const QString gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter")); app.setProperty("alakarte_test_gamecenter", gamecenterPath); QStringList filtered; filtered.reserve(args.size()); if (!args.isEmpty()) { filtered.push_back(args.first()); } for (int i = 1; i < args.size(); ++i) { const QString a = args.at(i); if (a == QLatin1String("--gamecenter")) { ++i; continue; } filtered.push_back(a); } StopLaunchingRegressionTest tc; return QTest::qExec(&tc, filtered); } #include "stop_launching_regression_test.moc"