File indexing completed on 2024-04-21 15:03:04

0001 /*
0002     SPDX-FileCopyrightText: 2006 Aaron Seigo <aseigo@kde.org>
0003     SPDX-FileCopyrightText: 2007, 2009 Ryan P. Bitanga <ryan.bitanga@gmail.com>
0004     SPDX-FileCopyrightText: 2008 Jordi Polo <mumismo@gmail.com>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "runnermanager.h"
0010 
0011 #include <QCoreApplication>
0012 #include <QDir>
0013 #include <QElapsedTimer>
0014 #include <QRegularExpression>
0015 #include <QStandardPaths>
0016 #include <QTimer>
0017 
0018 #include <KConfigWatcher>
0019 #include <KFileUtils>
0020 #include <KPluginMetaData>
0021 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72)
0022 #include <KServiceTypeTrader>
0023 #else
0024 #define KSERVICE_BUILD_DEPRECATED_SINCE(a, b) 0
0025 #endif
0026 #include <KSharedConfig>
0027 
0028 #include "config.h"
0029 #if HAVE_KACTIVITIES
0030 #include <KActivities/Consumer>
0031 #endif
0032 
0033 #include <ThreadWeaver/DebuggingAids>
0034 #include <ThreadWeaver/Queue>
0035 #include <ThreadWeaver/Thread>
0036 
0037 #if KRUNNER_ENABLE_DEPRECATED_SINCE(5, 65)
0038 #include <plasma/version.h>
0039 #endif
0040 
0041 #include "dbusrunner_p.h"
0042 #include "kpluginmetadata_utils_p.h"
0043 #include "krunner_debug.h"
0044 #include "querymatch.h"
0045 #include "runnerjobs_p.h"
0046 
0047 using ThreadWeaver::Queue;
0048 
0049 namespace Plasma
0050 {
0051 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72) && KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
0052 void warnAboutDeprecatedMetaData(const KPluginInfo &pluginInfo)
0053 {
0054     if (!pluginInfo.libraryPath().isEmpty()) {
0055         qCWarning(KRUNNER).nospace() << "KRunner plugin " << pluginInfo.pluginName() << " still uses a .desktop file (" << pluginInfo.entryPath()
0056                                      << "). Please port it to JSON metadata.";
0057     } else {
0058         qCWarning(KRUNNER).nospace() << "KRunner D-Bus plugin " << pluginInfo.pluginName() << " installs the .desktop file (" << pluginInfo.entryPath()
0059                                      << ") still in the kservices5 folder. Please install it to ${KDE_INSTALL_DATAROOTDIR}/krunner/dbusplugins.";
0060     }
0061 }
0062 #endif
0063 
0064 class RunnerManagerPrivate
0065 {
0066 public:
0067     RunnerManagerPrivate(RunnerManager *parent)
0068         : q(parent)
0069     {
0070         matchChangeTimer.setSingleShot(true);
0071         matchChangeTimer.setTimerType(Qt::TimerType::PreciseTimer); // Without this, autotest will fail due to imprecision of this timer
0072         delayTimer.setSingleShot(true);
0073 
0074         QObject::connect(&matchChangeTimer, &QTimer::timeout, q, [this]() {
0075             matchesChanged();
0076         });
0077         QObject::connect(&context, &RunnerContext::matchesChanged, q, [this]() {
0078             scheduleMatchesChanged();
0079         });
0080         QObject::connect(&delayTimer, &QTimer::timeout, q, [this]() {
0081             unblockJobs();
0082         });
0083 
0084         // Set up tracking of the last time matchesChanged was signalled
0085         lastMatchChangeSignalled.start();
0086         QObject::connect(q, &RunnerManager::matchesChanged, q, [&] {
0087             lastMatchChangeSignalled.restart();
0088         });
0089     }
0090 
0091     void scheduleMatchesChanged()
0092     {
0093         // We avoid over-refreshing the client. We only refresh every this much milliseconds
0094         constexpr int refreshPeriod = 250;
0095         // This will tell us if we are reseting the matches to start a new search. RunnerContext::reset() clears its query string for its emission
0096         if (context.query().isEmpty()) {
0097             matchChangeTimer.stop();
0098             // This actually contains the query string for the new search that we're launching (if any):
0099             if (!this->untrimmedTerm.trimmed().isEmpty()) {
0100                 // We are starting a new search, we shall stall for some time before deciding to show an empty matches list.
0101                 // This stall should be enough for the engine to provide more meaningful result, so we avoid refreshing with
0102                 // an empty results list if possible.
0103                 matchChangeTimer.start(refreshPeriod);
0104                 // We "pretend" that we have refreshed it so the next call will be forced to wait the timeout:
0105                 lastMatchChangeSignalled.restart();
0106             } else {
0107                 // 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
0108                 Q_EMIT q->matchesChanged(context.matches());
0109             }
0110         } else if (lastMatchChangeSignalled.hasExpired(refreshPeriod)) {
0111             matchChangeTimer.stop();
0112             Q_EMIT q->matchesChanged(context.matches());
0113         } else {
0114             matchChangeTimer.start(refreshPeriod - lastMatchChangeSignalled.elapsed());
0115         }
0116     }
0117 
0118     void matchesChanged()
0119     {
0120         Q_EMIT q->matchesChanged(context.matches());
0121     }
0122 
0123     void loadConfiguration()
0124     {
0125         // Limit the number of instances of a single normal speed runner and all of the slow runners
0126         // to half the number of threads
0127         DefaultRunnerPolicy::instance().setCap(qMax(2, Queue::instance()->maximumNumberOfThreads() / 2));
0128 
0129 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0130         enabledCategories = stateData.readEntry("enabledCategories", QStringList());
0131 #endif
0132 #if HAVE_KACTIVITIES
0133         // Wait for consumer to be ready
0134         QObject::connect(&activitiesConsumer,
0135                          &KActivities::Consumer::serviceStatusChanged,
0136                          &activitiesConsumer,
0137                          [this](KActivities::Consumer::ServiceStatus status) {
0138                              if (status == KActivities::Consumer::Running) {
0139                                  deleteHistoryOfDeletedActivities();
0140                              }
0141                          });
0142 #endif
0143         const KConfigGroup generalConfig = configPrt->group("General");
0144         const bool _historyEnabled = generalConfig.readEntry("HistoryEnabled", true);
0145         if (historyEnabled != _historyEnabled) {
0146             historyEnabled = _historyEnabled;
0147             Q_EMIT q->historyEnabledChanged();
0148         }
0149         activityAware = generalConfig.readEntry("ActivityAware", true);
0150         retainPriorSearch = generalConfig.readEntry("RetainPriorSearch", true);
0151         context.restore(stateData);
0152     }
0153 
0154     void loadSingleRunner()
0155     {
0156         // In case we are not in the single runner mode of we do not have an id
0157         if (!singleMode || singleModeRunnerId.isEmpty()) {
0158             currentSingleRunner = nullptr;
0159             return;
0160         }
0161 
0162         if (currentSingleRunner && currentSingleRunner->id() == singleModeRunnerId) {
0163             return;
0164         }
0165         currentSingleRunner = q->runner(singleModeRunnerId);
0166         // If there are no runners loaded or the single runner could no be loaded,
0167         // this is the case if it was disabled but gets queries using the singleRunnerMode, BUG: 435050
0168         if (runners.isEmpty() || !currentSingleRunner) {
0169             loadRunners(singleModeRunnerId);
0170             currentSingleRunner = q->runner(singleModeRunnerId);
0171         }
0172     }
0173 
0174     void loadRunners(const QString &singleRunnerId = QString())
0175     {
0176         QVector<KPluginMetaData> offers = RunnerManager::runnerMetaDataList();
0177 
0178         const bool loadAll = stateData.readEntry("loadAll", false);
0179         const bool noWhiteList = whiteList.isEmpty();
0180         KConfigGroup pluginConf = configPrt->group("Plugins");
0181 
0182         QSet<AbstractRunner *> deadRunners;
0183         QMutableVectorIterator<KPluginMetaData> it(offers);
0184         while (it.hasNext()) {
0185             KPluginMetaData &description = it.next();
0186             qCDebug(KRUNNER) << "Loading runner: " << description.pluginId();
0187 
0188 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 85)
0189             const QString tryExec = description.value(QStringLiteral("TryExec"));
0190             if (!tryExec.isEmpty()) {
0191                 qCDebug(KRUNNER) << description.pluginId() << "The TryExec property is deprecated, manually check if the application exists if needed";
0192                 if (QStandardPaths::findExecutable(tryExec).isEmpty()) {
0193                     // we don't actually have this application!
0194                     continue;
0195                 }
0196             }
0197 #endif
0198 
0199             const QString runnerName = description.pluginId();
0200             const bool isPluginEnabled = description.isEnabled(pluginConf);
0201             const bool loaded = runners.contains(runnerName);
0202             bool selected = loadAll || disabledRunnerIds.contains(runnerName) || (isPluginEnabled && (noWhiteList || whiteList.contains(runnerName)));
0203             if (!selected && runnerName == singleRunnerId) {
0204                 selected = true;
0205                 disabledRunnerIds << runnerName;
0206             }
0207 
0208             if (selected) {
0209                 AbstractRunner *runner = nullptr;
0210                 if (!loaded) {
0211                     runner = loadInstalledRunner(description);
0212                 } else {
0213                     runner = runners.value(runnerName);
0214                 }
0215 
0216                 if (runner) {
0217                     bool allCategoriesDisabled = true;
0218 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0219                     const QStringList categories = runner->categories();
0220 
0221                     for (const QString &cat : categories) {
0222                         if (enabledCategories.contains(cat)) {
0223                             allCategoriesDisabled = false;
0224                             break;
0225                         }
0226                     }
0227 #endif
0228 
0229                     if (enabledCategories.isEmpty() || !allCategoriesDisabled) {
0230                         qCDebug(KRUNNER) << "Loaded:" << runnerName;
0231                         runners.insert(runnerName, runner);
0232                     } else {
0233                         runners.remove(runnerName);
0234                         deadRunners.insert(runner);
0235                         qCDebug(KRUNNER) << "Categories not enabled. Removing runner: " << runnerName;
0236                     }
0237                 }
0238             } else if (loaded) {
0239                 // Remove runner
0240                 deadRunners.insert(runners.take(runnerName));
0241                 qCDebug(KRUNNER) << "Plugin disabled. Removing runner: " << runnerName;
0242             }
0243         }
0244 
0245         if (!deadRunners.isEmpty()) {
0246             QSet<QSharedPointer<FindMatchesJob>> deadJobs;
0247             auto it = searchJobs.begin();
0248             while (it != searchJobs.end()) {
0249                 auto &job = (*it);
0250                 if (deadRunners.contains(job->runner())) {
0251                     QObject::disconnect(job.data(), &FindMatchesJob::done, q, nullptr);
0252                     it = searchJobs.erase(it);
0253                     deadJobs.insert(job);
0254                 } else {
0255                     it++;
0256                 }
0257             }
0258 
0259             it = oldSearchJobs.begin();
0260             while (it != oldSearchJobs.end()) {
0261                 auto &job = (*it);
0262                 if (deadRunners.contains(job->runner())) {
0263                     it = oldSearchJobs.erase(it);
0264                     deadJobs.insert(job);
0265                 } else {
0266                     it++;
0267                 }
0268             }
0269 
0270             if (deadJobs.isEmpty()) {
0271                 qDeleteAll(deadRunners);
0272             } else {
0273                 new DelayedJobCleaner(deadJobs, deadRunners);
0274             }
0275         }
0276 
0277         // in case we deleted it up above, just to be sure we do not have a dangeling pointer
0278         currentSingleRunner = nullptr;
0279 
0280         qCDebug(KRUNNER) << "All runners loaded, total:" << runners.count();
0281     }
0282 
0283     AbstractRunner *loadInstalledRunner(const KPluginMetaData &pluginMetaData)
0284     {
0285         if (!pluginMetaData.isValid()) {
0286             return nullptr;
0287         }
0288 
0289         AbstractRunner *runner = nullptr;
0290 
0291         const QString api = pluginMetaData.value(QStringLiteral("X-Plasma-API"));
0292 
0293         if (api.isEmpty()) {
0294                 const QVariantList args
0295                 {
0296 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 77)
0297 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
0298                     pluginMetaData.metaDataFileName(),
0299 #endif
0300                         QVariant::fromValue(pluginMetaData),
0301 #endif
0302                 };
0303                 auto res = KPluginFactory::instantiatePlugin<AbstractRunner>(pluginMetaData, q, args);
0304                 if (res) {
0305                     runner = res.plugin;
0306                 } else {
0307                     qCWarning(KRUNNER).nospace() << "Could not load runner " << pluginMetaData.name() << ":" << res.errorString
0308                                                  << " (library path was:" << pluginMetaData.fileName() << ")";
0309                 }
0310         } else if (api.startsWith(QLatin1String("DBus"))) {
0311             runner = new DBusRunner(q, pluginMetaData, {});
0312         } else {
0313             qCWarning(KRUNNER) << "Unknown X-Plasma-API requested for runner" << pluginMetaData.fileName();
0314             return nullptr;
0315         }
0316 
0317         if (runner) {
0318             QObject::connect(runner, &AbstractRunner::matchingSuspended, q, [this](bool state) {
0319                 runnerMatchingSuspended(state);
0320             });
0321             runner->init();
0322             if (prepped) {
0323                 Q_EMIT runner->prepare();
0324             }
0325         }
0326 
0327         return runner;
0328     }
0329 
0330     void jobDone(ThreadWeaver::JobPointer job)
0331     {
0332         auto runJob = job.dynamicCast<FindMatchesJob>();
0333 
0334         if (!runJob) {
0335             return;
0336         }
0337 
0338         searchJobs.remove(runJob);
0339         oldSearchJobs.remove(runJob);
0340 
0341         if (searchJobs.isEmpty()) {
0342             // If there are any new matches scheduled to be notified, we should anticipate it and just refresh right now
0343             if (matchChangeTimer.isActive()) {
0344                 matchChangeTimer.stop();
0345                 Q_EMIT q->matchesChanged(context.matches());
0346             } else if (context.matches().isEmpty()) {
0347                 // we finished our run, and there are no valid matches, and so no
0348                 // signal will have been sent out. so we need to emit the signal
0349                 // ourselves here
0350                 Q_EMIT q->matchesChanged(context.matches());
0351             }
0352             Q_EMIT q->queryFinished();
0353         }
0354     }
0355 
0356     void checkTearDown()
0357     {
0358         if (!prepped || !teardownRequested) {
0359             return;
0360         }
0361 
0362         if (Queue::instance()->isIdle()) {
0363             searchJobs.clear();
0364             oldSearchJobs.clear();
0365         }
0366 
0367         if (searchJobs.isEmpty() && oldSearchJobs.isEmpty()) {
0368             if (allRunnersPrepped) {
0369                 for (AbstractRunner *runner : std::as_const(runners)) {
0370                     Q_EMIT runner->teardown();
0371                 }
0372 
0373                 allRunnersPrepped = false;
0374             }
0375 
0376             if (singleRunnerPrepped) {
0377                 if (currentSingleRunner) {
0378                     Q_EMIT currentSingleRunner->teardown();
0379                 }
0380 
0381                 singleRunnerPrepped = false;
0382             }
0383 
0384             prepped = false;
0385             teardownRequested = false;
0386         }
0387     }
0388 
0389     void unblockJobs()
0390     {
0391         if (searchJobs.isEmpty() && Queue::instance()->isIdle()) {
0392             oldSearchJobs.clear();
0393             checkTearDown();
0394             return;
0395         }
0396 
0397         Queue::instance()->reschedule();
0398     }
0399 
0400     void runnerMatchingSuspended(bool suspended)
0401     {
0402         auto *runner = qobject_cast<AbstractRunner *>(q->sender());
0403         if (suspended || !prepped || teardownRequested || !runner) {
0404             return;
0405         }
0406 
0407         const QString query = context.query();
0408         if (singleMode || runner->minLetterCount() <= query.size()) {
0409             if (singleMode || !runner->hasMatchRegex() || runner->matchRegex().match(query).hasMatch()) {
0410                 startJob(runner);
0411             }
0412         }
0413     }
0414 
0415     void startJob(AbstractRunner *runner)
0416     {
0417 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0418         if ((runner->ignoredTypes() & context.type()) != 0) {
0419             return;
0420         }
0421 #endif
0422         QSharedPointer<FindMatchesJob> job(new FindMatchesJob(runner, &context, Queue::instance()));
0423         QObject::connect(job.data(), &FindMatchesJob::done, q, [this](ThreadWeaver::JobPointer jobPtr) {
0424             jobDone(jobPtr);
0425         });
0426 
0427 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 81)
0428         if (runner->speed() == AbstractRunner::SlowSpeed) {
0429             job->setDelayTimer(&delayTimer);
0430         }
0431 #endif
0432         Queue::instance()->enqueue(job);
0433         searchJobs.insert(job);
0434     }
0435 
0436     inline QString getActivityKey()
0437     {
0438 #if HAVE_KACTIVITIES
0439         if (activityAware) {
0440             const QString currentActivity = activitiesConsumer.currentActivity();
0441             return currentActivity.isEmpty() ? nulluuid : currentActivity;
0442         }
0443 #endif
0444         return nulluuid;
0445     }
0446 
0447     void addToHistory()
0448     {
0449         const QString term = context.query();
0450         // We want to imitate the shall behavior
0451         if (!historyEnabled || term.isEmpty() || untrimmedTerm.startsWith(QLatin1Char(' '))) {
0452             return;
0453         }
0454         QStringList historyEntries = readHistoryForCurrentActivity();
0455         // Avoid removing the same item from the front and prepending it again
0456         if (!historyEntries.isEmpty() && historyEntries.constFirst() == term) {
0457             return;
0458         }
0459 
0460         historyEntries.removeOne(term);
0461         historyEntries.prepend(term);
0462 
0463         while (historyEntries.count() > 50) { // we don't want to store more than 50 entries
0464             historyEntries.removeLast();
0465         }
0466         writeActivityHistory(historyEntries);
0467     }
0468 
0469     void writeActivityHistory(const QStringList &historyEntries)
0470     {
0471         stateData.group("History").writeEntry(getActivityKey(), historyEntries, KConfig::Notify);
0472         stateData.sync();
0473     }
0474 
0475 #if HAVE_KACTIVITIES
0476     void deleteHistoryOfDeletedActivities()
0477     {
0478         KConfigGroup historyGroup = stateData.group("History");
0479         QStringList historyEntries = historyGroup.keyList();
0480         historyEntries.removeOne(nulluuid);
0481 
0482         // Check if history still exists
0483         const QStringList activities = activitiesConsumer.activities();
0484         for (const auto &a : activities) {
0485             historyEntries.removeOne(a);
0486         }
0487 
0488         for (const QString &deletedActivity : std::as_const(historyEntries)) {
0489             historyGroup.deleteEntry(deletedActivity);
0490         }
0491         historyGroup.sync();
0492     }
0493 #endif
0494 
0495     inline QStringList readHistoryForCurrentActivity()
0496     {
0497         return stateData.group("History").readEntry(getActivityKey(), QStringList());
0498     }
0499 
0500     // Delay in ms before slow runners are allowed to run
0501     static const int slowRunDelay = 400;
0502 
0503     RunnerManager *const q;
0504     RunnerContext context;
0505     QTimer matchChangeTimer;
0506     QTimer delayTimer; // Timer to control when to run slow runners
0507     QElapsedTimer lastMatchChangeSignalled;
0508     QHash<QString, AbstractRunner *> runners;
0509     AbstractRunner *currentSingleRunner = nullptr;
0510     QSet<QSharedPointer<FindMatchesJob>> searchJobs;
0511     QSet<QSharedPointer<FindMatchesJob>> oldSearchJobs;
0512     QStringList enabledCategories;
0513     QString singleModeRunnerId;
0514     bool prepped = false;
0515     bool allRunnersPrepped = false;
0516     bool singleRunnerPrepped = false;
0517     bool teardownRequested = false;
0518     bool singleMode = false;
0519     bool activityAware = false;
0520     bool historyEnabled = false;
0521     bool retainPriorSearch = false;
0522     QStringList whiteList;
0523     KConfigWatcher::Ptr watcher;
0524     QHash<QString, QString> priorSearch;
0525     QString untrimmedTerm;
0526     QString nulluuid = QStringLiteral("00000000-0000-0000-0000-000000000000");
0527     KSharedConfigPtr configPrt;
0528     KConfigGroup stateData;
0529     QSet<QString> disabledRunnerIds; // Runners that are disabled but were loaded as single runners
0530 #if HAVE_KACTIVITIES
0531     const KActivities::Consumer activitiesConsumer;
0532 #endif
0533 };
0534 
0535 RunnerManager::RunnerManager(QObject *parent)
0536     : RunnerManager(QString(), parent)
0537 {
0538 }
0539 
0540 RunnerManager::RunnerManager(const QString &configFile, QObject *parent)
0541     : QObject(parent)
0542     , d(new RunnerManagerPrivate(this))
0543 {
0544     d->configPrt = KSharedConfig::openConfig(configFile);
0545     // If the old config group still exists the migration script wasn't executed
0546     // so we keep using this location
0547     KConfigGroup oldStateDataGroup = d->configPrt->group("PlasmaRunnerManager");
0548     if (oldStateDataGroup.exists() && !oldStateDataGroup.readEntry("migrated", false)) {
0549         d->stateData = oldStateDataGroup;
0550     } else {
0551         d->stateData =
0552             KSharedConfig::openConfig(QStringLiteral("krunnerstaterc"), KConfig::NoGlobals, QStandardPaths::GenericDataLocation)->group("PlasmaRunnerManager");
0553     }
0554     d->loadConfiguration();
0555 }
0556 
0557 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0558 RunnerManager::RunnerManager(KConfigGroup &c, QObject *parent)
0559     : QObject(parent)
0560     , d(new RunnerManagerPrivate(this))
0561 {
0562     d->configPrt = KSharedConfig::openConfig();
0563     d->stateData = KConfigGroup(&c, "PlasmaRunnerManager");
0564     d->loadConfiguration();
0565 }
0566 #endif
0567 
0568 RunnerManager::~RunnerManager()
0569 {
0570     if (!qApp->closingDown() && (!d->searchJobs.isEmpty() || !d->oldSearchJobs.isEmpty())) {
0571         const QSet<QSharedPointer<FindMatchesJob>> jobs(d->searchJobs + d->oldSearchJobs);
0572         QSet<AbstractRunner *> runners;
0573         for (auto &job : jobs) {
0574             job->runner()->setParent(nullptr);
0575             runners << job->runner();
0576         }
0577         new DelayedJobCleaner(jobs, runners);
0578     }
0579 }
0580 
0581 void RunnerManager::reloadConfiguration()
0582 {
0583     d->configPrt->reparseConfiguration();
0584     d->stateData.config()->reparseConfiguration();
0585     d->loadConfiguration();
0586     d->loadRunners();
0587 }
0588 
0589 void RunnerManager::setAllowedRunners(const QStringList &runners)
0590 {
0591     d->whiteList = runners;
0592     if (!d->runners.isEmpty()) {
0593         // this has been called with runners already created. so let's do an instant reload
0594         d->loadRunners();
0595     }
0596 }
0597 
0598 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0599 void RunnerManager::setEnabledCategories(const QStringList &categories)
0600 {
0601     d->stateData.writeEntry("enabledCategories", categories);
0602     d->enabledCategories = categories;
0603 
0604     if (!d->runners.isEmpty()) {
0605         d->loadRunners();
0606     }
0607 }
0608 #endif
0609 
0610 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 88)
0611 QStringList RunnerManager::allowedRunners() const
0612 {
0613     return d->stateData.readEntry("pluginWhiteList", QStringList());
0614 }
0615 #endif
0616 
0617 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0618 QStringList RunnerManager::enabledCategories() const
0619 {
0620     QStringList list = d->stateData.readEntry("enabledCategories", QStringList());
0621     if (list.isEmpty()) {
0622         list.reserve(d->runners.count());
0623         for (AbstractRunner *runner : std::as_const(d->runners)) {
0624             list << runner->categories();
0625         }
0626     }
0627 
0628     return list;
0629 }
0630 #endif
0631 
0632 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72) && KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
0633 void RunnerManager::loadRunner(const KService::Ptr service)
0634 {
0635     QT_WARNING_PUSH
0636     QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0637     QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0638     KPluginInfo description(service);
0639     QT_WARNING_POP
0640     loadRunner(description.toMetaData());
0641 }
0642 #endif
0643 
0644 void RunnerManager::loadRunner(const KPluginMetaData &pluginMetaData)
0645 {
0646     const QString runnerName = pluginMetaData.pluginId();
0647     if (!runnerName.isEmpty() && !d->runners.contains(runnerName)) {
0648         if (AbstractRunner *runner = d->loadInstalledRunner(pluginMetaData)) {
0649             d->runners.insert(runnerName, runner);
0650         }
0651     }
0652 }
0653 
0654 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 77)
0655 void RunnerManager::loadRunner(const QString &path)
0656 {
0657     if (!d->runners.contains(path)) {
0658         AbstractRunner *runner = new AbstractRunner(this, path);
0659         connect(runner, &AbstractRunner::matchingSuspended, this, [this](bool state) {
0660             d->runnerMatchingSuspended(state);
0661         });
0662         d->runners.insert(path, runner);
0663     }
0664 }
0665 #endif
0666 
0667 AbstractRunner *RunnerManager::runner(const QString &name) const
0668 {
0669     if (d->runners.isEmpty()) {
0670         d->loadRunners();
0671     }
0672 
0673     return d->runners.value(name, nullptr);
0674 }
0675 
0676 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 82)
0677 AbstractRunner *RunnerManager::singleModeRunner() const
0678 {
0679     return d->currentSingleRunner;
0680 }
0681 
0682 void RunnerManager::setSingleModeRunnerId(const QString &id)
0683 {
0684     d->singleModeRunnerId = id;
0685     d->loadSingleRunner();
0686 }
0687 
0688 QString RunnerManager::singleModeRunnerId() const
0689 {
0690     return d->singleModeRunnerId;
0691 }
0692 
0693 bool RunnerManager::singleMode() const
0694 {
0695     return d->singleMode;
0696 }
0697 
0698 void RunnerManager::setSingleMode(bool singleMode)
0699 {
0700     if (d->singleMode == singleMode) {
0701         return;
0702     }
0703 
0704     Plasma::AbstractRunner *prevSingleRunner = d->currentSingleRunner;
0705     d->singleMode = singleMode;
0706     d->loadSingleRunner();
0707     d->singleMode = d->currentSingleRunner;
0708 
0709     if (prevSingleRunner != d->currentSingleRunner) {
0710         if (d->prepped) {
0711             matchSessionComplete();
0712 
0713             if (d->singleMode) {
0714                 setupMatchSession();
0715             }
0716         }
0717     }
0718 }
0719 
0720 QStringList RunnerManager::singleModeAdvertisedRunnerIds() const
0721 {
0722     QStringList advertiseSingleRunnerIds;
0723     for (auto *runner : std::as_const(d->runners)) {
0724         if (runner->metadata(RunnerReturnPluginMetaData).rawData().value(QStringLiteral("X-Plasma-AdvertiseSingleRunnerQueryMode")).toVariant().toBool()) {
0725             advertiseSingleRunnerIds << runner->id();
0726         }
0727     }
0728     return advertiseSingleRunnerIds;
0729 }
0730 
0731 QString RunnerManager::runnerName(const QString &id) const
0732 {
0733     return d->runners.contains(id) ? d->runners.value(id)->name() : QString();
0734 }
0735 #endif
0736 
0737 QList<AbstractRunner *> RunnerManager::runners() const
0738 {
0739     return d->runners.values();
0740 }
0741 
0742 RunnerContext *RunnerManager::searchContext() const
0743 {
0744     return &d->context;
0745 }
0746 
0747 // Reordering is here so data is not reordered till strictly needed
0748 QList<QueryMatch> RunnerManager::matches() const
0749 {
0750     return d->context.matches();
0751 }
0752 
0753 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 79)
0754 void RunnerManager::run(const QString &matchId)
0755 {
0756     run(d->context.match(matchId));
0757 }
0758 #endif
0759 
0760 void RunnerManager::run(const QueryMatch &match)
0761 {
0762     if (match.isEnabled()) {
0763         d->context.run(match);
0764     }
0765 }
0766 
0767 bool RunnerManager::runMatch(const QueryMatch &match)
0768 {
0769 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 99)
0770     if (match.type() == Plasma::QueryMatch::InformationalMatch && !match.selectedAction()) {
0771         d->addToHistory();
0772         const QString info = match.data().toString();
0773         qWarning() << Q_FUNC_INFO << info << match.data();
0774         if (!info.isEmpty()) {
0775             Q_EMIT setSearchTerm(info, info.length());
0776             return false;
0777         }
0778     }
0779 #endif
0780     d->context.run(match);
0781     if (!d->context.shouldIgnoreCurrentMatchForHistory()) {
0782         d->addToHistory();
0783     }
0784     if (d->context.requestedQueryString().isEmpty()) {
0785         return true;
0786     } else {
0787         Q_EMIT setSearchTerm(d->context.requestedQueryString(), d->context.requestedCursorPosition());
0788         return false;
0789     }
0790 }
0791 
0792 QList<QAction *> RunnerManager::actionsForMatch(const QueryMatch &match)
0793 {
0794     if (AbstractRunner *runner = match.runner()) {
0795         return runner->actionsForMatch(match);
0796     }
0797 
0798     return QList<QAction *>();
0799 }
0800 
0801 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 79)
0802 QMimeData *RunnerManager::mimeDataForMatch(const QString &id) const
0803 {
0804     return mimeDataForMatch(d->context.match(id));
0805 }
0806 #endif
0807 
0808 QMimeData *RunnerManager::mimeDataForMatch(const QueryMatch &match) const
0809 {
0810     return match.isValid() ? match.runner()->mimeDataForMatch(match) : nullptr;
0811 }
0812 
0813 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 85)
0814 QVector<KPluginMetaData> RunnerManager::runnerMetaDataList(const QString &parentApp)
0815 {
0816     // get binary plugins
0817     // filter rule also covers parentApp.isEmpty()
0818     auto filterParentApp = [&parentApp](const KPluginMetaData &md) -> bool {
0819         return md.value(QStringLiteral("X-KDE-ParentApp")) == parentApp;
0820     };
0821 
0822     QVector<KPluginMetaData> pluginMetaDatas = KPluginMetaData::findPlugins(QStringLiteral("kf5/krunner"), filterParentApp);
0823     QSet<QString> knownRunnerIds;
0824     knownRunnerIds.reserve(pluginMetaDatas.size());
0825     for (const KPluginMetaData &pluginMetaData : std::as_const(pluginMetaDatas)) {
0826         knownRunnerIds.insert(pluginMetaData.pluginId());
0827     }
0828 
0829     const QStringList dBusPlugindirs =
0830         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("krunner/dbusplugins"), QStandardPaths::LocateDirectory);
0831     const QStringList dbusRunnerFiles = KFileUtils::findAllUniqueFiles(dBusPlugindirs, QStringList(QStringLiteral("*.desktop")));
0832     for (const QString &dbusRunnerFile : dbusRunnerFiles) {
0833         KPluginMetaData pluginMetaData = parseMetaDataFromDesktopFile(dbusRunnerFile);
0834         if (pluginMetaData.isValid() && !knownRunnerIds.contains(pluginMetaData.pluginId())) {
0835             pluginMetaDatas.append(pluginMetaData);
0836             knownRunnerIds.insert(pluginMetaData.pluginId());
0837         }
0838     }
0839 
0840 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
0841     // also search for deprecated kservice-based KRunner plugins metadata
0842     const QString constraint = parentApp.isEmpty() ? QStringLiteral("not exist [X-KDE-ParentApp] or [X-KDE-ParentApp] == ''")
0843                                                    : QStringLiteral("[X-KDE-ParentApp] == '") + parentApp + QLatin1Char('\'');
0844 
0845     QT_WARNING_PUSH
0846     QT_WARNING_DISABLE_DEPRECATED
0847     const KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("Plasma/Runner"), constraint);
0848     const KPluginInfo::List backwardCompatPluginInfos = KPluginInfo::fromServices(offers);
0849     QT_WARNING_POP
0850 
0851     for (const KPluginInfo &pluginInfo : backwardCompatPluginInfos) {
0852         if (!knownRunnerIds.contains(pluginInfo.pluginName())) {
0853             warnAboutDeprecatedMetaData(pluginInfo);
0854             pluginMetaDatas.append(pluginInfo.toMetaData());
0855         }
0856     }
0857 #endif
0858 
0859     return pluginMetaDatas;
0860 }
0861 #endif
0862 
0863 QVector<KPluginMetaData> RunnerManager::runnerMetaDataList()
0864 {
0865     QVector<KPluginMetaData> pluginMetaDatas = KPluginMetaData::findPlugins(QStringLiteral("kf" QT_STRINGIFY(QT_VERSION_MAJOR) "/krunner"));
0866     QSet<QString> knownRunnerIds;
0867     knownRunnerIds.reserve(pluginMetaDatas.size());
0868     for (const KPluginMetaData &pluginMetaData : std::as_const(pluginMetaDatas)) {
0869         knownRunnerIds.insert(pluginMetaData.pluginId());
0870     }
0871 
0872     const QStringList dBusPlugindirs =
0873         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("krunner/dbusplugins"), QStandardPaths::LocateDirectory);
0874     const QStringList dbusRunnerFiles = KFileUtils::findAllUniqueFiles(dBusPlugindirs, QStringList(QStringLiteral("*.desktop")));
0875     for (const QString &dbusRunnerFile : dbusRunnerFiles) {
0876         KPluginMetaData pluginMetaData = parseMetaDataFromDesktopFile(dbusRunnerFile);
0877         if (pluginMetaData.isValid() && !knownRunnerIds.contains(pluginMetaData.pluginId())) {
0878             pluginMetaDatas.append(pluginMetaData);
0879             knownRunnerIds.insert(pluginMetaData.pluginId());
0880         }
0881     }
0882 
0883 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72) && KSERVICE_BUILD_DEPRECATED_SINCE(5, 0)
0884     // also search for deprecated kservice-based KRunner plugins metadata
0885     QT_WARNING_PUSH
0886     QT_WARNING_DISABLE_DEPRECATED
0887     const KService::List offers = KServiceTypeTrader::self()->query(QStringLiteral("Plasma/Runner"));
0888     const KPluginInfo::List backwardCompatPluginInfos = KPluginInfo::fromServices(offers);
0889     QT_WARNING_POP
0890 
0891     for (const KPluginInfo &pluginInfo : backwardCompatPluginInfos) {
0892         if (!knownRunnerIds.contains(pluginInfo.pluginName())) {
0893             warnAboutDeprecatedMetaData(pluginInfo);
0894             pluginMetaDatas.append(pluginInfo.toMetaData());
0895         }
0896     }
0897 #endif
0898 
0899     return pluginMetaDatas;
0900 }
0901 
0902 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 72)
0903 KPluginInfo::List RunnerManager::listRunnerInfo(const QString &parentApp)
0904 {
0905     QT_WARNING_PUSH
0906     QT_WARNING_DISABLE_DEPRECATED
0907     return KPluginInfo::fromMetaData(runnerMetaDataList(parentApp));
0908     QT_WARNING_POP
0909 }
0910 #endif
0911 
0912 void RunnerManager::setupMatchSession()
0913 {
0914     d->teardownRequested = false;
0915 
0916     if (d->prepped) {
0917         return;
0918     }
0919 
0920     d->prepped = true;
0921     if (d->singleMode) {
0922         if (d->currentSingleRunner) {
0923             Q_EMIT d->currentSingleRunner->prepare();
0924             d->singleRunnerPrepped = true;
0925         }
0926     } else {
0927         for (AbstractRunner *runner : std::as_const(d->runners)) {
0928             if (!d->disabledRunnerIds.contains(runner->name())) {
0929                 Q_EMIT runner->prepare();
0930             }
0931         }
0932 
0933         d->allRunnersPrepped = true;
0934     }
0935 }
0936 
0937 void RunnerManager::matchSessionComplete()
0938 {
0939     if (!d->prepped) {
0940         return;
0941     }
0942 
0943     d->teardownRequested = true;
0944     d->checkTearDown();
0945     // We save the context config after each session, just like the history entries
0946     // BUG: 424505
0947     d->context.save(d->stateData);
0948 }
0949 
0950 void RunnerManager::launchQuery(const QString &term)
0951 {
0952     launchQuery(term, QString());
0953 }
0954 
0955 void RunnerManager::launchQuery(const QString &untrimmedTerm, const QString &runnerName)
0956 {
0957     setupMatchSession();
0958     QString term = untrimmedTerm.trimmed();
0959     const QString prevSingleRunner = d->singleModeRunnerId;
0960     d->untrimmedTerm = untrimmedTerm;
0961 
0962     // Set the required values and load the runner
0963     d->singleModeRunnerId = runnerName;
0964     d->singleMode = !runnerName.isEmpty();
0965     d->loadSingleRunner();
0966     // If we could not load the single runner we reset
0967     if (!runnerName.isEmpty() && !d->currentSingleRunner) {
0968         reset();
0969         return;
0970     }
0971 
0972     if (d->context.query() == term && prevSingleRunner == runnerName) {
0973         // we already are searching for this!
0974         return;
0975     }
0976 
0977     if (!d->singleMode && d->runners.isEmpty()) {
0978         d->loadRunners();
0979     }
0980 
0981     reset();
0982     d->context.setQuery(term);
0983 #if KRUNNER_BUILD_DEPRECATED_SINCE(5, 76)
0984     d->context.setEnabledCategories(d->enabledCategories);
0985 #endif
0986 
0987     QHash<QString, AbstractRunner *> runnable;
0988 
0989     // if the name is not empty we will launch only the specified runner
0990     if (d->singleMode) {
0991         runnable.insert(QString(), d->currentSingleRunner);
0992         d->context.setSingleRunnerQueryMode(true);
0993     } else {
0994         runnable = d->runners;
0995     }
0996 
0997     const int queryLetterCount = term.count();
0998     for (Plasma::AbstractRunner *r : std::as_const(runnable)) {
0999         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({});
1023             Q_EMIT queryFinished();
1024         });
1025     }
1026 
1027     // Start timer to unblock slow runners
1028     d->delayTimer.start(RunnerManagerPrivate::slowRunDelay);
1029 }
1030 
1031 QString RunnerManager::query() const
1032 {
1033     return d->context.query();
1034 }
1035 
1036 QStringList RunnerManager::history() const
1037 {
1038     return d->readHistoryForCurrentActivity();
1039 }
1040 
1041 void RunnerManager::removeFromHistory(int index)
1042 {
1043     QStringList changedHistory = history();
1044     if (index < changedHistory.length()) {
1045         changedHistory.removeAt(index);
1046         d->writeActivityHistory(changedHistory);
1047     }
1048 }
1049 
1050 QString RunnerManager::getHistorySuggestion(const QString &typedQuery) const
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 
1061 void RunnerManager::reset()
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()) {
1077         Q_EMIT queryFinished();
1078     }
1079 }
1080 
1081 KPluginMetaData RunnerManager::convertDBusRunnerToJson(const QString &filename) const
1082 {
1083     return parseMetaDataFromDesktopFile(filename);
1084 }
1085 
1086 void RunnerManager::enableKNotifyPluginWatcher()
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"