2026-02-09 13:01:15 +00:00
|
|
|
// 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>
|
2026-02-12 13:44:20 +00:00
|
|
|
#include <QDBusVariant>
|
2026-02-09 13:01:15 +00:00
|
|
|
#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");
|
|
|
|
|
|
2026-02-12 13:44:20 +00:00
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
static QVariant unwrapDbusVariant(QVariant v)
|
|
|
|
|
{
|
|
|
|
|
if (v.canConvert<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 QVariantMap normalizeMap(QVariantMap m)
|
|
|
|
|
{
|
|
|
|
|
for (auto it = m.begin(); it != m.end(); ++it) {
|
|
|
|
|
it.value() = unwrapDbusVariant(it.value());
|
|
|
|
|
if (it.key() == QLatin1String("envOverrides")) {
|
|
|
|
|
it.value() = unwrapVariantMap(it.value());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return m;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static QVariantMap normalizeResultWithProfile(QVariantMap result)
|
|
|
|
|
{
|
|
|
|
|
result = normalizeMap(result);
|
|
|
|
|
if (result.contains(QStringLiteral("profile"))) {
|
|
|
|
|
QVariantMap profile = unwrapVariantMap(result.value(QStringLiteral("profile")));
|
|
|
|
|
profile = normalizeMap(profile);
|
|
|
|
|
result.insert(QStringLiteral("profile"), profile);
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 13:01:15 +00:00
|
|
|
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)));
|
2026-02-12 13:44:20 +00:00
|
|
|
|
|
|
|
|
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("GameProfilesChanged"), this, SLOT(onGameProfilesChanged()));
|
2026-02-09 13:01:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (QCoreApplication::instance()) {
|
|
|
|
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
|
|
|
|
shutdownSpawnedRunnerDaemon();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshRunners();
|
2026-02-12 13:44:20 +00:00
|
|
|
refreshGameProfiles();
|
2026-02-09 13:01:15 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 13:44:20 +00:00
|
|
|
QVariantList RunnerManagerClient::gameProfiles() const
|
|
|
|
|
{
|
|
|
|
|
return m_gameProfiles;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 13:01:15 +00:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 13:44:20 +00:00
|
|
|
void RunnerManagerClient::onGameProfilesChanged()
|
|
|
|
|
{
|
|
|
|
|
refreshGameProfiles();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 13:01:15 +00:00
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 13:44:20 +00:00
|
|
|
void RunnerManagerClient::refreshGameProfiles()
|
|
|
|
|
{
|
|
|
|
|
ensureRunnerDaemon();
|
|
|
|
|
|
|
|
|
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
|
|
|
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListGameProfiles"));
|
|
|
|
|
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_refreshProfilesRetryPending && (reply.error().type() == QDBusError::ServiceUnknown || reply.error().type() == QDBusError::NoReply)) {
|
|
|
|
|
m_refreshProfilesRetryPending = true;
|
|
|
|
|
ensureRunnerDaemon();
|
|
|
|
|
QTimer::singleShot(500, this, [this]() {
|
|
|
|
|
m_refreshProfilesRetryPending = false;
|
|
|
|
|
refreshGameProfiles();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QVariantList raw = reply.value();
|
|
|
|
|
|
|
|
|
|
QVariantList list;
|
|
|
|
|
list.reserve(raw.size());
|
|
|
|
|
for (const QVariant &v : raw) {
|
|
|
|
|
QVariantMap map;
|
|
|
|
|
if (v.metaType() == QMetaType::fromType<QVariantMap>()) {
|
|
|
|
|
map = v.toMap();
|
|
|
|
|
} else if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
|
|
|
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
|
|
|
|
map = qdbus_cast<QVariantMap>(arg);
|
|
|
|
|
} else if (v.canConvert<QVariantMap>()) {
|
|
|
|
|
map = v.toMap();
|
|
|
|
|
}
|
|
|
|
|
list.push_back(normalizeMap(map));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_gameProfiles == list) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
m_gameProfiles = list;
|
|
|
|
|
Q_EMIT gameProfilesChanged();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RunnerManagerClient::requestGameProfile(const QString &gameId)
|
|
|
|
|
{
|
|
|
|
|
const QString id = gameId.trimmed();
|
|
|
|
|
if (id.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensureRunnerDaemon();
|
|
|
|
|
|
|
|
|
|
auto attempts = QSharedPointer<int>::create(0);
|
|
|
|
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
|
|
|
|
*callPtr = [this, id, attempts, callPtr]() {
|
|
|
|
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
|
|
|
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("GetGameProfile"), id);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
QVariantMap err;
|
|
|
|
|
err.insert(QStringLiteral("ok"), false);
|
|
|
|
|
err.insert(QStringLiteral("error"), reply.error().message());
|
|
|
|
|
Q_EMIT gameProfileFetched(id, err);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariantMap result = normalizeResultWithProfile(reply.value());
|
|
|
|
|
Q_EMIT gameProfileFetched(id, result);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
(*callPtr)();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RunnerManagerClient::setGameProfile(const QVariantMap &spec)
|
|
|
|
|
{
|
|
|
|
|
const QString id = spec.value(QStringLiteral("gameId")).toString().trimmed();
|
|
|
|
|
if (id.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensureRunnerDaemon();
|
|
|
|
|
|
|
|
|
|
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("SetGameProfile"), 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;
|
|
|
|
|
}
|
|
|
|
|
QVariantMap err;
|
|
|
|
|
err.insert(QStringLiteral("ok"), false);
|
|
|
|
|
err.insert(QStringLiteral("error"), reply.error().message());
|
|
|
|
|
Q_EMIT gameProfileSaved(id, err);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariantMap result = normalizeResultWithProfile(reply.value());
|
|
|
|
|
Q_EMIT gameProfileSaved(id, result);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
(*callPtr)();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RunnerManagerClient::clearGameProfile(const QString &gameId)
|
|
|
|
|
{
|
|
|
|
|
const QString id = gameId.trimmed();
|
|
|
|
|
if (id.isEmpty()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensureRunnerDaemon();
|
|
|
|
|
|
|
|
|
|
auto attempts = QSharedPointer<int>::create(0);
|
|
|
|
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
|
|
|
|
*callPtr = [this, id, attempts, callPtr]() {
|
|
|
|
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
|
|
|
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ClearGameProfile"), id);
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
QVariantMap err;
|
|
|
|
|
err.insert(QStringLiteral("ok"), false);
|
|
|
|
|
err.insert(QStringLiteral("error"), reply.error().message());
|
|
|
|
|
Q_EMIT gameProfileCleared(id, err);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariantMap result = normalizeMap(reply.value());
|
|
|
|
|
Q_EMIT gameProfileCleared(id, result);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
(*callPtr)();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 13:01:15 +00:00
|
|
|
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();
|
|
|
|
|
}
|