mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
gamecenter: systemd scopes + Stop reliability
This commit is contained in:
parent
c7956eed8b
commit
fbd9311384
8 changed files with 364 additions and 123 deletions
|
|
@ -13,7 +13,10 @@ add_executable(alakarte_gamecenter
|
|||
|
||||
target_link_libraries(alakarte_gamecenter PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Concurrent
|
||||
Qt6::DBus
|
||||
KF6::CoreAddons
|
||||
KF6::DBusAddons
|
||||
)
|
||||
|
||||
set_target_properties(alakarte_gamecenter PROPERTIES
|
||||
|
|
@ -24,9 +27,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}/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
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
||||
|
|
@ -38,21 +38,6 @@ configure_file(systemd/org.kde.GameCenter1.service.in
|
|||
@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"
|
||||
DESTINATION ${KDE_INSTALL_DBUSSERVICEDIR}
|
||||
)
|
||||
|
|
@ -60,15 +45,3 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/dbus/org.kde.GameCenter1.service"
|
|||
install(FILES "${CMAKE_CURRENT_BINARY_DIR}/systemd/org.kde.GameCenter1.service"
|
||||
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 <signal.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#ifdef ALAKARTE_HAVE_KAUTH
|
||||
#include <KAuth/Action>
|
||||
#include <KAuth/ExecuteJob>
|
||||
#endif
|
||||
|
||||
#include <utility>
|
||||
|
||||
static const QString kRunnerService = QStringLiteral("org.kde.ALaKarte.Runner1");
|
||||
|
|
@ -43,6 +51,26 @@ static QString ensureScopeUnitName(const QString &unitName)
|
|||
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)
|
||||
{
|
||||
if (v.canConvert<QDBusVariant>()) {
|
||||
|
|
@ -236,6 +264,8 @@ private:
|
|||
|
||||
} // namespace
|
||||
|
||||
#if 0
|
||||
|
||||
static const QString kGameCenterService = QStringLiteral("org.kde.GameCenter1");
|
||||
static const QString kGameCenterPath = QStringLiteral("/org/kde/GameCenter1");
|
||||
static const QString kGameCenterInterface = QStringLiteral("org.kde.GameCenter1");
|
||||
|
|
@ -675,6 +705,8 @@ bool GameCenterSystemProxy::init()
|
|||
return d->init();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath)
|
||||
{
|
||||
if (unitName.isEmpty()) {
|
||||
|
|
@ -910,6 +942,18 @@ QVariantMap GameCenterDaemon::GetCapabilities() const
|
|||
QVariantMap caps;
|
||||
caps.insert(QStringLiteral("supportsSystemd"), m_systemd.isAvailable());
|
||||
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 steamAvailable = !QStandardPaths::findExecutable(QStringLiteral("steam")).isEmpty();
|
||||
const bool lutrisAvailable = !QStandardPaths::findExecutable(QStringLiteral("lutris")).isEmpty();
|
||||
|
|
@ -948,12 +992,44 @@ void GameCenterDaemon::SetPolicy(const QVariantMap &policy)
|
|||
}
|
||||
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
|
||||
{
|
||||
return {
|
||||
{QStringLiteral("maxConcurrent"), m_maxConcurrent},
|
||||
{QStringLiteral("powerProfile"), m_powerProfile},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1020,7 +1096,10 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
|
|||
v = v.value<QDBusVariant>().variant();
|
||||
}
|
||||
if (v.canConvert<QVariantMap>()) {
|
||||
return v.toMap();
|
||||
const QVariantMap asMap = v.toMap();
|
||||
if (!asMap.isEmpty()) {
|
||||
return asMap;
|
||||
}
|
||||
}
|
||||
if (v.canConvert<QDBusArgument>()) {
|
||||
const QDBusArgument arg = v.value<QDBusArgument>();
|
||||
|
|
@ -1033,9 +1112,24 @@ QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
|
|||
return converted;
|
||||
}
|
||||
|
||||
QVariantMap asMap = qdbus_cast<QVariantMap>(arg);
|
||||
if (!asMap.isEmpty()) {
|
||||
return asMap;
|
||||
QVariantMap converted;
|
||||
{
|
||||
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{};
|
||||
|
|
@ -1238,10 +1332,7 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
|
|||
|
||||
matcher = [appId]() -> QList<ProcessScanner::Match> {
|
||||
// Primary: look for SteamAppId=<appId> in environ
|
||||
QList<ProcessScanner::Match> results = ProcessScanner::findByEnvironment(QStringLiteral("SteamAppId"), appId);
|
||||
if (results.isEmpty()) {
|
||||
results = ProcessScanner::findByEnvironment(QStringLiteral("SteamGameId"), appId);
|
||||
}
|
||||
QList<ProcessScanner::Match> results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId);
|
||||
// Filter out the steam client itself
|
||||
QList<ProcessScanner::Match> filtered;
|
||||
for (const auto &m : std::as_const(results)) {
|
||||
|
|
@ -1330,6 +1421,34 @@ QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const Q
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
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"));
|
||||
|
||||
if (!it.value().unitName.isEmpty()) {
|
||||
m_systemd.stopUnit(it.value().unitName);
|
||||
unwatchSystemdUnit(it.value().unitName, it.value().unitPath);
|
||||
}
|
||||
|
||||
if (it.value().scanner) {
|
||||
it.value().scanner->cancel();
|
||||
it.value().scanner->deleteLater();
|
||||
|
|
@ -1408,27 +1532,49 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
|
|||
|
||||
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 QDBusReply<QDBusObjectPath> reply = m_systemd.startTransientScope(unitName, pids, description);
|
||||
if (!reply.isValid()) {
|
||||
const QVariantMap ctx = {
|
||||
{QStringLiteral("gameId"), it.value().gameId},
|
||||
{QStringLiteral("provider"), it.value().provider},
|
||||
{QStringLiteral("unit"), unitName},
|
||||
{QStringLiteral("error"), reply.error().message()},
|
||||
};
|
||||
QDBusReply<void> attachReply = m_systemd.attachProcessesToUnit(unitName, pids);
|
||||
if (!attachReply.isValid()) {
|
||||
if (attachReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
|
||||
const QDBusReply<QDBusObjectPath> startReply = m_systemd.startTransientScope(unitName, pids, description);
|
||||
if (!startReply.isValid()) {
|
||||
const QVariantMap ctx = {
|
||||
{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"));
|
||||
if (it.value().process) {
|
||||
it.value().process->deleteLater();
|
||||
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 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;
|
||||
|
|
@ -1446,11 +1592,25 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
|
|||
}
|
||||
|
||||
if (it.value().stopping) {
|
||||
m_systemd.stopUnit(it.value().unitName);
|
||||
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped"));
|
||||
Q_EMIT SessionRemoved(sessionId, finalState);
|
||||
const QDBusReply<QDBusObjectPath> stopReply = m_systemd.stopUnit(it.value().unitName);
|
||||
if (stopReply.isValid()) {
|
||||
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);
|
||||
m_sessions.erase(it);
|
||||
removeSessionInternal(sessionId, QStringLiteral("Stopped"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1534,12 +1694,6 @@ void GameCenterDaemon::Stop(const QString &sessionId)
|
|||
if (it.value().scanner) {
|
||||
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;
|
||||
it.value().process = nullptr;
|
||||
if (proc) {
|
||||
|
|
@ -1556,9 +1710,10 @@ void GameCenterDaemon::Stop(const QString &sessionId)
|
|||
}
|
||||
}
|
||||
|
||||
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped"));
|
||||
Q_EMIT SessionRemoved(sessionId, finalState);
|
||||
m_sessions.erase(it);
|
||||
if (!it.value().unitName.isEmpty()) {
|
||||
m_systemd.stopUnit(it.value().unitName);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1575,6 +1730,12 @@ void GameCenterDaemon::Stop(const QString &sessionId)
|
|||
|
||||
const QDBusReply<QDBusObjectPath> reply = m_systemd.stopUnit(it.value().unitName);
|
||||
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) {
|
||||
it.value().process->terminate();
|
||||
QTimer::singleShot(5000, it.value().process, [process = it.value().process]() {
|
||||
|
|
@ -1617,12 +1778,12 @@ QString GameCenterDaemon::sessionState(const Session &session)
|
|||
if (session.stopping) {
|
||||
return QStringLiteral("Stopping");
|
||||
}
|
||||
if (!session.unitName.isEmpty()) {
|
||||
return QStringLiteral("Running");
|
||||
}
|
||||
if (session.scanner) {
|
||||
return QStringLiteral("Launching");
|
||||
}
|
||||
if (!session.unitName.isEmpty()) {
|
||||
return QStringLiteral("Running");
|
||||
}
|
||||
if (session.process) {
|
||||
return session.process->state() == QProcess::Running ? QStringLiteral("Running") : QStringLiteral("Exited");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,23 +92,8 @@ private:
|
|||
SystemdUserManager m_systemd;
|
||||
QHash<QString, Session> m_sessions;
|
||||
int m_maxConcurrent = 0;
|
||||
QString m_powerProfile;
|
||||
QHash<QString, QString> m_unitPathToSessionId;
|
||||
QHash<QString, QString> m_unitNameToSessionId;
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,31 +5,37 @@
|
|||
#include <QCommandLineParser>
|
||||
#include <QCoreApplication>
|
||||
|
||||
#include <KDBusService>
|
||||
#include <KSignalHandler>
|
||||
|
||||
#include "gamecenterdaemon.h"
|
||||
|
||||
#include <signal.h>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
app.setOrganizationDomain(QStringLiteral("kde.org"));
|
||||
app.setApplicationName(QStringLiteral("alakarte_gamecenter"));
|
||||
|
||||
KDBusService service(KDBusService::Unique);
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription(QStringLiteral("A-La-Karte Game Center"));
|
||||
parser.addHelpOption();
|
||||
parser.addOption(QCommandLineOption(QStringLiteral("system"), QStringLiteral("Run as system bus proxy")));
|
||||
parser.process(app);
|
||||
|
||||
if (parser.isSet(QStringLiteral("system"))) {
|
||||
GameCenterSystemProxy proxy;
|
||||
if (!proxy.init()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
GameCenterDaemon daemon;
|
||||
if (!daemon.init()) {
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QtConcurrentRun>
|
||||
|
||||
ProcessScanner::ProcessScanner(QObject *parent)
|
||||
: QObject(parent)
|
||||
|
|
@ -15,21 +16,61 @@ ProcessScanner::ProcessScanner(QObject *parent)
|
|||
|
||||
connect(&m_deadline, &QTimer::timeout, this, [this]() {
|
||||
m_timer.stop();
|
||||
m_matcher = nullptr;
|
||||
++m_generation;
|
||||
m_scanInFlight = false;
|
||||
Q_EMIT timedOut();
|
||||
});
|
||||
|
||||
connect(&m_timer, &QTimer::timeout, this, [this]() {
|
||||
if (!m_matcher) {
|
||||
m_timer.stop();
|
||||
startScan();
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const QList<Match> results = m_matcher();
|
||||
|
||||
if (!results.isEmpty()) {
|
||||
m_timer.stop();
|
||||
m_deadline.stop();
|
||||
m_matcher = nullptr;
|
||||
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)
|
||||
|
|
@ -56,6 +97,28 @@ static QByteArray readEnviron(const QString &pidDir)
|
|||
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()
|
||||
{
|
||||
QList<uint> pids;
|
||||
|
|
@ -84,22 +147,7 @@ QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &ke
|
|||
continue;
|
||||
}
|
||||
|
||||
// environ entries are separated by null bytes
|
||||
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) {
|
||||
if (containsNullSeparatedEntry(env, needle)) {
|
||||
Match m;
|
||||
m.pid = pid;
|
||||
m.exe = readExeLink(pidDir);
|
||||
|
|
@ -111,6 +159,54 @@ QList<ProcessScanner::Match> ProcessScanner::findByEnvironment(const QString &ke
|
|||
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<Match> matches;
|
||||
|
|
@ -164,15 +260,10 @@ void ProcessScanner::pollUntilFound(std::function<QList<Match>()> matcher, int i
|
|||
cancel();
|
||||
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_timer.start(intervalMs);
|
||||
|
||||
startScan();
|
||||
}
|
||||
|
||||
void ProcessScanner::cancel()
|
||||
|
|
@ -180,4 +271,6 @@ void ProcessScanner::cancel()
|
|||
m_timer.stop();
|
||||
m_deadline.stop();
|
||||
m_matcher = nullptr;
|
||||
++m_generation;
|
||||
m_scanInFlight = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <QFutureWatcher>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QtGlobal>
|
||||
#include <functional>
|
||||
|
||||
class ProcessScanner : public QObject
|
||||
|
|
@ -25,6 +27,9 @@ public:
|
|||
// Find PIDs whose /proc/<pid>/environ contains key=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
|
||||
static QList<Match> findByCmdline(const QString &substring);
|
||||
|
||||
|
|
@ -42,7 +47,12 @@ Q_SIGNALS:
|
|||
void timedOut();
|
||||
|
||||
private:
|
||||
void startScan();
|
||||
|
||||
QTimer m_timer;
|
||||
QTimer m_deadline;
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ public:
|
|||
|
||||
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<SystemdUnitInfoList> listUnits();
|
||||
|
|
|
|||
Loading…
Reference in a new issue