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

KDE's Doxygen guidelines are available online.