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