mirror of
https://invent.kde.org/marcoa/a-la-karte.git
synced 2026-03-26 17:03:08 +00:00
1883 lines
64 KiB
C++
1883 lines
64 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
|
|
|
|
#include "gamecenterdaemon.h"
|
|
|
|
#include <QDBusArgument>
|
|
#include <QDBusConnection>
|
|
#include <QDBusConnectionInterface>
|
|
#include <QDBusError>
|
|
#include <QDBusInterface>
|
|
#include <QDBusMessage>
|
|
#include <QDBusObjectPath>
|
|
#include <QDBusReply>
|
|
#include <QDBusVariant>
|
|
#include <QDBusVirtualObject>
|
|
#include <QDir>
|
|
#include <QProcess>
|
|
#include <QProcessEnvironment>
|
|
#include <QStandardPaths>
|
|
#include <QTimer>
|
|
#include <QUuid>
|
|
#include <QVariantMap>
|
|
|
|
#include <QRegularExpression>
|
|
|
|
#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");
|
|
static const QString kRunnerPath = QStringLiteral("/org/kde/ALaKarte/Runner1");
|
|
static const QString kRunnerInterface = QStringLiteral("org.kde.ALaKarte.Runner1");
|
|
|
|
static QString ensureScopeUnitName(const QString &unitName)
|
|
{
|
|
if (unitName.endsWith(QLatin1String(".scope"))) {
|
|
return unitName;
|
|
}
|
|
|
|
return unitName + QStringLiteral(".scope");
|
|
}
|
|
|
|
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>()) {
|
|
v = v.value<QDBusVariant>().variant();
|
|
}
|
|
return v;
|
|
}
|
|
|
|
static QVariant normalizeForDbus(QVariant v)
|
|
{
|
|
v = unwrapDbusVariant(v);
|
|
|
|
if (v.userType() == QMetaType::QChar) {
|
|
return QString(v.toChar());
|
|
}
|
|
|
|
if (v.userType() == QMetaType::QString || v.userType() == QMetaType::QStringList) {
|
|
return v;
|
|
}
|
|
|
|
if (v.canConvert<QDBusArgument>()) {
|
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
|
const QMap<QString, QString> asStringMap = qdbus_cast<QMap<QString, QString>>(arg);
|
|
if (!asStringMap.isEmpty()) {
|
|
QVariantMap converted;
|
|
for (auto it = asStringMap.constBegin(); it != asStringMap.constEnd(); ++it) {
|
|
converted.insert(it.key(), it.value());
|
|
}
|
|
return normalizeForDbus(converted);
|
|
}
|
|
|
|
const QVariantMap asMap = qdbus_cast<QVariantMap>(arg);
|
|
if (!asMap.isEmpty()) {
|
|
return normalizeForDbus(asMap);
|
|
}
|
|
|
|
const QStringList asStringList = qdbus_cast<QStringList>(arg);
|
|
if (!asStringList.isEmpty()) {
|
|
return asStringList;
|
|
}
|
|
}
|
|
if (v.canConvert<QVariantMap>()) {
|
|
QVariantMap m = v.toMap();
|
|
for (auto it = m.begin(); it != m.end(); ++it) {
|
|
it.value() = normalizeForDbus(it.value());
|
|
}
|
|
return m;
|
|
}
|
|
if (v.userType() == QMetaType::QVariantList) {
|
|
QVariantList list = v.toList();
|
|
for (QVariant &item : list) {
|
|
item = normalizeForDbus(item);
|
|
}
|
|
return list;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
static QVariantMap normalizeVariantMapForDbus(const QVariantMap &map)
|
|
{
|
|
QVariantMap out;
|
|
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
|
|
out.insert(it.key(), normalizeForDbus(it.value()));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
static bool tryResolveWithRunnerManager(const QVariantMap &spec, QVariantMap &out)
|
|
{
|
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
|
if (!bus.isConnected() || !bus.interface()) {
|
|
return false;
|
|
}
|
|
|
|
if (!bus.interface()->isServiceRegistered(kRunnerService)) {
|
|
bus.interface()->startService(kRunnerService);
|
|
}
|
|
|
|
QDBusInterface iface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
|
if (!iface.isValid()) {
|
|
if (bus.interface()->startService(kRunnerService).isValid()) {
|
|
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
|
if (retryIface.isValid()) {
|
|
retryIface.setTimeout(2000);
|
|
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
|
if (retryReply.isValid()) {
|
|
out = normalizeVariantMapForDbus(retryReply.value());
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
iface.setTimeout(2000);
|
|
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
|
|
if (!reply.isValid()) {
|
|
if (reply.error().type() == QDBusError::ServiceUnknown) {
|
|
bus.interface()->startService(kRunnerService);
|
|
QDBusInterface retryIface(kRunnerService, kRunnerPath, kRunnerInterface, bus);
|
|
if (!retryIface.isValid()) {
|
|
return false;
|
|
}
|
|
retryIface.setTimeout(2000);
|
|
const QDBusReply<QVariantMap> retryReply = retryIface.call(QStringLiteral("ResolveLaunch"), spec);
|
|
if (!retryReply.isValid()) {
|
|
return false;
|
|
}
|
|
out = normalizeVariantMapForDbus(retryReply.value());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
out = normalizeVariantMapForDbus(reply.value());
|
|
return true;
|
|
}
|
|
|
|
static QString mapStringValue(const QVariantMap &map, const QStringList &keys)
|
|
{
|
|
for (const QString &k : keys) {
|
|
const auto it = map.constFind(k);
|
|
if (it == map.constEnd()) {
|
|
continue;
|
|
}
|
|
return unwrapDbusVariant(*it).toString();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static QStringList variantToStringList(QVariant v)
|
|
{
|
|
v = unwrapDbusVariant(v);
|
|
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) {
|
|
out.push_back(unwrapDbusVariant(item).toString());
|
|
}
|
|
return out;
|
|
}
|
|
if (v.canConvert<QDBusArgument>()) {
|
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
|
const QStringList out = qdbus_cast<QStringList>(arg);
|
|
if (!out.isEmpty()) {
|
|
return out;
|
|
}
|
|
}
|
|
const QString s = v.toString();
|
|
if (!s.trimmed().isEmpty()) {
|
|
return QProcess::splitCommand(s);
|
|
}
|
|
return {};
|
|
}
|
|
|
|
class UnitPropertiesWatcher : public QObject
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
explicit UnitPropertiesWatcher(const QDBusObjectPath &path, QObject *parent = nullptr)
|
|
: QObject(parent)
|
|
, m_path(path)
|
|
{
|
|
}
|
|
|
|
QDBusObjectPath path() const
|
|
{
|
|
return m_path;
|
|
}
|
|
|
|
public Q_SLOTS:
|
|
void onPropertiesChanged(const QString &interfaceName, const QVariantMap &changedProperties, const QStringList &invalidated)
|
|
{
|
|
Q_UNUSED(invalidated)
|
|
if (interfaceName != QLatin1String("org.freedesktop.systemd1.Unit")) {
|
|
return;
|
|
}
|
|
Q_EMIT propertiesChanged(m_path, changedProperties);
|
|
}
|
|
|
|
Q_SIGNALS:
|
|
void propertiesChanged(const QDBusObjectPath &path, const QVariantMap &changedProperties);
|
|
|
|
private:
|
|
QDBusObjectPath m_path;
|
|
};
|
|
|
|
} // 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");
|
|
|
|
namespace
|
|
{
|
|
|
|
static QString gameCenterIntrospectionXml()
|
|
{
|
|
return QStringLiteral(
|
|
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-Bus Object Introspection 1.0//EN\" "
|
|
"\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">"
|
|
"<node>"
|
|
" <interface name=\"org.freedesktop.DBus.Introspectable\">"
|
|
" <method name=\"Introspect\">"
|
|
" <arg name=\"xml\" type=\"s\" direction=\"out\"/>"
|
|
" </method>"
|
|
" </interface>"
|
|
" <interface name=\"org.kde.GameCenter1\">"
|
|
" <method name=\"Ping\">"
|
|
" <arg name=\"out\" type=\"s\" direction=\"out\"/>"
|
|
" </method>"
|
|
" <method name=\"Version\">"
|
|
" <arg name=\"major\" type=\"u\" direction=\"out\"/>"
|
|
" <arg name=\"minor\" type=\"u\" direction=\"out\"/>"
|
|
" </method>"
|
|
" <method name=\"GetCapabilities\">"
|
|
" <arg name=\"caps\" type=\"a{sv}\" direction=\"out\"/>"
|
|
" </method>"
|
|
" <method name=\"ListSessions\">"
|
|
" <arg name=\"sessions\" type=\"av\" direction=\"out\"/>"
|
|
" </method>"
|
|
" <method name=\"GetSession\">"
|
|
" <arg name=\"sessionId\" type=\"s\" direction=\"in\"/>"
|
|
" <arg name=\"session\" type=\"a{sv}\" direction=\"out\"/>"
|
|
" </method>"
|
|
" <method name=\"Launch\">"
|
|
" <arg name=\"launchSpec\" type=\"a{sv}\" direction=\"in\"/>"
|
|
" <arg name=\"sessionId\" type=\"s\" direction=\"out\"/>"
|
|
" </method>"
|
|
" <method name=\"Stop\">"
|
|
" <arg name=\"sessionId\" type=\"s\" direction=\"in\"/>"
|
|
" </method>"
|
|
" <method name=\"StopByGameId\">"
|
|
" <arg name=\"gameId\" type=\"s\" direction=\"in\"/>"
|
|
" </method>"
|
|
" <method name=\"SetPolicy\">"
|
|
" <arg name=\"policy\" type=\"a{sv}\" direction=\"in\"/>"
|
|
" </method>"
|
|
" <method name=\"GetPolicy\">"
|
|
" <arg name=\"policy\" type=\"a{sv}\" direction=\"out\"/>"
|
|
" </method>"
|
|
" <signal name=\"SessionAdded\">"
|
|
" <arg name=\"session\" type=\"a{sv}\"/>"
|
|
" </signal>"
|
|
" <signal name=\"SessionChanged\">"
|
|
" <arg name=\"session\" type=\"a{sv}\"/>"
|
|
" </signal>"
|
|
" <signal name=\"SessionRemoved\">"
|
|
" <arg name=\"sessionId\" type=\"s\"/>"
|
|
" <arg name=\"finalState\" type=\"a{sv}\"/>"
|
|
" </signal>"
|
|
" <signal name=\"LaunchFailed\">"
|
|
" <arg name=\"error\" type=\"a{sv}\"/>"
|
|
" </signal>"
|
|
" </interface>"
|
|
"</node>");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
class GameCenterSystemProxyPrivate;
|
|
|
|
class GameCenterProxyVirtualObject : public QDBusVirtualObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
explicit GameCenterProxyVirtualObject(GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr)
|
|
: QDBusVirtualObject(parent)
|
|
, m_proxy(proxy)
|
|
{
|
|
}
|
|
|
|
QString introspect(const QString &path) const override;
|
|
bool handleMessage(const QDBusMessage &message, const QDBusConnection &connection) override;
|
|
|
|
private:
|
|
GameCenterSystemProxyPrivate *m_proxy = nullptr;
|
|
};
|
|
|
|
class BackendSignalForwarder : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
explicit BackendSignalForwarder(uint uid, GameCenterSystemProxyPrivate *proxy, QObject *parent = nullptr)
|
|
: QObject(parent)
|
|
, m_uid(uid)
|
|
, m_proxy(proxy)
|
|
{
|
|
}
|
|
|
|
public Q_SLOTS:
|
|
void onSessionAdded(const QVariantMap &session);
|
|
void onSessionChanged(const QVariantMap &session);
|
|
void onSessionRemoved(const QString &sessionId, const QVariantMap &finalState);
|
|
void onLaunchFailed(const QVariantMap &error);
|
|
|
|
private:
|
|
uint m_uid = 0;
|
|
GameCenterSystemProxyPrivate *m_proxy = nullptr;
|
|
};
|
|
|
|
struct BackendBusEntry {
|
|
BackendBusEntry(const QDBusConnection &bus, BackendSignalForwarder *forwarder)
|
|
: bus(bus)
|
|
, forwarder(forwarder)
|
|
{
|
|
}
|
|
|
|
QDBusConnection bus;
|
|
BackendSignalForwarder *forwarder = nullptr;
|
|
};
|
|
|
|
class GameCenterSystemProxyPrivate : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
public:
|
|
explicit GameCenterSystemProxyPrivate(QObject *parent = nullptr)
|
|
: QObject(parent)
|
|
{
|
|
}
|
|
|
|
bool init();
|
|
|
|
bool handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection);
|
|
void forwardSignalToUid(uint uid, const QString &member, const QList<QVariant> &args);
|
|
|
|
private Q_SLOTS:
|
|
void handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner);
|
|
|
|
private:
|
|
bool uidForSystemSender(const QString &senderUniqueName, uint &uid) const;
|
|
void rememberClient(const QString &senderUniqueName, uint uid);
|
|
void forgetClient(const QString &senderUniqueName);
|
|
BackendBusEntry *ensureBackend(uint uid);
|
|
|
|
QDBusConnection m_systemBus = QDBusConnection::systemBus();
|
|
std::unique_ptr<GameCenterProxyVirtualObject> m_virtualObject;
|
|
|
|
QHash<uint, BackendBusEntry> m_backends;
|
|
QHash<QString, uint> m_clientUid;
|
|
QHash<uint, QSet<QString>> m_uidClients;
|
|
};
|
|
|
|
QString GameCenterProxyVirtualObject::introspect(const QString &path) const
|
|
{
|
|
if (path == kGameCenterPath) {
|
|
return gameCenterIntrospectionXml();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
bool GameCenterProxyVirtualObject::handleMessage(const QDBusMessage &message, const QDBusConnection &connection)
|
|
{
|
|
if (!m_proxy) {
|
|
return false;
|
|
}
|
|
return m_proxy->handleSystemMessage(message, connection);
|
|
}
|
|
|
|
void BackendSignalForwarder::onSessionAdded(const QVariantMap &session)
|
|
{
|
|
if (m_proxy) {
|
|
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionAdded"), {session});
|
|
}
|
|
}
|
|
|
|
void BackendSignalForwarder::onSessionChanged(const QVariantMap &session)
|
|
{
|
|
if (m_proxy) {
|
|
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionChanged"), {session});
|
|
}
|
|
}
|
|
|
|
void BackendSignalForwarder::onSessionRemoved(const QString &sessionId, const QVariantMap &finalState)
|
|
{
|
|
if (m_proxy) {
|
|
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("SessionRemoved"), {sessionId, finalState});
|
|
}
|
|
}
|
|
|
|
void BackendSignalForwarder::onLaunchFailed(const QVariantMap &error)
|
|
{
|
|
if (m_proxy) {
|
|
m_proxy->forwardSignalToUid(m_uid, QStringLiteral("LaunchFailed"), {error});
|
|
}
|
|
}
|
|
|
|
bool GameCenterSystemProxyPrivate::init()
|
|
{
|
|
if (!m_systemBus.isConnected()) {
|
|
return false;
|
|
}
|
|
|
|
if (!m_systemBus.registerService(kGameCenterService)) {
|
|
return false;
|
|
}
|
|
|
|
m_virtualObject = std::make_unique<GameCenterProxyVirtualObject>(this);
|
|
if (!m_systemBus.registerVirtualObject(kGameCenterPath, m_virtualObject.get(), QDBusConnection::SingleNode)) {
|
|
return false;
|
|
}
|
|
|
|
if (m_systemBus.interface()) {
|
|
connect(m_systemBus.interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, &GameCenterSystemProxyPrivate::handleServiceOwnerChanged);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GameCenterSystemProxyPrivate::uidForSystemSender(const QString &senderUniqueName, uint &uid) const
|
|
{
|
|
if (!m_systemBus.interface()) {
|
|
return false;
|
|
}
|
|
|
|
const QDBusReply<uint> uidReply = m_systemBus.interface()->serviceUid(senderUniqueName);
|
|
if (!uidReply.isValid()) {
|
|
return false;
|
|
}
|
|
|
|
uid = uidReply.value();
|
|
return true;
|
|
}
|
|
|
|
void GameCenterSystemProxyPrivate::rememberClient(const QString &senderUniqueName, uint uid)
|
|
{
|
|
const auto it = m_clientUid.constFind(senderUniqueName);
|
|
if (it != m_clientUid.constEnd()) {
|
|
if (it.value() == uid) {
|
|
return;
|
|
}
|
|
forgetClient(senderUniqueName);
|
|
}
|
|
|
|
m_clientUid.insert(senderUniqueName, uid);
|
|
m_uidClients[uid].insert(senderUniqueName);
|
|
}
|
|
|
|
void GameCenterSystemProxyPrivate::forgetClient(const QString &senderUniqueName)
|
|
{
|
|
const auto it = m_clientUid.find(senderUniqueName);
|
|
if (it == m_clientUid.end()) {
|
|
return;
|
|
}
|
|
|
|
const uint uid = it.value();
|
|
m_clientUid.erase(it);
|
|
|
|
auto clientsIt = m_uidClients.find(uid);
|
|
if (clientsIt != m_uidClients.end()) {
|
|
clientsIt.value().remove(senderUniqueName);
|
|
if (clientsIt.value().isEmpty()) {
|
|
m_uidClients.erase(clientsIt);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GameCenterSystemProxyPrivate::handleServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner)
|
|
{
|
|
Q_UNUSED(oldOwner)
|
|
|
|
if (!newOwner.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
if (!name.startsWith(QLatin1Char(':'))) {
|
|
return;
|
|
}
|
|
|
|
forgetClient(name);
|
|
}
|
|
|
|
BackendBusEntry *GameCenterSystemProxyPrivate::ensureBackend(uint uid)
|
|
{
|
|
const auto it = m_backends.find(uid);
|
|
if (it != m_backends.end()) {
|
|
return &it.value();
|
|
}
|
|
|
|
const QString address = QStringLiteral("unix:path=/run/user/%1/bus").arg(uid);
|
|
const QString connectionName = QStringLiteral("gamecenter-user-%1").arg(uid);
|
|
QDBusConnection backend = QDBusConnection::connectToBus(address, connectionName);
|
|
if (!backend.isConnected()) {
|
|
return nullptr;
|
|
}
|
|
|
|
auto *forwarder = new BackendSignalForwarder(uid, this, this);
|
|
BackendBusEntry entry(backend, forwarder);
|
|
|
|
backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionAdded"), forwarder, SLOT(onSessionAdded(QVariantMap)));
|
|
|
|
backend
|
|
.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("SessionChanged"), forwarder, SLOT(onSessionChanged(QVariantMap)));
|
|
|
|
backend.connect(kGameCenterService,
|
|
kGameCenterPath,
|
|
kGameCenterInterface,
|
|
QStringLiteral("SessionRemoved"),
|
|
forwarder,
|
|
SLOT(onSessionRemoved(QString, QVariantMap)));
|
|
|
|
backend.connect(kGameCenterService, kGameCenterPath, kGameCenterInterface, QStringLiteral("LaunchFailed"), forwarder, SLOT(onLaunchFailed(QVariantMap)));
|
|
|
|
auto inserted = m_backends.insert(uid, entry);
|
|
return &inserted.value();
|
|
}
|
|
|
|
void GameCenterSystemProxyPrivate::forwardSignalToUid(uint uid, const QString &member, const QList<QVariant> &args)
|
|
{
|
|
if (!m_systemBus.isConnected()) {
|
|
return;
|
|
}
|
|
|
|
const auto clientsIt = m_uidClients.constFind(uid);
|
|
if (clientsIt == m_uidClients.constEnd()) {
|
|
return;
|
|
}
|
|
|
|
const QSet<QString> clients = clientsIt.value();
|
|
for (const QString &client : clients) {
|
|
if (client.isEmpty()) {
|
|
continue;
|
|
}
|
|
|
|
QDBusMessage signal = QDBusMessage::createTargetedSignal(client, kGameCenterPath, kGameCenterInterface, member);
|
|
signal.setArguments(args);
|
|
m_systemBus.send(signal);
|
|
}
|
|
}
|
|
|
|
bool GameCenterSystemProxyPrivate::handleSystemMessage(const QDBusMessage &message, const QDBusConnection &connection)
|
|
{
|
|
if (message.type() != QDBusMessage::MethodCallMessage) {
|
|
return false;
|
|
}
|
|
|
|
if (message.interface() == QLatin1String("org.freedesktop.DBus.Introspectable") && message.member() == QLatin1String("Introspect")) {
|
|
if (!message.isReplyRequired()) {
|
|
return true;
|
|
}
|
|
connection.send(message.createReply(gameCenterIntrospectionXml()));
|
|
return true;
|
|
}
|
|
|
|
const QString sender = message.service();
|
|
if (sender.isEmpty()) {
|
|
if (message.isReplyRequired()) {
|
|
connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("missing sender")));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
uint uid = 0;
|
|
if (!uidForSystemSender(sender, uid)) {
|
|
if (message.isReplyRequired()) {
|
|
connection.send(message.createErrorReply(QDBusError::AccessDenied, QStringLiteral("failed to determine caller uid")));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
rememberClient(sender, uid);
|
|
|
|
BackendBusEntry *backend = ensureBackend(uid);
|
|
if (!backend) {
|
|
if (message.isReplyRequired()) {
|
|
connection.send(message.createErrorReply(QDBusError::ServiceUnknown, QStringLiteral("user bus backend unavailable")));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
QDBusMessage forward = QDBusMessage::createMethodCall(kGameCenterService, message.path(), message.interface(), message.member());
|
|
forward.setArguments(message.arguments());
|
|
forward.setAutoStartService(true);
|
|
|
|
const QDBusMessage backendReply = backend->bus.call(forward, QDBus::Block, -1);
|
|
if (!message.isReplyRequired()) {
|
|
return true;
|
|
}
|
|
|
|
if (backendReply.type() == QDBusMessage::ErrorMessage) {
|
|
connection.send(message.createErrorReply(backendReply.errorName(), backendReply.errorMessage()));
|
|
return true;
|
|
}
|
|
|
|
if (backendReply.type() != QDBusMessage::ReplyMessage) {
|
|
connection.send(message.createErrorReply(QDBusError::Failed, QStringLiteral("backend did not reply")));
|
|
return true;
|
|
}
|
|
|
|
QDBusMessage out = message.createReply(backendReply.arguments());
|
|
if (message.interface() == kGameCenterInterface && message.member() == QLatin1String("GetCapabilities")) {
|
|
if (!out.arguments().isEmpty()) {
|
|
QVariant capsV = unwrapDbusVariant(out.arguments().first());
|
|
QVariantMap caps;
|
|
if (capsV.canConvert<QDBusArgument>()) {
|
|
const QDBusArgument arg = capsV.value<QDBusArgument>();
|
|
caps = qdbus_cast<QVariantMap>(arg);
|
|
} else if (capsV.canConvert<QVariantMap>()) {
|
|
caps = capsV.toMap();
|
|
}
|
|
|
|
if (!caps.isEmpty()) {
|
|
caps.insert(QStringLiteral("supportsSystemBus"), true);
|
|
out.setArguments({caps});
|
|
}
|
|
}
|
|
}
|
|
|
|
connection.send(out);
|
|
return true;
|
|
}
|
|
|
|
GameCenterSystemProxy::GameCenterSystemProxy(QObject *parent)
|
|
: QObject(parent)
|
|
, d(std::make_unique<GameCenterSystemProxyPrivate>())
|
|
{
|
|
}
|
|
|
|
GameCenterSystemProxy::~GameCenterSystemProxy() = default;
|
|
|
|
bool GameCenterSystemProxy::init()
|
|
{
|
|
return d->init();
|
|
}
|
|
|
|
#endif
|
|
|
|
void GameCenterDaemon::watchSystemdUnit(const QString &sessionId, const QString &unitName, const QDBusObjectPath &unitPath)
|
|
{
|
|
if (unitName.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
m_unitNameToSessionId.insert(unitName, sessionId);
|
|
|
|
if (unitPath.path().isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const QString unitPathKey = unitPath.path();
|
|
m_unitPathToSessionId.insert(unitPathKey, sessionId);
|
|
|
|
if (m_unitPathWatchers.contains(unitPathKey)) {
|
|
return;
|
|
}
|
|
|
|
auto *watcher = new UnitPropertiesWatcher(unitPath, this);
|
|
m_unitPathWatchers.insert(unitPathKey, watcher);
|
|
|
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
|
bus.connect(QStringLiteral("org.freedesktop.systemd1"),
|
|
unitPath.path(),
|
|
QStringLiteral("org.freedesktop.DBus.Properties"),
|
|
QStringLiteral("PropertiesChanged"),
|
|
watcher,
|
|
SLOT(onPropertiesChanged(QString, QVariantMap, QStringList)));
|
|
|
|
connect(watcher, &UnitPropertiesWatcher::propertiesChanged, this, &GameCenterDaemon::handleSystemdUnitPropertiesChanged);
|
|
}
|
|
|
|
void GameCenterDaemon::unwatchSystemdUnit(const QString &unitName, const QDBusObjectPath &unitPath)
|
|
{
|
|
if (!unitName.isEmpty()) {
|
|
m_unitNameToSessionId.remove(unitName);
|
|
}
|
|
|
|
if (!unitPath.path().isEmpty()) {
|
|
const QString unitPathKey = unitPath.path();
|
|
m_unitPathToSessionId.remove(unitPathKey);
|
|
if (QObject *watcher = m_unitPathWatchers.take(unitPathKey)) {
|
|
watcher->deleteLater();
|
|
}
|
|
}
|
|
}
|
|
|
|
void GameCenterDaemon::handleSystemdUnitNew(const QString &unitName, const QDBusObjectPath &unitPath)
|
|
{
|
|
const auto it = m_unitNameToSessionId.constFind(unitName);
|
|
if (it == m_unitNameToSessionId.constEnd()) {
|
|
return;
|
|
}
|
|
|
|
const QString sessionId = it.value();
|
|
auto sit = m_sessions.find(sessionId);
|
|
if (sit == m_sessions.end()) {
|
|
return;
|
|
}
|
|
|
|
if (sit.value().unitPath.path().isEmpty() && !unitPath.path().isEmpty()) {
|
|
sit.value().unitPath = unitPath;
|
|
watchSystemdUnit(sessionId, unitName, unitPath);
|
|
}
|
|
}
|
|
|
|
void GameCenterDaemon::handleSystemdUnitRemoved(const QString &unitName, const QDBusObjectPath &unitPath)
|
|
{
|
|
QString sessionId;
|
|
|
|
if (!unitPath.path().isEmpty()) {
|
|
const auto it = m_unitPathToSessionId.constFind(unitPath.path());
|
|
if (it != m_unitPathToSessionId.constEnd()) {
|
|
sessionId = it.value();
|
|
}
|
|
}
|
|
|
|
if (sessionId.isEmpty() && !unitName.isEmpty()) {
|
|
const auto it = m_unitNameToSessionId.constFind(unitName);
|
|
if (it != m_unitNameToSessionId.constEnd()) {
|
|
sessionId = it.value();
|
|
}
|
|
}
|
|
|
|
if (sessionId.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
auto sit = m_sessions.find(sessionId);
|
|
if (sit == m_sessions.end()) {
|
|
unwatchSystemdUnit(unitName, unitPath);
|
|
return;
|
|
}
|
|
|
|
unwatchSystemdUnit(sit.value().unitName, sit.value().unitPath);
|
|
removeSessionInternal(sessionId, sit.value().stopping ? QStringLiteral("Stopped") : QStringLiteral("Exited"));
|
|
}
|
|
|
|
void GameCenterDaemon::handleSystemdUnitPropertiesChanged(const QDBusObjectPath &unitPath, const QVariantMap &changedProperties)
|
|
{
|
|
if (unitPath.path().isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
const auto sidIt = m_unitPathToSessionId.constFind(unitPath.path());
|
|
if (sidIt == m_unitPathToSessionId.constEnd()) {
|
|
return;
|
|
}
|
|
const QString sessionId = sidIt.value();
|
|
|
|
auto sit = m_sessions.find(sessionId);
|
|
if (sit == m_sessions.end()) {
|
|
return;
|
|
}
|
|
|
|
QVariant activeStateV = changedProperties.value(QStringLiteral("ActiveState"));
|
|
if (activeStateV.canConvert<QDBusVariant>()) {
|
|
activeStateV = activeStateV.value<QDBusVariant>().variant();
|
|
}
|
|
if (!activeStateV.isValid()) {
|
|
return;
|
|
}
|
|
const QString activeState = activeStateV.toString();
|
|
|
|
if (activeState == QLatin1String("active") || activeState == QLatin1String("activating") || activeState == QLatin1String("deactivating")) {
|
|
const QList<uint> pids = m_systemd.scopePids(unitPath);
|
|
if (!pids.isEmpty()) {
|
|
sit.value().mainPid = pids.first();
|
|
}
|
|
Q_EMIT SessionChanged(sessionToVariantMap(sit.value(), sessionState(sit.value())));
|
|
return;
|
|
}
|
|
|
|
const QString finalState =
|
|
sit.value().stopping ? QStringLiteral("Stopped") : (activeState == QLatin1String("failed") ? QStringLiteral("Failed") : QStringLiteral("Exited"));
|
|
unwatchSystemdUnit(sit.value().unitName, sit.value().unitPath);
|
|
removeSessionInternal(sessionId, finalState);
|
|
}
|
|
|
|
void GameCenterDaemon::removeSessionInternal(const QString &sessionId, const QString &finalState)
|
|
{
|
|
auto it = m_sessions.find(sessionId);
|
|
if (it == m_sessions.end()) {
|
|
return;
|
|
}
|
|
|
|
QVariantMap finalMap = sessionToVariantMap(it.value(), finalState);
|
|
if (it.value().hasExitInfo) {
|
|
finalMap.insert(QStringLiteral("exitCode"), it.value().exitCode);
|
|
finalMap.insert(QStringLiteral("exitStatus"), it.value().exitStatus);
|
|
}
|
|
|
|
if (it.value().process) {
|
|
it.value().process->deleteLater();
|
|
it.value().process = nullptr;
|
|
}
|
|
if (it.value().scanner) {
|
|
it.value().scanner->cancel();
|
|
it.value().scanner->deleteLater();
|
|
it.value().scanner = nullptr;
|
|
}
|
|
|
|
Q_EMIT SessionRemoved(sessionId, finalMap);
|
|
m_sessions.erase(it);
|
|
}
|
|
GameCenterDaemon::GameCenterDaemon(QObject *parent)
|
|
: QObject(parent)
|
|
{
|
|
}
|
|
|
|
bool GameCenterDaemon::init()
|
|
{
|
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
|
if (!bus.isConnected()) {
|
|
return false;
|
|
}
|
|
|
|
if (!bus.registerService(QStringLiteral("org.kde.GameCenter1"))) {
|
|
return false;
|
|
}
|
|
|
|
if (!bus.registerObject(QStringLiteral("/org/kde/GameCenter1"), this, QDBusConnection::ExportAllSlots | QDBusConnection::ExportAllSignals)) {
|
|
return false;
|
|
}
|
|
|
|
setupSystemdSubscriptions();
|
|
|
|
recoverExistingSessions();
|
|
|
|
return true;
|
|
}
|
|
|
|
void GameCenterDaemon::setupSystemdSubscriptions()
|
|
{
|
|
if (!m_systemd.isAvailable()) {
|
|
return;
|
|
}
|
|
|
|
QDBusConnection bus = QDBusConnection::sessionBus();
|
|
if (!bus.isConnected()) {
|
|
return;
|
|
}
|
|
|
|
bus.connect(QStringLiteral("org.freedesktop.systemd1"),
|
|
QStringLiteral("/org/freedesktop/systemd1"),
|
|
QStringLiteral("org.freedesktop.systemd1.Manager"),
|
|
QStringLiteral("UnitNew"),
|
|
this,
|
|
SLOT(handleSystemdUnitNew(QString, QDBusObjectPath)));
|
|
|
|
bus.connect(QStringLiteral("org.freedesktop.systemd1"),
|
|
QStringLiteral("/org/freedesktop/systemd1"),
|
|
QStringLiteral("org.freedesktop.systemd1.Manager"),
|
|
QStringLiteral("UnitRemoved"),
|
|
this,
|
|
SLOT(handleSystemdUnitRemoved(QString, QDBusObjectPath)));
|
|
}
|
|
|
|
QString GameCenterDaemon::Ping() const
|
|
{
|
|
return QStringLiteral("ok");
|
|
}
|
|
|
|
void GameCenterDaemon::Version(uint &major, uint &minor) const
|
|
{
|
|
major = 1;
|
|
minor = 0;
|
|
}
|
|
|
|
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();
|
|
caps.insert(QStringLiteral("supportsSteamAttach"), systemdAvailable && steamAvailable);
|
|
caps.insert(QStringLiteral("supportsLutrisAttach"), systemdAvailable && lutrisAvailable);
|
|
return caps;
|
|
}
|
|
|
|
QVariantList GameCenterDaemon::ListSessions() const
|
|
{
|
|
QVariantList sessions;
|
|
sessions.reserve(m_sessions.size());
|
|
|
|
for (const auto &session : m_sessions) {
|
|
sessions.push_back(sessionToVariantMap(session, sessionState(session)));
|
|
}
|
|
|
|
return sessions;
|
|
}
|
|
|
|
QVariantMap GameCenterDaemon::GetSession(const QString &sessionId) const
|
|
{
|
|
const auto it = m_sessions.constFind(sessionId);
|
|
if (it == m_sessions.constEnd()) {
|
|
return {};
|
|
}
|
|
return sessionToVariantMap(it.value(), sessionState(it.value()));
|
|
}
|
|
|
|
void GameCenterDaemon::SetPolicy(const QVariantMap &policy)
|
|
{
|
|
if (policy.contains(QStringLiteral("maxConcurrent"))) {
|
|
QVariant v = policy.value(QStringLiteral("maxConcurrent"));
|
|
if (v.canConvert<QDBusVariant>()) {
|
|
v = v.value<QDBusVariant>().variant();
|
|
}
|
|
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},
|
|
};
|
|
}
|
|
|
|
QString GameCenterDaemon::Launch(const QVariantMap &launchSpec)
|
|
{
|
|
if (!m_systemd.isAvailable()) {
|
|
failLaunch(QStringLiteral("systemd user manager is not available"), launchSpec);
|
|
return {};
|
|
}
|
|
|
|
if (m_maxConcurrent > 0 && m_sessions.size() >= m_maxConcurrent) {
|
|
QVariantMap ctx = launchSpec;
|
|
ctx.insert(QStringLiteral("maxConcurrent"), m_maxConcurrent);
|
|
ctx.insert(QStringLiteral("activeSessions"), m_sessions.size());
|
|
failLaunch(QStringLiteral("max concurrent session limit reached"), ctx);
|
|
return {};
|
|
}
|
|
|
|
const QString provider = detectProvider(launchSpec);
|
|
|
|
if (provider == QLatin1String("steam") || provider == QLatin1String("lutris")) {
|
|
return launchMonitored(launchSpec, provider);
|
|
}
|
|
|
|
return launchDirect(launchSpec);
|
|
}
|
|
|
|
QString GameCenterDaemon::launchDirect(const QVariantMap &launchSpec)
|
|
{
|
|
const QString gameId = mapStringValue(launchSpec, {QStringLiteral("gameId"), QStringLiteral("id")});
|
|
const QString displayName = mapStringValue(launchSpec, {QStringLiteral("displayName"), QStringLiteral("name"), QStringLiteral("title")});
|
|
const QString workingDirectory = mapStringValue(launchSpec, {QStringLiteral("workingDirectory"), QStringLiteral("cwd"), QStringLiteral("workingDir")});
|
|
|
|
const QString command = mapStringValue(launchSpec, {QStringLiteral("command"), QStringLiteral("launchCommand")});
|
|
QString program = mapStringValue(launchSpec, {QStringLiteral("program"), QStringLiteral("executable"), QStringLiteral("exe")});
|
|
QStringList args;
|
|
if (launchSpec.contains(QStringLiteral("args"))) {
|
|
args = variantToStringList(launchSpec.value(QStringLiteral("args")));
|
|
} else if (launchSpec.contains(QStringLiteral("argv"))) {
|
|
args = variantToStringList(launchSpec.value(QStringLiteral("argv")));
|
|
} else if (launchSpec.contains(QStringLiteral("arguments"))) {
|
|
args = variantToStringList(launchSpec.value(QStringLiteral("arguments")));
|
|
}
|
|
|
|
if (program.trimmed().isEmpty()) {
|
|
if (command.trimmed().isEmpty()) {
|
|
failLaunch(QStringLiteral("launchSpec.command or launchSpec.program is required"), launchSpec);
|
|
return {};
|
|
}
|
|
|
|
const QStringList argv = QProcess::splitCommand(command);
|
|
if (argv.isEmpty()) {
|
|
failLaunch(QStringLiteral("failed to parse command"), launchSpec);
|
|
return {};
|
|
}
|
|
|
|
program = argv.first();
|
|
args = argv.mid(1);
|
|
}
|
|
|
|
QVariantMap envOverrides;
|
|
auto extractVariantMap = [](QVariant v) {
|
|
if (v.canConvert<QDBusVariant>()) {
|
|
v = v.value<QDBusVariant>().variant();
|
|
}
|
|
if (v.canConvert<QVariantMap>()) {
|
|
const QVariantMap asMap = v.toMap();
|
|
if (!asMap.isEmpty()) {
|
|
return asMap;
|
|
}
|
|
}
|
|
if (v.canConvert<QDBusArgument>()) {
|
|
const QDBusArgument arg = v.value<QDBusArgument>();
|
|
const QMap<QString, QString> asStringMap = qdbus_cast<QMap<QString, QString>>(arg);
|
|
if (!asStringMap.isEmpty()) {
|
|
QVariantMap converted;
|
|
for (auto it = asStringMap.constBegin(); it != asStringMap.constEnd(); ++it) {
|
|
converted.insert(it.key(), it.value());
|
|
}
|
|
return converted;
|
|
}
|
|
|
|
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{};
|
|
};
|
|
|
|
if (launchSpec.contains(QStringLiteral("env"))) {
|
|
envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("env")));
|
|
} else if (launchSpec.contains(QStringLiteral("environment"))) {
|
|
envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("environment")));
|
|
} else if (launchSpec.contains(QStringLiteral("envOverrides"))) {
|
|
envOverrides = extractVariantMap(launchSpec.value(QStringLiteral("envOverrides")));
|
|
}
|
|
|
|
const QString runnerId = mapStringValue(launchSpec, {QStringLiteral("runnerId")});
|
|
const QString runner = mapStringValue(launchSpec, {QStringLiteral("runner")});
|
|
const QString runnerPath = mapStringValue(launchSpec, {QStringLiteral("runnerPath")});
|
|
const QString prefixPath = mapStringValue(launchSpec, {QStringLiteral("prefixPath")});
|
|
|
|
const QString requestedProgram = mapStringValue(launchSpec, {QStringLiteral("requestedProgram")});
|
|
QStringList requestedArgs;
|
|
if (launchSpec.contains(QStringLiteral("requestedArgs"))) {
|
|
requestedArgs = variantToStringList(launchSpec.value(QStringLiteral("requestedArgs")));
|
|
}
|
|
|
|
QVariantMap requestedEnvOverrides;
|
|
if (launchSpec.contains(QStringLiteral("requestedEnvOverrides"))) {
|
|
requestedEnvOverrides = extractVariantMap(launchSpec.value(QStringLiteral("requestedEnvOverrides")));
|
|
}
|
|
|
|
const bool wantsRunnerResolution = !runnerId.isEmpty() || runner == QLatin1String("wine") || runner == QLatin1String("proton");
|
|
if (wantsRunnerResolution) {
|
|
const QString baseProgram = requestedProgram.isEmpty() ? program : requestedProgram;
|
|
const QStringList baseArgs = requestedArgs.isEmpty() ? args : requestedArgs;
|
|
const QVariantMap baseEnv = requestedEnvOverrides.isEmpty() ? envOverrides : requestedEnvOverrides;
|
|
|
|
const QVariantMap resolveSpec = {
|
|
{QStringLiteral("runnerId"), runnerId},
|
|
{QStringLiteral("runner"), runner},
|
|
{QStringLiteral("runnerPath"), runnerPath},
|
|
{QStringLiteral("gameId"), gameId},
|
|
{QStringLiteral("prefixPath"), prefixPath},
|
|
{QStringLiteral("program"), baseProgram},
|
|
{QStringLiteral("args"), baseArgs},
|
|
{QStringLiteral("envOverrides"), baseEnv},
|
|
};
|
|
|
|
QVariantMap resolved;
|
|
if (tryResolveWithRunnerManager(resolveSpec, resolved)) {
|
|
if (!resolved.value(QStringLiteral("ok")).toBool()) {
|
|
QVariantMap ctx = launchSpec;
|
|
ctx.insert(QStringLiteral("runnerResolution"), resolved);
|
|
failLaunch(resolved.value(QStringLiteral("error")).toString(), ctx);
|
|
return {};
|
|
}
|
|
|
|
program = mapStringValue(resolved, {QStringLiteral("finalProgram")});
|
|
if (resolved.contains(QStringLiteral("finalArgs"))) {
|
|
args = variantToStringList(resolved.value(QStringLiteral("finalArgs")));
|
|
}
|
|
if (resolved.contains(QStringLiteral("effectiveEnv"))) {
|
|
envOverrides = extractVariantMap(resolved.value(QStringLiteral("effectiveEnv")));
|
|
}
|
|
|
|
const QString resolvedPrefixPath = mapStringValue(resolved, {QStringLiteral("resolvedPrefixPath")});
|
|
if (!resolvedPrefixPath.isEmpty()) {
|
|
QDir().mkpath(resolvedPrefixPath);
|
|
}
|
|
} else if (!runnerId.isEmpty()) {
|
|
QVariantMap ctx = launchSpec;
|
|
ctx.insert(QStringLiteral("runnerResolution"),
|
|
QVariantMap{{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("runner service unavailable")}});
|
|
failLaunch(QStringLiteral("runner service is not available"), ctx);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
const QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
|
const QString unitName = ensureScopeUnitName(QStringLiteral("alakarte-game-%1").arg(sessionId));
|
|
|
|
auto *process = new QProcess(this);
|
|
if (!workingDirectory.isEmpty()) {
|
|
process->setWorkingDirectory(workingDirectory);
|
|
}
|
|
|
|
if (!envOverrides.isEmpty()) {
|
|
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
|
|
for (auto it = envOverrides.constBegin(); it != envOverrides.constEnd(); ++it) {
|
|
const QString key = it.key();
|
|
if (key.isEmpty() || key.contains(QLatin1Char('='))) {
|
|
continue;
|
|
}
|
|
|
|
QVariant value = it.value();
|
|
if (value.canConvert<QDBusVariant>()) {
|
|
value = value.value<QDBusVariant>().variant();
|
|
}
|
|
env.insert(key, value.toString());
|
|
}
|
|
process->setProcessEnvironment(env);
|
|
}
|
|
|
|
process->start(program, args);
|
|
if (!process->waitForStarted()) {
|
|
const QVariantMap ctx = {
|
|
{QStringLiteral("command"), command},
|
|
{QStringLiteral("program"), program},
|
|
{QStringLiteral("args"), args},
|
|
{QStringLiteral("gameId"), gameId},
|
|
{QStringLiteral("workingDirectory"), workingDirectory},
|
|
{QStringLiteral("envOverrides"), envOverrides},
|
|
{QStringLiteral("error"), process->errorString()},
|
|
};
|
|
process->deleteLater();
|
|
failLaunch(QStringLiteral("failed to start process"), ctx);
|
|
return {};
|
|
}
|
|
|
|
const QList<uint> pids = {static_cast<uint>(process->processId())};
|
|
const QString description = gameId.isEmpty() ? QStringLiteral("A-La-Karte game") : QStringLiteral("A-La-Karte game %1").arg(gameId);
|
|
|
|
const QDBusReply<QDBusObjectPath> startReply = m_systemd.startTransientScope(unitName, pids, description);
|
|
if (!startReply.isValid()) {
|
|
const QVariantMap ctx = {
|
|
{QStringLiteral("command"), command},
|
|
{QStringLiteral("program"), program},
|
|
{QStringLiteral("args"), args},
|
|
{QStringLiteral("gameId"), gameId},
|
|
{QStringLiteral("workingDirectory"), workingDirectory},
|
|
{QStringLiteral("envOverrides"), envOverrides},
|
|
{QStringLiteral("unit"), unitName},
|
|
{QStringLiteral("error"), startReply.error().message()},
|
|
};
|
|
process->kill();
|
|
process->deleteLater();
|
|
failLaunch(QStringLiteral("failed to create transient scope"), ctx);
|
|
return {};
|
|
}
|
|
|
|
QDBusObjectPath unitPath;
|
|
const QDBusReply<QDBusObjectPath> getUnitReply = m_systemd.getUnit(unitName);
|
|
if (getUnitReply.isValid()) {
|
|
unitPath = getUnitReply.value();
|
|
}
|
|
|
|
Session session;
|
|
session.sessionId = sessionId;
|
|
session.gameId = gameId;
|
|
session.displayName = displayName;
|
|
session.unitName = unitName;
|
|
session.unitPath = unitPath;
|
|
session.provider = QStringLiteral("manual");
|
|
session.startTime = QDateTime::currentDateTimeUtc();
|
|
session.process = process;
|
|
session.mainPid = static_cast<uint>(process->processId());
|
|
m_sessions.insert(sessionId, session);
|
|
|
|
watchSystemdUnit(sessionId, unitName, unitPath);
|
|
|
|
Q_EMIT SessionAdded(sessionToVariantMap(session, QStringLiteral("Running")));
|
|
|
|
connect(process, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, [this, sessionId](int exitCode, QProcess::ExitStatus exitStatus) {
|
|
const auto it = m_sessions.find(sessionId);
|
|
if (it == m_sessions.end()) {
|
|
return;
|
|
}
|
|
it.value().hasExitInfo = true;
|
|
it.value().exitCode = exitCode;
|
|
it.value().exitStatus = static_cast<int>(exitStatus);
|
|
if (it.value().process) {
|
|
it.value().process->deleteLater();
|
|
it.value().process = nullptr;
|
|
}
|
|
});
|
|
|
|
return sessionId;
|
|
}
|
|
|
|
QString GameCenterDaemon::launchMonitored(const QVariantMap &launchSpec, const QString &provider)
|
|
{
|
|
const QString gameId = mapStringValue(launchSpec, {QStringLiteral("gameId"), QStringLiteral("id")});
|
|
const QString displayName = mapStringValue(launchSpec, {QStringLiteral("displayName"), QStringLiteral("name"), QStringLiteral("title")});
|
|
const QString command = mapStringValue(launchSpec, {QStringLiteral("command"), QStringLiteral("launchCommand")});
|
|
|
|
const QString sessionId = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
|
|
|
// Determine bootstrap command and scanner matcher
|
|
QString bootstrapProgram;
|
|
QStringList bootstrapArgs;
|
|
std::function<QList<ProcessScanner::Match>()> matcher;
|
|
|
|
if (provider == QLatin1String("steam")) {
|
|
const QString appId = extractSteamAppId(command);
|
|
if (appId.isEmpty()) {
|
|
failLaunch(QStringLiteral("could not extract Steam appId from command"), launchSpec);
|
|
return {};
|
|
}
|
|
|
|
bootstrapProgram = QStringLiteral("steam");
|
|
bootstrapArgs = {QStringLiteral("-silent"), QStringLiteral("-applaunch"), appId};
|
|
|
|
matcher = [appId]() -> QList<ProcessScanner::Match> {
|
|
// Primary: look for SteamAppId=<appId> in environ
|
|
QList<ProcessScanner::Match> results = ProcessScanner::findByAnyEnvironment({QStringLiteral("SteamAppId"), QStringLiteral("SteamGameId")}, appId);
|
|
// Filter out the steam client itself
|
|
QList<ProcessScanner::Match> filtered;
|
|
for (const auto &m : std::as_const(results)) {
|
|
if (!m.exe.contains(QLatin1String("steam")) || m.exe.contains(QLatin1String("proton"))) {
|
|
filtered.push_back(m);
|
|
}
|
|
}
|
|
return filtered.isEmpty() ? results : filtered;
|
|
};
|
|
|
|
} else if (provider == QLatin1String("lutris")) {
|
|
const QString lutrisId = extractLutrisId(command);
|
|
if (lutrisId.isEmpty()) {
|
|
failLaunch(QStringLiteral("could not extract Lutris ID from command"), launchSpec);
|
|
return {};
|
|
}
|
|
|
|
bootstrapProgram = QStringLiteral("lutris");
|
|
bootstrapArgs = {QStringLiteral("lutris:rungameid/%1").arg(lutrisId)};
|
|
|
|
// For Lutris, look for child processes with the game slug or lutris game env
|
|
matcher = [lutrisId]() -> QList<ProcessScanner::Match> {
|
|
QList<ProcessScanner::Match> results = ProcessScanner::findByEnvironment(QStringLiteral("LUTRIS_GAME_SLUG"), lutrisId);
|
|
if (results.isEmpty()) {
|
|
results = ProcessScanner::findByCmdline(lutrisId);
|
|
// Filter out lutris itself
|
|
QList<ProcessScanner::Match> filtered;
|
|
for (const auto &m : std::as_const(results)) {
|
|
if (!m.exe.endsWith(QLatin1String("/lutris")) && !m.exe.endsWith(QLatin1String("/python3"))) {
|
|
filtered.push_back(m);
|
|
}
|
|
}
|
|
if (!filtered.isEmpty()) {
|
|
results = filtered;
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
}
|
|
|
|
if (bootstrapProgram.isEmpty() || !matcher) {
|
|
QVariantMap ctx = launchSpec;
|
|
ctx.insert(QStringLiteral("provider"), provider);
|
|
failLaunch(QStringLiteral("unsupported monitored provider"), ctx);
|
|
return {};
|
|
}
|
|
|
|
if (QStandardPaths::findExecutable(bootstrapProgram).isEmpty()) {
|
|
const QVariantMap ctx = {
|
|
{QStringLiteral("gameId"), gameId},
|
|
{QStringLiteral("provider"), provider},
|
|
{QStringLiteral("program"), bootstrapProgram},
|
|
};
|
|
failLaunch(QStringLiteral("bootstrap executable not found"), ctx);
|
|
return {};
|
|
}
|
|
|
|
// Create session in Launching state
|
|
Session session;
|
|
session.sessionId = sessionId;
|
|
session.gameId = gameId;
|
|
session.displayName = displayName;
|
|
session.provider = provider;
|
|
session.startTime = QDateTime::currentDateTimeUtc();
|
|
m_sessions.insert(sessionId, session);
|
|
|
|
Q_EMIT SessionAdded(sessionToVariantMap(session, QStringLiteral("Launching")));
|
|
|
|
// Start bootstrap process (e.g. steam -applaunch or lutris ...)
|
|
auto *bootstrap = new QProcess(this);
|
|
bootstrap->start(bootstrapProgram, bootstrapArgs);
|
|
|
|
if (!bootstrap->waitForStarted(5000)) {
|
|
const QVariantMap ctx = {
|
|
{QStringLiteral("gameId"), gameId},
|
|
{QStringLiteral("provider"), provider},
|
|
{QStringLiteral("command"), command},
|
|
{QStringLiteral("error"), bootstrap->errorString()},
|
|
};
|
|
bootstrap->deleteLater();
|
|
QVariantMap finalState = sessionToVariantMap(session, QStringLiteral("Failed"));
|
|
Q_EMIT SessionRemoved(sessionId, finalState);
|
|
m_sessions.remove(sessionId);
|
|
failLaunch(QStringLiteral("failed to start bootstrap process"), ctx);
|
|
return {};
|
|
}
|
|
|
|
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) {
|
|
const auto it = m_sessions.find(sessionId);
|
|
if (it == m_sessions.end()) {
|
|
return;
|
|
}
|
|
if (it.value().scanner || it.value().stopping) {
|
|
return;
|
|
}
|
|
if (it.value().process) {
|
|
it.value().process->deleteLater();
|
|
it.value().process = nullptr;
|
|
}
|
|
});
|
|
|
|
// Start polling for game PID
|
|
auto *scanner = new ProcessScanner(this);
|
|
m_sessions[sessionId].scanner = scanner;
|
|
|
|
connect(scanner, &ProcessScanner::found, this, [this, sessionId](const QList<ProcessScanner::Match> &matches) {
|
|
QList<uint> pids;
|
|
pids.reserve(matches.size());
|
|
for (const auto &m : matches) {
|
|
pids.push_back(m.pid);
|
|
}
|
|
attachPidsToSession(sessionId, pids);
|
|
});
|
|
|
|
connect(scanner, &ProcessScanner::timedOut, this, [this, sessionId, launchSpec]() {
|
|
auto it = m_sessions.find(sessionId);
|
|
if (it == m_sessions.end()) {
|
|
return;
|
|
}
|
|
|
|
const bool stopping = it.value().stopping;
|
|
const QVariantMap ctx = {
|
|
{QStringLiteral("gameId"), it.value().gameId},
|
|
{QStringLiteral("provider"), it.value().provider},
|
|
};
|
|
|
|
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();
|
|
}
|
|
if (it.value().process) {
|
|
it.value().process->deleteLater();
|
|
}
|
|
Q_EMIT SessionRemoved(sessionId, finalState);
|
|
m_sessions.erase(it);
|
|
|
|
if (!stopping) {
|
|
failLaunch(QStringLiteral("timed out waiting for game process"), ctx);
|
|
}
|
|
});
|
|
|
|
scanner->pollUntilFound(std::move(matcher), 500, 20000);
|
|
|
|
return sessionId;
|
|
}
|
|
|
|
void GameCenterDaemon::attachPidsToSession(const QString &sessionId, const QList<uint> &pids)
|
|
{
|
|
auto it = m_sessions.find(sessionId);
|
|
if (it == m_sessions.end() || pids.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
if (it.value().scanner) {
|
|
it.value().scanner->cancel();
|
|
it.value().scanner->deleteLater();
|
|
it.value().scanner = nullptr;
|
|
}
|
|
|
|
it.value().mainPid = pids.first();
|
|
|
|
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);
|
|
|
|
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();
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
|
|
it.value().unitName = unitName;
|
|
|
|
const QDBusReply<QDBusObjectPath> getUnitReply = m_systemd.getUnit(unitName);
|
|
if (getUnitReply.isValid()) {
|
|
it.value().unitPath = getUnitReply.value();
|
|
}
|
|
|
|
watchSystemdUnit(sessionId, unitName, it.value().unitPath);
|
|
|
|
if (it.value().process) {
|
|
it.value().process->deleteLater();
|
|
it.value().process = nullptr;
|
|
}
|
|
|
|
if (it.value().stopping) {
|
|
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);
|
|
removeSessionInternal(sessionId, QStringLiteral("Stopped"));
|
|
return;
|
|
}
|
|
|
|
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Running")));
|
|
}
|
|
|
|
QString GameCenterDaemon::detectProvider(const QVariantMap &launchSpec)
|
|
{
|
|
// Explicit provider hint takes priority
|
|
QString explicit_provider;
|
|
{
|
|
QVariant v = launchSpec.value(QStringLiteral("provider"));
|
|
if (v.canConvert<QDBusVariant>()) {
|
|
v = v.value<QDBusVariant>().variant();
|
|
}
|
|
explicit_provider = v.toString().toLower();
|
|
}
|
|
if (!explicit_provider.isEmpty()) {
|
|
return explicit_provider;
|
|
}
|
|
|
|
// Auto-detect from command
|
|
const QString command = mapStringValue(launchSpec, {QStringLiteral("command"), QStringLiteral("launchCommand")});
|
|
if (command.contains(QLatin1String("steam://rungameid/")) || command.contains(QLatin1String("steam -applaunch"))) {
|
|
return QStringLiteral("steam");
|
|
}
|
|
if (command.startsWith(QLatin1String("lutris ")) || command.contains(QLatin1String("lutris:rungame"))) {
|
|
return QStringLiteral("lutris");
|
|
}
|
|
|
|
return QStringLiteral("manual");
|
|
}
|
|
|
|
QString GameCenterDaemon::extractSteamAppId(const QString &command)
|
|
{
|
|
// steam://rungameid/<appid>
|
|
static const QRegularExpression steamUrlRe(QStringLiteral("steam://rungameid/(\\d+)"));
|
|
QRegularExpressionMatch match = steamUrlRe.match(command);
|
|
if (match.hasMatch()) {
|
|
return match.captured(1);
|
|
}
|
|
|
|
// steam -applaunch <appid>
|
|
static const QRegularExpression applaunchRe(QStringLiteral("-applaunch\\s+(\\d+)"));
|
|
match = applaunchRe.match(command);
|
|
if (match.hasMatch()) {
|
|
return match.captured(1);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
QString GameCenterDaemon::extractLutrisId(const QString &command)
|
|
{
|
|
// lutris:rungameid/<id> or lutris:rungame/<slug>
|
|
static const QRegularExpression lutrisRe(QStringLiteral("lutris:rungame(?:id)?/([^\\s]+)"));
|
|
QRegularExpressionMatch match = lutrisRe.match(command);
|
|
if (match.hasMatch()) {
|
|
return match.captured(1);
|
|
}
|
|
|
|
// "lutris lutris:rungameid/<id>" form
|
|
static const QRegularExpression lutrisCmdRe(QStringLiteral("lutris\\s+lutris:rungame(?:id)?/([^\\s]+)"));
|
|
match = lutrisCmdRe.match(command);
|
|
if (match.hasMatch()) {
|
|
return match.captured(1);
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
void GameCenterDaemon::Stop(const QString &sessionId)
|
|
{
|
|
auto it = m_sessions.find(sessionId);
|
|
if (it == m_sessions.end()) {
|
|
return;
|
|
}
|
|
|
|
it.value().stopping = true;
|
|
|
|
if (it.value().scanner) {
|
|
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping")));
|
|
|
|
QPointer<QProcess> proc = it.value().process;
|
|
it.value().process = nullptr;
|
|
if (proc) {
|
|
connect(proc, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), proc, &QObject::deleteLater);
|
|
if (proc->state() != QProcess::NotRunning) {
|
|
proc->terminate();
|
|
QTimer::singleShot(5000, proc, [proc]() {
|
|
if (proc && proc->state() != QProcess::NotRunning) {
|
|
proc->kill();
|
|
}
|
|
});
|
|
} else {
|
|
proc->deleteLater();
|
|
}
|
|
}
|
|
|
|
if (!it.value().unitName.isEmpty()) {
|
|
m_systemd.stopUnit(it.value().unitName);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (it.value().unitName.isEmpty()) {
|
|
QVariantMap finalState = sessionToVariantMap(it.value(), QStringLiteral("Stopped"));
|
|
if (it.value().process) {
|
|
it.value().process->kill();
|
|
it.value().process->deleteLater();
|
|
}
|
|
Q_EMIT SessionRemoved(sessionId, finalState);
|
|
m_sessions.erase(it);
|
|
return;
|
|
}
|
|
|
|
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]() {
|
|
if (process && process->state() != QProcess::NotRunning) {
|
|
process->kill();
|
|
}
|
|
});
|
|
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping")));
|
|
} else {
|
|
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Error")));
|
|
}
|
|
return;
|
|
}
|
|
|
|
Q_EMIT SessionChanged(sessionToVariantMap(it.value(), QStringLiteral("Stopping")));
|
|
}
|
|
|
|
void GameCenterDaemon::StopByGameId(const QString &gameId)
|
|
{
|
|
if (gameId.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QStringList sessionsToStop;
|
|
sessionsToStop.reserve(m_sessions.size());
|
|
|
|
for (const auto &session : m_sessions) {
|
|
if (session.gameId == gameId) {
|
|
sessionsToStop.push_back(session.sessionId);
|
|
}
|
|
}
|
|
|
|
for (const QString &sessionId : sessionsToStop) {
|
|
Stop(sessionId);
|
|
}
|
|
}
|
|
|
|
QString GameCenterDaemon::sessionState(const Session &session)
|
|
{
|
|
if (session.stopping) {
|
|
return QStringLiteral("Stopping");
|
|
}
|
|
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");
|
|
}
|
|
return QStringLiteral("Unknown");
|
|
}
|
|
|
|
QVariantMap GameCenterDaemon::sessionToVariantMap(const Session &session, const QString &state) const
|
|
{
|
|
QVariantMap map;
|
|
map.insert(QStringLiteral("sessionId"), session.sessionId);
|
|
map.insert(QStringLiteral("gameId"), session.gameId);
|
|
if (!session.displayName.isEmpty()) {
|
|
map.insert(QStringLiteral("displayName"), session.displayName);
|
|
}
|
|
map.insert(QStringLiteral("unit"), session.unitName);
|
|
map.insert(QStringLiteral("unitName"), session.unitName);
|
|
map.insert(QStringLiteral("state"), state);
|
|
if (!session.provider.isEmpty()) {
|
|
map.insert(QStringLiteral("provider"), session.provider);
|
|
}
|
|
if (session.startTime.isValid()) {
|
|
map.insert(QStringLiteral("startTime"), session.startTime.toString(Qt::ISODate));
|
|
}
|
|
if (session.process && session.process->processId() > 0) {
|
|
map.insert(QStringLiteral("pid"), static_cast<qulonglong>(session.process->processId()));
|
|
} else if (session.mainPid > 0) {
|
|
map.insert(QStringLiteral("pid"), static_cast<qulonglong>(session.mainPid));
|
|
}
|
|
return map;
|
|
}
|
|
|
|
void GameCenterDaemon::failLaunch(const QString &reason, const QVariantMap &context)
|
|
{
|
|
QVariantMap error;
|
|
error.insert(QStringLiteral("error"), reason);
|
|
if (!context.isEmpty()) {
|
|
error.insert(QStringLiteral("context"), normalizeVariantMapForDbus(context));
|
|
const QString gameId = mapStringValue(context, {QStringLiteral("gameId"), QStringLiteral("id")});
|
|
if (!gameId.isEmpty()) {
|
|
error.insert(QStringLiteral("gameId"), gameId);
|
|
}
|
|
}
|
|
Q_EMIT LaunchFailed(error);
|
|
}
|
|
|
|
void GameCenterDaemon::recoverExistingSessions()
|
|
{
|
|
if (!m_systemd.isAvailable()) {
|
|
return;
|
|
}
|
|
|
|
const QDBusReply<SystemdUnitInfoList> reply = m_systemd.listUnits();
|
|
if (!reply.isValid()) {
|
|
return;
|
|
}
|
|
|
|
static const QString prefix = QStringLiteral("alakarte-game-");
|
|
static const QString suffix = QStringLiteral(".scope");
|
|
static const QString descPrefix = QStringLiteral("A-La-Karte game ");
|
|
|
|
const SystemdUnitInfoList units = reply.value();
|
|
for (const SystemdUnitInfo &unit : units) {
|
|
if (!unit.name.startsWith(prefix) || !unit.name.endsWith(suffix)) {
|
|
continue;
|
|
}
|
|
if (unit.activeState != QLatin1String("active")) {
|
|
continue;
|
|
}
|
|
|
|
const QString sessionId = unit.name.mid(prefix.length(), unit.name.length() - prefix.length() - suffix.length());
|
|
if (sessionId.isEmpty() || m_sessions.contains(sessionId)) {
|
|
continue;
|
|
}
|
|
|
|
QString gameId;
|
|
if (unit.description.startsWith(descPrefix)) {
|
|
gameId = unit.description.mid(descPrefix.length());
|
|
}
|
|
|
|
Session session;
|
|
session.sessionId = sessionId;
|
|
session.gameId = gameId;
|
|
session.unitName = unit.name;
|
|
session.unitPath = unit.objectPath;
|
|
|
|
const QList<uint> pids = m_systemd.scopePids(unit.objectPath);
|
|
if (!pids.isEmpty()) {
|
|
session.mainPid = pids.first();
|
|
}
|
|
|
|
m_sessions.insert(sessionId, session);
|
|
watchSystemdUnit(sessionId, session.unitName, session.unitPath);
|
|
Q_EMIT SessionAdded(sessionToVariantMap(session, QStringLiteral("Running")));
|
|
}
|
|
}
|
|
|
|
#include "gamecenterdaemon.moc"
|