mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-27 01:03:09 +00:00
1092 lines
36 KiB
C++
1092 lines
36 KiB
C++
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
|
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||
|
|
|
||
|
|
#include "runnermanagerdaemon.h"
|
||
|
|
|
||
|
|
#include <QCryptographicHash>
|
||
|
|
#include <QDBusConnection>
|
||
|
|
#include <QDBusError>
|
||
|
|
#include <QDBusMessage>
|
||
|
|
#include <QDBusReply>
|
||
|
|
#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 <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 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();
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!bus.registerObject(kRunnerPath, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) {
|
||
|
|
qWarning() << "RunnerManagerDaemon: failed to register object" << kRunnerPath << bus.lastError().message();
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
QString RunnerManagerDaemon::Ping() const
|
||
|
|
{
|
||
|
|
return QStringLiteral("ok");
|
||
|
|
}
|
||
|
|
|
||
|
|
void RunnerManagerDaemon::Version(uint &major, uint &minor) const
|
||
|
|
{
|
||
|
|
major = 1;
|
||
|
|
minor = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
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();
|
||
|
|
QDir().mkpath(QFileInfo(path).absolutePath());
|
||
|
|
|
||
|
|
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);
|
||
|
|
|
||
|
|
QFile f(path);
|
||
|
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
f.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
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::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();
|
||
|
|
|
||
|
|
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 gameId = spec.value(QStringLiteral("gameId")).toString();
|
||
|
|
const QString program = spec.value(QStringLiteral("program")).toString();
|
||
|
|
const QStringList args = spec.value(QStringLiteral("args")).toStringList();
|
||
|
|
QString prefixPath = spec.value(QStringLiteral("prefixPath")).toString();
|
||
|
|
|
||
|
|
if (program.isEmpty()) {
|
||
|
|
out.insert(QStringLiteral("error"), QStringLiteral("missing program"));
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
QVariantMap effectiveEnv = spec.value(QStringLiteral("envOverrides")).toMap();
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
out.insert(QStringLiteral("finalProgram"), finalProgram);
|
||
|
|
out.insert(QStringLiteral("finalArgs"), finalArgs);
|
||
|
|
out.insert(QStringLiteral("effectiveEnv"), effectiveEnv);
|
||
|
|
out.insert(QStringLiteral("ok"), true);
|
||
|
|
|
||
|
|
return out;
|
||
|
|
}
|