a-la-karte/src/runner/runnermanagerdaemon.cpp
Marco Allegretti e592e7b093 Add Plasma tray, notification inhibition, session control, and flatpak runner
Expand TrayController with daemon health polling every 10 seconds via
D-Bus Ping calls to org.kde.GameCenter1, org.kde.ALaKarte.Runner1, and
org.kde.ALaKarte.Input1. Add menu actions to toggle console behaviors,
notification mirroring, and to restart each daemon via systemctl --user.

Add NotificationInhibitor, owned by App, which calls
org.freedesktop.Notifications Inhibit/UnInhibit on the session bus
whenever the consoleBehaviors config setting changes. The cookie is
released on application quit.

Add consoleBehaviors and mirrorNotifications properties to Config with
KConfig persistence under the [Console] group.

Ship org.kde.alakarte.notifyrc defining GameLaunched, GameExited, and
LaunchFailed notification events so Plasma attributes them correctly in
the notification history.

Extend RunnerManagerDaemon::ResolveLaunch with a flatpak runner branch
that constructs a flatpak run command, translates environment overrides
to --env= arguments, and respects flatpakAppId, flatpakBranch,
flatpakArch, and flatpakArgs from the launch spec.

Add activate_session, switch_to_vt, and terminate_session actions to
the gamecenter KAuth helper and its polkit policy. Each action calls
the corresponding method on org.freedesktop.login1 over the system bus.

Add CouchSidebar.qml, a horizontal source tab bar shown in couch mode
above the library view, exposing the same sourceSelected, settingsRequested,
importRequested, and aboutRequested signals as SidebarView.

Fix duplicate adaptiveFocusRingWidth property in GameCard.qml.
2026-03-22 16:53:09 +01:00

1517 lines
50 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include "runnermanagerdaemon.h"
#include "runner1adaptor.h"
#include <QCryptographicHash>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusError>
#include <QDBusMessage>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDir>
#include <QDirIterator>
#include <QEventLoop>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QProcess>
#include <QSaveFile>
#include <QSet>
#include <QStandardPaths>
#include <QTimer>
#include <QUrl>
#include <QUuid>
#include <algorithm>
#include <QDebug>
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
static QVariant unwrapDbusVariant(QVariant v)
{
if (v.metaType() == QMetaType::fromType<QDBusVariant>()) {
v = v.value<QDBusVariant>().variant();
}
return v;
}
static QVariantMap unwrapVariantMap(QVariant v)
{
v = unwrapDbusVariant(v);
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>();
return qdbus_cast<QVariantMap>(arg);
}
if (v.canConvert<QVariantMap>()) {
return v.toMap();
}
return {};
}
static QStringList unwrapStringList(QVariant v)
{
v = unwrapDbusVariant(v);
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>();
return qdbus_cast<QStringList>(arg);
}
if (v.canConvert<QStringList>()) {
return v.toStringList();
}
if (v.canConvert<QVariantList>()) {
const QVariantList list = v.toList();
QStringList out;
out.reserve(list.size());
for (const QVariant &item : list) {
const QString s = item.toString();
if (!s.isEmpty()) {
out.push_back(s);
}
}
return out;
}
return {};
}
static QString stableIdForPath(const QString &prefix, const QString &path)
{
const QByteArray digest = QCryptographicHash::hash(path.toUtf8(), QCryptographicHash::Sha1).toHex();
return prefix + QLatin1Char(':') + QString::fromLatin1(digest);
}
RunnerManagerDaemon::RunnerManagerDaemon(QObject *parent)
: QObject(parent)
{
}
bool RunnerManagerDaemon::init()
{
loadRegistry();
loadGameProfiles();
m_discovered = discoverRunners();
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
qWarning() << "RunnerManagerDaemon: session bus not connected";
return false;
}
if (!bus.registerService(kRunnerService)) {
qWarning() << "RunnerManagerDaemon: failed to register service" << kRunnerService << bus.lastError().message();
return false;
}
new Runner1Adaptor(this);
if (!bus.registerObject(kRunnerPath, this, QDBusConnection::ExportAdaptors)) {
qWarning() << "RunnerManagerDaemon: failed to register object" << kRunnerPath << bus.lastError().message();
return false;
}
return true;
}
QString RunnerManagerDaemon::Ping() const
{
return QStringLiteral("ok");
}
uint RunnerManagerDaemon::Version(uint &minor) const
{
minor = 0;
return 1;
}
QVariantMap RunnerManagerDaemon::RunnerInfo::toVariantMap() const
{
return {
{QStringLiteral("id"), id},
{QStringLiteral("type"), type},
{QStringLiteral("name"), name},
{QStringLiteral("path"), path},
{QStringLiteral("source"), source},
{QStringLiteral("installDir"), installDir},
};
}
RunnerManagerDaemon::RunnerInfo RunnerManagerDaemon::RunnerInfo::fromVariantMap(const QVariantMap &map)
{
RunnerInfo out;
out.id = map.value(QStringLiteral("id")).toString();
out.type = map.value(QStringLiteral("type")).toString();
out.name = map.value(QStringLiteral("name")).toString();
out.path = map.value(QStringLiteral("path")).toString();
out.source = map.value(QStringLiteral("source")).toString();
out.installDir = map.value(QStringLiteral("installDir")).toString();
return out;
}
bool RunnerManagerDaemon::RunnerInfo::isValid() const
{
return !id.isEmpty() && !type.isEmpty() && !path.isEmpty();
}
QString RunnerManagerDaemon::registryPath() const
{
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
return base + QStringLiteral("/runner-registry.json");
}
void RunnerManagerDaemon::loadRegistry()
{
m_registered.clear();
QFile f(registryPath());
if (!f.open(QIODevice::ReadOnly)) {
return;
}
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
if (!doc.isObject()) {
return;
}
const QJsonArray arr = doc.object().value(QStringLiteral("runners")).toArray();
for (const QJsonValue &v : arr) {
if (!v.isObject()) {
continue;
}
const QJsonObject o = v.toObject();
RunnerInfo info;
info.id = o.value(QStringLiteral("id")).toString();
info.type = o.value(QStringLiteral("type")).toString();
info.name = o.value(QStringLiteral("name")).toString();
info.path = o.value(QStringLiteral("path")).toString();
info.source = o.value(QStringLiteral("source")).toString();
info.installDir = o.value(QStringLiteral("installDir")).toString();
if (info.source.isEmpty()) {
info.source = QStringLiteral("custom");
}
if (info.isValid()) {
m_registered.push_back(info);
}
}
}
bool RunnerManagerDaemon::saveRegistry() const
{
const QString path = registryPath();
const QString dirPath = QFileInfo(path).absolutePath();
if (!QDir().mkpath(dirPath)) {
return false;
}
QJsonArray arr;
for (const RunnerInfo &info : m_registered) {
QJsonObject o;
o.insert(QStringLiteral("id"), info.id);
o.insert(QStringLiteral("type"), info.type);
o.insert(QStringLiteral("name"), info.name);
o.insert(QStringLiteral("path"), info.path);
o.insert(QStringLiteral("source"), info.source);
if (!info.installDir.isEmpty()) {
o.insert(QStringLiteral("installDir"), info.installDir);
}
arr.push_back(o);
}
QJsonObject root;
root.insert(QStringLiteral("runners"), arr);
QSaveFile f(path);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return false;
}
const QByteArray payload = QJsonDocument(root).toJson(QJsonDocument::Indented);
if (f.write(payload) != payload.size()) {
return false;
}
return f.commit();
}
QString RunnerManagerDaemon::gameProfilesPath() const
{
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
return base + QStringLiteral("/runner-game-profiles.json");
}
void RunnerManagerDaemon::loadGameProfiles()
{
m_gameProfiles.clear();
QFile f(gameProfilesPath());
if (!f.open(QIODevice::ReadOnly)) {
return;
}
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
if (!doc.isObject()) {
return;
}
const QJsonArray arr = doc.object().value(QStringLiteral("profiles")).toArray();
for (const QJsonValue &v : arr) {
if (!v.isObject()) {
continue;
}
const QVariantMap map = v.toObject().toVariantMap();
const QString gameId = map.value(QStringLiteral("gameId")).toString();
if (gameId.isEmpty()) {
continue;
}
m_gameProfiles.insert(gameId, map);
}
}
bool RunnerManagerDaemon::saveGameProfiles() const
{
const QString path = gameProfilesPath();
const QString dirPath = QFileInfo(path).absolutePath();
if (!QDir().mkpath(dirPath)) {
return false;
}
QJsonArray arr;
for (auto it = m_gameProfiles.constBegin(); it != m_gameProfiles.constEnd(); ++it) {
QVariantMap profile = it.value();
if (!profile.contains(QStringLiteral("gameId"))) {
profile.insert(QStringLiteral("gameId"), it.key());
}
arr.push_back(QJsonObject::fromVariantMap(profile));
}
QJsonObject root;
root.insert(QStringLiteral("profiles"), arr);
QSaveFile f(path);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return false;
}
const QByteArray payload = QJsonDocument(root).toJson(QJsonDocument::Indented);
if (f.write(payload) != payload.size()) {
return false;
}
return f.commit();
}
QVariantMap RunnerManagerDaemon::gameProfileForGameId(const QString &gameId) const
{
const QString id = gameId.trimmed();
if (id.isEmpty()) {
return {};
}
const auto it = m_gameProfiles.constFind(id);
return it == m_gameProfiles.constEnd() ? QVariantMap{} : it.value();
}
QStringList RunnerManagerDaemon::steamCandidateRoots()
{
const QString home = QDir::homePath();
return {
home + QStringLiteral("/.steam/root"),
home + QStringLiteral("/.steam/steam"),
home + QStringLiteral("/.local/share/Steam"),
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/data/Steam"),
home + QStringLiteral("/.var/app/com.valvesoftware.Steam/.local/share/Steam"),
};
}
QString RunnerManagerDaemon::findSteamClientInstallPathFromProton(const QString &protonExe)
{
if (protonExe.isEmpty()) {
return {};
}
QDir dir(QFileInfo(protonExe).absoluteDir());
for (int i = 0; i < 10; ++i) {
if (dir.exists(QStringLiteral("steamapps"))) {
return dir.absolutePath();
}
if (!dir.cdUp()) {
break;
}
}
for (const QString &root : steamCandidateRoots()) {
if (QDir(root).exists(QStringLiteral("steamapps"))) {
return root;
}
}
return {};
}
QList<RunnerManagerDaemon::RunnerInfo> RunnerManagerDaemon::discoverRunners() const
{
QList<RunnerInfo> out;
{
const QString wineExe = QStandardPaths::findExecutable(QStringLiteral("wine"));
if (!wineExe.isEmpty()) {
RunnerInfo info;
info.type = QStringLiteral("wine");
info.path = wineExe;
info.name = QStringLiteral("Wine");
info.source = QStringLiteral("system");
info.id = stableIdForPath(QStringLiteral("wine"), wineExe);
out.push_back(info);
}
}
QStringList protonCandidates;
for (const QString &root : steamCandidateRoots()) {
const QString compatTools = root + QStringLiteral("/compatibilitytools.d");
QDir compatDir(compatTools);
if (compatDir.exists()) {
const QStringList toolDirs = compatDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &tool : toolDirs) {
const QString protonPath = compatTools + QLatin1Char('/') + tool + QStringLiteral("/proton");
if (QFileInfo::exists(protonPath)) {
protonCandidates.append(protonPath);
}
}
}
const QString common = root + QStringLiteral("/steamapps/common");
QDir commonDir(common);
if (commonDir.exists()) {
const QStringList dirs = commonDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &d : dirs) {
if (!d.contains(QStringLiteral("Proton"), Qt::CaseInsensitive)) {
continue;
}
const QString protonPath = common + QLatin1Char('/') + d + QStringLiteral("/proton");
if (QFileInfo::exists(protonPath)) {
protonCandidates.append(protonPath);
}
}
}
}
protonCandidates.removeDuplicates();
protonCandidates.sort();
for (const QString &protonExe : protonCandidates) {
RunnerInfo info;
info.type = QStringLiteral("proton");
info.path = protonExe;
info.source = QStringLiteral("steam");
info.id = stableIdForPath(QStringLiteral("proton"), protonExe);
const QString dirName = QFileInfo(protonExe).absoluteDir().dirName();
info.name = dirName.isEmpty() ? QStringLiteral("Proton") : dirName;
out.push_back(info);
}
return out;
}
QString RunnerManagerDaemon::defaultProtonExecutable() const
{
QStringList candidates;
candidates.reserve(m_discovered.size());
for (const RunnerInfo &r : m_discovered) {
if (r.type == QLatin1String("proton")) {
candidates.push_back(r.path);
}
}
candidates.removeDuplicates();
candidates.sort();
for (const QString &c : candidates) {
if (c.contains(QStringLiteral("Proton - Experimental"))) {
return c;
}
}
return candidates.isEmpty() ? QString() : candidates.constLast();
}
RunnerManagerDaemon::RunnerInfo RunnerManagerDaemon::runnerById(const QString &runnerId) const
{
for (const RunnerInfo &r : m_registered) {
if (r.id == runnerId) {
return r;
}
}
for (const RunnerInfo &r : m_discovered) {
if (r.id == runnerId) {
return r;
}
}
return {};
}
QVariantList RunnerManagerDaemon::ListRunners()
{
QVariantList out;
m_discovered = discoverRunners();
QSet<QString> seen;
for (const RunnerInfo &r : m_registered) {
if (!r.isValid() || seen.contains(r.id)) {
continue;
}
out.push_back(r.toVariantMap());
seen.insert(r.id);
}
for (const RunnerInfo &r : m_discovered) {
if (!r.isValid() || seen.contains(r.id)) {
continue;
}
out.push_back(r.toVariantMap());
seen.insert(r.id);
}
return out;
}
bool RunnerManagerDaemon::Rescan()
{
m_discovered = discoverRunners();
return true;
}
bool RunnerManagerDaemon::RegisterRunner(const QVariantMap &runner)
{
RunnerInfo info = RunnerInfo::fromVariantMap(runner);
if (info.source.isEmpty()) {
info.source = QStringLiteral("custom");
}
if (!info.isValid()) {
return false;
}
for (RunnerInfo &existing : m_registered) {
if (existing.id == info.id) {
existing = info;
return saveRegistry();
}
}
m_registered.push_back(info);
return saveRegistry();
}
bool RunnerManagerDaemon::UnregisterRunner(const QString &runnerId)
{
if (runnerId.isEmpty()) {
return false;
}
const int before = m_registered.size();
m_registered.erase(std::remove_if(m_registered.begin(),
m_registered.end(),
[&](const RunnerInfo &r) {
return r.id == runnerId;
}),
m_registered.end());
if (m_registered.size() == before) {
return false;
}
return saveRegistry();
}
bool RunnerManagerDaemon::CancelInstall(const QString &installId)
{
if (installId.isEmpty() || !m_activeInstalls.contains(installId)) {
return false;
}
m_cancelRequested.insert(installId);
if (m_installReplies.contains(installId)) {
const QPointer<QNetworkReply> replyPtr = m_installReplies.value(installId);
if (QNetworkReply *reply = replyPtr.data()) {
reply->abort();
}
}
if (m_installProcesses.contains(installId)) {
const QPointer<QProcess> procPtr = m_installProcesses.value(installId);
if (QProcess *p = procPtr.data()) {
p->kill();
}
}
return true;
}
static QString runnersBaseDir()
{
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
return base + QStringLiteral("/runners");
}
static QString installDirNameFromId(QString id)
{
id.replace(QLatin1Char(':'), QLatin1Char('_'));
id.replace(QLatin1Char('/'), QLatin1Char('_'));
id.replace(QLatin1Char('\\'), QLatin1Char('_'));
return id;
}
static QString findExecutableNamed(const QString &rootDir, const QString &fileName)
{
QDirIterator it(rootDir, {fileName}, QDir::Files, QDirIterator::Subdirectories);
while (it.hasNext()) {
const QString path = it.next();
const QFileInfo fi(path);
if (fi.isFile() && fi.isExecutable()) {
return path;
}
}
return {};
}
QVariantMap RunnerManagerDaemon::InstallRunnerFromArchive(const QVariantMap &spec)
{
QVariantMap out;
out.insert(QStringLiteral("ok"), false);
const QString installId = spec.value(QStringLiteral("installId")).toString();
const QString archivePath = spec.value(QStringLiteral("archivePath")).toString();
QString type = spec.value(QStringLiteral("type")).toString();
const QString name = spec.value(QStringLiteral("name")).toString();
QString id = spec.value(QStringLiteral("id")).toString();
const bool overwrite = spec.contains(QStringLiteral("overwrite")) ? spec.value(QStringLiteral("overwrite")).toBool() : false;
if (archivePath.isEmpty() || !QFileInfo::exists(archivePath)) {
out.insert(QStringLiteral("error"), QStringLiteral("archivePath not found"));
return out;
}
if (type.isEmpty()) {
type = QStringLiteral("proton");
}
if (id.isEmpty()) {
id = stableIdForPath(type, name.isEmpty() ? archivePath : name);
}
const QString baseDir = runnersBaseDir();
if (!QDir().mkpath(baseDir)) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to create runners base dir"));
return out;
}
const QString installDir = baseDir + QLatin1Char('/') + installDirNameFromId(id);
if (QFileInfo::exists(installDir)) {
if (!overwrite) {
out.insert(QStringLiteral("error"), QStringLiteral("installDir already exists"));
out.insert(QStringLiteral("installDir"), installDir);
out.insert(QStringLiteral("id"), id);
return out;
}
QDir dir(installDir);
if (!dir.removeRecursively()) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to remove existing installDir"));
out.insert(QStringLiteral("installDir"), installDir);
out.insert(QStringLiteral("id"), id);
return out;
}
}
if (!QDir().mkpath(installDir)) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to create installDir"));
out.insert(QStringLiteral("installDir"), installDir);
out.insert(QStringLiteral("id"), id);
return out;
}
const QString archiveLower = archivePath.toLower();
QString extractorProgram = QStringLiteral("tar");
QStringList extractorArgs = {QStringLiteral("-xf"), archivePath, QStringLiteral("-C"), installDir};
if (archiveLower.endsWith(QLatin1String(".zip"))) {
extractorProgram = QStringLiteral("unzip");
extractorArgs = {QStringLiteral("-q"), archivePath, QStringLiteral("-d"), installDir};
}
QProcess tar;
tar.setProgram(extractorProgram);
tar.setArguments(extractorArgs);
if (!installId.isEmpty()) {
m_installProcesses.insert(installId, &tar);
}
tar.start();
if (!tar.waitForStarted()) {
if (!installId.isEmpty()) {
m_installProcesses.remove(installId);
}
QDir dir(installDir);
dir.removeRecursively();
out.insert(QStringLiteral("error"), QStringLiteral("failed to start extract"));
out.insert(QStringLiteral("extractProgram"), extractorProgram);
out.insert(QStringLiteral("tarError"), tar.errorString());
out.insert(QStringLiteral("tarStdout"), QString::fromUtf8(tar.readAllStandardOutput()));
out.insert(QStringLiteral("tarStderr"), QString::fromUtf8(tar.readAllStandardError()));
return out;
}
{
QEventLoop loop;
QObject::connect(&tar, &QProcess::finished, &loop, [&]() {
loop.quit();
});
QObject::connect(&tar, &QProcess::errorOccurred, &loop, [&]() {
loop.quit();
});
loop.exec();
}
if (!installId.isEmpty()) {
m_installProcesses.remove(installId);
if (m_cancelRequested.contains(installId)) {
QDir dir(installDir);
dir.removeRecursively();
out.insert(QStringLiteral("error"), QStringLiteral("cancelled"));
return out;
}
}
if (tar.exitStatus() != QProcess::NormalExit || tar.exitCode() != 0) {
QDir dir(installDir);
dir.removeRecursively();
out.insert(QStringLiteral("error"), QStringLiteral("failed to extract archive"));
out.insert(QStringLiteral("extractProgram"), extractorProgram);
out.insert(QStringLiteral("tarError"), tar.errorString());
out.insert(QStringLiteral("tarStdout"), QString::fromUtf8(tar.readAllStandardOutput()));
out.insert(QStringLiteral("tarStderr"), QString::fromUtf8(tar.readAllStandardError()));
return out;
}
QString runnerExe;
if (type == QLatin1String("proton")) {
runnerExe = findExecutableNamed(installDir, QStringLiteral("proton"));
} else if (type == QLatin1String("wine")) {
runnerExe = findExecutableNamed(installDir, QStringLiteral("wine"));
}
if (runnerExe.isEmpty()) {
QDir dir(installDir);
dir.removeRecursively();
out.insert(QStringLiteral("error"), QStringLiteral("runner executable not found in archive (did you download a runner release archive?)"));
out.insert(QStringLiteral("installDir"), installDir);
out.insert(QStringLiteral("id"), id);
return out;
}
RunnerInfo info;
info.id = id;
info.type = type;
info.path = runnerExe;
info.name = name.isEmpty() ? QFileInfo(runnerExe).absoluteDir().dirName() : name;
info.source = QStringLiteral("installed");
info.installDir = installDir;
bool updated = false;
for (RunnerInfo &existing : m_registered) {
if (existing.id == info.id) {
existing = info;
updated = true;
break;
}
}
if (!updated) {
m_registered.push_back(info);
}
if (!saveRegistry()) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to save registry"));
return out;
}
out.insert(QStringLiteral("ok"), true);
out.insert(QStringLiteral("runner"), info.toVariantMap());
return out;
}
QVariantMap RunnerManagerDaemon::InstallRunnerFromUrl(const QVariantMap &spec)
{
QVariantMap out;
out.insert(QStringLiteral("ok"), false);
QString installId = spec.value(QStringLiteral("installId")).toString();
if (installId.isEmpty()) {
installId = QUuid::createUuid().toString(QUuid::WithoutBraces);
}
out.insert(QStringLiteral("installId"), installId);
QVariantMap startedSpec = spec;
startedSpec.insert(QStringLiteral("installId"), installId);
Q_EMIT InstallStarted(installId, startedSpec);
m_activeInstalls.insert(installId);
m_cancelRequested.remove(installId);
auto finish = [&](QVariantMap result) -> QVariantMap {
result.insert(QStringLiteral("installId"), installId);
Q_EMIT InstallFinished(installId, result);
m_activeInstalls.remove(installId);
m_cancelRequested.remove(installId);
m_installReplies.remove(installId);
m_installProcesses.remove(installId);
return result;
};
QString urlString = spec.value(QStringLiteral("url")).toString();
if (urlString.isEmpty()) {
urlString = spec.value(QStringLiteral("archiveUrl")).toString();
}
if (urlString.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("missing url"));
return finish(out);
}
urlString = urlString.trimmed();
if (urlString.startsWith(QLatin1String("~/"))) {
urlString = QDir::homePath() + QLatin1Char('/') + urlString.mid(2);
}
const QString urlLower = urlString.toLower();
if (urlLower.endsWith(QLatin1String(".git"))) {
out.insert(QStringLiteral("error"), QStringLiteral("git repository URLs are not supported; use a direct download link to a release archive (.tar.gz)"));
return finish(out);
}
if (urlLower.contains(QLatin1String("github.com")) && urlLower.contains(QLatin1String("/archive/refs/heads/"))) {
out.insert(QStringLiteral("error"), QStringLiteral("this looks like a GitHub source code archive; use a release asset archive (.tar.gz) instead"));
return finish(out);
}
if (urlLower.endsWith(QLatin1String("-master.zip")) || urlLower.endsWith(QLatin1String("-main.zip"))) {
out.insert(QStringLiteral("error"), QStringLiteral("this looks like a source code zip; use a runner release archive (.tar.gz) instead"));
return finish(out);
}
QUrl url(urlString);
if (url.scheme().isEmpty()) {
const QFileInfo fi(urlString);
if (fi.exists()) {
url = QUrl::fromLocalFile(fi.absoluteFilePath());
} else if (urlString.startsWith(QLatin1Char('/')) || urlString.startsWith(QLatin1String("~/"))) {
out.insert(QStringLiteral("error"), QStringLiteral("file not found"));
out.insert(QStringLiteral("path"), urlString);
return finish(out);
}
}
if (!url.isValid() || url.scheme().isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("invalid url"));
return finish(out);
}
const QString scheme = url.scheme().toLower();
if (scheme != QLatin1String("http") && scheme != QLatin1String("https") && scheme != QLatin1String("file")) {
out.insert(QStringLiteral("error"), QStringLiteral("unsupported url scheme"));
return finish(out);
}
auto normalizeHex = [](QString s) {
s = s.trimmed().toLower();
const int colon = s.indexOf(QLatin1Char(':'));
if (colon >= 0) {
const QString prefix = s.left(colon);
if (prefix == QLatin1String("sha256") || prefix == QLatin1String("sha512")) {
s = s.mid(colon + 1);
}
}
QString out;
out.reserve(s.size());
for (const QChar &c : s) {
if ((c >= QLatin1Char('0') && c <= QLatin1Char('9')) || (c >= QLatin1Char('a') && c <= QLatin1Char('f'))) {
out.push_back(c);
}
}
return out;
};
QString expectedHex;
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha256;
const QString expectedSha512Spec = normalizeHex(spec.value(QStringLiteral("sha512")).toString());
QString expectedShaSpec = spec.value(QStringLiteral("sha256")).toString();
expectedShaSpec = normalizeHex(expectedShaSpec);
if (!expectedSha512Spec.isEmpty()) {
algo = QCryptographicHash::Sha512;
expectedHex = expectedSha512Spec;
} else if (!expectedShaSpec.isEmpty()) {
if (expectedShaSpec.size() == 128) {
algo = QCryptographicHash::Sha512;
expectedHex = expectedShaSpec;
} else {
algo = QCryptographicHash::Sha256;
expectedHex = expectedShaSpec;
}
}
const bool checkHash = !expectedHex.isEmpty();
const int timeoutMs = spec.contains(QStringLiteral("timeoutMs")) ? spec.value(QStringLiteral("timeoutMs")).toInt() : 10 * 60 * 1000;
QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
if (tempDir.isEmpty()) {
tempDir = QDir::tempPath();
}
QString suffix;
const QString pathPart = url.path();
const int lastDot = pathPart.lastIndexOf(QLatin1Char('.'));
if (lastDot >= 0 && (pathPart.size() - lastDot) <= 16) {
suffix = pathPart.mid(lastDot);
}
if (suffix.isEmpty()) {
suffix = QStringLiteral(".download");
}
const QString tempPath = QDir(tempDir).filePath(QStringLiteral("alakarte-runner-%1%2").arg(QUuid::createUuid().toString(QUuid::WithoutBraces), suffix));
QFile file(tempPath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to create temp file"));
return finish(out);
}
QNetworkAccessManager nam;
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply *reply = nam.get(req);
m_installReplies.insert(installId, reply);
QObject::connect(reply, &QNetworkReply::downloadProgress, this, [this, installId](qint64 received, qint64 total) {
Q_EMIT InstallProgress(installId, received, total);
});
QEventLoop loop;
QTimer timer;
timer.setSingleShot(true);
bool didTimeout = false;
QCryptographicHash hash(algo);
QObject::connect(&timer, &QTimer::timeout, &loop, [&]() {
didTimeout = true;
reply->abort();
loop.quit();
});
QObject::connect(reply, &QNetworkReply::finished, &loop, [&]() {
loop.quit();
});
QObject::connect(reply, &QIODevice::readyRead, &loop, [&]() {
const QByteArray chunk = reply->readAll();
if (!chunk.isEmpty()) {
file.write(chunk);
if (checkHash) {
hash.addData(chunk);
}
}
});
timer.start(timeoutMs);
loop.exec();
if (file.isOpen()) {
file.flush();
file.close();
}
m_installReplies.remove(installId);
if (m_cancelRequested.contains(installId)) {
reply->deleteLater();
QFile::remove(tempPath);
out.insert(QStringLiteral("error"), QStringLiteral("cancelled"));
return finish(out);
}
const bool ok = !didTimeout && reply->error() == QNetworkReply::NoError;
const QString replyError = reply->errorString();
reply->deleteLater();
if (!ok) {
QFile::remove(tempPath);
out.insert(QStringLiteral("error"), didTimeout ? QStringLiteral("download timeout") : QStringLiteral("download failed"));
out.insert(QStringLiteral("details"), replyError);
return finish(out);
}
if (checkHash) {
const QString got = QString::fromLatin1(hash.result().toHex());
if (got != expectedHex) {
QFile::remove(tempPath);
if (algo == QCryptographicHash::Sha512) {
out.insert(QStringLiteral("error"), QStringLiteral("sha512 mismatch"));
out.insert(QStringLiteral("sha512"), got);
} else {
out.insert(QStringLiteral("error"), QStringLiteral("sha256 mismatch"));
out.insert(QStringLiteral("sha256"), got);
}
return finish(out);
}
}
QVariantMap archiveSpec;
archiveSpec.insert(QStringLiteral("archivePath"), tempPath);
archiveSpec.insert(QStringLiteral("type"), spec.value(QStringLiteral("type")).toString());
archiveSpec.insert(QStringLiteral("name"), spec.value(QStringLiteral("name")).toString());
archiveSpec.insert(QStringLiteral("id"), spec.value(QStringLiteral("id")).toString());
archiveSpec.insert(QStringLiteral("installId"), installId);
if (spec.contains(QStringLiteral("overwrite"))) {
archiveSpec.insert(QStringLiteral("overwrite"), spec.value(QStringLiteral("overwrite")).toBool());
}
QVariantMap installed = InstallRunnerFromArchive(archiveSpec);
QFile::remove(tempPath);
return finish(installed);
}
QVariantMap RunnerManagerDaemon::UninstallRunner(const QString &runnerId)
{
QVariantMap out;
out.insert(QStringLiteral("ok"), false);
if (runnerId.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("missing runnerId"));
return out;
}
const QString baseDir = QDir(runnersBaseDir()).canonicalPath();
if (baseDir.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("runners base dir not available"));
return out;
}
for (int i = 0; i < m_registered.size(); ++i) {
const RunnerInfo info = m_registered.at(i);
if (info.id != runnerId) {
continue;
}
if (info.installDir.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("runner is not uninstallable"));
return out;
}
const QString canonicalInstallDir = QDir(info.installDir).canonicalPath();
if (canonicalInstallDir.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("installDir not found"));
return out;
}
if (canonicalInstallDir == baseDir || !canonicalInstallDir.startsWith(baseDir + QLatin1Char('/'))) {
out.insert(QStringLiteral("error"), QStringLiteral("refusing to remove installDir outside runners base dir"));
out.insert(QStringLiteral("installDir"), canonicalInstallDir);
return out;
}
QDir dir(canonicalInstallDir);
if (!dir.removeRecursively()) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to remove installDir"));
out.insert(QStringLiteral("installDir"), canonicalInstallDir);
return out;
}
m_registered.removeAt(i);
if (!saveRegistry()) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to save registry"));
return out;
}
out.insert(QStringLiteral("ok"), true);
out.insert(QStringLiteral("runnerId"), runnerId);
return out;
}
out.insert(QStringLiteral("error"), QStringLiteral("unknown runnerId"));
return out;
}
QVariantMap RunnerManagerDaemon::EnsurePrefix(const QVariantMap &spec)
{
QVariantMap out;
out.insert(QStringLiteral("ok"), false);
const QString runner = spec.value(QStringLiteral("runner")).toString();
const QString gameId = spec.value(QStringLiteral("gameId")).toString();
QString prefixPath = spec.value(QStringLiteral("prefixPath")).toString();
if (prefixPath.isEmpty()) {
if (gameId.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("missing gameId"));
return out;
}
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
prefixPath = base + QStringLiteral("/prefixes/") + gameId;
}
if ((runner == QLatin1String("wine")) || (runner == QLatin1String("proton"))) {
if (!QDir().mkpath(prefixPath)) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to create prefix directory"));
return out;
}
}
out.insert(QStringLiteral("ok"), true);
out.insert(QStringLiteral("prefixPath"), prefixPath);
return out;
}
QVariantMap RunnerManagerDaemon::DeletePrefix(const QVariantMap &spec)
{
QVariantMap out;
out.insert(QStringLiteral("ok"), false);
const QString gameId = spec.value(QStringLiteral("gameId")).toString();
QString prefixPath = spec.value(QStringLiteral("prefixPath")).toString();
if (prefixPath.isEmpty()) {
if (gameId.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("missing gameId"));
return out;
}
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
prefixPath = base + QStringLiteral("/prefixes/") + gameId;
}
const QString baseDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/prefixes");
const QString canonicalBaseDir = QDir(baseDir).canonicalPath();
const QString canonicalPrefixDir = QDir(prefixPath).canonicalPath();
if (canonicalPrefixDir.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("prefixPath not found"));
return out;
}
if (canonicalBaseDir.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("prefix base dir not found"));
return out;
}
if (canonicalPrefixDir == canonicalBaseDir || !canonicalPrefixDir.startsWith(canonicalBaseDir + QLatin1Char('/'))) {
out.insert(QStringLiteral("error"), QStringLiteral("refusing to remove prefix outside managed prefixes dir"));
out.insert(QStringLiteral("prefixPath"), canonicalPrefixDir);
out.insert(QStringLiteral("prefixBase"), canonicalBaseDir);
return out;
}
QDir dir(canonicalPrefixDir);
if (!dir.removeRecursively()) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to remove prefix"));
out.insert(QStringLiteral("prefixPath"), canonicalPrefixDir);
return out;
}
out.insert(QStringLiteral("ok"), true);
out.insert(QStringLiteral("prefixPath"), canonicalPrefixDir);
return out;
}
QVariantMap RunnerManagerDaemon::GetGameProfile(const QString &gameId) const
{
QVariantMap out;
out.insert(QStringLiteral("ok"), false);
const QString id = gameId.trimmed();
if (id.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("missing gameId"));
return out;
}
const QVariantMap profile = gameProfileForGameId(id);
out.insert(QStringLiteral("ok"), true);
out.insert(QStringLiteral("profile"), profile);
return out;
}
QVariantList RunnerManagerDaemon::ListGameProfiles() const
{
QVariantList out;
out.reserve(m_gameProfiles.size());
for (auto it = m_gameProfiles.constBegin(); it != m_gameProfiles.constEnd(); ++it) {
QVariantMap profile = it.value();
if (!profile.contains(QStringLiteral("gameId"))) {
profile.insert(QStringLiteral("gameId"), it.key());
}
out.push_back(profile);
}
return out;
}
QVariantMap RunnerManagerDaemon::SetGameProfile(const QVariantMap &spec)
{
QVariantMap out;
out.insert(QStringLiteral("ok"), false);
const QString gameId = spec.value(QStringLiteral("gameId")).toString().trimmed();
if (gameId.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("missing gameId"));
return out;
}
QVariantMap profile;
profile.insert(QStringLiteral("gameId"), gameId);
auto takeStringIfPresent = [&](const QString &key) {
if (!spec.contains(key)) {
return;
}
const QString v = spec.value(key).toString().trimmed();
if (!v.isEmpty()) {
profile.insert(key, v);
}
};
takeStringIfPresent(QStringLiteral("runnerId"));
takeStringIfPresent(QStringLiteral("runner"));
takeStringIfPresent(QStringLiteral("runnerPath"));
takeStringIfPresent(QStringLiteral("prefixPath"));
if (spec.contains(QStringLiteral("dllOverrides"))) {
const QVariantMap raw = unwrapVariantMap(spec.value(QStringLiteral("dllOverrides")));
QVariantMap dllOverrides;
for (auto it = raw.constBegin(); it != raw.constEnd(); ++it) {
const QString key = it.key().trimmed();
if (key.isEmpty() || key.contains(QLatin1Char('=')) || key.contains(QLatin1Char(';'))) {
continue;
}
const QString value = it.value().toString().trimmed();
if (value.isEmpty()) {
continue;
}
dllOverrides.insert(key, value);
}
if (!dllOverrides.isEmpty()) {
profile.insert(QStringLiteral("dllOverrides"), dllOverrides);
}
}
if (spec.contains(QStringLiteral("envOverrides"))) {
const QVariantMap rawEnv = unwrapVariantMap(spec.value(QStringLiteral("envOverrides")));
QVariantMap env;
for (auto it = rawEnv.constBegin(); it != rawEnv.constEnd(); ++it) {
const QString key = it.key();
if (key.isEmpty() || key.contains(QLatin1Char('='))) {
continue;
}
env.insert(key, it.value().toString());
}
if (!env.isEmpty()) {
profile.insert(QStringLiteral("envOverrides"), env);
}
}
if (spec.contains(QStringLiteral("extraArgs"))) {
const QStringList raw = unwrapStringList(spec.value(QStringLiteral("extraArgs")));
QStringList extraArgs;
extraArgs.reserve(raw.size());
for (const QString &a : raw) {
const QString trimmed = a.trimmed();
if (!trimmed.isEmpty()) {
extraArgs.push_back(trimmed);
}
}
if (!extraArgs.isEmpty()) {
profile.insert(QStringLiteral("extraArgs"), extraArgs);
}
}
if (profile.size() <= 1) {
out.insert(QStringLiteral("error"), QStringLiteral("empty profile"));
return out;
}
m_gameProfiles.insert(gameId, profile);
if (!saveGameProfiles()) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to save game profiles"));
return out;
}
Q_EMIT GameProfilesChanged();
out.insert(QStringLiteral("ok"), true);
out.insert(QStringLiteral("profile"), profile);
return out;
}
QVariantMap RunnerManagerDaemon::ClearGameProfile(const QString &gameId)
{
QVariantMap out;
out.insert(QStringLiteral("ok"), false);
const QString id = gameId.trimmed();
if (id.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("missing gameId"));
return out;
}
if (!m_gameProfiles.contains(id)) {
out.insert(QStringLiteral("ok"), true);
out.insert(QStringLiteral("gameId"), id);
return out;
}
m_gameProfiles.remove(id);
if (!saveGameProfiles()) {
out.insert(QStringLiteral("error"), QStringLiteral("failed to save game profiles"));
return out;
}
Q_EMIT GameProfilesChanged();
out.insert(QStringLiteral("ok"), true);
out.insert(QStringLiteral("gameId"), id);
return out;
}
QVariantMap RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const
{
QVariantMap out;
out.insert(QStringLiteral("ok"), false);
const QString gameId = spec.value(QStringLiteral("gameId")).toString();
const bool useGameProfile = spec.contains(QStringLiteral("useGameProfile")) ? spec.value(QStringLiteral("useGameProfile")).toBool() : false;
QVariantMap profile;
if (useGameProfile && !gameId.isEmpty()) {
profile = gameProfileForGameId(gameId);
}
const QString specRunnerId = spec.value(QStringLiteral("runnerId")).toString();
const QString specRunner = spec.value(QStringLiteral("runner")).toString();
const QString specRunnerPath = spec.value(QStringLiteral("runnerPath")).toString();
QString runnerId = specRunnerId;
QString runner = specRunner;
QString runnerPath = specRunnerPath;
if (runnerId.isEmpty() && profile.contains(QStringLiteral("runnerId"))) {
runnerId = profile.value(QStringLiteral("runnerId")).toString();
}
if (runner.isEmpty() && profile.contains(QStringLiteral("runner"))) {
runner = profile.value(QStringLiteral("runner")).toString();
}
if (runnerPath.isEmpty() && profile.contains(QStringLiteral("runnerPath"))) {
runnerPath = profile.value(QStringLiteral("runnerPath")).toString();
}
if (!runnerId.isEmpty()) {
const RunnerInfo info = runnerById(runnerId);
if (!info.isValid()) {
out.insert(QStringLiteral("error"), QStringLiteral("unknown runnerId"));
return out;
}
runner = info.type;
runnerPath = info.path;
}
const QString program = spec.value(QStringLiteral("program")).toString();
QStringList args = unwrapStringList(spec.value(QStringLiteral("args")));
QString prefixPath = spec.value(QStringLiteral("prefixPath")).toString();
if (prefixPath.isEmpty() && profile.contains(QStringLiteral("prefixPath"))) {
prefixPath = profile.value(QStringLiteral("prefixPath")).toString();
}
if (program.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("missing program"));
return out;
}
{
const QStringList rawProfileExtraArgs = unwrapStringList(profile.value(QStringLiteral("extraArgs")));
for (const QString &a : rawProfileExtraArgs) {
const QString trimmed = a.trimmed();
if (!trimmed.isEmpty()) {
args.append(trimmed);
}
}
const QStringList rawSpecExtraArgs = unwrapStringList(spec.value(QStringLiteral("extraArgs")));
for (const QString &a : rawSpecExtraArgs) {
const QString trimmed = a.trimmed();
if (!trimmed.isEmpty()) {
args.append(trimmed);
}
}
}
QVariantMap effectiveEnv;
if (profile.contains(QStringLiteral("envOverrides"))) {
effectiveEnv = profile.value(QStringLiteral("envOverrides")).toMap();
}
const QVariantMap specEnv = unwrapVariantMap(spec.value(QStringLiteral("envOverrides")));
for (auto it = specEnv.constBegin(); it != specEnv.constEnd(); ++it) {
effectiveEnv.insert(it.key(), it.value());
}
QVariantMap dllOverrides;
if (profile.contains(QStringLiteral("dllOverrides"))) {
dllOverrides = profile.value(QStringLiteral("dllOverrides")).toMap();
}
const QVariantMap specDllOverrides = unwrapVariantMap(spec.value(QStringLiteral("dllOverrides")));
for (auto it = specDllOverrides.constBegin(); it != specDllOverrides.constEnd(); ++it) {
const QString key = it.key().trimmed();
if (key.isEmpty() || key.contains(QLatin1Char('=')) || key.contains(QLatin1Char(';'))) {
continue;
}
const QString value = it.value().toString().trimmed();
if (value.isEmpty()) {
continue;
}
dllOverrides.insert(key, value);
}
if (!dllOverrides.isEmpty() && !effectiveEnv.contains(QStringLiteral("WINEDLLOVERRIDES"))
&& ((runner == QLatin1String("wine")) || (runner == QLatin1String("proton")))) {
QStringList keys;
keys.reserve(dllOverrides.size());
for (auto it = dllOverrides.constBegin(); it != dllOverrides.constEnd(); ++it) {
keys.push_back(it.key());
}
std::sort(keys.begin(), keys.end());
QStringList parts;
parts.reserve(keys.size());
for (const QString &k : keys) {
const QString v = dllOverrides.value(k).toString().trimmed();
if (v.isEmpty()) {
continue;
}
parts.push_back(k + QLatin1Char('=') + v);
}
if (!parts.isEmpty()) {
effectiveEnv.insert(QStringLiteral("WINEDLLOVERRIDES"), parts.join(QStringLiteral(";")));
}
}
if ((runner == QLatin1String("wine")) || (runner == QLatin1String("proton"))) {
if (prefixPath.isEmpty()) {
if (gameId.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("missing prefixPath or gameId"));
return out;
}
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
prefixPath = base + QStringLiteral("/prefixes/") + gameId;
}
out.insert(QStringLiteral("resolvedPrefixPath"), prefixPath);
}
QString finalProgram = program;
QStringList finalArgs = args;
if (runner == QLatin1String("wine")) {
QString wineExe = runnerPath;
if (wineExe.isEmpty()) {
wineExe = QStandardPaths::findExecutable(QStringLiteral("wine"));
}
if (wineExe.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("wine not found"));
return out;
}
effectiveEnv.insert(QStringLiteral("WINEPREFIX"), prefixPath);
finalProgram = wineExe;
finalArgs = {program};
finalArgs.append(args);
} else if (runner == QLatin1String("proton")) {
QString protonExe = runnerPath;
if (protonExe.isEmpty()) {
protonExe = defaultProtonExecutable();
}
if (protonExe.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("proton not found"));
return out;
}
const QString steamInstallPath = findSteamClientInstallPathFromProton(protonExe);
out.insert(QStringLiteral("resolvedSteamInstallPath"), steamInstallPath);
effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_DATA_PATH"), prefixPath);
if (!steamInstallPath.isEmpty()) {
effectiveEnv.insert(QStringLiteral("STEAM_COMPAT_CLIENT_INSTALL_PATH"), steamInstallPath);
}
finalProgram = protonExe;
finalArgs = {QStringLiteral("run"), program};
finalArgs.append(args);
} else if (runner == QLatin1String("flatpak")) {
const QString flatpakExe = QStandardPaths::findExecutable(QStringLiteral("flatpak"));
if (flatpakExe.isEmpty()) {
out.insert(QStringLiteral("error"), QStringLiteral("flatpak not found"));
return out;
}
const QString flatpakAppId = spec.value(QStringLiteral("flatpakAppId")).toString();
const QString appId = !flatpakAppId.isEmpty() ? flatpakAppId : program;
const QString flatpakBranch = spec.value(QStringLiteral("flatpakBranch")).toString();
const QString flatpakArch = spec.value(QStringLiteral("flatpakArch")).toString();
const QStringList flatpakExtraArgs = unwrapStringList(spec.value(QStringLiteral("flatpakArgs")));
QStringList runArgs;
runArgs.push_back(QStringLiteral("run"));
if (!flatpakBranch.isEmpty()) {
runArgs.push_back(QStringLiteral("--branch=") + flatpakBranch);
}
if (!flatpakArch.isEmpty()) {
runArgs.push_back(QStringLiteral("--arch=") + flatpakArch);
}
for (auto it = effectiveEnv.constBegin(); it != effectiveEnv.constEnd(); ++it) {
const QString val = it.value().toString();
if (!val.isEmpty()) {
runArgs.push_back(QStringLiteral("--env=") + it.key() + QLatin1Char('=') + val);
}
}
for (const QString &a : flatpakExtraArgs) {
if (!a.trimmed().isEmpty()) {
runArgs.push_back(a);
}
}
runArgs.push_back(appId);
runArgs.append(args);
finalProgram = flatpakExe;
finalArgs = runArgs;
effectiveEnv.clear();
}
out.insert(QStringLiteral("finalProgram"), finalProgram);
out.insert(QStringLiteral("finalArgs"), finalArgs);
out.insert(QStringLiteral("effectiveEnv"), effectiveEnv);
out.insert(QStringLiteral("ok"), true);
return out;
}