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{
77 QProcessEnvironment env = process.processEnvironment();
78 if (env.isEmpty()) {
79 env = QProcessEnvironment::systemEnvironment();
80 }
81 env.insert(e: 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,
89 KIO::ApplicationLauncherJob::RunFlags flags,
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(first: actions.cbegin(), last: actions.cend(), pred: [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(execLine: 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(errorMsg: 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(process&: *instance->m_process, mod: 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(options: QUrl::RemoveFilename).toLocalFile();
138 }
139 instance->m_process->setWorkingDirectory(workingDir);
140
141 if ((flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles) == 0) {
142 // Remember we opened those urls, for the "recent documents" menu in kicker
143 for (const QUrl &url : urls) {
144 KRecentDocument::add(url, desktopEntryName: service->desktopEntryName());
145 }
146 }
147
148 instance->init(service, serviceEntryPath, userVisibleName: 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(execLine: 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(executableName: 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(execLine: executable);
196 instance->m_process->setProgram(exe: executable, args);
197 instance->initFromDesktopName(desktopName, execName: 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(name: desktopName);
213 if (service) {
214 if (m_executable.isEmpty()) {
215 m_executable = KIO::DesktopExecParser::executablePath(execLine: service->exec());
216 }
217 init(service, serviceEntryPath: service->entryPath(), userVisibleName: service->name(), asn);
218 return;
219 }
220 }
221 init(service: KService::Ptr(), serviceEntryPath: QString{}, userVisibleName: 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(path: 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(s: QLatin1String(".desktop"))) { // always true, in theory
241 m_desktopName.chop(n: strlen(s: ".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(a: 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: service.data(), silent_arg: &silent, wmclass_arg: &wmclass));
262 if (startup_notify) {
263 m_startupId.initId(id: 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.
269 const QString bin = KIO::DesktopExecParser::executableName(execLine: m_executable);
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(id: m_startupId, data);
290 }
291 }
292#else
293 Q_UNUSED(userVisibleName);
294#endif
295
296#if HAVE_WAYLAND
297 if (KWindowSystem::isPlatformWayland()) {
298 if (!asn.isEmpty()) {
299 m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), value: QString::fromUtf8(ba: asn));
300 } else {
301 bool silent;
302 QByteArray wmclass;
303 const bool startup_notify = service && KIOGuiPrivate::checkStartupNotify(service: service.data(), silent_arg: &silent, wmclass_arg: &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 sender: KWaylandExtras::self(),
314 signal: &KWaylandExtras::xdgActivationTokenArrived,
315 context: m_process.get(),
316 slot: [this, launchedSerial](int tokenSerial, const QString &token) {
317 if (tokenSerial == launchedSerial) {
318 m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), value: token);
319 m_waitingForXdgToken = false;
320 startProcess();
321 }
322 },
323 type: Qt::SingleShotConnection);
324 KWaylandExtras::requestXdgActivationToken(win: window, serial: launchedSerial, app_id: resolveServiceAlias());
325 }
326 }
327 }
328 }
329#endif
330
331 if (!m_waitingForXdgToken) {
332 startProcess();
333 }
334}
335
336void ForkingProcessRunner::startProcess()
337{
338 connect(sender: m_process.get(), signal: &QProcess::finished, context: this, slot: &ForkingProcessRunner::slotProcessExited);
339 connect(sender: m_process.get(), signal: &QProcess::started, context: this, slot: &ForkingProcessRunner::slotProcessStarted, type: Qt::QueuedConnection);
340 connect(sender: m_process.get(), signal: &QProcess::errorOccurred, context: this, slot: &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(sender: m_process.get(), signal: &QProcess::stateChanged, context: &loop, slot: &QEventLoop::quit);
349 QTimer::singleShot(interval: timeout, receiver: &loop, slot: &QEventLoop::quit);
350 loop.exec();
351 }
352 return m_process->waitForStarted(msecs: 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(errorString: 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(pid: static_cast<int>(m_pid));
378 KStartupInfo::sendChange(id: m_startupId, data);
379 KStartupInfo::resetStartupEnv();
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(pid: static_cast<int>(m_pid)); // announce this pid for the startup notification has finished
403 data.setHostname();
404 KStartupInfo::sendFinish(id: 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(a: c, fieldWidth: 2, base: 16, fillChar: 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(errorString: errorMsg);
447 deleteLater();
448 };
449 QMetaObject::invokeMethod(object: this, function&: func, type: Qt::QueuedConnection);
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(sep: 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"
510

source code of kio/src/gui/kprocessrunner.cpp