1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 1999-2000 Waldo Bastian <bastian@kde.org>
4 SPDX-FileCopyrightText: 2005-2009 David Faure <faure@kde.org>
5 SPDX-FileCopyrightText: 2008 Hamish Rodda <rodda@kde.org>
6 SPDX-FileCopyrightText: 2020-2022 Harald Sitter <sitter@kde.org>
7
8 SPDX-License-Identifier: LGPL-2.0-only
9*/
10
11#include "ksycoca.h"
12#include "ksycoca_p.h"
13#include "ksycocafactory_p.h"
14#include "ksycocatype.h"
15#include "ksycocautils_p.h"
16#include "sycocadebug.h"
17#include <KConfigGroup>
18#include <KSandbox>
19#include <KSharedConfig>
20
21#include <QCoreApplication>
22#include <QDataStream>
23#include <QFile>
24#include <QFileInfo>
25#include <QMetaMethod>
26#include <QStandardPaths>
27#include <QThread>
28#include <QThreadStorage>
29
30#include <QCryptographicHash>
31#include <fcntl.h>
32#include <kmimetypefactory_p.h>
33#include <kservicefactory_p.h>
34#include <kservicegroupfactory_p.h>
35
36#include "kbuildsycoca_p.h"
37#include "ksycocadevices_p.h"
38
39#ifdef Q_OS_UNIX
40#include <sys/time.h>
41#include <utime.h>
42#endif
43
44/**
45 * Sycoca file version number.
46 * If the existing file is outdated, it will not get read
47 * but instead we'll regenerate a new one.
48 * However running apps should still be able to read it, so
49 * only add to the data, never remove/modify.
50 */
51#define KSYCOCA_VERSION 306
52
53#if HAVE_MADVISE || HAVE_MMAP
54#include <sys/mman.h> // This #include was checked when looking for posix_madvise
55#endif
56
57#ifndef MAP_FAILED
58#define MAP_FAILED ((void *)-1)
59#endif
60
61QDataStream &operator>>(QDataStream &in, KSycocaHeader &h)
62{
63 in >> h.prefixes >> h.timeStamp >> h.language >> h.updateSignature;
64 return in;
65}
66
67// The following limitations are in place:
68// Maximum length of a single string: 8192 bytes
69// Maximum length of a string list: 1024 strings
70// Maximum number of entries: 8192
71//
72// The purpose of these limitations is to limit the impact
73// of database corruption.
74
75Q_DECLARE_OPERATORS_FOR_FLAGS(KSycocaPrivate::BehaviorsIfNotFound)
76
77KSycocaPrivate::KSycocaPrivate(KSycoca *qq)
78 : databaseStatus(DatabaseNotOpen)
79 , readError(false)
80 , timeStamp(0)
81 , m_databasePath()
82 , updateSig(0)
83 , m_fileWatcher(new KDirWatch)
84 , m_haveListeners(false)
85 , q(qq)
86 , sycoca_size(0)
87 , sycoca_mmap(nullptr)
88 , m_mmapFile(nullptr)
89 , m_device(nullptr)
90 , m_mimeTypeFactory(nullptr)
91 , m_serviceFactory(nullptr)
92 , m_serviceGroupFactory(nullptr)
93{
94#ifdef Q_OS_WIN
95 /*
96 on windows we use KMemFile (QSharedMemory) to avoid problems
97 with mmap (can't delete a mmap'd file)
98 */
99 m_sycocaStrategy = StrategyMemFile;
100#else
101 m_sycocaStrategy = StrategyMmap;
102#endif
103 KConfigGroup config(KSharedConfig::openConfig(), QStringLiteral("KSycoca"));
104 setStrategyFromString(config.readEntry(key: "strategy"));
105}
106
107void KSycocaPrivate::setStrategyFromString(const QString &strategy)
108{
109 if (strategy == QLatin1String("mmap")) {
110 m_sycocaStrategy = StrategyMmap;
111 } else if (strategy == QLatin1String("file")) {
112 m_sycocaStrategy = StrategyFile;
113 } else if (strategy == QLatin1String("sharedmem")) {
114 m_sycocaStrategy = StrategyMemFile;
115 } else if (!strategy.isEmpty()) {
116 qCWarning(SYCOCA) << "Unknown sycoca strategy:" << strategy;
117 }
118}
119
120bool KSycocaPrivate::tryMmap()
121{
122#if HAVE_MMAP
123 Q_ASSERT(!m_databasePath.isEmpty());
124 m_mmapFile = new QFile(m_databasePath);
125 const bool canRead = m_mmapFile->open(flags: QIODevice::ReadOnly);
126 Q_ASSERT(canRead);
127 if (!canRead) {
128 return false;
129 }
130 fcntl(fd: m_mmapFile->handle(), F_SETFD, FD_CLOEXEC);
131 sycoca_size = m_mmapFile->size();
132 void *mmapRet = mmap(addr: nullptr, len: sycoca_size, PROT_READ, MAP_SHARED, fd: m_mmapFile->handle(), offset: 0);
133 /* POSIX mandates only MAP_FAILED, but we are paranoid so check for
134 null pointer too. */
135 if (mmapRet == MAP_FAILED || mmapRet == nullptr) {
136 qCDebug(SYCOCA).nospace() << "mmap failed. (length = " << sycoca_size << ")";
137 sycoca_mmap = nullptr;
138 return false;
139 } else {
140 sycoca_mmap = static_cast<const char *>(mmapRet);
141#if HAVE_MADVISE
142 (void)posix_madvise(addr: mmapRet, len: sycoca_size, POSIX_MADV_WILLNEED);
143#endif // HAVE_MADVISE
144 return true;
145 }
146#else
147 return false;
148#endif // HAVE_MMAP
149}
150
151int KSycoca::version()
152{
153 return KSYCOCA_VERSION;
154}
155
156class KSycocaSingleton
157{
158public:
159 KSycocaSingleton()
160 {
161 }
162 ~KSycocaSingleton()
163 {
164 }
165
166 bool hasSycoca() const
167 {
168 return m_threadSycocas.hasLocalData();
169 }
170 KSycoca *sycoca()
171 {
172 if (!m_threadSycocas.hasLocalData()) {
173 m_threadSycocas.setLocalData(new KSycoca);
174 }
175 return m_threadSycocas.localData();
176 }
177 void setSycoca(KSycoca *s)
178 {
179 m_threadSycocas.setLocalData(s);
180 }
181
182private:
183 QThreadStorage<KSycoca *> m_threadSycocas;
184};
185
186Q_GLOBAL_STATIC(KSycocaSingleton, ksycocaInstance)
187
188QString KSycocaPrivate::findDatabase()
189{
190 Q_ASSERT(databaseStatus == DatabaseNotOpen);
191
192 const QString path = KSycoca::absoluteFilePath();
193 const QFileInfo info(path);
194 if (info.isReadable()) {
195 if (m_haveListeners && m_fileWatcher) {
196 m_fileWatcher->addFile(file: path);
197 }
198 return path;
199 }
200 // Let's be notified when it gets created - by another process or by ourselves
201 if (m_fileWatcher) {
202 m_fileWatcher->addFile(file: path);
203 }
204 return QString();
205}
206
207// Read-only constructor
208// One instance per thread
209KSycoca::KSycoca()
210 : d(new KSycocaPrivate(this))
211{
212 if (d->m_fileWatcher) {
213 // We always delete and recreate the DB, so KDirWatch normally emits created
214 connect(sender: d->m_fileWatcher.get(), signal: &KDirWatch::created, context: this, slot: [this]() {
215 d->slotDatabaseChanged();
216 });
217 // In some cases, KDirWatch only thinks the file was modified though
218 connect(sender: d->m_fileWatcher.get(), signal: &KDirWatch::dirty, context: this, slot: [this]() {
219 d->slotDatabaseChanged();
220 });
221 }
222}
223
224bool KSycocaPrivate::openDatabase()
225{
226 Q_ASSERT(databaseStatus == DatabaseNotOpen);
227
228 delete m_device;
229 m_device = nullptr;
230
231 if (m_databasePath.isEmpty()) {
232 m_databasePath = findDatabase();
233 }
234
235 bool result = true;
236 if (!m_databasePath.isEmpty()) {
237 static bool firstTime = true;
238 if (firstTime) {
239 firstTime = false;
240 if (KSandbox::isFlatpak()) {
241 // We're running inside flatpak, which sets all times to 1970
242 // So the first very time, don't use an existing database, recreate it
243 qCDebug(SYCOCA) << "flatpak detected, ignoring" << m_databasePath;
244 return false;
245 }
246 }
247
248 qCDebug(SYCOCA) << "Opening ksycoca from" << m_databasePath;
249 m_dbLastModified = QFileInfo(m_databasePath).lastModified();
250 result = checkVersion();
251 } else { // No database file
252 // qCDebug(SYCOCA) << "Could not open ksycoca";
253 result = false;
254 }
255 return result;
256}
257
258KSycocaAbstractDevice *KSycocaPrivate::device()
259{
260 if (m_device) {
261 return m_device;
262 }
263
264 KSycocaAbstractDevice *device = m_device;
265 Q_ASSERT(!m_databasePath.isEmpty());
266#if HAVE_MMAP
267 if (m_sycocaStrategy == StrategyMmap && tryMmap()) {
268 device = new KSycocaMmapDevice(sycoca_mmap, sycoca_size);
269 if (!device->device()->open(mode: QIODevice::ReadOnly)) {
270 delete device;
271 device = nullptr;
272 }
273 }
274#endif
275#ifndef QT_NO_SHAREDMEMORY
276 if (!device && m_sycocaStrategy == StrategyMemFile) {
277 device = new KSycocaMemFileDevice(m_databasePath);
278 if (!device->device()->open(mode: QIODevice::ReadOnly)) {
279 delete device;
280 device = nullptr;
281 }
282 }
283#endif
284 if (!device) {
285 device = new KSycocaFileDevice(m_databasePath);
286 if (!device->device()->open(mode: QIODevice::ReadOnly)) {
287 qCWarning(SYCOCA) << "Couldn't open" << m_databasePath << "even though it is readable? Impossible.";
288 // delete device; device = 0; // this would crash in the return statement...
289 }
290 }
291 if (device) {
292 m_device = device;
293 }
294 return m_device;
295}
296
297QDataStream *&KSycocaPrivate::stream()
298{
299 if (!m_device) {
300 if (databaseStatus == DatabaseNotOpen) {
301 checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundRecreate);
302 }
303
304 device(); // create m_device
305 }
306
307 return m_device->stream();
308}
309
310void KSycocaPrivate::slotDatabaseChanged()
311{
312 qCDebug(SYCOCA) << QThread::currentThread() << "got a notifyDatabaseChanged signal";
313 // In case we have changed the database outselves, we have already notified the application
314 if (!m_dbLastModified.isValid() || m_dbLastModified != QFileInfo(m_databasePath).lastModified()) {
315 // KDirWatch tells us the database file changed
316 // We would have found out in the next call to ensureCacheValid(), but for
317 // now keep the call to closeDatabase, to help refcounting to 0 the old mmapped file earlier.
318 closeDatabase();
319 // Start monitoring the new file right away
320 m_databasePath = findDatabase();
321
322 // Now notify applications
323 Q_EMIT q->databaseChanged();
324 }
325}
326
327KMimeTypeFactory *KSycocaPrivate::mimeTypeFactory()
328{
329 if (!m_mimeTypeFactory) {
330 m_mimeTypeFactory = new KMimeTypeFactory(q);
331 }
332 return m_mimeTypeFactory;
333}
334
335KServiceFactory *KSycocaPrivate::serviceFactory()
336{
337 if (!m_serviceFactory) {
338 m_serviceFactory = new KServiceFactory(q);
339 }
340 return m_serviceFactory;
341}
342
343KServiceGroupFactory *KSycocaPrivate::serviceGroupFactory()
344{
345 if (!m_serviceGroupFactory) {
346 m_serviceGroupFactory = new KServiceGroupFactory(q);
347 }
348 return m_serviceGroupFactory;
349}
350
351// Read-write constructor - only for KBuildSycoca
352KSycoca::KSycoca(bool /* dummy */)
353 : d(new KSycocaPrivate(this))
354{
355}
356
357KSycoca *KSycoca::self()
358{
359 KSycoca *s = ksycocaInstance()->sycoca();
360 Q_ASSERT(s);
361 return s;
362}
363
364KSycoca::~KSycoca()
365{
366 d->closeDatabase();
367 delete d;
368 // if (ksycocaInstance.exists()
369 // && ksycocaInstance->self == this)
370 // ksycocaInstance->self = 0;
371}
372
373bool KSycoca::isAvailable() // TODO KF6: make it non-static (mostly useful for unittests)
374{
375 return self()->d->checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundDoNothing);
376}
377
378void KSycocaPrivate::closeDatabase()
379{
380 delete m_device;
381 m_device = nullptr;
382
383 // It is very important to delete all factories here
384 // since they cache information about the database file
385 // But other threads might be using them, so this class is
386 // refcounted, and deleted when the last thread is done with them
387 qDeleteAll(c: m_factories);
388 m_factories.clear();
389
390 m_mimeTypeFactory = nullptr;
391 m_serviceFactory = nullptr;
392 m_serviceGroupFactory = nullptr;
393
394#if HAVE_MMAP
395 if (sycoca_mmap) {
396 // Solaris has munmap(char*, size_t) and everything else should
397 // be happy with a char* for munmap(void*, size_t)
398 munmap(addr: const_cast<char *>(sycoca_mmap), len: sycoca_size);
399 sycoca_mmap = nullptr;
400 }
401 delete m_mmapFile;
402 m_mmapFile = nullptr;
403#endif
404
405 databaseStatus = DatabaseNotOpen;
406 m_databasePath.clear();
407 timeStamp = 0;
408}
409
410void KSycoca::addFactory(KSycocaFactory *factory)
411{
412 d->addFactory(factory);
413}
414
415QDataStream *KSycoca::findEntry(int offset, KSycocaType &type)
416{
417 QDataStream *str = stream();
418 Q_ASSERT(str);
419 // qCDebug(SYCOCA) << QString("KSycoca::_findEntry(offset=%1)").arg(offset,8,16);
420 str->device()->seek(pos: offset);
421 qint32 aType;
422 *str >> aType;
423 type = KSycocaType(aType);
424 // qCDebug(SYCOCA) << QString("KSycoca::found type %1").arg(aType);
425 return str;
426}
427
428KSycocaFactoryList *KSycoca::factories()
429{
430 return d->factories();
431}
432
433// Warning, checkVersion rewinds to the beginning of stream().
434bool KSycocaPrivate::checkVersion()
435{
436 QDataStream *m_str = device()->stream();
437 Q_ASSERT(m_str);
438 m_str->device()->seek(pos: 0);
439 qint32 aVersion;
440 *m_str >> aVersion;
441 if (aVersion < KSYCOCA_VERSION) {
442 qCDebug(SYCOCA) << "Found version" << aVersion << ", expecting version" << KSYCOCA_VERSION << "or higher.";
443 databaseStatus = BadVersion;
444 return false;
445 } else {
446 databaseStatus = DatabaseOK;
447 return true;
448 }
449}
450
451// If it returns true, we have a valid database and the stream has rewinded to the beginning
452// and past the version number.
453bool KSycocaPrivate::checkDatabase(BehaviorsIfNotFound ifNotFound)
454{
455 if (databaseStatus == DatabaseOK) {
456 if (checkVersion()) { // we know the version is ok, but we must rewind the stream anyway
457 return true;
458 }
459 }
460
461 closeDatabase(); // close the dummy one
462
463 // Check if new database already available
464 if (openDatabase()) {
465 // Database exists, and version is ok, we can read it.
466
467 if (qAppName() != QLatin1String(KBUILDSYCOCA_EXENAME) && ifNotFound != IfNotFoundDoNothing) {
468 // Ensure it's up-to-date, rebuild if needed
469 checkDirectories();
470
471 // Don't check again for some time
472 m_lastCheck.start();
473 }
474
475 return true;
476 }
477
478 if (ifNotFound & IfNotFoundRecreate) {
479 return buildSycoca();
480 }
481
482 return false;
483}
484
485QDataStream *KSycoca::findFactory(KSycocaFactoryId id)
486{
487 // Ensure we have a valid database (right version, and rewinded to beginning)
488 if (!d->checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundRecreate)) {
489 return nullptr;
490 }
491
492 QDataStream *str = stream();
493 Q_ASSERT(str);
494
495 qint32 aId;
496 qint32 aOffset;
497 while (true) {
498 *str >> aId;
499 if (aId == 0) {
500 qCWarning(SYCOCA) << "Error, KSycocaFactory (id =" << int(id) << ") not found!";
501 break;
502 }
503 *str >> aOffset;
504 if (aId == id) {
505 // qCDebug(SYCOCA) << "KSycoca::findFactory(" << id << ") offset " << aOffset;
506 str->device()->seek(pos: aOffset);
507 return str;
508 }
509 }
510 return nullptr;
511}
512
513bool KSycoca::needsRebuild()
514{
515 return d->needsRebuild();
516}
517
518KSycocaHeader KSycocaPrivate::readSycocaHeader()
519{
520 KSycocaHeader header;
521 // do not try to launch kbuildsycoca from here; this code is also called by kbuildsycoca.
522 if (!checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundDoNothing)) {
523 return header;
524 }
525 QDataStream *str = stream();
526 qint64 oldPos = str->device()->pos();
527
528 Q_ASSERT(str);
529 qint32 aId;
530 qint32 aOffset;
531 // skip factories offsets
532 while (true) {
533 *str >> aId;
534 if (aId) {
535 *str >> aOffset;
536 } else {
537 break; // just read 0
538 }
539 }
540 // We now point to the header
541 QStringList directoryList;
542 *str >> header >> directoryList;
543 allResourceDirs.clear();
544 for (int i = 0; i < directoryList.count(); ++i) {
545 qint64 mtime;
546 *str >> mtime;
547 allResourceDirs.insert(key: directoryList.at(i), value: mtime);
548 }
549
550 QStringList fileList;
551 *str >> fileList;
552 extraFiles.clear();
553 for (const auto &fileName : std::as_const(t&: fileList)) {
554 qint64 mtime;
555 *str >> mtime;
556 extraFiles.insert(key: fileName, value: mtime);
557 }
558
559 str->device()->seek(pos: oldPos);
560
561 timeStamp = header.timeStamp;
562
563 // for the useless public accessors. KF6: remove these two lines, the accessors and the vars.
564 language = header.language;
565 updateSig = header.updateSignature;
566
567 return header;
568}
569
570class TimestampChecker
571{
572public:
573 TimestampChecker()
574 : m_now(QDateTime::currentDateTime())
575 {
576 }
577
578 // Check times of last modification of all directories on which ksycoca depends,
579 // If none of them is newer than the mtime we stored for that directory at the
580 // last rebuild, this means that there's no need to rebuild ksycoca.
581 bool checkDirectoriesTimestamps(const QMap<QString, qint64> &dirs) const
582 {
583 Q_ASSERT(!dirs.isEmpty());
584 // qCDebug(SYCOCA) << "checking file timestamps";
585 for (auto it = dirs.begin(); it != dirs.end(); ++it) {
586 const QString dir = it.key();
587 const qint64 lastStamp = it.value();
588
589 auto visitor = [&](const QFileInfo &fi) {
590 const QDateTime mtime = fi.lastModified();
591 if (mtime.toMSecsSinceEpoch() > lastStamp) {
592 if (mtime > m_now) {
593 qCDebug(SYCOCA) << fi.filePath() << "has a modification time in the future" << mtime;
594 }
595 qCDebug(SYCOCA) << "dir timestamp changed:" << fi.filePath() << mtime << ">" << QDateTime::fromMSecsSinceEpoch(msecs: lastStamp);
596 // no need to continue search
597 return false;
598 }
599
600 return true;
601 };
602
603 if (!KSycocaUtilsPrivate::visitResourceDirectory(dirname: dir, visitor)) {
604 return false;
605 }
606 }
607 return true;
608 }
609
610 bool checkFilesTimestamps(const QMap<QString, qint64> &files) const
611 {
612 for (auto it = files.begin(); it != files.end(); ++it) {
613 const QString fileName = it.key();
614 const qint64 lastStamp = it.value();
615
616 QFileInfo fi(fileName);
617 if (!fi.exists()) {
618 return false;
619 }
620 const QDateTime mtime = fi.lastModified();
621 if (mtime.toMSecsSinceEpoch() > lastStamp) {
622 if (mtime > m_now) {
623 qCDebug(SYCOCA) << fi.filePath() << "has a modification time in the future" << mtime;
624 }
625 qCDebug(SYCOCA) << "file timestamp changed:" << fi.filePath() << mtime << ">" << QDateTime::fromMSecsSinceEpoch(msecs: lastStamp);
626 return false;
627 }
628 }
629 return true;
630 }
631
632private:
633 QDateTime m_now;
634};
635
636void KSycocaPrivate::checkDirectories()
637{
638 if (needsRebuild()) {
639 buildSycoca();
640 }
641}
642
643bool KSycocaPrivate::needsRebuild()
644{
645 // In case it is not open, it might be due to another process/thread having rebuild it. Thus we read the header for both the not open and ok state
646 if (!timeStamp && databaseStatus != BadVersion) {
647 (void)readSycocaHeader();
648 }
649 // these days timeStamp is really a "bool headerFound", the value itself doesn't matter...
650 // KF6: replace it with bool.
651 const auto timestampChecker = TimestampChecker();
652 bool ret = timeStamp != 0
653 && (!timestampChecker.checkDirectoriesTimestamps(dirs: allResourceDirs) //
654 || !timestampChecker.checkFilesTimestamps(files: extraFiles));
655 if (ret) {
656 return true;
657 }
658 auto files = KBuildSycoca::factoryExtraFiles();
659 // ensure files are ordered so next comparison works
660 files.sort();
661 // to cover cases when extra files were added
662 return extraFiles.keys() != files;
663}
664
665bool KSycocaPrivate::buildSycoca()
666{
667 KBuildSycoca builder;
668 if (!builder.recreate()) {
669 return false; // error
670 }
671
672 closeDatabase(); // close the dummy one
673
674 // Ok, the new database should be here now, open it.
675 if (!openDatabase()) {
676 qCDebug(SYCOCA) << "Still no database...";
677 return false;
678 }
679 Q_EMIT q->databaseChanged();
680 return true;
681}
682
683QString KSycoca::absoluteFilePath()
684{
685 const QStringList paths = QStandardPaths::standardLocations(type: QStandardPaths::GenericDataLocation);
686 QString suffix = QLatin1Char('_') + QLocale().bcp47Name();
687
688 const QByteArray ksycoca_env = qgetenv(varName: "KDESYCOCA");
689 if (ksycoca_env.isEmpty()) {
690 const QByteArray pathHash = QCryptographicHash::hash(data: paths.join(sep: QLatin1Char(':')).toUtf8(), method: QCryptographicHash::Sha1);
691 suffix += QLatin1Char('_') + QString::fromLatin1(ba: pathHash.toBase64());
692 suffix.replace(before: QLatin1Char('/'), after: QLatin1Char('_'));
693#ifdef Q_OS_WIN
694 suffix.replace(QLatin1Char(':'), QLatin1Char('_'));
695#endif
696 const QString fileName = QLatin1String("ksycoca6") + suffix;
697 return QStandardPaths::writableLocation(type: QStandardPaths::GenericCacheLocation) + QLatin1Char('/') + fileName;
698 } else {
699 return QFile::decodeName(localFileName: ksycoca_env);
700 }
701}
702
703QStringList KSycoca::allResourceDirs()
704{
705 if (!d->timeStamp) {
706 (void)d->readSycocaHeader();
707 }
708 return d->allResourceDirs.keys();
709}
710
711void KSycoca::flagError()
712{
713 qCWarning(SYCOCA) << "ERROR: KSycoca database corruption!";
714 KSycoca *sycoca = self();
715 if (sycoca->d->readError) {
716 return;
717 }
718 sycoca->d->readError = true;
719 if (qAppName() != QLatin1String(KBUILDSYCOCA_EXENAME) && !sycoca->isBuilding()) {
720 // Rebuild the damned thing.
721 KBuildSycoca builder;
722 (void)builder.recreate();
723 }
724}
725
726bool KSycoca::isBuilding()
727{
728 return false;
729}
730
731void KSycoca::disableAutoRebuild()
732{
733 ksycocaInstance->sycoca()->d->m_fileWatcher = nullptr;
734}
735
736QDataStream *&KSycoca::stream()
737{
738 return d->stream();
739}
740
741void KSycoca::connectNotify(const QMetaMethod &signal)
742{
743 if (signal.name() == "databaseChanged" && !d->m_haveListeners) {
744 d->m_haveListeners = true;
745 if (d->m_databasePath.isEmpty()) {
746 d->m_databasePath = d->findDatabase();
747 } else if (d->m_fileWatcher) {
748 d->m_fileWatcher->addFile(file: d->m_databasePath);
749 }
750 }
751}
752
753void KSycoca::clearCaches()
754{
755 if (ksycocaInstance.exists() && ksycocaInstance()->hasSycoca()) {
756 ksycocaInstance()->sycoca()->d->closeDatabase();
757 }
758}
759
760extern KSERVICE_EXPORT int ksycoca_ms_between_checks;
761KSERVICE_EXPORT int ksycoca_ms_between_checks = 1500;
762
763void KSycoca::ensureCacheValid()
764{
765 if (qAppName() == QLatin1String(KBUILDSYCOCA_EXENAME)) {
766 return;
767 }
768
769 if (d->databaseStatus != KSycocaPrivate::DatabaseOK) {
770 if (!d->checkDatabase(ifNotFound: KSycocaPrivate::IfNotFoundRecreate)) {
771 return;
772 }
773 }
774
775 if (d->m_lastCheck.isValid() && d->m_lastCheck.elapsed() < ksycoca_ms_between_checks) {
776 return;
777 }
778 d->m_lastCheck.start();
779
780 // Check if the file on disk was modified since we last checked it.
781 QFileInfo info(d->m_databasePath);
782 if (info.lastModified() == d->m_dbLastModified) {
783 // Check if the watched directories were modified, then the cache needs a rebuild.
784 d->checkDirectories();
785 return;
786 }
787
788 // Close the database and forget all about what we knew.
789 // The next call to any public method will recreate
790 // everything that's needed.
791 d->closeDatabase();
792}
793
794void KSycoca::setupTestMenu()
795{
796 const QByteArray content = R"(<?xml version="1.0"?>
797<!DOCTYPE Menu PUBLIC "-//freedesktop//DTD Menu 1.0//EN" "http://www.freedesktop.org/standards/menu-spec/menu-1.0.dtd">
798<Menu>
799 <Name>Applications</Name>
800 <Directory>Applications.directory</Directory>
801 <DefaultAppDirs/>
802 <DefaultDirectoryDirs/>
803 <MergeDir>applications-merged</MergeDir>
804 <LegacyDir>/usr/share/applnk</LegacyDir>
805 <DefaultLayout>
806 <Merge type="menus"/>
807 <Merge type="files"/>
808 <Separator/>
809 <Menuname>More</Menuname>
810 </DefaultLayout>
811</Menu>
812)";
813
814 const QString destDir = QStandardPaths::writableLocation(type: QStandardPaths::GenericConfigLocation) + QLatin1String("/menus");
815 QDir(destDir).mkpath(QStringLiteral("."));
816 QFile output(destDir + QLatin1String("/applications.menu"));
817 output.open(flags: QIODevice::ReadWrite | QIODevice::Truncate);
818 output.write(data: content);
819}
820
821#include "moc_ksycoca.cpp"
822

source code of kservice/src/sycoca/ksycoca.cpp