feat(appruntime): add cutefish-appruntime and stop launching apps from the session

main
Reion Wong 3 weeks ago
parent 39bb9f83dc
commit 4c297d8cf0

@ -21,6 +21,10 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH})
include(GNUInstallDirs) include(GNUInstallDirs)
include_directories("${CMAKE_CURRENT_SOURCE_DIR}") 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(polkit-agent)
add_subdirectory(screen-brightness) add_subdirectory(screen-brightness)
add_subdirectory(session) add_subdirectory(session)

@ -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})

@ -0,0 +1,217 @@
#include "appruntime.h"
#include "applicationregistry.h"
#include "desktopentry.h"
#include <QDebug>
#include <QFileInfo>
#include <QProcess>
#include <QStandardPaths>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
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<uint>(pid), appId);
if (!m_reaper.isActive())
m_reaper.start();
emit applicationLaunched(appId, static_cast<uint>(pid));
return static_cast<uint>(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_t>(pid), SIGTERM);
QTimer::singleShot(kTerminateTimeout, this, [pid] {
if (isAlive(pid) && isOwnedByUser(pid))
::kill(static_cast<pid_t>(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<uint> AppRuntime::pidsForApplication(const QString &appId) const
{
QList<uint> 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<uint> 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_t>(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<uint>(::getpid())
&& pid != static_cast<uint>(::getpgrp())
&& pid != static_cast<uint>(::getsid(0));
}
bool AppRuntime::isOwnedByUser(uint pid)
{
const QFileInfo info(QStringLiteral("/proc/%1").arg(pid));
return info.exists() && info.ownerId() == ::getuid();
}

@ -0,0 +1,57 @@
#ifndef APPRUNTIME_H
#define APPRUNTIME_H
#include <QHash>
#include <QObject>
#include <QStringList>
#include <QTimer>
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<uint> 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<uint, QString> m_instances;
QTimer m_reaper;
};
#endif // APPRUNTIME_H

@ -0,0 +1,51 @@
<!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="com.cutefish.AppRuntime">
<!-- Launch the application with this desktop entry id. Returns 0 when the
entry is unknown or could not be started. -->
<method name="launchApplication">
<arg name="appId" type="s" direction="in"/>
<arg name="arguments" type="as" direction="in"/>
<arg name="pid" type="u" direction="out"/>
</method>
<!-- Launch a command that has no desktop entry. appId may be empty, it is
only the key the runtime tracks the instance under. -->
<method name="launchCommand">
<arg name="appId" type="s" direction="in"/>
<arg name="command" type="as" direction="in"/>
<arg name="workingDirectory" type="s" direction="in"/>
<arg name="pid" type="u" direction="out"/>
</method>
<method name="quitApplication">
<arg name="appId" type="s" direction="in"/>
<arg name="result" type="b" direction="out"/>
</method>
<method name="quitAll">
<arg name="result" type="b" direction="out"/>
</method>
<method name="quitByPid">
<arg name="pid" type="u" direction="in"/>
<arg name="result" type="b" direction="out"/>
</method>
<method name="isRunning">
<arg name="appId" type="s" direction="in"/>
<arg name="result" type="b" direction="out"/>
</method>
<method name="runningApplications">
<arg name="appIds" type="as" direction="out"/>
</method>
<method name="pidsForApplication">
<arg name="appId" type="s" direction="in"/>
<arg name="pids" type="au" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QList&lt;uint&gt;"/>
</method>
<signal name="applicationLaunched">
<arg name="appId" type="s"/>
<arg name="pid" type="u"/>
</signal>
<signal name="applicationQuit">
<arg name="appId" type="s"/>
<arg name="pid" type="u"/>
</signal>
</interface>
</node>

@ -0,0 +1,25 @@
#include "appruntime.h"
#include "appruntimeadaptor.h"
#include <QCoreApplication>
#include <QDBusConnection>
#include <QDebug>
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();
}

@ -20,6 +20,7 @@ set_source_files_properties(${DBUS_SOURCES} PROPERTIES SKIP_AUTOGEN ON)
add_executable(${TARGET} ${SOURCES} ${DBUS_SOURCES}) add_executable(${TARGET} ${SOURCES} ${DBUS_SOURCES})
target_link_libraries(${TARGET} target_link_libraries(${TARGET}
Cutefish::Applications
Qt6::Core Qt6::Core
Qt6::DBus Qt6::DBus
) )

@ -120,25 +120,6 @@ Application::Application(int &argc, char **argv)
QTimer::singleShot(100, m_processManager, &ProcessManager::start); 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() void Application::initEnvironments()
{ {
// Set defaults // Set defaults

@ -60,9 +60,6 @@ public slots:
m_networkProxyManager->update(); m_networkProxyManager->update();
} }
void launch(const QString &exec, const QStringList &args);
void launch(const QString &exec, const QString &workingDir, const QStringList &args);
private: private:
void initEnvironments(); void initEnvironments();
void initLanguage(); void initLanguage();

@ -19,14 +19,5 @@
<method name="updateNetworkProxy"> <method name="updateNetworkProxy">
<annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/> <annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
</method> </method>
<method name="launch">
<arg name="exec" type="s" direction="in"/>
<arg name="args" type="as" direction="in"/>
</method>
<method name="launch">
<arg name="exec" type="s" direction="in"/>
<arg name="workingDirectory" type="s" direction="in"/>
<arg name="args" type="as" direction="in"/>
</method>
</interface> </interface>
</node> </node>

@ -19,6 +19,8 @@
#include "processmanager.h" #include "processmanager.h"
#include "application.h" #include "application.h"
#include "applicationlauncher.h"
#include "applicationruntime.h"
#include <QCoreApplication> #include <QCoreApplication>
#include <QStandardPaths> #include <QStandardPaths>
@ -39,14 +41,11 @@ ProcessManager::ProcessManager(Application *app, QObject *parent)
ProcessManager::~ProcessManager() ProcessManager::~ProcessManager()
{ {
for (QMap<QString, QProcess *> *processes : {&m_coreProcesses, &m_autostartProcesses}) { QMapIterator<QString, QProcess *> i(m_coreProcesses);
QMapIterator<QString, QProcess *> i(*processes); while (i.hasNext()) {
while (i.hasNext()) { i.next();
i.next(); delete i.value();
QProcess *p = i.value(); m_coreProcesses[i.key()] = nullptr;
delete p;
(*processes)[i.key()] = nullptr;
}
} }
} }
@ -114,9 +113,14 @@ void ProcessManager::startAfterKWinReady()
void ProcessManager::logout() void ProcessManager::logout()
{ {
// Close what we started ourselves, the window manager last since // Applications belong to the runtime, so ask it to quit them, then close
// everything else is drawn on top of it. // what we started ourselves.
stopProcesses(m_autostartProcesses); ApplicationRuntime *runtime = ApplicationRuntime::instance();
if (!runtime->quitAll()) {
for (const QString &appId : runtime->runningApplications())
runtime->quitApplication(appId);
}
stopProcesses(m_coreProcesses); stopProcesses(m_coreProcesses);
// KWin is started with --exit-with-session, so returning from this // 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. // The status bar, dock, launcher, desktop and notifications are one process now.
list << qMakePair(QString("cutefish-shell"), QStringList()); 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<QString, QStringList> pair : list) { for (QPair<QString, QStringList> pair : list) {
QProcess *process = new QProcess; QProcess *process = new QProcess;
process->setProcessChannelMode(QProcess::ForwardedChannels); 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 // Auto start
QTimer::singleShot(100, this, &ProcessManager::loadAutoStartProcess); QTimer::singleShot(100, this, &ProcessManager::loadAutoStartProcess);
} }
@ -201,6 +206,9 @@ void ProcessManager::startDesktopProcess()
void ProcessManager::startDaemonProcess() void ProcessManager::startDaemonProcess()
{ {
QList<QPair<QString, QStringList>> list; QList<QPair<QString, QStringList>> 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. // This daemon provides the services used by the desktop components.
list << qMakePair(QString("cutefish-services"), QStringList()); list << qMakePair(QString("cutefish-services"), QStringList());
@ -244,7 +252,8 @@ void ProcessManager::loadAutoStartProcess()
const QString execValue = desktop.value("Exec").toString(); const QString execValue = desktop.value("Exec").toString();
if (execValue.contains("cutefish-services")) if (execValue.contains("cutefish-services") ||
execValue.contains("cutefish-appruntime"))
continue; continue;
if (!execValue.isEmpty()) { 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) { for (const QString &exec : execList) {
QProcess *process = new QProcess; QStringList command = QProcess::splitCommand(exec);
process->setProgram(exec);
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
environment.insert(QStringLiteral("QT_QPA_PLATFORM"), QStringLiteral("wayland"));
process->setProcessEnvironment(environment);
process->start();
process->waitForStarted();
if (process->exitCode() == 0) { // Autostart entries take no files, so their field codes expand to
m_autostartProcesses.insert(exec, process); // nothing rather than to a literal "%U" argument.
} else { command.removeIf([](const QString &argument) {
process->deleteLater(); return argument.size() == 2 && argument.startsWith(QLatin1Char('%'));
} });
if (command.isEmpty())
continue;
ApplicationLauncher::startDetached(command);
} }
} }

@ -53,7 +53,6 @@ private:
bool m_kwinReady = false; bool m_kwinReady = false;
bool m_desktopStarted = false; bool m_desktopStarted = false;
QMap<QString, QProcess *> m_coreProcesses; QMap<QString, QProcess *> m_coreProcesses;
QMap<QString, QProcess *> m_autostartProcesses;
}; };

Loading…
Cancel
Save