Verified Commit f26c5a79 authored by Alexander Lohnau's avatar Alexander Lohnau 💬
Browse files

dbusrunner: Add teardown method to D-Bus interface

This adds the prepare and teardown lifecycle
methods to the interface. This aligns with the
goal of making DBus runners first class citizens.
parent 8e246cc2
......@@ -49,6 +49,8 @@ private Q_SLOTS:
void testRequestActionsOnce();
void testDBusRunnerSyntaxIntegration();
void testIconData();
void testLifecycleMethods();
void testRequestActionsOnceWildcards();
#if WITH_KSERVICE
void testMulti_data();
#endif
......@@ -170,8 +172,9 @@ void DBusRunnerTest::testRequestActionsOnce()
fakeMatch.setId(QStringLiteral("dbusrunnertest_id1"));
fakeMatch.setData(QVariantList({QStringLiteral("net.krunnertests.dave"), QStringList({QStringLiteral("action1"), QStringLiteral("action2")})}));
// We haven't called the prepare slot or launched a query, if the implementation works
// the actions should already be available
// The actions should not be fetched before we have set up the match session
QCOMPARE(manager->actionsForMatch(fakeMatch).count(), 0);
launchQuery(QStringLiteral("foo"));
// We need to retry this, because the DBus call to fetch the actions is async
QTRY_COMPARE_WITH_TIMEOUT(manager->actionsForMatch(fakeMatch).count(), 2, 2500);
}
......@@ -235,6 +238,62 @@ void DBusRunnerTest::testIconData()
QCOMPARE(result.icon().pixmap(QSize(10, 10)), QPixmap::fromImage(expectedIcon));
}
void DBusRunnerTest::testLifecycleMethods()
{
QProcess *process = startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave"), QString()});
manager.reset(new RunnerManager()); // This case is special, because we want to load the runners manually
auto md = KPluginMetaData::fromDesktopFile(QFINDTESTDATA("dbusrunnertestruntimeconfig.desktop"), {QStringLiteral("plasma-runner.desktop")});
manager->loadRunner(md);
QCOMPARE(manager->runners().count(), 1);
// Match session should be set up automatically
launchQuery(QStringLiteral("fooo"));
// Make sure we got our match, end the match session and give the process a bit of time to get the DBus signal
QTRY_COMPARE_WITH_TIMEOUT(manager->matches().count(), 1, 2000);
manager->matchSessionComplete();
QTest::qWait(500);
const QStringList lifeCycleSteps = QString::fromLocal8Bit(process->readAllStandardOutput()).split(QLatin1Char('\n'), Qt::SkipEmptyParts);
const QStringList expectedLifeCycleSteps = {
QStringLiteral("Config"),
QStringLiteral("Matching:fooo"),
QStringLiteral("Teardown"),
};
QCOMPARE(lifeCycleSteps, expectedLifeCycleSteps);
// The query does not match our min letter count we set at runtime
launchQuery(QStringLiteral("foo"));
QVERIFY(manager->matches().isEmpty());
// The query does not match our match regex we set at runtime
launchQuery(QStringLiteral("barfoo"));
QVERIFY(manager->matches().isEmpty());
}
void DBusRunnerTest::testRequestActionsOnceWildcards()
{
initProperties();
manager.reset(new RunnerManager()); // This case is special, because we want to load the runners manually
auto md = KPluginMetaData::fromDesktopFile(QFINDTESTDATA("dbusrunnertestmulti.desktop"), {QStringLiteral("plasma-runner.desktop")});
QVERIFY(md.isValid());
manager->loadRunner(md);
QCOMPARE(manager->runners().count(), 1);
launchQuery("foo");
QVERIFY(manager->matches().isEmpty());
QueryMatch match(manager->runners().constFirst());
match.setId("test");
match.setData(QStringList{"net.krunnertests.multi.a1"});
QVERIFY(manager->actionsForMatch(match).isEmpty());
manager->matchSessionComplete();
// We have started the process later and the actions should now be fetched when the match session is started
startDBusRunnerProcess({QStringLiteral("net.krunnertests.multi.a1")}, QStringLiteral("net.krunnertests.multi.a1"));
QTest::qWait(500); // Wait a bit for the runner to pick up the new service
launchQuery("fooo");
QVERIFY(!manager->matches().isEmpty());
QVERIFY(!manager->actionsForMatch(match).isEmpty());
}
QTEST_MAIN(DBusRunnerTest)
#include "dbusrunnertest.moc"
......@@ -17,7 +17,7 @@
// Test DBus runner, if the search term contains "foo" it returns a match, otherwise nothing
// Run prints a line to stdout
TestRemoteRunner::TestRemoteRunner(const QString &serviceName)
TestRemoteRunner::TestRemoteRunner(const QString &serviceName, bool showLifecycleMethodCalls)
{
new Krunner1Adaptor(this);
qDBusRegisterMetaType<RemoteMatch>();
......@@ -27,6 +27,7 @@ TestRemoteRunner::TestRemoteRunner(const QString &serviceName)
qDBusRegisterMetaType<RemoteImage>();
Q_ASSERT(QDBusConnection::sessionBus().registerService(serviceName));
Q_ASSERT(QDBusConnection::sessionBus().registerObject(QStringLiteral("/dave"), this));
m_showLifecycleMethodCalls = showLifecycleMethodCalls;
}
static RemoteImage serializeImage(const QImage &image)
......@@ -90,11 +91,32 @@ void TestRemoteRunner::Run(const QString &id, const QString &actionId)
std::cout.flush();
}
void TestRemoteRunner::Teardown()
{
if (m_showLifecycleMethodCalls) {
std::cout << "Teardown" << std::endl;
std::cout.flush();
}
}
QVariantMap TestRemoteRunner::Config()
{
if (m_showLifecycleMethodCalls) {
std::cout << "Config" << std::endl;
std::cout.flush();
}
return {
{"X-Plasma-Runner-Match-Regex", "^fo"},
{"X-Plasma-Runner-Min-Letter-Count", 4},
};
}
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
const auto arguments = app.arguments();
Q_ASSERT(arguments.count() == 2);
TestRemoteRunner r(arguments[1]);
Q_ASSERT(arguments.count() >= 2);
TestRemoteRunner r(arguments[1], arguments.count() == 3);
app.exec();
}
......@@ -2,15 +2,21 @@
#include "../src/dbusutils_p.h"
#include <QObject>
#include <QVariantMap>
class TestRemoteRunner : public QObject
{
Q_OBJECT
public:
TestRemoteRunner(const QString &serviceName);
TestRemoteRunner(const QString &serviceName, bool showLifecycleMethodCalls);
public Q_SLOTS:
RemoteActions Actions();
RemoteMatches Match(const QString &searchTerm);
void Run(const QString &id, const QString &actionId);
void Teardown();
QVariantMap Config();
private:
bool m_showLifecycleMethodCalls = false;
};
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.kde.krunner1">
<!--
This method gets called when a match session is over.
It can be used to clear data which should not be kept in memory after a match session.
-->
<method name="Teardown"/>
<!--
This method can be used to set runner config at runtime. In case the service wildcard is used
the config is only for one service requested.
It gets only called when the X-Plasma-Runner-Lifecycle-Methods method is explicitly set to true.
This method is called before Prepare and before the matching is started.
-->
<method name="Config">
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap" />
<arg name="config" type="{sv}" direction="out">
</arg>
</method>
<!--
Returns a list of actions supported by this runner.
......
......@@ -16,6 +16,7 @@
#include <QDBusPendingReply>
#include <QIcon>
#include <QMutexLocker>
#include <qobjectdefs.h>
#include "dbusutils_p.h"
#include "krunner_debug.h"
......@@ -31,10 +32,12 @@ DBusRunner::DBusRunner(const KPluginMetaData &pluginMetaData, QObject *parent)
qDBusRegisterMetaType<RemoteAction>();
qDBusRegisterMetaType<RemoteActions>();
qDBusRegisterMetaType<RemoteImage>();
qRegisterMetaType<QMap<QString, RemoteActions>>();
QString requestedServiceName = pluginMetaData.value(QStringLiteral("X-Plasma-DBusRunner-Service"));
m_path = pluginMetaData.value(QStringLiteral("X-Plasma-DBusRunner-Path"));
m_hasUniqueResults = pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Runner-Unique-Results")).toBool();
m_callLifecycleMethods = pluginMetaData.value(QStringLiteral("X-Plasma-API")) == QLatin1String("DBus2");
if (requestedServiceName.isEmpty() || m_path.isEmpty()) {
qCWarning(KRUNNER) << "Invalid entry:" << pluginMetaData.name();
......@@ -77,11 +80,9 @@ DBusRunner::DBusRunner(const KPluginMetaData &pluginMetaData, QObject *parent)
// don't check when not wildcarded, as it could be used with DBus-activation
m_matchingServices << requestedServiceName;
}
if (pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Request-Actions-Once")).toVariant().toBool()) {
requestActions();
} else {
connect(this, &AbstractRunner::prepare, this, &DBusRunner::requestActions);
}
m_requestActionsOnce = pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Request-Actions-Once")).toVariant().toBool();
connect(this, &AbstractRunner::teardown, this, &DBusRunner::teardown);
// Load the runner syntaxes
const QStringList syntaxes = pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Runner-Syntaxes")).toVariant().toStringList();
......@@ -96,34 +97,101 @@ DBusRunner::DBusRunner(const KPluginMetaData &pluginMetaData, QObject *parent)
DBusRunner::~DBusRunner() = default;
void DBusRunner::requestActions()
void DBusRunner::reloadConfiguration()
{
// If we have already loaded a config, but the runner is told to reload it's config
if (m_callLifecycleMethods) {
suspendMatching(true);
requestConfig();
}
}
void DBusRunner::createQActionsFromRemoteActions(const QMap<QString, RemoteActions> remoteActions)
{
clearActions();
m_actions.clear();
for (auto it = remoteActions.begin(), end = remoteActions.end(); it != end; it++) {
const QString service = it.key();
const RemoteActions actions = it.value();
qDeleteAll(m_actions[service]);
m_actions[service].clear();
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::teardown()
{
if (m_callLifecycleMethods && m_matchWasCalled) {
for (const QString &service : qAsConst(m_matchingServices)) {
auto method = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Teardown"));
QDBusConnection::sessionBus().asyncCall(method);
}
}
m_actionsForSessionRequested = false;
m_matchWasCalled = false;
}
QMap<QString, RemoteActions> DBusRunner::requestActions()
{
// in the multi-services case, register separate actions from each plugin in case they happen to be somehow different
// then match together in matchForAction()
QMap<QString, RemoteActions> returnedActions;
for (const QString &service : qAsConst(m_matchingServices)) {
// if we only want to request the actions once and have done so we want to skip the service
// but in case it got newly loaded we need to request the actions, BUG: 435350
if (m_requestActionsOnce) {
if (m_requestedActionServices.contains(service)) {
continue;
} else {
m_requestedActionServices << service;
}
}
auto getActionsMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Actions"));
QDBusPendingReply<RemoteActions> reply = QDBusConnection::sessionBus().asyncCall(getActionsMethod);
QDBusPendingReply<RemoteActions> reply = QDBusConnection::sessionBus().call(getActionsMethod);
if (!reply.isValid()) {
qCDebug(KRUNNER) << "Error requesting actions; calling" << service << " :" << reply.error().name() << reply.error().message();
} else {
returnedActions.insert(service, reply.value());
}
}
return returnedActions;
}
auto watcher = new QDBusPendingCallWatcher(reply);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, service]() {
watcher->deleteLater();
QDBusReply<RemoteActions> reply = *watcher;
if (!reply.isValid()) {
qCDebug(KRUNNER) << "Error requestion actions; calling" << service << " :" << reply.error().name() << reply.error().message();
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::requestConfig()
{
const QString service = *m_matchingServices.constBegin();
auto getConfigMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Config"));
QDBusPendingReply<QVariantMap> reply = QDBusConnection::sessionBus().asyncCall(getConfigMethod);
auto watcher = new QDBusPendingCallWatcher(reply);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, service]() {
watcher->deleteLater();
QDBusReply<QVariantMap> reply = *watcher;
if (!reply.isValid()) {
suspendMatching(false);
qCDebug(KRUNNER) << "Error requesting config; calling" << service << " :" << reply.error().name() << reply.error().message();
return;
}
const QVariantMap config = reply.value();
for (auto it = config.cbegin(), end = config.cend(); it != end; ++it) {
if (it.key() == QLatin1String("X-Plasma-Runner-Match-Regex")) {
QRegularExpression regex(it.value().toString());
regex.optimize();
setMatchRegex(regex);
} else if (it.key() == QLatin1String("X-Plasma-Runner-Min-Letter-Count")) {
setMinLetterCount(it.value().toInt());
} else if (it.key() == QLatin1String("Actions")) {
const auto remoteActions = it.value().value<RemoteActions>();
createQActionsFromRemoteActions(QMap<QString, RemoteActions>{{service, remoteActions}});
m_actionsOnceRequested = true;
m_actionsForSessionRequested = true;
}
});
}
}
suspendMatching(false);
});
}
void DBusRunner::match(Plasma::RunnerContext &context)
......@@ -132,6 +200,17 @@ void DBusRunner::match(Plasma::RunnerContext &context)
{
QMutexLocker lock(&m_mutex);
services = m_matchingServices;
m_matchWasCalled = true;
// Request the actions
if ((m_requestActionsOnce && !m_actionsOnceRequested) // We only want to fetch the actions once but haven't done so yet
|| (!m_actionsForSessionRequested)) { // We want to fetch the actions for each match session
m_actionsOnceRequested = true;
m_actionsForSessionRequested = true;
auto actions = requestActions();
const auto actionsArg = QArgument<QMap<QString, RemoteActions>>("QMap<QString, RemoteActions>", actions);
QMetaObject::invokeMethod(this, "createQActionsFromRemoteActions", actionsArg);
}
}
// we scope watchers to make sure the lambda that captures context by reference definitely gets disconnected when this function ends
QList<QSharedPointer<QDBusPendingCallWatcher>> watchers;
......
......@@ -24,16 +24,29 @@ public:
~DBusRunner() override;
void match(Plasma::RunnerContext &context) override;
void reloadConfiguration() override;
void run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &action) override;
QList<QAction *> actionsForMatch(const Plasma::QueryMatch &match) override;
public Q_SLOTS:
void teardown();
// This method should only be called from the main thread!
void createQActionsFromRemoteActions(const QMap<QString, RemoteActions> remoteActions);
private:
void requestActions();
void setActions(const RemoteActions &remoteActions);
// Returns RemoteActions with service name as key
QMap<QString, RemoteActions> requestActions();
void requestConfig();
static QImage decodeImage(const RemoteImage &remoteImage);
QMutex m_mutex; // needed round any variable also accessed from Match
QString m_path;
QSet<QString> m_matchingServices;
QHash<QString, QList<QAction *>> m_actions;
bool m_hasUniqueResults = false;
bool m_requestActionsOnce = false;
bool m_actionsOnceRequested = false;
bool m_actionsForSessionRequested = false;
bool m_matchWasCalled = false;
bool m_callLifecycleMethods = false;
QSet<QString> m_requestedActionServices;
};
......@@ -294,7 +294,7 @@ public:
qCWarning(KRUNNER).nospace() << "Could not load runner " << pluginMetaData.name() << ":" << pluginLoader.errorString()
<< " (library path was:" << pluginMetaData.fileName() << ")";
}
} else if (api == QLatin1String("DBus")) {
} else if (api.startsWith(QLatin1String("DBus"))) {
runner = new DBusRunner(pluginMetaData, q);
} else {
runner = new AbstractRunner(pluginMetaData, q);
......@@ -379,12 +379,16 @@ public:
void runnerMatchingSuspended(bool suspended)
{
if (suspended || !prepped || teardownRequested) {
auto *runner = qobject_cast<AbstractRunner *>(q->sender());
if (suspended || !prepped || teardownRequested || !runner) {
return;
}
if (auto *runner = qobject_cast<AbstractRunner *>(q->sender())) {
startJob(runner);
const QString query = context.query();
if (singleMode || runner->minLetterCount() <= query.size()) {
if (singleMode || !runner->hasMatchRegex() || runner->matchRegex().match(query).hasMatch()) {
startJob(runner);
}
}
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment