Changeset View
Changeset View
Standalone View
Standalone View
libtaskmanager/tasktools.cpp
Show All 21 Lines | |||||
22 | #include "abstracttasksmodel.h" | 22 | #include "abstracttasksmodel.h" | ||
23 | 23 | | |||
24 | #include <KConfigGroup> | 24 | #include <KConfigGroup> | ||
25 | #include <KDesktopFile> | 25 | #include <KDesktopFile> | ||
26 | #include <kemailsettings.h> | 26 | #include <kemailsettings.h> | ||
27 | #include <KFileItem> | 27 | #include <KFileItem> | ||
28 | #include <KMimeTypeTrader> | 28 | #include <KMimeTypeTrader> | ||
29 | #include <KRun> | 29 | #include <KRun> | ||
30 | #include <KSharedConfig> | 30 | #include <KServiceTypeTrader> | ||
31 | #include <processcore/processes.h> | ||||
32 | #include <processcore/process.h> | ||||
31 | 33 | | |||
32 | #include <QDir> | 34 | #include <QDir> | ||
33 | #include <QGuiApplication> | 35 | #include <QGuiApplication> | ||
36 | #include <QRegularExpression> | ||||
34 | #include <QScreen> | 37 | #include <QScreen> | ||
35 | 38 | | |||
36 | namespace TaskManager | 39 | namespace TaskManager | ||
37 | { | 40 | { | ||
38 | 41 | | |||
39 | AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon) | 42 | AppData appDataFromUrl(const QUrl &url, const QIcon &fallbackIcon) | ||
40 | { | 43 | { | ||
41 | AppData data; | 44 | AppData data; | ||
▲ Show 20 Lines • Show All 107 Lines • ▼ Show 20 Line(s) | 143 | if (KDesktopFile::isDesktopFile(desktopFile) && QFile::exists(desktopFile)) { | |||
149 | data.name = f.readName(); | 152 | data.name = f.readName(); | ||
150 | data.genericName = f.readGenericName(); | 153 | data.genericName = f.readGenericName(); | ||
151 | data.url = QUrl::fromLocalFile(desktopFile); | 154 | data.url = QUrl::fromLocalFile(desktopFile); | ||
152 | } | 155 | } | ||
153 | 156 | | |||
154 | return data; | 157 | return data; | ||
155 | } | 158 | } | ||
156 | 159 | | |||
160 | QUrl windowUrlFromMetadata(const QString &appId, quint32 pid, | ||||
161 | KSharedConfig::Ptr rulesConfig, const QString &xWindowsWMClassName) | ||||
162 | { | ||||
163 | if (!rulesConfig) { | ||||
164 | return QUrl(); | ||||
165 | } | ||||
166 | | ||||
167 | QUrl url; | ||||
168 | KService::List services; | ||||
169 | bool triedPid = false; | ||||
170 | | ||||
171 | if (!(appId.isEmpty() && xWindowsWMClassName.isEmpty())) { | ||||
172 | // Check to see if this wmClass matched a saved one ... | ||||
173 | KConfigGroup grp(rulesConfig, "Mapping"); | ||||
174 | KConfigGroup set(rulesConfig, "Settings"); | ||||
175 | | ||||
176 | // Evaluate MatchCommandLineFirst directives from config first. | ||||
177 | // Some apps have different launchers depending upon command line ... | ||||
178 | QStringList matchCommandLineFirst = set.readEntry("MatchCommandLineFirst", QStringList()); | ||||
179 | | ||||
180 | if (!appId.isEmpty() && matchCommandLineFirst.contains(appId)) { | ||||
181 | triedPid = true; | ||||
182 | services = servicesFromPid(pid, rulesConfig); | ||||
183 | } | ||||
184 | | ||||
185 | // Try to match using xWindowsWMClassName also. | ||||
186 | if (!xWindowsWMClassName.isEmpty() && matchCommandLineFirst.contains("::"+xWindowsWMClassName)) { | ||||
187 | triedPid = true; | ||||
188 | services = servicesFromPid(pid, rulesConfig); | ||||
189 | } | ||||
190 | | ||||
191 | if (!appId.isEmpty()) { | ||||
192 | // Evaluate any mapping rules that map to a specific .desktop file. | ||||
193 | QString mapped(grp.readEntry(appId + "::" + xWindowsWMClassName, QString())); | ||||
194 | | ||||
195 | if (mapped.endsWith(QLatin1String(".desktop"))) { | ||||
196 | url = QUrl(mapped); | ||||
197 | return url; | ||||
198 | } | ||||
199 | | ||||
200 | if (mapped.isEmpty()) { | ||||
201 | mapped = grp.readEntry(appId, QString()); | ||||
202 | | ||||
203 | if (mapped.endsWith(QLatin1String(".desktop"))) { | ||||
204 | url = QUrl(mapped); | ||||
205 | return url; | ||||
206 | } | ||||
207 | } | ||||
208 | | ||||
209 | // Some apps, such as Wine, cannot use xWindowsWMClassName to map to launcher name - as Wine itself is not a GUI app | ||||
210 | // So, Settings/ManualOnly lists window classes where the user will always have to manualy set the launcher ... | ||||
211 | QStringList manualOnly = set.readEntry("ManualOnly", QStringList()); | ||||
212 | | ||||
213 | if (!appId.isEmpty() && manualOnly.contains(appId)) { | ||||
214 | return url; | ||||
215 | } | ||||
216 | | ||||
217 | // Try matching both appId and xWindowsWMClassName against StartupWMClass. | ||||
218 | // We do this before evaluating the mapping rules further, because StartupWMClass | ||||
219 | // is essentially a mapping rule, and we expect it to be set deliberately and | ||||
220 | // sensibly to instruct us what to do. Also, mapping rules | ||||
221 | // | ||||
222 | // StartupWMClass=STRING | ||||
223 | // | ||||
224 | // If true, it is KNOWN that the application will map at least one | ||||
225 | // window with the given string as its WM class or WM name hint. | ||||
226 | // | ||||
227 | // Source: https://specifications.freedesktop.org/startup-notification-spec/startup-notification-0.1.txt | ||||
228 | if (services.empty()) { | ||||
229 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(appId)); | ||||
230 | } | ||||
231 | | ||||
232 | if (services.empty()) { | ||||
233 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ StartupWMClass)").arg(xWindowsWMClassName)); | ||||
234 | } | ||||
235 | | ||||
236 | // Evaluate rewrite rules from config. | ||||
237 | if (services.empty()) { | ||||
238 | KConfigGroup rewriteRulesGroup(rulesConfig, QStringLiteral("Rewrite Rules")); | ||||
239 | if (rewriteRulesGroup.hasGroup(appId)) { | ||||
240 | KConfigGroup rewriteGroup(&rewriteRulesGroup, appId); | ||||
241 | | ||||
242 | const QStringList &rules = rewriteGroup.groupList(); | ||||
243 | for (const QString &rule : rules) { | ||||
244 | KConfigGroup ruleGroup(&rewriteGroup, rule); | ||||
245 | | ||||
246 | const QString propertyConfig = ruleGroup.readEntry(QStringLiteral("Property"), QString()); | ||||
247 | | ||||
248 | QString matchProperty; | ||||
249 | if (propertyConfig == QLatin1String("ClassClass")) { | ||||
250 | matchProperty = appId; | ||||
251 | } else if (propertyConfig == QLatin1String("ClassName")) { | ||||
252 | matchProperty = xWindowsWMClassName; | ||||
253 | } | ||||
254 | | ||||
255 | if (matchProperty.isEmpty()) { | ||||
256 | continue; | ||||
257 | } | ||||
258 | | ||||
259 | const QString serviceSearchIdentifier = ruleGroup.readEntry(QStringLiteral("Identifier"), QString()); | ||||
260 | if (serviceSearchIdentifier.isEmpty()) { | ||||
261 | continue; | ||||
262 | } | ||||
263 | | ||||
264 | QRegularExpression regExp(ruleGroup.readEntry(QStringLiteral("Match"))); | ||||
265 | const auto match = regExp.match(matchProperty); | ||||
266 | | ||||
267 | if (match.hasMatch()) { | ||||
268 | const QString actualMatch = match.captured(QStringLiteral("match")); | ||||
269 | if (actualMatch.isEmpty()) { | ||||
270 | continue; | ||||
271 | } | ||||
272 | | ||||
273 | QString rewrittenString = ruleGroup.readEntry(QStringLiteral("Target")).arg(actualMatch); | ||||
274 | // If no "Target" is provided, instead assume the matched property (appId/xWindowsWMClassName). | ||||
275 | if (rewrittenString.isEmpty()) { | ||||
276 | rewrittenString = matchProperty; | ||||
277 | } | ||||
278 | | ||||
279 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ %2)").arg(rewrittenString, serviceSearchIdentifier)); | ||||
280 | | ||||
281 | if (!services.isEmpty()) { | ||||
282 | break; | ||||
283 | } | ||||
284 | } | ||||
285 | } | ||||
286 | } | ||||
287 | } | ||||
288 | | ||||
289 | // The appId looks like a path. | ||||
290 | if (appId.startsWith(QStringLiteral("/"))) { | ||||
291 | // Check if it's a path to a .desktop file. | ||||
292 | if (KDesktopFile::isDesktopFile(appId) && QFile::exists(appId)) { | ||||
293 | return QUrl::fromLocalFile(appId); | ||||
294 | } | ||||
295 | | ||||
296 | // Check if the appId passes as a .desktop file path if we add the extension. | ||||
297 | const QString appIdPlusExtension(appId + QStringLiteral(".desktop")); | ||||
298 | | ||||
299 | if (KDesktopFile::isDesktopFile(appIdPlusExtension) && QFile::exists(appIdPlusExtension)) { | ||||
300 | return QUrl::fromLocalFile(appIdPlusExtension); | ||||
301 | } | ||||
302 | } | ||||
303 | | ||||
304 | // Try matching mapped name against DesktopEntryName. | ||||
305 | if (!mapped.isEmpty() && services.empty()) { | ||||
306 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(mapped)); | ||||
307 | } | ||||
308 | | ||||
309 | // Try matching mapped name against 'Name'. | ||||
310 | if (!mapped.isEmpty() && services.empty()) { | ||||
311 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(mapped)); | ||||
312 | } | ||||
313 | | ||||
314 | // Try matching appId against DesktopEntryName. | ||||
315 | if (services.empty()) { | ||||
316 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ DesktopEntryName)").arg(appId)); | ||||
317 | } | ||||
318 | | ||||
319 | // Try matching appId against 'Name'. | ||||
320 | // This has a shaky chance of success as appId is untranslated, but 'Name' may be localized. | ||||
321 | if (services.empty()) { | ||||
322 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Name) and (not exist NoDisplay or not NoDisplay)").arg(appId)); | ||||
323 | } | ||||
324 | } | ||||
325 | | ||||
326 | // Ok, absolute *last* chance, try matching via pid (but only if we have not already tried this!) ... | ||||
327 | if (services.empty() && !triedPid) { | ||||
328 | services = servicesFromPid(pid, rulesConfig); | ||||
329 | } | ||||
330 | } | ||||
331 | | ||||
332 | // Try to improve on a possible from-binary fallback. | ||||
333 | // If no services were found or we got a fake-service back from getServicesViaPid() | ||||
334 | // we attempt to improve on this by adding a loosely matched reverse-domain-name | ||||
335 | // DesktopEntryName. Namely anything that is '*.appId.desktop' would qualify here. | ||||
336 | // | ||||
337 | // Illustrative example of a case where the above heuristics would fail to produce | ||||
338 | // a reasonable result: | ||||
339 | // - org.kde.dragonplayer.desktop | ||||
340 | // - binary is 'dragon' | ||||
341 | // - qapp appname and thus appId is 'dragonplayer' | ||||
342 | // - appId cannot directly match the desktop file because of RDN | ||||
343 | // - appId also cannot match the binary because of name mismatch | ||||
344 | // - in the following code *.appId can match org.kde.dragonplayer though | ||||
345 | if (services.empty() || services.at(0)->desktopEntryName().isEmpty()) { | ||||
346 | auto matchingServices = KServiceTypeTrader::self()->query(QStringLiteral("Application"), | ||||
347 | QStringLiteral("exist Exec and ('%1' ~~ DesktopEntryName)").arg(appId)); | ||||
348 | QMutableListIterator<KService::Ptr> it(matchingServices); | ||||
349 | while (it.hasNext()) { | ||||
350 | auto service = it.next(); | ||||
351 | if (!service->desktopEntryName().endsWith("." + appId)) { | ||||
352 | it.remove(); | ||||
353 | } | ||||
354 | } | ||||
355 | // Exactly one match is expected, otherwise we discard the results as to reduce | ||||
356 | // the likelihood of false-positive mappings. Since we essentially eliminate the | ||||
357 | // uniqueness that RDN is meant to bring to the table we could potentially end | ||||
358 | // up with more than one match here. | ||||
359 | if (matchingServices.length() == 1) { | ||||
360 | services = matchingServices; | ||||
361 | } | ||||
362 | } | ||||
363 | | ||||
364 | if (!services.empty()) { | ||||
365 | QString path = services[0]->entryPath(); | ||||
366 | if (path.isEmpty()) { | ||||
367 | path = services[0]->exec(); | ||||
368 | } | ||||
369 | | ||||
370 | if (!path.isEmpty()) { | ||||
371 | url = QUrl::fromLocalFile(path); | ||||
372 | } | ||||
373 | } | ||||
374 | | ||||
375 | return url; | ||||
376 | } | ||||
377 | | ||||
378 | KService::List servicesFromPid(quint32 pid, KSharedConfig::Ptr rulesConfig) | ||||
379 | { | ||||
380 | if (pid == 0) { | ||||
381 | return KService::List(); | ||||
382 | } | ||||
383 | | ||||
384 | if (!rulesConfig) { | ||||
385 | return KService::List(); | ||||
386 | } | ||||
387 | | ||||
388 | KSysGuard::Processes procs; | ||||
389 | procs.updateOrAddProcess(pid); | ||||
390 | | ||||
391 | KSysGuard::Process *proc = procs.getProcess(pid); | ||||
392 | const QString &cmdLine = proc ? proc->command().simplified() : QString(); // proc->command has a trailing space??? | ||||
393 | | ||||
394 | if (cmdLine.isEmpty()) { | ||||
395 | return KService::List(); | ||||
396 | } | ||||
397 | | ||||
398 | return servicesFromCmdLine(cmdLine, proc->name(), rulesConfig); | ||||
399 | } | ||||
400 | | ||||
401 | KService::List servicesFromCmdLine(const QString &_cmdLine, const QString &processName, | ||||
402 | KSharedConfig::Ptr rulesConfig) | ||||
403 | { | ||||
404 | QString cmdLine = _cmdLine; | ||||
405 | KService::List services; | ||||
406 | | ||||
407 | if (!rulesConfig) { | ||||
408 | return services; | ||||
409 | } | ||||
410 | | ||||
411 | const int firstSpace = cmdLine.indexOf(' '); | ||||
412 | int slash = 0; | ||||
413 | | ||||
414 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine)); | ||||
415 | | ||||
416 | if (services.empty()) { | ||||
417 | // Could not find with complete command line, so strip out the path part ... | ||||
418 | slash = cmdLine.lastIndexOf('/', firstSpace); | ||||
419 | | ||||
420 | if (slash > 0) { | ||||
421 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1))); | ||||
422 | } | ||||
423 | } | ||||
424 | | ||||
425 | if (services.empty() && firstSpace > 0) { | ||||
426 | // Could not find with arguments, so try without ... | ||||
427 | cmdLine = cmdLine.left(firstSpace); | ||||
428 | | ||||
429 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine)); | ||||
430 | | ||||
431 | if (services.empty()) { | ||||
432 | slash = cmdLine.lastIndexOf('/'); | ||||
433 | | ||||
434 | if (slash > 0) { | ||||
435 | services = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("exist Exec and ('%1' =~ Exec)").arg(cmdLine.mid(slash + 1))); | ||||
436 | } | ||||
437 | } | ||||
438 | } | ||||
439 | | ||||
440 | if (services.empty()) { | ||||
441 | KConfigGroup set(rulesConfig, "Settings"); | ||||
442 | const QStringList &runtimes = set.readEntry("TryIgnoreRuntimes", QStringList()); | ||||
443 | | ||||
444 | bool ignore = runtimes.contains(cmdLine); | ||||
445 | | ||||
446 | if (!ignore && slash > 0) { | ||||
447 | ignore = runtimes.contains(cmdLine.mid(slash + 1)); | ||||
448 | } | ||||
449 | | ||||
450 | if (ignore) { | ||||
451 | return servicesFromCmdLine(_cmdLine.mid(firstSpace + 1), processName, rulesConfig); | ||||
452 | } | ||||
453 | } | ||||
454 | | ||||
455 | if (services.empty() && !processName.isEmpty() && !QStandardPaths::findExecutable(cmdLine).isEmpty()) { | ||||
456 | // cmdLine now exists without arguments if there were any. | ||||
457 | services << QExplicitlySharedDataPointer<KService>(new KService(processName, cmdLine, QString())); | ||||
458 | } | ||||
459 | | ||||
460 | return services; | ||||
461 | } | ||||
462 | | ||||
157 | QString defaultApplication(const QUrl &url) | 463 | QString defaultApplication(const QUrl &url) | ||
158 | { | 464 | { | ||
159 | if (url.scheme() != QLatin1String("preferred")) { | 465 | if (url.scheme() != QLatin1String("preferred")) { | ||
160 | return QString(); | 466 | return QString(); | ||
161 | } | 467 | } | ||
162 | 468 | | |||
163 | const QString &application = url.host(); | 469 | const QString &application = url.host(); | ||
164 | 470 | | |||
▲ Show 20 Lines • Show All 149 Lines • Show Last 20 Lines |