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"