KRunner

runnermanager.cpp
1 /*
2  SPDX-FileCopyrightText: 2006 Aaron Seigo <[email protected]>
3  SPDX-FileCopyrightText: 2007, 2009 Ryan P. Bitanga <[email protected]>
4  SPDX-FileCopyrightText: 2008 Jordi Polo <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "runnermanager.h"
10 
11 #include <QCoreApplication>
12 #include <QDir>
13 #include <QElapsedTimer>
14 #include <QRegularExpression>
15 #include <QStandardPaths>
16 #include <QTimer>
17 
18 #include <KConfigWatcher>
19 #include <KFileUtils>
20 #include <KPluginMetaData>
21 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72)
22 #include <KServiceTypeTrader>
23 #else
24 #define KSERVICE_BUILD_DEPRECATED_SINCE(a, b) 0
25 #endif
26 #include <KSharedConfig>
27 
28 #include "config.h"
29 #if HAVE_KACTIVITIES
30 #include <KActivities/Consumer>
31 #endif
32 
33 #include <ThreadWeaver/DebuggingAids>
34 #include <ThreadWeaver/Queue>
35 #include <ThreadWeaver/Thread>
36 
37 #if KRUNNER_ENABLE_DEPRECATED_SINCE(5, 65)
38 #include <plasma/version.h>
39 #endif
40 
41 #include "dbusrunner_p.h"
42 #include "kpluginmetadata_utils_p.h"
43 #include "krunner_debug.h"
44 #include "querymatch.h"
45 #include "runnerjobs_p.h"
46 
48 
49 namespace Plasma
50 {
51 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72) && KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
52 void warnAboutDeprecatedMetaData(const KPluginInfo &pluginInfo)
53 {
54  if (!pluginInfo.libraryPath().isEmpty()) {
55  qCWarning(KRUNNER).nospace() << "KRunner plugin " << pluginInfo.pluginName() << " still uses a .desktop file (" << pluginInfo.entryPath()
56  << "). Please port it to JSON metadata.";
57  } else {
58  qCWarning(KRUNNER).nospace() << "KRunner D-Bus plugin " << pluginInfo.pluginName() << " installs the .desktop file (" << pluginInfo.entryPath()
59  << ") still in the kservices5 folder. Please install it to ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins.";
60  }
61 }
62 #endif
63 
64 class RunnerManagerPrivate
65 {
66 public:
67  RunnerManagerPrivate(RunnerManager *parent)
68  : q(parent)
69  {
70  matchChangeTimer.setSingleShot(true);
71  matchChangeTimer.setTimerType(Qt::TimerType::PreciseTimer); // Without this, autotest will fail due to imprecision of this timer
72  delayTimer.setSingleShot(true);
73 
74  QObject::connect(&matchChangeTimer, &QTimer::timeout, q, [this]() {
75  matchesChanged();
76  });
77  QObject::connect(&context, &RunnerContext::matchesChanged, q, [this]() {
78  scheduleMatchesChanged();
79  });
80  QObject::connect(&delayTimer, &QTimer::timeout, q, [this]() {
81  unblockJobs();
82  });
83 
84  // Set up tracking of the last time matchesChanged was signalled
85  lastMatchChangeSignalled.start();
87  lastMatchChangeSignalled.restart();
88  });
89  }
90 
91  void scheduleMatchesChanged()
92  {
93  // We avoid over-refreshing the client. We only refresh every this much milliseconds
94  constexpr int refreshPeriod = 250;
95  // This will tell us if we are reseting the matches to start a new search. RunnerContext::reset() clears its query string for its emission
96  if (context.query().isEmpty()) {
97  matchChangeTimer.stop();
98  // This actually contains the query string for the new search that we're launching (if any):
99  if (!this->untrimmedTerm.trimmed().isEmpty()) {
100  // We are starting a new search, we shall stall for some time before deciding to show an empty matches list.
101  // This stall should be enough for the engine to provide more meaningful result, so we avoid refreshing with
102  // an empty results list if possible.
103  matchChangeTimer.start(refreshPeriod);
104  // We "pretend" that we have refreshed it so the next call will be forced to wait the timeout:
105  lastMatchChangeSignalled.restart();
106  } else {
107  // We have an empty input string, so it's not a real query. We don't expect any results to come, so no need to stall
108  Q_EMIT q->matchesChanged(context.matches());
109  }
110  } else if (lastMatchChangeSignalled.hasExpired(refreshPeriod)) {
111  matchChangeTimer.stop();
112  Q_EMIT q->matchesChanged(context.matches());
113  } else {
114  matchChangeTimer.start(refreshPeriod - lastMatchChangeSignalled.elapsed());
115  }
116  }
117 
118  void matchesChanged()
119  {
120  Q_EMIT q->matchesChanged(context.matches());
121  }
122 
123  void loadConfiguration()
124  {
125  // Limit the number of instances of a single normal speed runner and all of the slow runners
126  // to half the number of threads
127  DefaultRunnerPolicy::instance().setCap(qMax(2, Queue::instance()->maximumNumberOfThreads() / 2));
128 
129 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
130  enabledCategories = stateData.readEntry("enabledCategories", QStringList());
131 #endif
132 #if HAVE_KACTIVITIES
133  // Wait for consumer to be ready
134  QObject::connect(&activitiesConsumer,
135  &KActivities::Consumer::serviceStatusChanged,
136  &activitiesConsumer,
137  [this](KActivities::Consumer::ServiceStatus status) {
138  if (status == KActivities::Consumer::Running) {
139  deleteHistoryOfDeletedActivities();
140  }
141  });
142 #endif
143  const KConfigGroup generalConfig = configPrt->group("General");
144  const bool _historyEnabled = generalConfig.readEntry("HistoryEnabled", true);
145  if (historyEnabled != _historyEnabled) {
146  historyEnabled = _historyEnabled;
147  Q_EMIT q->historyEnabledChanged();
148  }
149  activityAware = generalConfig.readEntry("ActivityAware", true);
150  retainPriorSearch = generalConfig.readEntry("RetainPriorSearch", true);
151  context.restore(stateData);
152  }
153 
154  void loadSingleRunner()
155  {
156  // In case we are not in the single runner mode of we do not have an id
157  if (!singleMode || singleModeRunnerId.isEmpty()) {
158  currentSingleRunner = nullptr;
159  return;
160  }
161 
162  if (currentSingleRunner && currentSingleRunner->id() == singleModeRunnerId) {
163  return;
164  }
165  currentSingleRunner = q->runner(singleModeRunnerId);
166  // If there are no runners loaded or the single runner could no be loaded,
167  // this is the case if it was disabled but gets queries using the singleRunnerMode, BUG: 435050
168  if (runners.isEmpty() || !currentSingleRunner) {
169  loadRunners(singleModeRunnerId);
170  currentSingleRunner = q->runner(singleModeRunnerId);
171  }
172  }
173 
174  void loadRunners(const QString &singleRunnerId = QString())
175  {
177 
178  const bool loadAll = stateData.readEntry("loadAll", false);
179  const bool noWhiteList = whiteList.isEmpty();
180  KConfigGroup pluginConf = configPrt->group("Plugins");
181 
182  QSet<AbstractRunner *> deadRunners;
184  while (it.hasNext()) {
185  KPluginMetaData &description = it.next();
186  qCDebug(KRUNNER) << "Loading runner: " << description.pluginId();
187 
188 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 85)
189  const QString tryExec = description.value(QStringLiteral("TryExec"));
190  if (!tryExec.isEmpty()) {
191  qCDebug(KRUNNER) << description.pluginId() << "The TryExec property is deprecated, manually check if the application exists if needed";
192  if (QStandardPaths::findExecutable(tryExec).isEmpty()) {
193  // we don't actually have this application!
194  continue;
195  }
196  }
197 #endif
198 
199  const QString runnerName = description.pluginId();
200  const bool isPluginEnabled = description.isEnabled(pluginConf);
201  const bool loaded = runners.contains(runnerName);
202  bool selected = loadAll || disabledRunnerIds.contains(runnerName) || (isPluginEnabled && (noWhiteList || whiteList.contains(runnerName)));
203  if (!selected && runnerName == singleRunnerId) {
204  selected = true;
205  disabledRunnerIds << runnerName;
206  }
207 
208  if (selected) {
209  AbstractRunner *runner = nullptr;
210  if (!loaded) {
211  runner = loadInstalledRunner(description);
212  } else {
213  runner = runners.value(runnerName);
214  }
215 
216  if (runner) {
217  bool allCategoriesDisabled = true;
218 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
219  const QStringList categories = runner->categories();
220 
221  for (const QString &cat : categories) {
222  if (enabledCategories.contains(cat)) {
223  allCategoriesDisabled = false;
224  break;
225  }
226  }
227 #endif
228 
229  if (enabledCategories.isEmpty() || !allCategoriesDisabled) {
230  qCDebug(KRUNNER) << "Loaded:" << runnerName;
231  runners.insert(runnerName, runner);
232  } else {
233  runners.remove(runnerName);
234  deadRunners.insert(runner);
235  qCDebug(KRUNNER) << "Categories not enabled. Removing runner: " << runnerName;
236  }
237  }
238  } else if (loaded) {
239  // Remove runner
240  deadRunners.insert(runners.take(runnerName));
241  qCDebug(KRUNNER) << "Plugin disabled. Removing runner: " << runnerName;
242  }
243  }
244 
245  if (!deadRunners.isEmpty()) {
247  auto it = searchJobs.begin();
248  while (it != searchJobs.end()) {
249  auto &job = (*it);
250  if (deadRunners.contains(job->runner())) {
251  QObject::disconnect(job.data(), &FindMatchesJob::done, q, nullptr);
252  it = searchJobs.erase(it);
253  deadJobs.insert(job);
254  } else {
255  it++;
256  }
257  }
258 
259  it = oldSearchJobs.begin();
260  while (it != oldSearchJobs.end()) {
261  auto &job = (*it);
262  if (deadRunners.contains(job->runner())) {
263  it = oldSearchJobs.erase(it);
264  deadJobs.insert(job);
265  } else {
266  it++;
267  }
268  }
269 
270  if (deadJobs.isEmpty()) {
271  qDeleteAll(deadRunners);
272  } else {
273  new DelayedJobCleaner(deadJobs, deadRunners);
274  }
275  }
276 
277  // in case we deleted it up above, just to be sure we do not have a dangeling pointer
278  currentSingleRunner = nullptr;
279 
280  qCDebug(KRUNNER) << "All runners loaded, total:" << runners.count();
281  }
282 
283  AbstractRunner *loadInstalledRunner(const KPluginMetaData &pluginMetaData)
284  {
285  if (!pluginMetaData.isValid()) {
286  return nullptr;
287  }
288 
289  AbstractRunner *runner = nullptr;
290 
291  const QString api = pluginMetaData.value(QStringLiteral("X-Plasma-API"));
292 
293  if (api.isEmpty()) {
294  const QVariantList args
295  {
296 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 77)
297 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
298  pluginMetaData.metaDataFileName(),
299 #endif
300  QVariant::fromValue(pluginMetaData),
301 #endif
302  };
303  auto res = KPluginFactory::instantiatePlugin<AbstractRunner>(pluginMetaData, q, args);
304  if (res) {
305  runner = res.plugin;
306  } else {
307  qCWarning(KRUNNER).nospace() << "Could not load runner " << pluginMetaData.name() << ":" << res.errorString
308  << " (library path was:" << pluginMetaData.fileName() << ")";
309  }
310  } else if (api.startsWith(QLatin1String("DBus"))) {
311  runner = new DBusRunner(q, pluginMetaData, {});
312  } else {
313  qCWarning(KRUNNER) << "Unknown X-Plasma-API requested for runner" << pluginMetaData.fileName();
314  return nullptr;
315  }
316 
317  if (runner) {
318  QObject::connect(runner, &AbstractRunner::matchingSuspended, q, [this](bool state) {
319  runnerMatchingSuspended(state);
320  });
321  runner->init();
322  if (prepped) {
323  Q_EMIT runner->prepare();
324  }
325  }
326 
327  return runner;
328  }
329 
330  void jobDone(ThreadWeaver::JobPointer job)
331  {
332  auto runJob = job.dynamicCast<FindMatchesJob>();
333 
334  if (!runJob) {
335  return;
336  }
337 
338  searchJobs.remove(runJob);
339  oldSearchJobs.remove(runJob);
340 
341  if (searchJobs.isEmpty()) {
342  // If there are any new matches scheduled to be notified, we should anticipate it and just refresh right now
343  if (matchChangeTimer.isActive()) {
344  matchChangeTimer.stop();
345  Q_EMIT q->matchesChanged(context.matches());
346  } else if (context.matches().isEmpty()) {
347  // we finished our run, and there are no valid matches, and so no
348  // signal will have been sent out. so we need to emit the signal
349  // ourselves here
350  Q_EMIT q->matchesChanged(context.matches());
351  }
352  Q_EMIT q->queryFinished();
353  }
354  }
355 
356  void checkTearDown()
357  {
358  if (!prepped || !teardownRequested) {
359  return;
360  }
361 
362  if (Queue::instance()->isIdle()) {
363  searchJobs.clear();
364  oldSearchJobs.clear();
365  }
366 
367  if (searchJobs.isEmpty() && oldSearchJobs.isEmpty()) {
368  if (allRunnersPrepped) {
369  for (AbstractRunner *runner : std::as_const(runners)) {
370  Q_EMIT runner->teardown();
371  }
372 
373  allRunnersPrepped = false;
374  }
375 
376  if (singleRunnerPrepped) {
377  if (currentSingleRunner) {
378  Q_EMIT currentSingleRunner->teardown();
379  }
380 
381  singleRunnerPrepped = false;
382  }
383 
384  prepped = false;
385  teardownRequested = false;
386  }
387  }
388 
389  void unblockJobs()
390  {
391  if (searchJobs.isEmpty() && Queue::instance()->isIdle()) {
392  oldSearchJobs.clear();
393  checkTearDown();
394  return;
395  }
396 
397  Queue::instance()->reschedule();
398  }
399 
400  void runnerMatchingSuspended(bool suspended)
401  {
402  auto *runner = qobject_cast<AbstractRunner *>(q->sender());
403  if (suspended || !prepped || teardownRequested || !runner) {
404  return;
405  }
406 
407  const QString query = context.query();
408  if (singleMode || runner->minLetterCount() <= query.size()) {
409  if (singleMode || !runner->hasMatchRegex() || runner->matchRegex().match(query).hasMatch()) {
410  startJob(runner);
411  }
412  }
413  }
414 
415  void startJob(AbstractRunner *runner)
416  {
417 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
418  if ((runner->ignoredTypes() & context.type()) != 0) {
419  return;
420  }
421 #endif
422  QSharedPointer<FindMatchesJob> job(new FindMatchesJob(runner, &context, Queue::instance()));
423  QObject::connect(job.data(), &FindMatchesJob::done, q, [this](ThreadWeaver::JobPointer jobPtr) {
424  jobDone(jobPtr);
425  });
426 
427 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
428  if (runner->speed() == AbstractRunner::SlowSpeed) {
429  job->setDelayTimer(&delayTimer);
430  }
431 #endif
432  Queue::instance()->enqueue(job);
433  searchJobs.insert(job);
434  }
435 
436  inline QString getActivityKey()
437  {
438 #if HAVE_KACTIVITIES
439  if (activityAware) {
440  const QString currentActivity = activitiesConsumer.currentActivity();
441  return currentActivity.isEmpty() ? nulluuid : currentActivity;
442  }
443 #endif
444  return nulluuid;
445  }
446 
447  void addToHistory()
448  {
449  const QString term = context.query();
450  // We want to imitate the shall behavior
451  if (!historyEnabled || term.isEmpty() || untrimmedTerm.startsWith(QLatin1Char(' '))) {
452  return;
453  }
454  QStringList historyEntries = readHistoryForCurrentActivity();
455  // Avoid removing the same item from the front and prepending it again
456  if (!historyEntries.isEmpty() && historyEntries.constFirst() == term) {
457  return;
458  }
459 
460  historyEntries.removeOne(term);
461  historyEntries.prepend(term);
462 
463  while (historyEntries.count() > 50) { // we don't want to store more than 50 entries
464  historyEntries.removeLast();
465  }
466  writeActivityHistory(historyEntries);
467  }
468 
469  void writeActivityHistory(const QStringList &historyEntries)
470  {
471  stateData.group("History").writeEntry(getActivityKey(), historyEntries, KConfig::Notify);
472  stateData.sync();
473  }
474 
475 #if HAVE_KACTIVITIES
476  void deleteHistoryOfDeletedActivities()
477  {
478  KConfigGroup historyGroup = stateData.group("History");
479  QStringList historyEntries = historyGroup.keyList();
480  historyEntries.removeOne(nulluuid);
481 
482  // Check if history still exists
483  const QStringList activities = activitiesConsumer.activities();
484  for (const auto &a : activities) {
485  historyEntries.removeOne(a);
486  }
487 
488  for (const QString &deletedActivity : std::as_const(historyEntries)) {
489  historyGroup.deleteEntry(deletedActivity);
490  }
491  historyGroup.sync();
492  }
493 #endif
494 
495  inline QStringList readHistoryForCurrentActivity()
496  {
497  return stateData.group("History").readEntry(getActivityKey(), QStringList());
498  }
499 
500  // Delay in ms before slow runners are allowed to run
501  static const int slowRunDelay = 400;
502 
503  RunnerManager *const q;
504  RunnerContext context;
505  QTimer matchChangeTimer;
506  QTimer delayTimer; // Timer to control when to run slow runners
507  QElapsedTimer lastMatchChangeSignalled;
509  AbstractRunner *currentSingleRunner = nullptr;
512  QStringList enabledCategories;
513  QString singleModeRunnerId;
514  bool prepped = false;
515  bool allRunnersPrepped = false;
516  bool singleRunnerPrepped = false;
517  bool teardownRequested = false;
518  bool singleMode = false;
519  bool activityAware = false;
520  bool historyEnabled = false;
521  bool retainPriorSearch = false;
522  QStringList whiteList;
523  KConfigWatcher::Ptr watcher;
524  QHash<QString, QString> priorSearch;
525  QString untrimmedTerm;
526  QString nulluuid = QStringLiteral("00000000-0000-0000-0000-000000000000");
527  KSharedConfigPtr configPrt;
528  KConfigGroup stateData;
529  QSet<QString> disabledRunnerIds; // Runners that are disabled but were loaded as single runners
530 #if HAVE_KACTIVITIES
531  const KActivities::Consumer activitiesConsumer;
532 #endif
533 };
534 
535 RunnerManager::RunnerManager(QObject *parent)
536  : RunnerManager(QString(), parent)
537 {
538 }
539 
540 RunnerManager::RunnerManager(const QString &configFile, QObject *parent)
541  : QObject(parent)
542  , d(new RunnerManagerPrivate(this))
543 {
544  d->configPrt = KSharedConfig::openConfig(configFile);
545  // If the old config group still exists the migration script wasn't executed
546  // so we keep using this location
547  KConfigGroup oldStateDataGroup = d->configPrt->group("PlasmaRunnerManager");
548  if (oldStateDataGroup.exists() && !oldStateDataGroup.readEntry("migrated", false)) {
549  d->stateData = oldStateDataGroup;
550  } else {
551  d->stateData =
552  KSharedConfig::openConfig(QStringLiteral("krunnerstaterc"), KConfig::NoGlobals, QStandardPaths::GenericDataLocation)->group("PlasmaRunnerManager");
553  }
554  d->loadConfiguration();
555 }
556 
557 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
558 RunnerManager::RunnerManager(KConfigGroup &c, QObject *parent)
559  : QObject(parent)
560  , d(new RunnerManagerPrivate(this))
561 {
562  d->configPrt = KSharedConfig::openConfig();
563  d->stateData = KConfigGroup(&c, "PlasmaRunnerManager");
564  d->loadConfiguration();
565 }
566 #endif
567 
568 RunnerManager::~RunnerManager()
569 {
570  if (!qApp->closingDown() && (!d->searchJobs.isEmpty() || !d->oldSearchJobs.isEmpty())) {
571  const QSet<QSharedPointer<FindMatchesJob>> jobs(d->searchJobs + d->oldSearchJobs);
573  for (auto &job : jobs) {
574  job->runner()->setParent(nullptr);
575  runners << job->runner();
576  }
577  new DelayedJobCleaner(jobs, runners);
578  }
579 }
580 
582 {
583  d->configPrt->reparseConfiguration();
584  d->stateData.config()->reparseConfiguration();
585  d->loadConfiguration();
586  d->loadRunners();
587 }
588 
590 {
591  d->whiteList = runners;
592  if (!d->runners.isEmpty()) {
593  // this has been called with runners already created. so let's do an instant reload
594  d->loadRunners();
595  }
596 }
597 
598 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
600 {
601  d->stateData.writeEntry("enabledCategories", categories);
602  d->enabledCategories = categories;
603 
604  if (!d->runners.isEmpty()) {
605  d->loadRunners();
606  }
607 }
608 #endif
609 
610 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 88)
612 {
613  return d->stateData.readEntry("pluginWhiteList", QStringList());
614 }
615 #endif
616 
617 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
619 {
620  QStringList list = d->stateData.readEntry("enabledCategories", QStringList());
621  if (list.isEmpty()) {
622  list.reserve(d->runners.count());
623  for (AbstractRunner *runner : std::as_const(d->runners)) {
624  list << runner->categories();
625  }
626  }
627 
628  return list;
629 }
630 #endif
631 
632 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72) && KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
633 void RunnerManager::loadRunner(const KService::Ptr service)
634 {
635  QT_WARNING_PUSH
636  QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
637  QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
638  KPluginInfo description(service);
639  QT_WARNING_POP
640  loadRunner(description.toMetaData());
641 }
642 #endif
643 
644 void RunnerManager::loadRunner(const KPluginMetaData &pluginMetaData)
645 {
646  const QString runnerName = pluginMetaData.pluginId();
647  if (!runnerName.isEmpty() && !d->runners.contains(runnerName)) {
648  if (AbstractRunner *runner = d->loadInstalledRunner(pluginMetaData)) {
649  d->runners.insert(runnerName, runner);
650  }
651  }
652 }
653 
654 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 77)
656 {
657  if (!d->runners.contains(path)) {
658  AbstractRunner *runner = new AbstractRunner(this, path);
659  connect(runner, &AbstractRunner::matchingSuspended, this, [this](bool state) {
660  d->runnerMatchingSuspended(state);
661  });
662  d->runners.insert(path, runner);
663  }
664 }
665 #endif
666 
668 {
669  if (d->runners.isEmpty()) {
670  d->loadRunners();
671  }
672 
673  return d->runners.value(name, nullptr);
674 }
675 
676 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 82)
678 {
679  return d->currentSingleRunner;
680 }
681 
683 {
684  d->singleModeRunnerId = id;
685  d->loadSingleRunner();
686 }
687 
689 {
690  return d->singleModeRunnerId;
691 }
692 
694 {
695  return d->singleMode;
696 }
697 
699 {
700  if (d->singleMode == singleMode) {
701  return;
702  }
703 
704  Plasma::AbstractRunner *prevSingleRunner = d->currentSingleRunner;
705  d->singleMode = singleMode;
706  d->loadSingleRunner();
707  d->singleMode = d->currentSingleRunner;
708 
709  if (prevSingleRunner != d->currentSingleRunner) {
710  if (d->prepped) {
712 
713  if (d->singleMode) {
715  }
716  }
717  }
718 }
719 
721 {
722  QStringList advertiseSingleRunnerIds;
723  for (auto *runner : std::as_const(d->runners)) {
724  if (runner->metadata(RunnerReturnPluginMetaData).rawData().value(QStringLiteral("X-Plasma-AdvertiseSingleRunnerQueryMode")).toVariant().toBool()) {
725  advertiseSingleRunnerIds << runner->id();
726  }
727  }
728  return advertiseSingleRunnerIds;
729 }
730 
732 {
733  return d->runners.contains(id) ? d->runners.value(id)->name() : QString();
734 }
735 #endif
736 
738 {
739  return d->runners.values();
740 }
741 
743 {
744  return &d->context;
745 }
746 
747 // Reordering is here so data is not reordered till strictly needed
749 {
750  return d->context.matches();
751 }
752 
753 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 79)
754 void RunnerManager::run(const QString &matchId)
755 {
756  run(d->context.match(matchId));
757 }
758 #endif
759 
760 void RunnerManager::run(const QueryMatch &match)
761 {
762  if (match.isEnabled()) {
763  d->context.run(match);
764  }
765 }
766 
768 {
769 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 99)
770  if (match.type() == Plasma::QueryMatch::InformationalMatch && !match.selectedAction()) {
771  d->addToHistory();
772  const QString info = match.data().toString();
773  qWarning() << Q_FUNC_INFO << info << match.data();
774  if (!info.isEmpty()) {
775  Q_EMIT setSearchTerm(info, info.length());
776  return false;
777  }
778  }
779 #endif
780  d->context.run(match);
781  if (!d->context.shouldIgnoreCurrentMatchForHistory()) {
782  d->addToHistory();
783  }
784  if (d->context.requestedQueryString().isEmpty()) {
785  return true;
786  } else {
787  Q_EMIT setSearchTerm(d->context.requestedQueryString(), d->context.requestedCursorPosition());
788  return false;
789  }
790 }
791 
793 {
794  if (AbstractRunner *runner = match.runner()) {
795  return runner->actionsForMatch(match);
796  }
797 
798  return QList<QAction *>();
799 }
800 
801 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 79)
803 {
804  return mimeDataForMatch(d->context.match(id));
805 }
806 #endif
807 
809 {
810  return match.isValid() ? match.runner()->mimeDataForMatch(match) : nullptr;
811 }
812 
813 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 85)
815 {
816  // get binary plugins
817  // filter rule also covers parentApp.isEmpty()
818  auto filterParentApp = [&parentApp](const KPluginMetaData &md) -> bool {
819  return md.value(QStringLiteral("X-KDE-ParentApp")) == parentApp;
820  };
821 
822  QVector<KPluginMetaData> pluginMetaDatas = KPluginMetaData::findPlugins(QStringLiteral("kf5/krunner"), filterParentApp);
823  QSet<QString> knownRunnerIds;
824  knownRunnerIds.reserve(pluginMetaDatas.size());
825  for (const KPluginMetaData &pluginMetaData : std::as_const(pluginMetaDatas)) {
826  knownRunnerIds.insert(pluginMetaData.pluginId());
827  }
828 
829  const QStringList dBusPlugindirs =
831  const QStringList dbusRunnerFiles = KFileUtils::findAllUniqueFiles(dBusPlugindirs, QStringList(QStringLiteral("*.desktop")));
832  for (const QString &dbusRunnerFile : dbusRunnerFiles) {
833  KPluginMetaData pluginMetaData = parseMetaDataFromDesktopFile(dbusRunnerFile);
834  if (pluginMetaData.isValid() && !knownRunnerIds.contains(pluginMetaData.pluginId())) {
835  pluginMetaDatas.append(pluginMetaData);
836  knownRunnerIds.insert(pluginMetaData.pluginId());
837  }
838  }
839 
840 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
841  // also search for deprecated kservice-based KRunner plugins metadata
842  const QString constraint = parentApp.isEmpty() ? QStringLiteral("not exist [X-KDE-ParentApp] or [X-KDE-ParentApp] == ''")
843  : QStringLiteral("[X-KDE-ParentApp] == '") + parentApp + QLatin1Char('\'');
844 
845  QT_WARNING_PUSH
846  QT_WARNING_DISABLE_DEPRECATED
847  const KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("Plasma/Runner"), constraint);
848  const KPluginInfo::List backwardCompatPluginInfos = KPluginInfo::fromServices(offers);
849  QT_WARNING_POP
850 
851  for (const KPluginInfo &pluginInfo : backwardCompatPluginInfos) {
852  if (!knownRunnerIds.contains(pluginInfo.pluginName())) {
853  warnAboutDeprecatedMetaData(pluginInfo);
854  pluginMetaDatas.append(pluginInfo.toMetaData());
855  }
856  }
857 #endif
858 
859  return pluginMetaDatas;
860 }
861 #endif
862 
864 {
865  QVector<KPluginMetaData> pluginMetaDatas = KPluginMetaData::findPlugins(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/krunner"));
866  QSet<QString> knownRunnerIds;
867  knownRunnerIds.reserve(pluginMetaDatas.size());
868  for (const KPluginMetaData &pluginMetaData : std::as_const(pluginMetaDatas)) {
869  knownRunnerIds.insert(pluginMetaData.pluginId());
870  }
871 
872  const QStringList dBusPlugindirs =
874  const QStringList dbusRunnerFiles = KFileUtils::findAllUniqueFiles(dBusPlugindirs, QStringList(QStringLiteral("*.desktop")));
875  for (const QString &dbusRunnerFile : dbusRunnerFiles) {
876  KPluginMetaData pluginMetaData = parseMetaDataFromDesktopFile(dbusRunnerFile);
877  if (pluginMetaData.isValid() && !knownRunnerIds.contains(pluginMetaData.pluginId())) {
878  pluginMetaDatas.append(pluginMetaData);
879  knownRunnerIds.insert(pluginMetaData.pluginId());
880  }
881  }
882 
883 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72) && KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
884  // also search for deprecated kservice-based KRunner plugins metadata
885  QT_WARNING_PUSH
886  QT_WARNING_DISABLE_DEPRECATED
887  const KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("Plasma/Runner"));
888  const KPluginInfo::List backwardCompatPluginInfos = KPluginInfo::fromServices(offers);
889  QT_WARNING_POP
890 
891  for (const KPluginInfo &pluginInfo : backwardCompatPluginInfos) {
892  if (!knownRunnerIds.contains(pluginInfo.pluginName())) {
893  warnAboutDeprecatedMetaData(pluginInfo);
894  pluginMetaDatas.append(pluginInfo.toMetaData());
895  }
896  }
897 #endif
898 
899  return pluginMetaDatas;
900 }
901 
902 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72)
904 {
905  QT_WARNING_PUSH
906  QT_WARNING_DISABLE_DEPRECATED
908  QT_WARNING_POP
909 }
910 #endif
911 
913 {
914  d->teardownRequested = false;
915 
916  if (d->prepped) {
917  return;
918  }
919 
920  d->prepped = true;
921  if (d->singleMode) {
922  if (d->currentSingleRunner) {
923  Q_EMIT d->currentSingleRunner->prepare();
924  d->singleRunnerPrepped = true;
925  }
926  } else {
927  for (AbstractRunner *runner : std::as_const(d->runners)) {
928  if (!d->disabledRunnerIds.contains(runner->name())) {
929  Q_EMIT runner->prepare();
930  }
931  }
932 
933  d->allRunnersPrepped = true;
934  }
935 }
936 
938 {
939  if (!d->prepped) {
940  return;
941  }
942 
943  d->teardownRequested = true;
944  d->checkTearDown();
945  // We save the context config after each session, just like the history entries
946  // BUG: 424505
947  d->context.save(d->stateData);
948 }
949 
951 {
952  launchQuery(term, QString());
953 }
954 
955 void RunnerManager::launchQuery(const QString &untrimmedTerm, const QString &runnerName)
956 {
958  QString term = untrimmedTerm.trimmed();
959  const QString prevSingleRunner = d->singleModeRunnerId;
960  d->untrimmedTerm = untrimmedTerm;
961 
962  // Set the required values and load the runner
963  d->singleModeRunnerId = runnerName;
964  d->singleMode = !runnerName.isEmpty();
965  d->loadSingleRunner();
966  // If we could not load the single runner we reset
967  if (!runnerName.isEmpty() && !d->currentSingleRunner) {
968  reset();
969  return;
970  }
971 
972  if (d->context.query() == term && prevSingleRunner == runnerName) {
973  // we already are searching for this!
974  return;
975  }
976 
977  if (!d->singleMode && d->runners.isEmpty()) {
978  d->loadRunners();
979  }
980 
981  reset();
982  d->context.setQuery(term);
983 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
984  d->context.setEnabledCategories(d->enabledCategories);
985 #endif
986 
988 
989  // if the name is not empty we will launch only the specified runner
990  if (d->singleMode) {
991  runnable.insert(QString(), d->currentSingleRunner);
992  d->context.setSingleRunnerQueryMode(true);
993  } else {
994  runnable = d->runners;
995  }
996 
997  const int queryLetterCount = term.count();
998  for (Plasma::AbstractRunner *r : std::as_const(runnable)) {
999  if (r->isMatchingSuspended()) {
1000  continue;
1001  }
1002  // If this runner is loaded but disabled
1003  if (!d->singleMode && d->disabledRunnerIds.contains(r->id())) {
1004  continue;
1005  }
1006  // The runners can set the min letter count as a property, this way we don't
1007  // have to spawn threads just for the runner to reject the query, because it is too short
1008  if (!d->singleMode && queryLetterCount < r->minLetterCount()) {
1009  continue;
1010  }
1011  // If the runner has one ore more trigger words it can set the matchRegex to prevent
1012  // thread spawning if the pattern does not match
1013  if (!d->singleMode && r->hasMatchRegex() && !r->matchRegex().match(term).hasMatch()) {
1014  continue;
1015  }
1016 
1017  d->startJob(r);
1018  }
1019  // In the unlikely case that no runner gets queried we have to emit the signals here
1020  if (d->searchJobs.isEmpty()) {
1021  QTimer::singleShot(0, this, [this]() {
1022  Q_EMIT matchesChanged({});
1024  });
1025  }
1026 
1027  // Start timer to unblock slow runners
1028  d->delayTimer.start(RunnerManagerPrivate::slowRunDelay);
1029 }
1030 
1032 {
1033  return d->context.query();
1034 }
1035 
1036 QStringList RunnerManager::history() const
1037 {
1038  return d->readHistoryForCurrentActivity();
1039 }
1040 
1042 {
1043  QStringList changedHistory = history();
1044  if (index < changedHistory.length()) {
1045  changedHistory.removeAt(index);
1046  d->writeActivityHistory(changedHistory);
1047  }
1048 }
1049 
1051 {
1052  const QStringList historyList = history();
1053  for (const QString &entry : historyList) {
1054  if (entry.startsWith(typedQuery, Qt::CaseInsensitive)) {
1055  return entry;
1056  }
1057  }
1058  return QString();
1059 }
1060 
1062 {
1063  // If ThreadWeaver is idle, it is safe to clear previous jobs
1064  if (Queue::instance()->isIdle()) {
1065  d->oldSearchJobs.clear();
1066  } else {
1067  for (auto it = d->searchJobs.constBegin(); it != d->searchJobs.constEnd(); ++it) {
1068  Queue::instance()->dequeue((*it));
1069  }
1070  d->oldSearchJobs += d->searchJobs;
1071  }
1072 
1073  d->searchJobs.clear();
1074 
1075  d->context.reset();
1076  if (!d->oldSearchJobs.empty()) {
1078  }
1079 }
1080 
1081 KPluginMetaData RunnerManager::convertDBusRunnerToJson(const QString &filename) const
1082 {
1083  return parseMetaDataFromDesktopFile(filename);
1084 }
1085 
1087 {
1088  if (!d->watcher) {
1089  d->watcher = KConfigWatcher::create(d->configPrt);
1090  connect(d->watcher.data(), &KConfigWatcher::configChanged, this, [this](const KConfigGroup &group, const QByteArrayList &changedNames) {
1091  const QString groupName = group.name();
1092  if (groupName == QLatin1String("Plugins")) {
1093  reloadConfiguration();
1094  } else if (groupName == QLatin1String("Runners")) {
1095  for (auto *runner : std::as_const(d->runners)) {
1096  // Signals from the KCM contain the component name, which is the X-KDE-PluginInfo-Name property
1097  if (changedNames.contains(runner->metadata(RunnerReturnPluginMetaData).pluginId().toUtf8())) {
1098  runner->reloadConfiguration();
1099  }
1100  }
1101  } else if (group.parent().isValid() && group.parent().name() == QLatin1String("Runners")) {
1102  for (auto *runner : std::as_const(d->runners)) {
1103  // If the same config group has been modified which gets created in AbstractRunner::config()
1104  if (groupName == runner->id()) {
1105  runner->reloadConfiguration();
1106  }
1107  }
1108  }
1109  });
1110  }
1111 }
1112 
1113 QString RunnerManager::priorSearch() const
1114 {
1115  return d->priorSearch.value(d->getActivityKey());
1116 }
1117 
1118 void RunnerManager::setPriorSearch(const QString &search)
1119 {
1120  d->priorSearch.insert(d->getActivityKey(), search);
1121 }
1122 
1123 bool RunnerManager::historyEnabled()
1124 {
1125  return d->historyEnabled;
1126 }
1127 
1128 bool RunnerManager::retainPriorSearch()
1129 {
1130  return d->retainPriorSearch;
1131 }
1132 
1133 } // Plasma namespace
1134 
1135 #include "moc_runnermanager.cpp"
QString readEntry(const char *key, const char *aDefault=nullptr) const
std::optional< QSqlQuery > query(const QString &queryStatement)
QList< QAction * > actionsForMatch(const QueryMatch &match)
Retrieves the list of actions, if any, for a match.
bool disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
AbstractRunner * singleModeRunner() const
static KPluginInfo::List fromServices(const KService::List &services, const KConfigGroup &config=KConfigGroup())
void deleteEntry(const char *key, WriteConfigFlags pFlags=Normal)
void setSingleMode(bool singleMode)
Sets whether or not the manager is in single mode.
void setupMatchSession()
Call this method when the runners should be prepared for a query session.
CaseInsensitive
QVariant fromValue(const T &value)
QString libraryPath() const
Q_EMITQ_EMIT
QList< AbstractRunner * > runners() const
void configChanged(const KConfigGroup &group, const QByteArrayList &names)
QString metaDataFileName() const
int count(const T &value) const const
@ InformationalMatch
A purely informational, non-runnable match, such as the answer to a question or calculation.
Definition: querymatch.h:50
int length() const const
QString trimmed() const const
void append(const T &value)
void reserve(int size)
A match returned by an AbstractRunner in response to a given RunnerContext.
Definition: querymatch.h:34
bool isValid() const
QString fileName() const
void reloadConfiguration()
Causes a reload of the current configuration.
An abstract base class for Plasma Runner plugins.
static KPluginInfo fromMetaData(const KPluginMetaData &meta)
void loadRunner(const KService::Ptr service)
Attempts to add the AbstractRunner plugin represented by the KService passed in.
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void matchesChanged(const QList< Plasma::QueryMatch > &matches)
Emitted each time a new match is added to the list.
static KSharedConfig::Ptr openConfig(const QString &fileName=QString(), OpenFlags mode=FullConfig, QStandardPaths::StandardLocation type=QStandardPaths::GenericConfigLocation)
void queryFinished()
Emitted when the launchQuery finish.
QHash::iterator insert(const Key &key, const T &value)
void removeAt(int i)
QString findExecutable(const QString &executableName, const QStringList &paths)
void reserve(int alloc)
void removeLast()
bool exists() const
void setSingleModeRunnerId(const QString &id)
Puts the manager into "single runner" mode using the given runner; if the runner does not exist or ca...
void prepend(const T &value)
QString pluginName() const
void setAllowedRunners(const QStringList &runners)
Sets a whitelist for the plugins that can be loaded by this manager.
bool removeOne(const T &value)
QString name() const
void timeout()
bool isEmpty() const const
void setSearchTerm(const QString &term, int cursorPosition)
Put the given search term in the KRunner search field.
int length() const const
QStringList singleModeAdvertisedRunnerIds() const
RunnerContext * searchContext() const
Retrieves the current context.
QString entryPath() const
static Ptr create(const KSharedConfig::Ptr &config)
Q_SCRIPTABLE CaptureState status()
KConfigGroup group(const char *group)
bool isEmpty() const const
AbstractRunner * runner(const QString &pluginName) const
Finds and returns a loaded runner or NULL.
static KServiceTypeTrader * self()
QSet::iterator begin()
void run(const QueryMatch &match)
Runs a given match.
bool contains(const T &value) const const
bool value(const QString &key, bool defaultValue) const
QString & remove(int position, int n)
static QVector< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter, KPluginMetaDataOption option)
QMimeData * mimeDataForMatch(const QueryMatch &match) const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
virtual QStringList categories() const
Return a list of categories that this runner provides.
QStringList allowedRunners() const
QString singleModeRunnerId() const
Q_INVOKABLE void removeFromHistory(int index)
Delete the given index from the history.
int count() const const
QStringList keyList() const
QString & insert(int position, QChar ch)
Q_INVOKABLE QString getHistorySuggestion(const QString &typedQuery) const
Get the suggested history entry for the typed query.
QStringList locateAll(QStandardPaths::StandardLocation type, const QString &fileName, QStandardPaths::LocateOptions options)
void reset()
Reset the current data and stops the query.
KCOREADDONS_EXPORT QStringList findAllUniqueFiles(const QStringList &dirs, const QStringList &nameFilters={})
QString runnerName(const QString &id) const
Returns the translated name of a runner.
QSet::iterator insert(const T &value)
static QVector< KPluginMetaData > runnerMetaDataList()
int size() const const
QStringList enabledCategories() const
The RunnerContext class provides information related to a search, including the search term,...
Definition: runnercontext.h:31
const T & constFirst() const const
bool sync() override
bool runMatch(const QueryMatch &match)
Runs a given match.
void launchQuery(const QString &term, const QString &runnerId)
Launch a query, this will create threads and return immediately.
void prepare()
This signal is emitted when matching is about to commence, giving runners an opportunity to prepare t...
KService::List query(const QString &servicetype, const QString &constraint=QString()) const
KPluginInfo metadata() const
void setEnabledCategories(const QStringList &categories)
Sets the list of categories which matches should be returned for.
KPluginMetaData toMetaData() const
void matchSessionComplete()
Call this method when the query session is finished for the time being.
void enableKNotifyPluginWatcher()
If you call this method the manager will create a KConfigWatcher which reload its runners or the runn...
static KPluginInfo::List listRunnerInfo(const QString &parentApp=QString())
Returns a list of all known Runner implementations.
QString pluginId() const
virtual QList< QAction * > actionsForMatch(const Plasma::QueryMatch &match)
A given match can have more than action that can be performed on it.
bool isEmpty() const const
bool isEnabled(const T &config) const
QList< QueryMatch > matches() const
Retrieves all available matches found so far for the previously launched query.
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sat Dec 2 2023 03:51:00 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.