File indexing completed on 2024-04-21 03:56:47
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 <QDBusConnection> 0012 #include <QDBusConnectionInterface> 0013 #include <QDBusMessage> 0014 #include <QDBusMetaType> 0015 #include <QDBusPendingReply> 0016 #include <QIcon> 0017 #include <set> 0018 0019 #include "dbusutils_p.h" 0020 #include "krunner_debug.h" 0021 0022 namespace KRunner 0023 { 0024 DBusRunner::DBusRunner(QObject *parent, const KPluginMetaData &data) 0025 : KRunner::AbstractRunner(parent, data) 0026 , m_path(data.value(QStringLiteral("X-Plasma-DBusRunner-Path"), QStringLiteral("/runner"))) 0027 , m_hasUniqueResults(data.value(QStringLiteral("X-Plasma-Runner-Unique-Results"), false)) 0028 , m_requestActionsOnce(data.value(QStringLiteral("X-Plasma-Request-Actions-Once"), false)) 0029 , m_callLifecycleMethods(data.value(QStringLiteral("X-Plasma-API")) == QLatin1String("DBus2")) 0030 , m_ifaceName(QStringLiteral("org.kde.krunner1")) 0031 { 0032 qDBusRegisterMetaType<RemoteMatch>(); 0033 qDBusRegisterMetaType<RemoteMatches>(); 0034 qDBusRegisterMetaType<KRunner::Action>(); 0035 qDBusRegisterMetaType<KRunner::Actions>(); 0036 qDBusRegisterMetaType<RemoteImage>(); 0037 0038 QString requestedServiceName = data.value(QStringLiteral("X-Plasma-DBusRunner-Service")); 0039 if (requestedServiceName.isEmpty() || m_path.isEmpty()) { 0040 qCWarning(KRUNNER) << "Invalid entry:" << data; 0041 return; 0042 } 0043 0044 if (requestedServiceName.endsWith(QLatin1Char('*'))) { 0045 requestedServiceName.chop(1); 0046 // find existing matching names 0047 auto namesReply = QDBusConnection::sessionBus().interface()->registeredServiceNames(); 0048 if (namesReply.isValid()) { 0049 const auto names = namesReply.value(); 0050 for (const QString &serviceName : names) { 0051 if (serviceName.startsWith(requestedServiceName)) { 0052 m_matchingServices << serviceName; 0053 } 0054 } 0055 } 0056 // and watch for changes 0057 connect(QDBusConnection::sessionBus().interface(), 0058 &QDBusConnectionInterface::serviceOwnerChanged, 0059 this, 0060 [this, requestedServiceName](const QString &serviceName, const QString &oldOwner, const QString &newOwner) { 0061 if (!serviceName.startsWith(requestedServiceName)) { 0062 return; 0063 } 0064 if (!oldOwner.isEmpty() && !newOwner.isEmpty()) { 0065 // changed owner, but service still exists. Don't need to adjust anything 0066 return; 0067 } 0068 if (!newOwner.isEmpty()) { 0069 m_matchingServices.insert(serviceName); 0070 } 0071 if (!oldOwner.isEmpty()) { 0072 m_matchingServices.remove(serviceName); 0073 } 0074 }); 0075 } else { 0076 // don't check when not wildcarded, as it could be used with DBus-activation 0077 m_matchingServices << requestedServiceName; 0078 } 0079 0080 connect(this, &AbstractRunner::teardown, this, &DBusRunner::teardown); 0081 0082 // Load the runner syntaxes 0083 const QStringList syntaxes = data.value(QStringLiteral("X-Plasma-Runner-Syntaxes"), QStringList()); 0084 const QStringList syntaxDescriptions = data.value(QStringLiteral("X-Plasma-Runner-Syntax-Descriptions"), QStringList()); 0085 const int descriptionCount = syntaxDescriptions.count(); 0086 for (int i = 0; i < syntaxes.count(); ++i) { 0087 const QString &query = syntaxes.at(i); 0088 const QString description = i < descriptionCount ? syntaxDescriptions.at(i) : QString(); 0089 addSyntax(query, description); 0090 } 0091 } 0092 0093 void DBusRunner::reloadConfiguration() 0094 { 0095 // If we have already loaded a config, but the runner is told to reload it's config 0096 if (m_callLifecycleMethods) { 0097 suspendMatching(true); 0098 requestConfig(); 0099 } 0100 } 0101 0102 void DBusRunner::teardown() 0103 { 0104 if (m_matchWasCalled) { 0105 for (const QString &service : std::as_const(m_matchingServices)) { 0106 auto method = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Teardown")); 0107 QDBusConnection::sessionBus().asyncCall(method); 0108 } 0109 } 0110 m_actionsForSessionRequested = false; 0111 m_matchWasCalled = false; 0112 } 0113 0114 void DBusRunner::requestActionsForService(const QString &service, const std::function<void()> &finishedCallback) 0115 { 0116 if (m_actionsForSessionRequested) { 0117 finishedCallback(); 0118 return; // only once per match session 0119 } 0120 if (m_requestActionsOnce) { 0121 if (m_requestedActionServices.contains(service)) { 0122 finishedCallback(); 0123 return; 0124 } else { 0125 m_requestedActionServices << service; 0126 } 0127 } 0128 0129 auto getActionsMethod = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Actions")); 0130 QDBusPendingReply<QList<KRunner::Action>> reply = QDBusConnection::sessionBus().asyncCall(getActionsMethod); 0131 connect(new QDBusPendingCallWatcher(reply), &QDBusPendingCallWatcher::finished, this, [this, service, reply, finishedCallback](auto watcher) { 0132 watcher->deleteLater(); 0133 if (!reply.isValid()) { 0134 qCDebug(KRUNNER) << "Error requesting actions; calling" << service << " :" << reply.error().name() << reply.error().message(); 0135 } else { 0136 m_actions[service] = reply.value(); 0137 } 0138 finishedCallback(); 0139 }); 0140 } 0141 0142 void DBusRunner::requestConfig() 0143 { 0144 const QString service = *m_matchingServices.constBegin(); 0145 auto getConfigMethod = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Config")); 0146 QDBusPendingReply<QVariantMap> reply = QDBusConnection::sessionBus().asyncCall(getConfigMethod); 0147 0148 auto watcher = new QDBusPendingCallWatcher(reply); 0149 connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, service]() { 0150 watcher->deleteLater(); 0151 QDBusReply<QVariantMap> reply = *watcher; 0152 if (!reply.isValid()) { 0153 suspendMatching(false); 0154 qCWarning(KRUNNER) << "Error requesting config; calling" << service << " :" << reply.error().name() << reply.error().message(); 0155 return; 0156 } 0157 const QVariantMap config = reply.value(); 0158 for (auto it = config.cbegin(), end = config.cend(); it != end; ++it) { 0159 if (it.key() == QLatin1String("MatchRegex")) { 0160 QRegularExpression regex(it.value().toString()); 0161 setMatchRegex(regex); 0162 } else if (it.key() == QLatin1String("MinLetterCount")) { 0163 setMinLetterCount(it.value().toInt()); 0164 } else if (it.key() == QLatin1String("TriggerWords")) { 0165 setTriggerWords(it.value().toStringList()); 0166 } else if (it.key() == QLatin1String("Actions")) { 0167 m_actions[service] = it.value().value<QList<KRunner::Action>>(); 0168 m_requestedActionServices << service; 0169 } 0170 } 0171 suspendMatching(false); 0172 }); 0173 } 0174 0175 QList<QueryMatch> DBusRunner::convertMatches(const QString &service, const RemoteMatches &remoteMatches) 0176 { 0177 QList<KRunner::QueryMatch> matches; 0178 for (const RemoteMatch &match : remoteMatches) { 0179 KRunner::QueryMatch m(this); 0180 0181 m.setText(match.text); 0182 m.setIconName(match.iconName); 0183 m.setCategoryRelevance(match.categoryRelevance); 0184 m.setRelevance(match.relevance); 0185 0186 // split is essential items are as native DBus types, optional extras are in the property map (which is obviously a lot slower to parse) 0187 m.setUrls(QUrl::fromStringList(match.properties.value(QStringLiteral("urls")).toStringList())); 0188 m.setMatchCategory(match.properties.value(QStringLiteral("category")).toString()); 0189 m.setSubtext(match.properties.value(QStringLiteral("subtext")).toString()); 0190 m.setData(QVariantList({service})); 0191 m.setId(match.id); 0192 m.setMultiLine(match.properties.value(QStringLiteral("multiline")).toBool()); 0193 0194 const auto actionsIt = match.properties.find(QStringLiteral("actions")); 0195 const KRunner::Actions actionList = m_actions.value(service); 0196 if (actionsIt == match.properties.cend()) { 0197 m.setActions(actionList); 0198 } else { 0199 KRunner::Actions requestedActions; 0200 const QStringList actionIds = actionsIt.value().toStringList(); 0201 for (const auto &action : actionList) { 0202 if (actionIds.contains(action.id())) { 0203 requestedActions << action; 0204 } 0205 } 0206 m.setActions(requestedActions); 0207 } 0208 0209 const QVariant iconData = match.properties.value(QStringLiteral("icon-data")); 0210 if (iconData.isValid()) { 0211 const auto iconDataArgument = iconData.value<QDBusArgument>(); 0212 if (iconDataArgument.currentType() == QDBusArgument::StructureType && iconDataArgument.currentSignature() == QLatin1String("(iiibiiay)")) { 0213 const RemoteImage remoteImage = qdbus_cast<RemoteImage>(iconDataArgument); 0214 if (QImage decodedImage = decodeImage(remoteImage); !decodedImage.isNull()) { 0215 const QPixmap pix = QPixmap::fromImage(std::move(decodedImage)); 0216 QIcon icon(pix); 0217 m.setIcon(icon); 0218 // iconName normally takes precedence 0219 m.setIconName(QString()); 0220 } 0221 } else { 0222 qCWarning(KRUNNER) << "Invalid signature of icon-data property:" << iconDataArgument.currentSignature(); 0223 } 0224 } 0225 matches.append(m); 0226 } 0227 return matches; 0228 } 0229 void DBusRunner::matchInternal(KRunner::RunnerContext context) 0230 { 0231 const QString jobId = context.runnerJobId(this); 0232 if (m_matchingServices.isEmpty()) { 0233 Q_EMIT matchInternalFinished(jobId); 0234 } 0235 m_matchWasCalled = true; 0236 0237 // we scope watchers to make sure the lambda that captures context by reference definitely gets disconnected when this function ends 0238 std::shared_ptr<std::set<QString>> pendingServices(new std::set<QString>); 0239 0240 for (const QString &service : std::as_const(m_matchingServices)) { 0241 pendingServices->insert(service); 0242 0243 const auto onActionsFinished = [=, this]() mutable { 0244 auto matchMethod = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Match")); 0245 matchMethod.setArguments(QList<QVariant>({context.query()})); 0246 QDBusPendingReply<RemoteMatches> reply = QDBusConnection::sessionBus().asyncCall(matchMethod); 0247 0248 auto watcher = new QDBusPendingCallWatcher(reply); 0249 0250 connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, service, context, reply, jobId, pendingServices, watcher]() mutable { 0251 watcher->deleteLater(); 0252 pendingServices->erase(service); 0253 if (reply.isError()) { 0254 qCWarning(KRUNNER) << "Error requesting matches; calling" << service << " :" << reply.error().name() << reply.error().message(); 0255 return; 0256 } 0257 context.addMatches(convertMatches(service, reply.value())); 0258 // We are finished when all watchers finished 0259 if (pendingServices->size() == 0) { 0260 Q_EMIT matchInternalFinished(jobId); 0261 } 0262 }); 0263 }; 0264 requestActionsForService(service, onActionsFinished); 0265 } 0266 m_actionsForSessionRequested = true; 0267 } 0268 0269 void DBusRunner::run(const KRunner::RunnerContext & /*context*/, const KRunner::QueryMatch &match) 0270 { 0271 QString actionId; 0272 QString matchId; 0273 if (m_hasUniqueResults) { 0274 matchId = match.id(); 0275 } else { 0276 matchId = match.id().mid(id().length() + 1); // QueryMatch::setId mangles the match ID with runnerID + '_'. This unmangles it 0277 } 0278 const QString service = match.data().toList().constFirst().toString(); 0279 0280 if (match.selectedAction()) { 0281 actionId = match.selectedAction().id(); 0282 } 0283 0284 auto runMethod = QDBusMessage::createMethodCall(service, m_path, m_ifaceName, QStringLiteral("Run")); 0285 runMethod.setArguments(QList<QVariant>({matchId, actionId})); 0286 QDBusConnection::sessionBus().call(runMethod, QDBus::NoBlock); 0287 } 0288 0289 QImage DBusRunner::decodeImage(const RemoteImage &remoteImage) 0290 { 0291 auto copyLineRGB32 = [](QRgb *dst, const char *src, int width) { 0292 const char *end = src + width * 3; 0293 for (; src != end; ++dst, src += 3) { 0294 *dst = qRgb(src[0], src[1], src[2]); 0295 } 0296 }; 0297 0298 auto copyLineARGB32 = [](QRgb *dst, const char *src, int width) { 0299 const char *end = src + width * 4; 0300 for (; src != end; ++dst, src += 4) { 0301 *dst = qRgba(src[0], src[1], src[2], src[3]); 0302 } 0303 }; 0304 0305 if (remoteImage.width <= 0 || remoteImage.width >= 2048 || remoteImage.height <= 0 || remoteImage.height >= 2048 || remoteImage.rowStride <= 0) { 0306 qCWarning(KRUNNER) << "Invalid image metadata (width:" << remoteImage.width << "height:" << remoteImage.height << "rowStride:" << remoteImage.rowStride 0307 << ")"; 0308 return QImage(); 0309 } 0310 0311 QImage::Format format = QImage::Format_Invalid; 0312 void (*copyFn)(QRgb *, const char *, int) = nullptr; 0313 if (remoteImage.bitsPerSample == 8) { 0314 if (remoteImage.channels == 4) { 0315 format = QImage::Format_ARGB32; 0316 copyFn = copyLineARGB32; 0317 } else if (remoteImage.channels == 3) { 0318 format = QImage::Format_RGB32; 0319 copyFn = copyLineRGB32; 0320 } 0321 } 0322 if (format == QImage::Format_Invalid) { 0323 qCWarning(KRUNNER) << "Unsupported image format (hasAlpha:" << remoteImage.hasAlpha << "bitsPerSample:" << remoteImage.bitsPerSample 0324 << "channels:" << remoteImage.channels << ")"; 0325 return QImage(); 0326 } 0327 0328 QImage image(remoteImage.width, remoteImage.height, format); 0329 const QByteArray pixels = remoteImage.data; 0330 const char *ptr = pixels.data(); 0331 const char *end = ptr + pixels.length(); 0332 for (int y = 0; y < remoteImage.height; ++y, ptr += remoteImage.rowStride) { 0333 if (Q_UNLIKELY(ptr + remoteImage.channels * remoteImage.width > end)) { 0334 qCWarning(KRUNNER) << "Image data is incomplete. y:" << y << "height:" << remoteImage.height; 0335 break; 0336 } 0337 copyFn(reinterpret_cast<QRgb *>(image.scanLine(y)), ptr, remoteImage.width); 0338 } 0339 0340 return image; 0341 } 0342 } 0343 #include "moc_dbusrunner_p.cpp"