mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
Compare commits
8 commits
c7956eed8b
...
b0d09be497
| Author | SHA1 | Date | |
|---|---|---|---|
| b0d09be497 | |||
| e9071c2b86 | |||
| 9dbff6fdb8 | |||
| 1e5a86fda8 | |||
| 7724205354 | |||
| 27a385e3c3 | |||
| 8e77a35f7f | |||
| fbd9311384 |
34 changed files with 4690 additions and 183 deletions
|
|
@ -26,6 +26,7 @@ include(ECMPoQmTools)
|
||||||
include(ECMDeprecationSettings)
|
include(ECMDeprecationSettings)
|
||||||
include(ECMQmlModule)
|
include(ECMQmlModule)
|
||||||
include(ECMAddAppIcon)
|
include(ECMAddAppIcon)
|
||||||
|
include(CTest)
|
||||||
include(FeatureSummary)
|
include(FeatureSummary)
|
||||||
|
|
||||||
ecm_setup_version(${PROJECT_VERSION}
|
ecm_setup_version(${PROJECT_VERSION}
|
||||||
|
|
@ -104,6 +105,8 @@ find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS
|
||||||
WindowSystem
|
WindowSystem
|
||||||
)
|
)
|
||||||
|
|
||||||
|
find_package(KF6Auth ${KF_MIN_VERSION} QUIET)
|
||||||
|
|
||||||
find_package(KF6KirigamiAddons 1.0.0 REQUIRED)
|
find_package(KF6KirigamiAddons 1.0.0 REQUIRED)
|
||||||
|
|
||||||
qt_policy(SET QTP0001 NEW)
|
qt_policy(SET QTP0001 NEW)
|
||||||
|
|
@ -112,6 +115,10 @@ add_subdirectory(src)
|
||||||
add_subdirectory(src/krunner)
|
add_subdirectory(src/krunner)
|
||||||
add_subdirectory(icons)
|
add_subdirectory(icons)
|
||||||
|
|
||||||
|
if (BUILD_TESTING)
|
||||||
|
add_subdirectory(tests)
|
||||||
|
endif()
|
||||||
|
|
||||||
ki18n_install(po)
|
ki18n_install(po)
|
||||||
|
|
||||||
install(FILES org.kde.alakarte.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
install(FILES org.kde.alakarte.desktop DESTINATION ${KDE_INSTALL_APPDIR})
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ add_executable(alakarte
|
||||||
game.cpp
|
game.cpp
|
||||||
gamepadmanager.cpp
|
gamepadmanager.cpp
|
||||||
inputmanager.cpp
|
inputmanager.cpp
|
||||||
|
inputserviceclient.cpp
|
||||||
mediamanager.cpp
|
mediamanager.cpp
|
||||||
runnermanagerclient.cpp
|
runnermanagerclient.cpp
|
||||||
screenshotmodel.cpp
|
screenshotmodel.cpp
|
||||||
|
|
@ -122,6 +123,7 @@ ecm_add_qml_module(alakarte URI org.kde.alakarte
|
||||||
game.h
|
game.h
|
||||||
gamepadmanager.h
|
gamepadmanager.h
|
||||||
inputmanager.h
|
inputmanager.h
|
||||||
|
inputserviceclient.h
|
||||||
platformimporter.h
|
platformimporter.h
|
||||||
steamimporter.h
|
steamimporter.h
|
||||||
lutrisimporter.h
|
lutrisimporter.h
|
||||||
|
|
@ -142,3 +144,5 @@ install(TARGETS alakarte ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
add_subdirectory(gamecenter)
|
add_subdirectory(gamecenter)
|
||||||
|
|
||||||
add_subdirectory(runner)
|
add_subdirectory(runner)
|
||||||
|
|
||||||
|
add_subdirectory(input)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ App::App(QObject *parent)
|
||||||
, m_gameModel(new GameModel(this))
|
, m_gameModel(new GameModel(this))
|
||||||
, m_launcher(new GameLauncher(this))
|
, m_launcher(new GameLauncher(this))
|
||||||
, m_runnerManager(new RunnerManagerClient(this))
|
, m_runnerManager(new RunnerManagerClient(this))
|
||||||
|
, m_inputService(new InputServiceClient(this))
|
||||||
, m_steamGridDB(new SteamGridDB(this))
|
, m_steamGridDB(new SteamGridDB(this))
|
||||||
, m_mediaManager(new MediaManager(this))
|
, m_mediaManager(new MediaManager(this))
|
||||||
, m_config(new Config(this))
|
, m_config(new Config(this))
|
||||||
|
|
@ -175,6 +176,11 @@ RunnerManagerClient *App::runnerManager() const
|
||||||
return m_runnerManager;
|
return m_runnerManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InputServiceClient *App::inputService() const
|
||||||
|
{
|
||||||
|
return m_inputService;
|
||||||
|
}
|
||||||
|
|
||||||
SteamGridDB *App::steamGridDB() const
|
SteamGridDB *App::steamGridDB() const
|
||||||
{
|
{
|
||||||
return m_steamGridDB;
|
return m_steamGridDB;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "gamelauncher.h"
|
#include "gamelauncher.h"
|
||||||
#include "gamemodel.h"
|
#include "gamemodel.h"
|
||||||
|
#include "inputserviceclient.h"
|
||||||
#include "mediamanager.h"
|
#include "mediamanager.h"
|
||||||
#include "runnermanagerclient.h"
|
#include "runnermanagerclient.h"
|
||||||
#include "steamgriddb.h"
|
#include "steamgriddb.h"
|
||||||
|
|
@ -24,6 +25,7 @@ class App : public QObject
|
||||||
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
Q_PROPERTY(GameModel *gameModel READ gameModel CONSTANT)
|
||||||
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
Q_PROPERTY(GameLauncher *launcher READ launcher CONSTANT)
|
||||||
Q_PROPERTY(RunnerManagerClient *runnerManager READ runnerManager CONSTANT)
|
Q_PROPERTY(RunnerManagerClient *runnerManager READ runnerManager CONSTANT)
|
||||||
|
Q_PROPERTY(InputServiceClient *inputService READ inputService CONSTANT)
|
||||||
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
Q_PROPERTY(SteamGridDB *steamGridDB READ steamGridDB CONSTANT)
|
||||||
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
Q_PROPERTY(MediaManager *mediaManager READ mediaManager CONSTANT)
|
||||||
Q_PROPERTY(Config *config READ config CONSTANT)
|
Q_PROPERTY(Config *config READ config CONSTANT)
|
||||||
|
|
@ -37,6 +39,7 @@ public:
|
||||||
GameModel *gameModel() const;
|
GameModel *gameModel() const;
|
||||||
GameLauncher *launcher() const;
|
GameLauncher *launcher() const;
|
||||||
RunnerManagerClient *runnerManager() const;
|
RunnerManagerClient *runnerManager() const;
|
||||||
|
InputServiceClient *inputService() const;
|
||||||
SteamGridDB *steamGridDB() const;
|
SteamGridDB *steamGridDB() const;
|
||||||
MediaManager *mediaManager() const;
|
MediaManager *mediaManager() const;
|
||||||
Config *config() const;
|
Config *config() const;
|
||||||
|
|
@ -78,6 +81,7 @@ private:
|
||||||
GameModel *m_gameModel;
|
GameModel *m_gameModel;
|
||||||
GameLauncher *m_launcher;
|
GameLauncher *m_launcher;
|
||||||
RunnerManagerClient *m_runnerManager;
|
RunnerManagerClient *m_runnerManager;
|
||||||
|
InputServiceClient *m_inputService;
|
||||||
SteamGridDB *m_steamGridDB;
|
SteamGridDB *m_steamGridDB;
|
||||||
MediaManager *m_mediaManager;
|
MediaManager *m_mediaManager;
|
||||||
Config *m_config;
|
Config *m_config;
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,40 @@ add_executable(alakarte_gamecenter
|
||||||
|
|
||||||
target_link_libraries(alakarte_gamecenter PRIVATE
|
target_link_libraries(alakarte_gamecenter PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
|
Qt6::Concurrent
|
||||||
Qt6::DBus
|
Qt6::DBus
|
||||||
|
KF6::CoreAddons
|
||||||
|
KF6::DBusAddons
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (KF6Auth_FOUND)
|
||||||
|
target_compile_definitions(alakarte_gamecenter PRIVATE
|
||||||
|
ALAKARTE_HAVE_KAUTH
|
||||||
|
)
|
||||||
|
target_link_libraries(alakarte_gamecenter PRIVATE
|
||||||
|
KF6::AuthCore
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(alakarte_gamecenter_helper
|
||||||
|
gamecenterkauthhelper.cpp
|
||||||
|
)
|
||||||
|
target_link_libraries(alakarte_gamecenter_helper PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::DBus
|
||||||
|
KF6::AuthCore
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS alakarte_gamecenter_helper
|
||||||
|
DESTINATION ${KAUTH_HELPER_INSTALL_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES polkit/org.kde.alakarte.gamecenter.helper.policy
|
||||||
|
DESTINATION ${KAUTH_POLICY_FILES_INSTALL_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
KAUTH_INSTALL_HELPER_FILES(alakarte_gamecenter_helper org.kde.alakarte.gamecenter.helper root)
|
||||||
|
endif()
|
||||||
|
|
||||||
set_target_properties(alakarte_gamecenter PROPERTIES
|
set_target_properties(alakarte_gamecenter PROPERTIES
|
||||||
OUTPUT_NAME "alakarte-gamecenter"
|
OUTPUT_NAME "alakarte-gamecenter"
|
||||||
)
|
)
|
||||||
|
|
@ -24,9 +55,6 @@ install(TARGETS alakarte_gamecenter ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
|
|
||||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
|
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
|
||||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
|
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
|
||||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus-system")
|
|
||||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd-system")
|
|
||||||
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus-policy")
|
|
||||||
|
|
||||||
configure_file(dbus/org.kde.GameCenter1.service.in
|
configure_file(dbus/org.kde.GameCenter1.service.in
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
||||||
|
|
@ -38,21 +66,6 @@ configure_file(systemd/org.kde.GameCenter1.service.in
|
||||||
@ONLY
|
@ONLY
|
||||||
)
|
)
|
||||||
|
|
||||||
configure_file(dbus/org.kde.GameCenter1.system.service.in
|
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/dbus-system/org.kde.GameCenter1.service"
|
|
||||||
@ONLY
|
|
||||||
)
|
|
||||||
|
|
||||||
configure_file(systemd/org.kde.GameCenter1.system.service.in
|
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/systemd-system/org.kde.GameCenter1.service"
|
|
||||||
@ONLY
|
|
||||||
)
|
|
||||||
|
|
||||||
configure_file(dbus/org.kde.GameCenter1.conf.in
|
|
||||||
"${CMAKE_CURRENT_BINARY_DIR}/dbus-policy/org.kde.GameCenter1.conf"
|
|
||||||
@ONLY
|
|
||||||
)
|
|
||||||
|
|
||||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
||||||
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
|
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
|
||||||
)
|
)
|
||||||
|
|
@ -60,15 +73,3 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
||||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service"
|
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service"
|
||||||
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
|
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus-system/org.kde.GameCenter1.service"
|
|
||||||
DESTINATION ${KDE_INSTALL_DBUSSYSTEMSERVICEDIR}
|
|
||||||
)
|
|
||||||
|
|
||||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd-system/org.kde.GameCenter1.service"
|
|
||||||
DESTINATION ${KDE_INSTALL_SYSTEMDUNITDIR}
|
|
||||||
)
|
|
||||||
|
|
||||||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus-policy/org.kde.GameCenter1.conf"
|
|
||||||
DESTINATION ${KDE_INSTALL_SYSCONFDIR}/dbus-1/system.d
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,14 @@
|
||||||
|
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
|
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#ifdef ALAKARTE_HAVE_KAUTH
|
||||||
|
#include <KAuth/Action>
|
||||||
|
#include <KAuth/ExecuteJob>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||||
|
|
@ -43,6 +51,26 @@ static QString ensureScopeUnitName(const QString &unitName)
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
static void terminatePids(const QList<uint> &pids)
|
||||||
|
{
|
||||||
|
for (uint pid : pids) {
|
||||||
|
if (pid == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
::kill(static_cast<pid_t>(pid), SIGTERM);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void killPids(const QList<uint> &pids)
|
||||||
|
{
|
||||||
|
for (uint pid : pids) {
|
||||||
|
if (pid == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
::kill(static_cast<pid_t>(pid), SIGKILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static QVariant unwrapDbusVariant(QVariant v)
|
static QVariant unwrapDbusVariant(QVariant v)
|
||||||
{
|
{
|
||||||
if (v.canConvert<QDBusVariant>()) {
|
if (v.canConvert<QDBusVariant>()) {
|
||||||
|
|
@ -236,6 +264,8 @@ private:
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
#if 0
|
||||||
|
|
||||||
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/GameCenter1");
|
||||||
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
|
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
|
||||||
|
|
@ -675,6 +705,8 @@ bool GameCenterSystemProxy::init()
|
||||||
return d->init();
|
return d->init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath)
|
void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath)
|
||||||
{
|
{
|
||||||
if (unitName.isEmpty()) {
|
if (unitName.isEmpty()) {
|
||||||
|
|
@ -910,6 +942,18 @@ QVariantMap GameCenterDaemon::GetCapabilities() const
|
||||||
QVariantMap caps;
|
QVariantMap caps;
|
||||||
caps.insert(QStringLiteral("supportsSystemd"), m_systemd.isAvailable());
|
caps.insert(QStringLiteral("supportsSystemd"), m_systemd.isAvailable());
|
||||||
caps.insert(QStringLiteral("supportsSystemBus"), false);
|
caps.insert(QStringLiteral("supportsSystemBus"), false);
|
||||||
|
#ifdef ALAKARTE_HAVE_KAUTH
|
||||||
|
bool supportsPowerProfiles = false;
|
||||||
|
{
|
||||||
|
QDBusConnection sysBus = QDBusConnection::systemBus();
|
||||||
|
if (sysBus.isConnected() && sysBus.interface()) {
|
||||||
|
supportsPowerProfiles = sysBus.interface()->isServiceRegistered(QStringLiteral("net.hadess.PowerProfiles"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
caps.insert(QStringLiteral("supportsPowerProfiles"), supportsPowerProfiles);
|
||||||
|
#else
|
||||||
|
caps.insert(QStringLiteral("supportsPowerProfiles"), false);
|
||||||
|
#endif
|
||||||
const bool systemdAvailable = m_systemd.isAvailable();
|
const bool systemdAvailable = m_systemd.isAvailable();
|
||||||
const bool steamAvailable = !QStandardPaths::findExecutable(QStringLiteral("steam")).isEmpty();
|
const bool steamAvailable = !QStandardPaths::findExecutable(QStringLiteral("steam")).isEmpty();
|
||||||
const bool lutrisAvailable = !QStandardPaths::findExecutable(QStringLiteral("lutris")).isEmpty();
|
const bool lutrisAvailable = !QStandardPaths::findExecutable(QStringLiteral("lutris")).isEmpty();
|
||||||
|
|
@ -948,12 +992,44 @@ void GameCenterDaemon::SetPolicy(const QVariantMap &policy)
|
||||||
}
|
}
|
||||||
m_maxConcurrent = v.toInt();
|
m_maxConcurrent = v.toInt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (policy.contains(QStringLiteral("powerProfile"))) {
|
||||||
|
QVariant v = policy.value(QStringLiteral("powerProfile"));
|
||||||
|
if (v.canConvert<QDBusVariant>()) {
|
||||||
|
v = v.value<QDBusVariant>().variant();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString nextProfile = v.toString();
|
||||||
|
if (!nextProfile.isEmpty() && nextProfile != m_powerProfile) {
|
||||||
|
#ifdef ALAKARTE_HAVE_KAUTH
|
||||||
|
const QString previousProfile = m_powerProfile;
|
||||||
|
QVariantMap args;
|
||||||
|
args.insert(QStringLiteral("profile"), nextProfile);
|
||||||
|
|
||||||
|
KAuth::Action action(QStringLiteral("org.kde.alakarte.gamecenter.helper.setpowerprofile"));
|
||||||
|
action.setHelperId(QStringLiteral("org.kde.alakarte.gamecenter.helper"));
|
||||||
|
action.setArguments(args);
|
||||||
|
|
||||||
|
KAuth::ExecuteJob *job = action.execute();
|
||||||
|
if (job) {
|
||||||
|
if (job->exec()) {
|
||||||
|
m_powerProfile = nextProfile;
|
||||||
|
} else {
|
||||||
|
m_powerProfile = previousProfile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
m_powerProfile = nextProfile;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap GameCenterDaemon::GetPolicy() const
|
QVariantMap GameCenterDaemon::GetPolicy() const
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
{QStringLiteral("maxConcurrent"), m_maxConcurrent},
|
{QStringLiteral("maxConcurrent"), m_maxConcurrent},
|
||||||
|
{QStringLiteral("powerProfile"), m_powerProfile},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1020,7 +1096,10 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
|
||||||
v = v.value<QDBusVariant>().variant();
|
v = v.value<QDBusVariant>().variant();
|
||||||
}
|
}
|
||||||
if (v.canConvert<QVariantMap>()) {
|
if (v.canConvert<QVariantMap>()) {
|
||||||
return v.toMap();
|
const QVariantMap asMap = v.toMap();
|
||||||
|
if (!asMap.isEmpty()) {
|
||||||
|
return asMap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (v.canConvert<QDBusArgument>()) {
|
if (v.canConvert<QDBusArgument>()) {
|
||||||
const QDBusArgument arg = v.value<QDBusArgument>();
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
|
@ -1033,9 +1112,24 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
|
||||||
return converted;
|
return converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap asMap = qdbus_cast<QVariantMap>(arg);
|
QVariantMap converted;
|
||||||
if (!asMap.isEmpty()) {
|
{
|
||||||
return asMap;
|
QDBusArgument mapArg = arg;
|
||||||
|
mapArg.beginMap();
|
||||||
|
while (!mapArg.atEnd()) {
|
||||||
|
mapArg.beginMapEntry();
|
||||||
|
QString key;
|
||||||
|
QDBusVariant value;
|
||||||
|
mapArg >> key >> value;
|
||||||
|
mapArg.endMapEntry();
|
||||||
|
if (!key.isEmpty()) {
|
||||||
|
converted.insert(key, value.variant());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mapArg.endMap();
|
||||||
|
}
|
||||||
|
if (!converted.isEmpty()) {
|
||||||
|
return converted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return QVariantMap{};
|
return QVariantMap{};
|
||||||
|
|
@ -1238,10 +1332,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
|
||||||
|
|
||||||
matcher = [appId]() -> QList<ProcessScanner::Match> {
|
matcher = [appId]() -> QList<ProcessScanner::Match> {
|
||||||
// Primary: look for SteamAppId=<appId> in environ
|
// Primary: look for SteamAppId=<appId> in environ
|
||||||
QList<ProcessScanner::Match> results = ProcessScanner::findByEnvironment(QStringLiteral("SteamAppId"), appId);
|
QList<ProcessScanner::Match> results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId);
|
||||||
if (results.isEmpty()) {
|
|
||||||
results = ProcessScanner::findByEnvironment(QStringLiteral("SteamGameId"), appId);
|
|
||||||
}
|
|
||||||
// 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)) {
|
||||||
|
|
@ -1330,6 +1421,34 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
|
||||||
}
|
}
|
||||||
|
|
||||||
session.process = bootstrap;
|
session.process = bootstrap;
|
||||||
|
|
||||||
|
const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId));
|
||||||
|
const QString description = session.gameId.isEmpty() ? QStringLiteral("A-La-Karte game") : QStringLiteral("A-La-Karte game %1").arg(session.gameId);
|
||||||
|
|
||||||
|
const QDBusReply<QDBusObjectPath> scopeReply = m_systemd.startTransientScope(unitName, {static_cast<uint>(bootstrap->processId())}, description);
|
||||||
|
if (!scopeReply.isValid()) {
|
||||||
|
const QVariantMap ctx = {
|
||||||
|
{QStringLiteral("gameId"), gameId},
|
||||||
|
{QStringLiteral("provider"), provider},
|
||||||
|
{QStringLiteral("command"), command},
|
||||||
|
{QStringLiteral("unit"), unitName},
|
||||||
|
{QStringLiteral("error"), scopeReply.error().message()},
|
||||||
|
};
|
||||||
|
bootstrap->kill();
|
||||||
|
bootstrap->deleteLater();
|
||||||
|
QVariantMap finalState = sessionToVariantMap(session, QStringLiteral("Failed"));
|
||||||
|
Q_EMIT SessionRemoved(sessionId, finalState);
|
||||||
|
m_sessions.remove(sessionId);
|
||||||
|
failLaunch(QStringLiteral("failed to create transient scope"), ctx);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
session.unitName = unitName;
|
||||||
|
const QDBusReply<QDBusObjectPath> getUnitReply = m_systemd.getUnit(unitName);
|
||||||
|
if (getUnitReply.isValid()) {
|
||||||
|
session.unitPath = getUnitReply.value();
|
||||||
|
}
|
||||||
|
|
||||||
m_sessions[sessionId] = session;
|
m_sessions[sessionId] = session;
|
||||||
|
|
||||||
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) {
|
||||||
|
|
@ -1373,6 +1492,11 @@ 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()) {
|
||||||
|
m_systemd.stopUnit(it.value().unitName);
|
||||||
|
unwatchSystemdUnit(it.value().unitName, it.value().unitPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (it.value().scanner) {
|
if (it.value().scanner) {
|
||||||
it.value().scanner->cancel();
|
it.value().scanner->cancel();
|
||||||
it.value().scanner->deleteLater();
|
it.value().scanner->deleteLater();
|
||||||
|
|
@ -1408,27 +1532,49 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
|
||||||
|
|
||||||
it.value().mainPid = pids.first();
|
it.value().mainPid = pids.first();
|
||||||
|
|
||||||
const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId));
|
const QString unitName = it.value().unitName.isEmpty() ? ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId)) : it.value().unitName;
|
||||||
const QString description = it.value().gameId.isEmpty() ? QStringLiteral("A-La-Karte game") : QStringLiteral("A-La-Karte game %1").arg(it.value().gameId);
|
const QString description = it.value().gameId.isEmpty() ? QStringLiteral("A-La-Karte game") : QStringLiteral("A-La-Karte game %1").arg(it.value().gameId);
|
||||||
|
|
||||||
const QDBusReply<QDBusObjectPath> reply = m_systemd.startTransientScope(unitName, pids, description);
|
QDBusReply<void> attachReply = m_systemd.attachProcessesToUnit(unitName, pids);
|
||||||
if (!reply.isValid()) {
|
if (!attachReply.isValid()) {
|
||||||
const QVariantMap ctx = {
|
if (attachReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
|
||||||
{QStringLiteral("gameId"), it.value().gameId},
|
const QDBusReply<QDBusObjectPath> startReply = m_systemd.startTransientScope(unitName, pids, description);
|
||||||
{QStringLiteral("provider"), it.value().provider},
|
if (!startReply.isValid()) {
|
||||||
{QStringLiteral("unit"), unitName},
|
const QVariantMap ctx = {
|
||||||
{QStringLiteral("error"), reply.error().message()},
|
{QStringLiteral("gameId"), it.value().gameId},
|
||||||
};
|
{QStringLiteral("provider"), it.value().provider},
|
||||||
|
{QStringLiteral("unit"), unitName},
|
||||||
|
{QStringLiteral("error"), startReply.error().message()},
|
||||||
|
};
|
||||||
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
Q_EMIT SessionRemoved(sessionId, finalState);
|
||||||
|
m_sessions.erase(it);
|
||||||
|
|
||||||
|
failLaunch(QStringLiteral("failed to create transient scope for monitored game"), ctx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const QVariantMap ctx = {
|
||||||
|
{QStringLiteral("gameId"), it.value().gameId},
|
||||||
|
{QStringLiteral("provider"), it.value().provider},
|
||||||
|
{QStringLiteral("unit"), unitName},
|
||||||
|
{QStringLiteral("error"), attachReply.error().message()},
|
||||||
|
};
|
||||||
|
|
||||||
|
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed"));
|
||||||
|
if (it.value().process) {
|
||||||
|
it.value().process->deleteLater();
|
||||||
|
}
|
||||||
|
Q_EMIT SessionRemoved(sessionId, finalState);
|
||||||
|
m_sessions.erase(it);
|
||||||
|
|
||||||
|
failLaunch(QStringLiteral("failed to attach monitored game to scope"), ctx);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Q_EMIT SessionRemoved(sessionId, finalState);
|
|
||||||
m_sessions.erase(it);
|
|
||||||
|
|
||||||
failLaunch(QStringLiteral("failed to create transient scope for monitored game"), ctx);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
it.value().unitName = unitName;
|
it.value().unitName = unitName;
|
||||||
|
|
@ -1446,11 +1592,25 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
|
||||||
}
|
}
|
||||||
|
|
||||||
if (it.value().stopping) {
|
if (it.value().stopping) {
|
||||||
m_systemd.stopUnit(it.value().unitName);
|
const QDBusReply<QDBusObjectPath> stopReply = m_systemd.stopUnit(it.value().unitName);
|
||||||
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped"));
|
if (stopReply.isValid()) {
|
||||||
Q_EMIT SessionRemoved(sessionId, finalState);
|
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
|
||||||
|
unwatchSystemdUnit(unitName, it.value().unitPath);
|
||||||
|
removeSessionInternal(sessionId, QStringLiteral("Stopped"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
terminatePids(pids);
|
||||||
|
QTimer::singleShot(5000, this, [pids]() {
|
||||||
|
killPids(pids);
|
||||||
|
});
|
||||||
|
|
||||||
unwatchSystemdUnit(unitName, it.value().unitPath);
|
unwatchSystemdUnit(unitName, it.value().unitPath);
|
||||||
m_sessions.erase(it);
|
removeSessionInternal(sessionId, QStringLiteral("Stopped"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1534,12 +1694,6 @@ void GameCenterDaemon::Stop(const QString &sessionId)
|
||||||
if (it.value().scanner) {
|
if (it.value().scanner) {
|
||||||
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping")));
|
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping")));
|
||||||
|
|
||||||
if (it.value().scanner) {
|
|
||||||
it.value().scanner->cancel();
|
|
||||||
it.value().scanner->deleteLater();
|
|
||||||
it.value().scanner = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPointer<QProcess> proc = it.value().process;
|
QPointer<QProcess> proc = it.value().process;
|
||||||
it.value().process = nullptr;
|
it.value().process = nullptr;
|
||||||
if (proc) {
|
if (proc) {
|
||||||
|
|
@ -1556,9 +1710,10 @@ void GameCenterDaemon::Stop(const QString &sessionId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped"));
|
if (!it.value().unitName.isEmpty()) {
|
||||||
Q_EMIT SessionRemoved(sessionId, finalState);
|
m_systemd.stopUnit(it.value().unitName);
|
||||||
m_sessions.erase(it);
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1575,6 +1730,12 @@ void GameCenterDaemon::Stop(const QString &sessionId)
|
||||||
|
|
||||||
const QDBusReply<QDBusObjectPath> reply = m_systemd.stopUnit(it.value().unitName);
|
const QDBusReply<QDBusObjectPath> reply = m_systemd.stopUnit(it.value().unitName);
|
||||||
if (!reply.isValid()) {
|
if (!reply.isValid()) {
|
||||||
|
if (reply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
|
||||||
|
unwatchSystemdUnit(it.value().unitName, it.value().unitPath);
|
||||||
|
removeSessionInternal(sessionId, QStringLiteral("Stopped"));
|
||||||
|
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]() {
|
||||||
|
|
@ -1617,12 +1778,12 @@ QString GameCenterDaemon::sessionState(const Session &session)
|
||||||
if (session.stopping) {
|
if (session.stopping) {
|
||||||
return QStringLiteral("Stopping");
|
return QStringLiteral("Stopping");
|
||||||
}
|
}
|
||||||
if (!session.unitName.isEmpty()) {
|
|
||||||
return QStringLiteral("Running");
|
|
||||||
}
|
|
||||||
if (session.scanner) {
|
if (session.scanner) {
|
||||||
return QStringLiteral("Launching");
|
return QStringLiteral("Launching");
|
||||||
}
|
}
|
||||||
|
if (!session.unitName.isEmpty()) {
|
||||||
|
return QStringLiteral("Running");
|
||||||
|
}
|
||||||
if (session.process) {
|
if (session.process) {
|
||||||
return session.process->state() == QProcess::Running ? QStringLiteral("Running") : QStringLiteral("Exited");
|
return session.process->state() == QProcess::Running ? QStringLiteral("Running") : QStringLiteral("Exited");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,23 +92,8 @@ private:
|
||||||
SystemdUserManager m_systemd;
|
SystemdUserManager m_systemd;
|
||||||
QHash<QString, Session> m_sessions;
|
QHash<QString, Session> m_sessions;
|
||||||
int m_maxConcurrent = 0;
|
int m_maxConcurrent = 0;
|
||||||
|
QString m_powerProfile;
|
||||||
QHash<QString, QString> m_unitPathToSessionId;
|
QHash<QString, QString> m_unitPathToSessionId;
|
||||||
QHash<QString, QString> m_unitNameToSessionId;
|
QHash<QString, QString> m_unitNameToSessionId;
|
||||||
QHash<QString, QObject *> m_unitPathWatchers;
|
QHash<QString, QObject *> m_unitPathWatchers;
|
||||||
};
|
};
|
||||||
|
|
||||||
class GameCenterSystemProxyPrivate;
|
|
||||||
|
|
||||||
class GameCenterSystemProxy : public QObject
|
|
||||||
{
|
|
||||||
Q_OBJECT
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit GameCenterSystemProxy(QObject *parent = nullptr);
|
|
||||||
~GameCenterSystemProxy() override;
|
|
||||||
|
|
||||||
bool init();
|
|
||||||
|
|
||||||
private:
|
|
||||||
std::unique_ptr<GameCenterSystemProxyPrivate> d;
|
|
||||||
};
|
|
||||||
|
|
|
||||||
79
src/gamecenter/gamecenterkauthhelper.cpp
Normal file
79
src/gamecenter/gamecenterkauthhelper.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include <KAuth/ActionReply>
|
||||||
|
#include <KAuth/HelperSupport>
|
||||||
|
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusError>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QDBusMessage>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDBusVariant>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
using namespace KAuth;
|
||||||
|
|
||||||
|
static const QString kPowerProfilesService = QStringLiteral("net.hadess.PowerProfiles");
|
||||||
|
static const QString kPowerProfilesPath = QStringLiteral("/net/hadess/PowerProfiles");
|
||||||
|
static const QString kPowerProfilesInterface = QStringLiteral("net.hadess.PowerProfiles");
|
||||||
|
|
||||||
|
class GameCenterHelper : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
ActionReply setpowerprofile(const QVariantMap &args);
|
||||||
|
};
|
||||||
|
|
||||||
|
static QString unwrapStringArg(const QVariantMap &args, const QString &key)
|
||||||
|
{
|
||||||
|
QVariant v = args.value(key);
|
||||||
|
if (v.canConvert<QDBusVariant>()) {
|
||||||
|
v = v.value<QDBusVariant>().variant();
|
||||||
|
}
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
ActionReply GameCenterHelper::setpowerprofile(const QVariantMap &args)
|
||||||
|
{
|
||||||
|
const QString profile = unwrapStringArg(args, QStringLiteral("profile"));
|
||||||
|
if (profile.isEmpty()) {
|
||||||
|
ActionReply reply = ActionReply::HelperErrorReply();
|
||||||
|
reply.setErrorDescription(QStringLiteral("missing profile"));
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusConnection bus = QDBusConnection::systemBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
ActionReply reply = ActionReply::HelperErrorReply();
|
||||||
|
reply.setErrorDescription(QStringLiteral("system bus not connected"));
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface props(kPowerProfilesService, kPowerProfilesPath, QStringLiteral("org.freedesktop.DBus.Properties"), bus);
|
||||||
|
if (!props.isValid()) {
|
||||||
|
ActionReply reply = ActionReply::HelperErrorReply();
|
||||||
|
reply.setErrorDescription(QStringLiteral("power-profiles-daemon D-Bus interface not available"));
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusMessage msg =
|
||||||
|
QDBusMessage::createMethodCall(kPowerProfilesService, kPowerProfilesPath, QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("Set"));
|
||||||
|
msg.setArguments({kPowerProfilesInterface, QStringLiteral("ActiveProfile"), QVariant::fromValue(QDBusVariant(profile))});
|
||||||
|
|
||||||
|
const QDBusMessage replyMsg = bus.call(msg, QDBus::Block, 5000);
|
||||||
|
if (replyMsg.type() == QDBusMessage::ErrorMessage) {
|
||||||
|
ActionReply reply = ActionReply::HelperErrorReply();
|
||||||
|
reply.setErrorDescription(replyMsg.errorName() + QStringLiteral(": ") + replyMsg.errorMessage());
|
||||||
|
return reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ActionReply::SuccessReply();
|
||||||
|
}
|
||||||
|
|
||||||
|
KAUTH_HELPER_MAIN("org.kde.alakarte.gamecenter.helper", GameCenterHelper)
|
||||||
|
|
||||||
|
#include "gamecenterkauthhelper.moc"
|
||||||
|
|
@ -5,31 +5,37 @@
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
#include <QCoreApplication>
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
#include <KDBusService>
|
||||||
|
#include <KSignalHandler>
|
||||||
|
|
||||||
#include "gamecenterdaemon.h"
|
#include "gamecenterdaemon.h"
|
||||||
|
|
||||||
|
#include <signal.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"));
|
||||||
|
app.setApplicationName(QStringLiteral("alakarte_gamecenter"));
|
||||||
|
|
||||||
|
KDBusService service(KDBusService::Unique);
|
||||||
|
|
||||||
QCommandLineParser parser;
|
QCommandLineParser parser;
|
||||||
parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center"));
|
parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center"));
|
||||||
parser.addHelpOption();
|
parser.addHelpOption();
|
||||||
parser.addOption(QCommandLineOption(QStringLiteral("system"), QStringLiteral("Run as system bus proxy")));
|
|
||||||
parser.process(app);
|
parser.process(app);
|
||||||
|
|
||||||
if (parser.isSet(QStringLiteral("system"))) {
|
|
||||||
GameCenterSystemProxy proxy;
|
|
||||||
if (!proxy.init()) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return app.exec();
|
|
||||||
}
|
|
||||||
|
|
||||||
GameCenterDaemon daemon;
|
GameCenterDaemon daemon;
|
||||||
if (!daemon.init()) {
|
if (!daemon.init()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KSignalHandler::self()->watchSignal(SIGTERM);
|
||||||
|
KSignalHandler::self()->watchSignal(SIGINT);
|
||||||
|
QObject::connect(KSignalHandler::self(), &KSignalHandler::signalReceived, &app, [&app](int signal) {
|
||||||
|
Q_UNUSED(signal);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
return app.exec();
|
return app.exec();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!DOCTYPE policyconfig PUBLIC
|
||||||
|
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
|
||||||
|
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
|
||||||
|
<policyconfig>
|
||||||
|
<vendor>A-La-Karte</vendor>
|
||||||
|
<vendor_url>https://invent.kde.org/marcoa/a-la-karte</vendor_url>
|
||||||
|
<icon_name>org.kde.alakarte</icon_name>
|
||||||
|
<action id="org.kde.alakarte.gamecenter.helper.setpowerprofile">
|
||||||
|
<description>Set system power profile</description>
|
||||||
|
<message>Authentication is required to set the system power profile.</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_inactive>no</allow_inactive>
|
||||||
|
<allow_active>auth_admin_keep</allow_active>
|
||||||
|
</defaults>
|
||||||
|
</action>
|
||||||
|
</policyconfig>
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QtConcurrentRun>
|
||||||
|
|
||||||
ProcessScanner::ProcessScanner(QObject *parent)
|
ProcessScanner::ProcessScanner(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
|
|
@ -15,21 +16,61 @@ 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;
|
||||||
|
++m_generation;
|
||||||
|
m_scanInFlight = false;
|
||||||
Q_EMIT timedOut();
|
Q_EMIT timedOut();
|
||||||
});
|
});
|
||||||
|
|
||||||
connect(&m_timer, &QTimer::timeout, this, [this]() {
|
connect(&m_timer, &QTimer::timeout, this, [this]() {
|
||||||
if (!m_matcher) {
|
startScan();
|
||||||
m_timer.stop();
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessScanner::startScan()
|
||||||
|
{
|
||||||
|
if (!m_matcher) {
|
||||||
|
m_timer.stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_scanInFlight && m_scanGeneration == m_generation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const quint64 gen = m_generation;
|
||||||
|
m_scanInFlight = true;
|
||||||
|
m_scanGeneration = gen;
|
||||||
|
|
||||||
|
const std::function<QList<Match>()> matcher = m_matcher;
|
||||||
|
auto *watcher = new QFutureWatcher<QList<Match>>(this);
|
||||||
|
|
||||||
|
connect(watcher, &QFutureWatcher<QList<Match>>::finished, this, [this, watcher, gen]() {
|
||||||
|
const QList<Match> results = watcher->result();
|
||||||
|
watcher->deleteLater();
|
||||||
|
|
||||||
|
if (m_scanGeneration == gen) {
|
||||||
|
m_scanInFlight = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gen != m_generation || !m_matcher) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QList<Match> results = m_matcher();
|
|
||||||
if (!results.isEmpty()) {
|
if (!results.isEmpty()) {
|
||||||
m_timer.stop();
|
m_timer.stop();
|
||||||
m_deadline.stop();
|
m_deadline.stop();
|
||||||
|
m_matcher = nullptr;
|
||||||
Q_EMIT found(results);
|
Q_EMIT found(results);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watcher->setFuture(QtConcurrent::run([matcher]() {
|
||||||
|
if (!matcher) {
|
||||||
|
return QList<Match>{};
|
||||||
|
}
|
||||||
|
return matcher();
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
|
static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
|
||||||
|
|
@ -56,6 +97,28 @@ static QByteArray readEnviron(const QString &pidDir)
|
||||||
return readProcFile(pidDir + QStringLiteral("/environ"));
|
return readProcFile(pidDir + QStringLiteral("/environ"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool containsNullSeparatedEntry(const QByteArray &blob, const QByteArray &needle)
|
||||||
|
{
|
||||||
|
if (blob.isEmpty() || needle.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pos = -1;
|
||||||
|
while (true) {
|
||||||
|
pos = blob.indexOf(needle, pos + 1);
|
||||||
|
if (pos < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool startOk = (pos == 0) || (blob.at(pos - 1) == '\0');
|
||||||
|
const int endPos = pos + needle.size();
|
||||||
|
const bool endOk = (endPos == blob.size()) || (blob.at(endPos) == '\0');
|
||||||
|
if (startOk && endOk) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static QList<uint> listPids()
|
static QList<uint> listPids()
|
||||||
{
|
{
|
||||||
QList<uint> pids;
|
QList<uint> pids;
|
||||||
|
|
@ -84,22 +147,7 @@ QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &ke
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// environ entries are separated by null bytes
|
if (containsNullSeparatedEntry(env, needle)) {
|
||||||
bool found = false;
|
|
||||||
int start = 0;
|
|
||||||
while (start < env.size()) {
|
|
||||||
int end = env.indexOf('\0', start);
|
|
||||||
if (end < 0) {
|
|
||||||
end = env.size();
|
|
||||||
}
|
|
||||||
if (env.mid(start, end - start) == needle) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
start = end + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (found) {
|
|
||||||
Match m;
|
Match m;
|
||||||
m.pid = pid;
|
m.pid = pid;
|
||||||
m.exe = readExeLink(pidDir);
|
m.exe = readExeLink(pidDir);
|
||||||
|
|
@ -111,6 +159,54 @@ QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &ke
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<ProcessScanner::Match> ProcessScanner::findByAnyEnvironment(const QStringList &keys, const QString &value)
|
||||||
|
{
|
||||||
|
QList<Match> matches;
|
||||||
|
if (keys.isEmpty()) {
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QByteArray> needles;
|
||||||
|
needles.reserve(keys.size());
|
||||||
|
for (const QString &key : keys) {
|
||||||
|
if (!key.isEmpty()) {
|
||||||
|
needles.push_back((key + QLatin1Char('=') + value).toUtf8());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (needles.isEmpty()) {
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QList<uint> pids = listPids();
|
||||||
|
for (uint pid : pids) {
|
||||||
|
const QString pidDir = QStringLiteral("/proc/%1").arg(pid);
|
||||||
|
const QByteArray env = readEnviron(pidDir);
|
||||||
|
if (env.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (const QByteArray &needle : needles) {
|
||||||
|
if (containsNullSeparatedEntry(env, needle)) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Match m;
|
||||||
|
m.pid = pid;
|
||||||
|
m.exe = readExeLink(pidDir);
|
||||||
|
const QByteArray cmd = readCmdline(pidDir);
|
||||||
|
m.cmdline = QString::fromLocal8Bit(cmd).replace(QLatin1Char('\0'), QLatin1Char(' ')).trimmed();
|
||||||
|
matches.push_back(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
|
||||||
QList<ProcessScanner::Match> ProcessScanner::findByCmdline(const QString &substring)
|
QList<ProcessScanner::Match> ProcessScanner::findByCmdline(const QString &substring)
|
||||||
{
|
{
|
||||||
QList<Match> matches;
|
QList<Match> matches;
|
||||||
|
|
@ -164,15 +260,10 @@ void ProcessScanner::pollUntilFound(std::function<QList<Match>()> matcher, int i
|
||||||
cancel();
|
cancel();
|
||||||
m_matcher = std::move(matcher);
|
m_matcher = std::move(matcher);
|
||||||
|
|
||||||
// Try once immediately
|
|
||||||
const QList<Match> immediate = m_matcher();
|
|
||||||
if (!immediate.isEmpty()) {
|
|
||||||
Q_EMIT found(immediate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_deadline.start(timeoutMs);
|
m_deadline.start(timeoutMs);
|
||||||
m_timer.start(intervalMs);
|
m_timer.start(intervalMs);
|
||||||
|
|
||||||
|
startScan();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProcessScanner::cancel()
|
void ProcessScanner::cancel()
|
||||||
|
|
@ -180,4 +271,6 @@ void ProcessScanner::cancel()
|
||||||
m_timer.stop();
|
m_timer.stop();
|
||||||
m_deadline.stop();
|
m_deadline.stop();
|
||||||
m_matcher = nullptr;
|
m_matcher = nullptr;
|
||||||
|
++m_generation;
|
||||||
|
m_scanInFlight = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QFutureWatcher>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
#include <QtGlobal>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
class ProcessScanner : public QObject
|
class ProcessScanner : public QObject
|
||||||
|
|
@ -25,6 +27,9 @@ public:
|
||||||
// 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);
|
||||||
|
|
||||||
|
// Find PIDs whose /proc/<pid>/environ contains any of key=value for the provided keys
|
||||||
|
static QList<Match> findByAnyEnvironment(const QStringList &keys, const QString &value);
|
||||||
|
|
||||||
// Find PIDs whose /proc/<pid>/cmdline contains the substring
|
// Find PIDs whose /proc/<pid>/cmdline contains the substring
|
||||||
static QList<Match> findByCmdline(const QString &substring);
|
static QList<Match> findByCmdline(const QString &substring);
|
||||||
|
|
||||||
|
|
@ -42,7 +47,12 @@ Q_SIGNALS:
|
||||||
void timedOut();
|
void timedOut();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void startScan();
|
||||||
|
|
||||||
QTimer m_timer;
|
QTimer m_timer;
|
||||||
QTimer m_deadline;
|
QTimer m_deadline;
|
||||||
std::function<QList<Match>()> m_matcher;
|
std::function<QList<Match>()> m_matcher;
|
||||||
|
quint64 m_generation = 0;
|
||||||
|
bool m_scanInFlight = false;
|
||||||
|
quint64 m_scanGeneration = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,17 @@ QDBusReply<QDBusObjectPath> SystemdUserManager::stopUnit(const QString &unitName
|
||||||
return QDBusReply<QDBusObjectPath>(reply);
|
return QDBusReply<QDBusObjectPath>(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QDBusReply<void> SystemdUserManager::attachProcessesToUnit(const QString &unitName, const QList<uint> &pids, const QString &subcgroup)
|
||||||
|
{
|
||||||
|
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
|
||||||
|
QStringLiteral("/org/freedesktop/systemd1"),
|
||||||
|
QStringLiteral("org.freedesktop.systemd1.Manager"),
|
||||||
|
QDBusConnection::sessionBus());
|
||||||
|
|
||||||
|
const QDBusMessage reply = manager.call(QStringLiteral("AttachProcessesToUnit"), unitName, subcgroup, QVariant::fromValue(pids));
|
||||||
|
return QDBusReply<void>(reply);
|
||||||
|
}
|
||||||
|
|
||||||
QDBusReply<QDBusObjectPath> SystemdUserManager::getUnit(const QString &unitName)
|
QDBusReply<QDBusObjectPath> SystemdUserManager::getUnit(const QString &unitName)
|
||||||
{
|
{
|
||||||
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
|
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ public:
|
||||||
|
|
||||||
QDBusReply<QDBusObjectPath> stopUnit(const QString &unitName, const QString &mode = QStringLiteral("replace"));
|
QDBusReply<QDBusObjectPath> stopUnit(const QString &unitName, const QString &mode = QStringLiteral("replace"));
|
||||||
|
|
||||||
|
QDBusReply<void> attachProcessesToUnit(const QString &unitName, const QList<uint> &pids, const QString &subcgroup = QString());
|
||||||
|
|
||||||
QDBusReply<QDBusObjectPath> getUnit(const QString &unitName);
|
QDBusReply<QDBusObjectPath> getUnit(const QString &unitName);
|
||||||
|
|
||||||
QDBusReply<SystemdUnitInfoList> listUnits();
|
QDBusReply<SystemdUnitInfoList> listUnits();
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,11 @@
|
||||||
#include <QDBusReply>
|
#include <QDBusReply>
|
||||||
#include <QDBusVariant>
|
#include <QDBusVariant>
|
||||||
#include <QDateTime>
|
#include <QDateTime>
|
||||||
#include <QDesktopServices>
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUrl>
|
|
||||||
|
|
||||||
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/GameCenter1");
|
||||||
|
|
@ -399,7 +397,7 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!runnerId.isEmpty() || runner == QLatin1String("wine") || runner == QLatin1String("proton")) {
|
{
|
||||||
QVariantMap runnerSpec = {
|
QVariantMap runnerSpec = {
|
||||||
{QStringLiteral("runnerId"), runnerId},
|
{QStringLiteral("runnerId"), runnerId},
|
||||||
{QStringLiteral("runner"), runner},
|
{QStringLiteral("runner"), runner},
|
||||||
|
|
@ -409,6 +407,7 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
|
||||||
{QStringLiteral("program"), program},
|
{QStringLiteral("program"), program},
|
||||||
{QStringLiteral("args"), parts},
|
{QStringLiteral("args"), parts},
|
||||||
{QStringLiteral("envOverrides"), envOverrides},
|
{QStringLiteral("envOverrides"), envOverrides},
|
||||||
|
{QStringLiteral("useGameProfile"), true},
|
||||||
};
|
};
|
||||||
|
|
||||||
QVariantMap resolved;
|
QVariantMap resolved;
|
||||||
|
|
@ -418,22 +417,33 @@ QVariantMap GameLauncher::resolveLaunchInfo(Game *game) const
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
info.insert(QStringLiteral("finalProgram"), resolved.value(QStringLiteral("finalProgram")).toString());
|
if (resolved.contains(QStringLiteral("effectiveEnv"))) {
|
||||||
info.insert(QStringLiteral("finalArgs"), resolved.value(QStringLiteral("finalArgs")).toStringList());
|
effectiveEnv = resolved.value(QStringLiteral("effectiveEnv")).toMap();
|
||||||
info.insert(QStringLiteral("effectiveEnv"), resolved.value(QStringLiteral("effectiveEnv")).toMap());
|
}
|
||||||
|
|
||||||
|
const QString resolvedFinalProgram = resolved.value(QStringLiteral("finalProgram")).toString();
|
||||||
|
const QStringList resolvedFinalArgs = resolved.value(QStringLiteral("finalArgs")).toStringList();
|
||||||
const QString resolvedPrefixPath = resolved.value(QStringLiteral("resolvedPrefixPath")).toString();
|
const QString resolvedPrefixPath = resolved.value(QStringLiteral("resolvedPrefixPath")).toString();
|
||||||
if (!resolvedPrefixPath.isEmpty()) {
|
|
||||||
info.insert(QStringLiteral("resolvedPrefixPath"), resolvedPrefixPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const QString resolvedSteamInstallPath = resolved.value(QStringLiteral("resolvedSteamInstallPath")).toString();
|
const QString resolvedSteamInstallPath = resolved.value(QStringLiteral("resolvedSteamInstallPath")).toString();
|
||||||
if (!resolvedSteamInstallPath.isEmpty()) {
|
|
||||||
info.insert(QStringLiteral("resolvedSteamInstallPath"), resolvedSteamInstallPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
info.insert(QStringLiteral("ok"), true);
|
const bool runnerManagerWrapped =
|
||||||
return info;
|
(resolvedFinalProgram != program) || (resolvedFinalArgs != parts) || !resolvedPrefixPath.isEmpty() || !resolvedSteamInstallPath.isEmpty();
|
||||||
|
|
||||||
|
if (runnerManagerWrapped) {
|
||||||
|
info.insert(QStringLiteral("finalProgram"), resolvedFinalProgram);
|
||||||
|
info.insert(QStringLiteral("finalArgs"), resolvedFinalArgs);
|
||||||
|
info.insert(QStringLiteral("effectiveEnv"), effectiveEnv);
|
||||||
|
|
||||||
|
if (!resolvedPrefixPath.isEmpty()) {
|
||||||
|
info.insert(QStringLiteral("resolvedPrefixPath"), resolvedPrefixPath);
|
||||||
|
}
|
||||||
|
if (!resolvedSteamInstallPath.isEmpty()) {
|
||||||
|
info.insert(QStringLiteral("resolvedSteamInstallPath"), resolvedSteamInstallPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
info.insert(QStringLiteral("ok"), true);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
} else if (!runnerId.isEmpty()) {
|
} else if (!runnerId.isEmpty()) {
|
||||||
info.insert(QStringLiteral("error"), tr("Runner service is not available"));
|
info.insert(QStringLiteral("error"), tr("Runner service is not available"));
|
||||||
return info;
|
return info;
|
||||||
|
|
@ -606,17 +616,6 @@ void GameLauncher::launchGame(Game *game)
|
||||||
|
|
||||||
const QString launchError = reply.isValid() ? QString() : reply.error().message();
|
const QString launchError = reply.isValid() ? QString() : reply.error().message();
|
||||||
|
|
||||||
// Daemon call failed — fallback for URL-type commands only
|
|
||||||
if (launchType == QLatin1String("url")) {
|
|
||||||
const QString url = info.value(QStringLiteral("url")).toString();
|
|
||||||
if (!url.isEmpty()) {
|
|
||||||
QDesktopServices::openUrl(QUrl(url));
|
|
||||||
game->setLastPlayed(QDateTime::currentDateTime());
|
|
||||||
Q_EMIT gameStarted(game);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No fallback for non-URL commands — emit error
|
// No fallback for non-URL commands — emit error
|
||||||
if (!launchError.isEmpty()) {
|
if (!launchError.isEmpty()) {
|
||||||
Q_EMIT gameError(game, tr("Game Center launch failed: %1").arg(launchError));
|
Q_EMIT gameError(game, tr("Game Center launch failed: %1").arg(launchError));
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,21 @@ bool GamepadManager::active() const
|
||||||
return m_active;
|
return m_active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString GamepadManager::name() const
|
||||||
|
{
|
||||||
|
return m_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GamepadManager::vendorId() const
|
||||||
|
{
|
||||||
|
return m_vendorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
int GamepadManager::productId() const
|
||||||
|
{
|
||||||
|
return m_productId;
|
||||||
|
}
|
||||||
|
|
||||||
GamepadManager::ControllerStyle GamepadManager::controllerStyle() const
|
GamepadManager::ControllerStyle GamepadManager::controllerStyle() const
|
||||||
{
|
{
|
||||||
return m_controllerStyle;
|
return m_controllerStyle;
|
||||||
|
|
@ -229,14 +244,24 @@ void GamepadManager::closeGamepad()
|
||||||
void GamepadManager::updateControllerInfo()
|
void GamepadManager::updateControllerInfo()
|
||||||
{
|
{
|
||||||
ControllerStyle style = UnknownController;
|
ControllerStyle style = UnknownController;
|
||||||
|
QString name;
|
||||||
|
int vendorId = 0;
|
||||||
|
int productId = 0;
|
||||||
QString confirmLabel;
|
QString confirmLabel;
|
||||||
QString backLabel;
|
QString backLabel;
|
||||||
QString detailsLabel;
|
QString detailsLabel;
|
||||||
QString searchLabel;
|
QString searchLabel;
|
||||||
|
|
||||||
if (m_gamepad) {
|
if (m_gamepad) {
|
||||||
|
const char *padName = SDL_GetGamepadName(m_gamepad);
|
||||||
|
if (padName) {
|
||||||
|
name = QString::fromUtf8(padName);
|
||||||
|
}
|
||||||
const SDL_GamepadType type = SDL_GetGamepadType(m_gamepad);
|
const SDL_GamepadType type = SDL_GetGamepadType(m_gamepad);
|
||||||
const Uint16 vendor = SDL_GetGamepadVendor(m_gamepad);
|
const Uint16 vendor = SDL_GetGamepadVendor(m_gamepad);
|
||||||
|
const Uint16 product = SDL_GetGamepadProduct(m_gamepad);
|
||||||
|
vendorId = static_cast<int>(vendor);
|
||||||
|
productId = static_cast<int>(product);
|
||||||
style = controllerStyleFromTypeVendor(type, vendor);
|
style = controllerStyleFromTypeVendor(type, vendor);
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(3, 2, 0)
|
#if SDL_VERSION_ATLEAST(3, 2, 0)
|
||||||
|
|
@ -291,9 +316,13 @@ void GamepadManager::updateControllerInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool changed = (style != m_controllerStyle) || (confirmLabel != m_confirmButtonLabel) || (backLabel != m_backButtonLabel)
|
const bool changed = (style != m_controllerStyle) || (confirmLabel != m_confirmButtonLabel) || (backLabel != m_backButtonLabel)
|
||||||
|| (detailsLabel != m_detailsButtonLabel) || (searchLabel != m_searchButtonLabel);
|
|| (detailsLabel != m_detailsButtonLabel) || (searchLabel != m_searchButtonLabel) || (name != m_name) || (vendorId != m_vendorId)
|
||||||
|
|| (productId != m_productId);
|
||||||
|
|
||||||
m_controllerStyle = style;
|
m_controllerStyle = style;
|
||||||
|
m_name = name;
|
||||||
|
m_vendorId = vendorId;
|
||||||
|
m_productId = productId;
|
||||||
m_confirmButtonLabel = confirmLabel;
|
m_confirmButtonLabel = confirmLabel;
|
||||||
m_backButtonLabel = backLabel;
|
m_backButtonLabel = backLabel;
|
||||||
m_detailsButtonLabel = detailsLabel;
|
m_detailsButtonLabel = detailsLabel;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ class GamepadManager : public QObject
|
||||||
|
|
||||||
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
|
Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged)
|
||||||
Q_PROPERTY(bool active READ active NOTIFY activeChanged)
|
Q_PROPERTY(bool active READ active NOTIFY activeChanged)
|
||||||
|
Q_PROPERTY(QString name READ name NOTIFY controllerInfoChanged)
|
||||||
|
Q_PROPERTY(int vendorId READ vendorId NOTIFY controllerInfoChanged)
|
||||||
|
Q_PROPERTY(int productId READ productId NOTIFY controllerInfoChanged)
|
||||||
Q_PROPERTY(ControllerStyle controllerStyle READ controllerStyle NOTIFY controllerInfoChanged)
|
Q_PROPERTY(ControllerStyle controllerStyle READ controllerStyle NOTIFY controllerInfoChanged)
|
||||||
Q_PROPERTY(QString confirmButtonLabel READ confirmButtonLabel NOTIFY controllerInfoChanged)
|
Q_PROPERTY(QString confirmButtonLabel READ confirmButtonLabel NOTIFY controllerInfoChanged)
|
||||||
Q_PROPERTY(QString backButtonLabel READ backButtonLabel NOTIFY controllerInfoChanged)
|
Q_PROPERTY(QString backButtonLabel READ backButtonLabel NOTIFY controllerInfoChanged)
|
||||||
|
|
@ -40,6 +43,10 @@ public:
|
||||||
bool connected() const;
|
bool connected() const;
|
||||||
bool active() const;
|
bool active() const;
|
||||||
|
|
||||||
|
QString name() const;
|
||||||
|
int vendorId() const;
|
||||||
|
int productId() const;
|
||||||
|
|
||||||
ControllerStyle controllerStyle() const;
|
ControllerStyle controllerStyle() const;
|
||||||
QString confirmButtonLabel() const;
|
QString confirmButtonLabel() const;
|
||||||
QString backButtonLabel() const;
|
QString backButtonLabel() const;
|
||||||
|
|
@ -86,6 +93,10 @@ private:
|
||||||
qint8 m_leftDirX = 0;
|
qint8 m_leftDirX = 0;
|
||||||
qint8 m_leftDirY = 0;
|
qint8 m_leftDirY = 0;
|
||||||
|
|
||||||
|
QString m_name;
|
||||||
|
int m_vendorId = 0;
|
||||||
|
int m_productId = 0;
|
||||||
|
|
||||||
void openFirstAvailableGamepad();
|
void openFirstAvailableGamepad();
|
||||||
void openGamepad(int deviceId);
|
void openGamepad(int deviceId);
|
||||||
void closeGamepad();
|
void closeGamepad();
|
||||||
|
|
|
||||||
41
src/input/CMakeLists.txt
Normal file
41
src/input/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
# SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
add_executable(alakarte_inputd
|
||||||
|
main.cpp
|
||||||
|
inputdaemon.cpp
|
||||||
|
inputdaemon.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(alakarte_inputd PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::DBus
|
||||||
|
SDL3::SDL3
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(alakarte_inputd PROPERTIES
|
||||||
|
OUTPUT_NAME "alakarte-inputd"
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS alakarte_inputd ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||||
|
|
||||||
|
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/dbus")
|
||||||
|
file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/systemd")
|
||||||
|
|
||||||
|
configure_file(dbus/org.kde.ALaKarte.Input1.service.in
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Input1.service"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
|
||||||
|
configure_file(systemd/org.kde.ALaKarte.Input1.service.in
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Input1.service"
|
||||||
|
@ONLY
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.ALaKarte.Input1.service"
|
||||||
|
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.ALaKarte.Input1.service"
|
||||||
|
DESTINATION ${KDE_INSTALL_SYSTEMDUSERUNITDIR}
|
||||||
|
)
|
||||||
4
src/input/dbus/org.kde.ALaKarte.Input1.service.in
Normal file
4
src/input/dbus/org.kde.ALaKarte.Input1.service.in
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[D-BUS Service]
|
||||||
|
Name=org.kde.ALaKarte.Input1
|
||||||
|
Exec=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-inputd
|
||||||
|
SystemdService=org.kde.ALaKarte.Input1.service
|
||||||
618
src/input/inputdaemon.cpp
Normal file
618
src/input/inputdaemon.cpp
Normal file
|
|
@ -0,0 +1,618 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include "inputdaemon.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
|
#include <QDBusError>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#define SDL_MAIN_HANDLED 1
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include <SDL3/SDL_main.h>
|
||||||
|
|
||||||
|
static const QString kInputService = QStringLiteral("org.kde.ALaKarte.Input1");
|
||||||
|
static const QString kInputPath = QStringLiteral("/org/kde/ALaKarte/Input1");
|
||||||
|
static const QString kInputInterface = QStringLiteral("org.kde.ALaKarte.Input1");
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static QString guidToString(SDL_GUID guid)
|
||||||
|
{
|
||||||
|
char buf[64] = {0};
|
||||||
|
SDL_GUIDToString(guid, buf, sizeof(buf));
|
||||||
|
return QString::fromLatin1(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString powerStateToString(SDL_PowerState st)
|
||||||
|
{
|
||||||
|
switch (st) {
|
||||||
|
case SDL_POWERSTATE_ON_BATTERY:
|
||||||
|
return QStringLiteral("on_battery");
|
||||||
|
case SDL_POWERSTATE_CHARGING:
|
||||||
|
return QStringLiteral("charging");
|
||||||
|
case SDL_POWERSTATE_CHARGED:
|
||||||
|
return QStringLiteral("charged");
|
||||||
|
case SDL_POWERSTATE_NO_BATTERY:
|
||||||
|
return QStringLiteral("no_battery");
|
||||||
|
case SDL_POWERSTATE_UNKNOWN:
|
||||||
|
return QStringLiteral("unknown");
|
||||||
|
default:
|
||||||
|
return QStringLiteral("error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString connectionStateToString(SDL_JoystickConnectionState st)
|
||||||
|
{
|
||||||
|
switch (st) {
|
||||||
|
case SDL_JOYSTICK_CONNECTION_WIRED:
|
||||||
|
return QStringLiteral("wired");
|
||||||
|
case SDL_JOYSTICK_CONNECTION_WIRELESS:
|
||||||
|
return QStringLiteral("wireless");
|
||||||
|
default:
|
||||||
|
return QStringLiteral("unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString gamepadTypeToString(SDL_GamepadType t)
|
||||||
|
{
|
||||||
|
switch (t) {
|
||||||
|
case SDL_GAMEPAD_TYPE_XBOX360:
|
||||||
|
case SDL_GAMEPAD_TYPE_XBOXONE:
|
||||||
|
return QStringLiteral("xbox");
|
||||||
|
case SDL_GAMEPAD_TYPE_PS3:
|
||||||
|
case SDL_GAMEPAD_TYPE_PS4:
|
||||||
|
case SDL_GAMEPAD_TYPE_PS5:
|
||||||
|
return QStringLiteral("playstation");
|
||||||
|
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_PRO:
|
||||||
|
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT:
|
||||||
|
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT:
|
||||||
|
case SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_PAIR:
|
||||||
|
return QStringLiteral("nintendo");
|
||||||
|
case SDL_GAMEPAD_TYPE_STANDARD:
|
||||||
|
return QStringLiteral("standard");
|
||||||
|
default:
|
||||||
|
return QStringLiteral("unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap unwrapVariantMap(QVariant v)
|
||||||
|
{
|
||||||
|
if (v.canConvert<QVariantMap>()) {
|
||||||
|
return v.toMap();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::Profile::toVariantMap() const
|
||||||
|
{
|
||||||
|
QVariantMap out;
|
||||||
|
out.insert(QStringLiteral("id"), id);
|
||||||
|
out.insert(QStringLiteral("name"), name);
|
||||||
|
if (!mapping.isEmpty()) {
|
||||||
|
out.insert(QStringLiteral("mapping"), mapping);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDaemon::Profile InputDaemon::Profile::fromVariantMap(const QVariantMap &map)
|
||||||
|
{
|
||||||
|
Profile p;
|
||||||
|
p.id = map.value(QStringLiteral("id")).toString();
|
||||||
|
p.name = map.value(QStringLiteral("name")).toString();
|
||||||
|
p.mapping = map.value(QStringLiteral("mapping")).toMap();
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::Profile::isValid() const
|
||||||
|
{
|
||||||
|
return !id.isEmpty() && !name.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDaemon::InputDaemon(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
SDL_SetMainReady();
|
||||||
|
|
||||||
|
m_pollTimer.setInterval(16);
|
||||||
|
connect(&m_pollTimer, &QTimer::timeout, this, &InputDaemon::pollEvents);
|
||||||
|
|
||||||
|
if (QCoreApplication::instance()) {
|
||||||
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||||
|
shutdownSdl();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::init()
|
||||||
|
{
|
||||||
|
loadProfiles();
|
||||||
|
|
||||||
|
if (!initSdl()) {
|
||||||
|
qWarning() << "InputDaemon: SDL init failed" << SDL_GetError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
qWarning() << "InputDaemon: session bus not connected";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bus.registerService(kInputService)) {
|
||||||
|
qWarning() << "InputDaemon: failed to register service" << kInputService << bus.lastError().message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bus.registerObject(kInputPath, this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) {
|
||||||
|
qWarning() << "InputDaemon: failed to register object" << kInputPath << bus.lastError().message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
openExistingControllers();
|
||||||
|
m_pollTimer.start();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::initSdl()
|
||||||
|
{
|
||||||
|
if (m_sdlInitialized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_sdlInitialized = SDL_Init(SDL_INIT_GAMEPAD | SDL_INIT_JOYSTICK);
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_SetGamepadEventsEnabled(true);
|
||||||
|
SDL_SetJoystickEventsEnabled(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::shutdownSdl()
|
||||||
|
{
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_pollTimer.stop();
|
||||||
|
|
||||||
|
const QList<int> keys = m_controllers.keys();
|
||||||
|
for (int instanceId : keys) {
|
||||||
|
closeController(instanceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Quit();
|
||||||
|
m_sdlInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::Ping() const
|
||||||
|
{
|
||||||
|
return QStringLiteral("ok");
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::Version(uint &major, uint &minor) const
|
||||||
|
{
|
||||||
|
major = 1;
|
||||||
|
minor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::GetCapabilities() const
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{QStringLiteral("supportsBattery"), true},
|
||||||
|
{QStringLiteral("supportsHotplug"), true},
|
||||||
|
{QStringLiteral("supportsProfiles"), true},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::profilesPath() const
|
||||||
|
{
|
||||||
|
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||||
|
return base + QStringLiteral("/input-profiles.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::loadProfiles()
|
||||||
|
{
|
||||||
|
m_profiles.clear();
|
||||||
|
m_controllerKeyToProfileId.clear();
|
||||||
|
|
||||||
|
QFile f(profilesPath());
|
||||||
|
if (f.open(QIODevice::ReadOnly)) {
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
||||||
|
if (doc.isObject()) {
|
||||||
|
const QJsonObject root = doc.object();
|
||||||
|
|
||||||
|
const QJsonArray profiles = root.value(QStringLiteral("profiles")).toArray();
|
||||||
|
for (const QJsonValue &v : profiles) {
|
||||||
|
if (!v.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QVariantMap m = v.toObject().toVariantMap();
|
||||||
|
const Profile p = Profile::fromVariantMap(m);
|
||||||
|
if (p.isValid()) {
|
||||||
|
m_profiles.insert(p.id, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject assignments = root.value(QStringLiteral("assignments")).toObject();
|
||||||
|
for (auto it = assignments.begin(); it != assignments.end(); ++it) {
|
||||||
|
const QString controllerKey = it.key();
|
||||||
|
const QString profileId = it.value().toString();
|
||||||
|
if (!controllerKey.isEmpty() && !profileId.isEmpty()) {
|
||||||
|
m_controllerKeyToProfileId.insert(controllerKey, profileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_profiles.isEmpty()) {
|
||||||
|
Profile def;
|
||||||
|
def.id = QStringLiteral("default");
|
||||||
|
def.name = QStringLiteral("Default");
|
||||||
|
m_profiles.insert(def.id, def);
|
||||||
|
saveProfiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::saveProfiles() const
|
||||||
|
{
|
||||||
|
const QString path = profilesPath();
|
||||||
|
QDir().mkpath(QFileInfo(path).absolutePath());
|
||||||
|
|
||||||
|
QJsonArray profiles;
|
||||||
|
for (const Profile &p : m_profiles) {
|
||||||
|
profiles.push_back(QJsonObject::fromVariantMap(p.toVariantMap()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject assignments;
|
||||||
|
for (auto it = m_controllerKeyToProfileId.constBegin(); it != m_controllerKeyToProfileId.constEnd(); ++it) {
|
||||||
|
assignments.insert(it.key(), it.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
root.insert(QStringLiteral("profiles"), profiles);
|
||||||
|
root.insert(QStringLiteral("assignments"), assignments);
|
||||||
|
|
||||||
|
QFile f(path);
|
||||||
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.write(QJsonDocument(root).toJson());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputDaemon::Profile InputDaemon::profileById(const QString &profileId) const
|
||||||
|
{
|
||||||
|
auto it = m_profiles.constFind(profileId);
|
||||||
|
if (it == m_profiles.constEnd()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return it.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList InputDaemon::ListProfiles() const
|
||||||
|
{
|
||||||
|
QVariantList out;
|
||||||
|
out.reserve(m_profiles.size());
|
||||||
|
|
||||||
|
for (const Profile &p : m_profiles) {
|
||||||
|
out.push_back(p.toVariantMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::GetProfile(const QString &profileId) const
|
||||||
|
{
|
||||||
|
const Profile p = profileById(profileId);
|
||||||
|
return p.toVariantMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::CreateProfile(const QString &name)
|
||||||
|
{
|
||||||
|
const QString trimmed = name.trimmed();
|
||||||
|
if (trimmed.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString id = QString::number(QDateTime::currentMSecsSinceEpoch(), 16);
|
||||||
|
|
||||||
|
Profile p;
|
||||||
|
p.id = QStringLiteral("p_") + id;
|
||||||
|
p.name = trimmed;
|
||||||
|
|
||||||
|
if (!p.isValid()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
m_profiles.insert(p.id, p);
|
||||||
|
saveProfiles();
|
||||||
|
Q_EMIT ProfilesChanged();
|
||||||
|
|
||||||
|
return p.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::DeleteProfile(const QString &profileId)
|
||||||
|
{
|
||||||
|
if (profileId == QLatin1String("default")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_profiles.contains(profileId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_profiles.remove(profileId);
|
||||||
|
|
||||||
|
for (auto it = m_controllerKeyToProfileId.begin(); it != m_controllerKeyToProfileId.end();) {
|
||||||
|
if (it.value() == profileId) {
|
||||||
|
it = m_controllerKeyToProfileId.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveProfiles();
|
||||||
|
Q_EMIT ProfilesChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::controllerKeyForProfile(const Controller &c) const
|
||||||
|
{
|
||||||
|
if (!c.guid.isEmpty() && c.vendorId > 0 && c.productId > 0) {
|
||||||
|
return QStringLiteral("guid:%1:%2:%3").arg(c.guid).arg(c.vendorId).arg(c.productId);
|
||||||
|
}
|
||||||
|
if (!c.guid.isEmpty()) {
|
||||||
|
return QStringLiteral("guid:%1").arg(c.guid);
|
||||||
|
}
|
||||||
|
if (!c.path.isEmpty()) {
|
||||||
|
return QStringLiteral("path:%1").arg(c.path);
|
||||||
|
}
|
||||||
|
return c.controllerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputDaemon::GetActiveProfile(const QString &controllerId) const
|
||||||
|
{
|
||||||
|
for (const Controller &c : m_controllers) {
|
||||||
|
if (c.controllerId == controllerId) {
|
||||||
|
const QString key = controllerKeyForProfile(c);
|
||||||
|
return m_controllerKeyToProfileId.value(key, QStringLiteral("default"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QStringLiteral("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::SetActiveProfile(const QString &controllerId, const QString &profileId)
|
||||||
|
{
|
||||||
|
if (!m_profiles.contains(profileId)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Controller &c : m_controllers) {
|
||||||
|
if (c.controllerId == controllerId) {
|
||||||
|
const QString key = controllerKeyForProfile(c);
|
||||||
|
m_controllerKeyToProfileId.insert(key, profileId);
|
||||||
|
saveProfiles();
|
||||||
|
Q_EMIT ControllerProfileChanged(controllerId, profileId);
|
||||||
|
Q_EMIT ControllerChanged(controllerToVariantMap(c));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::controllerToVariantMap(const Controller &c) const
|
||||||
|
{
|
||||||
|
QVariantMap out;
|
||||||
|
out.insert(QStringLiteral("controllerId"), c.controllerId);
|
||||||
|
out.insert(QStringLiteral("instanceId"), c.instanceId);
|
||||||
|
out.insert(QStringLiteral("guid"), c.guid);
|
||||||
|
out.insert(QStringLiteral("name"), c.name);
|
||||||
|
out.insert(QStringLiteral("path"), c.path);
|
||||||
|
out.insert(QStringLiteral("vendorId"), c.vendorId);
|
||||||
|
out.insert(QStringLiteral("productId"), c.productId);
|
||||||
|
out.insert(QStringLiteral("type"), c.type);
|
||||||
|
out.insert(QStringLiteral("connectionState"), c.connectionState);
|
||||||
|
|
||||||
|
out.insert(QStringLiteral("batteryState"), c.batteryState);
|
||||||
|
out.insert(QStringLiteral("batteryStateName"), powerStateToString(static_cast<SDL_PowerState>(c.batteryState)));
|
||||||
|
out.insert(QStringLiteral("batteryPercent"), c.batteryPercent);
|
||||||
|
|
||||||
|
const QString activeId = m_controllerKeyToProfileId.value(controllerKeyForProfile(c), QStringLiteral("default"));
|
||||||
|
out.insert(QStringLiteral("activeProfileId"), activeId);
|
||||||
|
out.insert(QStringLiteral("activeProfileName"), profileById(activeId).name);
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList InputDaemon::ListControllers() const
|
||||||
|
{
|
||||||
|
QVariantList out;
|
||||||
|
out.reserve(m_controllers.size());
|
||||||
|
|
||||||
|
for (const Controller &c : m_controllers) {
|
||||||
|
out.push_back(controllerToVariantMap(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap InputDaemon::GetController(const QString &controllerId) const
|
||||||
|
{
|
||||||
|
for (const Controller &c : m_controllers) {
|
||||||
|
if (c.controllerId == controllerId) {
|
||||||
|
return controllerToVariantMap(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputDaemon::Rescan()
|
||||||
|
{
|
||||||
|
openExistingControllers();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::openExistingControllers()
|
||||||
|
{
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
std::unique_ptr<SDL_JoystickID[], decltype(&SDL_free)> ids(SDL_GetGamepads(&count), SDL_free);
|
||||||
|
if (!ids || count <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
const int instanceId = static_cast<int>(ids[i]);
|
||||||
|
if (!m_controllers.contains(instanceId)) {
|
||||||
|
openController(instanceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::refreshControllerPower(Controller &c)
|
||||||
|
{
|
||||||
|
if (!c.pad) {
|
||||||
|
c.batteryState = static_cast<int>(SDL_POWERSTATE_UNKNOWN);
|
||||||
|
c.batteryPercent = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int percent = -1;
|
||||||
|
const SDL_PowerState st = SDL_GetGamepadPowerInfo(c.pad, &percent);
|
||||||
|
c.batteryState = static_cast<int>(st);
|
||||||
|
c.batteryPercent = percent;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::openController(int instanceId)
|
||||||
|
{
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_controllers.contains(instanceId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Gamepad *pad = SDL_OpenGamepad(static_cast<SDL_JoystickID>(instanceId));
|
||||||
|
if (!pad) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Controller c;
|
||||||
|
c.pad = pad;
|
||||||
|
c.instanceId = instanceId;
|
||||||
|
c.controllerId = QStringLiteral("sdl:%1").arg(instanceId);
|
||||||
|
|
||||||
|
c.guid = guidToString(SDL_GetGamepadGUIDForID(static_cast<SDL_JoystickID>(instanceId)));
|
||||||
|
|
||||||
|
const char *name = SDL_GetGamepadName(pad);
|
||||||
|
if (name) {
|
||||||
|
c.name = QString::fromUtf8(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *path = SDL_GetGamepadPath(pad);
|
||||||
|
if (path) {
|
||||||
|
c.path = QString::fromUtf8(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.vendorId = SDL_GetGamepadVendor(pad);
|
||||||
|
c.productId = SDL_GetGamepadProduct(pad);
|
||||||
|
c.type = gamepadTypeToString(SDL_GetGamepadType(pad));
|
||||||
|
c.connectionState = connectionStateToString(SDL_GetGamepadConnectionState(pad));
|
||||||
|
|
||||||
|
refreshControllerPower(c);
|
||||||
|
|
||||||
|
m_controllers.insert(instanceId, c);
|
||||||
|
Q_EMIT ControllerAdded(controllerToVariantMap(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::closeController(int instanceId)
|
||||||
|
{
|
||||||
|
auto it = m_controllers.find(instanceId);
|
||||||
|
if (it == m_controllers.end()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString controllerId = it.value().controllerId;
|
||||||
|
|
||||||
|
if (it.value().pad) {
|
||||||
|
SDL_CloseGamepad(it.value().pad);
|
||||||
|
it.value().pad = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_controllers.erase(it);
|
||||||
|
Q_EMIT ControllerRemoved(controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputDaemon::pollEvents()
|
||||||
|
{
|
||||||
|
if (!m_sdlInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Event e;
|
||||||
|
bool anyChanged = false;
|
||||||
|
|
||||||
|
while (SDL_PollEvent(&e)) {
|
||||||
|
switch (e.type) {
|
||||||
|
case SDL_EVENT_GAMEPAD_ADDED:
|
||||||
|
openController(static_cast<int>(e.gdevice.which));
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_GAMEPAD_REMOVED:
|
||||||
|
closeController(static_cast<int>(e.gdevice.which));
|
||||||
|
break;
|
||||||
|
case SDL_EVENT_JOYSTICK_BATTERY_UPDATED: {
|
||||||
|
const int instanceId = static_cast<int>(e.jbattery.which);
|
||||||
|
auto it = m_controllers.find(instanceId);
|
||||||
|
if (it == m_controllers.end()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
it.value().batteryState = static_cast<int>(e.jbattery.state);
|
||||||
|
it.value().batteryPercent = e.jbattery.percent;
|
||||||
|
Q_EMIT ControllerChanged(controllerToVariantMap(it.value()));
|
||||||
|
anyChanged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
|
||||||
|
if (m_lastPowerRefreshMs <= 0 || (nowMs - m_lastPowerRefreshMs) > 5000) {
|
||||||
|
m_lastPowerRefreshMs = nowMs;
|
||||||
|
for (auto it = m_controllers.begin(); it != m_controllers.end(); ++it) {
|
||||||
|
const int oldPercent = it.value().batteryPercent;
|
||||||
|
const int oldState = it.value().batteryState;
|
||||||
|
refreshControllerPower(it.value());
|
||||||
|
if (it.value().batteryPercent != oldPercent || it.value().batteryState != oldState) {
|
||||||
|
Q_EMIT ControllerChanged(controllerToVariantMap(it.value()));
|
||||||
|
anyChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_UNUSED(anyChanged)
|
||||||
|
}
|
||||||
104
src/input/inputdaemon.h
Normal file
104
src/input/inputdaemon.h
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QVariantList>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
struct SDL_Gamepad;
|
||||||
|
|
||||||
|
class InputDaemon : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_CLASSINFO("D-Bus Interface", "org.kde.ALaKarte.Input1")
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit InputDaemon(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
bool init();
|
||||||
|
|
||||||
|
public Q_SLOTS:
|
||||||
|
QString Ping() const;
|
||||||
|
void Version(uint &major, uint &minor) const;
|
||||||
|
QVariantMap GetCapabilities() const;
|
||||||
|
|
||||||
|
QVariantList ListControllers() const;
|
||||||
|
QVariantMap GetController(const QString &controllerId) const;
|
||||||
|
bool Rescan();
|
||||||
|
|
||||||
|
QVariantList ListProfiles() const;
|
||||||
|
QVariantMap GetProfile(const QString &profileId) const;
|
||||||
|
QString CreateProfile(const QString &name);
|
||||||
|
bool DeleteProfile(const QString &profileId);
|
||||||
|
|
||||||
|
bool SetActiveProfile(const QString &controllerId, const QString &profileId);
|
||||||
|
QString GetActiveProfile(const QString &controllerId) const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void ControllerAdded(const QVariantMap &controller);
|
||||||
|
void ControllerRemoved(const QString &controllerId);
|
||||||
|
void ControllerChanged(const QVariantMap &controller);
|
||||||
|
|
||||||
|
void ProfilesChanged();
|
||||||
|
void ControllerProfileChanged(const QString &controllerId, const QString &profileId);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Controller {
|
||||||
|
SDL_Gamepad *pad = nullptr;
|
||||||
|
int instanceId = 0;
|
||||||
|
QString controllerId;
|
||||||
|
QString guid;
|
||||||
|
QString name;
|
||||||
|
QString path;
|
||||||
|
uint vendorId = 0;
|
||||||
|
uint productId = 0;
|
||||||
|
QString type;
|
||||||
|
QString connectionState;
|
||||||
|
int batteryState = 0;
|
||||||
|
int batteryPercent = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Profile {
|
||||||
|
QString id;
|
||||||
|
QString name;
|
||||||
|
QVariantMap mapping;
|
||||||
|
|
||||||
|
QVariantMap toVariantMap() const;
|
||||||
|
static Profile fromVariantMap(const QVariantMap &map);
|
||||||
|
bool isValid() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool initSdl();
|
||||||
|
void shutdownSdl();
|
||||||
|
|
||||||
|
void pollEvents();
|
||||||
|
|
||||||
|
void openExistingControllers();
|
||||||
|
void openController(int instanceId);
|
||||||
|
void closeController(int instanceId);
|
||||||
|
|
||||||
|
void refreshControllerPower(Controller &c);
|
||||||
|
|
||||||
|
QString controllerKeyForProfile(const Controller &c) const;
|
||||||
|
QVariantMap controllerToVariantMap(const Controller &c) const;
|
||||||
|
|
||||||
|
QString profilesPath() const;
|
||||||
|
void loadProfiles();
|
||||||
|
bool saveProfiles() const;
|
||||||
|
|
||||||
|
Profile profileById(const QString &profileId) const;
|
||||||
|
|
||||||
|
QHash<int, Controller> m_controllers;
|
||||||
|
|
||||||
|
QHash<QString, Profile> m_profiles;
|
||||||
|
QHash<QString, QString> m_controllerKeyToProfileId;
|
||||||
|
|
||||||
|
bool m_sdlInitialized = false;
|
||||||
|
QTimer m_pollTimer;
|
||||||
|
qint64 m_lastPowerRefreshMs = 0;
|
||||||
|
};
|
||||||
19
src/input/main.cpp
Normal file
19
src/input/main.cpp
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
|
||||||
|
#include "inputdaemon.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
QCoreApplication::setApplicationName(QStringLiteral("alakarte"));
|
||||||
|
|
||||||
|
InputDaemon daemon;
|
||||||
|
if (!daemon.init()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
9
src/input/systemd/org.kde.ALaKarte.Input1.service.in
Normal file
9
src/input/systemd/org.kde.ALaKarte.Input1.service.in
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
[Unit]
|
||||||
|
Description=A-La-Karte Input Service
|
||||||
|
PartOf=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=@CMAKE_INSTALL_PREFIX@/@KDE_INSTALL_BINDIR@/alakarte-inputd
|
||||||
|
Type=dbus
|
||||||
|
BusName=org.kde.ALaKarte.Input1
|
||||||
|
Restart=no
|
||||||
354
src/inputserviceclient.cpp
Normal file
354
src/inputserviceclient.cpp
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include "inputserviceclient.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
|
#include <QDBusError>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDBusServiceWatcher>
|
||||||
|
#include <QDBusVariant>
|
||||||
|
|
||||||
|
static const QString kInputService = QStringLiteral("org.kde.ALaKarte.Input1");
|
||||||
|
static const QString kInputPath = QStringLiteral("/org/kde/ALaKarte/Input1");
|
||||||
|
static const QString kInputInterface = QStringLiteral("org.kde.ALaKarte.Input1");
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
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<QVariantMap>()) {
|
||||||
|
return v.toMap();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap normalizeMap(QVariantMap m)
|
||||||
|
{
|
||||||
|
for (auto it = m.begin(); it != m.end(); ++it) {
|
||||||
|
it.value() = unwrapDbusVariant(it.value());
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputServiceClient::InputServiceClient(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (bus.isConnected()) {
|
||||||
|
m_watcher = new QDBusServiceWatcher(kInputService, bus, QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this);
|
||||||
|
connect(m_watcher, &QDBusServiceWatcher::serviceRegistered, this, [this]() {
|
||||||
|
setAvailable(true);
|
||||||
|
connectSignals();
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
connect(m_watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this]() {
|
||||||
|
setAvailable(false);
|
||||||
|
m_controllers.clear();
|
||||||
|
m_profiles.clear();
|
||||||
|
Q_EMIT controllersChanged();
|
||||||
|
Q_EMIT profilesChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (QCoreApplication::instance()) {
|
||||||
|
connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() {
|
||||||
|
m_connectedSignals = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureService();
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
InputServiceClient::~InputServiceClient() = default;
|
||||||
|
|
||||||
|
bool InputServiceClient::available() const
|
||||||
|
{
|
||||||
|
return m_available;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputServiceClient::lastError() const
|
||||||
|
{
|
||||||
|
return m_lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList InputServiceClient::controllers() const
|
||||||
|
{
|
||||||
|
return m_controllers;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList InputServiceClient::profiles() const
|
||||||
|
{
|
||||||
|
return m_profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::setAvailable(bool available)
|
||||||
|
{
|
||||||
|
if (m_available == available) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_available = available;
|
||||||
|
Q_EMIT availableChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::setLastError(const QString &error)
|
||||||
|
{
|
||||||
|
if (m_lastError == error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_lastError = error;
|
||||||
|
Q_EMIT lastErrorChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::ensureService()
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected() || !bus.interface()) {
|
||||||
|
setAvailable(false);
|
||||||
|
setLastError(QStringLiteral("session bus not available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bus.interface()->isServiceRegistered(kInputService)) {
|
||||||
|
setAvailable(true);
|
||||||
|
connectSignals();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool activatable = false;
|
||||||
|
{
|
||||||
|
const QDBusReply<QStringList> names = bus.interface()->activatableServiceNames();
|
||||||
|
if (names.isValid()) {
|
||||||
|
activatable = names.value().contains(kInputService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDBusReply<void> reply = bus.interface()->startService(kInputService);
|
||||||
|
if (reply.isValid() || bus.interface()->isServiceRegistered(kInputService)) {
|
||||||
|
setAvailable(true);
|
||||||
|
connectSignals();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvailable(false);
|
||||||
|
if (activatable) {
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
} else {
|
||||||
|
setLastError(QStringLiteral("Input service is not available (missing DBus activation for org.kde.ALaKarte.Input1)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::connectSignals()
|
||||||
|
{
|
||||||
|
if (m_connectedSignals) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool ok1 = bus.connect(kInputService, kInputPath, kInputInterface, QStringLiteral("ControllerAdded"), this, SLOT(onControllerAdded(QVariantMap)));
|
||||||
|
|
||||||
|
const bool ok2 = bus.connect(kInputService, kInputPath, kInputInterface, QStringLiteral("ControllerChanged"), this, SLOT(onControllerChanged(QVariantMap)));
|
||||||
|
|
||||||
|
const bool ok3 = bus.connect(kInputService, kInputPath, kInputInterface, QStringLiteral("ControllerRemoved"), this, SLOT(onControllerRemoved(QString)));
|
||||||
|
|
||||||
|
const bool ok4 = bus.connect(kInputService, kInputPath, kInputInterface, QStringLiteral("ProfilesChanged"), this, SLOT(onProfilesChanged()));
|
||||||
|
|
||||||
|
m_connectedSignals = ok1 && ok2 && ok3 && ok4;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::onControllerAdded(QVariantMap controller)
|
||||||
|
{
|
||||||
|
upsertController(normalizeMap(controller));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::onControllerChanged(QVariantMap controller)
|
||||||
|
{
|
||||||
|
upsertController(normalizeMap(controller));
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::onControllerRemoved(const QString &controllerId)
|
||||||
|
{
|
||||||
|
removeControllerById(controllerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::onProfilesChanged()
|
||||||
|
{
|
||||||
|
refreshProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::upsertController(const QVariantMap &controller)
|
||||||
|
{
|
||||||
|
const QString id = controller.value(QStringLiteral("controllerId")).toString();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_controllers.size(); ++i) {
|
||||||
|
const QVariantMap existing = m_controllers.at(i).toMap();
|
||||||
|
if (existing.value(QStringLiteral("controllerId")).toString() == id) {
|
||||||
|
m_controllers[i] = controller;
|
||||||
|
Q_EMIT controllersChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_controllers.push_back(controller);
|
||||||
|
Q_EMIT controllersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::removeControllerById(const QString &controllerId)
|
||||||
|
{
|
||||||
|
if (controllerId.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_controllers.size(); ++i) {
|
||||||
|
const QVariantMap existing = m_controllers.at(i).toMap();
|
||||||
|
if (existing.value(QStringLiteral("controllerId")).toString() == controllerId) {
|
||||||
|
m_controllers.removeAt(i);
|
||||||
|
Q_EMIT controllersChanged();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::refresh()
|
||||||
|
{
|
||||||
|
refreshControllers();
|
||||||
|
refreshProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::refreshControllers()
|
||||||
|
{
|
||||||
|
ensureService();
|
||||||
|
if (!m_available) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
||||||
|
i.setTimeout(2000);
|
||||||
|
|
||||||
|
const QDBusReply<bool> rescanReply = i.call(QStringLiteral("Rescan"));
|
||||||
|
if (!rescanReply.isValid()) {
|
||||||
|
setLastError(rescanReply.error().message());
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDBusReply<QVariantList> reply = i.call(QStringLiteral("ListControllers"));
|
||||||
|
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_controllers.clear();
|
||||||
|
const QVariantList list = reply.value();
|
||||||
|
m_controllers.reserve(list.size());
|
||||||
|
for (const QVariant &v : list) {
|
||||||
|
m_controllers.push_back(unwrapVariantMap(v));
|
||||||
|
}
|
||||||
|
Q_EMIT controllersChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputServiceClient::refreshProfiles()
|
||||||
|
{
|
||||||
|
ensureService();
|
||||||
|
if (!m_available) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
||||||
|
i.setTimeout(2000);
|
||||||
|
const QDBusReply<QVariantList> reply = i.call(QStringLiteral("ListProfiles"));
|
||||||
|
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_profiles.clear();
|
||||||
|
const QVariantList list = reply.value();
|
||||||
|
m_profiles.reserve(list.size());
|
||||||
|
for (const QVariant &v : list) {
|
||||||
|
m_profiles.push_back(unwrapVariantMap(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT profilesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputServiceClient::setActiveProfile(const QString &controllerId, const QString &profileId)
|
||||||
|
{
|
||||||
|
ensureService();
|
||||||
|
if (!m_available) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
||||||
|
i.setTimeout(2000);
|
||||||
|
const QDBusReply<bool> reply = i.call(QStringLiteral("SetActiveProfile"), controllerId, profileId);
|
||||||
|
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString InputServiceClient::createProfile(const QString &name)
|
||||||
|
{
|
||||||
|
ensureService();
|
||||||
|
if (!m_available) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
||||||
|
i.setTimeout(2000);
|
||||||
|
const QDBusReply<QString> reply = i.call(QStringLiteral("CreateProfile"), name);
|
||||||
|
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshProfiles();
|
||||||
|
return reply.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputServiceClient::deleteProfile(const QString &profileId)
|
||||||
|
{
|
||||||
|
ensureService();
|
||||||
|
if (!m_available) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface i(kInputService, kInputPath, kInputInterface, QDBusConnection::sessionBus());
|
||||||
|
i.setTimeout(2000);
|
||||||
|
const QDBusReply<bool> reply = i.call(QStringLiteral("DeleteProfile"), profileId);
|
||||||
|
|
||||||
|
if (!reply.isValid()) {
|
||||||
|
setLastError(reply.error().message());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshProfiles();
|
||||||
|
refreshControllers();
|
||||||
|
return reply.value();
|
||||||
|
}
|
||||||
68
src/inputserviceclient.h
Normal file
68
src/inputserviceclient.h
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariantList>
|
||||||
|
#include <QVariantMap>
|
||||||
|
|
||||||
|
class QDBusServiceWatcher;
|
||||||
|
|
||||||
|
class InputServiceClient : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(bool available READ available NOTIFY availableChanged)
|
||||||
|
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||||
|
Q_PROPERTY(QVariantList controllers READ controllers NOTIFY controllersChanged)
|
||||||
|
Q_PROPERTY(QVariantList profiles READ profiles NOTIFY profilesChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit InputServiceClient(QObject *parent = nullptr);
|
||||||
|
~InputServiceClient() override;
|
||||||
|
|
||||||
|
bool available() const;
|
||||||
|
QString lastError() const;
|
||||||
|
|
||||||
|
QVariantList controllers() const;
|
||||||
|
QVariantList profiles() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void refresh();
|
||||||
|
Q_INVOKABLE void refreshControllers();
|
||||||
|
Q_INVOKABLE void refreshProfiles();
|
||||||
|
|
||||||
|
Q_INVOKABLE bool setActiveProfile(const QString &controllerId, const QString &profileId);
|
||||||
|
Q_INVOKABLE QString createProfile(const QString &name);
|
||||||
|
Q_INVOKABLE bool deleteProfile(const QString &profileId);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void availableChanged();
|
||||||
|
void lastErrorChanged();
|
||||||
|
void controllersChanged();
|
||||||
|
void profilesChanged();
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void onControllerAdded(QVariantMap controller);
|
||||||
|
void onControllerChanged(QVariantMap controller);
|
||||||
|
void onControllerRemoved(const QString &controllerId);
|
||||||
|
void onProfilesChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ensureService();
|
||||||
|
void connectSignals();
|
||||||
|
void setAvailable(bool available);
|
||||||
|
void setLastError(const QString &error);
|
||||||
|
|
||||||
|
void upsertController(const QVariantMap &controller);
|
||||||
|
void removeControllerById(const QString &controllerId);
|
||||||
|
|
||||||
|
bool m_available = false;
|
||||||
|
QString m_lastError;
|
||||||
|
|
||||||
|
QVariantList m_controllers;
|
||||||
|
QVariantList m_profiles;
|
||||||
|
|
||||||
|
QDBusServiceWatcher *m_watcher = nullptr;
|
||||||
|
bool m_connectedSignals = false;
|
||||||
|
};
|
||||||
|
|
@ -185,18 +185,6 @@ void AlakarteRunner::run(const KRunner::RunnerContext &context, const KRunner::Q
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: daemon unavailable or no game ID
|
|
||||||
if (command.startsWith(QLatin1String("steam://")) || command.startsWith(QLatin1String("lutris:"))) {
|
|
||||||
QProcess::startDetached(QStringLiteral("xdg-open"), {command});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QStringList args = QProcess::splitCommand(command);
|
|
||||||
if (!args.isEmpty()) {
|
|
||||||
QString program = args.takeFirst();
|
|
||||||
QProcess::startDetached(program, args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "alakarterunner.moc"
|
#include "alakarterunner.moc"
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -4,10 +4,12 @@
|
||||||
#include "runnermanagerdaemon.h"
|
#include "runnermanagerdaemon.h"
|
||||||
|
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
|
#include <QDBusArgument>
|
||||||
#include <QDBusConnection>
|
#include <QDBusConnection>
|
||||||
#include <QDBusError>
|
#include <QDBusError>
|
||||||
#include <QDBusMessage>
|
#include <QDBusMessage>
|
||||||
#include <QDBusReply>
|
#include <QDBusReply>
|
||||||
|
#include <QDBusVariant>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QDirIterator>
|
#include <QDirIterator>
|
||||||
#include <QEventLoop>
|
#include <QEventLoop>
|
||||||
|
|
@ -32,6 +34,52 @@
|
||||||
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 QVariant unwrapDbusVariant(QVariant v)
|
||||||
|
{
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusVariant>()) {
|
||||||
|
v = v.value<QDBusVariant>().variant();
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap unwrapVariantMap(QVariant v)
|
||||||
|
{
|
||||||
|
v = unwrapDbusVariant(v);
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
return qdbus_cast<QVariantMap>(arg);
|
||||||
|
}
|
||||||
|
if (v.canConvert<QVariantMap>()) {
|
||||||
|
return v.toMap();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QStringList unwrapStringList(QVariant v)
|
||||||
|
{
|
||||||
|
v = unwrapDbusVariant(v);
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
return qdbus_cast<QStringList>(arg);
|
||||||
|
}
|
||||||
|
if (v.canConvert<QStringList>()) {
|
||||||
|
return v.toStringList();
|
||||||
|
}
|
||||||
|
if (v.canConvert<QVariantList>()) {
|
||||||
|
const QVariantList list = v.toList();
|
||||||
|
QStringList out;
|
||||||
|
out.reserve(list.size());
|
||||||
|
for (const QVariant &item : list) {
|
||||||
|
const QString s = item.toString();
|
||||||
|
if (!s.isEmpty()) {
|
||||||
|
out.push_back(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
static QString stableIdForPath(const QString &prefix, const QString &path)
|
static QString stableIdForPath(const QString &prefix, const QString &path)
|
||||||
{
|
{
|
||||||
const QByteArray digest = QCryptographicHash::hash(path.toUtf8(), QCryptographicHash::Sha1).toHex();
|
const QByteArray digest = QCryptographicHash::hash(path.toUtf8(), QCryptographicHash::Sha1).toHex();
|
||||||
|
|
@ -46,6 +94,7 @@ RunnerManagerDaemon::RunnerManagerDaemon(QObject *parent)
|
||||||
bool RunnerManagerDaemon::init()
|
bool RunnerManagerDaemon::init()
|
||||||
{
|
{
|
||||||
loadRegistry();
|
loadRegistry();
|
||||||
|
loadGameProfiles();
|
||||||
m_discovered = discoverRunners();
|
m_discovered = discoverRunners();
|
||||||
|
|
||||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
|
@ -181,6 +230,76 @@ bool RunnerManagerDaemon::saveRegistry() const
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString RunnerManagerDaemon::gameProfilesPath() const
|
||||||
|
{
|
||||||
|
const QString base = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||||
|
return base + QStringLiteral("/runner-game-profiles.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerDaemon::loadGameProfiles()
|
||||||
|
{
|
||||||
|
m_gameProfiles.clear();
|
||||||
|
|
||||||
|
QFile f(gameProfilesPath());
|
||||||
|
if (!f.open(QIODevice::ReadOnly)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
|
||||||
|
if (!doc.isObject()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonArray arr = doc.object().value(QStringLiteral("profiles")).toArray();
|
||||||
|
for (const QJsonValue &v : arr) {
|
||||||
|
if (!v.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QVariantMap map = v.toObject().toVariantMap();
|
||||||
|
const QString gameId = map.value(QStringLiteral("gameId")).toString();
|
||||||
|
if (gameId.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
m_gameProfiles.insert(gameId, map);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RunnerManagerDaemon::saveGameProfiles() const
|
||||||
|
{
|
||||||
|
const QString path = gameProfilesPath();
|
||||||
|
QDir().mkpath(QFileInfo(path).absolutePath());
|
||||||
|
|
||||||
|
QJsonArray arr;
|
||||||
|
for (auto it = m_gameProfiles.constBegin(); it != m_gameProfiles.constEnd(); ++it) {
|
||||||
|
QVariantMap profile = it.value();
|
||||||
|
if (!profile.contains(QStringLiteral("gameId"))) {
|
||||||
|
profile.insert(QStringLiteral("gameId"), it.key());
|
||||||
|
}
|
||||||
|
arr.push_back(QJsonObject::fromVariantMap(profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root;
|
||||||
|
root.insert(QStringLiteral("profiles"), arr);
|
||||||
|
|
||||||
|
QFile f(path);
|
||||||
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
f.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap RunnerManagerDaemon::gameProfileForGameId(const QString &gameId) const
|
||||||
|
{
|
||||||
|
const QString id = gameId.trimmed();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const auto it = m_gameProfiles.constFind(id);
|
||||||
|
return it == m_gameProfiles.constEnd() ? QVariantMap{} : it.value();
|
||||||
|
}
|
||||||
|
|
||||||
QStringList RunnerManagerDaemon::steamCandidateRoots()
|
QStringList RunnerManagerDaemon::steamCandidateRoots()
|
||||||
{
|
{
|
||||||
const QString home = QDir::homePath();
|
const QString home = QDir::homePath();
|
||||||
|
|
@ -998,14 +1117,192 @@ QVariantMap RunnerManagerDaemon::DeletePrefix(const QVariantMap &spec)
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantMap RunnerManagerDaemon::GetGameProfile(const QString &gameId) const
|
||||||
|
{
|
||||||
|
QVariantMap out;
|
||||||
|
out.insert(QStringLiteral("ok"), false);
|
||||||
|
|
||||||
|
const QString id = gameId.trimmed();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
out.insert(QStringLiteral("error"), QStringLiteral("missing gameId"));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap profile = gameProfileForGameId(id);
|
||||||
|
out.insert(QStringLiteral("ok"), true);
|
||||||
|
out.insert(QStringLiteral("profile"), profile);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList RunnerManagerDaemon::ListGameProfiles() const
|
||||||
|
{
|
||||||
|
QVariantList out;
|
||||||
|
out.reserve(m_gameProfiles.size());
|
||||||
|
for (auto it = m_gameProfiles.constBegin(); it != m_gameProfiles.constEnd(); ++it) {
|
||||||
|
QVariantMap profile = it.value();
|
||||||
|
if (!profile.contains(QStringLiteral("gameId"))) {
|
||||||
|
profile.insert(QStringLiteral("gameId"), it.key());
|
||||||
|
}
|
||||||
|
out.push_back(profile);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap RunnerManagerDaemon::SetGameProfile(const QVariantMap &spec)
|
||||||
|
{
|
||||||
|
QVariantMap out;
|
||||||
|
out.insert(QStringLiteral("ok"), false);
|
||||||
|
|
||||||
|
const QString gameId = spec.value(QStringLiteral("gameId")).toString().trimmed();
|
||||||
|
if (gameId.isEmpty()) {
|
||||||
|
out.insert(QStringLiteral("error"), QStringLiteral("missing gameId"));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap profile;
|
||||||
|
profile.insert(QStringLiteral("gameId"), gameId);
|
||||||
|
|
||||||
|
auto takeStringIfPresent = [&](const QString &key) {
|
||||||
|
if (!spec.contains(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QString v = spec.value(key).toString().trimmed();
|
||||||
|
if (!v.isEmpty()) {
|
||||||
|
profile.insert(key, v);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
takeStringIfPresent(QStringLiteral("runnerId"));
|
||||||
|
takeStringIfPresent(QStringLiteral("runner"));
|
||||||
|
takeStringIfPresent(QStringLiteral("runnerPath"));
|
||||||
|
takeStringIfPresent(QStringLiteral("prefixPath"));
|
||||||
|
|
||||||
|
if (spec.contains(QStringLiteral("dllOverrides"))) {
|
||||||
|
const QVariantMap raw = unwrapVariantMap(spec.value(QStringLiteral("dllOverrides")));
|
||||||
|
QVariantMap dllOverrides;
|
||||||
|
for (auto it = raw.constBegin(); it != raw.constEnd(); ++it) {
|
||||||
|
const QString key = it.key().trimmed();
|
||||||
|
if (key.isEmpty() || key.contains(QLatin1Char('=')) || key.contains(QLatin1Char(';'))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString value = it.value().toString().trimmed();
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dllOverrides.insert(key, value);
|
||||||
|
}
|
||||||
|
if (!dllOverrides.isEmpty()) {
|
||||||
|
profile.insert(QStringLiteral("dllOverrides"), dllOverrides);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec.contains(QStringLiteral("envOverrides"))) {
|
||||||
|
const QVariantMap rawEnv = unwrapVariantMap(spec.value(QStringLiteral("envOverrides")));
|
||||||
|
QVariantMap env;
|
||||||
|
for (auto it = rawEnv.constBegin(); it != rawEnv.constEnd(); ++it) {
|
||||||
|
const QString key = it.key();
|
||||||
|
if (key.isEmpty() || key.contains(QLatin1Char('='))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
env.insert(key, it.value().toString());
|
||||||
|
}
|
||||||
|
if (!env.isEmpty()) {
|
||||||
|
profile.insert(QStringLiteral("envOverrides"), env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec.contains(QStringLiteral("extraArgs"))) {
|
||||||
|
const QStringList raw = unwrapStringList(spec.value(QStringLiteral("extraArgs")));
|
||||||
|
QStringList extraArgs;
|
||||||
|
extraArgs.reserve(raw.size());
|
||||||
|
for (const QString &a : raw) {
|
||||||
|
const QString trimmed = a.trimmed();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
extraArgs.push_back(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!extraArgs.isEmpty()) {
|
||||||
|
profile.insert(QStringLiteral("extraArgs"), extraArgs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (profile.size() <= 1) {
|
||||||
|
out.insert(QStringLiteral("error"), QStringLiteral("empty profile"));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_gameProfiles.insert(gameId, profile);
|
||||||
|
if (!saveGameProfiles()) {
|
||||||
|
out.insert(QStringLiteral("error"), QStringLiteral("failed to save game profiles"));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT GameProfilesChanged();
|
||||||
|
out.insert(QStringLiteral("ok"), true);
|
||||||
|
out.insert(QStringLiteral("profile"), profile);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap RunnerManagerDaemon::ClearGameProfile(const QString &gameId)
|
||||||
|
{
|
||||||
|
QVariantMap out;
|
||||||
|
out.insert(QStringLiteral("ok"), false);
|
||||||
|
|
||||||
|
const QString id = gameId.trimmed();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
out.insert(QStringLiteral("error"), QStringLiteral("missing gameId"));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_gameProfiles.contains(id)) {
|
||||||
|
out.insert(QStringLiteral("ok"), true);
|
||||||
|
out.insert(QStringLiteral("gameId"), id);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_gameProfiles.remove(id);
|
||||||
|
if (!saveGameProfiles()) {
|
||||||
|
out.insert(QStringLiteral("error"), QStringLiteral("failed to save game profiles"));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT GameProfilesChanged();
|
||||||
|
out.insert(QStringLiteral("ok"), true);
|
||||||
|
out.insert(QStringLiteral("gameId"), id);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
QVariantMap RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const
|
QVariantMap RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const
|
||||||
{
|
{
|
||||||
QVariantMap out;
|
QVariantMap out;
|
||||||
out.insert(QStringLiteral("ok"), false);
|
out.insert(QStringLiteral("ok"), false);
|
||||||
|
|
||||||
const QString runnerId = spec.value(QStringLiteral("runnerId")).toString();
|
const QString gameId = spec.value(QStringLiteral("gameId")).toString();
|
||||||
QString runner = spec.value(QStringLiteral("runner")).toString();
|
|
||||||
QString runnerPath = spec.value(QStringLiteral("runnerPath")).toString();
|
const bool useGameProfile = spec.contains(QStringLiteral("useGameProfile")) ? spec.value(QStringLiteral("useGameProfile")).toBool() : false;
|
||||||
|
|
||||||
|
QVariantMap profile;
|
||||||
|
if (useGameProfile && !gameId.isEmpty()) {
|
||||||
|
profile = gameProfileForGameId(gameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString specRunnerId = spec.value(QStringLiteral("runnerId")).toString();
|
||||||
|
const QString specRunner = spec.value(QStringLiteral("runner")).toString();
|
||||||
|
const QString specRunnerPath = spec.value(QStringLiteral("runnerPath")).toString();
|
||||||
|
|
||||||
|
QString runnerId = specRunnerId;
|
||||||
|
QString runner = specRunner;
|
||||||
|
QString runnerPath = specRunnerPath;
|
||||||
|
|
||||||
|
if (runnerId.isEmpty() && profile.contains(QStringLiteral("runnerId"))) {
|
||||||
|
runnerId = profile.value(QStringLiteral("runnerId")).toString();
|
||||||
|
}
|
||||||
|
if (runner.isEmpty() && profile.contains(QStringLiteral("runner"))) {
|
||||||
|
runner = profile.value(QStringLiteral("runner")).toString();
|
||||||
|
}
|
||||||
|
if (runnerPath.isEmpty() && profile.contains(QStringLiteral("runnerPath"))) {
|
||||||
|
runnerPath = profile.value(QStringLiteral("runnerPath")).toString();
|
||||||
|
}
|
||||||
|
|
||||||
if (!runnerId.isEmpty()) {
|
if (!runnerId.isEmpty()) {
|
||||||
const RunnerInfo info = runnerById(runnerId);
|
const RunnerInfo info = runnerById(runnerId);
|
||||||
|
|
@ -1017,17 +1314,84 @@ QVariantMap RunnerManagerDaemon::ResolveLaunch(const QVariantMap &spec) const
|
||||||
runnerPath = info.path;
|
runnerPath = info.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString gameId = spec.value(QStringLiteral("gameId")).toString();
|
|
||||||
const QString program = spec.value(QStringLiteral("program")).toString();
|
const QString program = spec.value(QStringLiteral("program")).toString();
|
||||||
const QStringList args = spec.value(QStringLiteral("args")).toStringList();
|
QStringList args = unwrapStringList(spec.value(QStringLiteral("args")));
|
||||||
QString prefixPath = spec.value(QStringLiteral("prefixPath")).toString();
|
QString prefixPath = spec.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
if (prefixPath.isEmpty() && profile.contains(QStringLiteral("prefixPath"))) {
|
||||||
|
prefixPath = profile.value(QStringLiteral("prefixPath")).toString();
|
||||||
|
}
|
||||||
|
|
||||||
if (program.isEmpty()) {
|
if (program.isEmpty()) {
|
||||||
out.insert(QStringLiteral("error"), QStringLiteral("missing program"));
|
out.insert(QStringLiteral("error"), QStringLiteral("missing program"));
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVariantMap effectiveEnv = spec.value(QStringLiteral("envOverrides")).toMap();
|
{
|
||||||
|
const QStringList rawProfileExtraArgs = unwrapStringList(profile.value(QStringLiteral("extraArgs")));
|
||||||
|
for (const QString &a : rawProfileExtraArgs) {
|
||||||
|
const QString trimmed = a.trimmed();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
args.append(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList rawSpecExtraArgs = unwrapStringList(spec.value(QStringLiteral("extraArgs")));
|
||||||
|
for (const QString &a : rawSpecExtraArgs) {
|
||||||
|
const QString trimmed = a.trimmed();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
args.append(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap effectiveEnv;
|
||||||
|
if (profile.contains(QStringLiteral("envOverrides"))) {
|
||||||
|
effectiveEnv = profile.value(QStringLiteral("envOverrides")).toMap();
|
||||||
|
}
|
||||||
|
const QVariantMap specEnv = unwrapVariantMap(spec.value(QStringLiteral("envOverrides")));
|
||||||
|
for (auto it = specEnv.constBegin(); it != specEnv.constEnd(); ++it) {
|
||||||
|
effectiveEnv.insert(it.key(), it.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap dllOverrides;
|
||||||
|
if (profile.contains(QStringLiteral("dllOverrides"))) {
|
||||||
|
dllOverrides = profile.value(QStringLiteral("dllOverrides")).toMap();
|
||||||
|
}
|
||||||
|
const QVariantMap specDllOverrides = unwrapVariantMap(spec.value(QStringLiteral("dllOverrides")));
|
||||||
|
for (auto it = specDllOverrides.constBegin(); it != specDllOverrides.constEnd(); ++it) {
|
||||||
|
const QString key = it.key().trimmed();
|
||||||
|
if (key.isEmpty() || key.contains(QLatin1Char('=')) || key.contains(QLatin1Char(';'))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QString value = it.value().toString().trimmed();
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
dllOverrides.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dllOverrides.isEmpty() && !effectiveEnv.contains(QStringLiteral("WINEDLLOVERRIDES"))
|
||||||
|
&& ((runner == QLatin1String("wine")) || (runner == QLatin1String("proton")))) {
|
||||||
|
QStringList keys;
|
||||||
|
keys.reserve(dllOverrides.size());
|
||||||
|
for (auto it = dllOverrides.constBegin(); it != dllOverrides.constEnd(); ++it) {
|
||||||
|
keys.push_back(it.key());
|
||||||
|
}
|
||||||
|
std::sort(keys.begin(), keys.end());
|
||||||
|
|
||||||
|
QStringList parts;
|
||||||
|
parts.reserve(keys.size());
|
||||||
|
for (const QString &k : keys) {
|
||||||
|
const QString v = dllOverrides.value(k).toString().trimmed();
|
||||||
|
if (v.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parts.push_back(k + QLatin1Char('=') + v);
|
||||||
|
}
|
||||||
|
if (!parts.isEmpty()) {
|
||||||
|
effectiveEnv.insert(QStringLiteral("WINEDLLOVERRIDES"), parts.join(QStringLiteral(";")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((runner == QLatin1String("wine")) || (runner == QLatin1String("proton"))) {
|
if ((runner == QLatin1String("wine")) || (runner == QLatin1String("proton"))) {
|
||||||
if (prefixPath.isEmpty()) {
|
if (prefixPath.isEmpty()) {
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,18 @@ public Q_SLOTS:
|
||||||
QVariantMap DeletePrefix(const QVariantMap &spec);
|
QVariantMap DeletePrefix(const QVariantMap &spec);
|
||||||
QVariantMap ResolveLaunch(const QVariantMap &spec) const;
|
QVariantMap ResolveLaunch(const QVariantMap &spec) const;
|
||||||
|
|
||||||
|
QVariantMap GetGameProfile(const QString &gameId) const;
|
||||||
|
QVariantMap SetGameProfile(const QVariantMap &spec);
|
||||||
|
QVariantMap ClearGameProfile(const QString &gameId);
|
||||||
|
QVariantList ListGameProfiles() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void InstallStarted(const QString &installId, const QVariantMap &spec);
|
void InstallStarted(const QString &installId, const QVariantMap &spec);
|
||||||
void InstallProgress(const QString &installId, qint64 receivedBytes, qint64 totalBytes);
|
void InstallProgress(const QString &installId, qint64 receivedBytes, qint64 totalBytes);
|
||||||
void InstallFinished(const QString &installId, const QVariantMap &result);
|
void InstallFinished(const QString &installId, const QVariantMap &result);
|
||||||
|
|
||||||
|
void GameProfilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct RunnerInfo {
|
struct RunnerInfo {
|
||||||
QString id;
|
QString id;
|
||||||
|
|
@ -65,6 +72,11 @@ private:
|
||||||
void loadRegistry();
|
void loadRegistry();
|
||||||
bool saveRegistry() const;
|
bool saveRegistry() const;
|
||||||
|
|
||||||
|
QString gameProfilesPath() const;
|
||||||
|
void loadGameProfiles();
|
||||||
|
bool saveGameProfiles() const;
|
||||||
|
QVariantMap gameProfileForGameId(const QString &gameId) const;
|
||||||
|
|
||||||
QList<RunnerInfo> discoverRunners() const;
|
QList<RunnerInfo> discoverRunners() const;
|
||||||
QString defaultProtonExecutable() const;
|
QString defaultProtonExecutable() const;
|
||||||
|
|
||||||
|
|
@ -81,4 +93,6 @@ private:
|
||||||
|
|
||||||
QHash<QString, QPointer<QNetworkReply>> m_installReplies;
|
QHash<QString, QPointer<QNetworkReply>> m_installReplies;
|
||||||
QHash<QString, QPointer<QProcess>> m_installProcesses;
|
QHash<QString, QPointer<QProcess>> m_installProcesses;
|
||||||
|
|
||||||
|
QHash<QString, QVariantMap> m_gameProfiles;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
#include <QDBusPendingCall>
|
#include <QDBusPendingCall>
|
||||||
#include <QDBusPendingCallWatcher>
|
#include <QDBusPendingCallWatcher>
|
||||||
#include <QDBusReply>
|
#include <QDBusReply>
|
||||||
|
#include <QDBusVariant>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
|
@ -26,6 +27,52 @@ 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 const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
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.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
return qdbus_cast<QVariantMap>(arg);
|
||||||
|
}
|
||||||
|
if (v.canConvert<QVariantMap>()) {
|
||||||
|
return v.toMap();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap normalizeMap(QVariantMap m)
|
||||||
|
{
|
||||||
|
for (auto it = m.begin(); it != m.end(); ++it) {
|
||||||
|
it.value() = unwrapDbusVariant(it.value());
|
||||||
|
if (it.key() == QLatin1String("envOverrides")) {
|
||||||
|
it.value() = unwrapVariantMap(it.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap normalizeResultWithProfile(QVariantMap result)
|
||||||
|
{
|
||||||
|
result = normalizeMap(result);
|
||||||
|
if (result.contains(QStringLiteral("profile"))) {
|
||||||
|
QVariantMap profile = unwrapVariantMap(result.value(QStringLiteral("profile")));
|
||||||
|
profile = normalizeMap(profile);
|
||||||
|
result.insert(QStringLiteral("profile"), profile);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static QString stableIdForUrl(const QString &type, const QString &url)
|
static QString stableIdForUrl(const QString &type, const QString &url)
|
||||||
{
|
{
|
||||||
const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex();
|
const QByteArray digest = QCryptographicHash::hash(url.toUtf8(), QCryptographicHash::Sha1).toHex();
|
||||||
|
|
@ -80,6 +127,8 @@ RunnerManagerClient::RunnerManagerClient(QObject *parent)
|
||||||
SLOT(onInstallProgress(QString, qlonglong, qlonglong)));
|
SLOT(onInstallProgress(QString, qlonglong, qlonglong)));
|
||||||
|
|
||||||
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallFinished"), this, SLOT(onInstallFinished(QString, QVariantMap)));
|
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("InstallFinished"), this, SLOT(onInstallFinished(QString, QVariantMap)));
|
||||||
|
|
||||||
|
bus.connect(kRunnerService, kRunnerPath, kRunnerInterface, QStringLiteral("GameProfilesChanged"), this, SLOT(onGameProfilesChanged()));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (QCoreApplication::instance()) {
|
if (QCoreApplication::instance()) {
|
||||||
|
|
@ -89,6 +138,7 @@ RunnerManagerClient::RunnerManagerClient(QObject *parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshRunners();
|
refreshRunners();
|
||||||
|
refreshGameProfiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
RunnerManagerClient::~RunnerManagerClient()
|
RunnerManagerClient::~RunnerManagerClient()
|
||||||
|
|
@ -213,6 +263,11 @@ QVariantList RunnerManagerClient::runners() const
|
||||||
return m_runners;
|
return m_runners;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QVariantList RunnerManagerClient::gameProfiles() const
|
||||||
|
{
|
||||||
|
return m_gameProfiles;
|
||||||
|
}
|
||||||
|
|
||||||
static QString normalizeHex(QString s)
|
static QString normalizeHex(QString s)
|
||||||
{
|
{
|
||||||
s = s.trimmed().toLower();
|
s = s.trimmed().toLower();
|
||||||
|
|
@ -417,6 +472,11 @@ void RunnerManagerClient::onInstallFinished(const QString &installId, const QVar
|
||||||
refreshRunners();
|
refreshRunners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::onGameProfilesChanged()
|
||||||
|
{
|
||||||
|
refreshGameProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
void RunnerManagerClient::refreshRunners()
|
void RunnerManagerClient::refreshRunners()
|
||||||
{
|
{
|
||||||
ensureRunnerDaemon();
|
ensureRunnerDaemon();
|
||||||
|
|
@ -472,6 +532,179 @@ void RunnerManagerClient::refreshRunners()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::refreshGameProfiles()
|
||||||
|
{
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ListGameProfiles"));
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *w) {
|
||||||
|
QDBusPendingReply<QVariantList> reply = *w;
|
||||||
|
w->deleteLater();
|
||||||
|
if (reply.isError()) {
|
||||||
|
if (!m_refreshProfilesRetryPending && (reply.error().type() == QDBusError::ServiceUnknown || reply.error().type() == QDBusError::NoReply)) {
|
||||||
|
m_refreshProfilesRetryPending = true;
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
QTimer::singleShot(500, this, [this]() {
|
||||||
|
m_refreshProfilesRetryPending = false;
|
||||||
|
refreshGameProfiles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantList raw = reply.value();
|
||||||
|
|
||||||
|
QVariantList list;
|
||||||
|
list.reserve(raw.size());
|
||||||
|
for (const QVariant &v : raw) {
|
||||||
|
QVariantMap map;
|
||||||
|
if (v.metaType() == QMetaType::fromType<QVariantMap>()) {
|
||||||
|
map = v.toMap();
|
||||||
|
} else if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
map = qdbus_cast<QVariantMap>(arg);
|
||||||
|
} else if (v.canConvert<QVariantMap>()) {
|
||||||
|
map = v.toMap();
|
||||||
|
}
|
||||||
|
list.push_back(normalizeMap(map));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_gameProfiles == list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m_gameProfiles = list;
|
||||||
|
Q_EMIT gameProfilesChanged();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::requestGameProfile(const QString &gameId)
|
||||||
|
{
|
||||||
|
const QString id = gameId.trimmed();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
|
*callPtr = [this, id, attempts, callPtr]() {
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("GetGameProfile"), id);
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
w->deleteLater();
|
||||||
|
if (reply.isError()) {
|
||||||
|
const QDBusError::ErrorType t = reply.error().type();
|
||||||
|
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
|
||||||
|
(*attempts)++;
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
QTimer::singleShot(500, this, [callPtr]() {
|
||||||
|
(*callPtr)();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QVariantMap err;
|
||||||
|
err.insert(QStringLiteral("ok"), false);
|
||||||
|
err.insert(QStringLiteral("error"), reply.error().message());
|
||||||
|
Q_EMIT gameProfileFetched(id, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap result = normalizeResultWithProfile(reply.value());
|
||||||
|
Q_EMIT gameProfileFetched(id, result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
(*callPtr)();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::setGameProfile(const QVariantMap &spec)
|
||||||
|
{
|
||||||
|
const QString id = spec.value(QStringLiteral("gameId")).toString().trimmed();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
|
*callPtr = [this, spec, id, attempts, callPtr]() {
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("SetGameProfile"), spec);
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
w->deleteLater();
|
||||||
|
if (reply.isError()) {
|
||||||
|
const QDBusError::ErrorType t = reply.error().type();
|
||||||
|
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
|
||||||
|
(*attempts)++;
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
QTimer::singleShot(500, this, [callPtr]() {
|
||||||
|
(*callPtr)();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QVariantMap err;
|
||||||
|
err.insert(QStringLiteral("ok"), false);
|
||||||
|
err.insert(QStringLiteral("error"), reply.error().message());
|
||||||
|
Q_EMIT gameProfileSaved(id, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap result = normalizeResultWithProfile(reply.value());
|
||||||
|
Q_EMIT gameProfileSaved(id, result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
(*callPtr)();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RunnerManagerClient::clearGameProfile(const QString &gameId)
|
||||||
|
{
|
||||||
|
const QString id = gameId.trimmed();
|
||||||
|
if (id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
|
||||||
|
auto attempts = QSharedPointer<int>::create(0);
|
||||||
|
auto callPtr = QSharedPointer<std::function<void()>>::create();
|
||||||
|
*callPtr = [this, id, attempts, callPtr]() {
|
||||||
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, QDBusConnection::sessionBus());
|
||||||
|
QDBusPendingCall call = iface.asyncCall(QStringLiteral("ClearGameProfile"), id);
|
||||||
|
auto *watcher = new QDBusPendingCallWatcher(call, this);
|
||||||
|
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, id, attempts, callPtr](QDBusPendingCallWatcher *w) {
|
||||||
|
QDBusPendingReply<QVariantMap> reply = *w;
|
||||||
|
w->deleteLater();
|
||||||
|
if (reply.isError()) {
|
||||||
|
const QDBusError::ErrorType t = reply.error().type();
|
||||||
|
if ((*attempts) < 1 && (t == QDBusError::ServiceUnknown || t == QDBusError::NoReply)) {
|
||||||
|
(*attempts)++;
|
||||||
|
ensureRunnerDaemon();
|
||||||
|
QTimer::singleShot(500, this, [callPtr]() {
|
||||||
|
(*callPtr)();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QVariantMap err;
|
||||||
|
err.insert(QStringLiteral("ok"), false);
|
||||||
|
err.insert(QStringLiteral("error"), reply.error().message());
|
||||||
|
Q_EMIT gameProfileCleared(id, err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap result = normalizeMap(reply.value());
|
||||||
|
Q_EMIT gameProfileCleared(id, result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
(*callPtr)();
|
||||||
|
}
|
||||||
|
|
||||||
void RunnerManagerClient::uninstallRunner(const QString &runnerId)
|
void RunnerManagerClient::uninstallRunner(const QString &runnerId)
|
||||||
{
|
{
|
||||||
const QString id = runnerId.trimmed();
|
const QString id = runnerId.trimmed();
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ class RunnerManagerClient : public QObject
|
||||||
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
|
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
|
||||||
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
Q_PROPERTY(QString lastError READ lastError NOTIFY lastErrorChanged)
|
||||||
Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged)
|
Q_PROPERTY(QVariantList runners READ runners NOTIFY runnersChanged)
|
||||||
|
Q_PROPERTY(QVariantList gameProfiles READ gameProfiles NOTIFY gameProfilesChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit RunnerManagerClient(QObject *parent = nullptr);
|
explicit RunnerManagerClient(QObject *parent = nullptr);
|
||||||
|
|
@ -29,6 +30,7 @@ public:
|
||||||
QString status() const;
|
QString status() const;
|
||||||
QString lastError() const;
|
QString lastError() const;
|
||||||
QVariantList runners() const;
|
QVariantList runners() const;
|
||||||
|
QVariantList gameProfiles() const;
|
||||||
|
|
||||||
Q_INVOKABLE void installRunnerFromUrl(const QString &url,
|
Q_INVOKABLE void installRunnerFromUrl(const QString &url,
|
||||||
const QString &sha256 = QString(),
|
const QString &sha256 = QString(),
|
||||||
|
|
@ -41,6 +43,11 @@ public:
|
||||||
Q_INVOKABLE void refreshRunners();
|
Q_INVOKABLE void refreshRunners();
|
||||||
Q_INVOKABLE void uninstallRunner(const QString &runnerId);
|
Q_INVOKABLE void uninstallRunner(const QString &runnerId);
|
||||||
|
|
||||||
|
Q_INVOKABLE void refreshGameProfiles();
|
||||||
|
Q_INVOKABLE void requestGameProfile(const QString &gameId);
|
||||||
|
Q_INVOKABLE void setGameProfile(const QVariantMap &spec);
|
||||||
|
Q_INVOKABLE void clearGameProfile(const QString &gameId);
|
||||||
|
|
||||||
Q_INVOKABLE void ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath = QString());
|
Q_INVOKABLE void ensurePrefix(const QString &gameId, const QString &runner, const QString &prefixPath = QString());
|
||||||
|
|
||||||
Q_INVOKABLE void deletePrefix(const QString &gameId, const QString &prefixPath = QString());
|
Q_INVOKABLE void deletePrefix(const QString &gameId, const QString &prefixPath = QString());
|
||||||
|
|
@ -52,14 +59,21 @@ Q_SIGNALS:
|
||||||
void statusChanged();
|
void statusChanged();
|
||||||
void lastErrorChanged();
|
void lastErrorChanged();
|
||||||
void runnersChanged();
|
void runnersChanged();
|
||||||
|
void gameProfilesChanged();
|
||||||
void prefixEnsured(const QString &gameId, const QString &prefixPath);
|
void prefixEnsured(const QString &gameId, const QString &prefixPath);
|
||||||
void prefixDeleted(const QString &gameId, const QString &prefixPath);
|
void prefixDeleted(const QString &gameId, const QString &prefixPath);
|
||||||
|
|
||||||
|
void gameProfileFetched(const QString &gameId, const QVariantMap &result);
|
||||||
|
void gameProfileSaved(const QString &gameId, const QVariantMap &result);
|
||||||
|
void gameProfileCleared(const QString &gameId, const QVariantMap &result);
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void onInstallStarted(const QString &installId, const QVariantMap &spec);
|
void onInstallStarted(const QString &installId, const QVariantMap &spec);
|
||||||
void onInstallProgress(const QString &installId, qlonglong received, qlonglong total);
|
void onInstallProgress(const QString &installId, qlonglong received, qlonglong total);
|
||||||
void onInstallFinished(const QString &installId, const QVariantMap &result);
|
void onInstallFinished(const QString &installId, const QVariantMap &result);
|
||||||
|
|
||||||
|
void onGameProfilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void ensureRunnerDaemon();
|
void ensureRunnerDaemon();
|
||||||
void shutdownSpawnedRunnerDaemon();
|
void shutdownSpawnedRunnerDaemon();
|
||||||
|
|
@ -77,8 +91,10 @@ private:
|
||||||
QString m_lastError;
|
QString m_lastError;
|
||||||
|
|
||||||
QVariantList m_runners;
|
QVariantList m_runners;
|
||||||
|
QVariantList m_gameProfiles;
|
||||||
|
|
||||||
bool m_refreshRetryPending = false;
|
bool m_refreshRetryPending = false;
|
||||||
|
bool m_refreshProfilesRetryPending = false;
|
||||||
|
|
||||||
class QProcess *m_runnerdProcess = nullptr;
|
class QProcess *m_runnerdProcess = nullptr;
|
||||||
bool m_runnerdSpawnAttempted = false;
|
bool m_runnerdSpawnAttempted = false;
|
||||||
|
|
|
||||||
45
tests/CMakeLists.txt
Normal file
45
tests/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
find_package(Qt6 ${QT_MIN_VERSION} QUIET COMPONENTS Test)
|
||||||
|
|
||||||
|
if (NOT Qt6Test_FOUND OR NOT TARGET Qt6::Test)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_executable(alakarte_dbus_smoketest
|
||||||
|
dbus_smoketest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(alakarte_dbus_smoketest PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::DBus
|
||||||
|
Qt6::Test
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(NAME alakarte_dbus_smoketest
|
||||||
|
COMMAND alakarte_dbus_smoketest
|
||||||
|
--gamecenter $<TARGET_FILE:alakarte_gamecenter>
|
||||||
|
--runnerd $<TARGET_FILE:alakarte_runnerd>
|
||||||
|
--inputd $<TARGET_FILE:alakarte_inputd>
|
||||||
|
)
|
||||||
|
|
||||||
|
set_tests_properties(alakarte_dbus_smoketest PROPERTIES
|
||||||
|
TIMEOUT 30
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(alakarte_stop_launching_regression_test
|
||||||
|
stop_launching_regression_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(alakarte_stop_launching_regression_test PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::DBus
|
||||||
|
Qt6::Test
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(NAME alakarte_stop_launching_regression_test
|
||||||
|
COMMAND alakarte_stop_launching_regression_test
|
||||||
|
--gamecenter $<TARGET_FILE:alakarte_gamecenter>
|
||||||
|
)
|
||||||
|
|
||||||
|
set_tests_properties(alakarte_stop_launching_regression_test PROPERTIES
|
||||||
|
TIMEOUT 60
|
||||||
|
)
|
||||||
438
tests/dbus_smoketest.cpp
Normal file
438
tests/dbus_smoketest.cpp
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDBusArgument>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDBusVariant>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QProcessEnvironment>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static QString takeArgValue(const QStringList &args, const QString &key)
|
||||||
|
{
|
||||||
|
const int idx = args.indexOf(key);
|
||||||
|
if (idx < 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (idx + 1 >= args.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return args.at(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool waitForService(QDBusConnection &bus, const QString &service, int timeoutMs)
|
||||||
|
{
|
||||||
|
if (!bus.isConnected() || !bus.interface()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QElapsedTimer t;
|
||||||
|
t.start();
|
||||||
|
while (t.elapsed() < timeoutMs) {
|
||||||
|
if (bus.interface()->isServiceRegistered(service)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
QTest::qWait(50);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariant unwrapDbusVariant(QVariant v)
|
||||||
|
{
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusVariant>()) {
|
||||||
|
v = v.value<QDBusVariant>().variant();
|
||||||
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QVariantMap unwrapVariantMap(QVariant v)
|
||||||
|
{
|
||||||
|
v = unwrapDbusVariant(v);
|
||||||
|
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
|
||||||
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||||
|
return qdbus_cast<QVariantMap>(arg);
|
||||||
|
}
|
||||||
|
if (v.canConvert<QVariantMap>()) {
|
||||||
|
return v.toMap();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DbusSmokeTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void initTestCase();
|
||||||
|
void cleanupTestCase();
|
||||||
|
|
||||||
|
void pingGameCenter();
|
||||||
|
void pingRunner();
|
||||||
|
void pingInput();
|
||||||
|
void runnerResolveLaunchNative();
|
||||||
|
void runnerGameProfiles();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_gamecenterPath;
|
||||||
|
QString m_runnerdPath;
|
||||||
|
QString m_inputdPath;
|
||||||
|
|
||||||
|
QString m_dbusAddress;
|
||||||
|
QProcess m_dbusDaemon;
|
||||||
|
QProcess m_gamecenter;
|
||||||
|
QProcess m_runnerd;
|
||||||
|
QProcess m_inputd;
|
||||||
|
|
||||||
|
QString m_testXdgBase;
|
||||||
|
|
||||||
|
QDBusConnection m_bus = QDBusConnection::connectToBus(QString(), QStringLiteral("alakarte_test"));
|
||||||
|
|
||||||
|
bool startPrivateBus();
|
||||||
|
bool haveDbusDaemon() const;
|
||||||
|
void startDaemon(QProcess &p, const QString &program);
|
||||||
|
void stopProcess(QProcess &p);
|
||||||
|
};
|
||||||
|
|
||||||
|
bool DbusSmokeTest::haveDbusDaemon() const
|
||||||
|
{
|
||||||
|
return !QStandardPaths::findExecutable(QStringLiteral("dbus-daemon")).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DbusSmokeTest::startPrivateBus()
|
||||||
|
{
|
||||||
|
const QString dbusDaemon = QStandardPaths::findExecutable(QStringLiteral("dbus-daemon"));
|
||||||
|
if (dbusDaemon.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dbusDaemon.setProgram(dbusDaemon);
|
||||||
|
m_dbusDaemon.setArguments({QStringLiteral("--session"), QStringLiteral("--nofork"), QStringLiteral("--print-address=1")});
|
||||||
|
m_dbusDaemon.setProcessChannelMode(QProcess::MergedChannels);
|
||||||
|
m_dbusDaemon.start();
|
||||||
|
if (!m_dbusDaemon.waitForStarted(5000)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_dbusDaemon.waitForReadyRead(5000)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dbusAddress = QString::fromUtf8(m_dbusDaemon.readLine()).trimmed();
|
||||||
|
if (m_dbusAddress.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusConnection::disconnectFromBus(QStringLiteral("alakarte_test"));
|
||||||
|
m_bus = QDBusConnection::connectToBus(m_dbusAddress, QStringLiteral("alakarte_test"));
|
||||||
|
return m_bus.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbusSmokeTest::startDaemon(QProcess &p, const QString &program)
|
||||||
|
{
|
||||||
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||||
|
env.insert(QStringLiteral("DBUS_SESSION_BUS_ADDRESS"), m_dbusAddress);
|
||||||
|
if (!m_testXdgBase.isEmpty()) {
|
||||||
|
env.insert(QStringLiteral("XDG_DATA_HOME"), m_testXdgBase + QStringLiteral("/data"));
|
||||||
|
env.insert(QStringLiteral("XDG_CONFIG_HOME"), m_testXdgBase + QStringLiteral("/config"));
|
||||||
|
env.insert(QStringLiteral("XDG_CACHE_HOME"), m_testXdgBase + QStringLiteral("/cache"));
|
||||||
|
}
|
||||||
|
p.setProcessEnvironment(env);
|
||||||
|
p.setProgram(program);
|
||||||
|
p.setArguments({});
|
||||||
|
p.setProcessChannelMode(QProcess::MergedChannels);
|
||||||
|
p.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbusSmokeTest::stopProcess(QProcess &p)
|
||||||
|
{
|
||||||
|
if (p.state() == QProcess::NotRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.terminate();
|
||||||
|
if (!p.waitForFinished(3000)) {
|
||||||
|
p.kill();
|
||||||
|
p.waitForFinished(3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbusSmokeTest::initTestCase()
|
||||||
|
{
|
||||||
|
m_gamecenterPath = qApp ? qApp->property("alakarte_test_gamecenter").toString() : QString();
|
||||||
|
m_runnerdPath = qApp ? qApp->property("alakarte_test_runnerd").toString() : QString();
|
||||||
|
m_inputdPath = qApp ? qApp->property("alakarte_test_inputd").toString() : QString();
|
||||||
|
|
||||||
|
if (m_gamecenterPath.isEmpty() || m_runnerdPath.isEmpty() || m_inputdPath.isEmpty()) {
|
||||||
|
const QStringList args = QCoreApplication::arguments();
|
||||||
|
if (m_gamecenterPath.isEmpty()) {
|
||||||
|
m_gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter"));
|
||||||
|
}
|
||||||
|
if (m_runnerdPath.isEmpty()) {
|
||||||
|
m_runnerdPath = takeArgValue(args, QStringLiteral("--runnerd"));
|
||||||
|
}
|
||||||
|
if (m_inputdPath.isEmpty()) {
|
||||||
|
m_inputdPath = takeArgValue(args, QStringLiteral("--inputd"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QVERIFY2(!m_gamecenterPath.isEmpty(), "--gamecenter is required");
|
||||||
|
QVERIFY2(!m_runnerdPath.isEmpty(), "--runnerd is required");
|
||||||
|
QVERIFY2(!m_inputdPath.isEmpty(), "--inputd is required");
|
||||||
|
|
||||||
|
if (!haveDbusDaemon()) {
|
||||||
|
QSKIP("dbus-daemon not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!startPrivateBus()) {
|
||||||
|
QSKIP("failed to start private dbus session");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_testXdgBase = QDir::tempPath() + QStringLiteral("/alakarte-test-xdg-") + QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/data")));
|
||||||
|
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/config")));
|
||||||
|
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/cache")));
|
||||||
|
|
||||||
|
startDaemon(m_runnerd, m_runnerdPath);
|
||||||
|
QVERIFY(m_runnerd.waitForStarted(5000));
|
||||||
|
|
||||||
|
startDaemon(m_inputd, m_inputdPath);
|
||||||
|
QVERIFY(m_inputd.waitForStarted(5000));
|
||||||
|
|
||||||
|
startDaemon(m_gamecenter, m_gamecenterPath);
|
||||||
|
QVERIFY(m_gamecenter.waitForStarted(5000));
|
||||||
|
|
||||||
|
QVERIFY2(waitForService(m_bus, QStringLiteral("org.kde.ALaKarte.Runner1"), 5000), "Runner1 service did not appear on the bus");
|
||||||
|
QVERIFY2(waitForService(m_bus, QStringLiteral("org.kde.ALaKarte.Input1"), 5000), "Input1 service did not appear on the bus");
|
||||||
|
QVERIFY2(waitForService(m_bus, QStringLiteral("org.kde.GameCenter1"), 5000), "GameCenter1 service did not appear on the bus");
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbusSmokeTest::cleanupTestCase()
|
||||||
|
{
|
||||||
|
stopProcess(m_gamecenter);
|
||||||
|
stopProcess(m_inputd);
|
||||||
|
stopProcess(m_runnerd);
|
||||||
|
stopProcess(m_dbusDaemon);
|
||||||
|
QDBusConnection::disconnectFromBus(QStringLiteral("alakarte_test"));
|
||||||
|
|
||||||
|
if (!m_testXdgBase.isEmpty()) {
|
||||||
|
QDir(m_testXdgBase).removeRecursively();
|
||||||
|
m_testXdgBase.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbusSmokeTest::pingGameCenter()
|
||||||
|
{
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), m_bus);
|
||||||
|
QVERIFY(iface.isValid());
|
||||||
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
|
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
QCOMPARE(reply.value(), QStringLiteral("ok"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbusSmokeTest::pingRunner()
|
||||||
|
{
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
|
||||||
|
QStringLiteral("/org/kde/ALaKarte/Runner1"),
|
||||||
|
QStringLiteral("org.kde.ALaKarte.Runner1"),
|
||||||
|
m_bus);
|
||||||
|
QVERIFY(iface.isValid());
|
||||||
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
|
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
QCOMPARE(reply.value(), QStringLiteral("ok"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbusSmokeTest::pingInput()
|
||||||
|
{
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Input1"),
|
||||||
|
QStringLiteral("/org/kde/ALaKarte/Input1"),
|
||||||
|
QStringLiteral("org.kde.ALaKarte.Input1"),
|
||||||
|
m_bus);
|
||||||
|
QVERIFY(iface.isValid());
|
||||||
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
|
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
QCOMPARE(reply.value(), QStringLiteral("ok"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbusSmokeTest::runnerResolveLaunchNative()
|
||||||
|
{
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
|
||||||
|
QStringLiteral("/org/kde/ALaKarte/Runner1"),
|
||||||
|
QStringLiteral("org.kde.ALaKarte.Runner1"),
|
||||||
|
m_bus);
|
||||||
|
QVERIFY(iface.isValid());
|
||||||
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
|
||||||
|
spec.insert(QStringLiteral("args"), QStringList{});
|
||||||
|
|
||||||
|
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
|
||||||
|
const QVariantMap out = reply.value();
|
||||||
|
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
||||||
|
QCOMPARE(out.value(QStringLiteral("finalProgram")).toString(), QStringLiteral("/bin/true"));
|
||||||
|
QCOMPARE(out.value(QStringLiteral("finalArgs")).toStringList(), QStringList{});
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbusSmokeTest::runnerGameProfiles()
|
||||||
|
{
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
|
||||||
|
QStringLiteral("/org/kde/ALaKarte/Runner1"),
|
||||||
|
QStringLiteral("org.kde.ALaKarte.Runner1"),
|
||||||
|
m_bus);
|
||||||
|
QVERIFY(iface.isValid());
|
||||||
|
iface.setTimeout(2000);
|
||||||
|
|
||||||
|
const QString gameId = QStringLiteral("alakarte-test-game");
|
||||||
|
|
||||||
|
{
|
||||||
|
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ClearGameProfile"), gameId);
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("gameId"), gameId);
|
||||||
|
spec.insert(QStringLiteral("runner"), QStringLiteral("wine"));
|
||||||
|
spec.insert(QStringLiteral("runnerPath"), QStringLiteral("/bin/true"));
|
||||||
|
spec.insert(QStringLiteral("envOverrides"), QVariantMap{{QStringLiteral("ALAKARTE_TEST_VAR"), QStringLiteral("1")}});
|
||||||
|
spec.insert(QStringLiteral("extraArgs"), QStringList{QStringLiteral("--profile-arg")});
|
||||||
|
spec.insert(QStringLiteral("dllOverrides"),
|
||||||
|
QVariantMap{{QStringLiteral("d3d11"), QStringLiteral("native,builtin")}, {QStringLiteral("dxgi"), QStringLiteral("native,builtin")}});
|
||||||
|
|
||||||
|
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("SetGameProfile"), spec);
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
const QVariantMap out = reply.value();
|
||||||
|
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
||||||
|
|
||||||
|
const QVariantMap profile = unwrapVariantMap(out.value(QStringLiteral("profile")));
|
||||||
|
QCOMPARE(profile.value(QStringLiteral("gameId")).toString(), gameId);
|
||||||
|
const QVariantMap env = unwrapVariantMap(profile.value(QStringLiteral("envOverrides")));
|
||||||
|
QCOMPARE(env.value(QStringLiteral("ALAKARTE_TEST_VAR")).toString(), QStringLiteral("1"));
|
||||||
|
|
||||||
|
QCOMPARE(profile.value(QStringLiteral("extraArgs")).toStringList(), QStringList{QStringLiteral("--profile-arg")});
|
||||||
|
|
||||||
|
const QVariantMap dll = unwrapVariantMap(profile.value(QStringLiteral("dllOverrides")));
|
||||||
|
QCOMPARE(dll.value(QStringLiteral("d3d11")).toString(), QStringLiteral("native,builtin"));
|
||||||
|
QCOMPARE(dll.value(QStringLiteral("dxgi")).toString(), QStringLiteral("native,builtin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("GetGameProfile"), gameId);
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
const QVariantMap out = reply.value();
|
||||||
|
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
||||||
|
const QVariantMap profile = unwrapVariantMap(out.value(QStringLiteral("profile")));
|
||||||
|
QCOMPARE(profile.value(QStringLiteral("gameId")).toString(), gameId);
|
||||||
|
|
||||||
|
QCOMPARE(profile.value(QStringLiteral("extraArgs")).toStringList(), QStringList{QStringLiteral("--profile-arg")});
|
||||||
|
|
||||||
|
const QVariantMap dll = unwrapVariantMap(profile.value(QStringLiteral("dllOverrides")));
|
||||||
|
QCOMPARE(dll.value(QStringLiteral("d3d11")).toString(), QStringLiteral("native,builtin"));
|
||||||
|
QCOMPARE(dll.value(QStringLiteral("dxgi")).toString(), QStringLiteral("native,builtin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("gameId"), gameId);
|
||||||
|
spec.insert(QStringLiteral("useGameProfile"), true);
|
||||||
|
spec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
|
||||||
|
spec.insert(QStringLiteral("args"), QStringList{QStringLiteral("--base-arg")});
|
||||||
|
spec.insert(QStringLiteral("extraArgs"), QStringList{QStringLiteral("--spec-arg")});
|
||||||
|
|
||||||
|
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
const QVariantMap out = reply.value();
|
||||||
|
QVERIFY(out.value(QStringLiteral("ok")).toBool());
|
||||||
|
QCOMPARE(out.value(QStringLiteral("finalProgram")).toString(), QStringLiteral("/bin/true"));
|
||||||
|
const QStringList expectedFinalArgs = QStringList({
|
||||||
|
QStringLiteral("/bin/true"),
|
||||||
|
QStringLiteral("--base-arg"),
|
||||||
|
QStringLiteral("--profile-arg"),
|
||||||
|
QStringLiteral("--spec-arg"),
|
||||||
|
});
|
||||||
|
QCOMPARE(out.value(QStringLiteral("finalArgs")).toStringList(), expectedFinalArgs);
|
||||||
|
|
||||||
|
const QVariantMap env = unwrapVariantMap(out.value(QStringLiteral("effectiveEnv")));
|
||||||
|
QCOMPARE(env.value(QStringLiteral("ALAKARTE_TEST_VAR")).toString(), QStringLiteral("1"));
|
||||||
|
QCOMPARE(env.value(QStringLiteral("WINEDLLOVERRIDES")).toString(), QStringLiteral("d3d11=native,builtin;dxgi=native,builtin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListGameProfiles"));
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
const QVariantList list = reply.value();
|
||||||
|
bool found = false;
|
||||||
|
for (const QVariant &v : list) {
|
||||||
|
const QVariantMap m = unwrapVariantMap(v);
|
||||||
|
if (m.value(QStringLiteral("gameId")).toString() == gameId) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QVERIFY(found);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ClearGameProfile"), gameId);
|
||||||
|
QVERIFY(reply.isValid());
|
||||||
|
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
|
||||||
|
const QStringList args = app.arguments();
|
||||||
|
const QString gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter"));
|
||||||
|
const QString runnerdPath = takeArgValue(args, QStringLiteral("--runnerd"));
|
||||||
|
const QString inputdPath = takeArgValue(args, QStringLiteral("--inputd"));
|
||||||
|
|
||||||
|
app.setProperty("alakarte_test_gamecenter", gamecenterPath);
|
||||||
|
app.setProperty("alakarte_test_runnerd", runnerdPath);
|
||||||
|
app.setProperty("alakarte_test_inputd", inputdPath);
|
||||||
|
|
||||||
|
QStringList filtered;
|
||||||
|
filtered.reserve(args.size());
|
||||||
|
if (!args.isEmpty()) {
|
||||||
|
filtered.push_back(args.first());
|
||||||
|
}
|
||||||
|
for (int i = 1; i < args.size(); ++i) {
|
||||||
|
const QString a = args.at(i);
|
||||||
|
if (a == QLatin1String("--gamecenter") || a == QLatin1String("--runnerd")) {
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (a == QLatin1String("--inputd")) {
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filtered.push_back(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
DbusSmokeTest tc;
|
||||||
|
return QTest::qExec(&tc, filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "dbus_smoketest.moc"
|
||||||
630
tests/stop_launching_regression_test.cpp
Normal file
630
tests/stop_launching_regression_test.cpp
Normal file
|
|
@ -0,0 +1,630 @@
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QDBusConnection>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDBusVariant>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QElapsedTimer>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QProcessEnvironment>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QTemporaryDir>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
static QString takeArgValue(const QStringList &args, const QString &key)
|
||||||
|
{
|
||||||
|
const int idx = args.indexOf(key);
|
||||||
|
if (idx < 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (idx + 1 >= args.size()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return args.at(idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool waitForService(QDBusConnection &bus, const QString &service, int timeoutMs)
|
||||||
|
{
|
||||||
|
if (!bus.isConnected() || !bus.interface()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QElapsedTimer t;
|
||||||
|
t.start();
|
||||||
|
while (t.elapsed() < timeoutMs) {
|
||||||
|
if (bus.interface()->isServiceRegistered(service)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
QTest::qWait(50);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
|
||||||
|
{
|
||||||
|
QFile f(path);
|
||||||
|
if (!f.open(QIODevice::ReadOnly)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return f.read(maxSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool containsNullSeparatedEntry(const QByteArray &blob, const QByteArray &needle)
|
||||||
|
{
|
||||||
|
if (blob.isEmpty() || needle.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pos = -1;
|
||||||
|
while (true) {
|
||||||
|
pos = blob.indexOf(needle, pos + 1);
|
||||||
|
if (pos < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool startOk = (pos == 0) || (blob.at(pos - 1) == '\0');
|
||||||
|
const int endPos = pos + needle.size();
|
||||||
|
const bool endOk = (endPos == blob.size()) || (blob.at(endPos) == '\0');
|
||||||
|
if (startOk && endOk) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static QList<uint> pidsWithEnvironEntry(const QByteArray &needle)
|
||||||
|
{
|
||||||
|
QList<uint> out;
|
||||||
|
const QDir proc(QStringLiteral("/proc"));
|
||||||
|
const QStringList entries = proc.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
for (const QString &entry : entries) {
|
||||||
|
bool ok = false;
|
||||||
|
const uint pid = entry.toUInt(&ok);
|
||||||
|
if (!ok || pid <= 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray env = readProcFile(QStringLiteral("/proc/%1/environ").arg(pid));
|
||||||
|
if (containsNullSeparatedEntry(env, needle)) {
|
||||||
|
out.push_back(pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool waitForAnyEnvironEntry(const QByteArray &needle, int timeoutMs)
|
||||||
|
{
|
||||||
|
QElapsedTimer t;
|
||||||
|
t.start();
|
||||||
|
while (t.elapsed() < timeoutMs) {
|
||||||
|
if (!pidsWithEnvironEntry(needle).isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
QTest::qWait(50);
|
||||||
|
}
|
||||||
|
return !pidsWithEnvironEntry(needle).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool waitForNoEnvironEntry(const QByteArray &needle, int timeoutMs)
|
||||||
|
{
|
||||||
|
QElapsedTimer t;
|
||||||
|
t.start();
|
||||||
|
while (t.elapsed() < timeoutMs) {
|
||||||
|
if (pidsWithEnvironEntry(needle).isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
QTest::qWait(150);
|
||||||
|
}
|
||||||
|
return pidsWithEnvironEntry(needle).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void killPids(const QList<uint> &pids)
|
||||||
|
{
|
||||||
|
for (uint pid : pids) {
|
||||||
|
if (pid > 1) {
|
||||||
|
::kill(static_cast<pid_t>(pid), SIGKILL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void stopProcess(QProcess &p)
|
||||||
|
{
|
||||||
|
if (p.state() == QProcess::NotRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.terminate();
|
||||||
|
if (!p.waitForFinished(3000)) {
|
||||||
|
p.kill();
|
||||||
|
p.waitForFinished(3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool waitForUnitNotActive(const QString &unitName, int timeoutMs)
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
|
||||||
|
QStringLiteral("/org/freedesktop/systemd1"),
|
||||||
|
QStringLiteral("org.freedesktop.systemd1.Manager"),
|
||||||
|
bus);
|
||||||
|
if (!manager.isValid()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QElapsedTimer t;
|
||||||
|
t.start();
|
||||||
|
while (t.elapsed() < timeoutMs) {
|
||||||
|
const QDBusReply<QDBusObjectPath> unitReply = manager.call(QStringLiteral("GetUnit"), unitName);
|
||||||
|
if (!unitReply.isValid()) {
|
||||||
|
if (unitReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
QTest::qWait(100);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface props(QStringLiteral("org.freedesktop.systemd1"), unitReply.value().path(), QStringLiteral("org.freedesktop.DBus.Properties"), bus);
|
||||||
|
if (!props.isValid()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDBusReply<QVariant> stateReply =
|
||||||
|
props.call(QStringLiteral("Get"), QStringLiteral("org.freedesktop.systemd1.Unit"), QStringLiteral("ActiveState"));
|
||||||
|
if (!stateReply.isValid()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant v = stateReply.value();
|
||||||
|
if (v.canConvert<QDBusVariant>()) {
|
||||||
|
v = v.value<QDBusVariant>().variant();
|
||||||
|
}
|
||||||
|
const QString state = v.toString();
|
||||||
|
if (state != QLatin1String("active") && state != QLatin1String("activating") && state != QLatin1String("deactivating")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTest::qWait(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDBusReply<QDBusObjectPath> unitReply = manager.call(QStringLiteral("GetUnit"), unitName);
|
||||||
|
if (!unitReply.isValid() && unitReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StopLaunchingRegressionTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void initTestCase();
|
||||||
|
void cleanupTestCase();
|
||||||
|
|
||||||
|
void stopWhileLaunchingSteam();
|
||||||
|
void stopByGameIdWhileLaunchingSteam();
|
||||||
|
void stopWhileLaunchingLutris();
|
||||||
|
void stopByGameIdWhileLaunchingLutris();
|
||||||
|
|
||||||
|
void stopDirectLaunch();
|
||||||
|
void stopByGameIdDirectLaunch();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_gamecenterPath;
|
||||||
|
QProcess m_gamecenter;
|
||||||
|
QString m_testXdgBase;
|
||||||
|
QTemporaryDir m_stubBin;
|
||||||
|
QString m_markerValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
void StopLaunchingRegressionTest::initTestCase()
|
||||||
|
{
|
||||||
|
m_gamecenterPath = qApp->property("alakarte_test_gamecenter").toString();
|
||||||
|
if (m_gamecenterPath.isEmpty()) {
|
||||||
|
const QStringList args = QCoreApplication::arguments();
|
||||||
|
m_gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter"));
|
||||||
|
}
|
||||||
|
QVERIFY2(!m_gamecenterPath.isEmpty(), "--gamecenter is required");
|
||||||
|
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected() || !bus.interface()) {
|
||||||
|
QSKIP("session bus not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface systemd(QStringLiteral("org.freedesktop.systemd1"),
|
||||||
|
QStringLiteral("/org/freedesktop/systemd1"),
|
||||||
|
QStringLiteral("org.freedesktop.systemd1.Manager"),
|
||||||
|
bus);
|
||||||
|
if (!systemd.isValid()) {
|
||||||
|
QSKIP("systemd user manager not available on session bus");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bus.interface()->isServiceRegistered(QStringLiteral("org.kde.GameCenter1"))) {
|
||||||
|
QSKIP("org.kde.GameCenter1 already registered on the session bus");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_stubBin.isValid()) {
|
||||||
|
QSKIP("failed to create temporary directory for stub executables");
|
||||||
|
}
|
||||||
|
|
||||||
|
m_markerValue = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
||||||
|
killPids(pidsWithEnvironEntry(markerNeedle));
|
||||||
|
|
||||||
|
m_testXdgBase = QDir::tempPath() + QStringLiteral("/alakarte-stop-launching-test-xdg-") + QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/data")));
|
||||||
|
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/config")));
|
||||||
|
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/cache")));
|
||||||
|
|
||||||
|
const QString steamPath = m_stubBin.path() + QStringLiteral("/steam");
|
||||||
|
{
|
||||||
|
QFile steam(steamPath);
|
||||||
|
QVERIFY(steam.open(QIODevice::WriteOnly | QIODevice::Truncate));
|
||||||
|
const QByteArray script =
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"appid=\"\"\n"
|
||||||
|
"while [ $# -gt 0 ]; do\n"
|
||||||
|
" if [ \"$1\" = \"-applaunch\" ] && [ $# -ge 2 ]; then appid=\"$2\"; break; fi\n"
|
||||||
|
" shift\n"
|
||||||
|
"done\n"
|
||||||
|
"if [ -z \"$appid\" ]; then appid=\"123456\"; fi\n"
|
||||||
|
"marker=\"${ALAKARTE_TEST_MARKER_VALUE}\"\n"
|
||||||
|
"nohup sh -c \"sleep 1; env SteamAppId=$appid SteamGameId=$appid ALAKARTE_TEST_MARKER=$marker /bin/sleep 60\" >/dev/null 2>&1 &\n"
|
||||||
|
"exit 0\n";
|
||||||
|
QVERIFY(steam.write(script) == script.size());
|
||||||
|
}
|
||||||
|
QVERIFY(QFile::setPermissions(steamPath,
|
||||||
|
QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | QFileDevice::ReadGroup | QFileDevice::ExeGroup
|
||||||
|
| QFileDevice::ReadOther | QFileDevice::ExeOther));
|
||||||
|
|
||||||
|
const QString lutrisPath = m_stubBin.path() + QStringLiteral("/lutris");
|
||||||
|
{
|
||||||
|
QFile lutris(lutrisPath);
|
||||||
|
QVERIFY(lutris.open(QIODevice::WriteOnly | QIODevice::Truncate));
|
||||||
|
const QByteArray script =
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"id=\"\"\n"
|
||||||
|
"if [ $# -ge 1 ]; then\n"
|
||||||
|
" id=\"$1\"\n"
|
||||||
|
"fi\n"
|
||||||
|
"case \"$id\" in\n"
|
||||||
|
" lutris:rungameid/*) id=\"${id##*/}\" ;;\n"
|
||||||
|
" lutris:rungame/*) id=\"${id##*/}\" ;;\n"
|
||||||
|
"esac\n"
|
||||||
|
"if [ -z \"$id\" ]; then id=\"test-lutris\"; fi\n"
|
||||||
|
"marker=\"${ALAKARTE_TEST_MARKER_VALUE}\"\n"
|
||||||
|
"nohup sh -c \"sleep 1; env LUTRIS_GAME_SLUG=$id ALAKARTE_TEST_MARKER=$marker /bin/sleep 60\" >/dev/null 2>&1 &\n"
|
||||||
|
"exit 0\n";
|
||||||
|
QVERIFY(lutris.write(script) == script.size());
|
||||||
|
}
|
||||||
|
QVERIFY(QFile::setPermissions(lutrisPath,
|
||||||
|
QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | QFileDevice::ReadGroup | QFileDevice::ExeGroup
|
||||||
|
| QFileDevice::ReadOther | QFileDevice::ExeOther));
|
||||||
|
|
||||||
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
||||||
|
env.insert(QStringLiteral("PATH"), m_stubBin.path() + QStringLiteral(":") + env.value(QStringLiteral("PATH")));
|
||||||
|
env.insert(QStringLiteral("ALAKARTE_TEST_MARKER_VALUE"), m_markerValue);
|
||||||
|
env.insert(QStringLiteral("XDG_DATA_HOME"), m_testXdgBase + QStringLiteral("/data"));
|
||||||
|
env.insert(QStringLiteral("XDG_CONFIG_HOME"), m_testXdgBase + QStringLiteral("/config"));
|
||||||
|
env.insert(QStringLiteral("XDG_CACHE_HOME"), m_testXdgBase + QStringLiteral("/cache"));
|
||||||
|
|
||||||
|
m_gamecenter.setProcessEnvironment(env);
|
||||||
|
m_gamecenter.setProgram(m_gamecenterPath);
|
||||||
|
m_gamecenter.setArguments({});
|
||||||
|
m_gamecenter.setProcessChannelMode(QProcess::MergedChannels);
|
||||||
|
m_gamecenter.start();
|
||||||
|
QVERIFY(m_gamecenter.waitForStarted(5000));
|
||||||
|
|
||||||
|
QVERIFY2(waitForService(bus, QStringLiteral("org.kde.GameCenter1"), 5000), "GameCenter1 service did not appear on the session bus");
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopLaunchingRegressionTest::cleanupTestCase()
|
||||||
|
{
|
||||||
|
stopProcess(m_gamecenter);
|
||||||
|
|
||||||
|
if (!m_markerValue.isEmpty()) {
|
||||||
|
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
||||||
|
killPids(pidsWithEnvironEntry(markerNeedle));
|
||||||
|
m_markerValue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!m_testXdgBase.isEmpty()) {
|
||||||
|
QDir(m_testXdgBase).removeRecursively();
|
||||||
|
m_testXdgBase.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopLaunchingRegressionTest::stopWhileLaunchingSteam()
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
QSKIP("session bus not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
|
||||||
|
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
|
||||||
|
|
||||||
|
const QString appId = QStringLiteral("987654");
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("provider"), QStringLiteral("steam"));
|
||||||
|
spec.insert(QStringLiteral("command"), QStringLiteral("steam -applaunch %1").arg(appId));
|
||||||
|
spec.insert(QStringLiteral("gameId"), QStringLiteral("test-steam-%1").arg(appId));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
QDBusReply<void> stopReply = iface.call(QStringLiteral("Stop"), sessionId);
|
||||||
|
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
|
||||||
|
|
||||||
|
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
|
||||||
|
waitForAnyEnvironEntry(markerNeedle, 5000);
|
||||||
|
|
||||||
|
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
|
||||||
|
if (!cleaned) {
|
||||||
|
const QList<uint> pids = pidsWithEnvironEntry(markerNeedle);
|
||||||
|
killPids(pids);
|
||||||
|
}
|
||||||
|
QVERIFY2(cleaned, "marker process still alive after Stop() during Launching");
|
||||||
|
|
||||||
|
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() during Launching");
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingSteam()
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
QSKIP("session bus not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/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 appId = QStringLiteral("876543");
|
||||||
|
const QString gameId = QStringLiteral("test-steam-stop-by-gameid-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
|
||||||
|
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("provider"), QStringLiteral("steam"));
|
||||||
|
spec.insert(QStringLiteral("command"), QStringLiteral("steam -applaunch %1").arg(appId));
|
||||||
|
spec.insert(QStringLiteral("gameId"), gameId);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
QDBusReply<void> stopReply = iface.call(QStringLiteral("StopByGameId"), gameId);
|
||||||
|
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
|
||||||
|
|
||||||
|
waitForAnyEnvironEntry(markerNeedle, 5000);
|
||||||
|
|
||||||
|
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
|
||||||
|
if (!cleaned) {
|
||||||
|
const QList<uint> pids = pidsWithEnvironEntry(markerNeedle);
|
||||||
|
killPids(pids);
|
||||||
|
}
|
||||||
|
QVERIFY2(cleaned, "marker process still alive after StopByGameId() during Launching");
|
||||||
|
|
||||||
|
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after StopByGameId() during Launching");
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopLaunchingRegressionTest::stopWhileLaunchingLutris()
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
QSKIP("session bus not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/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 lutrisId = QStringLiteral("rungameid/%1").arg(QStringLiteral("lutris-test-1"));
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("provider"), QStringLiteral("lutris"));
|
||||||
|
spec.insert(QStringLiteral("command"), QStringLiteral("lutris lutris:%1").arg(lutrisId));
|
||||||
|
spec.insert(QStringLiteral("gameId"), QStringLiteral("test-lutris-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces)));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
QDBusReply<void> stopReply = iface.call(QStringLiteral("Stop"), sessionId);
|
||||||
|
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
|
||||||
|
|
||||||
|
waitForAnyEnvironEntry(markerNeedle, 5000);
|
||||||
|
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
|
||||||
|
if (!cleaned) {
|
||||||
|
killPids(pidsWithEnvironEntry(markerNeedle));
|
||||||
|
}
|
||||||
|
QVERIFY2(cleaned, "marker process still alive after Stop() during Launching (lutris)");
|
||||||
|
|
||||||
|
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() during Launching (lutris)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingLutris()
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
QSKIP("session bus not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/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 gameId = QStringLiteral("test-lutris-stop-by-gameid-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
|
||||||
|
QVariantMap spec;
|
||||||
|
spec.insert(QStringLiteral("provider"), QStringLiteral("lutris"));
|
||||||
|
spec.insert(QStringLiteral("command"), QStringLiteral("lutris lutris:rungameid/%1").arg(QStringLiteral("lutris-test-2")));
|
||||||
|
spec.insert(QStringLiteral("gameId"), gameId);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
QDBusReply<void> stopReply = iface.call(QStringLiteral("StopByGameId"), gameId);
|
||||||
|
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
|
||||||
|
|
||||||
|
waitForAnyEnvironEntry(markerNeedle, 5000);
|
||||||
|
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
|
||||||
|
if (!cleaned) {
|
||||||
|
killPids(pidsWithEnvironEntry(markerNeedle));
|
||||||
|
}
|
||||||
|
QVERIFY2(cleaned, "marker process still alive after StopByGameId() during Launching (lutris)");
|
||||||
|
|
||||||
|
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after StopByGameId() during Launching (lutris)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopLaunchingRegressionTest::stopDirectLaunch()
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
QSKIP("session bus not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/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 gameId = QStringLiteral("test-direct-stop-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
|
||||||
|
|
||||||
|
const QString envExe = QStandardPaths::findExecutable(QStringLiteral("env"));
|
||||||
|
if (envExe.isEmpty()) {
|
||||||
|
QSKIP("env executable not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
QDBusReply<void> stopReply = iface.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() (direct launch)");
|
||||||
|
|
||||||
|
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() (direct launch)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopLaunchingRegressionTest::stopByGameIdDirectLaunch()
|
||||||
|
{
|
||||||
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||||
|
if (!bus.isConnected()) {
|
||||||
|
QSKIP("session bus not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/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 gameId = QStringLiteral("test-direct-stop-by-gameid-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
|
||||||
|
|
||||||
|
const QString envExe = QStandardPaths::findExecutable(QStringLiteral("env"));
|
||||||
|
if (envExe.isEmpty()) {
|
||||||
|
QSKIP("env executable not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
QDBusReply<void> stopReply = iface.call(QStringLiteral("StopByGameId"), gameId);
|
||||||
|
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 StopByGameId() (direct launch)");
|
||||||
|
|
||||||
|
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after StopByGameId() (direct launch)");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
QCoreApplication app(argc, argv);
|
||||||
|
|
||||||
|
const QStringList args = app.arguments();
|
||||||
|
const QString gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter"));
|
||||||
|
app.setProperty("alakarte_test_gamecenter", gamecenterPath);
|
||||||
|
|
||||||
|
QStringList filtered;
|
||||||
|
filtered.reserve(args.size());
|
||||||
|
if (!args.isEmpty()) {
|
||||||
|
filtered.push_back(args.first());
|
||||||
|
}
|
||||||
|
for (int i = 1; i < args.size(); ++i) {
|
||||||
|
const QString a = args.at(i);
|
||||||
|
if (a == QLatin1String("--gamecenter")) {
|
||||||
|
++i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filtered.push_back(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
StopLaunchingRegressionTest tc;
|
||||||
|
return QTest::qExec(&tc, filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
#include "stop_launching_regression_test.moc"
|
||||||
Loading…
Reference in a new issue