mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-27 01:03:09 +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
|
target_link_libraries(alakarte_gamecenter PRIVATE
|
||||||
Qt6::Core
|
Qt6::Core
|
||||||
|
Qt6::Concurrent
|
||||||
Qt6::DBus
|
Qt6::DBus
|
||||||
|
KF6::CoreAddons
|
||||||
|
KF6::DBusAddons
|
||||||
)
|
)
|
||||||
|
|
||||||
set_target_properties(alakarte_gamecenter PROPERTIES
|
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}/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 +38,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 +45,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,16 +1532,19 @@ 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()) {
|
||||||
|
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 = {
|
const QVariantMap ctx = {
|
||||||
{QStringLiteral("gameId"), it.value().gameId},
|
{QStringLiteral("gameId"), it.value().gameId},
|
||||||
{QStringLiteral("provider"), it.value().provider},
|
{QStringLiteral("provider"), it.value().provider},
|
||||||
{QStringLiteral("unit"), unitName},
|
{QStringLiteral("unit"), unitName},
|
||||||
{QStringLiteral("error"), reply.error().message()},
|
{QStringLiteral("error"), startReply.error().message()},
|
||||||
};
|
};
|
||||||
|
|
||||||
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed"));
|
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Failed"));
|
||||||
|
|
@ -1430,6 +1557,25 @@ void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList
|
||||||
failLaunch(QStringLiteral("failed to create transient scope for monitored game"), ctx);
|
failLaunch(QStringLiteral("failed to create transient scope for monitored game"), ctx);
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
unwatchSystemdUnit(unitName, it.value().unitPath);
|
||||||
m_sessions.erase(it);
|
removeSessionInternal(sessionId, QStringLiteral("Stopped"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
terminatePids(pids);
|
||||||
|
QTimer::singleShot(5000, this, [pids]() {
|
||||||
|
killPids(pids);
|
||||||
|
});
|
||||||
|
|
||||||
|
unwatchSystemdUnit(unitName, it.value().unitPath);
|
||||||
|
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;
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]() {
|
||||||
|
startScan();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessScanner::startScan()
|
||||||
|
{
|
||||||
if (!m_matcher) {
|
if (!m_matcher) {
|
||||||
m_timer.stop();
|
m_timer.stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QList<Match> results = m_matcher();
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue