mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
Compare commits
29 commits
cca49615d6
...
0b10b01cc4
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b10b01cc4 | |||
| 09026ca26a | |||
| b24417df69 | |||
| 13b65135f1 | |||
| d254b272fe | |||
| a893d4fd71 | |||
| cd23253386 | |||
| 47cab1cbe5 | |||
| c58801828c | |||
| 1d61f3f84f | |||
| 7c9a0c4a89 | |||
| fe253d89bc | |||
| 239e3a9071 | |||
| 16d4129ede | |||
| 4676b1d9a6 | |||
| d93dbc9ecc | |||
| d94029fbc4 | |||
| ed0e6ae181 | |||
| c0938aa62a | |||
| 9cda4ce476 | |||
| 5b26b85cc3 | |||
| f03eb95b52 | |||
| 7200ad179c | |||
| 985f6dac03 | |||
| d56b91dbd0 | |||
| f3b130008f | |||
| 8891e85dbc | |||
| 57d1e6e130 | |||
| d4e0326974 |
33 changed files with 1658 additions and 311 deletions
|
|
@ -47,6 +47,7 @@ find_package(Qt6 ${QT_MIN_VERSION} REQUIRED COMPONENTS
|
||||||
Network
|
Network
|
||||||
Concurrent
|
Concurrent
|
||||||
DBus
|
DBus
|
||||||
|
DBusTools
|
||||||
Svg
|
Svg
|
||||||
Sql
|
Sql
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,31 @@ add_executable(alakarte
|
||||||
config.cpp
|
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
|
set_source_files_properties(qml/icons/app/org.kde.alakarte.svg PROPERTIES
|
||||||
QT_RESOURCE_ALIAS "sc-apps-org.kde.alakarte.svg"
|
QT_RESOURCE_ALIAS "sc-apps-org.kde.alakarte.svg"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,30 @@ add_executable(alakarte_gamecenter
|
||||||
systemdusermanager.h
|
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
|
target_link_libraries(alakarte_gamecenter PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::Concurrent
|
Qt6::Concurrent
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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
|
|
||||||
63
src/gamecenter/dbus/org.kde.GameCenter1.xml
Normal file
63
src/gamecenter/dbus/org.kde.GameCenter1.xml
Normal 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>
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
#include "gamecenterdaemon.h"
|
#include "gamecenterdaemon.h"
|
||||||
|
|
||||||
|
#include "gamecenter1adaptor.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDBusArgument>
|
#include <QDBusArgument>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
|
|
@ -803,6 +805,18 @@ void GameCenterDaemon::handleSystemdUnitRemoved(const QString &unitName, const Q
|
||||||
return;
|
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);
|
unwatchSystemdUnit(sit.value().unitName, sit.value().unitPath);
|
||||||
removeSessionInternal(sessionId, sit.value().stopping ? QStringLiteral("Stopped") : QStringLiteral("Exited"));
|
removeSessionInternal(sessionId, sit.value().stopping ? QStringLiteral("Stopped") : QStringLiteral("Exited"));
|
||||||
}
|
}
|
||||||
|
|
@ -821,6 +835,15 @@ void GameCenterDaemon::handleSystemdUnitPropertiesChanged(const QDBusObjectPath
|
||||||
|
|
||||||
auto sit = m_sessions.find(sessionId);
|
auto sit = m_sessions.find(sessionId);
|
||||||
if (sit == m_sessions.end()) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -833,6 +856,25 @@ void GameCenterDaemon::handleSystemdUnitPropertiesChanged(const QDBusObjectPath
|
||||||
}
|
}
|
||||||
const QString activeState = activeStateV.toString();
|
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")) {
|
if (activeState == QLatin1String("active") || activeState == QLatin1String("activating") || activeState == QLatin1String("deactivating")) {
|
||||||
const QList<uint> pids = m_systemd.scopePids(unitPath);
|
const QList<uint> pids = m_systemd.scopePids(unitPath);
|
||||||
if (!pids.isEmpty()) {
|
if (!pids.isEmpty()) {
|
||||||
|
|
@ -928,11 +970,10 @@ bool GameCenterDaemon::init()
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bus.registerService(QStringLiteral("org.kde.GameCenter1"))) {
|
new GameCenter1Adaptor(this);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -978,10 +1019,10 @@ QString GameCenterDaemon::Ping() const
|
||||||
return QStringLiteral("ok");
|
return QStringLiteral("ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameCenterDaemon::Version(uint &major, uint &minor) const
|
uint GameCenterDaemon::Version(uint &minor) const
|
||||||
{
|
{
|
||||||
major = 1;
|
|
||||||
minor = 0;
|
minor = 0;
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap GameCenterDaemon::GetCapabilities() const
|
QVariantMap GameCenterDaemon::GetCapabilities() const
|
||||||
|
|
@ -1365,7 +1406,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
|
||||||
// Determine bootstrap command and scanner matcher
|
// Determine bootstrap command and scanner matcher
|
||||||
QString bootstrapProgram;
|
QString bootstrapProgram;
|
||||||
QStringList bootstrapArgs;
|
QStringList bootstrapArgs;
|
||||||
std::function<QList<ProcessScanner::Match>()> matcher;
|
ProcessScanner::Matcher matcher;
|
||||||
|
|
||||||
if (provider == QLatin1String("steam")) {
|
if (provider == QLatin1String("steam")) {
|
||||||
const QString appId = extractSteamAppId(command);
|
const QString appId = extractSteamAppId(command);
|
||||||
|
|
@ -1377,9 +1418,13 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
|
||||||
bootstrapProgram = QStringLiteral("steam");
|
bootstrapProgram = QStringLiteral("steam");
|
||||||
bootstrapArgs = {QStringLiteral("-silent"), QStringLiteral("-applaunch"), appId};
|
bootstrapArgs = {QStringLiteral("-silent"), QStringLiteral("-applaunch"), appId};
|
||||||
|
|
||||||
matcher = [appId]() -> QList<ProcessScanner::Match> {
|
matcher = [appId](const std::atomic_bool &cancelled) -> QList<ProcessScanner::Match> {
|
||||||
// Primary: look for SteamAppId=<appId> in environ
|
ProcessScanner::ScanOptions opts;
|
||||||
QList<ProcessScanner::Match> results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId);
|
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
|
// Filter out the steam client itself
|
||||||
QList<ProcessScanner::Match> filtered;
|
QList<ProcessScanner::Match> filtered;
|
||||||
for (const auto &m : std::as_const(results)) {
|
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)};
|
bootstrapArgs = {QStringLiteral("lutris:rungameid/%1").arg(lutrisId)};
|
||||||
|
|
||||||
// For Lutris, look for child processes with the game slug or lutris game env
|
// For Lutris, look for child processes with the game slug or lutris game env
|
||||||
matcher = [lutrisId]() -> QList<ProcessScanner::Match> {
|
matcher = [lutrisId](const std::atomic_bool &cancelled) -> QList<ProcessScanner::Match> {
|
||||||
QList<ProcessScanner::Match> results = ProcessScanner::findByEnvironment(QStringLiteral("LUTRIS_GAME_SLUG"), lutrisId);
|
ProcessScanner::ScanOptions opts;
|
||||||
if (results.isEmpty()) {
|
opts.envKeys = {QStringLiteral("LUTRIS_GAME_SLUG")};
|
||||||
results = ProcessScanner::findByCmdline(lutrisId);
|
opts.envValue = lutrisId;
|
||||||
// Filter out lutris itself
|
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;
|
QList<ProcessScanner::Match> filtered;
|
||||||
for (const auto &m : std::as_const(results)) {
|
for (const auto &m : std::as_const(results)) {
|
||||||
if (!m.exe.endsWith(QLatin1String("/lutris")) && !m.exe.endsWith(QLatin1String("/python3"))) {
|
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;
|
results = filtered;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1498,6 +1558,8 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
|
||||||
|
|
||||||
m_sessions[sessionId] = session;
|
m_sessions[sessionId] = session;
|
||||||
|
|
||||||
|
watchSystemdUnit(sessionId, session.unitName, session.unitPath);
|
||||||
|
|
||||||
connect(bootstrap, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this, sessionId](int, QProcess::ExitStatus) {
|
connect(bootstrap, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this, sessionId](int, QProcess::ExitStatus) {
|
||||||
const auto it = m_sessions.find(sessionId);
|
const auto it = m_sessions.find(sessionId);
|
||||||
if (it == m_sessions.end()) {
|
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"));
|
QVariantMap finalState = sessionToVariantMap(it.value(), stopping ? QStringLiteral("Stopped") : QStringLiteral("Failed"));
|
||||||
|
|
||||||
if (!it.value().unitName.isEmpty()) {
|
if (!it.value().unitName.isEmpty()) {
|
||||||
m_systemd.stopUnit(it.value().unitName);
|
const QString unitName = it.value().unitName;
|
||||||
unwatchSystemdUnit(it.value().unitName, it.value().unitPath);
|
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) {
|
if (it.value().scanner) {
|
||||||
|
|
@ -1612,6 +1697,28 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
|
||||||
{QStringLiteral("error"), attachReply.error().message()},
|
{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"));
|
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed"));
|
||||||
if (it.value().process) {
|
if (it.value().process) {
|
||||||
it.value().process->deleteLater();
|
it.value().process->deleteLater();
|
||||||
|
|
@ -1783,6 +1890,32 @@ void GameCenterDaemon::Stop(const QString &sessionId)
|
||||||
return;
|
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) {
|
if (it.value().process && it.value().process->state() != QProcess::NotRunning) {
|
||||||
it.value().process->terminate();
|
it.value().process->terminate();
|
||||||
QTimer::singleShot(5000, it.value().process, [process = it.value().process]() {
|
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 prefix = QStringLiteral("alakarte-game-");
|
||||||
static const QString suffix = QStringLiteral(".scope");
|
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();
|
const SystemdUnitInfoList units = reply.value();
|
||||||
for (const SystemdUnitInfo &unit : units) {
|
for (const SystemdUnitInfo &unit : units) {
|
||||||
|
|
@ -1900,6 +2034,10 @@ void GameCenterDaemon::recoverExistingSessions()
|
||||||
continue;
|
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());
|
const QString sessionId = unit.name.mid(prefix.length(), unit.name.length() - prefix.length() - suffix.length());
|
||||||
if (sessionId.isEmpty() || m_sessions.contains(sessionId)) {
|
if (sessionId.isEmpty() || m_sessions.contains(sessionId)) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public:
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
QString Ping() const;
|
QString Ping() const;
|
||||||
void Version(uint &major, uint &minor) const;
|
uint Version(uint &minor) const;
|
||||||
QVariantMap GetCapabilities() const;
|
QVariantMap GetCapabilities() const;
|
||||||
|
|
||||||
QVariantList ListSessions() const;
|
QVariantList ListSessions() const;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@
|
||||||
#include <KAuth/ActionReply>
|
#include <KAuth/ActionReply>
|
||||||
#include <KAuth/HelperSupport>
|
#include <KAuth/HelperSupport>
|
||||||
|
|
||||||
|
#include <QDBusArgument>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
#include <QDBusError>
|
#include <QDBusError>
|
||||||
#include <QDBusInterface>
|
#include <QDBusInterface>
|
||||||
#include <QDBusMessage>
|
#include <QDBusMessage>
|
||||||
|
|
@ -28,6 +30,14 @@ public Q_SLOTS:
|
||||||
ActionReply setpowerprofile(const QVariantMap &args);
|
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)
|
static QString unwrapStringArg(const QVariantMap &args, const QString &key)
|
||||||
{
|
{
|
||||||
QVariant v = args.value(key);
|
QVariant v = args.value(key);
|
||||||
|
|
@ -53,6 +63,12 @@ ActionReply GameCenterHelper::setpowerprofile(const QVariantMap &args)
|
||||||
return reply;
|
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);
|
QDBusInterface props(kPowerProfilesService, kPowerProfilesPath, QStringLiteral("org.freedesktop.DBus.Properties"), bus);
|
||||||
if (!props.isValid()) {
|
if (!props.isValid()) {
|
||||||
ActionReply reply = ActionReply::HelperErrorReply();
|
ActionReply reply = ActionReply::HelperErrorReply();
|
||||||
|
|
@ -60,6 +76,44 @@ ActionReply GameCenterHelper::setpowerprofile(const QVariantMap &args)
|
||||||
return reply;
|
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 msg =
|
||||||
QDBusMessage::createMethodCall(kPowerProfilesService, kPowerProfilesPath, QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Set"));
|
QDBusMessage::createMethodCall(kPowerProfilesService, kPowerProfilesPath, QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Set"));
|
||||||
msg.setArguments({kPowerProfilesInterface, QStringLiteral("ActiveProfile"), QVariant::fromValue(QDBusVariant(profile))});
|
msg.setArguments({kPowerProfilesInterface, QStringLiteral("ActiveProfile"), QVariant::fromValue(QDBusVariant(profile))});
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
#include <KAboutData>
|
||||||
#include <KDBusService>
|
#include <KDBusService>
|
||||||
#include <KSignalHandler>
|
#include <KSignalHandler>
|
||||||
|
|
||||||
|
#include "alakarte-version.h"
|
||||||
#include "gamecenterdaemon.h"
|
#include "gamecenterdaemon.h"
|
||||||
|
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
|
@ -16,17 +18,34 @@ int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
QCoreApplication app(argc, argv);
|
QCoreApplication app(argc, argv);
|
||||||
app.setOrganizationDomain(QStringLiteral("kde.org"));
|
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;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center"));
|
aboutData.setupCommandLine(&parser);
|
||||||
parser.addHelpOption();
|
|
||||||
parser.process(app);
|
parser.process(app);
|
||||||
|
aboutData.processCommandLine(&parser);
|
||||||
|
|
||||||
GameCenterDaemon daemon;
|
GameCenterDaemon daemon;
|
||||||
if (!daemon.init()) {
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,19 @@
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QLoggingCategory>
|
||||||
#include <QtConcurrentRun>
|
#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)
|
ProcessScanner::ProcessScanner(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
|
|
@ -16,7 +27,11 @@ ProcessScanner::ProcessScanner(QObject *parent)
|
||||||
|
|
||||||
connect(&m_deadline, &QTimer::timeout, this, [this]() {
|
connect(&m_deadline, &QTimer::timeout, this, [this]() {
|
||||||
m_timer.stop();
|
m_timer.stop();
|
||||||
m_matcher = nullptr;
|
if (m_cancelToken) {
|
||||||
|
m_cancelToken->store(true);
|
||||||
|
}
|
||||||
|
m_cancelToken.reset();
|
||||||
|
m_matcher = {};
|
||||||
++m_generation;
|
++m_generation;
|
||||||
m_scanInFlight = false;
|
m_scanInFlight = false;
|
||||||
Q_EMIT timedOut();
|
Q_EMIT timedOut();
|
||||||
|
|
@ -42,7 +57,8 @@ void ProcessScanner::startScan()
|
||||||
m_scanInFlight = true;
|
m_scanInFlight = true;
|
||||||
m_scanGeneration = gen;
|
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);
|
auto *watcher = new QFutureWatcher<QList<Match>>(this);
|
||||||
|
|
||||||
connect(watcher, &QFutureWatcher<QList<Match>>::finished, this, [this, watcher, gen]() {
|
connect(watcher, &QFutureWatcher<QList<Match>>::finished, this, [this, watcher, gen]() {
|
||||||
|
|
@ -60,19 +76,130 @@ void ProcessScanner::startScan()
|
||||||
if (!results.isEmpty()) {
|
if (!results.isEmpty()) {
|
||||||
m_timer.stop();
|
m_timer.stop();
|
||||||
m_deadline.stop();
|
m_deadline.stop();
|
||||||
m_matcher = nullptr;
|
if (m_cancelToken) {
|
||||||
|
m_cancelToken->store(true);
|
||||||
|
}
|
||||||
|
m_cancelToken.reset();
|
||||||
|
m_matcher = {};
|
||||||
Q_EMIT found(results);
|
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) {
|
if (!matcher) {
|
||||||
return QList<Match>{};
|
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)
|
static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
|
||||||
{
|
{
|
||||||
QFile f(path);
|
QFile f(path);
|
||||||
|
|
@ -136,141 +263,75 @@ static QList<uint> listPids()
|
||||||
|
|
||||||
QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &key, const QString &value)
|
QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &key, const QString &value)
|
||||||
{
|
{
|
||||||
QList<Match> matches;
|
ScanOptions opts;
|
||||||
const QByteArray needle = (key + QLatin1Char('=') + value).toUtf8();
|
opts.envKeys = {key};
|
||||||
|
opts.envValue = value;
|
||||||
const QList<uint> pids = listPids();
|
return scan(opts);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ProcessScanner::Match> ProcessScanner::findByAnyEnvironment(const QStringList &keys, const QString &value)
|
QList<ProcessScanner::Match> ProcessScanner::findByAnyEnvironment(const QStringList &keys, const QString &value)
|
||||||
{
|
{
|
||||||
QList<Match> matches;
|
ScanOptions opts;
|
||||||
if (keys.isEmpty()) {
|
opts.envKeys = keys;
|
||||||
return matches;
|
opts.envValue = value;
|
||||||
}
|
return scan(opts);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ProcessScanner::Match> ProcessScanner::findByCmdline(const QString &substring)
|
QList<ProcessScanner::Match> ProcessScanner::findByCmdline(const QString &substring)
|
||||||
{
|
{
|
||||||
QList<Match> matches;
|
ScanOptions opts;
|
||||||
const QByteArray needle = substring.toUtf8();
|
opts.cmdlineSubstring = substring;
|
||||||
|
return scan(opts);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ProcessScanner::Match> ProcessScanner::findByExePath(const QString &dirPrefix)
|
QList<ProcessScanner::Match> ProcessScanner::findByExePath(const QString &dirPrefix)
|
||||||
{
|
{
|
||||||
QList<Match> matches;
|
ScanOptions opts;
|
||||||
|
opts.exePathPrefix = dirPrefix;
|
||||||
const QList<uint> pids = listPids();
|
return scan(opts);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProcessScanner::pollUntilFound(std::function<QList<Match>()> matcher, int intervalMs, int timeoutMs)
|
void ProcessScanner::pollUntilFound(Matcher matcher, int intervalMs, int timeoutMs)
|
||||||
{
|
{
|
||||||
cancel();
|
cancel();
|
||||||
|
m_cancelToken = std::make_shared<std::atomic_bool>(false);
|
||||||
m_matcher = std::move(matcher);
|
m_matcher = std::move(matcher);
|
||||||
|
|
||||||
|
if (!m_matcher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
m_deadline.start(timeoutMs);
|
m_deadline.start(timeoutMs);
|
||||||
m_timer.start(intervalMs);
|
m_timer.start(intervalMs);
|
||||||
|
|
||||||
startScan();
|
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()
|
void ProcessScanner::cancel()
|
||||||
{
|
{
|
||||||
m_timer.stop();
|
m_timer.stop();
|
||||||
m_deadline.stop();
|
m_deadline.stop();
|
||||||
m_matcher = nullptr;
|
if (m_cancelToken) {
|
||||||
|
m_cancelToken->store(true);
|
||||||
|
}
|
||||||
|
m_cancelToken.reset();
|
||||||
|
m_matcher = {};
|
||||||
++m_generation;
|
++m_generation;
|
||||||
m_scanInFlight = false;
|
m_scanInFlight = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,16 @@
|
||||||
|
|
||||||
#include <QFutureWatcher>
|
#include <QFutureWatcher>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QLoggingCategory>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QtGlobal>
|
#include <QtGlobal>
|
||||||
|
#include <atomic>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
Q_DECLARE_LOGGING_CATEGORY(lcProcessScanner)
|
||||||
|
|
||||||
class ProcessScanner : public QObject
|
class ProcessScanner : public QObject
|
||||||
{
|
{
|
||||||
|
|
@ -18,12 +23,28 @@ class ProcessScanner : public QObject
|
||||||
public:
|
public:
|
||||||
explicit ProcessScanner(QObject *parent = nullptr);
|
explicit ProcessScanner(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
struct ScanOptions {
|
||||||
|
QStringList envKeys;
|
||||||
|
QString envValue;
|
||||||
|
QString cmdlineSubstring;
|
||||||
|
QString exePathPrefix;
|
||||||
|
bool preferEnvironmentMatches = false;
|
||||||
|
};
|
||||||
|
|
||||||
struct Match {
|
struct Match {
|
||||||
uint pid = 0;
|
uint pid = 0;
|
||||||
QString exe;
|
QString exe;
|
||||||
QString cmdline;
|
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
|
// Find PIDs whose /proc/<pid>/environ contains key=value
|
||||||
static QList<Match> findByEnvironment(const QString &key, const QString &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.
|
// Async poll: calls matcher repeatedly until it returns non-empty or timeout.
|
||||||
// Emits found() with matching PIDs, or timedOut() on failure.
|
// 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 pollUntilFound(std::function<QList<Match>()> matcher, int intervalMs = 500, int timeoutMs = 15000);
|
||||||
|
|
||||||
void cancel();
|
void cancel();
|
||||||
|
|
@ -51,7 +73,8 @@ private:
|
||||||
|
|
||||||
QTimer m_timer;
|
QTimer m_timer;
|
||||||
QTimer m_deadline;
|
QTimer m_deadline;
|
||||||
std::function<QList<Match>()> m_matcher;
|
Matcher m_matcher;
|
||||||
|
std::shared_ptr<std::atomic_bool> m_cancelToken;
|
||||||
quint64 m_generation = 0;
|
quint64 m_generation = 0;
|
||||||
bool m_scanInFlight = false;
|
bool m_scanInFlight = false;
|
||||||
quint64 m_scanGeneration = 0;
|
quint64 m_scanGeneration = 0;
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -4,13 +4,14 @@
|
||||||
#include "gamelauncher.h"
|
#include "gamelauncher.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
|
||||||
|
#include "gamecenter1interface.h"
|
||||||
|
#include "runner1interface.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDBusArgument>
|
#include <QDBusArgument>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
#include <QDBusConnectionInterface>
|
#include <QDBusConnectionInterface>
|
||||||
#include <QDBusError>
|
#include <QDBusError>
|
||||||
#include <QDBusInterface>
|
|
||||||
#include <QDBusReply>
|
|
||||||
#include <QDBusVariant>
|
#include <QDBusVariant>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
|
|
@ -20,12 +21,11 @@
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
|
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 kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
|
||||||
|
|
||||||
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||||
static const QString kRunnerPath = 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)
|
static bool pingDaemon(QDBusConnection bus)
|
||||||
{
|
{
|
||||||
|
|
@ -33,14 +33,15 @@ static bool pingDaemon(QDBusConnection bus)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface iface(kGameCenterService, kGameCenterPath, kGameCenterInterface, bus);
|
org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, bus);
|
||||||
if (!iface.isValid()) {
|
if (!iface.isValid()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
iface.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
QDBusPendingReply<QString> reply = iface.Ping();
|
||||||
return reply.isValid() && reply.value() == QLatin1String("ok");
|
reply.waitForFinished();
|
||||||
|
return !reply.isError() && reply.value() == QLatin1String("ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void disconnectDaemonSignals(QDBusConnection bus, GameLauncher *launcher)
|
static void disconnectDaemonSignals(QDBusConnection bus, GameLauncher *launcher)
|
||||||
|
|
@ -131,14 +132,15 @@ static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &ou
|
||||||
bus.interface()->startService(kRunnerService);
|
bus.interface()->startService(kRunnerService);
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, bus);
|
||||||
if (!iface.isValid()) {
|
if (!iface.isValid()) {
|
||||||
if (bus.interface()->startService(kRunnerService).isValid()) {
|
if (bus.interface()->startService(kRunnerService).isValid()) {
|
||||||
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
org::kde::ALaKarte::Runner1 retryIface(kRunnerService, kRunnerPath, bus);
|
||||||
if (retryIface.isValid()) {
|
if (retryIface.isValid()) {
|
||||||
retryIface.setTimeout(2000);
|
retryIface.setTimeout(2000);
|
||||||
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
QDBusPendingReply<QVariantMap> retryReply = retryIface.ResolveLaunch(spec);
|
||||||
if (retryReply.isValid()) {
|
retryReply.waitForFinished();
|
||||||
|
if (!retryReply.isError()) {
|
||||||
out = unwrapVariantMap(retryReply.value());
|
out = unwrapVariantMap(retryReply.value());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -148,17 +150,19 @@ static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &ou
|
||||||
}
|
}
|
||||||
|
|
||||||
iface.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
|
QDBusPendingReply<QVariantMap> reply = iface.ResolveLaunch(spec);
|
||||||
if (!reply.isValid()) {
|
reply.waitForFinished();
|
||||||
|
if (reply.isError()) {
|
||||||
if (reply.error().type() == QDBusError::ServiceUnknown) {
|
if (reply.error().type() == QDBusError::ServiceUnknown) {
|
||||||
bus.interface()->startService(kRunnerService);
|
bus.interface()->startService(kRunnerService);
|
||||||
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
org::kde::ALaKarte::Runner1 retryIface(kRunnerService, kRunnerPath, bus);
|
||||||
if (!retryIface.isValid()) {
|
if (!retryIface.isValid()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
retryIface.setTimeout(2000);
|
retryIface.setTimeout(2000);
|
||||||
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
QDBusPendingReply<QVariantMap> retryReply = retryIface.ResolveLaunch(spec);
|
||||||
if (!retryReply.isValid()) {
|
retryReply.waitForFinished();
|
||||||
|
if (retryReply.isError()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
out = unwrapVariantMap(retryReply.value());
|
out = unwrapVariantMap(retryReply.value());
|
||||||
|
|
@ -270,7 +274,6 @@ static QStringList steamCandidateRoots()
|
||||||
GameLauncher::GameLauncher(QObject *parent)
|
GameLauncher::GameLauncher(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_daemonAvailable(false)
|
, m_daemonAvailable(false)
|
||||||
, m_usingSystemBus(false)
|
|
||||||
{
|
{
|
||||||
if (auto *app = qobject_cast<App *>(parent)) {
|
if (auto *app = qobject_cast<App *>(parent)) {
|
||||||
connect(app->gameModel(), &GameModel::countChanged, this, [this]() {
|
connect(app->gameModel(), &GameModel::countChanged, this, [this]() {
|
||||||
|
|
@ -557,10 +560,7 @@ void GameLauncher::launchGame(Game *game)
|
||||||
|
|
||||||
// Always try daemon first — for all launch types
|
// Always try daemon first — for all launch types
|
||||||
{
|
{
|
||||||
QDBusInterface iface(kGameCenterService,
|
org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus());
|
||||||
kGameCenterPath,
|
|
||||||
kGameCenterInterface,
|
|
||||||
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
|
|
||||||
QVariantMap launchSpec = {
|
QVariantMap launchSpec = {
|
||||||
{QStringLiteral("command"), launchCommand},
|
{QStringLiteral("command"), launchCommand},
|
||||||
{QStringLiteral("gameId"), game->id()},
|
{QStringLiteral("gameId"), game->id()},
|
||||||
|
|
@ -599,8 +599,10 @@ void GameLauncher::launchGame(Game *game)
|
||||||
launchSpec.insert(QStringLiteral("workingDirectory"), workingDirectory);
|
launchSpec.insert(QStringLiteral("workingDirectory"), workingDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
const QDBusReply<QString> reply = iface.call(QStringLiteral("Launch"), launchSpec);
|
iface.setTimeout(5000);
|
||||||
if (reply.isValid() && !reply.value().isEmpty()) {
|
QDBusPendingReply<QString> reply = iface.Launch(launchSpec);
|
||||||
|
reply.waitForFinished();
|
||||||
|
if (!reply.isError() && !reply.value().isEmpty()) {
|
||||||
m_daemonGameToSession.insert(game->id(), reply.value());
|
m_daemonGameToSession.insert(game->id(), reply.value());
|
||||||
m_daemonSessionToGame.insert(reply.value(), game->id());
|
m_daemonSessionToGame.insert(reply.value(), game->id());
|
||||||
game->setRunning(true);
|
game->setRunning(true);
|
||||||
|
|
@ -614,7 +616,7 @@ void GameLauncher::launchGame(Game *game)
|
||||||
return;
|
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
|
// No fallback for non-URL commands — emit error
|
||||||
if (!launchError.isEmpty()) {
|
if (!launchError.isEmpty()) {
|
||||||
|
|
@ -631,11 +633,8 @@ void GameLauncher::stopGame(Game *game)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface iface(kGameCenterService,
|
org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus());
|
||||||
kGameCenterPath,
|
iface.StopByGameId(game->id());
|
||||||
kGameCenterInterface,
|
|
||||||
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
|
|
||||||
iface.call(QStringLiteral("StopByGameId"), game->id());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GameLauncher::isGameRunning(Game *game) const
|
bool GameLauncher::isGameRunning(Game *game) const
|
||||||
|
|
@ -765,13 +764,12 @@ void GameLauncher::onDaemonLaunchFailed(const QVariantMap &error)
|
||||||
|
|
||||||
void GameLauncher::syncDaemonSessions()
|
void GameLauncher::syncDaemonSessions()
|
||||||
{
|
{
|
||||||
QDBusInterface iface(kGameCenterService,
|
org::kde::GameCenter1 iface(kGameCenterService, kGameCenterPath, QDBusConnection::sessionBus());
|
||||||
kGameCenterPath,
|
iface.setTimeout(2000);
|
||||||
kGameCenterInterface,
|
|
||||||
m_usingSystemBus ? QDBusConnection::systemBus() : QDBusConnection::sessionBus());
|
|
||||||
|
|
||||||
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListSessions"));
|
QDBusPendingReply<QVariantList> reply = iface.ListSessions();
|
||||||
if (!reply.isValid()) {
|
reply.waitForFinished();
|
||||||
|
if (reply.isError()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -805,28 +803,14 @@ void GameLauncher::applyRunningStateToLibrary()
|
||||||
|
|
||||||
void GameLauncher::checkDaemonAvailability()
|
void GameLauncher::checkDaemonAvailability()
|
||||||
{
|
{
|
||||||
const bool systemAvailable = pingDaemon(QDBusConnection::systemBus());
|
const bool available = pingDaemon(QDBusConnection::sessionBus());
|
||||||
const bool sessionAvailable = systemAvailable ? false : pingDaemon(QDBusConnection::sessionBus());
|
|
||||||
const bool available = systemAvailable || sessionAvailable;
|
|
||||||
const bool useSystemBus = systemAvailable;
|
|
||||||
|
|
||||||
disconnectDaemonSignals(QDBusConnection::systemBus(), this);
|
|
||||||
disconnectDaemonSignals(QDBusConnection::sessionBus(), this);
|
disconnectDaemonSignals(QDBusConnection::sessionBus(), this);
|
||||||
|
|
||||||
if (available) {
|
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) {
|
if (available != m_daemonAvailable) {
|
||||||
m_daemonAvailable = available;
|
m_daemonAvailable = available;
|
||||||
Q_EMIT daemonAvailableChanged();
|
Q_EMIT daemonAvailableChanged();
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ private:
|
||||||
QHash<QString, QString> m_daemonGameToSession;
|
QHash<QString, QString> m_daemonGameToSession;
|
||||||
QHash<QString, QString> m_daemonSessionToGame;
|
QHash<QString, QString> m_daemonSessionToGame;
|
||||||
bool m_daemonAvailable = false;
|
bool m_daemonAvailable = false;
|
||||||
bool m_usingSystemBus = false;
|
|
||||||
|
|
||||||
void checkDaemonAvailability();
|
void checkDaemonAvailability();
|
||||||
void syncDaemonSessions();
|
void syncDaemonSessions();
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,35 @@ add_executable(alakarte_inputd
|
||||||
inputdaemon.h
|
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
|
target_link_libraries(alakarte_inputd PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::DBus
|
Qt6::DBus
|
||||||
SDL3::SDL3
|
SDL3::SDL3
|
||||||
|
KF6::CoreAddons
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(alakarte_inputd PROPERTIES
|
set_target_properties(alakarte_inputd PROPERTIES
|
||||||
|
|
|
||||||
76
src/input/dbus/org.kde.ALaKarte.Input1.xml
Normal file
76
src/input/dbus/org.kde.ALaKarte.Input1.xml
Normal 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>
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
#include "inputdaemon.h"
|
#include "inputdaemon.h"
|
||||||
|
|
||||||
|
#include "input1adaptor.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
#include <QDBusConnectionInterface>
|
#include <QDBusConnectionInterface>
|
||||||
|
|
@ -150,7 +152,9 @@ bool InputDaemon::init()
|
||||||
return false;
|
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();
|
qWarning() << "InputDaemon: failed to register object" << kInputPath << bus.lastError().message();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -199,10 +203,10 @@ QString InputDaemon::Ping() const
|
||||||
return QStringLiteral("ok");
|
return QStringLiteral("ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputDaemon::Version(uint &major, uint &minor) const
|
uint InputDaemon::Version(uint &minor) const
|
||||||
{
|
{
|
||||||
major = 1;
|
|
||||||
minor = 0;
|
minor = 0;
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap InputDaemon::GetCapabilities() const
|
QVariantMap InputDaemon::GetCapabilities() const
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ public:
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
QString Ping() const;
|
QString Ping() const;
|
||||||
void Version(uint &major, uint &minor) const;
|
uint Version(uint &minor) const;
|
||||||
QVariantMap GetCapabilities() const;
|
QVariantMap GetCapabilities() const;
|
||||||
|
|
||||||
QVariantList ListControllers() const;
|
QVariantList ListControllers() const;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,35 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include <QCommandLineParser>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
#include <KAboutData>
|
||||||
|
|
||||||
|
#include "alakarte-version.h"
|
||||||
|
|
||||||
#include "inputdaemon.h"
|
#include "inputdaemon.h"
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
QCoreApplication app(argc, argv);
|
QCoreApplication app(argc, argv);
|
||||||
|
app.setOrganizationDomain(QStringLiteral("kde.org"));
|
||||||
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
|
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;
|
InputDaemon daemon;
|
||||||
if (!daemon.init()) {
|
if (!daemon.init()) {
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@
|
||||||
|
|
||||||
#include "inputserviceclient.h"
|
#include "inputserviceclient.h"
|
||||||
|
|
||||||
|
#include "input1interface.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
#include <QDBusConnectionInterface>
|
#include <QDBusConnectionInterface>
|
||||||
#include <QDBusError>
|
#include <QDBusError>
|
||||||
#include <QDBusInterface>
|
#include <QDBusPendingReply>
|
||||||
#include <QDBusReply>
|
#include <QDBusReply>
|
||||||
#include <QDBusServiceWatcher>
|
#include <QDBusServiceWatcher>
|
||||||
#include <QDBusVariant>
|
#include <QDBusVariant>
|
||||||
|
|
@ -243,17 +245,21 @@ void InputServiceClient::refreshControllers()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
|
||||||
i.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
const QDBusReply<bool> rescanReply = i.call(QStringLiteral("Rescan"));
|
{
|
||||||
if (!rescanReply.isValid()) {
|
QDBusPendingReply<bool> rescanReply = iface.Rescan();
|
||||||
setLastError(rescanReply.error().message());
|
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());
|
setLastError(reply.error().message());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -274,11 +280,13 @@ void InputServiceClient::refreshProfiles()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
|
||||||
i.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
const QDBusReply<QVariantList> reply = i.call(QStringLiteral("ListProfiles"));
|
|
||||||
|
|
||||||
if (!reply.isValid()) {
|
QDBusPendingReply<QVariantList> reply = iface.ListProfiles();
|
||||||
|
reply.waitForFinished();
|
||||||
|
|
||||||
|
if (reply.isError()) {
|
||||||
setLastError(reply.error().message());
|
setLastError(reply.error().message());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -300,11 +308,13 @@ bool InputServiceClient::setActiveProfile(const QString &controllerId, const QSt
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
|
||||||
i.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
const QDBusReply<bool> reply = i.call(QStringLiteral("SetActiveProfile"), controllerId, profileId);
|
|
||||||
|
|
||||||
if (!reply.isValid()) {
|
QDBusPendingReply<bool> reply = iface.SetActiveProfile(controllerId, profileId);
|
||||||
|
reply.waitForFinished();
|
||||||
|
|
||||||
|
if (reply.isError()) {
|
||||||
setLastError(reply.error().message());
|
setLastError(reply.error().message());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -319,11 +329,13 @@ QString InputServiceClient::createProfile(const QString &name)
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
|
||||||
i.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
const QDBusReply<QString> reply = i.call(QStringLiteral("CreateProfile"), name);
|
|
||||||
|
|
||||||
if (!reply.isValid()) {
|
QDBusPendingReply<QString> reply = iface.CreateProfile(name);
|
||||||
|
reply.waitForFinished();
|
||||||
|
|
||||||
|
if (reply.isError()) {
|
||||||
setLastError(reply.error().message());
|
setLastError(reply.error().message());
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -339,11 +351,13 @@ bool InputServiceClient::deleteProfile(const QString &profileId)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Input1 iface(kInputService, kInputPath, QDBusConnection::sessionBus());
|
||||||
i.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
const QDBusReply<bool> reply = i.call(QStringLiteral("DeleteProfile"), profileId);
|
|
||||||
|
|
||||||
if (!reply.isValid()) {
|
QDBusPendingReply<bool> reply = iface.DeleteProfile(profileId);
|
||||||
|
reply.waitForFinished();
|
||||||
|
|
||||||
|
if (reply.isError()) {
|
||||||
setLastError(reply.error().message());
|
setLastError(reply.error().message());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
K_PLUGIN_CLASS_WITH_JSON(AlakarteRunner, "plasma-runner-alakarte.json")
|
K_PLUGIN_CLASS_WITH_JSON(AlakarteRunner, "plasma-runner-alakarte.json")
|
||||||
|
|
||||||
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
|
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 kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
|
||||||
|
|
||||||
static bool launchViaDaemon(QDBusConnection bus, const QVariantMap &launchSpec)
|
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")},
|
{QStringLiteral("origin"), QStringLiteral("krunner")},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (launchViaDaemon(QDBusConnection::systemBus(), launchSpec) || launchViaDaemon(QDBusConnection::sessionBus(), launchSpec)) {
|
if (launchViaDaemon(QDBusConnection::sessionBus(), launchSpec)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ int main(int argc, char *argv[])
|
||||||
QGuiApplication app(argc, argv);
|
QGuiApplication app(argc, argv);
|
||||||
QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.alakarte"));
|
QGuiApplication::setDesktopFileName(QStringLiteral("org.kde.alakarte"));
|
||||||
|
|
||||||
|
app.setOrganizationDomain(QStringLiteral("kde.org"));
|
||||||
|
app.setApplicationName(QStringLiteral("alakarte"));
|
||||||
|
|
||||||
KLocalizedString::setApplicationDomain("alakarte");
|
KLocalizedString::setApplicationDomain("alakarte");
|
||||||
|
|
||||||
KAboutData aboutData(QStringLiteral("alakarte"),
|
KAboutData aboutData(QStringLiteral("alakarte"),
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,35 @@ add_executable(alakarte_runnerd
|
||||||
runnermanagerdaemon.h
|
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
|
target_link_libraries(alakarte_runnerd PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::DBus
|
Qt6::DBus
|
||||||
Qt6::Network
|
Qt6::Network
|
||||||
|
KF6::CoreAddons
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(alakarte_runnerd PROPERTIES
|
set_target_properties(alakarte_runnerd PROPERTIES
|
||||||
|
|
|
||||||
113
src/runner/dbus/org.kde.ALaKarte.Runner1.xml
Normal file
113
src/runner/dbus/org.kde.ALaKarte.Runner1.xml
Normal 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>
|
||||||
|
|
@ -1,16 +1,36 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include <QCommandLineParser>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
|
|
||||||
|
#include <KAboutData>
|
||||||
|
|
||||||
|
#include "alakarte-version.h"
|
||||||
|
|
||||||
#include "runnermanagerdaemon.h"
|
#include "runnermanagerdaemon.h"
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
QCoreApplication app(argc, argv);
|
QCoreApplication app(argc, argv);
|
||||||
|
app.setOrganizationDomain(QStringLiteral("kde.org"));
|
||||||
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
|
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;
|
RunnerManagerDaemon daemon;
|
||||||
if (!daemon.init()) {
|
if (!daemon.init()) {
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
#include "runnermanagerdaemon.h"
|
#include "runnermanagerdaemon.h"
|
||||||
|
|
||||||
|
#include "runner1adaptor.h"
|
||||||
|
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <QDBusArgument>
|
#include <QDBusArgument>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
|
|
@ -109,7 +111,9 @@ bool RunnerManagerDaemon::init()
|
||||||
return false;
|
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();
|
qWarning() << "RunnerManagerDaemon: failed to register object" << kRunnerPath << bus.lastError().message();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -122,10 +126,10 @@ QString RunnerManagerDaemon::Ping() const
|
||||||
return QStringLiteral("ok");
|
return QStringLiteral("ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
void RunnerManagerDaemon::Version(uint &major, uint &minor) const
|
uint RunnerManagerDaemon::Version(uint &minor) const
|
||||||
{
|
{
|
||||||
major = 1;
|
|
||||||
minor = 0;
|
minor = 0;
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap RunnerManagerDaemon::RunnerInfo::toVariantMap() const
|
QVariantMap RunnerManagerDaemon::RunnerInfo::toVariantMap() const
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ public:
|
||||||
|
|
||||||
public Q_SLOTS:
|
public Q_SLOTS:
|
||||||
QString Ping() const;
|
QString Ping() const;
|
||||||
void Version(uint &major, uint &minor) const;
|
uint Version(uint &minor) const;
|
||||||
|
|
||||||
QVariantList ListRunners();
|
QVariantList ListRunners();
|
||||||
bool Rescan();
|
bool Rescan();
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
|
|
||||||
#include "runnermanagerclient.h"
|
#include "runnermanagerclient.h"
|
||||||
|
|
||||||
|
#include "runner1interface.h"
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <QDBusArgument>
|
#include <QDBusArgument>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
#include <QDBusConnectionInterface>
|
#include <QDBusConnectionInterface>
|
||||||
#include <QDBusInterface>
|
|
||||||
#include <QDBusMetaType>
|
#include <QDBusMetaType>
|
||||||
#include <QDBusPendingCall>
|
#include <QDBusPendingCall>
|
||||||
#include <QDBusPendingCallWatcher>
|
#include <QDBusPendingCallWatcher>
|
||||||
|
|
@ -338,8 +339,8 @@ void RunnerManagerClient::installRunnerFromUrl(const QString &url, const QString
|
||||||
auto attempts = QSharedPointer<int>::create(0);
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
*callPtr = [this, spec, attempts, callPtr]() {
|
*callPtr = [this, spec, attempts, callPtr]() {
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("InstallRunnerFromUrl"), spec);
|
QDBusPendingCall call = iface.InstallRunnerFromUrl(spec);
|
||||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, spec, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, spec, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
QDBusPendingReply<QVariantMap> reply = *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 attempts = QSharedPointer<int>::create(0);
|
||||||
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
*callPtr = [this, spec, id, attempts, callPtr]() {
|
*callPtr = [this, spec, id, attempts, callPtr]() {
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("DeletePrefix"), spec);
|
QDBusPendingCall call = iface.DeletePrefix(spec);
|
||||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
QDBusPendingReply<QVariantMap> reply = *w;
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
|
@ -438,8 +439,8 @@ void RunnerManagerClient::cancelCurrentInstall()
|
||||||
|
|
||||||
setStatus(QStringLiteral("Cancelling..."));
|
setStatus(QStringLiteral("Cancelling..."));
|
||||||
|
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
iface.asyncCall(QStringLiteral("CancelInstall"), m_installId);
|
iface.CancelInstall(m_installId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RunnerManagerClient::onInstallStarted(const QString &installId, const QVariantMap &)
|
void RunnerManagerClient::onInstallStarted(const QString &installId, const QVariantMap &)
|
||||||
|
|
@ -489,8 +490,8 @@ void RunnerManagerClient::refreshRunners()
|
||||||
{
|
{
|
||||||
ensureRunnerDaemon();
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListRunners"));
|
QDBusPendingCall call = iface.ListRunners();
|
||||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
||||||
QDBusPendingReply<QVariantList> reply = *w;
|
QDBusPendingReply<QVariantList> reply = *w;
|
||||||
|
|
@ -544,8 +545,8 @@ void RunnerManagerClient::refreshGameProfiles()
|
||||||
{
|
{
|
||||||
ensureRunnerDaemon();
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListGameProfiles"));
|
QDBusPendingCall call = iface.ListGameProfiles();
|
||||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
||||||
QDBusPendingReply<QVariantList> reply = *w;
|
QDBusPendingReply<QVariantList> reply = *w;
|
||||||
|
|
@ -599,8 +600,8 @@ void RunnerManagerClient::requestGameProfile(const QString &gameId)
|
||||||
auto attempts = QSharedPointer<int>::create(0);
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
*callPtr = [this, id, attempts, callPtr]() {
|
*callPtr = [this, id, attempts, callPtr]() {
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("GetGameProfile"), id);
|
QDBusPendingCall call = iface.GetGameProfile(id);
|
||||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
QDBusPendingReply<QVariantMap> reply = *w;
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
|
@ -641,8 +642,8 @@ void RunnerManagerClient::setGameProfile(const QVariantMap &spec)
|
||||||
auto attempts = QSharedPointer<int>::create(0);
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
*callPtr = [this, spec, id, attempts, callPtr]() {
|
*callPtr = [this, spec, id, attempts, callPtr]() {
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("SetGameProfile"), spec);
|
QDBusPendingCall call = iface.SetGameProfile(spec);
|
||||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
QDBusPendingReply<QVariantMap> reply = *w;
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
|
@ -683,8 +684,8 @@ void RunnerManagerClient::clearGameProfile(const QString &gameId)
|
||||||
auto attempts = QSharedPointer<int>::create(0);
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
*callPtr = [this, id, attempts, callPtr]() {
|
*callPtr = [this, id, attempts, callPtr]() {
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ClearGameProfile"), id);
|
QDBusPendingCall call = iface.ClearGameProfile(id);
|
||||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
QDBusPendingReply<QVariantMap> reply = *w;
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
|
@ -720,8 +721,8 @@ void RunnerManagerClient::uninstallRunner(const QString &runnerId)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("UninstallRunner"), id);
|
QDBusPendingCall call = iface.UninstallRunner(id);
|
||||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
||||||
QDBusPendingReply<QVariantMap> reply = *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 attempts = QSharedPointer<int>::create(0);
|
||||||
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
*callPtr = [this, spec, id, attempts, callPtr]() {
|
*callPtr = [this, spec, id, attempts, callPtr]() {
|
||||||
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
org::kde::ALaKarte::Runner1 iface(kRunnerService, kRunnerPath, QDBusConnection::sessionBus());
|
||||||
QDBusPendingCall call = iface.asyncCall(QStringLiteral("EnsurePrefix"), spec);
|
QDBusPendingCall call = iface.EnsurePrefix(spec);
|
||||||
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
QDBusPendingReply<QVariantMap> reply = *w;
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,31 @@ add_executable(alakarte_dbus_smoketest
|
||||||
dbus_smoketest.cpp
|
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
|
target_link_libraries(alakarte_dbus_smoketest PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::DBus
|
Qt6::DBus
|
||||||
|
|
@ -43,3 +68,27 @@ add_test(NAME alakarte_stop_launching_regression_test
|
||||||
set_tests_properties(alakarte_stop_launching_regression_test PROPERTIES
|
set_tests_properties(alakarte_stop_launching_regression_test PROPERTIES
|
||||||
TIMEOUT 60
|
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
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,17 @@
|
||||||
#include <QElapsedTimer>
|
#include <QElapsedTimer>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QProcessEnvironment>
|
#include <QProcessEnvironment>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QSignalSpy>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QStringList>
|
#include <QStringList>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
#include <QtTest>
|
#include <QtTest>
|
||||||
|
|
||||||
|
#include "gamecenter1interface.h"
|
||||||
|
#include "input1interface.h"
|
||||||
|
#include "runner1interface.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static QString takeArgValue(const QStringList &args, const QString &key)
|
static QString takeArgValue(const QStringList &args, const QString &key)
|
||||||
|
|
@ -79,8 +85,17 @@ private Q_SLOTS:
|
||||||
void cleanupTestCase();
|
void cleanupTestCase();
|
||||||
|
|
||||||
void pingGameCenter();
|
void pingGameCenter();
|
||||||
|
void gameCenterUniqueness();
|
||||||
|
void gameCenterPolicyAndCapabilities();
|
||||||
|
void gameCenterLaunchFailureContract();
|
||||||
void pingRunner();
|
void pingRunner();
|
||||||
|
void runnerVersionAndListRunners();
|
||||||
|
void runnerResolveLaunchMissingProgram();
|
||||||
|
void runnerResolveLaunchUnknownRunnerId();
|
||||||
|
void runnerResolveLaunchWineMissingPrefixOrGameId();
|
||||||
void pingInput();
|
void pingInput();
|
||||||
|
void inputVersionAndCapabilities();
|
||||||
|
void inputProfilesCrud();
|
||||||
void runnerResolveLaunchNative();
|
void runnerResolveLaunchNative();
|
||||||
void runnerGameProfiles();
|
void runnerGameProfiles();
|
||||||
|
|
||||||
|
|
@ -234,49 +249,298 @@ void DbusSmokeTest::cleanupTestCase()
|
||||||
|
|
||||||
void DbusSmokeTest::pingGameCenter()
|
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());
|
QVERIFY(iface.isValid());
|
||||||
iface.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
QDBusPendingReply<QString> reply = iface.Ping();
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
QCOMPARE(reply.value(), QStringLiteral("ok"));
|
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()
|
void DbusSmokeTest::pingRunner()
|
||||||
{
|
{
|
||||||
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
|
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
|
||||||
QStringLiteral("/org/kde/ALaKarte/Runner1"),
|
|
||||||
QStringLiteral("org.kde.ALaKarte.Runner1"),
|
|
||||||
m_bus);
|
|
||||||
QVERIFY(iface.isValid());
|
QVERIFY(iface.isValid());
|
||||||
iface.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
QDBusPendingReply<QString> reply = iface.Ping();
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
QCOMPARE(reply.value(), QStringLiteral("ok"));
|
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()
|
void DbusSmokeTest::pingInput()
|
||||||
{
|
{
|
||||||
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Input1"),
|
org::kde::ALaKarte::Input1 iface(QStringLiteral("org.kde.ALaKarte.Input1"), QStringLiteral("/org/kde/ALaKarte/Input1"), m_bus);
|
||||||
QStringLiteral("/org/kde/ALaKarte/Input1"),
|
|
||||||
QStringLiteral("org.kde.ALaKarte.Input1"),
|
|
||||||
m_bus);
|
|
||||||
QVERIFY(iface.isValid());
|
QVERIFY(iface.isValid());
|
||||||
iface.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
QDBusPendingReply<QString> reply = iface.Ping();
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
QCOMPARE(reply.value(), QStringLiteral("ok"));
|
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()
|
void DbusSmokeTest::runnerResolveLaunchNative()
|
||||||
{
|
{
|
||||||
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
|
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
|
||||||
QStringLiteral("/org/kde/ALaKarte/Runner1"),
|
|
||||||
QStringLiteral("org.kde.ALaKarte.Runner1"),
|
|
||||||
m_bus);
|
|
||||||
QVERIFY(iface.isValid());
|
QVERIFY(iface.isValid());
|
||||||
iface.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
|
|
@ -284,8 +548,9 @@ void DbusSmokeTest::runnerResolveLaunchNative()
|
||||||
spec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
|
spec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
|
||||||
spec.insert(QStringLiteral("args"), QStringList{});
|
spec.insert(QStringLiteral("args"), QStringList{});
|
||||||
|
|
||||||
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
|
QDBusPendingReply<QVariantMap> reply = iface.ResolveLaunch(spec);
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
|
|
||||||
const QVariantMap out = reply.value();
|
const QVariantMap out = reply.value();
|
||||||
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
||||||
|
|
@ -293,20 +558,79 @@ void DbusSmokeTest::runnerResolveLaunchNative()
|
||||||
QCOMPARE(out.value(QStringLiteral("finalArgs")).toStringList(), QStringList{});
|
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()
|
void DbusSmokeTest::runnerGameProfiles()
|
||||||
{
|
{
|
||||||
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
|
org::kde::ALaKarte::Runner1 iface(QStringLiteral("org.kde.ALaKarte.Runner1"), QStringLiteral("/org/kde/ALaKarte/Runner1"), m_bus);
|
||||||
QStringLiteral("/org/kde/ALaKarte/Runner1"),
|
|
||||||
QStringLiteral("org.kde.ALaKarte.Runner1"),
|
|
||||||
m_bus);
|
|
||||||
QVERIFY(iface.isValid());
|
QVERIFY(iface.isValid());
|
||||||
iface.setTimeout(2000);
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
const QString gameId = QStringLiteral("alakarte-test-game");
|
const QString gameId = QStringLiteral("alakarte-test-game");
|
||||||
|
|
||||||
{
|
{
|
||||||
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ClearGameProfile"), gameId);
|
QDBusPendingReply<QVariantMap> reply = iface.ClearGameProfile(gameId);
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
|
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -320,8 +644,9 @@ void DbusSmokeTest::runnerGameProfiles()
|
||||||
spec.insert(QStringLiteral("dllOverrides"),
|
spec.insert(QStringLiteral("dllOverrides"),
|
||||||
QVariantMap{{QStringLiteral("d3d11"), QStringLiteral("native,builtin")}, {QStringLiteral("dxgi"), QStringLiteral("native,builtin")}});
|
QVariantMap{{QStringLiteral("d3d11"), QStringLiteral("native,builtin")}, {QStringLiteral("dxgi"), QStringLiteral("native,builtin")}});
|
||||||
|
|
||||||
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("SetGameProfile"), spec);
|
QDBusPendingReply<QVariantMap> reply = iface.SetGameProfile(spec);
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
const QVariantMap out = reply.value();
|
const QVariantMap out = reply.value();
|
||||||
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
||||||
|
|
||||||
|
|
@ -338,8 +663,9 @@ void DbusSmokeTest::runnerGameProfiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("GetGameProfile"), gameId);
|
QDBusPendingReply<QVariantMap> reply = iface.GetGameProfile(gameId);
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
const QVariantMap out = reply.value();
|
const QVariantMap out = reply.value();
|
||||||
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
||||||
const QVariantMap profile = unwrapVariantMap(out.value(QStringLiteral("profile")));
|
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("args"), QStringList{QStringLiteral("--base-arg")});
|
||||||
spec.insert(QStringLiteral("extraArgs"), QStringList{QStringLiteral("--spec-arg")});
|
spec.insert(QStringLiteral("extraArgs"), QStringList{QStringLiteral("--spec-arg")});
|
||||||
|
|
||||||
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
|
QDBusPendingReply<QVariantMap> reply = iface.ResolveLaunch(spec);
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
const QVariantMap out = reply.value();
|
const QVariantMap out = reply.value();
|
||||||
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
||||||
QCOMPARE(out.value(QStringLiteral("finalProgram")).toString(), QStringLiteral("/bin/true"));
|
QCOMPARE(out.value(QStringLiteral("finalProgram")).toString(), QStringLiteral("/bin/true"));
|
||||||
|
|
@ -379,8 +706,9 @@ void DbusSmokeTest::runnerGameProfiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListGameProfiles"));
|
QDBusPendingReply<QVariantList> reply = iface.ListGameProfiles();
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
const QVariantList list = reply.value();
|
const QVariantList list = reply.value();
|
||||||
bool found = false;
|
bool found = false;
|
||||||
for (const QVariant &v : list) {
|
for (const QVariant &v : list) {
|
||||||
|
|
@ -394,8 +722,9 @@ void DbusSmokeTest::runnerGameProfiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ClearGameProfile"), gameId);
|
QDBusPendingReply<QVariantMap> reply = iface.ClearGameProfile(gameId);
|
||||||
QVERIFY(reply.isValid());
|
reply.waitForFinished();
|
||||||
|
QVERIFY2(!reply.isError(), qPrintable(reply.error().message()));
|
||||||
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
|
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
202
tests/processscanner_test.cpp
Normal file
202
tests/processscanner_test.cpp
Normal 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"
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
#include <QDBusArgument>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
#include <QDBusConnectionInterface>
|
#include <QDBusConnectionInterface>
|
||||||
#include <QDBusInterface>
|
#include <QDBusInterface>
|
||||||
|
|
@ -53,6 +54,27 @@ static bool waitForService(QDBusConnection &bus, const QString &service, int tim
|
||||||
return false;
|
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)
|
static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
|
||||||
{
|
{
|
||||||
QFile f(path);
|
QFile f(path);
|
||||||
|
|
@ -226,6 +248,8 @@ private Q_SLOTS:
|
||||||
void stopDirectLaunch();
|
void stopDirectLaunch();
|
||||||
void stopByGameIdDirectLaunch();
|
void stopByGameIdDirectLaunch();
|
||||||
|
|
||||||
|
void recoverSessionsAfterDaemonRestart();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_gamecenterPath;
|
QString m_gamecenterPath;
|
||||||
QProcess m_gamecenter;
|
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()
|
void StopLaunchingRegressionTest::stopWhileLaunchingSteam()
|
||||||
{
|
{
|
||||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
|
@ -358,7 +454,7 @@ void StopLaunchingRegressionTest::stopWhileLaunchingSteam()
|
||||||
QSKIP("session bus not available");
|
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");
|
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
|
||||||
|
|
||||||
const QString appId = QStringLiteral("987654");
|
const QString appId = QStringLiteral("987654");
|
||||||
|
|
@ -396,7 +492,7 @@ void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingSteam()
|
||||||
QSKIP("session bus not available");
|
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");
|
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
|
||||||
|
|
||||||
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
||||||
|
|
@ -438,7 +534,7 @@ void StopLaunchingRegressionTest::stopWhileLaunchingLutris()
|
||||||
QSKIP("session bus not available");
|
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");
|
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
|
||||||
|
|
||||||
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
||||||
|
|
@ -476,7 +572,7 @@ void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingLutris()
|
||||||
QSKIP("session bus not available");
|
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");
|
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
|
||||||
|
|
||||||
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
||||||
|
|
@ -514,7 +610,7 @@ void StopLaunchingRegressionTest::stopDirectLaunch()
|
||||||
QSKIP("session bus not available");
|
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");
|
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
|
||||||
|
|
||||||
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
||||||
|
|
@ -561,7 +657,7 @@ void StopLaunchingRegressionTest::stopByGameIdDirectLaunch()
|
||||||
QSKIP("session bus not available");
|
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");
|
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
|
||||||
|
|
||||||
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue