File indexing completed on 2024-04-28 15:26:52
0001 /* 0002 This file is part of the KDE libraries 0003 SPDX-FileCopyrightText: 2020 David Faure <faure@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 0006 */ 0007 0008 #include "kprocessrunner_p.h" 0009 0010 #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) 0011 #include "systemd/scopedprocessrunner_p.h" 0012 #include "systemd/systemdprocessrunner_p.h" 0013 #endif 0014 0015 #include "config-kiogui.h" 0016 #include "dbusactivationrunner_p.h" 0017 #include "kiogui_debug.h" 0018 0019 #include "desktopexecparser.h" 0020 #include "gpudetection_p.h" 0021 #include "krecentdocument.h" 0022 #include <KDesktopFile> 0023 #include <KLocalizedString> 0024 #include <KWindowSystem> 0025 0026 #ifndef Q_OS_ANDROID 0027 #include <QDBusConnection> 0028 #include <QDBusInterface> 0029 #include <QDBusReply> 0030 #endif 0031 #include <QDir> 0032 #include <QFileInfo> 0033 #include <QGuiApplication> 0034 #include <QProcess> 0035 #include <QStandardPaths> 0036 #include <QString> 0037 #include <QTimer> 0038 #include <QUuid> 0039 0040 #ifdef Q_OS_WIN 0041 #include "windows.h" 0042 0043 #include "shellapi.h" // Must be included after "windows.h" 0044 #endif 0045 0046 static int s_instanceCount = 0; // for the unittest 0047 0048 KProcessRunner::KProcessRunner() 0049 : m_process{new KProcess} 0050 { 0051 ++s_instanceCount; 0052 } 0053 0054 static KProcessRunner *makeInstance() 0055 { 0056 #if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) 0057 if (SystemdProcessRunner::isAvailable()) { 0058 if (qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_SERVICE")) { 0059 return new SystemdProcessRunner(); 0060 } 0061 if (qEnvironmentVariableIntValue("KDE_APPLICATIONS_AS_SCOPE")) { 0062 return new ScopedProcessRunner(); 0063 } 0064 } 0065 #endif 0066 return new ForkingProcessRunner(); 0067 } 0068 0069 static void modifyEnv(KProcess &process, QProcessEnvironment mod) 0070 { 0071 QProcessEnvironment env = process.processEnvironment(); 0072 if (env.isEmpty()) { 0073 env = QProcessEnvironment::systemEnvironment(); 0074 } 0075 env.insert(mod); 0076 process.setProcessEnvironment(env); 0077 } 0078 0079 KProcessRunner *KProcessRunner::fromApplication(const KService::Ptr &service, 0080 const QString &serviceEntryPath, 0081 const QList<QUrl> &urls, 0082 KIO::ApplicationLauncherJob::RunFlags flags, 0083 const QString &suggestedFileName, 0084 const QByteArray &asn) 0085 { 0086 KProcessRunner *instance; 0087 // special case for applicationlauncherjob 0088 // FIXME: KProcessRunner is currently broken and fails to prepare the m_urls member 0089 // DBusActivationRunner uses, which then only calls "Activate", not "Open". 0090 // Possibly will need some special mode of DesktopExecParser 0091 // for the D-Bus activation call scenario to handle URLs with protocols 0092 // the invoked service/executable might not support. 0093 const bool notYetSupportedOpenActivationNeeded = !urls.isEmpty(); 0094 if (!notYetSupportedOpenActivationNeeded && DBusActivationRunner::activationPossible(service, flags, suggestedFileName)) { 0095 const auto actions = service->actions(); 0096 auto action = std::find_if(actions.cbegin(), actions.cend(), [service](const KServiceAction &action) { 0097 return action.exec() == service->exec(); 0098 }); 0099 instance = new DBusActivationRunner(action != actions.cend() ? action->name() : QString()); 0100 } else { 0101 instance = makeInstance(); 0102 } 0103 0104 if (!service->isValid()) { 0105 instance->emitDelayedError(i18n("The desktop entry file\n%1\nis not valid.", serviceEntryPath)); 0106 return instance; 0107 } 0108 instance->m_executable = KIO::DesktopExecParser::executablePath(service->exec()); 0109 0110 KIO::DesktopExecParser execParser(*service, urls); 0111 execParser.setUrlsAreTempFiles(flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles); 0112 execParser.setSuggestedFileName(suggestedFileName); 0113 const QStringList args = execParser.resultingArguments(); 0114 if (args.isEmpty()) { 0115 instance->emitDelayedError(execParser.errorMessage()); 0116 return instance; 0117 } 0118 0119 qCDebug(KIO_GUI) << "Starting process:" << args; 0120 *instance->m_process << args; 0121 0122 #ifndef Q_OS_ANDROID 0123 if (service->runOnDiscreteGpu()) { 0124 modifyEnv(*instance->m_process, KIO::discreteGpuEnvironment()); 0125 } 0126 #endif 0127 0128 QString workingDir(service->workingDirectory()); 0129 if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) { 0130 workingDir = urls.first().adjusted(QUrl::RemoveFilename).toLocalFile(); 0131 } 0132 instance->m_process->setWorkingDirectory(workingDir); 0133 0134 if ((flags & KIO::ApplicationLauncherJob::DeleteTemporaryFiles) == 0) { 0135 // Remember we opened those urls, for the "recent documents" menu in kicker 0136 for (const QUrl &url : urls) { 0137 KRecentDocument::add(url, service->desktopEntryName()); 0138 } 0139 } 0140 0141 instance->init(service, serviceEntryPath, service->name(), service->icon(), asn); 0142 return instance; 0143 } 0144 0145 KProcessRunner *KProcessRunner::fromCommand(const QString &cmd, 0146 const QString &desktopName, 0147 const QString &execName, 0148 const QString &iconName, 0149 const QByteArray &asn, 0150 const QString &workingDirectory, 0151 const QProcessEnvironment &environment) 0152 { 0153 auto instance = makeInstance(); 0154 0155 instance->m_executable = KIO::DesktopExecParser::executablePath(execName); 0156 instance->m_cmd = cmd; 0157 #ifdef Q_OS_WIN 0158 if (cmd.startsWith(QLatin1String("wt.exe")) || cmd.startsWith(QLatin1String("pwsh.exe")) || cmd.startsWith(QLatin1String("powershell.exe"))) { 0159 instance->m_process->setCreateProcessArgumentsModifier([](QProcess::CreateProcessArguments *args) { 0160 args->flags |= CREATE_NEW_CONSOLE; 0161 args->startupInfo->dwFlags &= ~STARTF_USESTDHANDLES; 0162 }); 0163 const int firstSpace = cmd.indexOf(QLatin1Char(' ')); 0164 instance->m_process->setProgram(cmd.left(firstSpace)); 0165 instance->m_process->setNativeArguments(cmd.mid(firstSpace + 1)); 0166 } else 0167 #endif 0168 instance->m_process->setShellCommand(cmd); 0169 0170 instance->initFromDesktopName(desktopName, execName, iconName, asn, workingDirectory, environment); 0171 return instance; 0172 } 0173 0174 KProcessRunner *KProcessRunner::fromExecutable(const QString &executable, 0175 const QStringList &args, 0176 const QString &desktopName, 0177 const QString &iconName, 0178 const QByteArray &asn, 0179 const QString &workingDirectory, 0180 const QProcessEnvironment &environment) 0181 { 0182 const QString actualExec = QStandardPaths::findExecutable(executable); 0183 if (actualExec.isEmpty()) { 0184 qCWarning(KIO_GUI) << "Could not find an executable named:" << executable; 0185 return {}; 0186 } 0187 0188 auto instance = makeInstance(); 0189 0190 instance->m_executable = KIO::DesktopExecParser::executablePath(executable); 0191 instance->m_process->setProgram(executable, args); 0192 instance->initFromDesktopName(desktopName, executable, iconName, asn, workingDirectory, environment); 0193 return instance; 0194 } 0195 0196 void KProcessRunner::initFromDesktopName(const QString &desktopName, 0197 const QString &execName, 0198 const QString &iconName, 0199 const QByteArray &asn, 0200 const QString &workingDirectory, 0201 const QProcessEnvironment &environment) 0202 { 0203 if (!workingDirectory.isEmpty()) { 0204 m_process->setWorkingDirectory(workingDirectory); 0205 } 0206 m_process->setProcessEnvironment(environment); 0207 if (!desktopName.isEmpty()) { 0208 KService::Ptr service = KService::serviceByDesktopName(desktopName); 0209 if (service) { 0210 if (m_executable.isEmpty()) { 0211 m_executable = KIO::DesktopExecParser::executablePath(service->exec()); 0212 } 0213 init(service, service->entryPath(), service->name(), service->icon(), asn); 0214 return; 0215 } 0216 } 0217 init(KService::Ptr(), QString{}, execName /*user-visible name*/, iconName, asn); 0218 } 0219 0220 void KProcessRunner::init(const KService::Ptr &service, 0221 const QString &serviceEntryPath, 0222 const QString &userVisibleName, 0223 const QString &iconName, 0224 const QByteArray &asn) 0225 { 0226 m_serviceEntryPath = serviceEntryPath; 0227 if (service && !serviceEntryPath.isEmpty() && !KDesktopFile::isAuthorizedDesktopFile(serviceEntryPath)) { 0228 qCWarning(KIO_GUI) << "No authorization to execute" << serviceEntryPath; 0229 emitDelayedError(i18n("You are not authorized to execute this file.")); 0230 return; 0231 } 0232 0233 #if HAVE_X11 0234 static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb"); 0235 if (isX11) { 0236 bool silent; 0237 QByteArray wmclass; 0238 const bool startup_notify = (asn != "0" && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass)); 0239 if (startup_notify) { 0240 m_startupId.initId(asn); 0241 m_startupId.setupStartupEnv(); 0242 KStartupInfoData data; 0243 data.setHostname(); 0244 // When it comes from a desktop file, m_executable can be a full shell command, so <bin> here is not 100% reliable. 0245 // E.g. it could be "cd", which isn't an existing binary. It's just a heuristic anyway. 0246 const QString bin = KIO::DesktopExecParser::executableName(m_executable); 0247 data.setBin(bin); 0248 if (!userVisibleName.isEmpty()) { 0249 data.setName(userVisibleName); 0250 } else if (service && !service->name().isEmpty()) { 0251 data.setName(service->name()); 0252 } 0253 data.setDescription(i18n("Launching %1", data.name())); 0254 if (!iconName.isEmpty()) { 0255 data.setIcon(iconName); 0256 } else if (service && !service->icon().isEmpty()) { 0257 data.setIcon(service->icon()); 0258 } 0259 if (!wmclass.isEmpty()) { 0260 data.setWMClass(wmclass); 0261 } 0262 if (silent) { 0263 data.setSilent(KStartupInfoData::Yes); 0264 } 0265 if (service && !serviceEntryPath.isEmpty()) { 0266 data.setApplicationId(serviceEntryPath); 0267 } 0268 KStartupInfo::sendStartup(m_startupId, data); 0269 } 0270 } 0271 #else 0272 Q_UNUSED(userVisibleName); 0273 Q_UNUSED(iconName); 0274 #endif 0275 0276 if (KWindowSystem::isPlatformWayland()) { 0277 if (!asn.isEmpty()) { 0278 m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), QString::fromUtf8(asn)); 0279 } else { 0280 bool silent; 0281 QByteArray wmclass; 0282 const bool startup_notify = service && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass); 0283 if (startup_notify && !silent) { 0284 auto window = qGuiApp->focusWindow(); 0285 if (!window && !qGuiApp->allWindows().isEmpty()) { 0286 window = qGuiApp->allWindows().constFirst(); 0287 } 0288 if (window) { 0289 const int launchedSerial = KWindowSystem::lastInputSerial(window); 0290 m_waitingForXdgToken = true; 0291 connect(this, &KProcessRunner::xdgActivationTokenArrived, m_process.get(), [this] { 0292 startProcess(); 0293 }); 0294 connect(KWindowSystem::self(), 0295 &KWindowSystem::xdgActivationTokenArrived, 0296 m_process.get(), 0297 [this, launchedSerial](int tokenSerial, const QString &token) { 0298 if (tokenSerial == launchedSerial) { 0299 m_process->setEnv(QStringLiteral("XDG_ACTIVATION_TOKEN"), token); 0300 Q_EMIT xdgActivationTokenArrived(); 0301 m_waitingForXdgToken = false; 0302 } 0303 }); 0304 KWindowSystem::requestXdgActivationToken(window, launchedSerial, maybeAliasedName(QFileInfo(m_serviceEntryPath).completeBaseName())); 0305 } 0306 } 0307 } 0308 } 0309 0310 if (service) { 0311 m_service = service; 0312 // Store the desktop name, used by debug output and for the systemd unit name 0313 m_desktopName = service->menuId(); 0314 if (m_desktopName.isEmpty() && m_executable == QLatin1String("systemsettings5") && m_service->hasServiceType(QLatin1String("KCModule"))) { 0315 m_desktopName = QStringLiteral("systemsettings.desktop"); 0316 } 0317 if (m_desktopName.endsWith(QLatin1String(".desktop"))) { // always true, in theory 0318 m_desktopName.chop(strlen(".desktop")); 0319 } 0320 if (m_desktopName.isEmpty()) { // desktop files not in the menu 0321 // desktopEntryName is lowercase so this is only a fallback 0322 m_desktopName = service->desktopEntryName(); 0323 } 0324 m_desktopFilePath = QFileInfo(serviceEntryPath).absoluteFilePath(); 0325 m_description = service->name(); 0326 if (!service->genericName().isEmpty()) { 0327 m_description.append(QStringLiteral(" - %1").arg(service->genericName())); 0328 } 0329 } else { 0330 m_description = userVisibleName; 0331 } 0332 0333 if (!m_waitingForXdgToken) { 0334 startProcess(); 0335 } 0336 } 0337 0338 void ForkingProcessRunner::startProcess() 0339 { 0340 connect(m_process.get(), qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this, &ForkingProcessRunner::slotProcessExited); 0341 connect(m_process.get(), &QProcess::started, this, &ForkingProcessRunner::slotProcessStarted, Qt::QueuedConnection); 0342 connect(m_process.get(), &QProcess::errorOccurred, this, &ForkingProcessRunner::slotProcessError); 0343 m_process->start(); 0344 } 0345 0346 bool ForkingProcessRunner::waitForStarted(int timeout) 0347 { 0348 if (m_process->state() == QProcess::NotRunning && m_waitingForXdgToken) { 0349 QEventLoop loop; 0350 QObject::connect(m_process.get(), &QProcess::stateChanged, &loop, &QEventLoop::quit); 0351 QTimer::singleShot(timeout, &loop, &QEventLoop::quit); 0352 loop.exec(); 0353 } 0354 return m_process->waitForStarted(timeout); 0355 } 0356 0357 void ForkingProcessRunner::slotProcessError(QProcess::ProcessError errorCode) 0358 { 0359 // E.g. the process crashed. 0360 // This is unlikely to happen while the ApplicationLauncherJob is still connected to the KProcessRunner. 0361 // So the emit does nothing, this is really just for debugging. 0362 qCDebug(KIO_GUI) << name() << "error=" << errorCode << m_process->errorString(); 0363 Q_EMIT error(m_process->errorString()); 0364 } 0365 0366 void ForkingProcessRunner::slotProcessStarted() 0367 { 0368 setPid(m_process->processId()); 0369 } 0370 0371 void KProcessRunner::setPid(qint64 pid) 0372 { 0373 if (!m_pid && pid) { 0374 qCDebug(KIO_GUI) << "Setting PID" << pid << "for:" << name(); 0375 m_pid = pid; 0376 #if HAVE_X11 0377 if (!m_startupId.isNull()) { 0378 KStartupInfoData data; 0379 data.addPid(static_cast<int>(m_pid)); 0380 KStartupInfo::sendChange(m_startupId, data); 0381 KStartupInfo::resetStartupEnv(); 0382 } 0383 #endif 0384 Q_EMIT processStarted(pid); 0385 } 0386 } 0387 0388 KProcessRunner::~KProcessRunner() 0389 { 0390 // This destructor deletes m_process, since it's a unique_ptr. 0391 --s_instanceCount; 0392 } 0393 0394 int KProcessRunner::instanceCount() 0395 { 0396 return s_instanceCount; 0397 } 0398 0399 void KProcessRunner::terminateStartupNotification() 0400 { 0401 #if HAVE_X11 0402 if (!m_startupId.isNull()) { 0403 KStartupInfoData data; 0404 data.addPid(static_cast<int>(m_pid)); // announce this pid for the startup notification has finished 0405 data.setHostname(); 0406 KStartupInfo::sendFinish(m_startupId, data); 0407 } 0408 #endif 0409 } 0410 0411 QString KProcessRunner::name() const 0412 { 0413 return !m_desktopName.isEmpty() ? m_desktopName : m_executable; 0414 } 0415 0416 // Only alphanum, ':' and '_' allowed in systemd unit names 0417 QString KProcessRunner::escapeUnitName(const QString &input) 0418 { 0419 QString res; 0420 const QByteArray bytes = input.toUtf8(); 0421 for (const auto &c : bytes) { 0422 if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == ':' || c == '_' || c == '.') { 0423 res += QLatin1Char(c); 0424 } else { 0425 res += QStringLiteral("\\x%1").arg(c, 2, 16, QLatin1Char('0')); 0426 } 0427 } 0428 return res; 0429 } 0430 0431 QString KProcessRunner::maybeAliasedName(const QString &pattern) const 0432 { 0433 // Don't actually load aliased desktop file to avoid having to deal with recursion 0434 QString servName = m_service ? m_service->aliasFor() : QString{}; 0435 if (servName.isEmpty()) { 0436 servName = name(); 0437 } 0438 0439 // As specified in "XDG standardization for applications" in https://systemd.io/DESKTOP_ENVIRONMENTS/ 0440 return pattern.arg(escapeUnitName(servName), QUuid::createUuid().toString(QUuid::Id128)); 0441 } 0442 0443 void KProcessRunner::emitDelayedError(const QString &errorMsg) 0444 { 0445 qCWarning(KIO_GUI) << errorMsg; 0446 terminateStartupNotification(); 0447 // Use delayed invocation so the caller has time to connect to the signal 0448 auto func = [this, errorMsg]() { 0449 Q_EMIT error(errorMsg); 0450 deleteLater(); 0451 }; 0452 QMetaObject::invokeMethod(this, func, Qt::QueuedConnection); 0453 } 0454 0455 void ForkingProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus) 0456 { 0457 qCDebug(KIO_GUI) << name() << "exitCode=" << exitCode << "exitStatus=" << exitStatus; 0458 terminateStartupNotification(); 0459 deleteLater(); 0460 #ifdef Q_OS_UNIX 0461 if (exitCode == 127) { 0462 #else 0463 if (exitCode == 9009) { 0464 #endif 0465 const QStringList args = m_cmd.split(QLatin1Char(' ')); 0466 emitDelayedError(xi18nc("@info", "The command <command>%1</command> could not be found.", args[0])); 0467 } 0468 } 0469 0470 // This code is also used in klauncher (and KRun). 0471 bool KIOGuiPrivate::checkStartupNotify(const KService *service, bool *silent_arg, QByteArray *wmclass_arg) 0472 { 0473 bool silent = false; 0474 QByteArray wmclass; 0475 if (service && service->property(QStringLiteral("StartupNotify")).isValid()) { 0476 silent = !service->property(QStringLiteral("StartupNotify")).toBool(); 0477 wmclass = service->property(QStringLiteral("StartupWMClass")).toString().toLatin1(); 0478 } else if (service && service->property(QStringLiteral("X-KDE-StartupNotify")).isValid()) { 0479 silent = !service->property(QStringLiteral("X-KDE-StartupNotify")).toBool(); 0480 wmclass = service->property(QStringLiteral("X-KDE-WMClass")).toString().toLatin1(); 0481 } else { // non-compliant app 0482 if (service) { 0483 if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant 0484 wmclass = "0"; // krazy:exclude=doublequote_chars 0485 } else { 0486 return false; // no startup notification at all 0487 } 0488 } else { 0489 #if 0 0490 // Create startup notification even for apps for which there shouldn't be any, 0491 // just without any visual feedback. This will ensure they'll be positioned on the proper 0492 // virtual desktop, and will get user timestamp from the ASN ID. 0493 wmclass = '0'; 0494 silent = true; 0495 #else // That unfortunately doesn't work, when the launched non-compliant application 0496 // launches another one that is compliant and there is any delay in between (bnc:#343359) 0497 return false; 0498 #endif 0499 } 0500 } 0501 if (silent_arg) { 0502 *silent_arg = silent; 0503 } 0504 if (wmclass_arg) { 0505 *wmclass_arg = wmclass; 0506 } 0507 return true; 0508 } 0509 0510 ForkingProcessRunner::ForkingProcessRunner() 0511 : KProcessRunner() 0512 { 0513 } 0514 0515 #include "moc_kprocessrunner_p.cpp"