Runner: add Runner1 daemon + client

This commit is contained in:
Marco Allegretti 2026-02-09 14:01:15 +01:00
parent d87c227347
commit 1f1be64c8f
11 changed files with 1950 additions and 0 deletions

View file

@ -10,6 +10,7 @@ add_executable(alakarte
gamepadmanager.cpp
inputmanager.cpp
mediamanager.cpp
runnermanagerclient.cpp
screenshotmodel.cpp
platformimporter.cpp
steamimporter.cpp
@ -139,3 +140,5 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
add_subdirectory(gamecenter)
add_subdirectory(runner)

View file

@ -27,6 +27,7 @@ App::App(QObject *parent)
: QObject(parent)
, m_gameModel(new GameModel(this))
, m_launcher(new GameLauncher(this))
, m_runnerManager(new RunnerManagerClient(this))
, m_steamGridDB(new SteamGridDB(this))
, m_mediaManager(new MediaManager(this))
, m_config(new Config(this))
@ -169,6 +170,11 @@ GameLauncher *App::launcher() const
return m_launcher;
}
RunnerManagerClient *App::runnerManager() const
{
return m_runnerManager;
}
SteamGridDB *App::steamGridDB() const
{
return m_steamGridDB;

View file

@ -12,6 +12,7 @@
#include "gamelauncher.h"
#include "gamemodel.h"
#include "mediamanager.h"
#include "runnermanagerclient.h"
#include "steamgriddb.h"
class App : public QObject
@ -22,6 +23,7 @@ class App : public QObject
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
Q_PROPERTY(RunnerManagerClient *runnerManager READ runnerManager CONSTANT)
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
Q_PROPERTY(Config *config READ config CONSTANT)
@ -34,6 +36,7 @@ public:
GameModel *gameModel() const;
GameLauncher *launcher() const;
RunnerManagerClient *runnerManager() const;
SteamGridDB *steamGridDB() const;
MediaManager *mediaManager() const;
Config *config() const;
@ -74,6 +77,7 @@ private:
GameModel *m_gameModel;
GameLauncher *m_launcher;
RunnerManagerClient *m_runnerManager;
SteamGridDB *m_steamGridDB;
MediaManager *m_mediaManager;
Config *m_config;

41
src/runner/CMakeLists.txt Normal file
View file

@ -0,0 +1,41 @@
# SPDX-License-Identifier: BSD-3-Clause
# SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
add_executable(alakarte_runnerd
main.cpp
runnermanagerdaemon.cpp
runnermanagerdaemon.h
)
target_link_libraries(alakarte_runnerd PRIVATE
Qt6::Core
Qt6::DBus
Qt6::Network
)
set_target_properties(alakarte_runnerd PROPERTIES
OUTPUT_NAME "alakarte-runnerd"
)
install(TARGETS alakarte_runnerd ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
configure_file(dbus/org.kde.ALaKarte.Runner1.service.in
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Runner1.service"
@ONLY
)
configure_file(systemd/org.kde.ALaKarte.Runner1.service.in
"${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Runner1.service"
@ONLY
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Runner1.service"
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
)
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Runner1.service"
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
)

View file

@ -0,0 +1,4 @@
[D-BUS Service]
Name=org.kde.ALaKarte.Runner1
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-runnerd
SystemdService=org.kde.ALaKarte.Runner1.service

20
src/runner/main.cpp Normal file
View file

@ -0,0 +1,20 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCoreApplication>
#include <QDBusConnection>
#include "runnermanagerdaemon.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
RunnerManagerDaemon daemon;
if (!daemon.init()) {
return 1;
}
return app.exec();
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#pragma once
#include <QHash>
#include <QObject>
#include <QPointer>
#include <QSet>
#include <QVariantMap>
class QNetworkReply;
class QProcess;
class RunnerManagerDaemon : public QObject
{
Q_OBJECT
Q_CLASSINFO("D-Bus Interface", "org.kde.ALaKarte.Runner1")
public:
explicit RunnerManagerDaemon(QObject *parent = nullptr);
bool init();
public Q_SLOTS:
QString Ping() const;
void Version(uint &major, uint &minor) const;
QVariantList ListRunners();
bool Rescan();
bool RegisterRunner(const QVariantMap &runner);
bool UnregisterRunner(const QString &runnerId);
QVariantMap InstallRunnerFromArchive(const QVariantMap &spec);
QVariantMap InstallRunnerFromUrl(const QVariantMap &spec);
QVariantMap UninstallRunner(const QString &runnerId);
bool CancelInstall(const QString &installId);
QVariantMap EnsurePrefix(const QVariantMap &spec);
QVariantMap DeletePrefix(const QVariantMap &spec);
QVariantMap ResolveLaunch(const QVariantMap &spec) const;
Q_SIGNALS:
void InstallStarted(const QString &installId, const QVariantMap &spec);
void InstallProgress(const QString &installId, qint64 receivedBytes, qint64 totalBytes);
void InstallFinished(const QString &installId, const QVariantMap &result);
private:
struct RunnerInfo {
QString id;
QString type;
QString name;
QString path;
QString source;
QString installDir;
QVariantMap toVariantMap() const;
static RunnerInfo fromVariantMap(const QVariantMap &map);
bool isValid() const;
};
QString registryPath() const;
void loadRegistry();
bool saveRegistry() const;
QList<RunnerInfo> discoverRunners() const;
QString defaultProtonExecutable() const;
static QStringList steamCandidateRoots();
static QString findSteamClientInstallPathFromProton(const QString &protonExe);
RunnerInfo runnerById(const QString &runnerId) const;
QList<RunnerInfo> m_registered;
QList<RunnerInfo> m_discovered;
QSet<QString> m_activeInstalls;
QSet<QString> m_cancelRequested;
QHash<QString, QPointer<QNetworkReply>> m_installReplies;
QHash<QString, QPointer<QProcess>> m_installProcesses;
};

View file

@ -0,0 +1,8 @@
[Unit]
Description=A-La-Karte Runner Manager
[Service]
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-runnerd
Type=dbus
BusName=org.kde.ALaKarte.Runner1
Restart=no

604
src/runnermanagerclient.cpp Normal file
View file

@ -0,0 +1,604 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include "runnermanagerclient.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusMetaType>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#include <QDBusReply>
#include <QDir>
#include <QFileInfo>
#include <QProcess>
#include <QSharedPointer>
#include <QTimer>
#include <QUrl>
#include <QUuid>
#include <functional>
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
static QString stableIdForUrl(const QString &type, const QString &url)
{
const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex();
return type + QStringLiteral(":url:") + QString::fromLatin1(digest);
}
static QString stableIdKeyForInputUrl(QString urlString)
{
urlString = urlString.trimmed();
if (urlString.startsWith(QLatin1String("~/"))) {
urlString = QDir::homePath() + QLatin1Char('/') + urlString.mid(2);
}
QUrl url(urlString);
if (url.isValid() && url.scheme().toLower() == QLatin1String("file")) {
QFileInfo fi(url.toLocalFile());
const QString canonical = fi.canonicalFilePath();
const QString abs = fi.absoluteFilePath();
const QString path = canonical.isEmpty() ? abs : canonical;
return QUrl::fromLocalFile(path).toString(QUrl::FullyEncoded);
}
if (url.scheme().isEmpty()) {
QFileInfo fi(urlString);
if (fi.isAbsolute() || urlString.startsWith(QLatin1String("./")) || urlString.startsWith(QLatin1String("../"))) {
const QString canonical = fi.exists() ? fi.canonicalFilePath() : QString();
const QString abs = fi.absoluteFilePath();
const QString path = canonical.isEmpty() ? abs : canonical;
return QUrl::fromLocalFile(path).toString(QUrl::FullyEncoded);
}
}
if (url.isValid() && !url.scheme().isEmpty()) {
return url.toString(QUrl::FullyEncoded);
}
return urlString;
}
RunnerManagerClient::RunnerManagerClient(QObject *parent)
: QObject(parent)
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (bus.isConnected()) {
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallStarted"), this, SLOT(onInstallStarted(QString, QVariantMap)));
bus.connect(kRunnerService,
kRunnerPath,
kRunnerInterface,
QStringLiteral("InstallProgress"),
this,
SLOT(onInstallProgress(QString, qlonglong, qlonglong)));
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallFinished"), this, SLOT(onInstallFinished(QString, QVariantMap)));
}
if (QCoreApplication::instance()) {
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
shutdownSpawnedRunnerDaemon();
});
}
refreshRunners();
}
RunnerManagerClient::~RunnerManagerClient()
{
shutdownSpawnedRunnerDaemon();
}
void RunnerManagerClient::shutdownSpawnedRunnerDaemon()
{
if (!m_runnerdProcess) {
return;
}
if (m_runnerdProcess->state() == QProcess::NotRunning) {
return;
}
m_runnerdProcess->terminate();
if (!m_runnerdProcess->waitForFinished(1000)) {
m_runnerdProcess->kill();
m_runnerdProcess->waitForFinished(1000);
}
}
void RunnerManagerClient::ensureRunnerDaemon()
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected() || !bus.interface()) {
return;
}
if (bus.interface()->isServiceRegistered(kRunnerService)) {
return;
}
bool activatable = false;
{
const QDBusReply<QStringList> names = bus.interface()->activatableServiceNames();
if (names.isValid()) {
activatable = names.value().contains(kRunnerService);
}
}
const QDBusReply<void> reply = bus.interface()->startService(kRunnerService);
if (reply.isValid()) {
return;
}
if (bus.interface()->isServiceRegistered(kRunnerService)) {
return;
}
if (activatable) {
setLastError(reply.error().message());
return;
}
#if defined(QT_NO_DEBUG)
setLastError(QStringLiteral("Runner service is not available (missing DBus activation for org.kde.ALaKarte.Runner1)"));
return;
#endif
if (m_runnerdSpawnAttempted) {
return;
}
m_runnerdSpawnAttempted = true;
QString program = QCoreApplication::applicationDirPath() + QLatin1String("/alakarte-runnerd");
if (!QFileInfo::exists(program)) {
program = QStringLiteral("alakarte-runnerd");
}
if (!m_runnerdProcess) {
m_runnerdProcess = new QProcess(this);
m_runnerdProcess->setProcessChannelMode(QProcess::MergedChannels);
connect(m_runnerdProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
setLastError(QStringLiteral("Failed to start runner service"));
});
connect(m_runnerdProcess, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
m_runnerdProcess->deleteLater();
m_runnerdProcess = nullptr;
});
}
m_runnerdProcess->setProgram(program);
m_runnerdProcess->setArguments({});
m_runnerdProcess->start();
}
bool RunnerManagerClient::busy() const
{
return m_busy;
}
QString RunnerManagerClient::installId() const
{
return m_installId;
}
qint64 RunnerManagerClient::receivedBytes() const
{
return m_receivedBytes;
}
qint64 RunnerManagerClient::totalBytes() const
{
return m_totalBytes;
}
QString RunnerManagerClient::status() const
{
return m_status;
}
QString RunnerManagerClient::lastError() const
{
return m_lastError;
}
QVariantList RunnerManagerClient::runners() const
{
return m_runners;
}
static QString 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;
}
void RunnerManagerClient::installRunnerFromUrl(const QString &url, const QString &sha256, const QString &name, const QString &type, bool overwrite)
{
const QString trimmedUrl = url.trimmed();
if (trimmedUrl.isEmpty() || m_busy) {
return;
}
const QString effectiveType = type.trimmed().isEmpty() ? QStringLiteral("proton") : type.trimmed();
setLastError(QString());
setProgress(0, 0);
setStatus(QStringLiteral("Starting..."));
const QString newInstallId = QUuid::createUuid().toString(QUuid::WithoutBraces);
setInstallId(newInstallId);
setBusy(true);
QVariantMap spec;
spec.insert(QStringLiteral("installId"), newInstallId);
spec.insert(QStringLiteral("url"), trimmedUrl);
spec.insert(QStringLiteral("type"), effectiveType);
spec.insert(QStringLiteral("name"), name.trimmed());
spec.insert(QStringLiteral("overwrite"), overwrite);
const QString expected = normalizeHex(sha256);
if (!expected.isEmpty()) {
if (expected.size() == 128) {
spec.insert(QStringLiteral("sha512"), expected);
} else {
spec.insert(QStringLiteral("sha256"), expected);
}
}
const QString stableId = stableIdForUrl(effectiveType, stableIdKeyForInputUrl(trimmedUrl));
spec.insert(QStringLiteral("id"), stableId);
ensureRunnerDaemon();
auto attempts = QSharedPointer<int>::create(0);
auto callPtr = QSharedPointer<std::function<void()>>::create();
*callPtr = [this, spec, attempts, callPtr]() {
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("InstallRunnerFromUrl"), spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, spec, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
w->deleteLater();
if (reply.isError()) {
const QDBusError::ErrorType t = reply.error().type();
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
(*attempts)++;
ensureRunnerDaemon();
QTimer::singleShot(500, this, [callPtr]() {
(*callPtr)();
});
return;
}
setStatus(QStringLiteral("Failed to call InstallRunnerFromUrl"));
setLastError(reply.error().message());
setBusy(false);
return;
}
QVariantMap result = reply.value();
const bool ok = result.value(QStringLiteral("ok")).toBool();
if (!ok) {
const QString err = result.value(QStringLiteral("error")).toString();
setLastError(err);
setStatus(err.isEmpty() ? QStringLiteral("Failed") : err);
setBusy(false);
return;
}
setStatus(QStringLiteral("Started"));
});
};
(*callPtr)();
}
void RunnerManagerClient::deletePrefix(const QString &gameId, const QString &prefixPath)
{
const QString id = gameId.trimmed();
const QString p = prefixPath.trimmed();
if (id.isEmpty()) {
return;
}
ensureRunnerDaemon();
QVariantMap spec;
spec.insert(QStringLiteral("gameId"), id);
if (!p.isEmpty()) {
spec.insert(QStringLiteral("prefixPath"), p);
}
auto attempts = QSharedPointer<int>::create(0);
auto callPtr = QSharedPointer<std::function<void()>>::create();
*callPtr = [this, spec, id, attempts, callPtr]() {
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("DeletePrefix"), spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
w->deleteLater();
if (reply.isError()) {
const QDBusError::ErrorType t = reply.error().type();
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
(*attempts)++;
ensureRunnerDaemon();
QTimer::singleShot(500, this, [callPtr]() {
(*callPtr)();
});
return;
}
setLastError(reply.error().message());
return;
}
const QVariantMap result = reply.value();
if (!result.value(QStringLiteral("ok")).toBool()) {
setLastError(result.value(QStringLiteral("error")).toString());
return;
}
const QString removed = result.value(QStringLiteral("prefixPath")).toString();
Q_EMIT prefixDeleted(id, removed);
});
};
(*callPtr)();
}
void RunnerManagerClient::cancelCurrentInstall()
{
if (!m_busy || m_installId.isEmpty()) {
return;
}
setStatus(QStringLiteral("Cancelling..."));
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
iface.asyncCall(QStringLiteral("CancelInstall"), m_installId);
}
void RunnerManagerClient::onInstallStarted(const QString &installId, const QVariantMap &)
{
if (installId.isEmpty() || installId != m_installId) {
return;
}
setStatus(QStringLiteral("Downloading..."));
}
void RunnerManagerClient::onInstallProgress(const QString &installId, qlonglong received, qlonglong total)
{
if (installId.isEmpty() || installId != m_installId) {
return;
}
setProgress(static_cast<qint64>(received), static_cast<qint64>(total));
}
void RunnerManagerClient::onInstallFinished(const QString &installId, const QVariantMap &result)
{
if (installId.isEmpty() || installId != m_installId) {
return;
}
const bool ok = result.value(QStringLiteral("ok")).toBool();
if (!ok) {
const QString err = result.value(QStringLiteral("error")).toString();
setLastError(err);
setStatus(err.isEmpty() ? QStringLiteral("Failed") : err);
} else {
setStatus(QStringLiteral("Installed"));
}
setBusy(false);
refreshRunners();
}
void RunnerManagerClient::refreshRunners()
{
ensureRunnerDaemon();
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListRunners"));
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantList> reply = *w;
w->deleteLater();
if (reply.isError()) {
if (!m_refreshRetryPending && (reply.error().type() == QDBusError::ServiceUnknown || reply.error().type() == QDBusError::NoReply)) {
m_refreshRetryPending = true;
ensureRunnerDaemon();
QTimer::singleShot(500, this, [this]() {
m_refreshRetryPending = false;
refreshRunners();
});
}
return;
}
const QVariantList raw = reply.value();
QVariantList list;
list.reserve(raw.size());
for (const QVariant &v : raw) {
if (v.metaType() == QMetaType::fromType<QVariantMap>()) {
list.push_back(v.toMap());
continue;
}
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>();
const QVariantMap map = qdbus_cast<QVariantMap>(arg);
list.push_back(map);
continue;
}
if (v.canConvert<QVariantMap>()) {
list.push_back(v.toMap());
continue;
}
list.push_back(QVariantMap{});
}
if (m_runners == list) {
return;
}
m_runners = list;
Q_EMIT runnersChanged();
});
}
void RunnerManagerClient::uninstallRunner(const QString &runnerId)
{
const QString id = runnerId.trimmed();
if (id.isEmpty()) {
return;
}
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("UninstallRunner"), id);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
w->deleteLater();
if (reply.isError()) {
setLastError(reply.error().message());
return;
}
const QVariantMap result = reply.value();
if (!result.value(QStringLiteral("ok")).toBool()) {
setLastError(result.value(QStringLiteral("error")).toString());
}
refreshRunners();
});
}
void RunnerManagerClient::ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath)
{
const QString id = gameId.trimmed();
const QString r = runner.trimmed();
const QString p = prefixPath.trimmed();
if (id.isEmpty()) {
return;
}
ensureRunnerDaemon();
QVariantMap spec;
spec.insert(QStringLiteral("gameId"), id);
if (!r.isEmpty()) {
spec.insert(QStringLiteral("runner"), r);
}
if (!p.isEmpty()) {
spec.insert(QStringLiteral("prefixPath"), p);
}
auto attempts = QSharedPointer<int>::create(0);
auto callPtr = QSharedPointer<std::function<void()>>::create();
*callPtr = [this, spec, id, attempts, callPtr]() {
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("EnsurePrefix"), spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
w->deleteLater();
if (reply.isError()) {
const QDBusError::ErrorType t = reply.error().type();
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
(*attempts)++;
ensureRunnerDaemon();
QTimer::singleShot(500, this, [callPtr]() {
(*callPtr)();
});
return;
}
setLastError(reply.error().message());
return;
}
const QVariantMap result = reply.value();
if (!result.value(QStringLiteral("ok")).toBool()) {
setLastError(result.value(QStringLiteral("error")).toString());
return;
}
const QString ensured = result.value(QStringLiteral("prefixPath")).toString();
if (!ensured.isEmpty()) {
Q_EMIT prefixEnsured(id, ensured);
}
});
};
(*callPtr)();
}
void RunnerManagerClient::setBusy(bool busy)
{
if (m_busy == busy) {
return;
}
m_busy = busy;
Q_EMIT busyChanged();
}
void RunnerManagerClient::setInstallId(const QString &installId)
{
if (m_installId == installId) {
return;
}
m_installId = installId;
Q_EMIT installIdChanged();
}
void RunnerManagerClient::setProgress(qint64 received, qint64 total)
{
if (m_receivedBytes == received && m_totalBytes == total) {
return;
}
m_receivedBytes = received;
m_totalBytes = total;
Q_EMIT progressChanged();
}
void RunnerManagerClient::setStatus(const QString &status)
{
if (m_status == status) {
return;
}
m_status = status;
Q_EMIT statusChanged();
}
void RunnerManagerClient::setLastError(const QString &error)
{
if (m_lastError == error) {
return;
}
m_lastError = error;
Q_EMIT lastErrorChanged();
}

85
src/runnermanagerclient.h Normal file
View file

@ -0,0 +1,85 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#pragma once
#include <QObject>
#include <QVariantList>
#include <QVariantMap>
class RunnerManagerClient : public QObject
{
Q_OBJECT
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
Q_PROPERTY(QString installId READ installId NOTIFY installIdChanged)
Q_PROPERTY(qint64 receivedBytes READ receivedBytes NOTIFY progressChanged)
Q_PROPERTY(qint64 totalBytes READ totalBytes NOTIFY progressChanged)
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged)
public:
explicit RunnerManagerClient(QObject *parent = nullptr);
~RunnerManagerClient() override;
bool busy() const;
QString installId() const;
qint64 receivedBytes() const;
qint64 totalBytes() const;
QString status() const;
QString lastError() const;
QVariantList runners() const;
Q_INVOKABLE void installRunnerFromUrl(const QString &url,
const QString &sha256 = QString(),
const QString &name = QString(),
const QString &type = QStringLiteral("proton"),
bool overwrite = true);
Q_INVOKABLE void cancelCurrentInstall();
Q_INVOKABLE void refreshRunners();
Q_INVOKABLE void uninstallRunner(const QString &runnerId);
Q_INVOKABLE void ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath = QString());
Q_INVOKABLE void deletePrefix(const QString &gameId, const QString &prefixPath = QString());
Q_SIGNALS:
void busyChanged();
void installIdChanged();
void progressChanged();
void statusChanged();
void lastErrorChanged();
void runnersChanged();
void prefixEnsured(const QString &gameId, const QString &prefixPath);
void prefixDeleted(const QString &gameId, const QString &prefixPath);
private Q_SLOTS:
void onInstallStarted(const QString &installId, const QVariantMap &spec);
void onInstallProgress(const QString &installId, qlonglong received, qlonglong total);
void onInstallFinished(const QString &installId, const QVariantMap &result);
private:
void ensureRunnerDaemon();
void shutdownSpawnedRunnerDaemon();
void setBusy(bool busy);
void setInstallId(const QString &installId);
void setProgress(qint64 received, qint64 total);
void setStatus(const QString &status);
void setLastError(const QString &error);
bool m_busy = false;
QString m_installId;
qint64 m_receivedBytes = 0;
qint64 m_totalBytes = 0;
QString m_status;
QString m_lastError;
QVariantList m_runners;
bool m_refreshRetryPending = false;
class QProcess *m_runnerdProcess = nullptr;
bool m_runnerdSpawnAttempted = false;
};