2026-02-09 13:01:15 +00:00
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
# include "runnermanagerdaemon.h"
2026-02-15 12:55:50 +00:00
# include "runner1adaptor.h"
2026-02-09 13:01:15 +00:00
# include <QCryptographicHash>
2026-02-12 13:44:20 +00:00
# include <QDBusArgument>
2026-02-09 13:01:15 +00:00
# include <QDBusConnection>
# include <QDBusError>
# include <QDBusMessage>
# include <QDBusReply>
2026-02-12 13:44:20 +00:00
# include <QDBusVariant>
2026-02-09 13:01:15 +00:00
# include <QDir>
# include <QDirIterator>
# include <QEventLoop>
# include <QFile>
# include <QFileInfo>
# include <QJsonArray>
# include <QJsonDocument>
# include <QJsonObject>
# include <QNetworkAccessManager>
# include <QNetworkReply>
# include <QNetworkRequest>
# include <QProcess>
2026-02-13 12:11:49 +00:00
# include <QSaveFile>
2026-02-09 13:01:15 +00:00
# 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 " ) ;
2026-02-12 13:44:20 +00:00
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 { } ;
}
2026-02-09 13:01:15 +00:00
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 ( ) ;
2026-02-12 13:44:20 +00:00
loadGameProfiles ( ) ;
2026-02-09 13:01:15 +00:00
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 ;
}
2026-02-15 12:55:50 +00:00
new Runner1Adaptor ( this ) ;
if ( ! bus . registerObject ( kRunnerPath , this , QDBusConnection : : ExportAdaptors ) ) {
2026-02-09 13:01:15 +00:00
qWarning ( ) < < " RunnerManagerDaemon: failed to register object " < < kRunnerPath < < bus . lastError ( ) . message ( ) ;
return false ;
}
return true ;
}
QString RunnerManagerDaemon : : Ping ( ) const
{
return QStringLiteral ( " ok " ) ;
}
2026-02-15 12:55:50 +00:00
uint RunnerManagerDaemon : : Version ( uint & minor ) const
2026-02-09 13:01:15 +00:00
{
minor = 0 ;
2026-02-15 12:55:50 +00:00
return 1 ;
2026-02-09 13:01:15 +00:00
}
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 ( ) ;
2026-02-13 12:11:49 +00:00
const QString dirPath = QFileInfo ( path ) . absolutePath ( ) ;
if ( ! QDir ( ) . mkpath ( dirPath ) ) {
return false ;
}
2026-02-09 13:01:15 +00:00
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 ) ;
2026-02-13 12:11:49 +00:00
QSaveFile f ( path ) ;
2026-02-09 13:01:15 +00:00
if ( ! f . open ( QIODevice : : WriteOnly | QIODevice : : Truncate ) ) {
return false ;
}
2026-02-13 12:11:49 +00:00
const QByteArray payload = QJsonDocument ( root ) . toJson ( QJsonDocument : : Indented ) ;
if ( f . write ( payload ) ! = payload . size ( ) ) {
return false ;
}
return f . commit ( ) ;
2026-02-09 13:01:15 +00:00
}
2026-02-12 13:44:20 +00:00
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 ( ) ;
2026-02-13 12:11:49 +00:00
const QString dirPath = QFileInfo ( path ) . absolutePath ( ) ;
if ( ! QDir ( ) . mkpath ( dirPath ) ) {
return false ;
}
2026-02-12 13:44:20 +00:00
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 ) ;
2026-02-13 12:11:49 +00:00
QSaveFile f ( path ) ;
2026-02-12 13:44:20 +00:00
if ( ! f . open ( QIODevice : : WriteOnly | QIODevice : : Truncate ) ) {
return false ;
}
2026-02-13 12:11:49 +00:00
const QByteArray payload = QJsonDocument ( root ) . toJson ( QJsonDocument : : Indented ) ;
if ( f . write ( payload ) ! = payload . size ( ) ) {
return false ;
}
return f . commit ( ) ;
2026-02-12 13:44:20 +00:00
}
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 ( ) ;
}
2026-02-09 13:01:15 +00:00
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 ;
}
2026-02-12 13:44:20 +00:00
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 ;
}
2026-02-09 13:01:15 +00:00
QVariantMap RunnerManagerDaemon : : ResolveLaunch ( const QVariantMap & spec ) const
{
QVariantMap out ;
out . insert ( QStringLiteral ( " ok " ) , false ) ;
2026-02-12 13:44:20 +00:00
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 ( ) ;
}
2026-02-09 13:01:15 +00:00
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 ( ) ;
2026-02-12 13:44:20 +00:00
QStringList args = unwrapStringList ( spec . value ( QStringLiteral ( " args " ) ) ) ;
2026-02-09 13:01:15 +00:00
QString prefixPath = spec . value ( QStringLiteral ( " prefixPath " ) ) . toString ( ) ;
2026-02-12 13:44:20 +00:00
if ( prefixPath . isEmpty ( ) & & profile . contains ( QStringLiteral ( " prefixPath " ) ) ) {
prefixPath = profile . value ( QStringLiteral ( " prefixPath " ) ) . toString ( ) ;
}
2026-02-09 13:01:15 +00:00
if ( program . isEmpty ( ) ) {
out . insert ( QStringLiteral ( " error " ) , QStringLiteral ( " missing program " ) ) ;
return out ;
}
2026-02-12 13:44:20 +00:00
{
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 ( " ; " ) ) ) ;
}
}
2026-02-09 13:01:15 +00:00
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 ) ;
Add Plasma tray, notification inhibition, session control, and flatpak runner
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.
2026-03-22 15:53:09 +00:00
} 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 ( ) ;
2026-02-09 13:01:15 +00:00
}
out . insert ( QStringLiteral ( " finalProgram " ) , finalProgram ) ;
out . insert ( QStringLiteral ( " finalArgs " ) , finalArgs ) ;
out . insert ( QStringLiteral ( " effectiveEnv " ) , effectiveEnv ) ;
out . insert ( QStringLiteral ( " ok " ) , true ) ;
return out ;
}