/* * Copyright (C) 2006 Aaron Seigo * Copyright (C) 2007, 2009 Ryan P. Bitanga * Copyright (C) 2008 Jordi Polo * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License as * published by the Free Software Foundation; either version 2, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details * * You should have received a copy of the GNU Library General Public * License along with this program; if not, write to the * Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "runnermanager.h" #include "config-plasma.h" #include #include #include #include #include #include #include #if !PLASMA_NO_SOLID #include #include #endif #include #include #include #include #include "private/runnerjobs_p.h" #include "pluginloader.h" #include "querymatch.h" using ThreadWeaver::Weaver; using ThreadWeaver::Job; //#define MEASURE_PREPTIME namespace Plasma { /***************************************************** * RunnerManager::Private class * *****************************************************/ class RunnerManagerPrivate { public: RunnerManagerPrivate(RunnerManager *parent) : q(parent), deferredRun(0), currentSingleRunner(0), prepped(false), allRunnersPrepped(false), singleRunnerPrepped(false), teardownRequested(false), singleMode(false), singleRunnerWasLoaded(false) { matchChangeTimer.setSingleShot(true); delayTimer.setSingleShot(true); QObject::connect(&matchChangeTimer, SIGNAL(timeout()), q, SLOT(matchesChanged())); QObject::connect(&context, SIGNAL(matchesChanged()), q, SLOT(scheduleMatchesChanged())); QObject::connect(&delayTimer, SIGNAL(timeout()), q, SLOT(unblockJobs())); } ~RunnerManagerPrivate() { KConfigGroup config = configGroup(); context.save(config); } void scheduleMatchesChanged() { matchChangeTimer.start(100); } void matchesChanged() { emit q->matchesChanged(context.matches()); } void loadConfiguration() { KConfigGroup config = configGroup(); //The number of threads used scales with the number of processors. #if !PLASMA_NO_SOLID const int numProcs = qMax(Solid::Device::listFromType(Solid::DeviceInterface::Processor).count(), 1); #else const int numProcs = 1; #endif //This entry allows to define a hard upper limit independent of the number of processors. const int maxThreads = config.readEntry("maxThreads", 16); const int numThreads = qMin(maxThreads, 2 + ((numProcs - 1) * 2)); //kDebug() << "setting up" << numThreads << "threads for" << numProcs << "processors"; if (numThreads > Weaver::instance()->maximumNumberOfThreads()) { Weaver::instance()->setMaximumNumberOfThreads(numThreads); } // Limit the number of instances of a single normal speed runner and all of the slow runners // to half the number of threads const int cap = qMax(2, numThreads/2); DefaultRunnerPolicy::instance().setCap(cap); context.restore(config); } KConfigGroup configGroup() { return conf.isValid() ? conf : KConfigGroup(KSharedConfig::openConfig(), "PlasmaRunnerManager"); } void clearSingleRunner() { if (singleRunnerWasLoaded) { delete currentSingleRunner; } currentSingleRunner = 0; } void loadSingleRunner() { if (!singleMode || singleModeRunnerId.isEmpty()) { clearSingleRunner(); return; } if (currentSingleRunner) { if (currentSingleRunner->id() == singleModeRunnerId) { return; } clearSingleRunner(); } AbstractRunner *loadedRunner = q->runner(singleModeRunnerId); if (loadedRunner) { singleRunnerWasLoaded = false; currentSingleRunner = loadedRunner; return; } KService::List offers = KServiceTypeTrader::self()->query("Plasma/Runner", QString("[X-KDE-PluginInfo-Name] == '%1'").arg(singleModeRunnerId)); if (!offers.isEmpty()) { const KService::Ptr &service = offers[0]; currentSingleRunner = loadInstalledRunner(service); if (currentSingleRunner) { emit currentSingleRunner->prepare(); singleRunnerWasLoaded = true; } } } void loadRunners() { KConfigGroup config = configGroup(); KPluginInfo::List offers = RunnerManager::listRunnerInfo(); const bool loadAll = config.readEntry("loadAll", false); const QStringList whiteList = config.readEntry("pluginWhiteList", QStringList()); const bool noWhiteList = whiteList.isEmpty(); KConfigGroup pluginConf; if (conf.isValid()) { pluginConf = KConfigGroup(&conf, "Plugins"); } else { pluginConf = KConfigGroup(KSharedConfig::openConfig(), "Plugins"); } advertiseSingleRunnerIds.clear(); QSet deadRunners; QMutableListIterator it(offers); while (it.hasNext()) { KPluginInfo &description = it.next(); //kDebug() << "Loading runner: " << service->name() << service->storageId(); QString tryExec = description.property("TryExec").toString(); //kDebug() << "TryExec is" << tryExec; if (!tryExec.isEmpty() && QStandardPaths::findExecutable(tryExec).isEmpty()) { // we don't actually have this application! continue; } const QString runnerName = description.pluginName(); description.load(pluginConf); const bool loaded = runners.contains(runnerName); const bool selected = loadAll || (description.isPluginEnabled() && (noWhiteList || whiteList.contains(runnerName))); const bool singleQueryModeEnabled = description.property("X-Plasma-AdvertiseSingleRunnerQueryMode").toBool(); if (singleQueryModeEnabled) { advertiseSingleRunnerIds.insert(runnerName, description.name()); } //kDebug() << loadAll << description.isPluginEnabled() << noWhiteList << whiteList.contains(runnerName); if (selected) { if (!loaded) { AbstractRunner *runner = loadInstalledRunner(description.service()); if (runner) { runners.insert(runnerName, runner); } } } else if (loaded) { //Remove runner deadRunners.insert(runners.take(runnerName)); #ifndef NDEBUG kDebug() << "Removing runner: " << runnerName; #endif } } if (!deadRunners.isEmpty()) { QSet deadJobs; foreach (FindMatchesJob *job, searchJobs) { if (deadRunners.contains(job->runner())) { QObject::disconnect(job, SIGNAL(done(ThreadWeaver::Job*)), q, SLOT(jobDone(ThreadWeaver::Job*))); searchJobs.remove(job); deadJobs.insert(job); } } foreach (FindMatchesJob *job, oldSearchJobs) { if (deadRunners.contains(job->runner())) { oldSearchJobs.remove(job); deadJobs.insert(job); } } if (deadJobs.isEmpty()) { qDeleteAll(deadRunners); } else { new DelayedJobCleaner(deadJobs, deadRunners); } } if (!singleRunnerWasLoaded) { // in case we deleted it up above clearSingleRunner(); } #ifndef NDEBUG kDebug() << "All runners loaded, total:" << runners.count(); #endif } AbstractRunner *loadInstalledRunner(const KService::Ptr service) { if (!service) { return 0; } AbstractRunner *runner = PluginLoader::self()->loadRunner(service->property("X-KDE-PluginInfo-Name", QVariant::String).toString()); if (runner) { runner->setParent(q); } else { const QString api = service->property("X-Plasma-API").toString(); if (api.isEmpty()) { QVariantList args; args << service->storageId(); if (Plasma::isPluginVersionCompatible(KPluginLoader(*service).pluginVersion())) { QString error; runner = service->createInstance(q, args, &error); if (!runner) { #ifndef NDEBUG kDebug() << "Failed to load runner:" << service->name() << ". error reported:" << error; #endif } } } else { //kDebug() << "got a script runner known as" << api; runner = new AbstractRunner(service, q); } } if (runner) { #ifndef NDEBUG kDebug() << "================= loading runner:" << service->name() << "================="; #endif QObject::connect(runner, SIGNAL(matchingSuspended(bool)), q, SLOT(runnerMatchingSuspended(bool))); runner->init(); if (prepped) { emit runner->prepare(); } } return runner; } void jobDone(ThreadWeaver::Job *job) { FindMatchesJob *runJob = dynamic_cast(job); if (!runJob) { return; } if (deferredRun.isEnabled() && runJob->runner() == deferredRun.runner()) { //kDebug() << "job actually done, running now **************"; QueryMatch tmpRun = deferredRun; deferredRun = QueryMatch(0); tmpRun.run(context); } searchJobs.remove(runJob); oldSearchJobs.remove(runJob); runJob->deleteLater(); if (searchJobs.isEmpty() && context.matches().isEmpty()) { // we finished our run, and there are no valid matches, and so no // signal will have been sent out. so we need to emit the signal // ourselves here emit q->matchesChanged(context.matches()); } checkTearDown(); } void checkTearDown() { //kDebug() << prepped << teardownRequested << searchJobs.count() << oldSearchJobs.count(); if (!prepped || !teardownRequested) { return; } if (Weaver::instance()->isIdle()) { qDeleteAll(searchJobs); searchJobs.clear(); qDeleteAll(oldSearchJobs); oldSearchJobs.clear(); } if (searchJobs.isEmpty() && oldSearchJobs.isEmpty()) { if (allRunnersPrepped) { foreach (AbstractRunner *runner, runners) { emit runner->teardown(); } allRunnersPrepped = false; } if (singleRunnerPrepped) { if (currentSingleRunner) { emit currentSingleRunner->teardown(); } singleRunnerPrepped = false; } emit q->queryFinished(); prepped = false; teardownRequested = false; } } void unblockJobs() { // WORKAROUND: Queue an empty job to force ThreadWeaver to awaken threads if (searchJobs.isEmpty() && Weaver::instance()->isIdle()) { qDeleteAll(oldSearchJobs); oldSearchJobs.clear(); checkTearDown(); return; } DummyJob *dummy = new DummyJob(q); Weaver::instance()->enqueue(dummy); QObject::connect(dummy, SIGNAL(done(ThreadWeaver::Job*)), dummy, SLOT(deleteLater())); } void runnerMatchingSuspended(bool suspended) { if (suspended || !prepped || teardownRequested) { return; } AbstractRunner *runner = qobject_cast(q->sender()); if (runner) { startJob(runner); } } void startJob(AbstractRunner *runner) { if ((runner->ignoredTypes() & context.type()) == 0) { FindMatchesJob *job = new FindMatchesJob(runner, &context, Weaver::instance()); QObject::connect(job, SIGNAL(done(ThreadWeaver::Job*)), q, SLOT(jobDone(ThreadWeaver::Job*))); if (runner->speed() == AbstractRunner::SlowSpeed) { job->setDelayTimer(&delayTimer); } Weaver::instance()->enqueue(job); searchJobs.insert(job); } } // Delay in ms before slow runners are allowed to run static const int slowRunDelay = 400; RunnerManager *q; QueryMatch deferredRun; RunnerContext context; QTimer matchChangeTimer; QTimer delayTimer; // Timer to control when to run slow runners QHash runners; QHash advertiseSingleRunnerIds; AbstractRunner* currentSingleRunner; QSet searchJobs; QSet oldSearchJobs; KConfigGroup conf; QString singleModeRunnerId; bool loadAll : 1; bool prepped : 1; bool allRunnersPrepped : 1; bool singleRunnerPrepped : 1; bool teardownRequested : 1; bool singleMode : 1; bool singleRunnerWasLoaded : 1; }; /***************************************************** * RunnerManager::Public class * *****************************************************/ RunnerManager::RunnerManager(QObject *parent) : QObject(parent), d(new RunnerManagerPrivate(this)) { d->loadConfiguration(); //ThreadWeaver::setDebugLevel(true, 4); } RunnerManager::RunnerManager(KConfigGroup &c, QObject *parent) : QObject(parent), d(new RunnerManagerPrivate(this)) { // Should this be really needed? Maybe d->loadConfiguration(c) would make // more sense. d->conf = KConfigGroup(&c, "PlasmaRunnerManager"); d->loadConfiguration(); //ThreadWeaver::setDebugLevel(true, 4); } RunnerManager::~RunnerManager() { if (!qApp->closingDown() && (!d->searchJobs.isEmpty() || !d->oldSearchJobs.isEmpty())) { new DelayedJobCleaner(d->searchJobs + d->oldSearchJobs); } delete d; } void RunnerManager::reloadConfiguration() { d->loadConfiguration(); d->loadRunners(); } void RunnerManager::setAllowedRunners(const QStringList &runners) { KConfigGroup config = d->configGroup(); config.writeEntry("pluginWhiteList", runners); if (!d->runners.isEmpty()) { // this has been called with runners already created. so let's do an instant reload d->loadRunners(); } } QStringList RunnerManager::allowedRunners() const { KConfigGroup config = d->configGroup(); return config.readEntry("pluginWhiteList", QStringList()); } void RunnerManager::loadRunner(const KService::Ptr service) { KPluginInfo description(service); const QString runnerName = description.pluginName(); if (!runnerName.isEmpty() && !d->runners.contains(runnerName)) { AbstractRunner *runner = d->loadInstalledRunner(service); if (runner) { d->runners.insert(runnerName, runner); } } } void RunnerManager::loadRunner(const QString &path) { if (!d->runners.contains(path)) { AbstractRunner *runner = new AbstractRunner(this, path); connect(runner, SIGNAL(matchingSuspended(bool)), this, SLOT(runnerMatchingSuspended(bool))); d->runners.insert(path, runner); } } AbstractRunner* RunnerManager::runner(const QString &name) const { if (d->runners.isEmpty()) { d->loadRunners(); } return d->runners.value(name, 0); } AbstractRunner *RunnerManager::singleModeRunner() const { return d->currentSingleRunner; } void RunnerManager::setSingleModeRunnerId(const QString &id) { d->singleModeRunnerId = id; d->loadSingleRunner(); } QString RunnerManager::singleModeRunnerId() const { return d->singleModeRunnerId; } bool RunnerManager::singleMode() const { return d->singleMode; } void RunnerManager::setSingleMode(bool singleMode) { if (d->singleMode == singleMode) { return; } Plasma::AbstractRunner *prevSingleRunner = d->currentSingleRunner; d->singleMode = singleMode; d->loadSingleRunner(); d->singleMode = d->currentSingleRunner; if (prevSingleRunner != d->currentSingleRunner) { if (d->prepped) { matchSessionComplete(); if (d->singleMode) { setupMatchSession(); } } } } QList RunnerManager::runners() const { return d->runners.values(); } QStringList RunnerManager::singleModeAdvertisedRunnerIds() const { return d->advertiseSingleRunnerIds.keys(); } QString RunnerManager::runnerName(const QString &id) const { if (runner(id)) { return runner(id)->name(); } else { return d->advertiseSingleRunnerIds.value(id, QString()); } } RunnerContext* RunnerManager::searchContext() const { return &d->context; } //Reordering is here so data is not reordered till strictly needed QList RunnerManager::matches() const { return d->context.matches(); } void RunnerManager::run(const QString &id) { run(d->context.match(id)); } void RunnerManager::run(const QueryMatch &match) { if (!match.isEnabled()) { return; } //TODO: this function is not const as it may be used for learning AbstractRunner *runner = match.runner(); foreach (FindMatchesJob *job, d->searchJobs) { if (job->runner() == runner && !job->isFinished()) { #ifndef NDEBUG kDebug() << "deferred run"; #endif d->deferredRun = match; return; } } if (d->deferredRun.isValid()) { d->deferredRun = QueryMatch(0); } d->context.run(match); } QList RunnerManager::actionsForMatch(const QueryMatch &match) { AbstractRunner *runner = match.runner(); if (runner) { return runner->actionsForMatch(match); } return QList(); } QMimeData * RunnerManager::mimeDataForMatch(const QString &id) const { return mimeDataForMatch(d->context.match(id)); } QMimeData * RunnerManager::mimeDataForMatch(const QueryMatch &match) const { AbstractRunner *runner = match.runner(); if (runner) { return runner->mimeDataForMatch(match); } return 0; } KPluginInfo::List RunnerManager::listRunnerInfo(const QString &parentApp) { return PluginLoader::self()->listRunnerInfo(parentApp); } void RunnerManager::setupMatchSession() { d->teardownRequested = false; if (d->prepped) { return; } d->prepped = true; if (d->singleMode) { if (d->currentSingleRunner) { emit d->currentSingleRunner->prepare(); d->singleRunnerPrepped = true; } } else { foreach (AbstractRunner *runner, d->runners) { #ifdef MEASURE_PREPTIME QTime t; t.start(); #endif emit runner->prepare(); #ifdef MEASURE_PREPTIME #ifndef NDEBUG kDebug() << t.elapsed() << runner->name(); #endif #endif } d->allRunnersPrepped = true; } } void RunnerManager::matchSessionComplete() { if (!d->prepped) { return; } d->teardownRequested = true; d->checkTearDown(); } void RunnerManager::launchQuery(const QString &term) { launchQuery(term, QString()); } void RunnerManager::launchQuery(const QString &untrimmedTerm, const QString &runnerName) { setupMatchSession(); QString term = untrimmedTerm.trimmed(); setSingleModeRunnerId(runnerName); setSingleMode(!runnerName.isEmpty()); if (term.isEmpty()) { if (d->singleMode && d->currentSingleRunner && d->currentSingleRunner->defaultSyntax()) { term = d->currentSingleRunner->defaultSyntax()->exampleQueries().first().remove(QRegExp(":q:")); } else { reset(); return; } } if (d->context.query() == term) { // we already are searching for this! return; } if (d->singleMode && !d->currentSingleRunner) { reset(); return; } if (d->runners.isEmpty()) { d->loadRunners(); } reset(); // kDebug() << "runners searching for" << term << "on" << runnerName; d->context.setQuery(term); QHash runable; //if the name is not empty we will launch only the specified runner if (d->singleMode && d->currentSingleRunner) { runable.insert(QString(), d->currentSingleRunner); d->context.setSingleRunnerQueryMode(true); } else { runable = d->runners; } foreach (Plasma::AbstractRunner *r, runable) { if (r->isMatchingSuspended()) { continue; } d->startJob(r); } // Start timer to unblock slow runners d->delayTimer.start(RunnerManagerPrivate::slowRunDelay); } bool RunnerManager::execQuery(const QString &term) { return execQuery(term, QString()); } bool RunnerManager::execQuery(const QString &untrimmedTerm, const QString &runnerName) { QString term = untrimmedTerm.trimmed(); if (term.isEmpty()) { reset(); return false; } if (d->runners.isEmpty()) { d->loadRunners(); } if (d->context.query() == term) { // we already are searching for this! emit matchesChanged(d->context.matches()); return false; } reset(); //kDebug() << "executing query about " << term << "on" << runnerName; d->context.setQuery(term); AbstractRunner *r = runner(runnerName); if (!r) { //kDebug() << "failed to find the runner"; return false; } if ((r->ignoredTypes() & d->context.type()) != 0) { //kDebug() << "ignored!"; return false; } r->performMatch(d->context); //kDebug() << "succeeded with" << d->context.matches().count() << "results"; emit matchesChanged(d->context.matches()); return true; } QString RunnerManager::query() const { return d->context.query(); } void RunnerManager::reset() { // If ThreadWeaver is idle, it is safe to clear previous jobs if (Weaver::instance()->isIdle()) { qDeleteAll(d->searchJobs); qDeleteAll(d->oldSearchJobs); d->oldSearchJobs.clear(); } else { Q_FOREACH(FindMatchesJob *job, d->searchJobs) { Weaver::instance()->dequeue(job); } d->oldSearchJobs += d->searchJobs; } d->searchJobs.clear(); if (d->deferredRun.isEnabled()) { //kDebug() << "job actually done, running now **************"; QueryMatch tmpRun = d->deferredRun; d->deferredRun = QueryMatch(0); tmpRun.run(d->context); } d->context.reset(); } } // Plasma namespace #include "moc_runnermanager.cpp"