File indexing completed on 2024-11-10 04:40:53
0001 /* 0002 SPDX-FileCopyrightText: 2010 Volker Krause <vkrause@kde.org> 0003 SPDX-FileCopyrightText: 2013 Daniel Vrátil <dvratil@redhat.com> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "searchmanager.h" 0009 #include "abstractsearchplugin.h" 0010 #include "akonadiserver_search_debug.h" 0011 0012 #include "agentsearchengine.h" 0013 #include "akonadi.h" 0014 #include "handler/searchhelper.h" 0015 #include "notificationmanager.h" 0016 #include "searchrequest.h" 0017 #include "searchtaskmanager.h" 0018 #include "storage/datastore.h" 0019 #include "storage/querybuilder.h" 0020 #include "storage/selectquerybuilder.h" 0021 #include "storage/transaction.h" 0022 0023 #include "private/protocol_p.h" 0024 0025 #include <QDBusConnection> 0026 #include <QDir> 0027 #include <QPluginLoader> 0028 #include <QTimer> 0029 0030 #include <memory> 0031 0032 Q_DECLARE_METATYPE(Akonadi::Server::NotificationCollector *) 0033 0034 using namespace Akonadi; 0035 using namespace Akonadi::Server; 0036 0037 Q_DECLARE_METATYPE(Collection) 0038 0039 SearchManager::SearchManager(const QStringList &searchEngines, SearchTaskManager &agentSearchManager) 0040 : AkThread(QStringLiteral("SearchManager"), AkThread::ManualStart, QThread::InheritPriority) 0041 , mAgentSearchManager(agentSearchManager) 0042 , mEngineNames(searchEngines) 0043 , mSearchUpdateTimer(nullptr) 0044 { 0045 qRegisterMetaType<Collection>(); 0046 0047 // We load search plugins (as in QLibrary::load()) in the main thread so that 0048 // static initialization happens in the QApplication thread 0049 loadSearchPlugins(); 0050 0051 // Register to DBus on the main thread connection - otherwise we don't appear 0052 // on the service. 0053 QDBusConnection conn = QDBusConnection::sessionBus(); 0054 conn.registerObject(QStringLiteral("/SearchManager"), this, QDBusConnection::ExportAllSlots); 0055 0056 // Delay-call init() 0057 startThread(); 0058 } 0059 0060 void SearchManager::init() 0061 { 0062 AkThread::init(); 0063 0064 mEngines.reserve(mEngineNames.size()); 0065 for (const QString &engineName : std::as_const(mEngineNames)) { 0066 if (engineName == QLatin1StringView("Agent")) { 0067 mEngines.append(new AgentSearchEngine); 0068 } else { 0069 qCCritical(AKONADISERVER_SEARCH_LOG) << "Unknown search engine type: " << engineName; 0070 } 0071 } 0072 0073 initSearchPlugins(); 0074 0075 // The timer will tick 15 seconds after last change notification. If a new notification 0076 // is delivered in the meantime, the timer is reset 0077 mSearchUpdateTimer = new QTimer(this); 0078 mSearchUpdateTimer->setInterval(15 * 1000); 0079 mSearchUpdateTimer->setSingleShot(true); 0080 connect(mSearchUpdateTimer, &QTimer::timeout, this, &SearchManager::searchUpdateTimeout); 0081 } 0082 0083 void SearchManager::quit() 0084 { 0085 QDBusConnection conn = QDBusConnection::sessionBus(); 0086 conn.unregisterObject(QStringLiteral("/SearchManager"), QDBusConnection::UnregisterTree); 0087 conn.disconnectFromBus(conn.name()); 0088 0089 // Make sure all children are deleted within context of this thread 0090 qDeleteAll(children()); 0091 0092 qDeleteAll(mEngines); 0093 qDeleteAll(mPlugins); 0094 /* 0095 * FIXME: Unloading plugin messes up some global statics from client libs 0096 * and causes crash on Akonadi shutdown (below main). Keeping the plugins 0097 * loaded is not really a big issue as this is only invoked on server shutdown 0098 * anyway, so we are not leaking any memory. 0099 Q_FOREACH (QPluginLoader *loader, mPluginLoaders) { 0100 loader->unload(); 0101 delete loader; 0102 } 0103 */ 0104 0105 AkThread::quit(); 0106 } 0107 0108 SearchManager::~SearchManager() 0109 { 0110 quitThread(); 0111 } 0112 0113 void SearchManager::registerInstance(const QString &id) 0114 { 0115 mAgentSearchManager.registerInstance(id); 0116 } 0117 0118 void SearchManager::unregisterInstance(const QString &id) 0119 { 0120 mAgentSearchManager.unregisterInstance(id); 0121 } 0122 0123 QList<AbstractSearchPlugin *> SearchManager::searchPlugins() const 0124 { 0125 return mPlugins; 0126 } 0127 0128 void SearchManager::loadSearchPlugins() 0129 { 0130 QStringList loadedPlugins; 0131 const QString pluginOverride = QString::fromLatin1(qgetenv("AKONADI_OVERRIDE_SEARCHPLUGIN")); 0132 if (!pluginOverride.isEmpty()) { 0133 qCInfo(AKONADISERVER_SEARCH_LOG) << "Overriding the search plugins with: " << pluginOverride; 0134 } 0135 0136 const QStringList dirs = QCoreApplication::libraryPaths(); 0137 for (const QString &pluginDir : dirs) { 0138 const QString path(pluginDir + QStringLiteral("/pim6/akonadi")); 0139 QDir dir(path); 0140 const QStringList fileNames = dir.entryList(QDir::Files); 0141 qCDebug(AKONADISERVER_SEARCH_LOG) << "SEARCH MANAGER: searching in " << path << ":" << fileNames; 0142 for (const QString &fileName : fileNames) { 0143 const QString filePath = path % QLatin1Char('/') % fileName; 0144 std::unique_ptr<QPluginLoader> loader(new QPluginLoader(filePath)); 0145 const QVariantMap metadata = loader->metaData().value(QStringLiteral("MetaData")).toVariant().toMap(); 0146 if (metadata.value(QStringLiteral("X-Akonadi-PluginType")).toString() != QLatin1StringView("SearchPlugin")) { 0147 continue; 0148 } 0149 0150 const QString libraryName = metadata.value(QStringLiteral("X-Akonadi-Library")).toString(); 0151 if (loadedPlugins.contains(libraryName)) { 0152 qCDebug(AKONADISERVER_SEARCH_LOG) << "Already loaded one version of this plugin, skipping: " << libraryName; 0153 continue; 0154 } 0155 0156 // When search plugin override is active, ignore all plugins except for the override 0157 if (!pluginOverride.isEmpty()) { 0158 if (libraryName != pluginOverride) { 0159 qCDebug(AKONADISERVER_SEARCH_LOG) << libraryName << "skipped because of AKONADI_OVERRIDE_SEARCHPLUGIN"; 0160 continue; 0161 } 0162 0163 // When there's no override, only load plugins enabled by default 0164 } else if (!metadata.value(QStringLiteral("X-Akonadi-LoadByDefault"), true).toBool()) { 0165 continue; 0166 } 0167 0168 if (!loader->load()) { 0169 qCCritical(AKONADISERVER_SEARCH_LOG) << "Failed to load search plugin" << libraryName << ":" << loader->errorString(); 0170 continue; 0171 } 0172 0173 mPluginLoaders << loader.release(); 0174 loadedPlugins << libraryName; 0175 } 0176 } 0177 } 0178 0179 void SearchManager::initSearchPlugins() 0180 { 0181 for (QPluginLoader *loader : std::as_const(mPluginLoaders)) { 0182 if (!loader->load()) { 0183 qCCritical(AKONADISERVER_SEARCH_LOG) << "Failed to load search plugin" << loader->fileName() << ":" << loader->errorString(); 0184 continue; 0185 } 0186 0187 AbstractSearchPlugin *plugin = qobject_cast<AbstractSearchPlugin *>(loader->instance()); 0188 if (!plugin) { 0189 qCCritical(AKONADISERVER_SEARCH_LOG) << loader->fileName() << "is not a valid Akonadi search plugin"; 0190 continue; 0191 } 0192 0193 qCDebug(AKONADISERVER_SEARCH_LOG) << "SearchManager: loaded search plugin" << loader->fileName(); 0194 mPlugins << plugin; 0195 } 0196 } 0197 0198 void SearchManager::scheduleSearchUpdate() 0199 { 0200 // Reset if the timer is active (use QueuedConnection to invoke start() from 0201 // the thread the QTimer lives in instead of caller's thread, otherwise crashes 0202 // and weird things can happen. 0203 QMetaObject::invokeMethod(mSearchUpdateTimer, qOverload<>(&QTimer::start), Qt::QueuedConnection); 0204 } 0205 0206 void SearchManager::searchUpdateTimeout() 0207 { 0208 // Get all search collections, that is subcollections of "Search", which always has ID 1 0209 const Collection::List collections = Collection::retrieveFiltered(Collection::parentIdFullColumnName(), 1); 0210 for (const Collection &collection : collections) { 0211 updateSearchAsync(collection); 0212 } 0213 } 0214 0215 void SearchManager::updateSearchAsync(const Collection &collection) 0216 { 0217 QMetaObject::invokeMethod( 0218 this, 0219 [this, collection]() { 0220 updateSearchImpl(collection); 0221 }, 0222 Qt::QueuedConnection); 0223 } 0224 0225 void SearchManager::updateSearch(const Collection &collection) 0226 { 0227 mLock.lock(); 0228 if (mUpdatingCollections.contains(collection.id())) { 0229 mLock.unlock(); 0230 return; 0231 // FIXME: If another thread already requested an update, we return to the caller before the 0232 // search update is performed; this contradicts the docs 0233 } 0234 mUpdatingCollections.insert(collection.id()); 0235 mLock.unlock(); 0236 QMetaObject::invokeMethod( 0237 this, 0238 [this, collection]() { 0239 updateSearchImpl(collection); 0240 }, 0241 Qt::BlockingQueuedConnection); 0242 mLock.lock(); 0243 mUpdatingCollections.remove(collection.id()); 0244 mLock.unlock(); 0245 } 0246 0247 void SearchManager::updateSearchImpl(const Collection &collection) 0248 { 0249 if (collection.queryString().size() >= 32768) { 0250 qCWarning(AKONADISERVER_SEARCH_LOG) << "The query is at least 32768 chars long, which is the maximum size supported by the akonadi db schema. The " 0251 "query is therefore most likely truncated and will not be executed."; 0252 return; 0253 } 0254 if (collection.queryString().isEmpty()) { 0255 return; 0256 } 0257 0258 const QStringList queryAttributes = collection.queryAttributes().split(QLatin1Char(' ')); 0259 const bool remoteSearch = queryAttributes.contains(QLatin1StringView(AKONADI_PARAM_REMOTE)); 0260 bool recursive = queryAttributes.contains(QLatin1StringView(AKONADI_PARAM_RECURSIVE)); 0261 0262 QStringList queryMimeTypes; 0263 const QList<MimeType> mimeTypes = collection.mimeTypes(); 0264 queryMimeTypes.reserve(mimeTypes.count()); 0265 0266 for (const MimeType &mt : mimeTypes) { 0267 queryMimeTypes << mt.name(); 0268 } 0269 0270 QList<qint64> queryAncestors; 0271 if (collection.queryCollections().isEmpty()) { 0272 queryAncestors << 0; 0273 recursive = true; 0274 } else { 0275 const QStringList collectionIds = collection.queryCollections().split(QLatin1Char(' ')); 0276 queryAncestors.reserve(collectionIds.count()); 0277 for (const QString &colId : collectionIds) { 0278 queryAncestors << colId.toLongLong(); 0279 } 0280 } 0281 0282 // Always query the given collections 0283 QList<qint64> queryCollections = queryAncestors; 0284 0285 if (recursive) { 0286 // Resolve subcollections if necessary 0287 queryCollections += SearchHelper::matchSubcollectionsByMimeType(queryAncestors, queryMimeTypes); 0288 } 0289 0290 // This happens if we try to search a virtual collection in recursive mode (because virtual collections are excluded from listCollectionsRecursive) 0291 if (queryCollections.isEmpty()) { 0292 qCDebug(AKONADISERVER_SEARCH_LOG) << "No collections to search, you're probably trying to search a virtual collection."; 0293 return; 0294 } 0295 0296 // Query all plugins for search results 0297 const QByteArray id = "searchUpdate-" + QByteArray::number(QDateTime::currentDateTimeUtc().toSecsSinceEpoch()); 0298 SearchRequest request(id, *this, mAgentSearchManager); 0299 request.setCollections(queryCollections); 0300 request.setMimeTypes(queryMimeTypes); 0301 request.setQuery(collection.queryString()); 0302 request.setRemoteSearch(remoteSearch); 0303 request.setStoreResults(true); 0304 request.setProperty("SearchCollection", QVariant::fromValue(collection)); 0305 connect(&request, &SearchRequest::resultsAvailable, this, &SearchManager::searchUpdateResultsAvailable); 0306 request.exec(); // blocks until all searches are done 0307 0308 const QSet<qint64> results = request.results(); 0309 0310 // Get all items in the collection 0311 QueryBuilder qb(CollectionPimItemRelation::tableName()); 0312 qb.addColumn(CollectionPimItemRelation::rightColumn()); 0313 qb.addValueCondition(CollectionPimItemRelation::leftColumn(), Query::Equals, collection.id()); 0314 if (!qb.exec()) { 0315 return; 0316 } 0317 0318 Transaction transaction(DataStore::self(), QStringLiteral("UPDATE SEARCH")); 0319 0320 // Unlink all items that were not in search results from the collection 0321 QVariantList toRemove; 0322 while (qb.query().next()) { 0323 const qint64 id = qb.query().value(0).toLongLong(); 0324 if (!results.contains(id)) { 0325 toRemove << id; 0326 Collection::removePimItem(collection.id(), id); 0327 } 0328 } 0329 0330 if (!transaction.commit()) { 0331 return; 0332 } 0333 0334 if (!toRemove.isEmpty()) { 0335 SelectQueryBuilder<PimItem> qb; 0336 qb.addValueCondition(PimItem::idFullColumnName(), Query::In, toRemove); 0337 if (!qb.exec()) { 0338 return; 0339 } 0340 0341 const QList<PimItem> removedItems = qb.result(); 0342 DataStore::self()->notificationCollector()->itemsUnlinked(removedItems, collection); 0343 } 0344 0345 qCInfo(AKONADISERVER_SEARCH_LOG) << "Search update for collection" << collection.name() << "(" << collection.id() << ") finished:" 0346 << "all results: " << results.count() << ", removed results:" << toRemove.count(); 0347 } 0348 0349 void SearchManager::searchUpdateResultsAvailable(const QSet<qint64> &results) 0350 { 0351 const auto collection = sender()->property("SearchCollection").value<Collection>(); 0352 qCDebug(AKONADISERVER_SEARCH_LOG) << "searchUpdateResultsAvailable" << collection.id() << results.count() << "results"; 0353 0354 QSet<qint64> newMatches = results; 0355 QSet<qint64> existingMatches; 0356 { 0357 QueryBuilder qb(CollectionPimItemRelation::tableName()); 0358 qb.addColumn(CollectionPimItemRelation::rightColumn()); 0359 qb.addValueCondition(CollectionPimItemRelation::leftColumn(), Query::Equals, collection.id()); 0360 if (!qb.exec()) { 0361 return; 0362 } 0363 0364 while (qb.query().next()) { 0365 const qint64 id = qb.query().value(0).toLongLong(); 0366 if (newMatches.contains(id)) { 0367 existingMatches << id; 0368 } 0369 } 0370 } 0371 0372 qCDebug(AKONADISERVER_SEARCH_LOG) << "Got" << newMatches.count() << "results, out of which" << existingMatches.count() << "are already in the collection"; 0373 0374 newMatches = newMatches - existingMatches; 0375 if (newMatches.isEmpty()) { 0376 qCDebug(AKONADISERVER_SEARCH_LOG) << "Added results: 0 (fast path)"; 0377 return; 0378 } 0379 0380 Transaction transaction(DataStore::self(), QStringLiteral("PUSH SEARCH RESULTS"), !DataStore::self()->inTransaction()); 0381 0382 // First query all the IDs we got from search plugin/agent against the DB. 0383 // This will remove IDs that no longer exist in the DB. 0384 constexpr int maximumParametersSize = 1000; 0385 QVariantList newMatchesVariant; 0386 newMatchesVariant.reserve(maximumParametersSize); 0387 QList<PimItem> items; 0388 0389 for (qint64 id : std::as_const(newMatches)) { 0390 newMatchesVariant << id; 0391 if (newMatchesVariant.size() >= maximumParametersSize) { 0392 SelectQueryBuilder<PimItem> qb; 0393 qb.addValueCondition(PimItem::idFullColumnName(), Query::In, newMatchesVariant); 0394 if (!qb.exec()) { 0395 return; 0396 } 0397 0398 items << qb.result(); 0399 0400 newMatchesVariant.clear(); 0401 } 0402 } 0403 0404 if (!newMatchesVariant.isEmpty()) { 0405 SelectQueryBuilder<PimItem> qb; 0406 qb.addValueCondition(PimItem::idFullColumnName(), Query::In, newMatchesVariant); 0407 if (!qb.exec()) { 0408 return; 0409 } 0410 0411 items << qb.result(); 0412 } 0413 0414 if (items.count() != newMatches.count()) { 0415 qCDebug(AKONADISERVER_SEARCH_LOG) << "Search backend returned" << (newMatches.count() - items.count()) << "results that no longer exist in Akonadi."; 0416 qCDebug(AKONADISERVER_SEARCH_LOG) << "Please reindex collection" << collection.id(); 0417 // TODO: Request the reindexing directly from here 0418 } 0419 0420 if (items.isEmpty()) { 0421 qCDebug(AKONADISERVER_SEARCH_LOG) << "Added results: 0 (no existing result)"; 0422 return; 0423 } 0424 0425 for (const auto &item : items) { 0426 Collection::addPimItem(collection.id(), item.id()); 0427 } 0428 0429 if (!transaction.commit()) { 0430 qCWarning(AKONADISERVER_SEARCH_LOG) << "Failed to commit search results transaction"; 0431 return; 0432 } 0433 0434 DataStore::self()->notificationCollector()->itemsLinked(items, collection); 0435 // Force collector to dispatch the notification now 0436 DataStore::self()->notificationCollector()->dispatchNotifications(); 0437 0438 qCDebug(AKONADISERVER_SEARCH_LOG) << "Added results:" << items.count(); 0439 } 0440 0441 #include "moc_searchmanager.cpp"