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