KRunner

dbusrunner.cpp
1 /*
2  SPDX-FileCopyrightText: 2017, 2018 David Edmundson <[email protected]>
3  SPDX-FileCopyrightText: 2020 Alexander Lohnau <[email protected]>
4  SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <[email protected]>
5 
6  SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "dbusrunner_p.h"
10 
11 #include <QAction>
12 #include <QDBusConnection>
13 #include <QDBusConnectionInterface>
14 #include <QDBusMessage>
15 #include <QDBusMetaType>
16 #include <QDBusPendingReply>
17 #include <QIcon>
18 #include <QMutexLocker>
19 #include <qobjectdefs.h>
20 
21 #include "dbusutils_p.h"
22 #include "krunner_debug.h"
23 
24 #define IFACE_NAME "org.kde.krunner1"
25 
26 DBusRunner::DBusRunner(QObject *parent, const KPluginMetaData &pluginMetaData, const QVariantList &args)
27  : Plasma::AbstractRunner(parent, pluginMetaData, args)
28 {
29  qDBusRegisterMetaType<RemoteMatch>();
30  qDBusRegisterMetaType<RemoteMatches>();
31  qDBusRegisterMetaType<RemoteAction>();
32  qDBusRegisterMetaType<RemoteActions>();
33  qDBusRegisterMetaType<RemoteImage>();
34  qRegisterMetaType<QMap<QString, RemoteActions>>("QMap<QString, RemoteActions>");
35 
36  QString requestedServiceName = pluginMetaData.value(QStringLiteral("X-Plasma-DBusRunner-Service"));
37  m_path = pluginMetaData.value(QStringLiteral("X-Plasma-DBusRunner-Path"));
38  m_hasUniqueResults = pluginMetaData.value(QStringLiteral("X-Plasma-Runner-Unique-Results"), false);
39  m_callLifecycleMethods = pluginMetaData.value(QStringLiteral("X-Plasma-API")) == QLatin1String("DBus2");
40 
41  if (requestedServiceName.isEmpty() || m_path.isEmpty()) {
42  qCWarning(KRUNNER) << "Invalid entry:" << pluginMetaData.name();
43  return;
44  }
45 
46  if (requestedServiceName.endsWith(QLatin1Char('*'))) {
47  requestedServiceName.chop(1);
48  // find existing matching names
50  if (namesReply.isValid()) {
51  const auto names = namesReply.value();
52  for (const QString &serviceName : names) {
53  if (serviceName.startsWith(requestedServiceName)) {
54  m_matchingServices << serviceName;
55  }
56  }
57  }
58  // and watch for changes
61  this,
62  [this, requestedServiceName](const QString &serviceName, const QString &oldOwner, const QString &newOwner) {
63  if (!serviceName.startsWith(requestedServiceName)) {
64  return;
65  }
66  if (!oldOwner.isEmpty() && !newOwner.isEmpty()) {
67  // changed owner, but service still exists. Don't need to adjust anything
68  return;
69  }
70  QMutexLocker lock(&m_mutex);
71  if (!newOwner.isEmpty()) {
72  m_matchingServices.insert(serviceName);
73  }
74  if (!oldOwner.isEmpty()) {
75  m_matchingServices.remove(serviceName);
76  }
77  });
78  } else {
79  // don't check when not wildcarded, as it could be used with DBus-activation
80  m_matchingServices << requestedServiceName;
81  }
82 
83  m_requestActionsOnce = pluginMetaData.value(QStringLiteral("X-Plasma-Request-Actions-Once"), false);
84  connect(this, &AbstractRunner::teardown, this, &DBusRunner::teardown);
85 
86  // Load the runner syntaxes
87  const QJsonValue syntaxesJson = pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Runner-Syntaxes"));
88  const QStringList syntaxes = syntaxesJson.isArray() ? syntaxesJson.toVariant().toStringList() : syntaxesJson.toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
89  const QJsonValue syntaxDescriptionsJson = pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Runner-Syntax-Descriptions"));
90  const QStringList syntaxDescriptions =
91  syntaxDescriptionsJson.isArray() ? syntaxDescriptionsJson.toVariant().toStringList() : syntaxDescriptionsJson.toString().split(QLatin1Char(','), Qt::SkipEmptyParts);
92  const int descriptionCount = syntaxDescriptions.count();
93  for (int i = 0; i < syntaxes.count(); ++i) {
94  const QString &query = syntaxes.at(i);
95  const QString description = i < descriptionCount ? syntaxDescriptions.at(i) : QString();
96  addSyntax(query, description);
97  }
98 }
99 
100 DBusRunner::~DBusRunner() = default;
101 
102 void DBusRunner::reloadConfiguration()
103 {
104  // If we have already loaded a config, but the runner is told to reload it's config
105  if (m_callLifecycleMethods) {
106  suspendMatching(true);
107  requestConfig();
108  }
109 }
110 
111 void DBusRunner::createQActionsFromRemoteActions(const QMap<QString, RemoteActions> &remoteActions)
112 {
113  for (auto it = remoteActions.begin(), end = remoteActions.end(); it != end; it++) {
114  const QString service = it.key();
115  const RemoteActions actions = it.value();
116  auto &serviceActions = m_actions[service];
117  qDeleteAll(serviceActions);
118  serviceActions.clear();
119  for (const RemoteAction &action : actions) {
120  auto a = new QAction(QIcon::fromTheme(action.iconName), action.text, this);
121  a->setData(action.id);
122  serviceActions.append(a);
123  }
124  }
125 }
126 
127 void DBusRunner::teardown()
128 {
129  if (m_matchWasCalled) {
130  for (const QString &service : std::as_const(m_matchingServices)) {
131  auto method = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Teardown"));
133  }
134  }
135  m_actionsForSessionRequested = false;
136  m_matchWasCalled = false;
137 }
138 
139 QMap<QString, RemoteActions> DBusRunner::requestActions()
140 {
141  // in the multi-services case, register separate actions from each plugin in case they happen to be somehow different
142  // then match together in matchForAction()
143  QMap<QString, RemoteActions> returnedActions;
144  for (const QString &service : std::as_const(m_matchingServices)) {
145  // if we only want to request the actions once and have done so we want to skip the service
146  // but in case it got newly loaded we need to request the actions, BUG: 435350
147  if (m_requestActionsOnce) {
148  if (m_requestedActionServices.contains(service)) {
149  continue;
150  } else {
151  m_requestedActionServices << service;
152  }
153  }
154 
155  auto getActionsMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Actions"));
157  if (!reply.isValid()) {
158  qCDebug(KRUNNER) << "Error requesting actions; calling" << service << " :" << reply.error().name() << reply.error().message();
159  } else {
160  returnedActions.insert(service, reply.value());
161  }
162  }
163  return returnedActions;
164 }
165 
166 void DBusRunner::requestConfig()
167 {
168  const QString service = *m_matchingServices.constBegin();
169  auto getConfigMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Config"));
171 
172  auto watcher = new QDBusPendingCallWatcher(reply);
173  connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, service]() {
174  watcher->deleteLater();
175  QDBusReply<QVariantMap> reply = *watcher;
176  if (!reply.isValid()) {
177  suspendMatching(false);
178  qCDebug(KRUNNER) << "Error requesting config; calling" << service << " :" << reply.error().name() << reply.error().message();
179  return;
180  }
181  const QVariantMap config = reply.value();
182  for (auto it = config.cbegin(), end = config.cend(); it != end; ++it) {
183  if (it.key() == QLatin1String("MatchRegex")) {
184  QRegularExpression regex(it.value().toString());
185  setMatchRegex(regex);
186  } else if (it.key() == QLatin1String("MinLetterCount")) {
187  setMinLetterCount(it.value().toInt());
188  } else if (it.key() == QLatin1String("TriggerWords")) {
189  setTriggerWords(it.value().toStringList());
190  } else if (it.key() == QLatin1String("Actions")) {
191  const auto remoteActions = it.value().value<RemoteActions>();
192  createQActionsFromRemoteActions(QMap<QString, RemoteActions>{{service, remoteActions}});
193  m_actionsOnceRequested = true;
194  m_actionsForSessionRequested = true;
195  }
196  }
197  suspendMatching(false);
198  });
199 }
200 
201 void DBusRunner::match(Plasma::RunnerContext &context)
202 {
203  QSet<QString> services;
204  {
205  QMutexLocker lock(&m_mutex);
206  services = m_matchingServices;
207  m_matchWasCalled = true;
208 
209  // Request the actions
210  if ((m_requestActionsOnce && !m_actionsOnceRequested) // We only want to fetch the actions once but haven't done so yet
211  || (!m_actionsForSessionRequested)) { // We want to fetch the actions for each match session
212  m_actionsOnceRequested = true;
213  m_actionsForSessionRequested = true;
214  auto actions = requestActions();
215  QMetaObject::invokeMethod(this, "createQActionsFromRemoteActions", QArgument("QMap<QString, RemoteActions>", actions));
216  }
217  }
218  // we scope watchers to make sure the lambda that captures context by reference definitely gets disconnected when this function ends
219  std::vector<std::unique_ptr<QDBusPendingCallWatcher>> watchers;
220 
221  for (const QString &service : std::as_const(services)) {
222  auto matchMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Match"));
223  matchMethod.setArguments(QList<QVariant>({context.query()}));
225 
226  watchers.push_back(std::make_unique<QDBusPendingCallWatcher>(reply));
227  connect(
228  watchers.back().get(),
230  this,
231  [this, service, &context, reply]() {
232  if (reply.isError()) {
233  qCDebug(KRUNNER) << "Error requesting matches; calling" << service << " :" << reply.error().name() << reply.error().message();
234  return;
235  }
236  const auto matches = reply.value();
237  for (const RemoteMatch &match : matches) {
238  Plasma::QueryMatch m(this);
239 
240  m.setText(match.text);
241  m.setIconName(match.iconName);
242  m.setType(match.type);
243  m.setRelevance(match.relevance);
244 
245  // split is essential items are as native DBus types, optional extras are in the property map (which is obviously a lot slower to parse)
246  m.setUrls(QUrl::fromStringList(match.properties.value(QStringLiteral("urls")).toStringList()));
247  m.setMatchCategory(match.properties.value(QStringLiteral("category")).toString());
248  m.setSubtext(match.properties.value(QStringLiteral("subtext")).toString());
249  const auto actionsIt = match.properties.find(QStringLiteral("actions"));
250  if (actionsIt == match.properties.cend()) {
251  m.setData(QVariantList({service}));
252  } else {
253  m.setData(QVariantList({service, actionsIt.value().toStringList()}));
254  }
255  m.setId(match.id);
256  m.setMultiLine(match.properties.value(QStringLiteral("multiline")).toBool());
257 
258  const QVariant iconData = match.properties.value(QStringLiteral("icon-data"));
259  if (iconData.isValid()) {
260  const auto iconDataArgument = iconData.value<QDBusArgument>();
261  if (iconDataArgument.currentType() == QDBusArgument::StructureType
262  && iconDataArgument.currentSignature() == QLatin1String("(iiibiiay)")) {
263  const RemoteImage remoteImage = qdbus_cast<RemoteImage>(iconDataArgument);
264  const QImage decodedImage = decodeImage(remoteImage);
265  if (!decodedImage.isNull()) {
266  const QPixmap pix = QPixmap::fromImage(decodedImage);
267  QIcon icon(pix);
268  m.setIcon(icon);
269  // iconName normally takes precedence
270  m.setIconName(QString());
271  }
272  } else {
273  qCWarning(KRUNNER) << "Invalid signature of icon-data property:" << iconDataArgument.currentSignature();
274  }
275  }
276 
277  context.addMatch(m);
278  };
279  },
280  Qt::DirectConnection); // process reply in the watcher's thread (aka the one running ::match not the one owning the runner)
281  }
282  // we're done matching when every service replies
283  for (auto &w : watchers) {
284  w->waitForFinished();
285  }
286 }
287 
288 QList<QAction *> DBusRunner::actionsForMatch(const Plasma::QueryMatch &match)
289 {
290  const QVariantList data = match.data().toList();
291  if (data.count() > 1) {
292  const QStringList actionIds = data.at(1).toStringList();
293  const QList<QAction *> actionList = m_actions.value(data.constFirst().toString());
294  QList<QAction *> requestedActions;
295  for (QAction *action : actionList) {
296  if (actionIds.contains(action->data().toString())) {
297  requestedActions << action;
298  }
299  }
300  return requestedActions;
301  } else {
302  return m_actions.value(data.constFirst().toString());
303  }
304 }
305 
306 void DBusRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match)
307 {
308  Q_UNUSED(context);
309 
310  QString actionId;
311  QString matchId;
312  if (m_hasUniqueResults) {
313  matchId = match.id();
314  } else {
315  matchId = match.id().mid(id().length() + 1); // QueryMatch::setId mangles the match ID with runnerID + '_'. This unmangles it
316  }
317  const QString service = match.data().toList().constFirst().toString();
318 
319  if (match.selectedAction()) {
320  actionId = match.selectedAction()->data().toString();
321  }
322 
323  auto runMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Run"));
324  runMethod.setArguments(QList<QVariant>({matchId, actionId}));
326 }
327 
328 QImage DBusRunner::decodeImage(const RemoteImage &remoteImage)
329 {
330  auto copyLineRGB32 = [](QRgb *dst, const char *src, int width) {
331  const char *end = src + width * 3;
332  for (; src != end; ++dst, src += 3) {
333  *dst = qRgb(src[0], src[1], src[2]);
334  }
335  };
336 
337  auto copyLineARGB32 = [](QRgb *dst, const char *src, int width) {
338  const char *end = src + width * 4;
339  for (; src != end; ++dst, src += 4) {
340  *dst = qRgba(src[0], src[1], src[2], src[3]);
341  }
342  };
343 
344  if (remoteImage.width <= 0 || remoteImage.width >= 2048 || remoteImage.height <= 0 || remoteImage.height >= 2048 || remoteImage.rowStride <= 0) {
345  qCWarning(KRUNNER) << "Invalid image metadata (width:" << remoteImage.width << "height:" << remoteImage.height << "rowStride:" << remoteImage.rowStride
346  << ")";
347  return QImage();
348  }
349 
351  void (*copyFn)(QRgb *, const char *, int) = nullptr;
352  if (remoteImage.bitsPerSample == 8) {
353  if (remoteImage.channels == 4) {
354  format = QImage::Format_ARGB32;
355  copyFn = copyLineARGB32;
356  } else if (remoteImage.channels == 3) {
357  format = QImage::Format_RGB32;
358  copyFn = copyLineRGB32;
359  }
360  }
361  if (format == QImage::Format_Invalid) {
362  qCWarning(KRUNNER) << "Unsupported image format (hasAlpha:" << remoteImage.hasAlpha << "bitsPerSample:" << remoteImage.bitsPerSample
363  << "channels:" << remoteImage.channels << ")";
364  return QImage();
365  }
366 
367  QImage image(remoteImage.width, remoteImage.height, format);
368  const QByteArray pixels = remoteImage.data;
369  const char *ptr = pixels.data();
370  const char *end = ptr + pixels.length();
371  for (int y = 0; y < remoteImage.height; ++y, ptr += remoteImage.rowStride) {
372  if (Q_UNLIKELY(ptr + remoteImage.channels * remoteImage.width > end)) {
373  qCWarning(KRUNNER) << "Image data is incomplete. y:" << y << "height:" << remoteImage.height;
374  break;
375  }
376  copyFn(reinterpret_cast<QRgb *>(image.scanLine(y)), ptr, remoteImage.width);
377  }
378 
379  return image;
380 }
381 
382 #include "moc_dbusrunner_p.cpp"
bool isValid() const const
QJsonObject rawData() const
void finished(QDBusPendingCallWatcher *self)
std::optional< QSqlQuery > query(const QString &queryStatement)
bool isValid() const const
QPixmap fromImage(const QImage &image, Qt::ImageConversionFlags flags)
const T value(const Key &key, const T &defaultValue) const const
QString message() const const
QStringList split(const QString &sep, QString::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
int count(const T &value) const const
T value() const const
bool isValid() const const
QMap::iterator begin()
A match returned by an AbstractRunner in response to a given RunnerContext.
Definition: querymatch.h:34
QIcon fromTheme(const QString &name)
QDBusMessage call(const QDBusMessage &message, QDBus::CallMode mode, int timeout) const const
void chop(int n)
QString toString() const const
void teardown()
This signal is emitted when a session of matches is complete, giving runners the opportunity to tear ...
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QList< QUrl > fromStringList(const QStringList &urls, QUrl::ParsingMode mode)
QMap::iterator insert(const Key &key, const T &value)
QMap::iterator end()
void addSyntax(const RunnerSyntax &syntax)
Adds a registered syntax that this runner understands.
QString::const_iterator constBegin() const const
QDBusConnection sessionBus()
SkipEmptyParts
QString name() const
bool isEmpty() const const
bool isNull() const const
const T & at(int i) const const
DirectConnection
void serviceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner)
QJsonValue value(const QString &key) const const
QVariant toVariant() const const
T1 value() const const
KSharedConfigPtr config()
QDBusMessage createMethodCall(const QString &service, const QString &path, const QString &interface, const QString &method)
QDBusError error() const const
QDBusConnectionInterface * interface() const const
bool value(const QString &key, bool defaultValue) const
bool startsWith(const QString &s, Qt::CaseSensitivity cs) const const
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType type, QGenericReturnArgument ret, QGenericArgument val0, QGenericArgument val1, QGenericArgument val2, QGenericArgument val3, QGenericArgument val4, QGenericArgument val5, QGenericArgument val6, QGenericArgument val7, QGenericArgument val8, QGenericArgument val9)
The RunnerContext class provides information related to a search, including the search term,...
Definition: runnercontext.h:31
QString query() const
QStringList toStringList() const const
bool addMatch(const QueryMatch &match)
Appends a match to the existing list of matches.
int length() const const
bool isArray() const const
QString name() const const
QList< RunnerSyntax > syntaxes() const
QDBusPendingCall asyncCall(const QDBusMessage &message, int timeout) const const
QList< StandardAction > actionIds()
const QList< QKeySequence > & end()
T value(int i) const const
QDBusReply::Type value() const const
char * data()
char * toString(const EngineQuery &query)
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Sat Dec 2 2023 03:50:59 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.