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

KDE's Doxygen guidelines are available online.