Compare commits

...

29 commits

Author SHA1 Message Date
0b10b01cc4 tests: cover Runner1 ResolveLaunch wine missing prefix/gameId 2026-02-15 15:48:11 +01:00
09026ca26a tests: cover Runner1 ResolveLaunch unknown runnerId contract 2026-02-15 15:42:42 +01:00
b24417df69 tests: cover Runner1 ResolveLaunch failure contract 2026-02-15 15:21:15 +01:00
13b65135f1 tests: cover Input1 version and capabilities 2026-02-15 15:16:09 +01:00
d254b272fe src: migrate InputServiceClient to generated Input1 proxy 2026-02-15 15:11:08 +01:00
a893d4fd71 cmake: generate Input1 proxy for main app 2026-02-15 15:11:01 +01:00
cd23253386 tests: use generated Input1 proxy and cover profile CRUD 2026-02-15 14:47:03 +01:00
47cab1cbe5 inputd: export Input1 via generated adaptor 2026-02-15 14:46:51 +01:00
c58801828c cmake: enable Input1 DBusTools codegen 2026-02-15 14:46:39 +01:00
1d61f3f84f dbus: add authoritative XML for Input1 2026-02-15 14:46:06 +01:00
7c9a0c4a89 tests: cover Runner1 version and runner list shape 2026-02-15 14:33:12 +01:00
fe253d89bc tests: cover GameCenter launch failure contract 2026-02-15 14:22:41 +01:00
239e3a9071 tests: cover GameCenter policy and capabilities 2026-02-15 14:17:11 +01:00
16d4129ede runnermanagerclient: use generated Runner1 proxy 2026-02-15 13:57:42 +01:00
4676b1d9a6 gamelauncher: use generated DBus proxies 2026-02-15 13:57:23 +01:00
d93dbc9ecc tests: use generated DBus proxies 2026-02-15 13:56:49 +01:00
d94029fbc4 dbus: export daemons via generated adaptors 2026-02-15 13:55:50 +01:00
ed0e6ae181 cmake: enable Qt DBusTools codegen 2026-02-15 13:55:09 +01:00
c0938aa62a dbus: add authoritative XML for GameCenter1/Runner1 2026-02-15 13:54:48 +01:00
9cda4ce476 app/daemons: set standard app metadata 2026-02-15 13:54:08 +01:00
5b26b85cc3 tests: add ProcessScanner stability test
Add a new QtTest that spawns marker processes and verifies ProcessScanner::scan() matches by env/cmdline, preferEnvironmentMatches behavior, and cancellation. Register the test with ctest.
2026-02-14 17:56:09 +01:00
f03eb95b52 gamecenter: refactor ProcessScanner for cancellable single-pass scans
Add ProcessScanner::scan(ScanOptions) for combined env/cmdline/exe matching, extend Match with match-source flags, and make pollUntilFound() cancellation-aware. Update monitored launch matchers to use the new API.
2026-02-14 17:38:28 +01:00
7200ad179c Harden systemd scope cleanup
Tighten transient unit lifecycle handling in GameCenter.

- Drop stale unit watcher mappings on PropertiesChanged for removed sessions.
- When stopping a monitored launch due to timeout or attach failure, fall back
  to terminating scope PIDs if StopUnit fails unexpectedly.

This reduces leaked scopes and improves robustness in failure paths.
2026-02-14 16:45:37 +01:00
985f6dac03 Harden transient scope lifecycle
Improve GameCenter daemon handling of systemd transient scopes.

- Watch monitored-launch scopes early and avoid dropping Launching sessions
  when the unit disappears before PIDs are attached.
- Stop leaked scopes on AttachProcessesToUnit failures.
- Make Stop() resilient for recovered sessions by falling back to TERM/KILL
  on scope PIDs when StopUnit fails unexpectedly.
- Only recover scopes that match the expected A-La-Karte description.
2026-02-14 16:16:59 +01:00
d56b91dbd0 gamecenter: harden KAuth helper power profile setting 2026-02-14 14:08:45 +01:00
f3b130008f ui: drop system-bus GameCenter fallback 2026-02-14 14:02:07 +01:00
8891e85dbc tests: recover sessions after gamecenter restart 2026-02-14 13:34:10 +01:00
57d1e6e130 tests: cover GameCenter unique DBus service behavior 2026-02-14 13:07:51 +01:00
d4e0326974 gamecenter: move DBus object path to avoid KDBusService conflict 2026-02-14 12:42:18 +01:00
33 changed files with 1658 additions and 311 deletions

View file

@ -47,6 +47,7 @@ find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS
Network
Concurrent
DBus
DBusTools
Svg
Sql
)

View file

@ -28,6 +28,31 @@ add_executable(alakarte
config.cpp
)
set(alakarte_dbus_sources)
qt_add_dbus_interface(alakarte_dbus_sources
${CMAKE_SOURCE_DIR}/src/gamecenter/dbus/org.kde.GameCenter1.xml
gamecenter1interface
)
qt_add_dbus_interface(alakarte_dbus_sources
${CMAKE_SOURCE_DIR}/src/runner/dbus/org.kde.ALaKarte.Runner1.xml
runner1interface
)
qt_add_dbus_interface(alakarte_dbus_sources
${CMAKE_SOURCE_DIR}/src/input/dbus/org.kde.ALaKarte.Input1.xml
input1interface
)
target_sources(alakarte PRIVATE
${alakarte_dbus_sources}
)
target_include_directories(alakarte PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
)
set_source_files_properties(qml/icons/app/org.kde.alakarte.svg PROPERTIES
QT_RESOURCE_ALIAS "sc-apps-org.kde.alakarte.svg"
)

View file

@ -11,6 +11,30 @@ add_executable(alakarte_gamecenter
systemdusermanager.h
)
set(alakarte_gamecenter_dbus_sources)
qt_add_dbus_adaptor(alakarte_gamecenter_dbus_sources
dbus/org.kde.GameCenter1.xml
gamecenterdaemon.h
GameCenterDaemon
gamecenter1adaptor
GameCenter1Adaptor
)
qt_add_dbus_interface(alakarte_gamecenter_dbus_sources
dbus/org.kde.GameCenter1.xml
gamecenter1interface
)
target_sources(alakarte_gamecenter PRIVATE
${alakarte_gamecenter_dbus_sources}
)
target_include_directories(alakarte_gamecenter PRIVATE
${PROJECT_BINARY_DIR}/src
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(alakarte_gamecenter PRIVATE
Qt6::Core
Qt6::Concurrent

View file

@ -1,13 +0,0 @@
<!DOCTYPE busconfig PUBLIC
"-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<policy user="root">
<allow own="org.kde.GameCenter1"/>
</policy>
<policy context="default">
<allow send_destination="org.kde.GameCenter1"/>
<allow receive_sender="org.kde.GameCenter1"/>
</policy>
</busconfig>

View file

@ -1,4 +0,0 @@
[D-BUS Service]
Name=org.kde.GameCenter1
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-gamecenter --system
SystemdService=org.kde.GameCenter1.service

View file

@ -0,0 +1,63 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-Bus Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.GameCenter1">
<method name="Ping">
<arg name="out" type="s" direction="out"/>
</method>
<method name="Version">
<arg name="major" type="u" direction="out"/>
<arg name="minor" type="u" direction="out"/>
</method>
<method name="GetCapabilities">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="caps" type="a{sv}" direction="out"/>
</method>
<method name="ListSessions">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantList"/>
<arg name="sessions" type="av" direction="out"/>
</method>
<method name="GetSession">
<arg name="sessionId" type="s" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="session" type="a{sv}" direction="out"/>
</method>
<method name="Launch">
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<arg name="launchSpec" type="a{sv}" direction="in"/>
<arg name="sessionId" type="s" direction="out"/>
</method>
<method name="Stop">
<arg name="sessionId" type="s" direction="in"/>
</method>
<method name="StopByGameId">
<arg name="gameId" type="s" direction="in"/>
</method>
<method name="SetPolicy">
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<arg name="policy" type="a{sv}" direction="in"/>
</method>
<method name="GetPolicy">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="policy" type="a{sv}" direction="out"/>
</method>
<signal name="SessionAdded">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="session" type="a{sv}"/>
</signal>
<signal name="SessionChanged">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="session" type="a{sv}"/>
</signal>
<signal name="SessionRemoved">
<arg name="sessionId" type="s"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
<arg name="finalState" type="a{sv}"/>
</signal>
<signal name="LaunchFailed">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="error" type="a{sv}"/>
</signal>
</interface>
</node>

View file

@ -3,6 +3,8 @@
#include "gamecenterdaemon.h"
#include "gamecenter1adaptor.h"
#include <QCoreApplication>
#include <QDBusArgument>
#include <QDBusConnection>
@ -803,6 +805,18 @@ void GameCenterDaemon::handleSystemdUnitRemoved(const QString &unitName, const Q
return;
}
if (sit.value().scanner) {
if (!sit.value().unitPath.path().isEmpty()) {
const QString unitPathKey = sit.value().unitPath.path();
m_unitPathToSessionId.remove(unitPathKey);
if (QObject *watcher = m_unitPathWatchers.take(unitPathKey)) {
watcher->deleteLater();
}
}
sit.value().unitPath = {};
return;
}
unwatchSystemdUnit(sit.value().unitName, sit.value().unitPath);
removeSessionInternal(sessionId, sit.value().stopping ? QStringLiteral("Stopped") : QStringLiteral("Exited"));
}
@ -821,6 +835,15 @@ void GameCenterDaemon::handleSystemdUnitPropertiesChanged(const QDBusObjectPath
auto sit = m_sessions.find(sessionId);
if (sit == m_sessions.end()) {
unwatchSystemdUnit({}, unitPath);
for (auto nameIt = m_unitNameToSessionId.begin(); nameIt != m_unitNameToSessionId.end();) {
if (nameIt.value() == sessionId) {
nameIt = m_unitNameToSessionId.erase(nameIt);
} else {
++nameIt;
}
}
return;
}
@ -833,6 +856,25 @@ void GameCenterDaemon::handleSystemdUnitPropertiesChanged(const QDBusObjectPath
}
const QString activeState = activeStateV.toString();
if (sit.value().scanner) {
if (activeState == QLatin1String("active") || activeState == QLatin1String("activating") || activeState == QLatin1String("deactivating")) {
const QList<uint> pids = m_systemd.scopePids(unitPath);
if (!pids.isEmpty()) {
sit.value().mainPid = pids.first();
}
} else {
const QString unitPathKey = unitPath.path();
m_unitPathToSessionId.remove(unitPathKey);
if (QObject *watcher = m_unitPathWatchers.take(unitPathKey)) {
watcher->deleteLater();
}
sit.value().unitPath = {};
}
Q_EMIT SessionChanged(sessionToVariantMap(sit.value(), sessionState(sit.value())));
return;
}
if (activeState == QLatin1String("active") || activeState == QLatin1String("activating") || activeState == QLatin1String("deactivating")) {
const QList<uint> pids = m_systemd.scopePids(unitPath);
if (!pids.isEmpty()) {
@ -928,11 +970,10 @@ bool GameCenterDaemon::init()
return false;
}
if (!bus.registerService(QStringLiteral("org.kde.GameCenter1"))) {
return false;
}
new GameCenter1Adaptor(this);
if (!bus.registerObject(QStringLiteral("/org/kde/GameCenter1"), this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) {
if (!bus.registerObject(QStringLiteral("/org/kde/ALaKarte/GameCenter1"), this, QDBusConnection::ExportAdaptors)) {
qWarning() << "Failed to register D-Bus object" << bus.lastError().name() << bus.lastError().message();
return false;
}
@ -978,10 +1019,10 @@ QString GameCenterDaemon::Ping() const
return QStringLiteral("ok");
}
void GameCenterDaemon::Version(uint &major, uint &minor) const
uint GameCenterDaemon::Version(uint &minor) const
{
major = 1;
minor = 0;
return 1;
}
QVariantMap GameCenterDaemon::GetCapabilities() const
@ -1365,7 +1406,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
// Determine bootstrap command and scanner matcher
QString bootstrapProgram;
QStringList bootstrapArgs;
std::function<QList<ProcessScanner::Match>()> matcher;
ProcessScanner::Matcher matcher;
if (provider == QLatin1String("steam")) {
const QString appId = extractSteamAppId(command);
@ -1377,9 +1418,13 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
bootstrapProgram = QStringLiteral("steam");
bootstrapArgs = {QStringLiteral("-silent"), QStringLiteral("-applaunch"), appId};
matcher = [appId]() -> QList<ProcessScanner::Match> {
// Primary: look for SteamAppId=<appId> in environ
QList<ProcessScanner::Match> results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId);
matcher = [appId](const std::atomic_bool &cancelled) -> QList<ProcessScanner::Match> {
ProcessScanner::ScanOptions opts;
opts.envKeys = {QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")};
opts.envValue = appId;
opts.preferEnvironmentMatches = true;
QList<ProcessScanner::Match> results = ProcessScanner::scan(opts, cancelled);
// Filter out the steam client itself
QList<ProcessScanner::Match> filtered;
for (const auto &m : std::as_const(results)) {
@ -1401,11 +1446,25 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
bootstrapArgs = {QStringLiteral("lutris:rungameid/%1").arg(lutrisId)};
// For Lutris, look for child processes with the game slug or lutris game env
matcher = [lutrisId]() -> QList<ProcessScanner::Match> {
QList<ProcessScanner::Match> results = ProcessScanner::findByEnvironment(QStringLiteral("LUTRIS_GAME_SLUG"), lutrisId);
if (results.isEmpty()) {
results = ProcessScanner::findByCmdline(lutrisId);
// Filter out lutris itself
matcher = [lutrisId](const std::atomic_bool &cancelled) -> QList<ProcessScanner::Match> {
ProcessScanner::ScanOptions opts;
opts.envKeys = {QStringLiteral("LUTRIS_GAME_SLUG")};
opts.envValue = lutrisId;
opts.cmdlineSubstring = lutrisId;
opts.preferEnvironmentMatches = true;
QList<ProcessScanner::Match> results = ProcessScanner::scan(opts, cancelled);
bool anyEnvMatch = false;
for (const auto &m : std::as_const(results)) {
if (m.envMatched) {
anyEnvMatch = true;
break;
}
}
if (!anyEnvMatch) {
// Filter out lutris itself (only for cmdline fallback)
QList<ProcessScanner::Match> filtered;
for (const auto &m : std::as_const(results)) {
if (!m.exe.endsWith(QLatin1String("/lutris")) && !m.exe.endsWith(QLatin1String("/python3"))) {
@ -1416,6 +1475,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
results = filtered;
}
}
return results;
};
}
@ -1498,6 +1558,8 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
m_sessions[sessionId] = session;
watchSystemdUnit(sessionId, session.unitName, session.unitPath);
connect(bootstrap, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this, sessionId](int, QProcess::ExitStatus) {
const auto it = m_sessions.find(sessionId);
if (it == m_sessions.end()) {
@ -1540,8 +1602,31 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
QVariantMap finalState = sessionToVariantMap(it.value(), stopping ? QStringLiteral("Stopped") : QStringLiteral("Failed"));
if (!it.value().unitName.isEmpty()) {
m_systemd.stopUnit(it.value().unitName);
unwatchSystemdUnit(it.value().unitName, it.value().unitPath);
const QString unitName = it.value().unitName;
const QDBusObjectPath unitPath = it.value().unitPath;
const uint mainPid = it.value().mainPid;
const QDBusReply<QDBusObjectPath> stopReply = m_systemd.stopUnit(unitName);
if (!stopReply.isValid() && stopReply.error().name() != QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
QList<uint> pids;
if (!unitPath.path().isEmpty()) {
pids = m_systemd.scopePids(unitPath);
}
if (pids.isEmpty() && it.value().process && it.value().process->processId() > 0) {
pids = {static_cast<uint>(it.value().process->processId())};
}
if (pids.isEmpty() && mainPid > 0) {
pids = {mainPid};
}
if (!pids.isEmpty()) {
terminatePids(pids);
QTimer::singleShot(5000, this, [pids]() {
killPids(pids);
});
}
}
unwatchSystemdUnit(unitName, unitPath);
}
if (it.value().scanner) {
@ -1612,6 +1697,28 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
{QStringLiteral("error"), attachReply.error().message()},
};
if (!it.value().unitName.isEmpty()) {
const QString staleUnitName = it.value().unitName;
const QDBusObjectPath staleUnitPath = it.value().unitPath;
const QDBusReply<QDBusObjectPath> stopReply = m_systemd.stopUnit(staleUnitName);
if (!stopReply.isValid() && stopReply.error().name() != QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
QList<uint> stalePids;
if (!staleUnitPath.path().isEmpty()) {
stalePids = m_systemd.scopePids(staleUnitPath);
}
if (stalePids.isEmpty()) {
stalePids = pids;
}
terminatePids(stalePids);
QTimer::singleShot(5000, this, [stalePids]() {
killPids(stalePids);
});
}
unwatchSystemdUnit(staleUnitName, staleUnitPath);
}
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed"));
if (it.value().process) {
it.value().process->deleteLater();
@ -1783,6 +1890,32 @@ void GameCenterDaemon::Stop(const QString &sessionId)
return;
}
QDBusObjectPath unitPath = it.value().unitPath;
if (unitPath.path().isEmpty()) {
const QDBusReply<QDBusObjectPath> getUnitReply = m_systemd.getUnit(it.value().unitName);
if (getUnitReply.isValid()) {
unitPath = getUnitReply.value();
it.value().unitPath = unitPath;
watchSystemdUnit(sessionId, it.value().unitName, unitPath);
}
}
QList<uint> pids;
if (!unitPath.path().isEmpty()) {
pids = m_systemd.scopePids(unitPath);
}
if (pids.isEmpty() && it.value().mainPid > 0) {
pids = {it.value().mainPid};
}
if (!pids.isEmpty()) {
terminatePids(pids);
QTimer::singleShot(5000, this, [pids]() {
killPids(pids);
});
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping")));
return;
}
if (it.value().process && it.value().process->state() != QProcess::NotRunning) {
it.value().process->terminate();
QTimer::singleShot(5000, it.value().process, [process = it.value().process]() {
@ -1889,7 +2022,8 @@ void GameCenterDaemon::recoverExistingSessions()
static const QString prefix = QStringLiteral("alakarte-game-");
static const QString suffix = QStringLiteral(".scope");
static const QString descPrefix = QStringLiteral("A-La-Karte game ");
static const QString descBase = QStringLiteral("A-La-Karte game");
static const QString descPrefix = descBase + QLatin1Char(' ');
const SystemdUnitInfoList units = reply.value();
for (const SystemdUnitInfo &unit : units) {
@ -1900,6 +2034,10 @@ void GameCenterDaemon::recoverExistingSessions()
continue;
}
if (unit.description != descBase && !unit.description.startsWith(descPrefix)) {
continue;
}
const QString sessionId = unit.name.mid(prefix.length(), unit.name.length() - prefix.length() - suffix.length());
if (sessionId.isEmpty() || m_sessions.contains(sessionId)) {
continue;

View file

@ -29,7 +29,7 @@ public:
public Q_SLOTS:
QString Ping() const;
void Version(uint &major, uint &minor) const;
uint Version(uint &minor) const;
QVariantMap GetCapabilities() const;
QVariantList ListSessions() const;

View file

@ -4,7 +4,9 @@
#include <KAuth/ActionReply>
#include <KAuth/HelperSupport>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusError>
#include <QDBusInterface>
#include <QDBusMessage>
@ -28,6 +30,14 @@ public Q_SLOTS:
ActionReply setpowerprofile(const QVariantMap &args);
};
static QVariant unwrapDbusVariant(QVariant v)
{
if (v.canConvert<QDBusVariant>()) {
v = v.value<QDBusVariant>().variant();
}
return v;
}
static QString unwrapStringArg(const QVariantMap &args, const QString &key)
{
QVariant v = args.value(key);
@ -53,6 +63,12 @@ ActionReply GameCenterHelper::setpowerprofile(const QVariantMap &args)
return reply;
}
if (bus.interface() && !bus.interface()->isServiceRegistered(kPowerProfilesService)) {
ActionReply reply = ActionReply::HelperErrorReply();
reply.setErrorDescription(QStringLiteral("power-profiles-daemon service not available"));
return reply;
}
QDBusInterface props(kPowerProfilesService, kPowerProfilesPath, QStringLiteral("org.freedesktop.DBus.Properties"), bus);
if (!props.isValid()) {
ActionReply reply = ActionReply::HelperErrorReply();
@ -60,6 +76,44 @@ ActionReply GameCenterHelper::setpowerprofile(const QVariantMap &args)
return reply;
}
{
const QDBusReply<QVariant> profilesReply = props.call(QStringLiteral("Get"), kPowerProfilesInterface, QStringLiteral("Profiles"));
if (profilesReply.isValid()) {
QVariant v = unwrapDbusVariant(profilesReply.value());
QVariantList list;
if (v.canConvert<QDBusArgument>()) {
list = qdbus_cast<QVariantList>(v.value<QDBusArgument>());
} else if (v.canConvert<QVariantList>()) {
list = v.toList();
}
if (!list.isEmpty()) {
QStringList available;
available.reserve(list.size());
for (const QVariant &item : list) {
QVariant mV = unwrapDbusVariant(item);
QVariantMap m;
if (mV.canConvert<QDBusArgument>()) {
m = qdbus_cast<QVariantMap>(mV.value<QDBusArgument>());
} else if (mV.canConvert<QVariantMap>()) {
m = mV.toMap();
}
const QString name = m.value(QStringLiteral("Profile")).toString();
if (!name.isEmpty()) {
available.push_back(name);
}
}
if (!available.isEmpty() && !available.contains(profile)) {
ActionReply reply = ActionReply::HelperErrorReply();
reply.setErrorDescription(QStringLiteral("unsupported profile '%1'").arg(profile));
reply.addData(QStringLiteral("available"), available);
return reply;
}
}
}
}
QDBusMessage msg =
QDBusMessage::createMethodCall(kPowerProfilesService, kPowerProfilesPath, QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Set"));
msg.setArguments({kPowerProfilesInterface, QStringLiteral("ActiveProfile"), QVariant::fromValue(QDBusVariant(profile))});

View file

@ -5,9 +5,11 @@
#include <QCommandLineParser>
#include <QCoreApplication>
#include <KAboutData>
#include <KDBusService>
#include <KSignalHandler>
#include "alakarte-version.h"
#include "gamecenterdaemon.h"
#include <signal.h>
@ -16,17 +18,34 @@ int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
app.setOrganizationDomain(QStringLiteral("kde.org"));
app.setApplicationName(QStringLiteral("alakarte_gamecenter"));
app.setApplicationName(QStringLiteral("GameCenter1"));
KDBusService service(KDBusService::Unique);
KAboutData aboutData(QStringLiteral("alakarte-gamecenter"),
QStringLiteral("A-La-Karte Game Center"),
QStringLiteral(ALAKARTE_VERSION_STRING),
QStringLiteral("Game session management daemon for A-La-Karte"),
KAboutLicense::GPL_V3,
QStringLiteral("© 2026 A-La-Karte Contributors"));
aboutData.addAuthor(QStringLiteral("A-La-Karte Contributors"), QStringLiteral("Developer"), QString());
KAboutData::setApplicationData(aboutData);
QCommandLineParser parser;
parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center"));
parser.addHelpOption();
aboutData.setupCommandLine(&parser);
parser.process(app);
aboutData.processCommandLine(&parser);
GameCenterDaemon daemon;
if (!daemon.init()) {
qWarning() << "GameCenter daemon initialization failed";
return 1;
}
app.setOrganizationDomain(QStringLiteral("kde.org"));
app.setApplicationName(QStringLiteral("GameCenter1"));
KDBusService service(KDBusService::Unique | KDBusService::NoExitOnFailure);
if (!service.isRegistered()) {
qWarning() << "Failed to register D-Bus service" << service.serviceName() << service.errorMessage();
return 1;
}

View file

@ -6,8 +6,19 @@
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QLoggingCategory>
#include <QtConcurrentRun>
#include <utility>
Q_LOGGING_CATEGORY(lcProcessScanner, "org.kde.alakarte.processscanner")
static QString readExeLink(const QString &pidDir);
static QByteArray readCmdline(const QString &pidDir);
static QByteArray readEnviron(const QString &pidDir);
static bool containsNullSeparatedEntry(const QByteArray &blob, const QByteArray &needle);
static QList<uint> listPids();
ProcessScanner::ProcessScanner(QObject *parent)
: QObject(parent)
{
@ -16,7 +27,11 @@ ProcessScanner::ProcessScanner(QObject *parent)
connect(&m_deadline, &QTimer::timeout, this, [this]() {
m_timer.stop();
m_matcher = nullptr;
if (m_cancelToken) {
m_cancelToken->store(true);
}
m_cancelToken.reset();
m_matcher = {};
++m_generation;
m_scanInFlight = false;
Q_EMIT timedOut();
@ -42,7 +57,8 @@ void ProcessScanner::startScan()
m_scanInFlight = true;
m_scanGeneration = gen;
const std::function<QList<Match>()> matcher = m_matcher;
const Matcher matcher = m_matcher;
const std::shared_ptr<std::atomic_bool> cancelToken = m_cancelToken;
auto *watcher = new QFutureWatcher<QList<Match>>(this);
connect(watcher, &QFutureWatcher<QList<Match>>::finished, this, [this, watcher, gen]() {
@ -60,19 +76,130 @@ void ProcessScanner::startScan()
if (!results.isEmpty()) {
m_timer.stop();
m_deadline.stop();
m_matcher = nullptr;
if (m_cancelToken) {
m_cancelToken->store(true);
}
m_cancelToken.reset();
m_matcher = {};
Q_EMIT found(results);
}
});
watcher->setFuture(QtConcurrent::run([matcher]() {
watcher->setFuture(QtConcurrent::run([matcher, cancelToken]() {
static const std::atomic_bool neverCancelled{false};
const std::atomic_bool &cancelled = cancelToken ? *cancelToken : neverCancelled;
if (!matcher) {
return QList<Match>{};
}
return matcher();
return matcher(cancelled);
}));
}
QList<ProcessScanner::Match> ProcessScanner::scan(const ScanOptions &options)
{
static const std::atomic_bool neverCancelled{false};
return scan(options, neverCancelled);
}
QList<ProcessScanner::Match> ProcessScanner::scan(const ScanOptions &options, const std::atomic_bool &cancelled)
{
QList<Match> matches;
QList<QByteArray> envNeedles;
if (!options.envKeys.isEmpty() && !options.envValue.isEmpty()) {
envNeedles.reserve(options.envKeys.size());
for (const QString &key : options.envKeys) {
if (!key.isEmpty()) {
envNeedles.push_back((key + QLatin1Char('=') + options.envValue).toUtf8());
}
}
}
const QByteArray cmdNeedle = options.cmdlineSubstring.isEmpty() ? QByteArray{} : options.cmdlineSubstring.toUtf8();
const bool checkExePrefix = !options.exePathPrefix.isEmpty();
const QList<uint> pids = listPids();
for (uint pid : pids) {
if (cancelled.load()) {
break;
}
const QString pidDir = QStringLiteral("/proc/%1").arg(pid);
bool envMatched = false;
if (!envNeedles.isEmpty()) {
const QByteArray env = readEnviron(pidDir);
if (!env.isEmpty()) {
for (const QByteArray &needle : envNeedles) {
if (containsNullSeparatedEntry(env, needle)) {
envMatched = true;
break;
}
}
}
}
bool cmdlineMatched = false;
QByteArray cmd;
if (!cmdNeedle.isEmpty()) {
cmd = readCmdline(pidDir);
cmdlineMatched = !cmd.isEmpty() && cmd.contains(cmdNeedle);
}
bool exeMatched = false;
QString exe;
if (checkExePrefix) {
exe = readExeLink(pidDir);
exeMatched = !exe.isEmpty() && exe.startsWith(options.exePathPrefix);
}
if (!envMatched && !cmdlineMatched && !exeMatched) {
continue;
}
Match m;
m.pid = pid;
m.envMatched = envMatched;
m.cmdlineMatched = cmdlineMatched;
m.exeMatched = exeMatched;
if (exe.isEmpty()) {
exe = readExeLink(pidDir);
}
m.exe = exe;
if (cmd.isEmpty()) {
cmd = readCmdline(pidDir);
}
m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed();
matches.push_back(m);
}
if (options.preferEnvironmentMatches) {
bool anyEnvMatch = false;
for (const Match &m : std::as_const(matches)) {
if (m.envMatched) {
anyEnvMatch = true;
break;
}
}
if (anyEnvMatch) {
QList<Match> filtered;
filtered.reserve(matches.size());
for (const Match &m : std::as_const(matches)) {
if (m.envMatched) {
filtered.push_back(m);
}
}
return filtered;
}
}
return matches;
}
static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
{
QFile f(path);
@ -136,141 +263,75 @@ static QList<uint> listPids()
QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &key, const QString &value)
{
QList<Match> matches;
const QByteArray needle = (key + QLatin1Char('=') + value).toUtf8();
const QList<uint> pids = listPids();
for (uint pid : pids) {
const QString pidDir = QStringLiteral("/proc/%1").arg(pid);
const QByteArray env = readEnviron(pidDir);
if (env.isEmpty()) {
continue;
}
if (containsNullSeparatedEntry(env, needle)) {
Match m;
m.pid = pid;
m.exe = readExeLink(pidDir);
const QByteArray cmd = readCmdline(pidDir);
m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed();
matches.push_back(m);
}
}
return matches;
ScanOptions opts;
opts.envKeys = {key};
opts.envValue = value;
return scan(opts);
}
QList<ProcessScanner::Match> ProcessScanner::findByAnyEnvironment(const QStringList &keys, const QString &value)
{
QList<Match> matches;
if (keys.isEmpty()) {
return matches;
}
QList<QByteArray> needles;
needles.reserve(keys.size());
for (const QString &key : keys) {
if (!key.isEmpty()) {
needles.push_back((key + QLatin1Char('=') + value).toUtf8());
}
}
if (needles.isEmpty()) {
return matches;
}
const QList<uint> pids = listPids();
for (uint pid : pids) {
const QString pidDir = QStringLiteral("/proc/%1").arg(pid);
const QByteArray env = readEnviron(pidDir);
if (env.isEmpty()) {
continue;
}
bool found = false;
for (const QByteArray &needle : needles) {
if (containsNullSeparatedEntry(env, needle)) {
found = true;
break;
}
}
if (!found) {
continue;
}
Match m;
m.pid = pid;
m.exe = readExeLink(pidDir);
const QByteArray cmd = readCmdline(pidDir);
m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed();
matches.push_back(m);
}
return matches;
ScanOptions opts;
opts.envKeys = keys;
opts.envValue = value;
return scan(opts);
}
QList<ProcessScanner::Match> ProcessScanner::findByCmdline(const QString &substring)
{
QList<Match> matches;
const QByteArray needle = substring.toUtf8();
const QList<uint> pids = listPids();
for (uint pid : pids) {
const QString pidDir = QStringLiteral("/proc/%1").arg(pid);
const QByteArray cmd = readCmdline(pidDir);
if (cmd.isEmpty()) {
continue;
}
// cmdline has null-separated args; search the whole blob
if (cmd.contains(needle)) {
Match m;
m.pid = pid;
m.exe = readExeLink(pidDir);
m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed();
matches.push_back(m);
}
}
return matches;
ScanOptions opts;
opts.cmdlineSubstring = substring;
return scan(opts);
}
QList<ProcessScanner::Match> ProcessScanner::findByExePath(const QString &dirPrefix)
{
QList<Match> matches;
const QList<uint> pids = listPids();
for (uint pid : pids) {
const QString pidDir = QStringLiteral("/proc/%1").arg(pid);
const QString exe = readExeLink(pidDir);
if (exe.isEmpty()) {
continue;
}
if (exe.startsWith(dirPrefix)) {
Match m;
m.pid = pid;
m.exe = exe;
const QByteArray cmd = readCmdline(pidDir);
m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed();
matches.push_back(m);
}
}
return matches;
ScanOptions opts;
opts.exePathPrefix = dirPrefix;
return scan(opts);
}
void ProcessScanner::pollUntilFound(std::function<QList<Match>()> matcher, int intervalMs, int timeoutMs)
void ProcessScanner::pollUntilFound(Matcher matcher, int intervalMs, int timeoutMs)
{
cancel();
m_cancelToken = std::make_shared<std::atomic_bool>(false);
m_matcher = std::move(matcher);
if (!m_matcher) {
return;
}
m_deadline.start(timeoutMs);
m_timer.start(intervalMs);
startScan();
}
void ProcessScanner::pollUntilFound(std::function<QList<Match>()> matcher, int intervalMs, int timeoutMs)
{
pollUntilFound(
[matcher = std::move(matcher)](const std::atomic_bool &cancelled) {
if (cancelled.load()) {
return QList<Match>{};
}
if (!matcher) {
return QList<Match>{};
}
return matcher();
},
intervalMs,
timeoutMs);
}
void ProcessScanner::cancel()
{
m_timer.stop();
m_deadline.stop();
m_matcher = nullptr;
if (m_cancelToken) {
m_cancelToken->store(true);
}
m_cancelToken.reset();
m_matcher = {};
++m_generation;
m_scanInFlight = false;
}

View file

@ -5,11 +5,16 @@
#include <QFutureWatcher>
#include <QList>
#include <QLoggingCategory>
#include <QObject>
#include <QString>
#include <QTimer>
#include <QtGlobal>
#include <atomic>
#include <functional>
#include <memory>
Q_DECLARE_LOGGING_CATEGORY(lcProcessScanner)
class ProcessScanner : public QObject
{
@ -18,12 +23,28 @@ class ProcessScanner : public QObject
public:
explicit ProcessScanner(QObject *parent = nullptr);
struct ScanOptions {
QStringList envKeys;
QString envValue;
QString cmdlineSubstring;
QString exePathPrefix;
bool preferEnvironmentMatches = false;
};
struct Match {
uint pid = 0;
QString exe;
QString cmdline;
bool envMatched = false;
bool cmdlineMatched = false;
bool exeMatched = false;
};
using Matcher = std::function<QList<Match>(const std::atomic_bool &cancelled)>;
static QList<Match> scan(const ScanOptions &options);
static QList<Match> scan(const ScanOptions &options, const std::atomic_bool &cancelled);
// Find PIDs whose /proc/<pid>/environ contains key=value
static QList<Match> findByEnvironment(const QString &key, const QString &value);
@ -38,6 +59,7 @@ public:
// Async poll: calls matcher repeatedly until it returns non-empty or timeout.
// Emits found() with matching PIDs, or timedOut() on failure.
void pollUntilFound(Matcher matcher, int intervalMs = 500, int timeoutMs = 15000);
void pollUntilFound(std::function<QList<Match>()> matcher, int intervalMs = 500, int timeoutMs = 15000);
void cancel();
@ -51,7 +73,8 @@ private:
QTimer m_timer;
QTimer m_deadline;
std::function<QList<Match>()> m_matcher;
Matcher m_matcher;
std::shared_ptr<std::atomic_bool> m_cancelToken;
quint64 m_generation = 0;
bool m_scanInFlight = false;
quint64 m_scanGeneration = 0;

View file

@ -1,8 +0,0 @@
[Unit]
Description=A-La-Karte Game Center
[Service]
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-gamecenter --system
Type=dbus
BusName=org.kde.GameCenter1
Restart=no

View file

@ -4,13 +4,14 @@
#include "gamelauncher.h"
#include "app.h"
#include "gamecenter1interface.h"
#include "runner1interface.h"
#include <QCoreApplication>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusError>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDateTime>
#include <QDir>
@ -20,12 +21,11 @@
#include <QTimer>
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
static const QString kGameCenterPath = QStringLiteral("/org/kde/ALaKarte/GameCenter1");
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
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 bool pingDaemon(QDBusConnection bus)
{
@ -33,14 +33,15 @@ static bool pingDaemon(QDBusConnection bus)
return false;
}
QDBusInterface iface(kGameCenterService, kGameCenterPath, kGameCenterInterface, bus);
org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, bus);
if (!iface.isValid()) {
return false;
}
iface.setTimeout(2000);
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
return reply.isValid() && reply.value() == QLatin1String("ok");
QDBusPendingReply<QString> reply = iface.Ping();
reply.waitForFinished();
return !reply.isError() && reply.value() == QLatin1String("ok");
}
static void disconnectDaemonSignals(QDBusConnection bus, GameLauncher *launcher)
@ -131,14 +132,15 @@ static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &ou
bus.interface()->startService(kRunnerService);
}
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, bus);
if (!iface.isValid()) {
if (bus.interface()->startService(kRunnerService).isValid()) {
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
org::kde::ALaKarte::Runner1 retryIface(kRunnerService, kRunnerPath, bus);
if (retryIface.isValid()) {
retryIface.setTimeout(2000);
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
if (retryReply.isValid()) {
QDBusPendingReply<QVariantMap> retryReply = retryIface.ResolveLaunch(spec);
retryReply.waitForFinished();
if (!retryReply.isError()) {
out = unwrapVariantMap(retryReply.value());
return true;
}
@ -148,17 +150,19 @@ static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &ou
}
iface.setTimeout(2000);
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
if (!reply.isValid()) {
QDBusPendingReply<QVariantMap> reply = iface.ResolveLaunch(spec);
reply.waitForFinished();
if (reply.isError()) {
if (reply.error().type() == QDBusError::ServiceUnknown) {
bus.interface()->startService(kRunnerService);
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
org::kde::ALaKarte::Runner1 retryIface(kRunnerService, kRunnerPath, bus);
if (!retryIface.isValid()) {
return false;
}
retryIface.setTimeout(2000);
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
if (!retryReply.isValid()) {
QDBusPendingReply<QVariantMap> retryReply = retryIface.ResolveLaunch(spec);
retryReply.waitForFinished();
if (retryReply.isError()) {
return false;
}
out = unwrapVariantMap(retryReply.value());
@ -270,7 +274,6 @@ static QStringList steamCandidateRoots()
GameLauncher::GameLauncher(QObject *parent)
: QObject(parent)
, m_daemonAvailable(false)
, m_usingSystemBus(false)
{
if (auto *app = qobject_cast<App *>(parent)) {
connect(app->gameModel(), &GameModel::countChanged, this, [this]() {
@ -557,10 +560,7 @@ void GameLauncher::launchGame(Game *game)
// Always try daemon first — for all launch types
{
QDBusInterface iface(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus());
QVariantMap launchSpec = {
{QStringLiteral("command"), launchCommand},
{QStringLiteral("gameId"), game->id()},
@ -599,8 +599,10 @@ void GameLauncher::launchGame(Game *game)
launchSpec.insert(QStringLiteral("workingDirectory"), workingDirectory);
}
const QDBusReply<QString> reply = iface.call(QStringLiteral("Launch"), launchSpec);
if (reply.isValid() && !reply.value().isEmpty()) {
iface.setTimeout(5000);
QDBusPendingReply<QString> reply = iface.Launch(launchSpec);
reply.waitForFinished();
if (!reply.isError() && !reply.value().isEmpty()) {
m_daemonGameToSession.insert(game->id(), reply.value());
m_daemonSessionToGame.insert(reply.value(), game->id());
game->setRunning(true);
@ -614,7 +616,7 @@ void GameLauncher::launchGame(Game *game)
return;
}
const QString launchError = reply.isValid() ? QString() : reply.error().message();
const QString launchError = reply.isError() ? reply.error().message() : QString();
// No fallback for non-URL commands — emit error
if (!launchError.isEmpty()) {
@ -631,11 +633,8 @@ void GameLauncher::stopGame(Game *game)
return;
}
QDBusInterface iface(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
iface.call(QStringLiteral("StopByGameId"), game->id());
org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus());
iface.StopByGameId(game->id());
}
bool GameLauncher::isGameRunning(Game *game) const
@ -765,13 +764,12 @@ void GameLauncher::onDaemonLaunchFailed(const QVariantMap &error)
void GameLauncher::syncDaemonSessions()
{
QDBusInterface iface(kGameCenterService,
kGameCenterPath,
kGameCenterInterface,
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus());
iface.setTimeout(2000);
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListSessions"));
if (!reply.isValid()) {
QDBusPendingReply<QVariantList> reply = iface.ListSessions();
reply.waitForFinished();
if (reply.isError()) {
return;
}
@ -805,28 +803,14 @@ void GameLauncher::applyRunningStateToLibrary()
void GameLauncher::checkDaemonAvailability()
{
const bool systemAvailable = pingDaemon(QDBusConnection::systemBus());
const bool sessionAvailable = systemAvailable ? false : pingDaemon(QDBusConnection::sessionBus());
const bool available = systemAvailable || sessionAvailable;
const bool useSystemBus = systemAvailable;
const bool available = pingDaemon(QDBusConnection::sessionBus());
disconnectDaemonSignals(QDBusConnection::systemBus(), this);
disconnectDaemonSignals(QDBusConnection::sessionBus(), this);
if (available) {
connectDaemonSignals(useSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus(), this);
connectDaemonSignals(QDBusConnection::sessionBus(), this);
}
const bool busChanged = m_usingSystemBus != useSystemBus;
if (busChanged) {
m_daemonGameToSession.clear();
m_daemonSessionToGame.clear();
Q_EMIT runningGamesChanged();
applyRunningStateToLibrary();
}
m_usingSystemBus = useSystemBus;
if (available != m_daemonAvailable) {
m_daemonAvailable = available;
Q_EMIT daemonAvailableChanged();

View file

@ -48,7 +48,6 @@ private:
QHash<QString, QString> m_daemonGameToSession;
QHash<QString, QString> m_daemonSessionToGame;
bool m_daemonAvailable = false;
bool m_usingSystemBus = false;
void checkDaemonAvailability();
void syncDaemonSessions();

View file

@ -7,10 +7,35 @@ add_executable(alakarte_inputd
inputdaemon.h
)
set(alakarte_inputd_dbus_sources)
qt_add_dbus_adaptor(alakarte_inputd_dbus_sources
dbus/org.kde.ALaKarte.Input1.xml
inputdaemon.h
InputDaemon
input1adaptor
Input1Adaptor
)
qt_add_dbus_interface(alakarte_inputd_dbus_sources
dbus/org.kde.ALaKarte.Input1.xml
input1interface
)
target_sources(alakarte_inputd PRIVATE
${alakarte_inputd_dbus_sources}
)
target_include_directories(alakarte_inputd PRIVATE
${PROJECT_BINARY_DIR}/src
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(alakarte_inputd PRIVATE
Qt6::Core
Qt6::DBus
SDL3::SDL3
KF6::CoreAddons
)
set_target_properties(alakarte_inputd PROPERTIES

View file

@ -0,0 +1,76 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-Bus Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.ALaKarte.Input1">
<method name="Ping">
<arg name="out" type="s" direction="out"/>
</method>
<method name="Version">
<arg name="major" type="u" direction="out"/>
<arg name="minor" type="u" direction="out"/>
</method>
<method name="GetCapabilities">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="caps" type="a{sv}" direction="out"/>
</method>
<method name="ListControllers">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantList"/>
<arg name="controllers" type="av" direction="out"/>
</method>
<method name="GetController">
<arg name="controllerId" type="s" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="controller" type="a{sv}" direction="out"/>
</method>
<method name="Rescan">
<arg name="ok" type="b" direction="out"/>
</method>
<method name="ListProfiles">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantList"/>
<arg name="profiles" type="av" direction="out"/>
</method>
<method name="GetProfile">
<arg name="profileId" type="s" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="profile" type="a{sv}" direction="out"/>
</method>
<method name="CreateProfile">
<arg name="name" type="s" direction="in"/>
<arg name="profileId" type="s" direction="out"/>
</method>
<method name="DeleteProfile">
<arg name="profileId" type="s" direction="in"/>
<arg name="ok" type="b" direction="out"/>
</method>
<method name="SetActiveProfile">
<arg name="controllerId" type="s" direction="in"/>
<arg name="profileId" type="s" direction="in"/>
<arg name="ok" type="b" direction="out"/>
</method>
<method name="GetActiveProfile">
<arg name="controllerId" type="s" direction="in"/>
<arg name="profileId" type="s" direction="out"/>
</method>
<signal name="ControllerAdded">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="controller" type="a{sv}"/>
</signal>
<signal name="ControllerRemoved">
<arg name="controllerId" type="s"/>
</signal>
<signal name="ControllerChanged">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="controller" type="a{sv}"/>
</signal>
<signal name="ProfilesChanged"/>
<signal name="ControllerProfileChanged">
<arg name="controllerId" type="s"/>
<arg name="profileId" type="s"/>
</signal>
</interface>
</node>

View file

@ -3,6 +3,8 @@
#include "inputdaemon.h"
#include "input1adaptor.h"
#include <QCoreApplication>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
@ -150,7 +152,9 @@ bool InputDaemon::init()
return false;
}
if (!bus.registerObject(kInputPath, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) {
new Input1Adaptor(this);
if (!bus.registerObject(kInputPath, this, QDBusConnection::ExportAdaptors)) {
qWarning() << "InputDaemon: failed to register object" << kInputPath << bus.lastError().message();
return false;
}
@ -199,10 +203,10 @@ QString InputDaemon::Ping() const
return QStringLiteral("ok");
}
void InputDaemon::Version(uint &major, uint &minor) const
uint InputDaemon::Version(uint &minor) const
{
major = 1;
minor = 0;
return 1;
}
QVariantMap InputDaemon::GetCapabilities() const

View file

@ -24,7 +24,7 @@ public:
public Q_SLOTS:
QString Ping() const;
void Version(uint &major, uint &minor) const;
uint Version(uint &minor) const;
QVariantMap GetCapabilities() const;
QVariantList ListControllers() const;

View file

@ -1,15 +1,35 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCommandLineParser>
#include <QCoreApplication>
#include <KAboutData>
#include "alakarte-version.h"
#include "inputdaemon.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
app.setOrganizationDomain(QStringLiteral("kde.org"));
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
KAboutData aboutData(QStringLiteral("alakarte-inputd"),
QStringLiteral("A-La-Karte Input Daemon"),
QStringLiteral(ALAKARTE_VERSION_STRING),
QStringLiteral("Gamepad input service for A-La-Karte"),
KAboutLicense::GPL_V3,
QStringLiteral("© 2026 A-La-Karte Contributors"));
aboutData.addAuthor(QStringLiteral("A-La-Karte Contributors"), QStringLiteral("Developer"), QString());
KAboutData::setApplicationData(aboutData);
QCommandLineParser parser;
aboutData.setupCommandLine(&parser);
parser.process(app);
aboutData.processCommandLine(&parser);
InputDaemon daemon;
if (!daemon.init()) {
return 1;

View file

@ -3,11 +3,13 @@
#include "inputserviceclient.h"
#include "input1interface.h"
#include <QCoreApplication>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusError>
#include <QDBusInterface>
#include <QDBusPendingReply>
#include <QDBusReply>
#include <QDBusServiceWatcher>
#include <QDBusVariant>
@ -243,17 +245,21 @@ void InputServiceClient::refreshControllers()
return;
}
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
i.setTimeout(2000);
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
iface.setTimeout(2000);
const QDBusReply<bool> rescanReply = i.call(QStringLiteral("Rescan"));
if (!rescanReply.isValid()) {
setLastError(rescanReply.error().message());
{
QDBusPendingReply<bool> rescanReply = iface.Rescan();
rescanReply.waitForFinished();
if (rescanReply.isError()) {
setLastError(rescanReply.error().message());
}
}
const QDBusReply<QVariantList> reply = i.call(QStringLiteral("ListControllers"));
QDBusPendingReply<QVariantList> reply = iface.ListControllers();
reply.waitForFinished();
if (!reply.isValid()) {
if (reply.isError()) {
setLastError(reply.error().message());
return;
}
@ -274,11 +280,13 @@ void InputServiceClient::refreshProfiles()
return;
}
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
i.setTimeout(2000);
const QDBusReply<QVariantList> reply = i.call(QStringLiteral("ListProfiles"));
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
iface.setTimeout(2000);
if (!reply.isValid()) {
QDBusPendingReply<QVariantList> reply = iface.ListProfiles();
reply.waitForFinished();
if (reply.isError()) {
setLastError(reply.error().message());
return;
}
@ -300,11 +308,13 @@ bool InputServiceClient::setActiveProfile(const QString &controllerId, const QSt
return false;
}
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
i.setTimeout(2000);
const QDBusReply<bool> reply = i.call(QStringLiteral("SetActiveProfile"), controllerId, profileId);
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
iface.setTimeout(2000);
if (!reply.isValid()) {
QDBusPendingReply<bool> reply = iface.SetActiveProfile(controllerId, profileId);
reply.waitForFinished();
if (reply.isError()) {
setLastError(reply.error().message());
return false;
}
@ -319,11 +329,13 @@ QString InputServiceClient::createProfile(const QString &name)
return {};
}
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
i.setTimeout(2000);
const QDBusReply<QString> reply = i.call(QStringLiteral("CreateProfile"), name);
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
iface.setTimeout(2000);
if (!reply.isValid()) {
QDBusPendingReply<QString> reply = iface.CreateProfile(name);
reply.waitForFinished();
if (reply.isError()) {
setLastError(reply.error().message());
return {};
}
@ -339,11 +351,13 @@ bool InputServiceClient::deleteProfile(const QString &profileId)
return false;
}
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
i.setTimeout(2000);
const QDBusReply<bool> reply = i.call(QStringLiteral("DeleteProfile"), profileId);
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
iface.setTimeout(2000);
if (!reply.isValid()) {
QDBusPendingReply<bool> reply = iface.DeleteProfile(profileId);
reply.waitForFinished();
if (reply.isError()) {
setLastError(reply.error().message());
return false;
}

View file

@ -18,7 +18,7 @@
K_PLUGIN_CLASS_WITH_JSON(AlakarteRunner, "plasma-runner-alakarte.json")
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
static const QString kGameCenterPath = QStringLiteral("/org/kde/ALaKarte/GameCenter1");
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
static bool launchViaDaemon(QDBusConnection bus, const QVariantMap &launchSpec)
@ -181,7 +181,7 @@ void AlakarteRunner::run(const KRunner::RunnerContext &context, const KRunner::Q
{QStringLiteral("origin"), QStringLiteral("krunner")},
};
if (launchViaDaemon(QDBusConnection::systemBus(), launchSpec) || launchViaDaemon(QDBusConnection::sessionBus(), launchSpec)) {
if (launchViaDaemon(QDBusConnection::sessionBus(), launchSpec)) {
return;
}
}

View file

@ -23,6 +23,9 @@ int main(int argc, char *argv[])
QGuiApplication app(argc, argv);
QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.alakarte"));
app.setOrganizationDomain(QStringLiteral("kde.org"));
app.setApplicationName(QStringLiteral("alakarte"));
KLocalizedString::setApplicationDomain("alakarte");
KAboutData aboutData(QStringLiteral("alakarte"),

View file

@ -7,10 +7,35 @@ add_executable(alakarte_runnerd
runnermanagerdaemon.h
)
set(alakarte_runnerd_dbus_sources)
qt_add_dbus_adaptor(alakarte_runnerd_dbus_sources
dbus/org.kde.ALaKarte.Runner1.xml
runnermanagerdaemon.h
RunnerManagerDaemon
runner1adaptor
Runner1Adaptor
)
qt_add_dbus_interface(alakarte_runnerd_dbus_sources
dbus/org.kde.ALaKarte.Runner1.xml
runner1interface
)
target_sources(alakarte_runnerd PRIVATE
${alakarte_runnerd_dbus_sources}
)
target_include_directories(alakarte_runnerd PRIVATE
${PROJECT_BINARY_DIR}/src
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(alakarte_runnerd PRIVATE
Qt6::Core
Qt6::DBus
Qt6::Network
KF6::CoreAddons
)
set_target_properties(alakarte_runnerd PROPERTIES

View file

@ -0,0 +1,113 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-Bus Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.ALaKarte.Runner1">
<method name="Ping">
<arg name="out" type="s" direction="out"/>
</method>
<method name="Version">
<arg name="major" type="u" direction="out"/>
<arg name="minor" type="u" direction="out"/>
</method>
<method name="ListRunners">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantList"/>
<arg name="runners" type="av" direction="out"/>
</method>
<method name="Rescan">
<arg name="ok" type="b" direction="out"/>
</method>
<method name="RegisterRunner">
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<arg name="runner" type="a{sv}" direction="in"/>
<arg name="ok" type="b" direction="out"/>
</method>
<method name="UnregisterRunner">
<arg name="runnerId" type="s" direction="in"/>
<arg name="ok" type="b" direction="out"/>
</method>
<method name="InstallRunnerFromArchive">
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="spec" type="a{sv}" direction="in"/>
<arg name="result" type="a{sv}" direction="out"/>
</method>
<method name="InstallRunnerFromUrl">
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="spec" type="a{sv}" direction="in"/>
<arg name="result" type="a{sv}" direction="out"/>
</method>
<method name="UninstallRunner">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="runnerId" type="s" direction="in"/>
<arg name="result" type="a{sv}" direction="out"/>
</method>
<method name="CancelInstall">
<arg name="installId" type="s" direction="in"/>
<arg name="ok" type="b" direction="out"/>
</method>
<method name="EnsurePrefix">
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="spec" type="a{sv}" direction="in"/>
<arg name="result" type="a{sv}" direction="out"/>
</method>
<method name="DeletePrefix">
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="spec" type="a{sv}" direction="in"/>
<arg name="result" type="a{sv}" direction="out"/>
</method>
<method name="ResolveLaunch">
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="spec" type="a{sv}" direction="in"/>
<arg name="result" type="a{sv}" direction="out"/>
</method>
<method name="GetGameProfile">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="gameId" type="s" direction="in"/>
<arg name="result" type="a{sv}" direction="out"/>
</method>
<method name="SetGameProfile">
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="spec" type="a{sv}" direction="in"/>
<arg name="result" type="a{sv}" direction="out"/>
</method>
<method name="ClearGameProfile">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
<arg name="gameId" type="s" direction="in"/>
<arg name="result" type="a{sv}" direction="out"/>
</method>
<method name="ListGameProfiles">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantList"/>
<arg name="profiles" type="av" direction="out"/>
</method>
<signal name="InstallStarted">
<arg name="installId" type="s"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
<arg name="spec" type="a{sv}"/>
</signal>
<signal name="InstallProgress">
<arg name="installId" type="s"/>
<arg name="receivedBytes" type="x"/>
<arg name="totalBytes" type="x"/>
</signal>
<signal name="InstallFinished">
<arg name="installId" type="s"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
<arg name="result" type="a{sv}"/>
</signal>
<signal name="GameProfilesChanged"/>
</interface>
</node>

View file

@ -1,16 +1,36 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QDBusConnection>
#include <KAboutData>
#include "alakarte-version.h"
#include "runnermanagerdaemon.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
app.setOrganizationDomain(QStringLiteral("kde.org"));
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
KAboutData aboutData(QStringLiteral("alakarte-runnerd"),
QStringLiteral("A-La-Karte Runner Daemon"),
QStringLiteral(ALAKARTE_VERSION_STRING),
QStringLiteral("Runner management service for A-La-Karte"),
KAboutLicense::GPL_V3,
QStringLiteral("© 2026 A-La-Karte Contributors"));
aboutData.addAuthor(QStringLiteral("A-La-Karte Contributors"), QStringLiteral("Developer"), QString());
KAboutData::setApplicationData(aboutData);
QCommandLineParser parser;
aboutData.setupCommandLine(&parser);
parser.process(app);
aboutData.processCommandLine(&parser);
RunnerManagerDaemon daemon;
if (!daemon.init()) {
return 1;

View file

@ -3,6 +3,8 @@
#include "runnermanagerdaemon.h"
#include "runner1adaptor.h"
#include <QCryptographicHash>
#include <QDBusArgument>
#include <QDBusConnection>
@ -109,7 +111,9 @@ bool RunnerManagerDaemon::init()
return false;
}
if (!bus.registerObject(kRunnerPath, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) {
new Runner1Adaptor(this);
if (!bus.registerObject(kRunnerPath, this, QDBusConnection::ExportAdaptors)) {
qWarning() << "RunnerManagerDaemon: failed to register object" << kRunnerPath << bus.lastError().message();
return false;
}
@ -122,10 +126,10 @@ QString RunnerManagerDaemon::Ping() const
return QStringLiteral("ok");
}
void RunnerManagerDaemon::Version(uint &major, uint &minor) const
uint RunnerManagerDaemon::Version(uint &minor) const
{
major = 1;
minor = 0;
return 1;
}
QVariantMap RunnerManagerDaemon::RunnerInfo::toVariantMap() const

View file

@ -24,7 +24,7 @@ public:
public Q_SLOTS:
QString Ping() const;
void Version(uint &major, uint &minor) const;
uint Version(uint &minor) const;
QVariantList ListRunners();
bool Rescan();

View file

@ -3,12 +3,13 @@
#include "runnermanagerclient.h"
#include "runner1interface.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusMetaType>
#include <QDBusPendingCall>
#include <QDBusPendingCallWatcher>
@ -338,8 +339,8 @@ void RunnerManagerClient::installRunnerFromUrl(const QString &url, const QString
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);
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.InstallRunnerFromUrl(spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, spec, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
@ -397,8 +398,8 @@ void RunnerManagerClient::deletePrefix(const QString &gameId, const QString &pre
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);
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.DeletePrefix(spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
@ -438,8 +439,8 @@ void RunnerManagerClient::cancelCurrentInstall()
setStatus(QStringLiteral("Cancelling..."));
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
iface.asyncCall(QStringLiteral("CancelInstall"), m_installId);
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
iface.CancelInstall(m_installId);
}
void RunnerManagerClient::onInstallStarted(const QString &installId, const QVariantMap &)
@ -489,8 +490,8 @@ void RunnerManagerClient::refreshRunners()
{
ensureRunnerDaemon();
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListRunners"));
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.ListRunners();
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantList> reply = *w;
@ -544,8 +545,8 @@ void RunnerManagerClient::refreshGameProfiles()
{
ensureRunnerDaemon();
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListGameProfiles"));
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.ListGameProfiles();
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantList> reply = *w;
@ -599,8 +600,8 @@ void RunnerManagerClient::requestGameProfile(const QString &gameId)
auto attempts = QSharedPointer<int>::create(0);
auto callPtr = QSharedPointer<std::function<void()>>::create();
*callPtr = [this, id, attempts, callPtr]() {
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("GetGameProfile"), id);
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.GetGameProfile(id);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
@ -641,8 +642,8 @@ void RunnerManagerClient::setGameProfile(const QVariantMap &spec)
auto attempts = QSharedPointer<int>::create(0);
auto callPtr = QSharedPointer<std::function<void()>>::create();
*callPtr = [this, spec, id, attempts, callPtr]() {
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("SetGameProfile"), spec);
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.SetGameProfile(spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
@ -683,8 +684,8 @@ void RunnerManagerClient::clearGameProfile(const QString &gameId)
auto attempts = QSharedPointer<int>::create(0);
auto callPtr = QSharedPointer<std::function<void()>>::create();
*callPtr = [this, id, attempts, callPtr]() {
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ClearGameProfile"), id);
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.ClearGameProfile(id);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
@ -720,8 +721,8 @@ void RunnerManagerClient::uninstallRunner(const QString &runnerId)
return;
}
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.asyncCall(QStringLiteral("UninstallRunner"), id);
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.UninstallRunner(id);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;
@ -763,8 +764,8 @@ void RunnerManagerClient::ensurePrefix(const QString &gameId, const QString &run
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);
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
QDBusPendingCall call = iface.EnsurePrefix(spec);
auto *watcher = new QDBusPendingCallWatcher(call, this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
QDBusPendingReply<QVariantMap> reply = *w;

View file

@ -8,6 +8,31 @@ add_executable(alakarte_dbus_smoketest
dbus_smoketest.cpp
)
set(alakarte_dbus_smoketest_dbus_sources)
qt_add_dbus_interface(alakarte_dbus_smoketest_dbus_sources
${CMAKE_SOURCE_DIR}/src/gamecenter/dbus/org.kde.GameCenter1.xml
gamecenter1interface
)
qt_add_dbus_interface(alakarte_dbus_smoketest_dbus_sources
${CMAKE_SOURCE_DIR}/src/runner/dbus/org.kde.ALaKarte.Runner1.xml
runner1interface
)
qt_add_dbus_interface(alakarte_dbus_smoketest_dbus_sources
${CMAKE_SOURCE_DIR}/src/input/dbus/org.kde.ALaKarte.Input1.xml
input1interface
)
target_sources(alakarte_dbus_smoketest PRIVATE
${alakarte_dbus_smoketest_dbus_sources}
)
target_include_directories(alakarte_dbus_smoketest PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
)
target_link_libraries(alakarte_dbus_smoketest PRIVATE
Qt6::Core
Qt6::DBus
@ -43,3 +68,27 @@ add_test(NAME alakarte_stop_launching_regression_test
set_tests_properties(alakarte_stop_launching_regression_test PROPERTIES
TIMEOUT 60
)
add_executable(alakarte_processscanner_test
processscanner_test.cpp
../src/gamecenter/processscanner.cpp
../src/gamecenter/processscanner.h
)
target_include_directories(alakarte_processscanner_test PRIVATE
${CMAKE_SOURCE_DIR}/src/gamecenter
)
target_link_libraries(alakarte_processscanner_test PRIVATE
Qt6::Core
Qt6::Concurrent
Qt6::Test
)
add_test(NAME alakarte_processscanner_test
COMMAND alakarte_processscanner_test
)
set_tests_properties(alakarte_processscanner_test PROPERTIES
TIMEOUT 20
)

View file

@ -12,11 +12,17 @@
#include <QElapsedTimer>
#include <QProcess>
#include <QProcessEnvironment>
#include <QSet>
#include <QSignalSpy>
#include <QStandardPaths>
#include <QStringList>
#include <QUuid>
#include <QtTest>
#include "gamecenter1interface.h"
#include "input1interface.h"
#include "runner1interface.h"
namespace
{
static QString takeArgValue(const QStringList &args, const QString &key)
@ -79,8 +85,17 @@ private Q_SLOTS:
void cleanupTestCase();
void pingGameCenter();
void gameCenterUniqueness();
void gameCenterPolicyAndCapabilities();
void gameCenterLaunchFailureContract();
void pingRunner();
void runnerVersionAndListRunners();
void runnerResolveLaunchMissingProgram();
void runnerResolveLaunchUnknownRunnerId();
void runnerResolveLaunchWineMissingPrefixOrGameId();
void pingInput();
void inputVersionAndCapabilities();
void inputProfilesCrud();
void runnerResolveLaunchNative();
void runnerGameProfiles();
@ -234,49 +249,298 @@ void DbusSmokeTest::cleanupTestCase()
void DbusSmokeTest::pingGameCenter()
{
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), m_bus);
org::kde::GameCenter1 iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
QVERIFY(reply.isValid());
QDBusPendingReply<QString> reply = iface.Ping();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QCOMPARE(reply.value(), QStringLiteral("ok"));
}
void DbusSmokeTest::gameCenterUniqueness()
{
pingGameCenter();
QVERIFY(m_bus.interface());
const QDBusReply<QString> ownerBeforeReply = m_bus.interface()->serviceOwner(QStringLiteral("org.kde.GameCenter1"));
QVERIFY(ownerBeforeReply.isValid());
const QString ownerBefore = ownerBeforeReply.value();
QVERIFY(!ownerBefore.isEmpty());
QProcess second;
startDaemon(second, m_gamecenterPath);
QVERIFY(second.waitForStarted(5000));
const bool finished = second.waitForFinished(5000);
if (!finished) {
stopProcess(second);
}
QVERIFY(finished);
QCOMPARE(second.exitStatus(), QProcess::NormalExit);
const QDBusReply<QString> ownerAfterReply = m_bus.interface()->serviceOwner(QStringLiteral("org.kde.GameCenter1"));
QVERIFY(ownerAfterReply.isValid());
QCOMPARE(ownerAfterReply.value(), ownerBefore);
pingGameCenter();
}
void DbusSmokeTest::gameCenterPolicyAndCapabilities()
{
org::kde::GameCenter1 iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
{
QDBusPendingReply<uint, uint> reply = iface.Version();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QCOMPARE(reply.argumentAt<0>(), 1u);
QCOMPARE(reply.argumentAt<1>(), 0u);
}
{
QDBusPendingReply<QVariantMap> reply = iface.GetCapabilities();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantMap caps = reply.value();
QVERIFY(caps.contains(QStringLiteral("supportsSystemd")));
QVERIFY(caps.value(QStringLiteral("supportsSystemd")).canConvert<bool>());
QVERIFY(caps.contains(QStringLiteral("supportsSystemBus")));
QCOMPARE(caps.value(QStringLiteral("supportsSystemBus")).toBool(), false);
QVERIFY(caps.contains(QStringLiteral("supportsPowerProfiles")));
QVERIFY(caps.value(QStringLiteral("supportsPowerProfiles")).canConvert<bool>());
}
QVariantMap originalPolicy;
{
QDBusPendingReply<QVariantMap> reply = iface.GetPolicy();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
originalPolicy = reply.value();
}
{
QVariantMap policy;
policy.insert(QStringLiteral("maxConcurrent"), 0);
QDBusPendingReply<> reply = iface.SetPolicy(policy);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
}
{
QDBusPendingReply<QVariantMap> reply = iface.GetPolicy();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QCOMPARE(reply.value().value(QStringLiteral("maxConcurrent")).toInt(), 0);
}
{
const int originalMaxConcurrent = originalPolicy.value(QStringLiteral("maxConcurrent"), 0).toInt();
QVariantMap policy;
policy.insert(QStringLiteral("maxConcurrent"), originalMaxConcurrent);
QDBusPendingReply<> reply = iface.SetPolicy(policy);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
}
{
QDBusPendingReply<QVariantList> reply = iface.ListSessions();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
}
{
QDBusPendingReply<> reply = iface.StopByGameId(QStringLiteral("alakarte-test-nonexistent"));
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
}
}
void DbusSmokeTest::gameCenterLaunchFailureContract()
{
org::kde::GameCenter1 iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
QSignalSpy launchFailedSpy(&iface, &org::kde::GameCenter1::LaunchFailed);
QVERIFY(launchFailedSpy.isValid());
QVariantMap launchSpec;
launchSpec.insert(QStringLiteral("gameId"), QStringLiteral("alakarte-test-game"));
launchSpec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
launchSpec.insert(QStringLiteral("args"), QStringList{});
QDBusPendingReply<QString> reply = iface.Launch(launchSpec);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QVERIFY(reply.value().isEmpty());
QTRY_COMPARE_WITH_TIMEOUT(launchFailedSpy.count(), 1, 2000);
const QList<QVariant> args = launchFailedSpy.takeFirst();
QCOMPARE(args.size(), 1);
const QVariantMap error = unwrapVariantMap(args.first());
QVERIFY(error.contains(QStringLiteral("error")));
QVERIFY(error.value(QStringLiteral("error")).toString().contains(QStringLiteral("systemd"), Qt::CaseInsensitive));
QCOMPARE(error.value(QStringLiteral("gameId")).toString(), QStringLiteral("alakarte-test-game"));
}
void DbusSmokeTest::pingRunner()
{
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
QStringLiteral("/org/kde/ALaKarte/Runner1"),
QStringLiteral("org.kde.ALaKarte.Runner1"),
m_bus);
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
QVERIFY(reply.isValid());
QDBusPendingReply<QString> reply = iface.Ping();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QCOMPARE(reply.value(), QStringLiteral("ok"));
}
void DbusSmokeTest::runnerVersionAndListRunners()
{
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(5000);
{
QDBusPendingReply<uint, uint> reply = iface.Version();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QCOMPARE(reply.argumentAt<0>(), 1u);
QCOMPARE(reply.argumentAt<1>(), 0u);
}
QDBusPendingReply<QVariantList> reply = iface.ListRunners();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantList list = reply.value();
QSet<QString> ids;
for (const QVariant &v : list) {
const QVariantMap m = unwrapVariantMap(v);
const QString id = m.value(QStringLiteral("id")).toString();
const QString type = m.value(QStringLiteral("type")).toString();
const QString path = m.value(QStringLiteral("path")).toString();
QVERIFY2(!id.isEmpty(), "runner entry missing id");
QVERIFY2(!type.isEmpty(), qPrintable(QStringLiteral("runner '%1' missing type").arg(id)));
QVERIFY2(!path.isEmpty(), qPrintable(QStringLiteral("runner '%1' missing path").arg(id)));
QVERIFY2(!ids.contains(id), qPrintable(QStringLiteral("duplicate runner id '%1'").arg(id)));
ids.insert(id);
}
}
void DbusSmokeTest::pingInput()
{
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Input1"),
QStringLiteral("/org/kde/ALaKarte/Input1"),
QStringLiteral("org.kde.ALaKarte.Input1"),
m_bus);
org::kde::ALaKarte::Input1 iface(QStringLiteral("org.kde.ALaKarte.Input1"), QStringLiteral("/org/kde/ALaKarte/Input1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
QVERIFY(reply.isValid());
QDBusPendingReply<QString> reply = iface.Ping();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QCOMPARE(reply.value(), QStringLiteral("ok"));
}
void DbusSmokeTest::inputVersionAndCapabilities()
{
org::kde::ALaKarte::Input1 iface(QStringLiteral("org.kde.ALaKarte.Input1"), QStringLiteral("/org/kde/ALaKarte/Input1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
{
QDBusPendingReply<uint, uint> reply = iface.Version();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QCOMPARE(reply.argumentAt<0>(), 1u);
QCOMPARE(reply.argumentAt<1>(), 0u);
}
{
QDBusPendingReply<QVariantMap> reply = iface.GetCapabilities();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantMap caps = reply.value();
QVERIFY(caps.contains(QStringLiteral("supportsBattery")));
QVERIFY(caps.value(QStringLiteral("supportsBattery")).canConvert<bool>());
QCOMPARE(caps.value(QStringLiteral("supportsBattery")).toBool(), true);
QVERIFY(caps.contains(QStringLiteral("supportsHotplug")));
QVERIFY(caps.value(QStringLiteral("supportsHotplug")).canConvert<bool>());
QCOMPARE(caps.value(QStringLiteral("supportsHotplug")).toBool(), true);
QVERIFY(caps.contains(QStringLiteral("supportsProfiles")));
QVERIFY(caps.value(QStringLiteral("supportsProfiles")).canConvert<bool>());
QCOMPARE(caps.value(QStringLiteral("supportsProfiles")).toBool(), true);
}
}
void DbusSmokeTest::inputProfilesCrud()
{
org::kde::ALaKarte::Input1 iface(QStringLiteral("org.kde.ALaKarte.Input1"), QStringLiteral("/org/kde/ALaKarte/Input1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
QDBusPendingReply<QString> createReply = iface.CreateProfile(QStringLiteral("Test Profile"));
createReply.waitForFinished();
QVERIFY2(!createReply.isError(), qPrintable(createReply.error().message()));
const QString profileId = createReply.value();
QVERIFY(!profileId.isEmpty());
{
QDBusPendingReply<QVariantList> listReply = iface.ListProfiles();
listReply.waitForFinished();
QVERIFY2(!listReply.isError(), qPrintable(listReply.error().message()));
const QVariantList list = listReply.value();
bool found = false;
for (const QVariant &v : list) {
const QVariantMap m = unwrapVariantMap(v);
if (m.value(QStringLiteral("id")).toString() == profileId) {
found = true;
QCOMPARE(m.value(QStringLiteral("name")).toString(), QStringLiteral("Test Profile"));
break;
}
}
QVERIFY(found);
}
{
QDBusPendingReply<QVariantMap> getReply = iface.GetProfile(profileId);
getReply.waitForFinished();
QVERIFY2(!getReply.isError(), qPrintable(getReply.error().message()));
const QVariantMap m = getReply.value();
QCOMPARE(m.value(QStringLiteral("id")).toString(), profileId);
QCOMPARE(m.value(QStringLiteral("name")).toString(), QStringLiteral("Test Profile"));
}
{
QDBusPendingReply<bool> delReply = iface.DeleteProfile(profileId);
delReply.waitForFinished();
QVERIFY2(!delReply.isError(), qPrintable(delReply.error().message()));
QVERIFY(delReply.value());
}
{
QDBusPendingReply<QVariantList> listReply = iface.ListProfiles();
listReply.waitForFinished();
QVERIFY2(!listReply.isError(), qPrintable(listReply.error().message()));
const QVariantList list = listReply.value();
for (const QVariant &v : list) {
const QVariantMap m = unwrapVariantMap(v);
QVERIFY(m.value(QStringLiteral("id")).toString() != profileId);
}
}
}
void DbusSmokeTest::runnerResolveLaunchNative()
{
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
QStringLiteral("/org/kde/ALaKarte/Runner1"),
QStringLiteral("org.kde.ALaKarte.Runner1"),
m_bus);
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
@ -284,8 +548,9 @@ void DbusSmokeTest::runnerResolveLaunchNative()
spec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
spec.insert(QStringLiteral("args"), QStringList{});
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
QVERIFY(reply.isValid());
QDBusPendingReply<QVariantMap> reply = iface.ResolveLaunch(spec);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantMap out = reply.value();
QVERIFY(out.value(QStringLiteral("ok")).toBool());
@ -293,20 +558,79 @@ void DbusSmokeTest::runnerResolveLaunchNative()
QCOMPARE(out.value(QStringLiteral("finalArgs")).toStringList(), QStringList{});
}
void DbusSmokeTest::runnerResolveLaunchMissingProgram()
{
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
QVariantMap spec;
spec.insert(QStringLiteral("args"), QStringList{});
QDBusPendingReply<QVariantMap> reply = iface.ResolveLaunch(spec);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantMap out = reply.value();
QCOMPARE(out.value(QStringLiteral("ok")).toBool(), false);
QVERIFY(out.contains(QStringLiteral("error")));
QVERIFY(out.value(QStringLiteral("error")).toString().contains(QStringLiteral("missing program"), Qt::CaseInsensitive));
}
void DbusSmokeTest::runnerResolveLaunchUnknownRunnerId()
{
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
QVariantMap spec;
spec.insert(QStringLiteral("runnerId"), QStringLiteral("alakarte-test-nonexistent"));
spec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
spec.insert(QStringLiteral("args"), QStringList{});
QDBusPendingReply<QVariantMap> reply = iface.ResolveLaunch(spec);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantMap out = reply.value();
QCOMPARE(out.value(QStringLiteral("ok")).toBool(), false);
QVERIFY(out.contains(QStringLiteral("error")));
QVERIFY(out.value(QStringLiteral("error")).toString().contains(QStringLiteral("unknown runnerId"), Qt::CaseInsensitive));
}
void DbusSmokeTest::runnerResolveLaunchWineMissingPrefixOrGameId()
{
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
QVariantMap spec;
spec.insert(QStringLiteral("runner"), QStringLiteral("wine"));
spec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
spec.insert(QStringLiteral("args"), QStringList{});
QDBusPendingReply<QVariantMap> reply = iface.ResolveLaunch(spec);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantMap out = reply.value();
QCOMPARE(out.value(QStringLiteral("ok")).toBool(), false);
QVERIFY(out.contains(QStringLiteral("error")));
QVERIFY(out.value(QStringLiteral("error")).toString().contains(QStringLiteral("missing prefixPath or gameId"), Qt::CaseInsensitive));
}
void DbusSmokeTest::runnerGameProfiles()
{
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
QStringLiteral("/org/kde/ALaKarte/Runner1"),
QStringLiteral("org.kde.ALaKarte.Runner1"),
m_bus);
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
const QString gameId = QStringLiteral("alakarte-test-game");
{
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ClearGameProfile"), gameId);
QVERIFY(reply.isValid());
QDBusPendingReply<QVariantMap> reply = iface.ClearGameProfile(gameId);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
}
@ -320,8 +644,9 @@ void DbusSmokeTest::runnerGameProfiles()
spec.insert(QStringLiteral("dllOverrides"),
QVariantMap{{QStringLiteral("d3d11"), QStringLiteral("native,builtin")}, {QStringLiteral("dxgi"), QStringLiteral("native,builtin")}});
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("SetGameProfile"), spec);
QVERIFY(reply.isValid());
QDBusPendingReply<QVariantMap> reply = iface.SetGameProfile(spec);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantMap out = reply.value();
QVERIFY(out.value(QStringLiteral("ok")).toBool());
@ -338,8 +663,9 @@ void DbusSmokeTest::runnerGameProfiles()
}
{
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("GetGameProfile"), gameId);
QVERIFY(reply.isValid());
QDBusPendingReply<QVariantMap> reply = iface.GetGameProfile(gameId);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantMap out = reply.value();
QVERIFY(out.value(QStringLiteral("ok")).toBool());
const QVariantMap profile = unwrapVariantMap(out.value(QStringLiteral("profile")));
@ -360,8 +686,9 @@ void DbusSmokeTest::runnerGameProfiles()
spec.insert(QStringLiteral("args"), QStringList{QStringLiteral("--base-arg")});
spec.insert(QStringLiteral("extraArgs"), QStringList{QStringLiteral("--spec-arg")});
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
QVERIFY(reply.isValid());
QDBusPendingReply<QVariantMap> reply = iface.ResolveLaunch(spec);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantMap out = reply.value();
QVERIFY(out.value(QStringLiteral("ok")).toBool());
QCOMPARE(out.value(QStringLiteral("finalProgram")).toString(), QStringLiteral("/bin/true"));
@ -379,8 +706,9 @@ void DbusSmokeTest::runnerGameProfiles()
}
{
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListGameProfiles"));
QVERIFY(reply.isValid());
QDBusPendingReply<QVariantList> reply = iface.ListGameProfiles();
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
const QVariantList list = reply.value();
bool found = false;
for (const QVariant &v : list) {
@ -394,8 +722,9 @@ void DbusSmokeTest::runnerGameProfiles()
}
{
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ClearGameProfile"), gameId);
QVERIFY(reply.isValid());
QDBusPendingReply<QVariantMap> reply = iface.ClearGameProfile(gameId);
reply.waitForFinished();
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
}
}

View file

@ -0,0 +1,202 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include "processscanner.h"
#include <QElapsedTimer>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
#include <QString>
#include <QUuid>
#include <QtTest>
#include <atomic>
#include <utility>
class ProcessScannerTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void scanFindsEnvAndCmdline();
void preferEnvironmentMatchesFilters();
void cancelledScanReturnsEmpty();
private:
QString m_shell;
QString m_marker;
QProcess m_envProc;
QProcess m_cmdProc;
static bool containsPid(const QList<ProcessScanner::Match> &matches, qint64 pid)
{
for (const auto &m : matches) {
if (m.pid == static_cast<uint>(pid)) {
return true;
}
}
return false;
}
static const ProcessScanner::Match *findByPid(const QList<ProcessScanner::Match> &matches, qint64 pid)
{
for (const auto &m : matches) {
if (m.pid == static_cast<uint>(pid)) {
return &m;
}
}
return nullptr;
}
static bool waitForScan(std::function<bool()> predicate, int timeoutMs)
{
QElapsedTimer t;
t.start();
while (t.elapsed() < timeoutMs) {
if (predicate()) {
return true;
}
QTest::qWait(50);
}
return false;
}
void startProcesses();
void stopProcess(QProcess &p);
};
void ProcessScannerTest::initTestCase()
{
if (!QFileInfo(QStringLiteral("/proc")).isDir()) {
QSKIP("/proc not available");
}
m_shell = QStandardPaths::findExecutable(QStringLiteral("sh"));
if (m_shell.isEmpty()) {
QSKIP("sh not found");
}
m_marker = QUuid::createUuid().toString(QUuid::WithoutBraces);
startProcesses();
}
void ProcessScannerTest::cleanupTestCase()
{
stopProcess(m_envProc);
stopProcess(m_cmdProc);
}
void ProcessScannerTest::startProcesses()
{
const QString script = QStringLiteral("while true; do sleep 60; done # %1").arg(m_marker);
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("ALAKARTE_PROCESSSCANNER_TEST_MARKER"), m_marker);
m_envProc.setProcessEnvironment(env);
m_envProc.start(m_shell, {QStringLiteral("-c"), script});
QVERIFY2(m_envProc.waitForStarted(5000), qPrintable(m_envProc.errorString()));
QVERIFY(m_envProc.processId() > 0);
}
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
m_cmdProc.setProcessEnvironment(env);
m_cmdProc.start(m_shell, {QStringLiteral("-c"), script});
QVERIFY2(m_cmdProc.waitForStarted(5000), qPrintable(m_cmdProc.errorString()));
QVERIFY(m_cmdProc.processId() > 0);
}
}
void ProcessScannerTest::stopProcess(QProcess &p)
{
if (p.state() == QProcess::NotRunning) {
return;
}
p.terminate();
if (!p.waitForFinished(3000)) {
p.kill();
p.waitForFinished(3000);
}
}
void ProcessScannerTest::scanFindsEnvAndCmdline()
{
QVERIFY(m_envProc.processId() > 0);
QVERIFY(m_cmdProc.processId() > 0);
ProcessScanner::ScanOptions opts;
opts.envKeys = {QStringLiteral("ALAKARTE_PROCESSSCANNER_TEST_MARKER")};
opts.envValue = m_marker;
opts.cmdlineSubstring = m_marker;
opts.preferEnvironmentMatches = false;
QList<ProcessScanner::Match> results;
const bool ok = waitForScan(
[&]() {
results = ProcessScanner::scan(opts);
return containsPid(results, m_envProc.processId()) && containsPid(results, m_cmdProc.processId());
},
5000);
QVERIFY(ok);
const auto *envMatch = findByPid(results, m_envProc.processId());
QVERIFY(envMatch);
QVERIFY(envMatch->envMatched);
const auto *cmdMatch = findByPid(results, m_cmdProc.processId());
QVERIFY(cmdMatch);
QVERIFY(!cmdMatch->envMatched);
}
void ProcessScannerTest::preferEnvironmentMatchesFilters()
{
QVERIFY(m_envProc.processId() > 0);
QVERIFY(m_cmdProc.processId() > 0);
ProcessScanner::ScanOptions opts;
opts.envKeys = {QStringLiteral("ALAKARTE_PROCESSSCANNER_TEST_MARKER")};
opts.envValue = m_marker;
opts.cmdlineSubstring = m_marker;
opts.preferEnvironmentMatches = true;
QList<ProcessScanner::Match> results;
const bool ok = waitForScan(
[&]() {
results = ProcessScanner::scan(opts);
return containsPid(results, m_envProc.processId());
},
5000);
QVERIFY(ok);
QVERIFY(containsPid(results, m_envProc.processId()));
QVERIFY(!containsPid(results, m_cmdProc.processId()));
for (const auto &m : std::as_const(results)) {
QVERIFY(m.envMatched);
}
}
void ProcessScannerTest::cancelledScanReturnsEmpty()
{
ProcessScanner::ScanOptions opts;
opts.envKeys = {QStringLiteral("ALAKARTE_PROCESSSCANNER_TEST_MARKER")};
opts.envValue = m_marker;
opts.cmdlineSubstring = m_marker;
opts.preferEnvironmentMatches = false;
const std::atomic_bool cancelled(true);
const QList<ProcessScanner::Match> results = ProcessScanner::scan(opts, cancelled);
QVERIFY(results.isEmpty());
}
QTEST_MAIN(ProcessScannerTest)
#include "processscanner_test.moc"

View file

@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCoreApplication>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
@ -53,6 +54,27 @@ static bool waitForService(QDBusConnection &bus, const QString &service, int tim
return false;
}
static QVariant unwrapDbusVariant(QVariant v)
{
if (v.canConvert<QDBusVariant>()) {
v = v.value<QDBusVariant>().variant();
}
return v;
}
static QVariantMap unwrapVariantMap(QVariant v)
{
v = unwrapDbusVariant(v);
if (v.canConvert<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>();
return qdbus_cast<QVariantMap>(arg);
}
if (v.canConvert<QVariantMap>()) {
return v.toMap();
}
return {};
}
static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
{
QFile f(path);
@ -226,6 +248,8 @@ private Q_SLOTS:
void stopDirectLaunch();
void stopByGameIdDirectLaunch();
void recoverSessionsAfterDaemonRestart();
private:
QString m_gamecenterPath;
QProcess m_gamecenter;
@ -351,6 +375,78 @@ void StopLaunchingRegressionTest::cleanupTestCase()
}
}
void StopLaunchingRegressionTest::recoverSessionsAfterDaemonRestart()
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
killPids(pidsWithEnvironEntry(markerNeedle));
const QString envExe = QStandardPaths::findExecutable(QStringLiteral("env"));
if (envExe.isEmpty()) {
QSKIP("env executable not found");
}
const QString gameId = QStringLiteral("test-reattach-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
QVariantMap spec;
spec.insert(QStringLiteral("provider"), QStringLiteral("manual"));
spec.insert(QStringLiteral("program"), envExe);
spec.insert(QStringLiteral("args"),
QStringList{QStringLiteral("ALAKARTE_TEST_MARKER=%1").arg(m_markerValue), QStringLiteral("/bin/sleep"), QStringLiteral("60")});
spec.insert(QStringLiteral("gameId"), gameId);
const QDBusReply<QString> launchReply = iface.call(QStringLiteral("Launch"), spec);
QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message()));
const QString sessionId = launchReply.value();
QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId");
const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId);
QVERIFY2(waitForAnyEnvironEntry(markerNeedle, 5000), "direct launch marker process did not appear");
m_gamecenter.kill();
QVERIFY(m_gamecenter.waitForFinished(5000));
QVERIFY2(waitForNoEnvironEntry(markerNeedle, 1000) == false, "marker process unexpectedly disappeared after daemon kill");
m_gamecenter.start();
QVERIFY(m_gamecenter.waitForStarted(5000));
QVERIFY2(waitForService(bus, QStringLiteral("org.kde.GameCenter1"), 5000), "GameCenter1 service did not re-appear after restart");
QDBusInterface iface2(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface2.isValid(), "GameCenter1 DBus interface not valid after restart");
const QDBusReply<QVariantList> listReply = iface2.call(QStringLiteral("ListSessions"));
QVERIFY2(listReply.isValid(), qPrintable(listReply.error().message()));
bool found = false;
for (const QVariant &v : listReply.value()) {
const QVariantMap m = unwrapVariantMap(v);
if (m.value(QStringLiteral("sessionId")).toString() == sessionId) {
found = true;
QCOMPARE(m.value(QStringLiteral("gameId")).toString(), gameId);
QCOMPARE(m.value(QStringLiteral("state")).toString(), QStringLiteral("Running"));
break;
}
}
QVERIFY2(found, "expected recovered session not found in ListSessions() after restart");
const QDBusReply<void> stopReply = iface2.call(QStringLiteral("Stop"), sessionId);
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
if (!cleaned) {
killPids(pidsWithEnvironEntry(markerNeedle));
}
QVERIFY2(cleaned, "marker process still alive after Stop() on recovered session");
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() on recovered session");
}
void StopLaunchingRegressionTest::stopWhileLaunchingSteam()
{
QDBusConnection bus = QDBusConnection::sessionBus();
@ -358,7 +454,7 @@ void StopLaunchingRegressionTest::stopWhileLaunchingSteam()
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QString appId = QStringLiteral("987654");
@ -396,7 +492,7 @@ void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingSteam()
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
@ -438,7 +534,7 @@ void StopLaunchingRegressionTest::stopWhileLaunchingLutris()
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
@ -476,7 +572,7 @@ void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingLutris()
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
@ -514,7 +610,7 @@ void StopLaunchingRegressionTest::stopDirectLaunch()
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
@ -561,7 +657,7 @@ void StopLaunchingRegressionTest::stopByGameIdDirectLaunch()
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();