mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-02-10 05:13:08 +00:00
Runner: add Runner1 daemon + client
This commit is contained in:
parent
d87c227347
commit
1f1be64c8f
11 changed files with 1950 additions and 0 deletions
|
|
@ -10,6 +10,7 @@ add_executable(alakarte
|
||||||
gamepadmanager.cpp
|
gamepadmanager.cpp
|
||||||
inputmanager.cpp
|
inputmanager.cpp
|
||||||
mediamanager.cpp
|
mediamanager.cpp
|
||||||
|
runnermanagerclient.cpp
|
||||||
screenshotmodel.cpp
|
screenshotmodel.cpp
|
||||||
platformimporter.cpp
|
platformimporter.cpp
|
||||||
steamimporter.cpp
|
steamimporter.cpp
|
||||||
|
|
@ -139,3 +140,5 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
|
||||||
install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
|
|
||||||
add_subdirectory(gamecenter)
|
add_subdirectory(gamecenter)
|
||||||
|
|
||||||
|
add_subdirectory(runner)
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ App::App(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_gameModel(new GameModel(this))
|
, m_gameModel(new GameModel(this))
|
||||||
, m_launcher(new GameLauncher(this))
|
, m_launcher(new GameLauncher(this))
|
||||||
|
, m_runnerManager(new RunnerManagerClient(this))
|
||||||
, m_steamGridDB(new SteamGridDB(this))
|
, m_steamGridDB(new SteamGridDB(this))
|
||||||
, m_mediaManager(new MediaManager(this))
|
, m_mediaManager(new MediaManager(this))
|
||||||
, m_config(new Config(this))
|
, m_config(new Config(this))
|
||||||
|
|
@ -169,6 +170,11 @@ GameLauncher *App::launcher() const
|
||||||
return m_launcher;
|
return m_launcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RunnerManagerClient *App::runnerManager() const
|
||||||
|
{
|
||||||
|
return m_runnerManager;
|
||||||
|
}
|
||||||
|
|
||||||
SteamGridDB *App::steamGridDB() const
|
SteamGridDB *App::steamGridDB() const
|
||||||
{
|
{
|
||||||
return m_steamGridDB;
|
return m_steamGridDB;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
#include "gamelauncher.h"
|
#include "gamelauncher.h"
|
||||||
#include "gamemodel.h"
|
#include "gamemodel.h"
|
||||||
#include "mediamanager.h"
|
#include "mediamanager.h"
|
||||||
|
#include "runnermanagerclient.h"
|
||||||
#include "steamgriddb.h"
|
#include "steamgriddb.h"
|
||||||
|
|
||||||
class App : public QObject
|
class App : public QObject
|
||||||
|
|
@ -22,6 +23,7 @@ class App : public QObject
|
||||||
|
|
||||||
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
||||||
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
||||||
|
Q_PROPERTY(RunnerManagerClient *runnerManager READ runnerManager CONSTANT)
|
||||||
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
||||||
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
||||||
Q_PROPERTY(Config *config READ config CONSTANT)
|
Q_PROPERTY(Config *config READ config CONSTANT)
|
||||||
|
|
@ -34,6 +36,7 @@ public:
|
||||||
|
|
||||||
GameModel *gameModel() const;
|
GameModel *gameModel() const;
|
||||||
GameLauncher *launcher() const;
|
GameLauncher *launcher() const;
|
||||||
|
RunnerManagerClient *runnerManager() const;
|
||||||
SteamGridDB *steamGridDB() const;
|
SteamGridDB *steamGridDB() const;
|
||||||
MediaManager *mediaManager() const;
|
MediaManager *mediaManager() const;
|
||||||
Config *config() const;
|
Config *config() const;
|
||||||
|
|
@ -74,6 +77,7 @@ private:
|
||||||
|
|
||||||
GameModel *m_gameModel;
|
GameModel *m_gameModel;
|
||||||
GameLauncher *m_launcher;
|
GameLauncher *m_launcher;
|
||||||
|
RunnerManagerClient *m_runnerManager;
|
||||||
SteamGridDB *m_steamGridDB;
|
SteamGridDB *m_steamGridDB;
|
||||||
MediaManager *m_mediaManager;
|
MediaManager *m_mediaManager;
|
||||||
Config *m_config;
|
Config *m_config;
|
||||||
|
|
|
||||||
41
src/runner/CMakeLists.txt
Normal file
41
src/runner/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
# SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
add_executable(alakarte_runnerd
|
||||||
|
main.cpp
|
||||||
|
runnermanagerdaemon.cpp
|
||||||
|
runnermanagerdaemon.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(alakarte_runnerd PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::DBus
|
||||||
|
Qt6::Network
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(alakarte_runnerd PROPERTIES
|
||||||
|
OUTPUT_NAME "alakarte-runnerd"
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS alakarte_runnerd ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
|
|
||||||
|
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
|
||||||
|
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
|
||||||
|
|
||||||
|
configure_file(dbus/org.kde.ALaKarte.Runner1.service.in
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Runner1.service"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
|
||||||
|
configure_file(systemd/org.kde.ALaKarte.Runner1.service.in
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Runner1.service"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Runner1.service"
|
||||||
|
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Runner1.service"
|
||||||
|
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
|
||||||
|
)
|
||||||
4
src/runner/dbus/org.kde.ALaKarte.Runner1.service.in
Normal file
4
src/runner/dbus/org.kde.ALaKarte.Runner1.service.in
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[D-BUS Service]
|
||||||
|
Name=org.kde.ALaKarte.Runner1
|
||||||
|
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-runnerd
|
||||||
|
SystemdService=org.kde.ALaKarte.Runner1.service
|
||||||
20
src/runner/main.cpp
Normal file
20
src/runner/main.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
|
||||||
|
#include "runnermanagerdaemon.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
|
||||||
|
|
||||||
|
RunnerManagerDaemon daemon;
|
||||||
|
if (!daemon.init()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
1091
src/runner/runnermanagerdaemon.cpp
Normal file
1091
src/runner/runnermanagerdaemon.cpp
Normal file
File diff suppressed because it is too large
Load diff
84
src/runner/runnermanagerdaemon.h
Normal file
84
src/runner/runnermanagerdaemon.h
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
class QNetworkReply;
|
||||||
|
class QProcess;
|
||||||
|
|
||||||
|
class RunnerManagerDaemon : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_CLASSINFO("D-Bus Interface", "org.kde.ALaKarte.Runner1")
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit RunnerManagerDaemon(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
bool init();
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
QString Ping() const;
|
||||||
|
void Version(uint &major, uint &minor) const;
|
||||||
|
|
||||||
|
QVariantList ListRunners();
|
||||||
|
bool Rescan();
|
||||||
|
|
||||||
|
bool RegisterRunner(const QVariantMap &runner);
|
||||||
|
bool UnregisterRunner(const QString &runnerId);
|
||||||
|
|
||||||
|
QVariantMap InstallRunnerFromArchive(const QVariantMap &spec);
|
||||||
|
QVariantMap InstallRunnerFromUrl(const QVariantMap &spec);
|
||||||
|
QVariantMap UninstallRunner(const QString &runnerId);
|
||||||
|
|
||||||
|
bool CancelInstall(const QString &installId);
|
||||||
|
|
||||||
|
QVariantMap EnsurePrefix(const QVariantMap &spec);
|
||||||
|
QVariantMap DeletePrefix(const QVariantMap &spec);
|
||||||
|
QVariantMap ResolveLaunch(const QVariantMap &spec) const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void InstallStarted(const QString &installId, const QVariantMap &spec);
|
||||||
|
void InstallProgress(const QString &installId, qint64 receivedBytes, qint64 totalBytes);
|
||||||
|
void InstallFinished(const QString &installId, const QVariantMap &result);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct RunnerInfo {
|
||||||
|
QString id;
|
||||||
|
QString type;
|
||||||
|
QString name;
|
||||||
|
QString path;
|
||||||
|
QString source;
|
||||||
|
QString installDir;
|
||||||
|
|
||||||
|
QVariantMap toVariantMap() const;
|
||||||
|
static RunnerInfo fromVariantMap(const QVariantMap &map);
|
||||||
|
bool isValid() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
QString registryPath() const;
|
||||||
|
void loadRegistry();
|
||||||
|
bool saveRegistry() const;
|
||||||
|
|
||||||
|
QList<RunnerInfo> discoverRunners() const;
|
||||||
|
QString defaultProtonExecutable() const;
|
||||||
|
|
||||||
|
static QStringList steamCandidateRoots();
|
||||||
|
static QString findSteamClientInstallPathFromProton(const QString &protonExe);
|
||||||
|
|
||||||
|
RunnerInfo runnerById(const QString &runnerId) const;
|
||||||
|
|
||||||
|
QList<RunnerInfo> m_registered;
|
||||||
|
QList<RunnerInfo> m_discovered;
|
||||||
|
|
||||||
|
QSet<QString> m_activeInstalls;
|
||||||
|
QSet<QString> m_cancelRequested;
|
||||||
|
|
||||||
|
QHash<QString, QPointer<QNetworkReply>> m_installReplies;
|
||||||
|
QHash<QString, QPointer<QProcess>> m_installProcesses;
|
||||||
|
};
|
||||||
8
src/runner/systemd/org.kde.ALaKarte.Runner1.service.in
Normal file
8
src/runner/systemd/org.kde.ALaKarte.Runner1.service.in
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
[Unit]
|
||||||
|
Description=A-La-Karte Runner Manager
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-runnerd
|
||||||
|
Type=dbus
|
||||||
|
BusName=org.kde.ALaKarte.Runner1
|
||||||
|
Restart=no
|
||||||
604
src/runnermanagerclient.cpp
Normal file
604
src/runnermanagerclient.cpp
Normal file
|
|
@ -0,0 +1,604 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include "runnermanagerclient.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QDBusArgument>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QDBusMetaType>
|
||||||
|
#include <QDBusPendingCall>
|
||||||
|
#include <QDBusPendingCallWatcher>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||||
|
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
|
||||||
|
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||||
|
|
||||||
|
static QString stableIdForUrl(const QString &type, const QString &url)
|
||||||
|
{
|
||||||
|
const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex();
|
||||||
|
return type + QStringLiteral(":url:") + QString::fromLatin1(digest);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString stableIdKeyForInputUrl(QString urlString)
|
||||||
|
{
|
||||||
|
urlString = urlString.trimmed();
|
||||||
|
if (urlString.startsWith(QLatin1String("~/"))) {
|
||||||
|
urlString = QDir::homePath() + QLatin1Char('/') + urlString.mid(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
QUrl url(urlString);
|
||||||
|
if (url.isValid() && url.scheme().toLower() == QLatin1String("file")) {
|
||||||
|
QFileInfo fi(url.toLocalFile());
|
||||||
|
const QString canonical = fi.canonicalFilePath();
|
||||||
|
const QString abs = fi.absoluteFilePath();
|
||||||
|
const QString path = canonical.isEmpty() ? abs : canonical;
|
||||||
|
return QUrl::fromLocalFile(path).toString(QUrl::FullyEncoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.scheme().isEmpty()) {
|
||||||
|
QFileInfo fi(urlString);
|
||||||
|
if (fi.isAbsolute() || urlString.startsWith(QLatin1String("./")) || urlString.startsWith(QLatin1String("../"))) {
|
||||||
|
const QString canonical = fi.exists() ? fi.canonicalFilePath() : QString();
|
||||||
|
const QString abs = fi.absoluteFilePath();
|
||||||
|
const QString path = canonical.isEmpty() ? abs : canonical;
|
||||||
|
return QUrl::fromLocalFile(path).toString(QUrl::FullyEncoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.isValid() && !url.scheme().isEmpty()) {
|
||||||
|
return url.toString(QUrl::FullyEncoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlString;
|
||||||
|
}
|
||||||
|
|
||||||
|
RunnerManagerClient::RunnerManagerClient(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (bus.isConnected()) {
|
||||||
|
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallStarted"), this, SLOT(onInstallStarted(QString, QVariantMap)));
|
||||||
|
|
||||||
|
bus.connect(kRunnerService,
|
||||||
|
kRunnerPath,
|
||||||
|
kRunnerInterface,
|
||||||
|
QStringLiteral("InstallProgress"),
|
||||||
|
this,
|
||||||
|
SLOT(onInstallProgress(QString, qlonglong, qlonglong)));
|
||||||
|
|
||||||
|
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallFinished"), this, SLOT(onInstallFinished(QString, QVariantMap)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QCoreApplication::instance()) {
|
||||||
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||||
|
shutdownSpawnedRunnerDaemon();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshRunners();
|
||||||
|
}
|
||||||
|
|
||||||
|
RunnerManagerClient::~RunnerManagerClient()
|
||||||
|
{
|
||||||
|
shutdownSpawnedRunnerDaemon();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::shutdownSpawnedRunnerDaemon()
|
||||||
|
{
|
||||||
|
if (!m_runnerdProcess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_runnerdProcess->state() == QProcess::NotRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_runnerdProcess->terminate();
|
||||||
|
if (!m_runnerdProcess->waitForFinished(1000)) {
|
||||||
|
m_runnerdProcess->kill();
|
||||||
|
m_runnerdProcess->waitForFinished(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::ensureRunnerDaemon()
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected() || !bus.interface()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bus.interface()->isServiceRegistered(kRunnerService)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool activatable = false;
|
||||||
|
{
|
||||||
|
const QDBusReply<QStringList> names = bus.interface()->activatableServiceNames();
|
||||||
|
if (names.isValid()) {
|
||||||
|
activatable = names.value().contains(kRunnerService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDBusReply<void> reply = bus.interface()->startService(kRunnerService);
|
||||||
|
if (reply.isValid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bus.interface()->isServiceRegistered(kRunnerService)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activatable) {
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(QT_NO_DEBUG)
|
||||||
|
setLastError(QStringLiteral("Runner service is not available (missing DBus activation for org.kde.ALaKarte.Runner1)"));
|
||||||
|
return;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (m_runnerdSpawnAttempted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_runnerdSpawnAttempted = true;
|
||||||
|
|
||||||
|
QString program = QCoreApplication::applicationDirPath() + QLatin1String("/alakarte-runnerd");
|
||||||
|
if (!QFileInfo::exists(program)) {
|
||||||
|
program = QStringLiteral("alakarte-runnerd");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_runnerdProcess) {
|
||||||
|
m_runnerdProcess = new QProcess(this);
|
||||||
|
m_runnerdProcess->setProcessChannelMode(QProcess::MergedChannels);
|
||||||
|
connect(m_runnerdProcess, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
|
||||||
|
setLastError(QStringLiteral("Failed to start runner service"));
|
||||||
|
});
|
||||||
|
connect(m_runnerdProcess, &QProcess::finished, this, [this](int, QProcess::ExitStatus) {
|
||||||
|
m_runnerdProcess->deleteLater();
|
||||||
|
m_runnerdProcess = nullptr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
m_runnerdProcess->setProgram(program);
|
||||||
|
m_runnerdProcess->setArguments({});
|
||||||
|
m_runnerdProcess->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RunnerManagerClient::busy() const
|
||||||
|
{
|
||||||
|
return m_busy;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RunnerManagerClient::installId() const
|
||||||
|
{
|
||||||
|
return m_installId;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 RunnerManagerClient::receivedBytes() const
|
||||||
|
{
|
||||||
|
return m_receivedBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 RunnerManagerClient::totalBytes() const
|
||||||
|
{
|
||||||
|
return m_totalBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RunnerManagerClient::status() const
|
||||||
|
{
|
||||||
|
return m_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString RunnerManagerClient::lastError() const
|
||||||
|
{
|
||||||
|
return m_lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList RunnerManagerClient::runners() const
|
||||||
|
{
|
||||||
|
return m_runners;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString normalizeHex(QString s)
|
||||||
|
{
|
||||||
|
s = s.trimmed().toLower();
|
||||||
|
const int colon = s.indexOf(QLatin1Char(':'));
|
||||||
|
if (colon >= 0) {
|
||||||
|
const QString prefix = s.left(colon);
|
||||||
|
if (prefix == QLatin1String("sha256") || prefix == QLatin1String("sha512")) {
|
||||||
|
s = s.mid(colon + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString out;
|
||||||
|
out.reserve(s.size());
|
||||||
|
for (const QChar &c : s) {
|
||||||
|
if ((c >= QLatin1Char('0') && c <= QLatin1Char('9')) || (c >= QLatin1Char('a') && c <= QLatin1Char('f'))) {
|
||||||
|
out.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::installRunnerFromUrl(const QString &url, const QString &sha256, const QString &name, const QString &type, bool overwrite)
|
||||||
|
{
|
||||||
|
const QString trimmedUrl = url.trimmed();
|
||||||
|
if (trimmedUrl.isEmpty() || m_busy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString effectiveType = type.trimmed().isEmpty() ? QStringLiteral("proton") : type.trimmed();
|
||||||
|
|
||||||
|
setLastError(QString());
|
||||||
|
setProgress(0, 0);
|
||||||
|
setStatus(QStringLiteral("Starting..."));
|
||||||
|
|
||||||
|
const QString newInstallId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
setInstallId(newInstallId);
|
||||||
|
setBusy(true);
|
||||||
|
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("installId"), newInstallId);
|
||||||
|
spec.insert(QStringLiteral("url"), trimmedUrl);
|
||||||
|
spec.insert(QStringLiteral("type"), effectiveType);
|
||||||
|
spec.insert(QStringLiteral("name"), name.trimmed());
|
||||||
|
spec.insert(QStringLiteral("overwrite"), overwrite);
|
||||||
|
|
||||||
|
const QString expected = normalizeHex(sha256);
|
||||||
|
if (!expected.isEmpty()) {
|
||||||
|
if (expected.size() == 128) {
|
||||||
|
spec.insert(QStringLiteral("sha512"), expected);
|
||||||
|
} else {
|
||||||
|
spec.insert(QStringLiteral("sha256"), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString stableId = stableIdForUrl(effectiveType, stableIdKeyForInputUrl(trimmedUrl));
|
||||||
|
spec.insert(QStringLiteral("id"), stableId);
|
||||||
|
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
|
*callPtr = [this, spec, attempts, callPtr]() {
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("InstallRunnerFromUrl"), spec);
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, spec, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
w->deleteLater();
|
||||||
|
if (reply.isError()) {
|
||||||
|
const QDBusError::ErrorType t = reply.error().type();
|
||||||
|
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
|
||||||
|
(*attempts)++;
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
QTimer::singleShot(500, this, [callPtr]() {
|
||||||
|
(*callPtr)();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(QStringLiteral("Failed to call InstallRunnerFromUrl"));
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
setBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap result = reply.value();
|
||||||
|
const bool ok = result.value(QStringLiteral("ok")).toBool();
|
||||||
|
if (!ok) {
|
||||||
|
const QString err = result.value(QStringLiteral("error")).toString();
|
||||||
|
setLastError(err);
|
||||||
|
setStatus(err.isEmpty() ? QStringLiteral("Failed") : err);
|
||||||
|
setBusy(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(QStringLiteral("Started"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
(*callPtr)();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::deletePrefix(const QString &gameId, const QString &prefixPath)
|
||||||
|
{
|
||||||
|
const QString id = gameId.trimmed();
|
||||||
|
const QString p = prefixPath.trimmed();
|
||||||
|
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("gameId"), id);
|
||||||
|
if (!p.isEmpty()) {
|
||||||
|
spec.insert(QStringLiteral("prefixPath"), p);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
|
*callPtr = [this, spec, id, attempts, callPtr]() {
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("DeletePrefix"), spec);
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
w->deleteLater();
|
||||||
|
if (reply.isError()) {
|
||||||
|
const QDBusError::ErrorType t = reply.error().type();
|
||||||
|
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
|
||||||
|
(*attempts)++;
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
QTimer::singleShot(500, this, [callPtr]() {
|
||||||
|
(*callPtr)();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap result = reply.value();
|
||||||
|
if (!result.value(QStringLiteral("ok")).toBool()) {
|
||||||
|
setLastError(result.value(QStringLiteral("error")).toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString removed = result.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
Q_EMIT prefixDeleted(id, removed);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
(*callPtr)();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::cancelCurrentInstall()
|
||||||
|
{
|
||||||
|
if (!m_busy || m_installId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(QStringLiteral("Cancelling..."));
|
||||||
|
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
iface.asyncCall(QStringLiteral("CancelInstall"), m_installId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::onInstallStarted(const QString &installId, const QVariantMap &)
|
||||||
|
{
|
||||||
|
if (installId.isEmpty() || installId != m_installId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus(QStringLiteral("Downloading..."));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::onInstallProgress(const QString &installId, qlonglong received, qlonglong total)
|
||||||
|
{
|
||||||
|
if (installId.isEmpty() || installId != m_installId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setProgress(static_cast<qint64>(received), static_cast<qint64>(total));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::onInstallFinished(const QString &installId, const QVariantMap &result)
|
||||||
|
{
|
||||||
|
if (installId.isEmpty() || installId != m_installId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool ok = result.value(QStringLiteral("ok")).toBool();
|
||||||
|
if (!ok) {
|
||||||
|
const QString err = result.value(QStringLiteral("error")).toString();
|
||||||
|
setLastError(err);
|
||||||
|
setStatus(err.isEmpty() ? QStringLiteral("Failed") : err);
|
||||||
|
} else {
|
||||||
|
setStatus(QStringLiteral("Installed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
setBusy(false);
|
||||||
|
|
||||||
|
refreshRunners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::refreshRunners()
|
||||||
|
{
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListRunners"));
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
||||||
|
QDBusPendingReply<QVariantList> reply = *w;
|
||||||
|
w->deleteLater();
|
||||||
|
if (reply.isError()) {
|
||||||
|
if (!m_refreshRetryPending && (reply.error().type() == QDBusError::ServiceUnknown || reply.error().type() == QDBusError::NoReply)) {
|
||||||
|
m_refreshRetryPending = true;
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
QTimer::singleShot(500, this, [this]() {
|
||||||
|
m_refreshRetryPending = false;
|
||||||
|
refreshRunners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantList raw = reply.value();
|
||||||
|
|
||||||
|
QVariantList list;
|
||||||
|
list.reserve(raw.size());
|
||||||
|
for (const QVariant &v : raw) {
|
||||||
|
if (v.metaType() == QMetaType::fromType<QVariantMap>()) {
|
||||||
|
list.push_back(v.toMap());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
const QVariantMap map = qdbus_cast<QVariantMap>(arg);
|
||||||
|
list.push_back(map);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (v.canConvert<QVariantMap>()) {
|
||||||
|
list.push_back(v.toMap());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.push_back(QVariantMap{});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_runners == list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_runners = list;
|
||||||
|
Q_EMIT runnersChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::uninstallRunner(const QString &runnerId)
|
||||||
|
{
|
||||||
|
const QString id = runnerId.trimmed();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("UninstallRunner"), id);
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
||||||
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
w->deleteLater();
|
||||||
|
if (reply.isError()) {
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap result = reply.value();
|
||||||
|
if (!result.value(QStringLiteral("ok")).toBool()) {
|
||||||
|
setLastError(result.value(QStringLiteral("error")).toString());
|
||||||
|
}
|
||||||
|
refreshRunners();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath)
|
||||||
|
{
|
||||||
|
const QString id = gameId.trimmed();
|
||||||
|
const QString r = runner.trimmed();
|
||||||
|
const QString p = prefixPath.trimmed();
|
||||||
|
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("gameId"), id);
|
||||||
|
if (!r.isEmpty()) {
|
||||||
|
spec.insert(QStringLiteral("runner"), r);
|
||||||
|
}
|
||||||
|
if (!p.isEmpty()) {
|
||||||
|
spec.insert(QStringLiteral("prefixPath"), p);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
|
*callPtr = [this, spec, id, attempts, callPtr]() {
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("EnsurePrefix"), spec);
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
w->deleteLater();
|
||||||
|
if (reply.isError()) {
|
||||||
|
const QDBusError::ErrorType t = reply.error().type();
|
||||||
|
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
|
||||||
|
(*attempts)++;
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
QTimer::singleShot(500, this, [callPtr]() {
|
||||||
|
(*callPtr)();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap result = reply.value();
|
||||||
|
if (!result.value(QStringLiteral("ok")).toBool()) {
|
||||||
|
setLastError(result.value(QStringLiteral("error")).toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString ensured = result.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
if (!ensured.isEmpty()) {
|
||||||
|
Q_EMIT prefixEnsured(id, ensured);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
(*callPtr)();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::setBusy(bool busy)
|
||||||
|
{
|
||||||
|
if (m_busy == busy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_busy = busy;
|
||||||
|
Q_EMIT busyChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::setInstallId(const QString &installId)
|
||||||
|
{
|
||||||
|
if (m_installId == installId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_installId = installId;
|
||||||
|
Q_EMIT installIdChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::setProgress(qint64 received, qint64 total)
|
||||||
|
{
|
||||||
|
if (m_receivedBytes == received && m_totalBytes == total) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_receivedBytes = received;
|
||||||
|
m_totalBytes = total;
|
||||||
|
Q_EMIT progressChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::setStatus(const QString &status)
|
||||||
|
{
|
||||||
|
if (m_status == status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_status = status;
|
||||||
|
Q_EMIT statusChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::setLastError(const QString &error)
|
||||||
|
{
|
||||||
|
if (m_lastError == error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_lastError = error;
|
||||||
|
Q_EMIT lastErrorChanged();
|
||||||
|
}
|
||||||
85
src/runnermanagerclient.h
Normal file
85
src/runnermanagerclient.h
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariantList>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
class RunnerManagerClient : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||||
|
Q_PROPERTY(QString installId READ installId NOTIFY installIdChanged)
|
||||||
|
Q_PROPERTY(qint64 receivedBytes READ receivedBytes NOTIFY progressChanged)
|
||||||
|
Q_PROPERTY(qint64 totalBytes READ totalBytes NOTIFY progressChanged)
|
||||||
|
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
|
||||||
|
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||||
|
Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit RunnerManagerClient(QObject *parent = nullptr);
|
||||||
|
~RunnerManagerClient() override;
|
||||||
|
|
||||||
|
bool busy() const;
|
||||||
|
QString installId() const;
|
||||||
|
qint64 receivedBytes() const;
|
||||||
|
qint64 totalBytes() const;
|
||||||
|
QString status() const;
|
||||||
|
QString lastError() const;
|
||||||
|
QVariantList runners() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void installRunnerFromUrl(const QString &url,
|
||||||
|
const QString &sha256 = QString(),
|
||||||
|
const QString &name = QString(),
|
||||||
|
const QString &type = QStringLiteral("proton"),
|
||||||
|
bool overwrite = true);
|
||||||
|
|
||||||
|
Q_INVOKABLE void cancelCurrentInstall();
|
||||||
|
|
||||||
|
Q_INVOKABLE void refreshRunners();
|
||||||
|
Q_INVOKABLE void uninstallRunner(const QString &runnerId);
|
||||||
|
|
||||||
|
Q_INVOKABLE void ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath = QString());
|
||||||
|
|
||||||
|
Q_INVOKABLE void deletePrefix(const QString &gameId, const QString &prefixPath = QString());
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void busyChanged();
|
||||||
|
void installIdChanged();
|
||||||
|
void progressChanged();
|
||||||
|
void statusChanged();
|
||||||
|
void lastErrorChanged();
|
||||||
|
void runnersChanged();
|
||||||
|
void prefixEnsured(const QString &gameId, const QString &prefixPath);
|
||||||
|
void prefixDeleted(const QString &gameId, const QString &prefixPath);
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void onInstallStarted(const QString &installId, const QVariantMap &spec);
|
||||||
|
void onInstallProgress(const QString &installId, qlonglong received, qlonglong total);
|
||||||
|
void onInstallFinished(const QString &installId, const QVariantMap &result);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ensureRunnerDaemon();
|
||||||
|
void shutdownSpawnedRunnerDaemon();
|
||||||
|
void setBusy(bool busy);
|
||||||
|
void setInstallId(const QString &installId);
|
||||||
|
void setProgress(qint64 received, qint64 total);
|
||||||
|
void setStatus(const QString &status);
|
||||||
|
void setLastError(const QString &error);
|
||||||
|
|
||||||
|
bool m_busy = false;
|
||||||
|
QString m_installId;
|
||||||
|
qint64 m_receivedBytes = 0;
|
||||||
|
qint64 m_totalBytes = 0;
|
||||||
|
QString m_status;
|
||||||
|
QString m_lastError;
|
||||||
|
|
||||||
|
QVariantList m_runners;
|
||||||
|
|
||||||
|
bool m_refreshRetryPending = false;
|
||||||
|
|
||||||
|
class QProcess *m_runnerdProcess = nullptr;
|
||||||
|
bool m_runnerdSpawnAttempted = false;
|
||||||
|
};
|
||||||
Loading…
Reference in a new issue