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