runner: add game profile API

This commit is contained in:
Marco Allegretti 2026-02-12 14:44:20 +01:00
parent 27a385e3c3
commit 7724205354
4 changed files with 633 additions and 6 deletions

View file

@ -4,10 +4,12 @@
#include "runnermanagerdaemon.h"
#include <QCryptographicHash>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusError>
#include <QDBusMessage>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDir>
#include <QDirIterator>
#include <QEventLoop>
@ -32,6 +34,52 @@
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();
@ -46,6 +94,7 @@ RunnerManagerDaemon::RunnerManagerDaemon(QObject *parent)
bool RunnerManagerDaemon::init()
{
loadRegistry();
loadGameProfiles();
m_discovered = discoverRunners();
QDBusConnection bus = QDBusConnection::sessionBus();
@ -181,6 +230,76 @@ bool RunnerManagerDaemon::saveRegistry() const
return true;
}
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();
QDir().mkpath(QFileInfo(path).absolutePath());
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);
QFile f(path);
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
return false;
}
f.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
return true;
}
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();
@ -998,14 +1117,192 @@ QVariantMap RunnerManagerDaemon::DeletePrefix(const QVariantMap &spec)
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 runnerId = spec.value(QStringLiteral("runnerId")).toString();
QString runner = spec.value(QStringLiteral("runner")).toString();
QString runnerPath = spec.value(QStringLiteral("runnerPath")).toString();
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);
@ -1017,17 +1314,84 @@ QVariantMap RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const
runnerPath = info.path;
}
const QString gameId = spec.value(QStringLiteral("gameId")).toString();
const QString program = spec.value(QStringLiteral("program")).toString();
const QStringList args = spec.value(QStringLiteral("args")).toStringList();
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;
}
QVariantMap effectiveEnv = spec.value(QStringLiteral("envOverrides")).toMap();
{
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()) {

View file

@ -42,11 +42,18 @@ public Q_SLOTS:
QVariantMap DeletePrefix(const QVariantMap &spec);
QVariantMap ResolveLaunch(const QVariantMap &spec) const;
QVariantMap GetGameProfile(const QString &gameId) const;
QVariantMap SetGameProfile(const QVariantMap &spec);
QVariantMap ClearGameProfile(const QString &gameId);
QVariantList ListGameProfiles() 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);
void GameProfilesChanged();
private:
struct RunnerInfo {
QString id;
@ -65,6 +72,11 @@ private:
void loadRegistry();
bool saveRegistry() const;
QString gameProfilesPath() const;
void loadGameProfiles();
bool saveGameProfiles() const;
QVariantMap gameProfileForGameId(const QString &gameId) const;
QList<RunnerInfo> discoverRunners() const;
QString defaultProtonExecutable() const;
@ -81,4 +93,6 @@ private:
QHash<QString, QPointer<QNetworkReply>> m_installReplies;
QHash<QString, QPointer<QProcess>> m_installProcesses;
QHash<QString, QVariantMap> m_gameProfiles;
};

View file

@ -13,6 +13,7 @@
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDir>
#include <QFileInfo>
#include <QProcess>
@ -26,6 +27,52 @@ 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");
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;
}
}
static QString stableIdForUrl(const QString &type, const QString &url)
{
const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex();
@ -80,6 +127,8 @@ RunnerManagerClient::RunnerManagerClient(QObject *parent)
SLOT(onInstallProgress(QString, qlonglong, qlonglong)));
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallFinished"), this, SLOT(onInstallFinished(QString, QVariantMap)));
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("GameProfilesChanged"), this, SLOT(onGameProfilesChanged()));
}
if (QCoreApplication::instance()) {
@ -89,6 +138,7 @@ RunnerManagerClient::RunnerManagerClient(QObject *parent)
}
refreshRunners();
refreshGameProfiles();
}
RunnerManagerClient::~RunnerManagerClient()
@ -213,6 +263,11 @@ QVariantList RunnerManagerClient::runners() const
return m_runners;
}
QVariantList RunnerManagerClient::gameProfiles() const
{
return m_gameProfiles;
}
static QString normalizeHex(QString s)
{
s = s.trimmed().toLower();
@ -417,6 +472,11 @@ void RunnerManagerClient::onInstallFinished(const QString &installId, const QVar
refreshRunners();
}
void RunnerManagerClient::onGameProfilesChanged()
{
refreshGameProfiles();
}
void RunnerManagerClient::refreshRunners()
{
ensureRunnerDaemon();
@ -472,6 +532,179 @@ void RunnerManagerClient::refreshRunners()
});
}
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)();
}
void RunnerManagerClient::uninstallRunner(const QString &runnerId)
{
const QString id = runnerId.trimmed();

View file

@ -17,6 +17,7 @@ class RunnerManagerClient : public QObject
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged)
Q_PROPERTY(QVariantList gameProfiles READ gameProfiles NOTIFY gameProfilesChanged)
public:
explicit RunnerManagerClient(QObject *parent = nullptr);
@ -29,6 +30,7 @@ public:
QString status() const;
QString lastError() const;
QVariantList runners() const;
QVariantList gameProfiles() const;
Q_INVOKABLE void installRunnerFromUrl(const QString &url,
const QString &sha256 = QString(),
@ -41,6 +43,11 @@ public:
Q_INVOKABLE void refreshRunners();
Q_INVOKABLE void uninstallRunner(const QString &runnerId);
Q_INVOKABLE void refreshGameProfiles();
Q_INVOKABLE void requestGameProfile(const QString &gameId);
Q_INVOKABLE void setGameProfile(const QVariantMap &spec);
Q_INVOKABLE void clearGameProfile(const QString &gameId);
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());
@ -52,14 +59,21 @@ Q_SIGNALS:
void statusChanged();
void lastErrorChanged();
void runnersChanged();
void gameProfilesChanged();
void prefixEnsured(const QString &gameId, const QString &prefixPath);
void prefixDeleted(const QString &gameId, const QString &prefixPath);
void gameProfileFetched(const QString &gameId, const QVariantMap &result);
void gameProfileSaved(const QString &gameId, const QVariantMap &result);
void gameProfileCleared(const QString &gameId, const QVariantMap &result);
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);
void onGameProfilesChanged();
private:
void ensureRunnerDaemon();
void shutdownSpawnedRunnerDaemon();
@ -77,8 +91,10 @@ private:
QString m_lastError;
QVariantList m_runners;
QVariantList m_gameProfiles;
bool m_refreshRetryPending = false;
bool m_refreshProfilesRetryPending = false;
class QProcess *m_runnerdProcess = nullptr;
bool m_runnerdSpawnAttempted = false;