File indexing completed on 2024-04-28 15:29:43
0001 /* 0002 SPDX-FileCopyrightText: 2017, 2018 David Edmundson <davidedmundson@kde.org> 0003 SPDX-FileCopyrightText: 2020 Alexander Lohnau <alexander.lohnau@gmx.de> 0004 SPDX-FileCopyrightText: 2020 Kai Uwe Broulik <kde@broulik.de> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "dbusrunner_p.h" 0010 0011 #include <QAction> 0012 #include <QDBusConnection> 0013 #include <QDBusConnectionInterface> 0014 #include <QDBusMessage> 0015 #include <QDBusMetaType> 0016 #include <QDBusPendingReply> 0017 #include <QIcon> 0018 #include <QMutexLocker> 0019 #include <qobjectdefs.h> 0020 0021 #include "dbusutils_p.h" 0022 #include "krunner_debug.h" 0023 0024 #define IFACE_NAME "org.kde.krunner1" 0025 0026 DBusRunner::DBusRunner(QObject *parent, const KPluginMetaData &pluginMetaData, const QVariantList &args) 0027 : Plasma::AbstractRunner(parent, pluginMetaData, args) 0028 { 0029 qDBusRegisterMetaType<RemoteMatch>(); 0030 qDBusRegisterMetaType<RemoteMatches>(); 0031 qDBusRegisterMetaType<RemoteAction>(); 0032 qDBusRegisterMetaType<RemoteActions>(); 0033 qDBusRegisterMetaType<RemoteImage>(); 0034 qRegisterMetaType<QMap<QString, RemoteActions>>("QMap<QString, RemoteActions>"); 0035 0036 QString requestedServiceName = pluginMetaData.value(QStringLiteral("X-Plasma-DBusRunner-Service")); 0037 m_path = pluginMetaData.value(QStringLiteral("X-Plasma-DBusRunner-Path"), QStringLiteral("/runner")); 0038 m_hasUniqueResults = pluginMetaData.value(QStringLiteral("X-Plasma-Runner-Unique-Results"), false); 0039 m_callLifecycleMethods = pluginMetaData.value(QStringLiteral("X-Plasma-API")) == QLatin1String("DBus2"); 0040 0041 if (requestedServiceName.isEmpty() || m_path.isEmpty()) { 0042 qCWarning(KRUNNER) << "Invalid entry:" << pluginMetaData.name(); 0043 return; 0044 } 0045 0046 if (requestedServiceName.endsWith(QLatin1Char('*'))) { 0047 requestedServiceName.chop(1); 0048 // find existing matching names 0049 auto namesReply = QDBusConnection::sessionBus().interface()->registeredServiceNames(); 0050 if (namesReply.isValid()) { 0051 const auto names = namesReply.value(); 0052 for (const QString &serviceName : names) { 0053 if (serviceName.startsWith(requestedServiceName)) { 0054 m_matchingServices << serviceName; 0055 } 0056 } 0057 } 0058 // and watch for changes 0059 connect(QDBusConnection::sessionBus().interface(), 0060 &QDBusConnectionInterface::serviceOwnerChanged, 0061 this, 0062 [this, requestedServiceName](const QString &serviceName, const QString &oldOwner, const QString &newOwner) { 0063 if (!serviceName.startsWith(requestedServiceName)) { 0064 return; 0065 } 0066 if (!oldOwner.isEmpty() && !newOwner.isEmpty()) { 0067 // changed owner, but service still exists. Don't need to adjust anything 0068 return; 0069 } 0070 QMutexLocker lock(&m_mutex); 0071 if (!newOwner.isEmpty()) { 0072 m_matchingServices.insert(serviceName); 0073 } 0074 if (!oldOwner.isEmpty()) { 0075 m_matchingServices.remove(serviceName); 0076 } 0077 }); 0078 } else { 0079 // don't check when not wildcarded, as it could be used with DBus-activation 0080 m_matchingServices << requestedServiceName; 0081 } 0082 0083 m_requestActionsOnce = pluginMetaData.value(QStringLiteral("X-Plasma-Request-Actions-Once"), false); 0084 connect(this, &AbstractRunner::teardown, this, &DBusRunner::teardown); 0085 0086 // Load the runner syntaxes 0087 const QJsonValue syntaxesJson = pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Runner-Syntaxes")); 0088 const QStringList syntaxes = syntaxesJson.isArray() ? syntaxesJson.toVariant().toStringList() : syntaxesJson.toString().split(QLatin1Char(','), Qt::SkipEmptyParts); 0089 const QJsonValue syntaxDescriptionsJson = pluginMetaData.rawData().value(QStringLiteral("X-Plasma-Runner-Syntax-Descriptions")); 0090 const QStringList syntaxDescriptions = 0091 syntaxDescriptionsJson.isArray() ? syntaxDescriptionsJson.toVariant().toStringList() : syntaxDescriptionsJson.toString().split(QLatin1Char(','), Qt::SkipEmptyParts); 0092 const int descriptionCount = syntaxDescriptions.count(); 0093 for (int i = 0; i < syntaxes.count(); ++i) { 0094 const QString &query = syntaxes.at(i); 0095 const QString description = i < descriptionCount ? syntaxDescriptions.at(i) : QString(); 0096 addSyntax(query, description); 0097 } 0098 } 0099 0100 DBusRunner::~DBusRunner() = default; 0101 0102 void DBusRunner::reloadConfiguration() 0103 { 0104 // If we have already loaded a config, but the runner is told to reload it's config 0105 if (m_callLifecycleMethods) { 0106 suspendMatching(true); 0107 requestConfig(); 0108 } 0109 } 0110 0111 void DBusRunner::createQActionsFromRemoteActions(const QMap<QString, RemoteActions> &remoteActions) 0112 { 0113 for (auto it = remoteActions.begin(), end = remoteActions.end(); it != end; it++) { 0114 const QString service = it.key(); 0115 const RemoteActions actions = it.value(); 0116 auto &serviceActions = m_actions[service]; 0117 qDeleteAll(serviceActions); 0118 serviceActions.clear(); 0119 for (const RemoteAction &action : actions) { 0120 auto a = new QAction(QIcon::fromTheme(action.iconName), action.text, this); 0121 a->setData(action.id); 0122 serviceActions.append(a); 0123 } 0124 } 0125 } 0126 0127 void DBusRunner::teardown() 0128 { 0129 if (m_matchWasCalled) { 0130 for (const QString &service : std::as_const(m_matchingServices)) { 0131 auto method = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Teardown")); 0132 QDBusConnection::sessionBus().asyncCall(method); 0133 } 0134 } 0135 m_actionsForSessionRequested = false; 0136 m_matchWasCalled = false; 0137 } 0138 0139 QMap<QString, RemoteActions> DBusRunner::requestActions() 0140 { 0141 // in the multi-services case, register separate actions from each plugin in case they happen to be somehow different 0142 // then match together in matchForAction() 0143 QMap<QString, RemoteActions> returnedActions; 0144 for (const QString &service : std::as_const(m_matchingServices)) { 0145 // if we only want to request the actions once and have done so we want to skip the service 0146 // but in case it got newly loaded we need to request the actions, BUG: 435350 0147 if (m_requestActionsOnce) { 0148 if (m_requestedActionServices.contains(service)) { 0149 continue; 0150 } else { 0151 m_requestedActionServices << service; 0152 } 0153 } 0154 0155 auto getActionsMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Actions")); 0156 QDBusPendingReply<RemoteActions> reply = QDBusConnection::sessionBus().call(getActionsMethod); 0157 if (!reply.isValid()) { 0158 qCDebug(KRUNNER) << "Error requesting actions; calling" << service << " :" << reply.error().name() << reply.error().message(); 0159 } else { 0160 returnedActions.insert(service, reply.value()); 0161 } 0162 } 0163 return returnedActions; 0164 } 0165 0166 void DBusRunner::requestConfig() 0167 { 0168 const QString service = *m_matchingServices.constBegin(); 0169 auto getConfigMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Config")); 0170 QDBusPendingReply<QVariantMap> reply = QDBusConnection::sessionBus().asyncCall(getConfigMethod); 0171 0172 auto watcher = new QDBusPendingCallWatcher(reply); 0173 connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, service]() { 0174 watcher->deleteLater(); 0175 QDBusReply<QVariantMap> reply = *watcher; 0176 if (!reply.isValid()) { 0177 suspendMatching(false); 0178 qCDebug(KRUNNER) << "Error requesting config; calling" << service << " :" << reply.error().name() << reply.error().message(); 0179 return; 0180 } 0181 const QVariantMap config = reply.value(); 0182 for (auto it = config.cbegin(), end = config.cend(); it != end; ++it) { 0183 if (it.key() == QLatin1String("MatchRegex")) { 0184 QRegularExpression regex(it.value().toString()); 0185 setMatchRegex(regex); 0186 } else if (it.key() == QLatin1String("MinLetterCount")) { 0187 setMinLetterCount(it.value().toInt()); 0188 } else if (it.key() == QLatin1String("TriggerWords")) { 0189 setTriggerWords(it.value().toStringList()); 0190 } else if (it.key() == QLatin1String("Actions")) { 0191 const auto remoteActions = it.value().value<RemoteActions>(); 0192 createQActionsFromRemoteActions(QMap<QString, RemoteActions>{{service, remoteActions}}); 0193 m_actionsOnceRequested = true; 0194 m_actionsForSessionRequested = true; 0195 } 0196 } 0197 suspendMatching(false); 0198 }); 0199 } 0200 0201 void DBusRunner::match(Plasma::RunnerContext &context) 0202 { 0203 QSet<QString> services; 0204 { 0205 QMutexLocker lock(&m_mutex); 0206 services = m_matchingServices; 0207 m_matchWasCalled = true; 0208 0209 // Request the actions 0210 if ((m_requestActionsOnce && !m_actionsOnceRequested) // We only want to fetch the actions once but haven't done so yet 0211 || (!m_actionsForSessionRequested)) { // We want to fetch the actions for each match session 0212 m_actionsOnceRequested = true; 0213 m_actionsForSessionRequested = true; 0214 auto actions = requestActions(); 0215 QMetaObject::invokeMethod(this, "createQActionsFromRemoteActions", QArgument("QMap<QString, RemoteActions>", actions)); 0216 } 0217 } 0218 // we scope watchers to make sure the lambda that captures context by reference definitely gets disconnected when this function ends 0219 std::vector<std::unique_ptr<QDBusPendingCallWatcher>> watchers; 0220 0221 for (const QString &service : std::as_const(services)) { 0222 auto matchMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Match")); 0223 matchMethod.setArguments(QList<QVariant>({context.query()})); 0224 QDBusPendingReply<RemoteMatches> reply = QDBusConnection::sessionBus().asyncCall(matchMethod); 0225 0226 watchers.push_back(std::make_unique<QDBusPendingCallWatcher>(reply)); 0227 connect( 0228 watchers.back().get(), 0229 &QDBusPendingCallWatcher::finished, 0230 this, 0231 [this, service, &context, reply]() { 0232 if (reply.isError()) { 0233 qCDebug(KRUNNER) << "Error requesting matches; calling" << service << " :" << reply.error().name() << reply.error().message(); 0234 return; 0235 } 0236 const auto matches = reply.value(); 0237 for (const RemoteMatch &match : matches) { 0238 Plasma::QueryMatch m(this); 0239 0240 m.setText(match.text); 0241 m.setIconName(match.iconName); 0242 m.setType(match.type); 0243 m.setRelevance(match.relevance); 0244 0245 // split is essential items are as native DBus types, optional extras are in the property map (which is obviously a lot slower to parse) 0246 m.setUrls(QUrl::fromStringList(match.properties.value(QStringLiteral("urls")).toStringList())); 0247 m.setMatchCategory(match.properties.value(QStringLiteral("category")).toString()); 0248 m.setSubtext(match.properties.value(QStringLiteral("subtext")).toString()); 0249 const auto actionsIt = match.properties.find(QStringLiteral("actions")); 0250 if (actionsIt == match.properties.cend()) { 0251 m.setData(QVariantList({service})); 0252 } else { 0253 m.setData(QVariantList({service, actionsIt.value().toStringList()})); 0254 } 0255 m.setId(match.id); 0256 m.setMultiLine(match.properties.value(QStringLiteral("multiline")).toBool()); 0257 0258 const QVariant iconData = match.properties.value(QStringLiteral("icon-data")); 0259 if (iconData.isValid()) { 0260 const auto iconDataArgument = iconData.value<QDBusArgument>(); 0261 if (iconDataArgument.currentType() == QDBusArgument::StructureType 0262 && iconDataArgument.currentSignature() == QLatin1String("(iiibiiay)")) { 0263 const RemoteImage remoteImage = qdbus_cast<RemoteImage>(iconDataArgument); 0264 const QImage decodedImage = decodeImage(remoteImage); 0265 if (!decodedImage.isNull()) { 0266 const QPixmap pix = QPixmap::fromImage(decodedImage); 0267 QIcon icon(pix); 0268 m.setIcon(icon); 0269 // iconName normally takes precedence 0270 m.setIconName(QString()); 0271 } 0272 } else { 0273 qCWarning(KRUNNER) << "Invalid signature of icon-data property:" << iconDataArgument.currentSignature(); 0274 } 0275 } 0276 0277 context.addMatch(m); 0278 }; 0279 }, 0280 Qt::DirectConnection); // process reply in the watcher's thread (aka the one running ::match not the one owning the runner) 0281 } 0282 // we're done matching when every service replies 0283 for (auto &w : watchers) { 0284 w->waitForFinished(); 0285 } 0286 } 0287 0288 QList<QAction *> DBusRunner::actionsForMatch(const Plasma::QueryMatch &match) 0289 { 0290 const QVariantList data = match.data().toList(); 0291 if (data.count() > 1) { 0292 const QStringList actionIds = data.at(1).toStringList(); 0293 const QList<QAction *> actionList = m_actions.value(data.constFirst().toString()); 0294 QList<QAction *> requestedActions; 0295 for (QAction *action : actionList) { 0296 if (actionIds.contains(action->data().toString())) { 0297 requestedActions << action; 0298 } 0299 } 0300 return requestedActions; 0301 } else { 0302 return m_actions.value(data.constFirst().toString()); 0303 } 0304 } 0305 0306 void DBusRunner::run(const Plasma::RunnerContext &context, const Plasma::QueryMatch &match) 0307 { 0308 Q_UNUSED(context); 0309 0310 QString actionId; 0311 QString matchId; 0312 if (m_hasUniqueResults) { 0313 matchId = match.id(); 0314 } else { 0315 matchId = match.id().mid(id().length() + 1); // QueryMatch::setId mangles the match ID with runnerID + '_'. This unmangles it 0316 } 0317 const QString service = match.data().toList().constFirst().toString(); 0318 0319 if (match.selectedAction()) { 0320 actionId = match.selectedAction()->data().toString(); 0321 } 0322 0323 auto runMethod = QDBusMessage::createMethodCall(service, m_path, QStringLiteral(IFACE_NAME), QStringLiteral("Run")); 0324 runMethod.setArguments(QList<QVariant>({matchId, actionId})); 0325 QDBusConnection::sessionBus().call(runMethod, QDBus::NoBlock); 0326 } 0327 0328 QImage DBusRunner::decodeImage(const RemoteImage &remoteImage) 0329 { 0330 auto copyLineRGB32 = [](QRgb *dst, const char *src, int width) { 0331 const char *end = src + width * 3; 0332 for (; src != end; ++dst, src += 3) { 0333 *dst = qRgb(src[0], src[1], src[2]); 0334 } 0335 }; 0336 0337 auto copyLineARGB32 = [](QRgb *dst, const char *src, int width) { 0338 const char *end = src + width * 4; 0339 for (; src != end; ++dst, src += 4) { 0340 *dst = qRgba(src[0], src[1], src[2], src[3]); 0341 } 0342 }; 0343 0344 if (remoteImage.width <= 0 || remoteImage.width >= 2048 || remoteImage.height <= 0 || remoteImage.height >= 2048 || remoteImage.rowStride <= 0) { 0345 qCWarning(KRUNNER) << "Invalid image metadata (width:" << remoteImage.width << "height:" << remoteImage.height << "rowStride:" << remoteImage.rowStride 0346 << ")"; 0347 return QImage(); 0348 } 0349 0350 QImage::Format format = QImage::Format_Invalid; 0351 void (*copyFn)(QRgb *, const char *, int) = nullptr; 0352 if (remoteImage.bitsPerSample == 8) { 0353 if (remoteImage.channels == 4) { 0354 format = QImage::Format_ARGB32; 0355 copyFn = copyLineARGB32; 0356 } else if (remoteImage.channels == 3) { 0357 format = QImage::Format_RGB32; 0358 copyFn = copyLineRGB32; 0359 } 0360 } 0361 if (format == QImage::Format_Invalid) { 0362 qCWarning(KRUNNER) << "Unsupported image format (hasAlpha:" << remoteImage.hasAlpha << "bitsPerSample:" << remoteImage.bitsPerSample 0363 << "channels:" << remoteImage.channels << ")"; 0364 return QImage(); 0365 } 0366 0367 QImage image(remoteImage.width, remoteImage.height, format); 0368 const QByteArray pixels = remoteImage.data; 0369 const char *ptr = pixels.data(); 0370 const char *end = ptr + pixels.length(); 0371 for (int y = 0; y < remoteImage.height; ++y, ptr += remoteImage.rowStride) { 0372 if (Q_UNLIKELY(ptr + remoteImage.channels * remoteImage.width > end)) { 0373 qCWarning(KRUNNER) << "Image data is incomplete. y:" << y << "height:" << remoteImage.height; 0374 break; 0375 } 0376 copyFn(reinterpret_cast<QRgb *>(image.scanLine(y)), ptr, remoteImage.width); 0377 } 0378 0379 return image; 0380 } 0381 0382 #include "moc_dbusrunner_p.cpp"