mirror of https://github.com/cutefishos/core
feat(appruntime): add cutefish-appruntime and stop launching apps from the session
parent
39bb9f83dc
commit
4c297d8cf0
@ -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<uint>"/>
|
||||
</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();
|
||||
}
|
||||
Loading…
Reference in New Issue