From b4ea1e83c077e1a2d20df305565c3faa201167c0 Mon Sep 17 00:00:00 2001 From: reionwong Date: Sat, 5 Sep 2026 03:06:12 -0400 Subject: [PATCH] feat(applications): route launching and quitting through the app runtime --- applications/CMakeLists.txt | 2 + applications/applicationlauncher.cpp | 30 ++---- applications/applicationlauncher.h | 7 +- applications/applicationregistry.cpp | 6 ++ applications/applicationruntime.cpp | 152 +++++++++++++++++++++++++++ applications/applicationruntime.h | 44 ++++++++ applications/desktopentry.cpp | 8 +- 7 files changed, 227 insertions(+), 22 deletions(-) create mode 100644 applications/applicationruntime.cpp create mode 100644 applications/applicationruntime.h diff --git a/applications/CMakeLists.txt b/applications/CMakeLists.txt index 427c39f..60b4f7f 100644 --- a/applications/CMakeLists.txt +++ b/applications/CMakeLists.txt @@ -3,6 +3,8 @@ find_package(Qt6 REQUIRED COMPONENTS Core DBus Gui Qml) set(CUTEFISH_APPLICATIONS_SRCS applicationlauncher.cpp applicationlauncher.h + applicationruntime.cpp + applicationruntime.h applicationregistry.cpp applicationregistry.h desktopentry.cpp diff --git a/applications/applicationlauncher.cpp b/applications/applicationlauncher.cpp index 3865783..674145e 100644 --- a/applications/applicationlauncher.cpp +++ b/applications/applicationlauncher.cpp @@ -1,26 +1,16 @@ #include "applicationlauncher.h" - -#include -#include -#include +#include "applicationruntime.h" bool ApplicationLauncher::startDetached(const QStringList &command, - const QString &workingDirectory) + const QString &workingDirectory, + const QString &appId) { - if (command.isEmpty() || command.first().isEmpty()) - return false; - - const QString program = command.first(); - const QStringList arguments = command.mid(1); - - // Keep using the session launch service when it is available. It applies - // the desktop session's environment and startup handling consistently. - QDBusInterface session("com.cutefish.Session", "/Session", - "com.cutefish.Session", QDBusConnection::sessionBus()); - if (session.isValid()) { - session.asyncCall("launch", program, arguments); - return true; - } + // Every application start of the session goes through the runtime, which + // falls back to starting the process here when it is not running. + return ApplicationRuntime::instance()->launchCommand(command, workingDirectory, appId); +} - return QProcess::startDetached(program, arguments, workingDirectory); +bool ApplicationLauncher::quit(const QString &appId) +{ + return ApplicationRuntime::instance()->quitApplication(appId); } diff --git a/applications/applicationlauncher.h b/applications/applicationlauncher.h index f2a2051..ad915a8 100644 --- a/applications/applicationlauncher.h +++ b/applications/applicationlauncher.h @@ -6,6 +6,11 @@ class ApplicationLauncher { public: + // appId is only a hint for the runtime: it is the key an instance is + // tracked and can later be quit under. static bool startDetached(const QStringList &command, - const QString &workingDirectory = QString()); + const QString &workingDirectory = QString(), + const QString &appId = QString()); + + static bool quit(const QString &appId); }; diff --git a/applications/applicationregistry.cpp b/applications/applicationregistry.cpp index 5045e86..e72f5d1 100644 --- a/applications/applicationregistry.cpp +++ b/applications/applicationregistry.cpp @@ -1,4 +1,5 @@ #include "applicationregistry.h" +#include "applicationruntime.h" #include #include @@ -427,6 +428,11 @@ void registerApplicationsQmlTypes() [](QQmlEngine *, QJSEngine *) -> QObject * { return ApplicationRegistry::instance(); }); + qmlRegisterSingletonType( + "Cutefish.Applications", 1, 0, "AppRuntime", + [](QQmlEngine *, QJSEngine *) -> QObject * { + return ApplicationRuntime::instance(); + }); qmlRegisterUncreatableType( "Cutefish.Applications", 1, 0, "DesktopEntry", QStringLiteral("DesktopEntry objects are provided by DesktopEntries")); diff --git a/applications/applicationruntime.cpp b/applications/applicationruntime.cpp new file mode 100644 index 0000000..d245a85 --- /dev/null +++ b/applications/applicationruntime.cpp @@ -0,0 +1,152 @@ +#include "applicationruntime.h" + +#include +#include +#include +#include + +static const char *kService = "com.cutefish.AppRuntime"; +static const char *kPath = "/AppRuntime"; + +ApplicationRuntime *ApplicationRuntime::instance() +{ + static ApplicationRuntime *runtime = new ApplicationRuntime; + return runtime; +} + +ApplicationRuntime::ApplicationRuntime(QObject *parent) + : QObject(parent) +{ +} + +QDBusInterface *ApplicationRuntime::interface() +{ + if (!m_interface) { + m_interface = new QDBusInterface(QLatin1String(kService), QLatin1String(kPath), + QLatin1String(kService), + QDBusConnection::sessionBus(), this); + // Launching is a blocking call from user interface code, so a runtime + // that stops answering must not freeze the caller for the default + // half minute. + m_interface->setTimeout(5000); + QDBusConnection::sessionBus().connect(QLatin1String(kService), QLatin1String(kPath), + QLatin1String(kService), + QStringLiteral("applicationLaunched"), this, + SIGNAL(applicationLaunched(QString, quint32))); + QDBusConnection::sessionBus().connect(QLatin1String(kService), QLatin1String(kPath), + QLatin1String(kService), + QStringLiteral("applicationQuit"), this, + SIGNAL(applicationQuit(QString, quint32))); + } + + return m_interface; +} + +bool ApplicationRuntime::available() +{ + return interface()->isValid(); +} + +bool ApplicationRuntime::launchApplication(const QString &appId, const QStringList &arguments) +{ + if (appId.isEmpty()) + return false; + + QDBusInterface *iface = interface(); + if (!iface->isValid()) + return false; + + const QDBusReply reply = iface->call(QStringLiteral("launchApplication"), appId, arguments); + + return reply.isValid() && reply.value() != 0; +} + +bool ApplicationRuntime::launchCommand(const QStringList &command, + const QString &workingDirectory, + const QString &appId) +{ + if (command.isEmpty() || command.first().isEmpty()) + return false; + + QDBusInterface *iface = interface(); + if (iface->isValid()) { + const QDBusReply reply = iface->call(QStringLiteral("launchCommand"), appId, + command, workingDirectory); + if (reply.isValid() && reply.value() != 0) + return true; + } + + return QProcess::startDetached(command.first(), command.mid(1), workingDirectory); +} + +bool ApplicationRuntime::quitApplication(const QString &appId) +{ + if (appId.isEmpty()) + return false; + + QDBusInterface *iface = interface(); + if (!iface->isValid()) + return false; + + const QDBusReply reply = iface->call(QStringLiteral("quitApplication"), appId); + + return reply.isValid() && reply.value(); +} + +bool ApplicationRuntime::quitAll() +{ + QDBusInterface *iface = interface(); + if (!iface->isValid()) + return false; + + const QDBusReply reply = iface->call(QStringLiteral("quitAll")); + + return reply.isValid() && reply.value(); +} + +bool ApplicationRuntime::quitByPid(quint32 pid) +{ + if (pid == 0) + return false; + + QDBusInterface *iface = interface(); + if (!iface->isValid()) + return false; + + const QDBusReply reply = iface->call(QStringLiteral("quitByPid"), pid); + + return reply.isValid() && reply.value(); +} + +bool ApplicationRuntime::isRunning(const QString &appId) +{ + QDBusInterface *iface = interface(); + if (!iface->isValid()) + return false; + + const QDBusReply reply = iface->call(QStringLiteral("isRunning"), appId); + + return reply.isValid() && reply.value(); +} + +QStringList ApplicationRuntime::runningApplications() +{ + QDBusInterface *iface = interface(); + if (!iface->isValid()) + return QStringList(); + + const QDBusReply reply = iface->call(QStringLiteral("runningApplications")); + + return reply.isValid() ? reply.value() : QStringList(); +} + +QList ApplicationRuntime::pidsForApplication(const QString &appId) +{ + QDBusInterface *iface = interface(); + if (!iface->isValid()) + return QList(); + + const QDBusReply> reply = iface->call(QStringLiteral("pidsForApplication"), appId); + + return reply.isValid() ? reply.value() : QList(); +} diff --git a/applications/applicationruntime.h b/applications/applicationruntime.h new file mode 100644 index 0000000..dcad878 --- /dev/null +++ b/applications/applicationruntime.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +class QDBusInterface; + +/** + * Client side of com.cutefish.AppRuntime, the process that owns every + * application start of the session. Falls back to starting the process in + * place when the runtime is not available, so callers never have to care. + */ +class ApplicationRuntime : public QObject +{ + Q_OBJECT + +public: + static ApplicationRuntime *instance(); + + explicit ApplicationRuntime(QObject *parent = nullptr); + + bool available(); + + Q_INVOKABLE bool launchApplication(const QString &appId, + const QStringList &arguments = QStringList()); + Q_INVOKABLE bool launchCommand(const QStringList &command, + const QString &workingDirectory = QString(), + const QString &appId = QString()); + Q_INVOKABLE bool quitApplication(const QString &appId); + Q_INVOKABLE bool quitAll(); + Q_INVOKABLE bool quitByPid(quint32 pid); + Q_INVOKABLE bool isRunning(const QString &appId); + Q_INVOKABLE QStringList runningApplications(); + Q_INVOKABLE QList pidsForApplication(const QString &appId); + +signals: + void applicationLaunched(const QString &appId, quint32 pid); + void applicationQuit(const QString &appId, quint32 pid); + +private: + QDBusInterface *interface(); + + QDBusInterface *m_interface = nullptr; +}; diff --git a/applications/desktopentry.cpp b/applications/desktopentry.cpp index 50d89f4..a73c50b 100644 --- a/applications/desktopentry.cpp +++ b/applications/desktopentry.cpp @@ -1,6 +1,7 @@ #include "desktopentry.h" #include "applicationlauncher.h" +#include "applicationruntime.h" #include #include @@ -246,8 +247,13 @@ QStringList DesktopEntry::commandForArguments(const QStringList &arguments) cons bool DesktopEntry::launch(const QStringList &arguments) const { + // Prefer the entry id so the runtime can tie the process to this + // application; the expanded command is only the fallback path. + if (ApplicationRuntime::instance()->launchApplication(m_id, arguments)) + return true; + return ApplicationLauncher::startDetached(commandForArguments(arguments), - m_workingDirectory); + m_workingDirectory, m_id); } QStringList DesktopEntry::parseExec(const QString &exec)