KIO

kprocessrunner.cpp
1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
6*/
7
8#include "kprocessrunner_p.h"
9
10#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
11#include "systemd/scopedprocessrunner_p.h"
12#include "systemd/systemdprocessrunner_p.h"
13#endif
14
15#include "config-kiogui.h"
16#include "kiogui_debug.h"
17
18#include "desktopexecparser.h"
19#include "gpudetection_p.h"
20#include "krecentdocument.h"
21#include <KDesktopFile>
22#include <KLocalizedString>
23#include <KWindowSystem>
24
25#if HAVE_WAYLAND
26#include <KWaylandExtras>
27#endif
28
29#ifdef WITH_QTDBUS
30#include <QDBusConnection>
31#include <QDBusInterface>
32#include <QDBusReply>
33
34#include "dbusactivationrunner_p.h"
35#endif
36
37#include <QDir>
38#include <QFileInfo>
39#include <QGuiApplication>
40#include <QProcess>
41#include <QStandardPaths>
42#include <QString>
43#include <QTimer>
44#include <QUuid>
45
46#ifdef Q_OS_WIN
47#include "windows.h"
48
49#include "shellapi.h" // Must be included after "windows.h"
50#endif
51
52static int s_instanceCount = 0; // for the unittest
53
54KProcessRunner::KProcessRunner()
55 : m_process{new KProcess}
56{
57 ++s_instanceCount;
58}
59
60static KProcessRunner *makeInstance()
61{
62#if defined(Q_OS_LINUX) && defined(WITH_QTDBUS)
63 switch (SystemdProcessRunner::modeAvailable()) {
64 case KProcessRunner::SystemdAsService:
65 return new SystemdProcessRunner();
66 case KProcessRunner::SystemdAsScope:
67 return new ScopedProcessRunner();
68 default:
69#else
70 {
71#endif
72 return new ForkingProcessRunner();
73 }
74}
75
76#ifndef Q_OS_ANDROID
77static void modifyEnv(KProcess &process, QProcessEnvironment mod)
78{
80 if (env.isEmpty()) {
82 }
83 env.insert(mod);
84 process.setProcessEnvironment(env);
85}
86#endif
87
88KProcessRunner *KProcessRunner::fromApplication(const KService::Ptr &service,
89 const QString &serviceEntryPath,
90 const QList<QUrl> &urls,
91 const QString &actionName,
93 const QString &suggestedFileName,
94 const QByteArray &asn)
95{
96#ifdef WITH_QTDBUS
97 // special case for applicationlauncherjob
98 // FIXME: KProcessRunner is currently broken and fails to prepare the m_urls member
99 // DBusActivationRunner uses, which then only calls "Activate", not "Open".
100 // Possibly will need some special mode of DesktopExecParser
101 // for the D-Bus activation call scenario to handle URLs with protocols
102 // the invoked service/executable might not support.
103 KProcessRunner *instance;
104 const bool notYetSupportedOpenActivationNeeded = !urls.isEmpty();
105 if (!notYetSupportedOpenActivationNeeded && DBusActivationRunner::activationPossible(service, flags, suggestedFileName)) {
106 const auto actions = service->actions();
107 auto action = std::find_if(actions.cbegin(), actions.cend(), [actionName](const KServiceAction &action) {
108 return action.name() == actionName;
109 });
110 if (!actionName.isEmpty() && action == actions.cend()) {
111 qCWarning(KIO_GUI) << "Requested action" << actionName << "cannot be found for" << service->name();
112 }
113 instance = new DBusActivationRunner(action != actions.cend() ? action->name() : QString());
114 } else {
115 instance = makeInstance();
116 }
117#else
118 KProcessRunner *instance = makeInstance();
119#endif
120
121 if (!service->isValid()) {
122 instance->emitDelayedError(i18n("The desktop entry file\n%1\nis not valid.", serviceEntryPath));
123 return instance;
124 }
125 instance->m_executable = KIO::DesktopExecParser::executablePath(service->exec());
126
127 KIO::DesktopExecParser execParser(*service, urls);
128 execParser.setUrlsAreTempFiles(flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
129 execParser.setSuggestedFileName(suggestedFileName);
130 const QStringList args = execParser.resultingArguments();
131 if (args.isEmpty()) {
132 instance->emitDelayedError(execParser.errorMessage());
133 return instance;
134 }
135
136 qCDebug(KIO_GUI) << "Starting process:" << args;
137 *instance->m_process << args;
138
139#ifndef Q_OS_ANDROID
140 if (service->runOnDiscreteGpu()) {
141 modifyEnv(*instance->m_process, KIO::discreteGpuEnvironment());
142 }
143#endif
144
145 QString workingDir(service->workingDirectory());
146 if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) {
147 // systemd requires working directory to be normalized, or '~'
148 workingDir = QFileInfo(urls.first().toLocalFile()).canonicalPath();
149 }
150 instance->m_process->setWorkingDirectory(workingDir);
151
153 // Remember we opened those urls, for the "recent documents" menu in kicker
154 for (const QUrl &url : urls) {
155 KRecentDocument::add(url, service->desktopEntryName());
156 }
157 }
158
159 instance->init(service, serviceEntryPath, service->name(), asn);
160 return instance;
161}
162
163KProcessRunner *KProcessRunner::fromCommand(const QString &cmd,
164 const QString &desktopName,
165 const QString &execName,
166 const QByteArray &asn,
167 const QString &workingDirectory,
168 const QProcessEnvironment &environment)
169{
170 auto instance = makeInstance();
171
172 instance->m_executable = KIO::DesktopExecParser::executablePath(execName);
173 instance->m_cmd = cmd;
174#ifdef Q_OS_WIN
175 if (cmd.startsWith(QLatin1String("wt.exe")) || cmd.startsWith(QLatin1String("pwsh.exe")) || cmd.startsWith(QLatin1String("powershell.exe"))) {
176 instance->m_process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments *args) {
177 args->flags |= CREATE_NEW_CONSOLE;
178 args->startupInfo->dwFlags &= ~STARTF_USESTDHANDLES;
179 });
180 const int firstSpace = cmd.indexOf(QLatin1Char(' '));
181 instance->m_process->setProgram(cmd.left(firstSpace));
182 instance->m_process->setNativeArguments(cmd.mid(firstSpace + 1));
183 } else
184#endif
185 instance->m_process->setShellCommand(cmd);
186
187 instance->initFromDesktopName(desktopName, execName, asn, workingDirectory, environment);
188 return instance;
189}
190
191KProcessRunner *KProcessRunner::fromExecutable(const QString &executable,
192 const QStringList &args,
193 const QString &desktopName,
194 const QByteArray &asn,
195 const QString &workingDirectory,
196 const QProcessEnvironment &environment)
197{
198 const QString actualExec = QStandardPaths::findExecutable(executable);
199 if (actualExec.isEmpty()) {
200 qCInfo(KIO_GUI) << "Could not find an executable named:" << executable;
201 return {};
202 }
203
204 auto instance = makeInstance();
205
206 instance->m_executable = KIO::DesktopExecParser::executablePath(executable);
207 instance->m_process->setProgram(actualExec, args);
208 instance->initFromDesktopName(desktopName, executable, asn, workingDirectory, environment);
209 return instance;
210}
211
212void KProcessRunner::initFromDesktopName(const QString &desktopName,
213 const QString &execName,
214 const QByteArray &asn,
215 const QString &workingDirectory,
216 const QProcessEnvironment &environment)
217{
218 if (!workingDirectory.isEmpty()) {
219 m_process->setWorkingDirectory(workingDirectory);
220 }
221 m_process->setProcessEnvironment(environment);
222 if (!desktopName.isEmpty()) {
223 KService::Ptr service = KService::serviceByDesktopName(desktopName);
224 if (service) {
225 if (m_executable.isEmpty()) {
226 m_executable = KIO::DesktopExecParser::executablePath(service->exec());
227 }
228 init(service, service->entryPath(), service->name(), asn);
229 return;
230 }
231 }
232 init(KService::Ptr(), QString{}, execName /*user-visible name*/, asn);
233}
234
235void KProcessRunner::init(const KService::Ptr &service, const QString &serviceEntryPath, const QString &userVisibleName, const QByteArray &asn)
236{
237 m_serviceEntryPath = serviceEntryPath;
238 if (service && !serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(serviceEntryPath)) {
239 qCWarning(KIO_GUI) << "No authorization to execute" << serviceEntryPath;
240 emitDelayedError(i18n("You are not authorized to execute this file."));
241 return;
242 }
243
244 if (service) {
245 m_service = service;
246 // Store the desktop name, used by debug output and for the systemd unit name
247 m_desktopName = service->menuId();
248 if (m_desktopName.isEmpty() && m_executable == QLatin1String("systemsettings")) {
249 m_desktopName = QStringLiteral("systemsettings.desktop");
250 }
251 if (m_desktopName.endsWith(QLatin1String(".desktop"))) { // always true, in theory
252 m_desktopName.chop(strlen(".desktop"));
253 }
254 if (m_desktopName.isEmpty()) { // desktop files not in the menu
255 // desktopEntryName is lowercase so this is only a fallback
256 m_desktopName = service->desktopEntryName();
257 }
258 m_desktopFilePath = QFileInfo(serviceEntryPath).absoluteFilePath();
259 m_description = service->name();
260 if (!service->genericName().isEmpty()) {
261 m_description.append(QStringLiteral(" - %1").arg(service->genericName()));
262 }
263 } else {
264 m_description = userVisibleName;
265 }
266
267#if HAVE_X11
268 static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb");
269 if (isX11) {
270 bool silent;
271 QByteArray wmclass;
272 const bool startup_notify = (asn != "0" && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass));
273 if (startup_notify) {
274 m_startupId.initId(asn);
275 m_startupId.setupStartupEnv();
276 KStartupInfoData data;
277 data.setHostname();
278 // When it comes from a desktop file, m_executable can be a full shell command, so <bin> here is not 100% reliable.
279 // E.g. it could be "cd", which isn't an existing binary. It's just a heuristic anyway.
281 data.setBin(bin);
282 if (!userVisibleName.isEmpty()) {
283 data.setName(userVisibleName);
284 } else if (service && !service->name().isEmpty()) {
285 data.setName(service->name());
286 }
287 data.setDescription(i18n("Launching %1", data.name()));
288 if (service && !service->icon().isEmpty()) {
289 data.setIcon(service->icon());
290 }
291 if (!wmclass.isEmpty()) {
292 data.setWMClass(wmclass);
293 }
294 if (silent) {
295 data.setSilent(KStartupInfoData::Yes);
296 }
297 if (service && !serviceEntryPath.isEmpty()) {
298 data.setApplicationId(serviceEntryPath);
299 }
300 KStartupInfo::sendStartup(m_startupId, data);
301 }
302 }
303#else
304 Q_UNUSED(userVisibleName);
305#endif
306
307#if HAVE_WAYLAND
309 if (!asn.isEmpty()) {
310 m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), QString::fromUtf8(asn));
311 } else {
312 auto window = qGuiApp->focusWindow();
313 if (!window && !qGuiApp->allWindows().isEmpty()) {
314 window = qGuiApp->allWindows().constFirst();
315 }
316 if (window) {
317 const int launchedSerial = KWaylandExtras::lastInputSerial(window);
318 m_waitingForXdgToken = true;
319 connect(
320 KWaylandExtras::self(),
322 m_process.get(),
323 [this, launchedSerial](int tokenSerial, const QString &token) {
324 if (tokenSerial == launchedSerial) {
325 m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), token);
326 m_waitingForXdgToken = false;
327 startProcess();
328 }
329 },
331 KWaylandExtras::requestXdgActivationToken(window, launchedSerial, resolveServiceAlias());
332 }
333 }
334 }
335#endif
336
337 if (!m_waitingForXdgToken) {
338 startProcess();
339 }
340}
341
342void ForkingProcessRunner::startProcess()
343{
344 connect(m_process.get(), &QProcess::finished, this, &ForkingProcessRunner::slotProcessExited);
345 connect(m_process.get(), &QProcess::started, this, &ForkingProcessRunner::slotProcessStarted, Qt::QueuedConnection);
346 connect(m_process.get(), &QProcess::errorOccurred, this, &ForkingProcessRunner::slotProcessError);
347 m_process->start();
348}
349
350bool ForkingProcessRunner::waitForStarted(int timeout)
351{
352 if (m_process->state() == QProcess::NotRunning && m_waitingForXdgToken) {
353 QEventLoop loop;
354 QObject::connect(m_process.get(), &QProcess::stateChanged, &loop, &QEventLoop::quit);
355 QTimer::singleShot(timeout, &loop, &QEventLoop::quit);
356 loop.exec();
357 }
358 return m_process->waitForStarted(timeout);
359}
360
361void ForkingProcessRunner::slotProcessError(QProcess::ProcessError errorCode)
362{
363 // E.g. the process crashed.
364 // This is unlikely to happen while the ApplicationLauncherJob is still connected to the KProcessRunner.
365 // So the emit does nothing, this is really just for debugging.
366 qCDebug(KIO_GUI) << name() << "error=" << errorCode << m_process->errorString();
367 Q_EMIT error(m_process->errorString());
368}
369
370void ForkingProcessRunner::slotProcessStarted()
371{
372 setPid(m_process->processId());
373}
374
375void KProcessRunner::setPid(qint64 pid)
376{
377 if (!m_pid && pid) {
378 qCDebug(KIO_GUI) << "Setting PID" << pid << "for:" << name();
379 m_pid = pid;
380#if HAVE_X11
381 if (!m_startupId.isNull()) {
382 KStartupInfoData data;
383 data.addPid(static_cast<int>(m_pid));
384 KStartupInfo::sendChange(m_startupId, data);
386 }
387#endif
388 Q_EMIT processStarted(pid);
389 }
390}
391
392KProcessRunner::~KProcessRunner()
393{
394 // This destructor deletes m_process, since it's a unique_ptr.
395 --s_instanceCount;
396}
397
398int KProcessRunner::instanceCount()
399{
400 return s_instanceCount;
401}
402
403void KProcessRunner::terminateStartupNotification()
404{
405#if HAVE_X11
406 if (!m_startupId.isNull()) {
407 KStartupInfoData data;
408 data.addPid(static_cast<int>(m_pid)); // announce this pid for the startup notification has finished
409 data.setHostname();
410 KStartupInfo::sendFinish(m_startupId, data);
411 }
412#endif
413}
414
415QString KProcessRunner::name() const
416{
417 return !m_desktopName.isEmpty() ? m_desktopName : m_executable;
418}
419
420// Only alphanum, ':' and '_' allowed in systemd unit names
421QString KProcessRunner::escapeUnitName(const QString &input)
422{
423 QString res;
424 const QByteArray bytes = input.toUtf8();
425 for (const unsigned char c : bytes) {
426 if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == ':' || c == '_' || c == '.') {
427 res += QLatin1Char(c);
428 } else {
429 res += QStringLiteral("\\x%1").arg(c, 2, 16, QLatin1Char('0'));
430 }
431 }
432 return res;
433}
434
435QString KProcessRunner::resolveServiceAlias() const
436{
437 // Don't actually load aliased desktop file to avoid having to deal with recursion
438 QString servName = m_service ? m_service->aliasFor() : QString{};
439 if (servName.isEmpty()) {
440 servName = name();
441 }
442
443 return servName;
444}
445
446void KProcessRunner::emitDelayedError(const QString &errorMsg)
447{
448 qCWarning(KIO_GUI) << name() << errorMsg;
449
450 terminateStartupNotification();
451 // Use delayed invocation so the caller has time to connect to the signal
452 auto func = [this, errorMsg]() {
453 Q_EMIT error(errorMsg);
454 deleteLater();
455 };
457}
458
459void ForkingProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus)
460{
461 qCDebug(KIO_GUI) << name() << "exitCode=" << exitCode << "exitStatus=" << exitStatus;
462#ifdef Q_OS_UNIX
463 if (exitCode == 127) {
464#else
465 if (exitCode == 9009) {
466#endif
467 const QStringList args = m_cmd.split(QLatin1Char(' '));
468 emitDelayedError(xi18nc("@info", "The command <command>%1</command> could not be found.", args[0])); // Calls deleteLater().
469 } else {
470 terminateStartupNotification();
471 deleteLater();
472 }
473}
474
475bool KIOGuiPrivate::checkStartupNotify(const KService *service, bool *silent_arg, QByteArray *wmclass_arg)
476{
477 bool silent = false;
478 QByteArray wmclass;
479
480 if (service && service->startupNotify().has_value()) {
481 silent = !service->startupNotify().value();
482 wmclass = service->property<QByteArray>(QStringLiteral("StartupWMClass"));
483 } else { // non-compliant app
484 if (service) {
485 if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant
486 wmclass = "0"; // krazy:exclude=doublequote_chars
487 } else {
488 return false; // no startup notification at all
489 }
490 } else {
491#if 0
492 // Create startup notification even for apps for which there shouldn't be any,
493 // just without any visual feedback. This will ensure they'll be positioned on the proper
494 // virtual desktop, and will get user timestamp from the ASN ID.
495 wmclass = '0';
496 silent = true;
497#else // That unfortunately doesn't work, when the launched non-compliant application
498 // launches another one that is compliant and there is any delay in between (bnc:#343359)
499 return false;
500#endif
501 }
502 }
503 if (silent_arg) {
504 *silent_arg = silent;
505 }
506 if (wmclass_arg) {
507 *wmclass_arg = wmclass;
508 }
509 return true;
510}
511
512ForkingProcessRunner::ForkingProcessRunner()
513 : KProcessRunner()
514{
515}
516
517#include "moc_kprocessrunner_p.cpp"
static bool isAuthorizedDesktopFile(const QString &path)
@ DeleteTemporaryFiles
the URLs passed to the service will be deleted when it exits (if the URLs are local files)
QFlags< RunFlag > RunFlags
Stores a combination of RunFlag values.
Parses the Exec= line from a .desktop file, and process all the '%' placeholders, e....
static QString executablePath(const QString &execLine)
Given a full command line (e.g. the Exec= line from a .desktop file), extract the name of the executa...
static QString executableName(const QString &execLine)
Given a full command line (e.g. the Exec= line from a .desktop file), extract the name of the executa...
static void add(const QUrl &url)
Add a new item to the Recent Document menu.
QString desktopEntryName() const
QList< KServiceAction > actions() const
QString genericName() const
T property(const QString &name) const
static Ptr serviceByDesktopName(const QString &_name)
QString menuId() const
QString exec() const
QString icon() const
QString workingDirectory() const
std::optional< bool > startupNotify() const
bool isApplication() const
QExplicitlySharedDataPointer< KService > Ptr
bool runOnDiscreteGpu() const
void setHostname(const QByteArray &hostname=QByteArray())
const QString & name() const
void addPid(pid_t pid)
void setName(const QString &name)
void setWMClass(const QByteArray &wmclass)
void setIcon(const QString &icon)
void setBin(const QString &bin)
void setDescription(const QString &descr)
void setSilent(TriState state)
void setApplicationId(const QString &desktop)
static bool sendStartup(const KStartupInfoId &id, const KStartupInfoData &data)
static bool sendChange(const KStartupInfoId &id, const KStartupInfoData &data)
static void resetStartupEnv()
static bool sendFinish(const KStartupInfoId &id)
bool isValid() const
QString name() const
QString entryPath() const
void xdgActivationTokenArrived(int serial, const QString &token)
static Q_INVOKABLE void requestXdgActivationToken(QWindow *win, uint32_t serial, const QString &app_id)
static Q_INVOKABLE quint32 lastInputSerial(QWindow *window)
static bool isPlatformWayland()
QString xi18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
QWidget * window(QObject *job)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
QString name(StandardAction id)
QCA_EXPORT void init()
bool isEmpty() const const
int exec(ProcessEventsFlags flags)
void quit()
QString absoluteFilePath() const const
QString canonicalPath() const const
T & first()
bool isEmpty() const const
bool invokeMethod(QObject *context, Functor &&function, FunctorReturnType *ret)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void errorOccurred(QProcess::ProcessError error)
void finished(int exitCode, QProcess::ExitStatus exitStatus)
QProcessEnvironment processEnvironment() const const
void setProcessEnvironment(const QProcessEnvironment &environment)
void started()
void stateChanged(QProcess::ProcessState newState)
void insert(const QProcessEnvironment &e)
bool isEmpty() const const
QProcessEnvironment systemEnvironment()
QString findExecutable(const QString &executableName, const QStringList &paths)
QString arg(Args &&... args) const const
QString fromUtf8(QByteArrayView str)
qsizetype indexOf(QChar ch, qsizetype from, Qt::CaseSensitivity cs) const const
bool isEmpty() const const
QString left(qsizetype n) const const
QString mid(qsizetype position, qsizetype n) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toUtf8() const const
SingleShotConnection
QTextStream & bin(QTextStream &stream)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri May 2 2025 12:02:24 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.