Advance Wayland and KDE package bring-up
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
/*
|
||||
This file is part of the KDE libraries
|
||||
SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
|
||||
*/
|
||||
|
||||
#include "kprocessrunner_p.h"
|
||||
|
||||
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
||||
#include "systemd/scopedprocessrunner_p.h"
|
||||
#include "systemd/systemdprocessrunner_p.h"
|
||||
#endif
|
||||
|
||||
#include "config-kiogui.h"
|
||||
#include "kiogui_debug.h"
|
||||
|
||||
#include "desktopexecparser.h"
|
||||
#include "gpudetection_p.h"
|
||||
#include "krecentdocument.h"
|
||||
#include <KDesktopFile>
|
||||
#include <KLocalizedString>
|
||||
#include <KWindowSystem>
|
||||
|
||||
#if HAVE_WAYLAND
|
||||
#include <KWaylandExtras>
|
||||
#endif
|
||||
|
||||
#ifdef WITH_QTDBUS
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
|
||||
#include "dbusactivationrunner_p.h"
|
||||
#endif
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QGuiApplication>
|
||||
#include <QProcess>
|
||||
#include <QStandardPaths>
|
||||
#include <QString>
|
||||
#include <QTimer>
|
||||
#include <QUuid>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include "windows.h"
|
||||
|
||||
#include "shellapi.h" // Must be included after "windows.h"
|
||||
#endif
|
||||
|
||||
static int s_instanceCount = 0; // for the unittest
|
||||
|
||||
KProcessRunner::KProcessRunner()
|
||||
: m_process{new KProcess}
|
||||
{
|
||||
++s_instanceCount;
|
||||
}
|
||||
|
||||
static KProcessRunner *makeInstance()
|
||||
{
|
||||
#if defined(Q_OS_LINUX) && defined(WITH_QTDBUS)
|
||||
switch (SystemdProcessRunner::modeAvailable()) {
|
||||
case KProcessRunner::SystemdAsService:
|
||||
return new SystemdProcessRunner();
|
||||
case KProcessRunner::SystemdAsScope:
|
||||
return new ScopedProcessRunner();
|
||||
default:
|
||||
#else
|
||||
{
|
||||
#endif
|
||||
return new ForkingProcessRunner();
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
static void modifyEnv(KProcess &process, QProcessEnvironment mod)
|
||||
{
|
||||
QProcessEnvironment env = process.processEnvironment();
|
||||
if (env.isEmpty()) {
|
||||
env = QProcessEnvironment::systemEnvironment();
|
||||
}
|
||||
env.insert(mod);
|
||||
process.setProcessEnvironment(env);
|
||||
}
|
||||
#endif
|
||||
|
||||
KProcessRunner *KProcessRunner::fromApplication(const KService::Ptr &service,
|
||||
const QString &serviceEntryPath,
|
||||
const QList<QUrl> &urls,
|
||||
KIO::ApplicationLauncherJob::RunFlags flags,
|
||||
const QString &suggestedFileName,
|
||||
const QByteArray &asn)
|
||||
{
|
||||
#ifdef WITH_QTDBUS
|
||||
// special case for applicationlauncherjob
|
||||
// FIXME: KProcessRunner is currently broken and fails to prepare the m_urls member
|
||||
// DBusActivationRunner uses, which then only calls "Activate", not "Open".
|
||||
// Possibly will need some special mode of DesktopExecParser
|
||||
// for the D-Bus activation call scenario to handle URLs with protocols
|
||||
// the invoked service/executable might not support.
|
||||
KProcessRunner *instance;
|
||||
const bool notYetSupportedOpenActivationNeeded = !urls.isEmpty();
|
||||
if (!notYetSupportedOpenActivationNeeded && DBusActivationRunner::activationPossible(service, flags, suggestedFileName)) {
|
||||
const auto actions = service->actions();
|
||||
auto action = std::find_if(actions.cbegin(), actions.cend(), [service](const KServiceAction &action) {
|
||||
return action.exec() == service->exec();
|
||||
});
|
||||
instance = new DBusActivationRunner(action != actions.cend() ? action->name() : QString());
|
||||
} else {
|
||||
instance = makeInstance();
|
||||
}
|
||||
#else
|
||||
KProcessRunner *instance = makeInstance();
|
||||
#endif
|
||||
|
||||
if (!service->isValid()) {
|
||||
instance->emitDelayedError(i18n("The desktop entry file\n%1\nis not valid.", serviceEntryPath));
|
||||
return instance;
|
||||
}
|
||||
instance->m_executable = KIO::DesktopExecParser::executablePath(service->exec());
|
||||
|
||||
KIO::DesktopExecParser execParser(*service, urls);
|
||||
execParser.setUrlsAreTempFiles(flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
|
||||
execParser.setSuggestedFileName(suggestedFileName);
|
||||
const QStringList args = execParser.resultingArguments();
|
||||
if (args.isEmpty()) {
|
||||
instance->emitDelayedError(execParser.errorMessage());
|
||||
return instance;
|
||||
}
|
||||
|
||||
qCDebug(KIO_GUI) << "Starting process:" << args;
|
||||
*instance->m_process << args;
|
||||
|
||||
#ifndef Q_OS_ANDROID
|
||||
if (service->runOnDiscreteGpu()) {
|
||||
modifyEnv(*instance->m_process, KIO::discreteGpuEnvironment());
|
||||
}
|
||||
#endif
|
||||
|
||||
QString workingDir(service->workingDirectory());
|
||||
if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) {
|
||||
// systemd requires working directory to be normalized, or '~'
|
||||
workingDir = QFileInfo(urls.first().toLocalFile()).canonicalPath();
|
||||
}
|
||||
instance->m_process->setWorkingDirectory(workingDir);
|
||||
|
||||
if ((flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles) == 0) {
|
||||
// Remember we opened those urls, for the "recent documents" menu in kicker
|
||||
for (const QUrl &url : urls) {
|
||||
KRecentDocument::add(url, service->desktopEntryName());
|
||||
}
|
||||
}
|
||||
|
||||
instance->init(service, serviceEntryPath, service->name(), asn);
|
||||
return instance;
|
||||
}
|
||||
|
||||
KProcessRunner *KProcessRunner::fromCommand(const QString &cmd,
|
||||
const QString &desktopName,
|
||||
const QString &execName,
|
||||
const QByteArray &asn,
|
||||
const QString &workingDirectory,
|
||||
const QProcessEnvironment &environment)
|
||||
{
|
||||
auto instance = makeInstance();
|
||||
|
||||
instance->m_executable = KIO::DesktopExecParser::executablePath(execName);
|
||||
instance->m_cmd = cmd;
|
||||
#ifdef Q_OS_WIN
|
||||
if (cmd.startsWith(QLatin1String("wt.exe")) || cmd.startsWith(QLatin1String("pwsh.exe")) || cmd.startsWith(QLatin1String("powershell.exe"))) {
|
||||
instance->m_process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments *args) {
|
||||
args->flags |= CREATE_NEW_CONSOLE;
|
||||
args->startupInfo->dwFlags &= ~STARTF_USESTDHANDLES;
|
||||
});
|
||||
const int firstSpace = cmd.indexOf(QLatin1Char(' '));
|
||||
instance->m_process->setProgram(cmd.left(firstSpace));
|
||||
instance->m_process->setNativeArguments(cmd.mid(firstSpace + 1));
|
||||
} else
|
||||
#endif
|
||||
instance->m_process->setShellCommand(cmd);
|
||||
|
||||
instance->initFromDesktopName(desktopName, execName, asn, workingDirectory, environment);
|
||||
return instance;
|
||||
}
|
||||
|
||||
KProcessRunner *KProcessRunner::fromExecutable(const QString &executable,
|
||||
const QStringList &args,
|
||||
const QString &desktopName,
|
||||
const QByteArray &asn,
|
||||
const QString &workingDirectory,
|
||||
const QProcessEnvironment &environment)
|
||||
{
|
||||
const QString actualExec = QStandardPaths::findExecutable(executable);
|
||||
if (actualExec.isEmpty()) {
|
||||
qCInfo(KIO_GUI) << "Could not find an executable named:" << executable;
|
||||
return {};
|
||||
}
|
||||
|
||||
auto instance = makeInstance();
|
||||
|
||||
instance->m_executable = KIO::DesktopExecParser::executablePath(executable);
|
||||
instance->m_process->setProgram(actualExec, args);
|
||||
instance->initFromDesktopName(desktopName, executable, asn, workingDirectory, environment);
|
||||
return instance;
|
||||
}
|
||||
|
||||
void KProcessRunner::initFromDesktopName(const QString &desktopName,
|
||||
const QString &execName,
|
||||
const QByteArray &asn,
|
||||
const QString &workingDirectory,
|
||||
const QProcessEnvironment &environment)
|
||||
{
|
||||
if (!workingDirectory.isEmpty()) {
|
||||
m_process->setWorkingDirectory(workingDirectory);
|
||||
}
|
||||
m_process->setProcessEnvironment(environment);
|
||||
if (!desktopName.isEmpty()) {
|
||||
KService::Ptr service = KService::serviceByDesktopName(desktopName);
|
||||
if (service) {
|
||||
if (m_executable.isEmpty()) {
|
||||
m_executable = KIO::DesktopExecParser::executablePath(service->exec());
|
||||
}
|
||||
init(service, service->entryPath(), service->name(), asn);
|
||||
return;
|
||||
}
|
||||
}
|
||||
init(KService::Ptr(), QString{}, execName /*user-visible name*/, asn);
|
||||
}
|
||||
|
||||
void KProcessRunner::init(const KService::Ptr &service, const QString &serviceEntryPath, const QString &userVisibleName, const QByteArray &asn)
|
||||
{
|
||||
m_serviceEntryPath = serviceEntryPath;
|
||||
if (service && !serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(serviceEntryPath)) {
|
||||
qCWarning(KIO_GUI) << "No authorization to execute" << serviceEntryPath;
|
||||
emitDelayedError(i18n("You are not authorized to execute this file."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (service) {
|
||||
m_service = service;
|
||||
// Store the desktop name, used by debug output and for the systemd unit name
|
||||
m_desktopName = service->menuId();
|
||||
if (m_desktopName.isEmpty() && m_executable == QLatin1String("systemsettings")) {
|
||||
m_desktopName = QStringLiteral("systemsettings.desktop");
|
||||
}
|
||||
if (m_desktopName.endsWith(QLatin1String(".desktop"))) { // always true, in theory
|
||||
m_desktopName.chop(strlen(".desktop"));
|
||||
}
|
||||
if (m_desktopName.isEmpty()) { // desktop files not in the menu
|
||||
// desktopEntryName is lowercase so this is only a fallback
|
||||
m_desktopName = service->desktopEntryName();
|
||||
}
|
||||
m_desktopFilePath = QFileInfo(serviceEntryPath).absoluteFilePath();
|
||||
m_description = service->name();
|
||||
if (!service->genericName().isEmpty()) {
|
||||
m_description.append(QStringLiteral(" - %1").arg(service->genericName()));
|
||||
}
|
||||
} else {
|
||||
m_description = userVisibleName;
|
||||
}
|
||||
|
||||
#if HAVE_X11
|
||||
static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb");
|
||||
if (isX11) {
|
||||
bool silent;
|
||||
QByteArray wmclass;
|
||||
const bool startup_notify = (asn != "0" && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass));
|
||||
if (startup_notify) {
|
||||
m_startupId.initId(asn);
|
||||
m_startupId.setupStartupEnv();
|
||||
KStartupInfoData data;
|
||||
data.setHostname();
|
||||
// When it comes from a desktop file, m_executable can be a full shell command, so <bin> here is not 100% reliable.
|
||||
// E.g. it could be "cd", which isn't an existing binary. It's just a heuristic anyway.
|
||||
const QString bin = KIO::DesktopExecParser::executableName(m_executable);
|
||||
data.setBin(bin);
|
||||
if (!userVisibleName.isEmpty()) {
|
||||
data.setName(userVisibleName);
|
||||
} else if (service && !service->name().isEmpty()) {
|
||||
data.setName(service->name());
|
||||
}
|
||||
data.setDescription(i18n("Launching %1", data.name()));
|
||||
if (service && !service->icon().isEmpty()) {
|
||||
data.setIcon(service->icon());
|
||||
}
|
||||
if (!wmclass.isEmpty()) {
|
||||
data.setWMClass(wmclass);
|
||||
}
|
||||
if (silent) {
|
||||
data.setSilent(KStartupInfoData::Yes);
|
||||
}
|
||||
if (service && !serviceEntryPath.isEmpty()) {
|
||||
data.setApplicationId(serviceEntryPath);
|
||||
}
|
||||
KStartupInfo::sendStartup(m_startupId, data);
|
||||
}
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(userVisibleName);
|
||||
#endif
|
||||
|
||||
#if HAVE_WAYLAND
|
||||
if (KWindowSystem::isPlatformWayland()) {
|
||||
if (!asn.isEmpty()) {
|
||||
m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), QString::fromUtf8(asn));
|
||||
} else {
|
||||
auto window = qGuiApp->focusWindow();
|
||||
if (!window && !qGuiApp->allWindows().isEmpty()) {
|
||||
window = qGuiApp->allWindows().constFirst();
|
||||
}
|
||||
if (window) {
|
||||
const int launchedSerial = KWaylandExtras::lastInputSerial(window);
|
||||
m_waitingForXdgToken = true;
|
||||
connect(
|
||||
KWaylandExtras::self(),
|
||||
&KWaylandExtras::xdgActivationTokenArrived,
|
||||
m_process.get(),
|
||||
[this, launchedSerial](int tokenSerial, const QString &token) {
|
||||
if (tokenSerial == launchedSerial) {
|
||||
m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), token);
|
||||
m_waitingForXdgToken = false;
|
||||
startProcess();
|
||||
}
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
KWaylandExtras::requestXdgActivationToken(window, launchedSerial, resolveServiceAlias());
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!m_waitingForXdgToken) {
|
||||
startProcess();
|
||||
}
|
||||
}
|
||||
|
||||
void ForkingProcessRunner::startProcess()
|
||||
{
|
||||
connect(m_process.get(), &QProcess::finished, this, &ForkingProcessRunner::slotProcessExited);
|
||||
connect(m_process.get(), &QProcess::started, this, &ForkingProcessRunner::slotProcessStarted, Qt::QueuedConnection);
|
||||
connect(m_process.get(), &QProcess::errorOccurred, this, &ForkingProcessRunner::slotProcessError);
|
||||
m_process->start();
|
||||
}
|
||||
|
||||
bool ForkingProcessRunner::waitForStarted(int timeout)
|
||||
{
|
||||
if (m_process->state() == QProcess::NotRunning && m_waitingForXdgToken) {
|
||||
QEventLoop loop;
|
||||
QObject::connect(m_process.get(), &QProcess::stateChanged, &loop, &QEventLoop::quit);
|
||||
QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
}
|
||||
return m_process->waitForStarted(timeout);
|
||||
}
|
||||
|
||||
void ForkingProcessRunner::slotProcessError(QProcess::ProcessError errorCode)
|
||||
{
|
||||
// E.g. the process crashed.
|
||||
// This is unlikely to happen while the ApplicationLauncherJob is still connected to the KProcessRunner.
|
||||
// So the emit does nothing, this is really just for debugging.
|
||||
qCDebug(KIO_GUI) << name() << "error=" << errorCode << m_process->errorString();
|
||||
Q_EMIT error(m_process->errorString());
|
||||
}
|
||||
|
||||
void ForkingProcessRunner::slotProcessStarted()
|
||||
{
|
||||
setPid(m_process->processId());
|
||||
}
|
||||
|
||||
void KProcessRunner::setPid(qint64 pid)
|
||||
{
|
||||
if (!m_pid && pid) {
|
||||
qCDebug(KIO_GUI) << "Setting PID" << pid << "for:" << name();
|
||||
m_pid = pid;
|
||||
#if HAVE_X11
|
||||
if (!m_startupId.isNull()) {
|
||||
KStartupInfoData data;
|
||||
data.addPid(static_cast<int>(m_pid));
|
||||
KStartupInfo::sendChange(m_startupId, data);
|
||||
KStartupInfo::resetStartupEnv();
|
||||
}
|
||||
#endif
|
||||
Q_EMIT processStarted(pid);
|
||||
}
|
||||
}
|
||||
|
||||
KProcessRunner::~KProcessRunner()
|
||||
{
|
||||
// This destructor deletes m_process, since it's a unique_ptr.
|
||||
--s_instanceCount;
|
||||
}
|
||||
|
||||
int KProcessRunner::instanceCount()
|
||||
{
|
||||
return s_instanceCount;
|
||||
}
|
||||
|
||||
void KProcessRunner::terminateStartupNotification()
|
||||
{
|
||||
#if HAVE_X11
|
||||
if (!m_startupId.isNull()) {
|
||||
KStartupInfoData data;
|
||||
data.addPid(static_cast<int>(m_pid)); // announce this pid for the startup notification has finished
|
||||
data.setHostname();
|
||||
KStartupInfo::sendFinish(m_startupId, data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
QString KProcessRunner::name() const
|
||||
{
|
||||
return !m_desktopName.isEmpty() ? m_desktopName : m_executable;
|
||||
}
|
||||
|
||||
// Only alphanum, ':' and '_' allowed in systemd unit names
|
||||
QString KProcessRunner::escapeUnitName(const QString &input)
|
||||
{
|
||||
QString res;
|
||||
const QByteArray bytes = input.toUtf8();
|
||||
for (const unsigned char c : bytes) {
|
||||
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == ':' || c == '_' || c == '.') {
|
||||
res += QLatin1Char(c);
|
||||
} else {
|
||||
res += QStringLiteral("\\x%1").arg(c, 2, 16, QLatin1Char('0'));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
QString KProcessRunner::resolveServiceAlias() const
|
||||
{
|
||||
// Don't actually load aliased desktop file to avoid having to deal with recursion
|
||||
QString servName = m_service ? m_service->aliasFor() : QString{};
|
||||
if (servName.isEmpty()) {
|
||||
servName = name();
|
||||
}
|
||||
|
||||
return servName;
|
||||
}
|
||||
|
||||
void KProcessRunner::emitDelayedError(const QString &errorMsg)
|
||||
{
|
||||
qCWarning(KIO_GUI) << name() << errorMsg;
|
||||
|
||||
terminateStartupNotification();
|
||||
// Use delayed invocation so the caller has time to connect to the signal
|
||||
auto func = [this, errorMsg]() {
|
||||
Q_EMIT error(errorMsg);
|
||||
deleteLater();
|
||||
};
|
||||
QMetaObject::invokeMethod(this, func, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
void ForkingProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus)
|
||||
{
|
||||
qCDebug(KIO_GUI) << name() << "exitCode=" << exitCode << "exitStatus=" << exitStatus;
|
||||
#ifdef Q_OS_UNIX
|
||||
if (exitCode == 127) {
|
||||
#else
|
||||
if (exitCode == 9009) {
|
||||
#endif
|
||||
const QStringList args = m_cmd.split(QLatin1Char(' '));
|
||||
emitDelayedError(xi18nc("@info", "The command <command>%1</command> could not be found.", args[0])); // Calls deleteLater().
|
||||
} else {
|
||||
terminateStartupNotification();
|
||||
deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
bool KIOGuiPrivate::checkStartupNotify(const KService *service, bool *silent_arg, QByteArray *wmclass_arg)
|
||||
{
|
||||
bool silent = false;
|
||||
QByteArray wmclass;
|
||||
|
||||
if (service && service->startupNotify().has_value()) {
|
||||
silent = !service->startupNotify().value();
|
||||
wmclass = service->property<QByteArray>(QStringLiteral("StartupWMClass"));
|
||||
} else { // non-compliant app
|
||||
if (service) {
|
||||
if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant
|
||||
wmclass = "0"; // krazy:exclude=doublequote_chars
|
||||
} else {
|
||||
return false; // no startup notification at all
|
||||
}
|
||||
} else {
|
||||
#if 0
|
||||
// Create startup notification even for apps for which there shouldn't be any,
|
||||
// just without any visual feedback. This will ensure they'll be positioned on the proper
|
||||
// virtual desktop, and will get user timestamp from the ASN ID.
|
||||
wmclass = '0';
|
||||
silent = true;
|
||||
#else // That unfortunately doesn't work, when the launched non-compliant application
|
||||
// launches another one that is compliant and there is any delay in between (bnc:#343359)
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (silent_arg) {
|
||||
*silent_arg = silent;
|
||||
}
|
||||
if (wmclass_arg) {
|
||||
*wmclass_arg = wmclass;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ForkingProcessRunner::ForkingProcessRunner()
|
||||
: KProcessRunner()
|
||||
{
|
||||
}
|
||||
|
||||
#include "moc_kprocessrunner_p.cpp"
|
||||
Reference in New Issue
Block a user