From 90bb30416ceb3876edf68a78beae6573c786edad Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Mon, 23 Mar 2026 13:14:09 +0100 Subject: [PATCH] Convert all blocking D-Bus calls in launcher to async --- src/gamelauncher.cpp | 426 +++++++++++++++++++++++++++++-------------- src/gamelauncher.h | 1 + 2 files changed, 293 insertions(+), 134 deletions(-) diff --git a/src/gamelauncher.cpp b/src/gamelauncher.cpp index 156a183..3bea3e3 100644 --- a/src/gamelauncher.cpp +++ b/src/gamelauncher.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -28,22 +29,6 @@ static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1" static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1"); static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1"); -static bool pingDaemon(QDBusConnection bus) -{ - if (!bus.isConnected()) { - return false; - } - org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, bus); - if (!iface.isValid()) { - return false; - } - - iface.setTimeout(2000); - QDBusPendingReply reply = iface.Ping(); - reply.waitForFinished(); - return !reply.isError() && reply.value() == QLatin1String("ok"); -} - static void disconnectDaemonSignals(QDBusConnection bus, GameLauncher *launcher) { if (!bus.isConnected()) { @@ -577,89 +562,15 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const return info; } -void GameLauncher::launchGame(Game *game) +void GameLauncher::dispatchLaunch(Game *game, const QVariantMap &launchSpec) { - if (!game) { - return; - } - - // Check if already running - if (m_daemonGameToSession.contains(game->id())) { - Q_EMIT gameError(game, tr("Game is already running")); - return; - } - - const QVariantMap info = resolveLaunchInfo(game); - if (!info.value(QStringLiteral("ok")).toBool()) { - Q_EMIT gameError(game, info.value(QStringLiteral("error")).toString()); - return; - } - - const QString launchCommand = info.value(QStringLiteral("launchCommand")).toString(); - const QString launchType = info.value(QStringLiteral("launchType")).toString(); - const QString provider = info.value(QStringLiteral("provider")).toString(); - - const QString runner = info.value(QStringLiteral("runner")).toString(); - const QString runnerId = info.value(QStringLiteral("runnerId")).toString(); - const QString runnerPath = info.value(QStringLiteral("runnerPath")).toString(); - const QString prefixPath = info.value(QStringLiteral("prefixPath")).toString(); - const QVariantMap envOverrides = info.value(QStringLiteral("envOverrides")).toMap(); - if (runner == QLatin1String("proton") || runner == QLatin1String("wine")) { - const QString resolvedPrefixPath = info.value(QStringLiteral("resolvedPrefixPath")).toString(); - if (!resolvedPrefixPath.isEmpty()) { - QDir().mkpath(resolvedPrefixPath); - } - } - - const QString finalProgram = info.value(QStringLiteral("finalProgram")).toString(); - const QStringList finalArgs = info.value(QStringLiteral("finalArgs")).toStringList(); - const QString workingDirectory = info.value(QStringLiteral("workingDirectory")).toString(); - const QVariantMap effectiveEnv = info.value(QStringLiteral("effectiveEnv")).toMap(); - - // Always try daemon first — for all launch types - { - org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus()); - QVariantMap launchSpec = { - {QStringLiteral("command"), launchCommand}, - {QStringLiteral("gameId"), game->id()}, - {QStringLiteral("displayName"), game->name()}, - {QStringLiteral("provider"), provider}, - {QStringLiteral("origin"), QStringLiteral("ui")}, - }; - - if (!runnerId.isEmpty()) { - launchSpec.insert(QStringLiteral("runnerId"), runnerId); - } - if (!runner.isEmpty()) { - launchSpec.insert(QStringLiteral("runner"), runner); - } - if (!runnerPath.isEmpty()) { - launchSpec.insert(QStringLiteral("runnerPath"), runnerPath); - } - if (!prefixPath.isEmpty()) { - launchSpec.insert(QStringLiteral("prefixPath"), prefixPath); - } - - launchSpec.insert(QStringLiteral("requestedProgram"), info.value(QStringLiteral("program")).toString()); - launchSpec.insert(QStringLiteral("requestedArgs"), info.value(QStringLiteral("args")).toStringList()); - if (!envOverrides.isEmpty()) { - launchSpec.insert(QStringLiteral("requestedEnvOverrides"), envOverrides); - } - - if (!finalProgram.isEmpty()) { - launchSpec.insert(QStringLiteral("program"), finalProgram); - launchSpec.insert(QStringLiteral("args"), finalArgs); - } - if (!effectiveEnv.isEmpty()) { - launchSpec.insert(QStringLiteral("envOverrides"), effectiveEnv); - } - if (!workingDirectory.isEmpty()) { - launchSpec.insert(QStringLiteral("workingDirectory"), workingDirectory); - } - - iface.setTimeout(5000); - QDBusPendingReply reply = iface.Launch(launchSpec); - reply.waitForFinished(); + auto *iface = new org::kde::GameCenter1(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus(), this); + iface->setTimeout(5000); + auto *watcher = new QDBusPendingCallWatcher(iface->Launch(launchSpec), this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, game, iface, watcher]() { + watcher->deleteLater(); + iface->deleteLater(); + QDBusPendingReply reply = *watcher; if (!reply.isError() && !reply.value().isEmpty()) { m_daemonGameToSession.insert(game->id(), reply.value()); m_daemonSessionToGame.insert(reply.value(), game->id()); @@ -667,22 +578,255 @@ void GameLauncher::launchGame(Game *game) game->setLastPlayed(QDateTime::currentDateTime()); Q_EMIT gameStarted(game); Q_EMIT runningGamesChanged(); - if (App::instance()->config()->exitAfterLaunch()) { QTimer::singleShot(500, qApp, &QCoreApplication::quit); } return; } - const QString launchError = reply.isError() ? reply.error().message() : QString(); - - // No fallback for non-URL commands — emit error if (!launchError.isEmpty()) { Q_EMIT gameError(game, tr("Game Center launch failed: %1").arg(launchError)); } else { Q_EMIT gameError(game, tr("Game Center daemon is not available")); } + }); +} + +void GameLauncher::launchGame(Game *game) +{ + if (!game) { + return; } + + if (m_daemonGameToSession.contains(game->id())) { + Q_EMIT gameError(game, tr("Game is already running")); + return; + } + + const QString command = game->launchCommand(); + if (command.isEmpty()) { + Q_EMIT gameError(game, tr("No launch command configured")); + return; + } + + const QString platform = game->platform().toLower(); + QString provider; + if (platform == QLatin1String("steam")) { + provider = QStringLiteral("steam"); + } else if (platform == QLatin1String("lutris")) { + provider = QStringLiteral("lutris"); + } else { + provider = QStringLiteral("manual"); + } + + const QString runner = game->launchRunner().trimmed(); + const QString runnerId = game->launchRunnerId().trimmed(); + const QString runnerPath = game->launchRunnerPath().trimmed(); + const QString prefixPath = game->launchPrefixPath().trimmed(); + const bool hasLaunchOverrides = !runner.isEmpty() || !runnerId.isEmpty() || !runnerPath.isEmpty() || !prefixPath.isEmpty() || !game->launchEnv().isEmpty(); + + QVariantMap envOverrides; + const QVariantMap launchEnv = game->launchEnv(); + for (auto it = launchEnv.constBegin(); it != launchEnv.constEnd(); ++it) { + const QString key = it.key(); + if (!key.isEmpty() && !key.contains(QLatin1Char('='))) { + envOverrides.insert(key, it.value().toString()); + } + } + + if (!hasLaunchOverrides && command.startsWith(QLatin1String("steam://"))) { + QVariantMap launchSpec = {{QStringLiteral("command"), command}, + {QStringLiteral("gameId"), game->id()}, + {QStringLiteral("displayName"), game->name()}, + {QStringLiteral("provider"), QStringLiteral("steam")}, + {QStringLiteral("origin"), QStringLiteral("ui")}}; + dispatchLaunch(game, launchSpec); + return; + } + if (!hasLaunchOverrides && command.startsWith(QLatin1String("lutris "))) { + QVariantMap launchSpec = {{QStringLiteral("command"), command}, + {QStringLiteral("gameId"), game->id()}, + {QStringLiteral("displayName"), game->name()}, + {QStringLiteral("provider"), QStringLiteral("lutris")}, + {QStringLiteral("origin"), QStringLiteral("ui")}}; + dispatchLaunch(game, launchSpec); + return; + } + + QStringList parts; + if (command.startsWith(QLatin1String("steam://"))) { + parts = {QStringLiteral("xdg-open"), command}; + provider = QStringLiteral("steam"); + } else if (command.startsWith(QLatin1String("lutris "))) { + parts = {QStringLiteral("xdg-open"), command.mid(7)}; + provider = QStringLiteral("lutris"); + } else { + parts = QProcess::splitCommand(command); + } + if (parts.isEmpty()) { + Q_EMIT gameError(game, tr("Invalid launch command")); + return; + } + const QString program = parts.takeFirst(); + const QStringList args = parts; + + if ((runner == QLatin1String("wine") || runner == QLatin1String("proton")) && (program == QLatin1String("xdg-open"))) { + Q_EMIT gameError(game, tr("This runner cannot be used with URL-based launch commands")); + return; + } + + if (!runnerId.isEmpty()) { + QVariantMap runnerSpec = {{QStringLiteral("runnerId"), runnerId}, + {QStringLiteral("runner"), runner}, + {QStringLiteral("runnerPath"), runnerPath}, + {QStringLiteral("gameId"), game->id()}, + {QStringLiteral("prefixPath"), prefixPath}, + {QStringLiteral("program"), program}, + {QStringLiteral("args"), args}, + {QStringLiteral("envOverrides"), envOverrides}, + {QStringLiteral("useGameProfile"), true}}; + + QDBusConnection bus = QDBusConnection::sessionBus(); + if (bus.isConnected() && bus.interface() && !bus.interface()->isServiceRegistered(kRunnerService)) { + bus.interface()->startService(kRunnerService); + } + + auto *runnerIface = new org::kde::ALaKarte::Runner1(kRunnerService, kRunnerPath, QDBusConnection::sessionBus(), this); + runnerIface->setTimeout(2000); + auto *watcher = new QDBusPendingCallWatcher(runnerIface->ResolveLaunch(runnerSpec), this); + connect(watcher, + &QDBusPendingCallWatcher::finished, + this, + [this, game, command, provider, runner, runnerId, runnerPath, prefixPath, program, args, envOverrides, runnerIface, watcher]() { + watcher->deleteLater(); + runnerIface->deleteLater(); + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + Q_EMIT gameError(game, tr("Runner service is not available")); + return; + } + const QVariantMap resolved = unwrapVariantMap(reply.value()); + if (!resolved.value(QStringLiteral("ok")).toBool()) { + Q_EMIT gameError(game, resolved.value(QStringLiteral("error")).toString()); + return; + } + const QString finalProgram = resolved.value(QStringLiteral("finalProgram")).toString(); + const QStringList finalArgs = resolved.value(QStringLiteral("finalArgs")).toStringList(); + const QVariantMap effectiveEnv = resolved.value(QStringLiteral("effectiveEnv")).toMap(); + const QString resolvedPrefixPath = resolved.value(QStringLiteral("resolvedPrefixPath")).toString(); + if (!resolvedPrefixPath.isEmpty()) { + QDir().mkpath(resolvedPrefixPath); + } + QVariantMap launchSpec = {{QStringLiteral("command"), command}, + {QStringLiteral("gameId"), game->id()}, + {QStringLiteral("displayName"), game->name()}, + {QStringLiteral("provider"), provider}, + {QStringLiteral("origin"), QStringLiteral("ui")}, + {QStringLiteral("runnerId"), runnerId}}; + if (!runner.isEmpty()) + launchSpec.insert(QStringLiteral("runner"), runner); + if (!runnerPath.isEmpty()) + launchSpec.insert(QStringLiteral("runnerPath"), runnerPath); + if (!prefixPath.isEmpty()) + launchSpec.insert(QStringLiteral("prefixPath"), prefixPath); + launchSpec.insert(QStringLiteral("requestedProgram"), program); + launchSpec.insert(QStringLiteral("requestedArgs"), args); + if (!envOverrides.isEmpty()) + launchSpec.insert(QStringLiteral("requestedEnvOverrides"), envOverrides); + if (!finalProgram.isEmpty()) { + launchSpec.insert(QStringLiteral("program"), finalProgram); + launchSpec.insert(QStringLiteral("args"), finalArgs); + } + if (!effectiveEnv.isEmpty()) + launchSpec.insert(QStringLiteral("envOverrides"), effectiveEnv); + dispatchLaunch(game, launchSpec); + }); + return; + } + + QString finalProgram = program; + QStringList finalArgs = args; + QVariantMap effectiveEnv = envOverrides; + QString resolvedPrefixPath; + QString resolvedSteamInstallPath; + + if (runner == QLatin1String("custom")) { + if (runnerPath.isEmpty()) { + Q_EMIT gameError(game, tr("Custom runner is enabled but no runner path is configured")); + return; + } + finalProgram = runnerPath; + finalArgs = QStringList{program} + args; + } else if (runner == QLatin1String("wine")) { + QString wineExe = runnerPath; + if (wineExe.isEmpty()) { + wineExe = QStandardPaths::findExecutable(QStringLiteral("wine")); + } + if (wineExe.isEmpty()) { + Q_EMIT gameError(game, tr("Wine runner is enabled but Wine was not found")); + return; + } + QString winePrefix = prefixPath; + if (winePrefix.isEmpty()) { + winePrefix = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id(); + } + effectiveEnv.insert(QStringLiteral("WINEPREFIX"), winePrefix); + resolvedPrefixPath = winePrefix; + finalProgram = wineExe; + finalArgs = QStringList{program} + args; + } else if (runner == QLatin1String("proton")) { + QString protonExe = runnerPath; + if (protonExe.isEmpty()) { + protonExe = discoverDefaultProtonExecutable(); + } + if (protonExe.isEmpty()) { + Q_EMIT gameError(game, tr("Proton runner is enabled but no Proton installation was found")); + return; + } + const QString steamInstallPath = findSteamClientInstallPathFromProton(protonExe); + resolvedSteamInstallPath = steamInstallPath; + QString compatDataPath = prefixPath; + if (compatDataPath.isEmpty()) { + compatDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes/") + game->id(); + } + effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), compatDataPath); + resolvedPrefixPath = compatDataPath; + if (!steamInstallPath.isEmpty()) { + effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_CLIENT_INSTALL_PATH"), steamInstallPath); + } + finalProgram = protonExe; + finalArgs = QStringList{QStringLiteral("run"), program} + args; + } + + if (!resolvedPrefixPath.isEmpty()) { + QDir().mkpath(resolvedPrefixPath); + } + + QVariantMap launchSpec = {{QStringLiteral("command"), command}, + {QStringLiteral("gameId"), game->id()}, + {QStringLiteral("displayName"), game->name()}, + {QStringLiteral("provider"), provider}, + {QStringLiteral("origin"), QStringLiteral("ui")}}; + if (!runner.isEmpty()) + launchSpec.insert(QStringLiteral("runner"), runner); + if (!runnerPath.isEmpty()) + launchSpec.insert(QStringLiteral("runnerPath"), runnerPath); + if (!prefixPath.isEmpty()) + launchSpec.insert(QStringLiteral("prefixPath"), prefixPath); + launchSpec.insert(QStringLiteral("requestedProgram"), program); + launchSpec.insert(QStringLiteral("requestedArgs"), args); + if (!envOverrides.isEmpty()) + launchSpec.insert(QStringLiteral("requestedEnvOverrides"), envOverrides); + launchSpec.insert(QStringLiteral("program"), finalProgram); + launchSpec.insert(QStringLiteral("args"), finalArgs); + if (!effectiveEnv.isEmpty()) + launchSpec.insert(QStringLiteral("envOverrides"), effectiveEnv); + if (!game->workingDirectory().isEmpty()) + launchSpec.insert(QStringLiteral("workingDirectory"), game->workingDirectory()); + if (!resolvedSteamInstallPath.isEmpty()) + launchSpec.insert(QStringLiteral("resolvedSteamInstallPath"), resolvedSteamInstallPath); + + dispatchLaunch(game, launchSpec); } void GameLauncher::stopGame(Game *game) @@ -836,29 +980,30 @@ void GameLauncher::onDaemonLaunchFailed(const QVariantMap &error) void GameLauncher::syncDaemonSessions() { - org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus()); - iface.setTimeout(2000); - - QDBusPendingReply reply = iface.ListSessions(); - reply.waitForFinished(); - if (reply.isError()) { - return; - } - - const QVariantList list = reply.value(); - for (const QVariant &v : list) { - QVariantMap map; - if (v.canConvert()) { - map = v.toMap(); - } else if (v.canConvert()) { - map = qdbus_cast(v.value()); + auto *iface = new org::kde::GameCenter1(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus(), this); + iface->setTimeout(2000); + auto *watcher = new QDBusPendingCallWatcher(iface->ListSessions(), this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, iface, watcher]() { + watcher->deleteLater(); + iface->deleteLater(); + QDBusPendingReply reply = *watcher; + if (reply.isError()) { + return; } - if (!map.isEmpty()) { - onDaemonSessionAdded(map); + const QVariantList list = reply.value(); + for (const QVariant &v : list) { + QVariantMap map; + if (v.canConvert()) { + map = v.toMap(); + } else if (v.canConvert()) { + map = qdbus_cast(v.value()); + } + if (!map.isEmpty()) { + onDaemonSessionAdded(map); + } } - } - - applyRunningStateToLibrary(); + applyRunningStateToLibrary(); + }); } void GameLauncher::applyRunningStateToLibrary() @@ -876,18 +1021,31 @@ void GameLauncher::applyRunningStateToLibrary() void GameLauncher::checkDaemonAvailability() { ensureDaemon(); - const bool available = pingDaemon(QDBusConnection::sessionBus()); - - disconnectDaemonSignals(QDBusConnection::sessionBus(), this); - - if (available) { - connectDaemonSignals(QDBusConnection::sessionBus(), this); - } - - if (available != m_daemonAvailable) { - m_daemonAvailable = available; - Q_EMIT daemonAvailableChanged(); + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.isConnected()) { + if (m_daemonAvailable) { + m_daemonAvailable = false; + Q_EMIT daemonAvailableChanged(); + } + return; } + auto *iface = new org::kde::GameCenter1(kGameCenterService, kGameCenterPath, bus, this); + iface->setTimeout(2000); + auto *watcher = new QDBusPendingCallWatcher(iface->Ping(), this); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, iface, watcher]() { + watcher->deleteLater(); + iface->deleteLater(); + QDBusPendingReply reply = *watcher; + const bool available = !reply.isError() && reply.value() == QLatin1String("ok"); + disconnectDaemonSignals(QDBusConnection::sessionBus(), this); + if (available) { + connectDaemonSignals(QDBusConnection::sessionBus(), this); + } + if (available != m_daemonAvailable) { + m_daemonAvailable = available; + Q_EMIT daemonAvailableChanged(); + } + }); } bool GameLauncher::daemonAvailable() const diff --git a/src/gamelauncher.h b/src/gamelauncher.h index 91621fc..b46b786 100644 --- a/src/gamelauncher.h +++ b/src/gamelauncher.h @@ -57,4 +57,5 @@ private: void shutdownSpawnedDaemon(); void syncDaemonSessions(); void applyRunningStateToLibrary(); + void dispatchLaunch(Game *game, const QVariantMap &launchSpec); };