Changeset View
Standalone View
src/gui/kprocessrunner.cpp
- This file was added.
1 | /* This file is part of the KDE libraries | ||||
---|---|---|---|---|---|
2 | Copyright (c) 2020 David Faure <faure@kde.org> | ||||
3 | | ||||
4 | This library is free software; you can redistribute it and/or modify | ||||
5 | it under the terms of the GNU Lesser General Public License as published by | ||||
6 | the Free Software Foundation; either version 2 of the License or ( at | ||||
7 | your option ) version 3 or, at the discretion of KDE e.V. ( which shall | ||||
8 | act as a proxy as in section 14 of the GPLv3 ), any later version. | ||||
9 | | ||||
10 | This library is distributed in the hope that it will be useful, | ||||
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||||
13 | Library General Public License for more details. | ||||
14 | | ||||
15 | You should have received a copy of the GNU Lesser General Public License | ||||
16 | along with this library; see the file COPYING.LIB. If not, write to | ||||
17 | the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, | ||||
18 | Boston, MA 02110-1301, USA. | ||||
19 | */ | ||||
20 | | ||||
21 | #include "kprocessrunner_p.h" | ||||
22 | | ||||
23 | #include "kiogui_debug.h" | ||||
24 | #include "config-kiogui.h" | ||||
25 | | ||||
26 | #include "desktopexecparser.h" | ||||
27 | #include "krecentdocument.h" | ||||
28 | #include <KDesktopFile> | ||||
29 | #include <KLocalizedString> | ||||
30 | #include <KWindowSystem> | ||||
31 | | ||||
32 | #include <QDBusConnection> | ||||
33 | #include <QDBusInterface> | ||||
34 | #include <QDBusReply> | ||||
35 | #include <QFileInfo> | ||||
36 | #include <QGuiApplication> | ||||
37 | #include <QStandardPaths> | ||||
38 | | ||||
39 | static int s_instanceCount = 0; // for the unittest | ||||
40 | | ||||
davidedmundson: WId as an int is problematic for wayland.
Can we do QWindow*? it'll allow adding support in… | |||||
Actually if D28016 is approved, I can remove the windowId altogether. dfaure: Actually if D28016 is approved, I can remove the windowId altogether. | |||||
41 | KProcessRunner::KProcessRunner(const KService::Ptr &service, const QList<QUrl> &urls, WId windowId, | ||||
42 | KIO::ProcessLauncherJob::RunFlags flags, const QString &suggestedFileName, const QByteArray &asn) | ||||
43 | : m_process{new KProcess}, | ||||
44 | m_executable(KIO::DesktopExecParser::executablePath(service->exec())) | ||||
45 | { | ||||
46 | ++s_instanceCount; | ||||
47 | KIO::DesktopExecParser execParser(*service, urls); | ||||
48 | | ||||
49 | const QString realExecutable = execParser.resultingArguments().at(0); | ||||
50 | if (!QFileInfo::exists(realExecutable) && QStandardPaths::findExecutable(realExecutable).isEmpty()) { | ||||
51 | emitDelayedError(i18n("Could not find the program '%1'", realExecutable)); | ||||
52 | return; | ||||
53 | } | ||||
54 | | ||||
55 | execParser.setUrlsAreTempFiles(flags & KIO::ProcessLauncherJob::DeleteTemporaryFiles); | ||||
56 | execParser.setSuggestedFileName(suggestedFileName); | ||||
57 | const QStringList args = execParser.resultingArguments(); | ||||
58 | if (args.isEmpty()) { | ||||
59 | emitDelayedError(i18n("Error processing Exec field in %1", service->entryPath())); | ||||
60 | return; | ||||
61 | } | ||||
62 | //qDebug() << "KProcess args=" << args; | ||||
63 | *m_process << args; | ||||
64 | | ||||
65 | enum DiscreteGpuCheck { NotChecked, Present, Absent }; | ||||
66 | static DiscreteGpuCheck s_gpuCheck = NotChecked; | ||||
67 | | ||||
68 | if (service->runOnDiscreteGpu() && s_gpuCheck == NotChecked) { | ||||
69 | // Check whether we have a discrete gpu | ||||
70 | bool hasDiscreteGpu = false; | ||||
71 | QDBusInterface iface(QStringLiteral("org.kde.Solid.PowerManagement"), | ||||
72 | QStringLiteral("/org/kde/Solid/PowerManagement"), | ||||
73 | QStringLiteral("org.kde.Solid.PowerManagement"), | ||||
74 | QDBusConnection::sessionBus()); | ||||
75 | if (iface.isValid()) { | ||||
76 | QDBusReply<bool> reply = iface.call(QStringLiteral("hasDualGpu")); | ||||
77 | if (reply.isValid()) { | ||||
78 | hasDiscreteGpu = reply.value(); | ||||
79 | } | ||||
80 | } | ||||
81 | | ||||
82 | s_gpuCheck = hasDiscreteGpu ? Present : Absent; | ||||
83 | } | ||||
84 | | ||||
85 | if (service->runOnDiscreteGpu() && s_gpuCheck == Present) { | ||||
86 | m_process->setEnv(QStringLiteral("DRI_PRIME"), QStringLiteral("1")); | ||||
87 | } | ||||
88 | | ||||
89 | QString workingDir(service->workingDirectory()); | ||||
90 | if (workingDir.isEmpty() && !urls.isEmpty() && urls.first().isLocalFile()) { | ||||
91 | workingDir = urls.first().adjusted(QUrl::RemoveFilename).toLocalFile(); | ||||
92 | } | ||||
93 | m_process->setWorkingDirectory(workingDir); | ||||
94 | | ||||
95 | if ((flags & KIO::ProcessLauncherJob::DeleteTemporaryFiles) == 0) { | ||||
96 | // Remember we opened those urls, for the "recent documents" menu in kicker | ||||
97 | for (const QUrl &url : urls) { | ||||
98 | KRecentDocument::add(url, service->desktopEntryName()); | ||||
99 | } | ||||
100 | } | ||||
101 | | ||||
102 | // m_executable can be a full shell command, so <bin> here is not 100% reliable. | ||||
103 | // E.g. it could be "cd", which isn't an existing binary. It's just a heuristic anyway. | ||||
104 | const QString bin = KIO::DesktopExecParser::executableName(m_executable); | ||||
105 | init(service, bin, service->name(), service->icon(), windowId, asn); | ||||
106 | } | ||||
107 | | ||||
108 | KProcessRunner::KProcessRunner(const QString &cmd, const QString &execName, const QString &iconName, WId windowId, const QByteArray &asn, const QString &workingDirectory) | ||||
109 | : m_process{new KProcess}, | ||||
110 | m_executable(execName) | ||||
111 | { | ||||
112 | ++s_instanceCount; | ||||
113 | m_process->setShellCommand(cmd); | ||||
114 | if (!workingDirectory.isEmpty()) { | ||||
115 | m_process->setWorkingDirectory(workingDirectory); | ||||
116 | } | ||||
117 | QString bin = KIO::DesktopExecParser::executableName(m_executable); | ||||
118 | KService::Ptr service = KService::serviceByDesktopName(bin); | ||||
119 | init(service, bin, | ||||
120 | execName /*user-visible name*/, | ||||
121 | iconName, windowId, asn); | ||||
122 | } | ||||
123 | | ||||
124 | void KProcessRunner::init(const KService::Ptr &service, const QString &bin, const QString &userVisibleName, const QString &iconName, WId windowId, const QByteArray &asn) | ||||
125 | { | ||||
126 | if (service && !service->entryPath().isEmpty() | ||||
127 | && !KDesktopFile::isAuthorizedDesktopFile(service->entryPath())) { | ||||
128 | qCWarning(KIO_GUI) << "No authorization to execute " << service->entryPath(); | ||||
129 | emitDelayedError(i18n("You are not authorized to execute this file.")); | ||||
130 | return; | ||||
131 | } | ||||
132 | | ||||
133 | #if HAVE_X11 | ||||
134 | static bool isX11 = QGuiApplication::platformName() == QLatin1String("xcb"); | ||||
135 | if (isX11) { | ||||
136 | bool silent; | ||||
137 | QByteArray wmclass; | ||||
138 | const bool startup_notify = (asn != "0" && KIOGuiPrivate::checkStartupNotify(service.data(), &silent, &wmclass)); | ||||
139 | if (startup_notify) { | ||||
140 | m_startupId.initId(asn); | ||||
141 | m_startupId.setupStartupEnv(); | ||||
142 | KStartupInfoData data; | ||||
143 | data.setHostname(); | ||||
144 | data.setBin(bin); | ||||
145 | if (!userVisibleName.isEmpty()) { | ||||
146 | data.setName(userVisibleName); | ||||
147 | } else if (service && !service->name().isEmpty()) { | ||||
148 | data.setName(service->name()); | ||||
149 | } | ||||
150 | data.setDescription(i18n("Launching %1", data.name())); | ||||
151 | if (!iconName.isEmpty()) { | ||||
152 | data.setIcon(iconName); | ||||
153 | } else if (service && !service->icon().isEmpty()) { | ||||
154 | data.setIcon(service->icon()); | ||||
155 | } | ||||
156 | if (!wmclass.isEmpty()) { | ||||
157 | data.setWMClass(wmclass); | ||||
158 | } | ||||
159 | if (silent) { | ||||
160 | data.setSilent(KStartupInfoData::Yes); | ||||
161 | } | ||||
162 | data.setDesktop(KWindowSystem::currentDesktop()); | ||||
163 | if (windowId) { | ||||
164 | data.setLaunchedBy(windowId); | ||||
165 | } | ||||
166 | if (service && !service->entryPath().isEmpty()) { | ||||
167 | data.setApplicationId(service->entryPath()); | ||||
168 | } | ||||
169 | KStartupInfo::sendStartup(m_startupId, data); | ||||
170 | } | ||||
171 | } | ||||
172 | #else | ||||
173 | Q_UNUSED(bin); | ||||
174 | Q_UNUSED(userVisibleName); | ||||
175 | Q_UNUSED(iconName); | ||||
176 | #endif | ||||
177 | startProcess(); | ||||
178 | } | ||||
179 | | ||||
180 | void KProcessRunner::startProcess() | ||||
181 | { | ||||
182 | connect(m_process.get(), QOverload<int,QProcess::ExitStatus>::of(&QProcess::finished), | ||||
183 | this, &KProcessRunner::slotProcessExited); | ||||
184 | connect(m_process.get(), &QProcess::started, | ||||
185 | this, &KProcessRunner::slotProcessStarted, Qt::QueuedConnection); | ||||
186 | connect(m_process.get(), &QProcess::errorOccurred, | ||||
187 | this, &KProcessRunner::slotProcessError); | ||||
188 | | ||||
189 | m_process->start(); | ||||
190 | } | ||||
191 | | ||||
192 | bool KProcessRunner::waitForStarted() | ||||
193 | { | ||||
194 | return m_process->waitForStarted(); | ||||
It's the DBus calls that come before start that I want to get async, not the tiny bit between fork and the child process exec()'ing. Obviously we can do that piecemeal later, and it isn't a reason to delay this. I'm not sure how much of that we'll be able to get both async and into this "waitForStarted" pattern without event loops, but worst case we can do it for KF6. It's all a bit frustrating as FWICT no-one even uses the returned PID. davidedmundson: It's the DBus calls that come before start that I want to get async, not the tiny bit between… | |||||
Ah you mean inside DesktopExecParser? I thought about that, but it's easier to fix later when the other users of DesktopExecParser (which need sync) don't exist anymore. Yes, waitForStarted will also disappear in a pure-async KF6 world for this class. And at least until KF6 we can port all apps to ProcessLauncherJob already. Interesting point about the PID being useless, I never thought that this might be the case. But indeed, why would one need this... dfaure: Ah you mean inside DesktopExecParser? I thought about that, but it's easier to fix later when… | |||||
195 | } | ||||
196 | | ||||
197 | void KProcessRunner::slotProcessError(QProcess::ProcessError errorCode) | ||||
198 | { | ||||
199 | // E.g. the process crashed. | ||||
200 | // This is unlikely to happen while the ProcessLauncherJob is still connected to the KProcessRunner. | ||||
201 | // So the emit does nothing, this is really just for debugging. | ||||
202 | qCDebug(KIO_GUI) << m_executable << "error=" << errorCode << m_process->errorString(); | ||||
203 | Q_EMIT error(m_process->errorString()); | ||||
204 | } | ||||
205 | | ||||
206 | void KProcessRunner::slotProcessStarted() | ||||
207 | { | ||||
208 | m_pid = m_process->processId(); | ||||
209 | | ||||
210 | #if HAVE_X11 | ||||
211 | if (!m_startupId.isNull() && m_pid) { | ||||
212 | KStartupInfoData data; | ||||
213 | data.addPid(m_pid); | ||||
214 | KStartupInfo::sendChange(m_startupId, data); | ||||
215 | KStartupInfo::resetStartupEnv(); | ||||
216 | } | ||||
217 | #endif | ||||
218 | emit processStarted(); | ||||
219 | } | ||||
220 | | ||||
221 | KProcessRunner::~KProcessRunner() | ||||
222 | { | ||||
223 | // This destructor deletes m_process, since it's a unique_ptr. | ||||
224 | --s_instanceCount; | ||||
225 | } | ||||
226 | | ||||
227 | int KProcessRunner::instanceCount() | ||||
228 | { | ||||
229 | return s_instanceCount; | ||||
230 | } | ||||
231 | | ||||
232 | qint64 KProcessRunner::pid() const | ||||
233 | { | ||||
234 | return m_pid; | ||||
235 | } | ||||
236 | | ||||
237 | void KProcessRunner::terminateStartupNotification() | ||||
238 | { | ||||
239 | #if HAVE_X11 | ||||
240 | if (!m_startupId.isNull()) { | ||||
241 | KStartupInfoData data; | ||||
242 | data.addPid(m_pid); // announce this pid for the startup notification has finished | ||||
243 | data.setHostname(); | ||||
244 | KStartupInfo::sendFinish(m_startupId, data); | ||||
245 | } | ||||
246 | #endif | ||||
247 | } | ||||
248 | | ||||
249 | void KProcessRunner::emitDelayedError(const QString &errorMsg) | ||||
250 | { | ||||
251 | terminateStartupNotification(); | ||||
252 | // Use delayed invocation so the caller has time to connect to the signal | ||||
253 | QMetaObject::invokeMethod(this, [this, errorMsg]() { | ||||
254 | emit error(errorMsg); | ||||
255 | deleteLater(); | ||||
256 | }, Qt::QueuedConnection); | ||||
257 | } | ||||
258 | | ||||
259 | void KProcessRunner::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus) | ||||
260 | { | ||||
261 | qCDebug(KIO_GUI) << m_executable << "exitCode=" << exitCode << "exitStatus=" << exitStatus; | ||||
262 | terminateStartupNotification(); | ||||
263 | deleteLater(); | ||||
264 | } | ||||
265 | | ||||
266 | // This code is also used in klauncher (and KRun). | ||||
267 | bool KIOGuiPrivate::checkStartupNotify(const KService *service, bool *silent_arg, QByteArray *wmclass_arg) | ||||
268 | { | ||||
269 | bool silent = false; | ||||
270 | QByteArray wmclass; | ||||
271 | if (service && service->property(QStringLiteral("StartupNotify")).isValid()) { | ||||
272 | silent = !service->property(QStringLiteral("StartupNotify")).toBool(); | ||||
273 | wmclass = service->property(QStringLiteral("StartupWMClass")).toString().toLatin1(); | ||||
274 | } else if (service && service->property(QStringLiteral("X-KDE-StartupNotify")).isValid()) { | ||||
275 | silent = !service->property(QStringLiteral("X-KDE-StartupNotify")).toBool(); | ||||
276 | wmclass = service->property(QStringLiteral("X-KDE-WMClass")).toString().toLatin1(); | ||||
277 | } else { // non-compliant app | ||||
278 | if (service) { | ||||
279 | if (service->isApplication()) { // doesn't have .desktop entries needed, start as non-compliant | ||||
280 | wmclass = "0"; // krazy:exclude=doublequote_chars | ||||
281 | } else { | ||||
282 | return false; // no startup notification at all | ||||
283 | } | ||||
284 | } else { | ||||
285 | #if 0 | ||||
286 | // Create startup notification even for apps for which there shouldn't be any, | ||||
287 | // just without any visual feedback. This will ensure they'll be positioned on the proper | ||||
288 | // virtual desktop, and will get user timestamp from the ASN ID. | ||||
289 | wmclass = '0'; | ||||
290 | silent = true; | ||||
291 | #else // That unfortunately doesn't work, when the launched non-compliant application | ||||
292 | // launches another one that is compliant and there is any delay inbetween (bnc:#343359) | ||||
293 | return false; | ||||
294 | #endif | ||||
295 | } | ||||
296 | } | ||||
297 | if (silent_arg) { | ||||
298 | *silent_arg = silent; | ||||
299 | } | ||||
300 | if (wmclass_arg) { | ||||
301 | *wmclass_arg = wmclass; | ||||
302 | } | ||||
303 | return true; | ||||
304 | } |
WId as an int is problematic for wayland.
Can we do QWindow*? it'll allow adding support in future.
For the compatibility path we can loop through QApp->windows to find the object from windowId