mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-09 21:13:08 +00:00
Runner: add Runner1 daemon + client
This commit is contained in:
parent
d87c227347
commit
1f1be64c8f
11 changed files with 1950 additions and 0 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
41
src/runner/CMakeLists.txt
Normal 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}
|
||||
)
|
||||
4
src/runner/dbus/org.kde.ALaKarte.Runner1.service.in
Normal file
4
src/runner/dbus/org.kde.ALaKarte.Runner1.service.in
Normal 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
20
src/runner/main.cpp
Normal 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();
|
||||
}
|
||||
1091
src/runner/runnermanagerdaemon.cpp
Normal file
1091
src/runner/runnermanagerdaemon.cpp
Normal file
File diff suppressed because it is too large
Load diff
84
src/runner/runnermanagerdaemon.h
Normal file
84
src/runner/runnermanagerdaemon.h
Normal 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;
|
||||
};
|
||||
8
src/runner/systemd/org.kde.ALaKarte.Runner1.service.in
Normal file
8
src/runner/systemd/org.kde.ALaKarte.Runner1.service.in
Normal 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
604
src/runnermanagerclient.cpp
Normal 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
85
src/runnermanagerclient.h
Normal 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;
|
||||
};
|
||||
Loading…
Reference in a new issue