diff --git a/autotests/dbusrunnertest.cpp b/autotests/dbusrunnertest.cpp index 756e44b..6808473 100644 --- a/autotests/dbusrunnertest.cpp +++ b/autotests/dbusrunnertest.cpp @@ -1,181 +1,215 @@ /* * Copyright (C) 2017 David Edmundson * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License version 2 as * published by the Free Software Foundation * * 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 #include #include #include #include "runnermanager.h" #include #include +#include +#include #include using namespace Plasma; Q_DECLARE_METATYPE(Plasma::QueryMatch) Q_DECLARE_METATYPE(QList) class DBusRunnerTest : public QObject { Q_OBJECT public: DBusRunnerTest(); ~DBusRunnerTest(); private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void testMatch(); void testMulti(); + void testRequestActionsOnce(); private: QStringList m_filesForCleanup; }; DBusRunnerTest::DBusRunnerTest() : QObject() { qRegisterMetaType >(); } DBusRunnerTest::~DBusRunnerTest() { } void DBusRunnerTest::initTestCase() { // Set up a layer in the bin dir so ksycoca finds the Plasma/Runner service type const QByteArray defaultDataDirs = qEnvironmentVariableIsSet("XDG_DATA_DIRS") ? qgetenv("XDG_DATA_DIRS") : QByteArray("/usr/local:/usr"); const QByteArray modifiedDataDirs = QFile::encodeName(QCoreApplication::applicationDirPath()) + QByteArrayLiteral("/data:") + defaultDataDirs; qputenv("XDG_DATA_DIRS", modifiedDataDirs); QStandardPaths::setTestModeEnabled(true); QDir(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation)).mkpath(QStringLiteral("kservices5")); { const QString fakeServicePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kservices5/dbusrunnertest.desktop"); QFile::copy(QFINDTESTDATA("dbusrunnertest.desktop"), fakeServicePath); m_filesForCleanup << fakeServicePath; } { const QString fakeServicePath = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/kservices5/dbusrunnertestmulti.desktop"); QFile::copy(QFINDTESTDATA("dbusrunnertestmulti.desktop"), fakeServicePath); m_filesForCleanup << fakeServicePath; } KSycoca::self()->ensureCacheValid(); } void DBusRunnerTest::cleanupTestCase() { - for(const QString path: m_filesForCleanup) { + for(const QString &path: qAsConst(m_filesForCleanup)) { QFile::remove(path); } } void DBusRunnerTest::testMatch() { QProcess process; process.start(QFINDTESTDATA("testremoterunner"), QStringList({QStringLiteral("net.krunnertests.dave")})); QVERIFY(process.waitForStarted()); QTest::qSleep(500); RunnerManager m; auto s = KService::serviceByDesktopPath(QStringLiteral("dbusrunnertest.desktop")); QVERIFY(s); m.loadRunner(s); m.launchQuery(QStringLiteral("foo")); QSignalSpy spy(&m, &RunnerManager::matchesChanged); QVERIFY(spy.wait()); //verify matches QCOMPARE(m.matches().count(), 1); auto result = m.matches().first(); //see testremoterunner.cpp QCOMPARE(result.id(), QStringLiteral("dbusrunnertest_id1")); //note the runner name is prepended QCOMPARE(result.text(), QStringLiteral("Match 1")); QCOMPARE(result.iconName(), QStringLiteral("icon1")); QCOMPARE(result.type(), Plasma::QueryMatch::ExactMatch); //relevance can't be compared easily because RunnerContext meddles with it //verify actions auto actions = m.actionsForMatch(result); QCOMPARE(actions.count(), 1); auto action = actions.first(); QCOMPARE(action->text(), QStringLiteral("Action 1")); QSignalSpy processSpy(&process, &QProcess::readyRead); m.run(result); processSpy.wait(); QCOMPARE(process.readAllStandardOutput().trimmed(), QByteArray("Running:id1:")); result.setSelectedAction(action); m.run(result); processSpy.wait(); QCOMPARE(process.readAllStandardOutput().trimmed(), QByteArray("Running:id1:action1")); process.kill(); process.waitForFinished(); } void DBusRunnerTest::testMulti() { QProcess process1; process1.start(QFINDTESTDATA("testremoterunner"), QStringList({QStringLiteral("net.krunnertests.multi.a1")})); QVERIFY(process1.waitForStarted()); QProcess process2; process2.start(QFINDTESTDATA("testremoterunner"), QStringList({QStringLiteral("net.krunnertests.multi.a2")})); QVERIFY(process2.waitForStarted()); QTest::qSleep(500); RunnerManager m; auto s = KService::serviceByDesktopPath(QStringLiteral("dbusrunnertestmulti.desktop")); QVERIFY(s); m.loadRunner(s); m.launchQuery(QStringLiteral("foo")); QSignalSpy spy(&m, &RunnerManager::matchesChanged); QVERIFY(spy.wait()); //verify matches, must be one from each QCOMPARE(m.matches().count(), 2); QString first = m.matches().at(0).data().toString(); QString second = m.matches().at(1).data().toString(); QVERIFY(first != second); QVERIFY(first == QLatin1String("net.krunnertests.multi.a1") || first == QStringLiteral("net.krunnertests.multi.a2")); QVERIFY(second == QLatin1String("net.krunnertests.multi.a1") || second == QStringLiteral("net.krunnertests.multi.a2")); process1.kill(); process2.kill(); process1.waitForFinished(); process2.waitForFinished(); } +void DBusRunnerTest::testRequestActionsOnce() +{ + QProcess process; + process.start(QFINDTESTDATA("testremoterunner"), QStringList({QStringLiteral("net.krunnertests.dave")})); + QVERIFY(process.waitForStarted()); + QTest::qSleep(500); + RunnerManager m; + auto s = KService::serviceByDesktopPath(QStringLiteral("dbusrunnertest.desktop")); + QVERIFY(s); + m.loadRunner(s); + + // Wait because dbus signal is async + QEventLoop loop; + QTimer t; + QTimer::connect(&t, &QTimer::timeout, &loop, &QEventLoop::quit); + t.start(500); + loop.exec(); + + // Construct a fake match with necesarry data + QueryMatch fakeMatch(m.runner(QStringLiteral("dbusrunnertest"))); + fakeMatch.setId(QStringLiteral("dbusrunnertest_id1")); + fakeMatch.setData(QStringLiteral("net.krunnertests.dave")); + + // We haven't called the prepare slot, if the implementation works + // the actions should alredy be available + auto actions = m.actionsForMatch(fakeMatch); + QCOMPARE(actions.count(), 1); + + process.kill(); + process.waitForFinished(); +} QTEST_MAIN(DBusRunnerTest) #include "dbusrunnertest.moc" diff --git a/autotests/dbusrunnertest.desktop b/autotests/dbusrunnertest.desktop index f0c8671..a8b4164 100644 --- a/autotests/dbusrunnertest.desktop +++ b/autotests/dbusrunnertest.desktop @@ -1,15 +1,16 @@ [Desktop Entry] Name=DBus runner test Comment=DBus runner test X-KDE-ServiceTypes=Plasma/Runner Type=Service Icon=internet-web-browser X-KDE-PluginInfo-Author=Some Developer X-KDE-PluginInfo-Email=kde@example.com X-KDE-PluginInfo-Name=dbusrunnertest X-KDE-PluginInfo-Version=1.0 X-KDE-PluginInfo-License=LGPL X-KDE-PluginInfo-EnabledByDefault=true X-Plasma-API=DBus X-Plasma-DBusRunner-Service=net.krunnertests.dave X-Plasma-DBusRunner-Path=/dave +X-Plasma-Request-Actions-Once=true diff --git a/src/data/servicetypes/plasma-runner.desktop b/src/data/servicetypes/plasma-runner.desktop index bd34f04..661b62f 100644 --- a/src/data/servicetypes/plasma-runner.desktop +++ b/src/data/servicetypes/plasma-runner.desktop @@ -1,70 +1,75 @@ [Desktop Entry] Type=ServiceType X-KDE-ServiceType=Plasma/Runner Comment=KRunner plugin Comment[ar]=ملحقة «مشغّلك» Comment[bs]=Priključak za KRunner Comment[ca]=Connector del KRunner Comment[ca@valencia]=Connector del KRunner Comment[cs]=Modul KRunneru Comment[da]=KRunner-plugin Comment[de]=KRunner-Modul Comment[el]=Πρόσθετο KRunner Comment[en_GB]=KRunner plugin Comment[es]=Complemento para KRunner Comment[et]=KRunneri plugin Comment[eu]=KRunner plugina Comment[fi]=KRunner-liitännäinen Comment[fr]=Module externe de KRunner Comment[gd]=Plugan KRunner Comment[gl]=Complemento KRunner Comment[hu]=KRunner bővítmény Comment[ia]=Plugin de KRunner Comment[id]=Plugin KRunner Comment[it]=Estensione di KRunner Comment[ko]=KRunner 플러그인 Comment[lt]=KRunner papildinys Comment[mr]=KRunner प्लगइन Comment[nb]=KRunner-programtillegg Comment[nds]=KRunner-Moduul Comment[nl]=KRunner-plugin Comment[nn]=KRunner-tillegg Comment[pa]=ਕੇਰਨਰ ਪਲੱਗਇਨ Comment[pl]=Wtyczka KRunner Comment[pt]='Plugin' do KRunner Comment[pt_BR]=Plugin do KRunner Comment[ro]=Extensie KRunner Comment[ru]=Расширение KRunner Comment[sk]=Zásuvný modul pre KRunner Comment[sl]=Vstavek za KRunner Comment[sr]=Прикључак К‑извођача Comment[sr@ijekavian]=Прикључак К‑извођача Comment[sr@ijekavianlatin]=Priključak K‑izvođača Comment[sr@latin]=Priključak K‑izvođača Comment[sv]=Insticksprogram för Kör program Comment[tg]=Васлкунаки KRunner Comment[tr]=KRunner eklentisi Comment[ug]=KRunner قىستۇرما Comment[uk]=Додаток KRunner Comment[x-test]=xxKRunner pluginxx Comment[zh_CN]=KRunner 插件 Comment[zh_TW]=KRunner 外掛程式 [PropertyDef::X-Plasma-AdvertiseSingleRunnerQueryMode] Type=bool [PropertyDef::TryExec] Type=QString [PropertyDef::X-Plasma-Args] Type=QStringList [PropertyDef::X-Plasma-Api] Type=QString [PropertyDef::X-Plasma-DBusRunner-Service] Type=QString [PropertyDef::X-Plasma-DBusRunner-Path] Type=QString + +# Request actions only when the plugin is initialized instead +# of each match session, default is false +[PropertyDef::X-Plasma-Request-Actions-Once] +Type=bool diff --git a/src/dbusrunner.cpp b/src/dbusrunner.cpp index ebffa34..e1c1213 100644 --- a/src/dbusrunner.cpp +++ b/src/dbusrunner.cpp @@ -1,192 +1,195 @@ /* * Copyright (C) 2017,2018 David Edmundson * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Library General Public License version 2 as * published by the Free Software Foundation * * 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 "dbusrunner_p.h" #include #include #include #include #include #include #include #include #include "krunner_debug.h" #include "dbusutils_p.h" #define IFACE_NAME "org.kde.krunner1" DBusRunner::DBusRunner(const KService::Ptr service, QObject *parent) : Plasma::AbstractRunner(service, parent) , m_mutex(QMutex::NonRecursive) { qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); qDBusRegisterMetaType(); QString requestedServiceName = service->property(QStringLiteral("X-Plasma-DBusRunner-Service")).toString(); m_path = service->property(QStringLiteral("X-Plasma-DBusRunner-Path")).toString(); if (requestedServiceName.isEmpty() || m_path.isEmpty()) { qCWarning(KRUNNER) << "Invalid entry:" << service->name(); return; } if (requestedServiceName.endsWith(QLatin1Char('*'))) { requestedServiceName.chop(1); //find existing matching names auto namesReply = QDBusConnection::sessionBus().interface()->registeredServiceNames(); if (namesReply.isValid()) { const auto names = namesReply.value(); for (const QString& serviceName : names) { if (serviceName.startsWith(requestedServiceName)) { m_matchingServices << serviceName; } } } //and watch for changes connect(QDBusConnection::sessionBus().interface(), &QDBusConnectionInterface::serviceOwnerChanged, this, [this, requestedServiceName](const QString &serviceName, const QString &oldOwner, const QString &newOwner) { if (!serviceName.startsWith(requestedServiceName)) { return; } if (!oldOwner.isEmpty() && !newOwner.isEmpty()) { //changed owner, but service still exists. Don't need to adjust anything return; } QMutexLocker lock(&m_mutex); if (!newOwner.isEmpty()) { m_matchingServices.insert(serviceName); } if (!oldOwner.isEmpty()) { m_matchingServices.remove(serviceName); } }); } else { //don't check when not wildcarded, as it could be used with DBus-activation m_matchingServices << requestedServiceName; } - - connect(this, &AbstractRunner::prepare, this, &DBusRunner::requestActions); + if (service->property(QStringLiteral("X-Plasma-Request-Actions-Once")).toBool()) { + requestActions(); + } else { + connect(this, &AbstractRunner::prepare, this, &DBusRunner::requestActions); + } } DBusRunner::~DBusRunner() = default; void DBusRunner::requestActions() { clearActions(); m_actions.clear(); //in the multi-services case, register separate actions from each plugin in case they happen to be somehow different //then match together in matchForAction() for (const QString &service: qAsConst(m_matchingServices)) { auto getActionsMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Actions")); QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(getActionsMethod); auto watcher = new QDBusPendingCallWatcher(reply); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, service]() { watcher->deleteLater(); QDBusReply reply = *watcher; if (!reply.isValid()) { return; } const auto actions = reply.value(); for(const RemoteAction &action: actions) { auto a = addAction(action.id, QIcon::fromTheme(action.iconName), action.text); a->setData(action.id); m_actions[service].append(a); } }); } } void DBusRunner::match(Plasma::RunnerContext &context) { QSet services; { QMutexLocker lock(&m_mutex); services = m_matchingServices; } //we scope watchers to make sure the lambda that captures context by reference definitely gets disconnected when this function ends QList> watchers; for (const QString& service : qAsConst(services)) { auto matchMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Match")); matchMethod.setArguments(QList({context.query()})); QDBusPendingReply reply = QDBusConnection::sessionBus().asyncCall(matchMethod); auto watcher = new QDBusPendingCallWatcher(reply); watchers << QSharedPointer(watcher); connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, service, &context, reply]() { if (reply.isError()) { qCDebug(KRUNNER) << "Error calling" << service << " :" << reply.error().name() << reply.error().message(); return; } const auto matches = reply.value(); for(const RemoteMatch &match: matches) { Plasma::QueryMatch m(this); m.setText(match.text); m.setId(match.id); m.setData(service); m.setIconName(match.iconName); m.setType(match.type); m.setRelevance(match.relevance); //split is essential items are as native DBus types, optional extras are in the property map (which is obviously a lot slower to parse) m.setUrls(QUrl::fromStringList(match.properties.value(QStringLiteral("urls")).toStringList())); m.setMatchCategory(match.properties.value(QStringLiteral("category")).toString()); m.setSubtext(match.properties.value(QStringLiteral("subtext")).toString()); context.addMatch(m); }; }, Qt::DirectConnection); // process reply in the watcher's thread (aka the one running ::match not the one owning the runner) } //we're done matching when every service replies for (auto w : qAsConst(watchers)) { w->waitForFinished(); } } QList DBusRunner::actionsForMatch(const Plasma::QueryMatch &match) { Q_UNUSED(match) const QString service = match.data().toString(); return m_actions.value(service); } void DBusRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) { Q_UNUSED(context); QString actionId; QString matchId = match.id().mid(id().length() + 1); //QueryMatch::setId mangles the match ID with runnerID + '_'. This unmangles it QString service = match.data().toString(); if (match.selectedAction()) { actionId = match.selectedAction()->data().toString(); } auto runMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Run")); runMethod.setArguments(QList({matchId, actionId})); QDBusConnection::sessionBus().call(runMethod, QDBus::NoBlock); }