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

KDE's Doxygen guidelines are available online.