diff --git a/CMakeLists.txt b/CMakeLists.txt index 6605366..1577887 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,10 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH}) include(GNUInstallDirs) include_directories("${CMAKE_CURRENT_SOURCE_DIR}") +add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../cutefish-framework/applications + ${CMAKE_CURRENT_BINARY_DIR}/cutefish-framework-applications-build) + +add_subdirectory(appruntime) add_subdirectory(polkit-agent) add_subdirectory(screen-brightness) add_subdirectory(session) diff --git a/appruntime/CMakeLists.txt b/appruntime/CMakeLists.txt new file mode 100644 index 0000000..5a6d34b --- /dev/null +++ b/appruntime/CMakeLists.txt @@ -0,0 +1,22 @@ +project(cutefish-appruntime) +set(TARGET cutefish-appruntime) + +set(SOURCES + appruntime.cpp + main.cpp +) + +qt6_add_dbus_adaptor(DBUS_SOURCES + com.cutefish.AppRuntime.xml + appruntime.h AppRuntime + appruntimeadaptor AppRuntimeAdaptor) +set_source_files_properties(${DBUS_SOURCES} PROPERTIES SKIP_AUTOGEN ON) + +add_executable(${TARGET} ${SOURCES} ${DBUS_SOURCES}) +target_link_libraries(${TARGET} + Cutefish::Applications + Qt6::Core + Qt6::DBus +) + +install(TARGETS ${TARGET} DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/appruntime/appruntime.cpp b/appruntime/appruntime.cpp new file mode 100644 index 0000000..87f5183 --- /dev/null +++ b/appruntime/appruntime.cpp @@ -0,0 +1,217 @@ +#include "appruntime.h" + +#include "applicationregistry.h" +#include "desktopentry.h" + +#include +#include +#include +#include + +#include +#include +#include + +static const int kTerminateTimeout = 5000; + +AppRuntime::AppRuntime(QObject *parent) + : QObject(parent) + , m_registry(ApplicationRegistry::instance()) +{ + // Instances are started detached, so there is no SIGCHLD to wait for. + m_reaper.setInterval(2000); + connect(&m_reaper, &QTimer::timeout, this, &AppRuntime::reap); +} + +uint AppRuntime::launchApplication(const QString &appId, const QStringList &arguments) +{ + if (appId.isEmpty()) + return 0; + + // Callers know an application either by its entry id or by the path of + // the desktop file they read. + DesktopEntry *entry = m_registry->byId(appId); + if (!entry && appId.contains(QLatin1Char('/'))) { + entry = m_registry->byPath(appId); + + if (!entry) + entry = m_registry->byId(QFileInfo(appId).completeBaseName()); + } + + if (!entry) { + qWarning() << "AppRuntime: no desktop entry for" << appId; + return 0; + } + + QStringList command = entry->commandForArguments(arguments); + if (command.isEmpty()) + return 0; + + if (entry->terminal()) { + QString terminal = QStandardPaths::findExecutable(QStringLiteral("x-terminal-emulator")); + if (terminal.isEmpty()) + terminal = QStandardPaths::findExecutable(QStringLiteral("xterm")); + + if (terminal.isEmpty()) + qWarning() << "AppRuntime: no terminal emulator for" << appId; + else + command = QStringList{terminal, QStringLiteral("-e")} + command; + } + + return startProcess(entry->id(), command, entry->workingDirectory()); +} + +uint AppRuntime::launchCommand(const QString &appId, const QStringList &command, + const QString &workingDirectory) +{ + if (command.isEmpty() || command.first().isEmpty()) + return 0; + + QString id = appId; + if (id.isEmpty()) + id = QFileInfo(command.first()).fileName(); + + return startProcess(id, command, workingDirectory); +} + +uint AppRuntime::startProcess(const QString &appId, const QStringList &command, + const QString &workingDirectory) +{ + QProcess process; + process.setProgram(command.first()); + process.setArguments(command.mid(1)); + process.setProcessEnvironment(QProcessEnvironment::systemEnvironment()); + + if (!workingDirectory.isEmpty()) + process.setWorkingDirectory(workingDirectory); + + qint64 pid = 0; + if (!process.startDetached(&pid) || pid <= 0) { + qWarning() << "AppRuntime: failed to start" << command << process.errorString(); + return 0; + } + + m_instances.insert(static_cast(pid), appId); + + if (!m_reaper.isActive()) + m_reaper.start(); + + emit applicationLaunched(appId, static_cast(pid)); + + return static_cast(pid); +} + +bool AppRuntime::quitApplication(const QString &appId) +{ + if (appId.isEmpty()) + return false; + + bool result = false; + for (const uint pid : pidsForApplication(appId)) + result |= terminate(pid); + + return result; +} + +// Logout path: one call instead of one per application. +bool AppRuntime::quitAll() +{ + bool result = false; + for (const uint pid : m_instances.keys()) + result |= terminate(pid); + + return result; +} + +bool AppRuntime::quitByPid(uint pid) +{ + return terminate(pid); +} + +bool AppRuntime::terminate(uint pid) +{ + if (!isSafeTarget(pid) || !isAlive(pid) || !isOwnedByUser(pid)) + return false; + + // Never signal a process group: Qt's detached children are led by an + // intermediate fork, so the group is not ours to interpret and a wrong + // one takes down the whole login session. + ::kill(static_cast(pid), SIGTERM); + + QTimer::singleShot(kTerminateTimeout, this, [pid] { + if (isAlive(pid) && isOwnedByUser(pid)) + ::kill(static_cast(pid), SIGKILL); + }); + + if (!m_reaper.isActive()) + m_reaper.start(); + + return true; +} + +bool AppRuntime::isRunning(const QString &appId) const +{ + return !pidsForApplication(appId).isEmpty(); +} + +QStringList AppRuntime::runningApplications() const +{ + QStringList result; + for (auto it = m_instances.constBegin(); it != m_instances.constEnd(); ++it) { + if (!result.contains(it.value())) + result.append(it.value()); + } + return result; +} + +QList AppRuntime::pidsForApplication(const QString &appId) const +{ + QList result; + for (auto it = m_instances.constBegin(); it != m_instances.constEnd(); ++it) { + if (it.value() == appId) + result.append(it.key()); + } + return result; +} + +void AppRuntime::reap() +{ + const QList pids = m_instances.keys(); + for (const uint pid : pids) { + if (isAlive(pid)) + continue; + + const QString appId = m_instances.take(pid); + emit applicationQuit(appId, pid); + } + + if (m_instances.isEmpty()) + m_reaper.stop(); +} + +bool AppRuntime::isAlive(uint pid) +{ + if (pid == 0) + return false; + + // Detached children are not ours to wait for, so a live pid is never a + // zombie here. + return ::kill(static_cast(pid), 0) == 0 || errno == EPERM; +} + +bool AppRuntime::isSafeTarget(uint pid) +{ + if (pid <= 1) + return false; + + // Refuse anything that would take the session down with the application. + return pid != static_cast(::getpid()) + && pid != static_cast(::getpgrp()) + && pid != static_cast(::getsid(0)); +} + +bool AppRuntime::isOwnedByUser(uint pid) +{ + const QFileInfo info(QStringLiteral("/proc/%1").arg(pid)); + return info.exists() && info.ownerId() == ::getuid(); +} diff --git a/appruntime/appruntime.h b/appruntime/appruntime.h new file mode 100644 index 0000000..edc69ed --- /dev/null +++ b/appruntime/appruntime.h @@ -0,0 +1,57 @@ +#ifndef APPRUNTIME_H +#define APPRUNTIME_H + +#include +#include +#include +#include + +class ApplicationRegistry; + +/** + * Owns every application start of the session: nothing else in the desktop + * spawns applications, so the runtime is the single place that knows which + * application a process belongs to and can therefore quit it again. + * + * startProcess() is the only place that actually creates a process, so a + * booster such as mapplauncherd can be plugged in there alone. + */ +class AppRuntime : public QObject +{ + Q_OBJECT + +public: + explicit AppRuntime(QObject *parent = nullptr); + +public slots: + uint launchApplication(const QString &appId, const QStringList &arguments); + uint launchCommand(const QString &appId, const QStringList &command, + const QString &workingDirectory); + bool quitApplication(const QString &appId); + bool quitAll(); + bool quitByPid(uint pid); + bool isRunning(const QString &appId) const; + QStringList runningApplications() const; + QList pidsForApplication(const QString &appId) const; + +signals: + void applicationLaunched(const QString &appId, uint pid); + void applicationQuit(const QString &appId, uint pid); + +private: + uint startProcess(const QString &appId, const QStringList &command, + const QString &workingDirectory); + bool terminate(uint pid); + void reap(); + + static bool isAlive(uint pid); + static bool isSafeTarget(uint pid); + static bool isOwnedByUser(uint pid); + + ApplicationRegistry *m_registry; + // pid -> application id + QHash m_instances; + QTimer m_reaper; +}; + +#endif // APPRUNTIME_H diff --git a/appruntime/com.cutefish.AppRuntime.xml b/appruntime/com.cutefish.AppRuntime.xml new file mode 100644 index 0000000..af7323c --- /dev/null +++ b/appruntime/com.cutefish.AppRuntime.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appruntime/main.cpp b/appruntime/main.cpp new file mode 100644 index 0000000..279d5d8 --- /dev/null +++ b/appruntime/main.cpp @@ -0,0 +1,25 @@ +#include "appruntime.h" +#include "appruntimeadaptor.h" + +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + app.setApplicationName(QStringLiteral("cutefish-appruntime")); + + AppRuntime runtime; + new AppRuntimeAdaptor(&runtime); + + QDBusConnection bus = QDBusConnection::sessionBus(); + if (!bus.registerService(QStringLiteral("com.cutefish.AppRuntime"))) { + qWarning() << "Another application runtime is already running"; + return 1; + } + + bus.registerObject(QStringLiteral("/AppRuntime"), &runtime); + + return app.exec(); +} diff --git a/session/CMakeLists.txt b/session/CMakeLists.txt index 85be550..c9864b6 100644 --- a/session/CMakeLists.txt +++ b/session/CMakeLists.txt @@ -20,6 +20,7 @@ set_source_files_properties(${DBUS_SOURCES} PROPERTIES SKIP_AUTOGEN ON) add_executable(${TARGET} ${SOURCES} ${DBUS_SOURCES}) target_link_libraries(${TARGET} + Cutefish::Applications Qt6::Core Qt6::DBus ) diff --git a/session/application.cpp b/session/application.cpp index 3e34ec4..22afa79 100644 --- a/session/application.cpp +++ b/session/application.cpp @@ -120,25 +120,6 @@ Application::Application(int &argc, char **argv) QTimer::singleShot(100, m_processManager, &ProcessManager::start); } -void Application::launch(const QString &exec, const QStringList &args) -{ - QProcess process; - process.setProgram(exec); - process.setProcessEnvironment(QProcessEnvironment::systemEnvironment()); - process.setArguments(args); - process.startDetached(); -} - -void Application::launch(const QString &exec, const QString &workingDir, const QStringList &args) -{ - QProcess process; - process.setProgram(exec); - process.setProcessEnvironment(QProcessEnvironment::systemEnvironment()); - process.setWorkingDirectory(workingDir); - process.setArguments(args); - process.startDetached(); -} - void Application::initEnvironments() { // Set defaults diff --git a/session/application.h b/session/application.h index 3d595a6..8fece74 100644 --- a/session/application.h +++ b/session/application.h @@ -60,9 +60,6 @@ public slots: m_networkProxyManager->update(); } - void launch(const QString &exec, const QStringList &args); - void launch(const QString &exec, const QString &workingDir, const QStringList &args); - private: void initEnvironments(); void initLanguage(); diff --git a/session/com.cutefish.Session.xml b/session/com.cutefish.Session.xml index d105403..08c0146 100644 --- a/session/com.cutefish.Session.xml +++ b/session/com.cutefish.Session.xml @@ -19,14 +19,5 @@ - - - - - - - - - diff --git a/session/processmanager.cpp b/session/processmanager.cpp index 3c291a1..f55f9ea 100644 --- a/session/processmanager.cpp +++ b/session/processmanager.cpp @@ -19,6 +19,8 @@ #include "processmanager.h" #include "application.h" +#include "applicationlauncher.h" +#include "applicationruntime.h" #include #include @@ -39,14 +41,11 @@ ProcessManager::ProcessManager(Application *app, QObject *parent) ProcessManager::~ProcessManager() { - for (QMap *processes : {&m_coreProcesses, &m_autostartProcesses}) { - QMapIterator i(*processes); - while (i.hasNext()) { - i.next(); - QProcess *p = i.value(); - delete p; - (*processes)[i.key()] = nullptr; - } + QMapIterator i(m_coreProcesses); + while (i.hasNext()) { + i.next(); + delete i.value(); + m_coreProcesses[i.key()] = nullptr; } } @@ -114,9 +113,14 @@ void ProcessManager::startAfterKWinReady() void ProcessManager::logout() { - // Close what we started ourselves, the window manager last since - // everything else is drawn on top of it. - stopProcesses(m_autostartProcesses); + // Applications belong to the runtime, so ask it to quit them, then close + // what we started ourselves. + ApplicationRuntime *runtime = ApplicationRuntime::instance(); + if (!runtime->quitAll()) { + for (const QString &appId : runtime->runningApplications()) + runtime->quitApplication(appId); + } + stopProcesses(m_coreProcesses); // KWin is started with --exit-with-session, so returning from this @@ -159,18 +163,6 @@ void ProcessManager::startDesktopProcess() // The status bar, dock, launcher, desktop and notifications are one process now. list << qMakePair(QString("cutefish-shell"), QStringList()); - // For CutefishOS. - if (QFile("/usr/bin/cutefish-welcome").exists() && - !QFile("/run/live/medium/live/filesystem.squashfs").exists()) { - QSettings settings("cutefishos", "login"); - - if (!settings.value("Finished", false).toBool()) { - list << qMakePair(QString("/usr/bin/cutefish-welcome"), QStringList()); - } else { - list << qMakePair(QString("/usr/bin/cutefish-welcome"), QStringList() << "-d"); - } - } - for (QPair pair : list) { QProcess *process = new QProcess; process->setProcessChannelMode(QProcess::ForwardedChannels); @@ -194,6 +186,19 @@ void ProcessManager::startDesktopProcess() } } + // For CutefishOS. An ordinary application, so the runtime starts it. + if (QFile("/usr/bin/cutefish-welcome").exists() && + !QFile("/run/live/medium/live/filesystem.squashfs").exists()) { + QSettings settings("cutefishos", "login"); + QStringList command{QStringLiteral("/usr/bin/cutefish-welcome")}; + + if (settings.value("Finished", false).toBool()) + command << QStringLiteral("-d"); + + ApplicationLauncher::startDetached(command, QString(), + QStringLiteral("cutefish-welcome")); + } + // Auto start QTimer::singleShot(100, this, &ProcessManager::loadAutoStartProcess); } @@ -201,6 +206,9 @@ void ProcessManager::startDesktopProcess() void ProcessManager::startDaemonProcess() { QList> list; + // The application runtime owns every application start, so it has to be up + // before anything that may want to launch one. + list << qMakePair(QString("cutefish-appruntime"), QStringList()); // This daemon provides the services used by the desktop components. list << qMakePair(QString("cutefish-services"), QStringList()); @@ -244,7 +252,8 @@ void ProcessManager::loadAutoStartProcess() const QString execValue = desktop.value("Exec").toString(); - if (execValue.contains("cutefish-services")) + if (execValue.contains("cutefish-services") || + execValue.contains("cutefish-appruntime")) continue; if (!execValue.isEmpty()) { @@ -253,19 +262,20 @@ void ProcessManager::loadAutoStartProcess() } } + // Autostart entries are applications: the runtime starts them, and keeps + // them quittable like any other application. for (const QString &exec : execList) { - QProcess *process = new QProcess; - process->setProgram(exec); - QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); - environment.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("wayland")); - process->setProcessEnvironment(environment); - process->start(); - process->waitForStarted(); + QStringList command = QProcess::splitCommand(exec); - if (process->exitCode() == 0) { - m_autostartProcesses.insert(exec, process); - } else { - process->deleteLater(); - } + // Autostart entries take no files, so their field codes expand to + // nothing rather than to a literal "%U" argument. + command.removeIf([](const QString &argument) { + return argument.size() == 2 && argument.startsWith(QLatin1Char('%')); + }); + + if (command.isEmpty()) + continue; + + ApplicationLauncher::startDetached(command); } } diff --git a/session/processmanager.h b/session/processmanager.h index d06868f..4616282 100644 --- a/session/processmanager.h +++ b/session/processmanager.h @@ -53,7 +53,6 @@ private: bool m_kwinReady = false; bool m_desktopStarted = false; QMap m_coreProcesses; - QMap m_autostartProcesses; };