1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 1999 David Faure <faure@kde.org>
4 SPDX-FileCopyrightText: 2000 Waldo Bastian <bastian@kde.org>
5
6 SPDX-License-Identifier: LGPL-2.0-only
7*/
8
9#include "kded.h"
10#include "kded_debug.h"
11#include "kded_version.h"
12#include "kdedadaptor.h"
13
14#include <KCrash>
15
16#include <qplatformdefs.h>
17
18#include <QApplication>
19#include <QCommandLineParser>
20#include <QDir>
21#include <QLoggingCategory>
22#include <QProcess>
23
24#include <QDBusConnection>
25#include <QDBusConnectionInterface>
26#include <QDBusServiceWatcher>
27
28#include <KConfigGroup>
29#include <KDBusService>
30#include <KDirWatch>
31#include <KPluginFactory>
32#include <KPluginMetaData>
33#include <KSharedConfig>
34
35#include <memory>
36
37Kded *Kded::_self = nullptr;
38
39static bool delayedCheck;
40static bool bCheckSycoca;
41static bool bCheckUpdates;
42
43#ifdef Q_DBUS_EXPORT
44extern Q_DBUS_EXPORT void qDBusAddSpyHook(void (*)(const QDBusMessage &));
45#else
46extern QDBUS_EXPORT void qDBusAddSpyHook(void (*)(const QDBusMessage &));
47#endif
48
49static void runKonfUpdate()
50{
51 int ret = QProcess::execute(QStringLiteral(KCONF_UPDATE_EXE), arguments: QStringList());
52 if (ret != 0) {
53 qCWarning(KDED) << KCONF_UPDATE_EXE << "returned" << ret;
54 }
55}
56
57Kded::Kded()
58 : m_pDirWatch(new KDirWatch(this))
59 , m_pTimer(new QTimer(this))
60 , m_needDelayedCheck(false)
61{
62 _self = this;
63
64 m_serviceWatcher = new QDBusServiceWatcher(this);
65 m_serviceWatcher->setConnection(QDBusConnection::sessionBus());
66 m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
67 QObject::connect(sender: m_serviceWatcher, signal: &QDBusServiceWatcher::serviceUnregistered, context: this, slot: &Kded::slotApplicationRemoved);
68
69 new KdedAdaptor(this);
70
71 QDBusConnection session = QDBusConnection::sessionBus();
72 session.registerObject(QStringLiteral("/kbuildsycoca"), object: this);
73 session.registerObject(QStringLiteral("/kded"), object: this);
74
75 m_pTimer->setSingleShot(true);
76 connect(sender: m_pTimer, signal: &QTimer::timeout, context: this, slot: static_cast<void (Kded::*)()>(&Kded::recreate));
77}
78
79Kded::~Kded()
80{
81 _self = nullptr;
82 m_pTimer->stop();
83
84 for (auto it = m_modules.cbegin(); it != m_modules.cend(); ++it) {
85 delete *it;
86 }
87 m_modules.clear();
88}
89
90// on-demand module loading
91// this function is called by the D-Bus message processing function before
92// calls are delivered to objects
93void Kded::messageFilter(const QDBusMessage &message)
94{
95 // This happens when kded goes down and some modules try to clean up.
96 if (!self()) {
97 return;
98 }
99
100 QString obj = KDEDModule::moduleForMessage(message);
101 if (obj.isEmpty() || obj == QLatin1String("ksycoca")) {
102 return;
103 }
104
105 if (self()->m_dontLoad.value(key: obj, defaultValue: nullptr)) {
106 return;
107 }
108
109 self()->loadModule(obj, onDemand: true);
110}
111
112static int phaseForModule(const KPluginMetaData &module)
113{
114 return module.value(QStringLiteral("X-KDE-Kded-phase"), defaultValue: 2);
115}
116
117QList<KPluginMetaData> Kded::availableModules() const
118{
119 QList<KPluginMetaData> plugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/kded"));
120 QSet<QString> moduleIds;
121 for (const KPluginMetaData &md : std::as_const(t&: plugins)) {
122 moduleIds.insert(value: md.pluginId());
123 }
124 return plugins;
125}
126
127static KPluginMetaData findModule(const QString &id)
128{
129 KPluginMetaData module(QStringLiteral("kf6/kded/") + id);
130 if (module.isValid()) {
131 return module;
132 }
133 qCWarning(KDED) << "could not find kded module with id" << id;
134 return KPluginMetaData();
135}
136
137void Kded::initModules()
138{
139 m_dontLoad.clear();
140
141 bool kde_running = !qEnvironmentVariableIsEmpty(varName: "KDE_FULL_SESSION");
142 if (kde_running) {
143 // not the same user like the one running the session (most likely we're run via sudo or something)
144 const QByteArray sessionUID = qgetenv(varName: "KDE_SESSION_UID");
145 if (!sessionUID.isEmpty() && uid_t(sessionUID.toInt()) != getuid()) {
146 kde_running = false;
147 }
148 // not the same kde version as the current desktop
149 const QByteArray kdeSession = qgetenv(varName: "KDE_SESSION_VERSION");
150 if (kdeSession.toInt() != 6) {
151 kde_running = false;
152 }
153 }
154
155 // Preload kded modules.
156 const QList<KPluginMetaData> kdedModules = availableModules();
157 for (const KPluginMetaData &module : kdedModules) {
158 // Should the service load on startup?
159 const bool autoload = isModuleAutoloaded(module);
160 if (!platformSupportsModule(module)) {
161 continue;
162 }
163
164 // see ksmserver's README for description of the phases
165 bool prevent_autoload = false;
166 switch (phaseForModule(module)) {
167 case 0: // always autoload
168 break;
169 case 1: // autoload only in KDE
170 if (!kde_running) {
171 prevent_autoload = true;
172 }
173 break;
174 case 2: // autoload delayed, only in KDE
175 default:
176 if (!kde_running) {
177 prevent_autoload = true;
178 }
179 break;
180 }
181
182 // Load the module if necessary and allowed
183 if (autoload && !prevent_autoload) {
184 if (!loadModule(module, onDemand: false)) {
185 continue;
186 }
187 }
188
189 // Remember if the module is allowed to load on demand
190 bool loadOnDemand = isModuleLoadedOnDemand(module);
191 if (!loadOnDemand) {
192 noDemandLoad(obj: module.pluginId());
193 }
194
195 // In case of reloading the configuration it is possible for a module
196 // to run even if it is now allowed to. Stop it then.
197 if (!loadOnDemand && !autoload) {
198 unloadModule(obj: module.pluginId());
199 }
200 }
201}
202
203void Kded::noDemandLoad(const QString &obj)
204{
205 m_dontLoad.insert(key: obj, value: this);
206}
207
208void Kded::setModuleAutoloading(const QString &obj, bool autoload)
209{
210 KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("kded5rc"));
211 // Ensure the service exists.
212 KPluginMetaData module = findModule(id: obj);
213 if (!module.isValid()) {
214 return;
215 }
216 KConfigGroup cg(config, QStringLiteral("Module-").append(s: module.pluginId()));
217 cg.writeEntry(key: "autoload", value: autoload);
218 cg.sync();
219}
220
221bool Kded::isModuleAutoloaded(const QString &obj) const
222{
223 return isModuleAutoloaded(module: findModule(id: obj));
224}
225
226bool Kded::isModuleAutoloaded(const KPluginMetaData &module) const
227{
228 if (!module.isValid()) {
229 return false;
230 }
231 KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("kded5rc"));
232 bool autoload = module.value(QStringLiteral("X-KDE-Kded-autoload"), defaultValue: false);
233 KConfigGroup cg(config, QStringLiteral("Module-").append(s: module.pluginId()));
234 autoload = cg.readEntry(key: "autoload", defaultValue: autoload);
235 return autoload;
236}
237
238bool Kded::platformSupportsModule(const KPluginMetaData &module) const
239{
240 const QStringList supportedPlatforms = module.value(QStringLiteral("X-KDE-OnlyShowOnQtPlatforms"), defaultValue: QStringList());
241
242 return supportedPlatforms.isEmpty() || supportedPlatforms.contains(qApp->platformName());
243}
244
245bool Kded::isModuleLoadedOnDemand(const QString &obj) const
246{
247 return isModuleLoadedOnDemand(module: findModule(id: obj));
248}
249
250bool Kded::isModuleLoadedOnDemand(const KPluginMetaData &module) const
251{
252 if (!module.isValid()) {
253 return false;
254 }
255 KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("kded5rc"));
256 return module.value(QStringLiteral("X-KDE-Kded-load-on-demand"), defaultValue: true);
257}
258
259KDEDModule *Kded::loadModule(const QString &obj, bool onDemand)
260{
261 // Make sure this method is only called with valid module names.
262 if (obj.contains(c: QLatin1Char('/'))) {
263 qCWarning(KDED) << "attempting to load invalid kded module name:" << obj;
264 return nullptr;
265 }
266 KDEDModule *module = m_modules.value(key: obj, defaultValue: nullptr);
267 if (module) {
268 return module;
269 }
270 return loadModule(module: findModule(id: obj), onDemand);
271}
272
273KDEDModule *Kded::loadModule(const KPluginMetaData &module, bool onDemand)
274{
275 if (!module.isValid() || module.fileName().isEmpty()) {
276 qCWarning(KDED) << "attempted to load an invalid module.";
277 return nullptr;
278 }
279 const QString moduleId = module.pluginId();
280 KDEDModule *oldModule = m_modules.value(key: moduleId, defaultValue: nullptr);
281 if (oldModule) {
282 qCDebug(KDED) << "kded module" << moduleId << "is already loaded.";
283 return oldModule;
284 }
285
286 if (onDemand) {
287 if (!module.value(QStringLiteral("X-KDE-Kded-load-on-demand"), defaultValue: true)) {
288 noDemandLoad(obj: moduleId);
289 return nullptr;
290 }
291 }
292
293 KDEDModule *kdedModule = nullptr;
294
295 auto factoryResult = KPluginFactory::loadFactory(data: module);
296 if (factoryResult) {
297 kdedModule = factoryResult.plugin->create<KDEDModule>(parent: this);
298 } else {
299 qCWarning(KDED).nospace() << "Could not load kded module " << moduleId << ":" << factoryResult.errorText << " (library path was:" << module.fileName()
300 << ")";
301 }
302
303 if (kdedModule) {
304 kdedModule->setModuleName(moduleId);
305 m_modules.insert(key: moduleId, value: kdedModule);
306 // m_libs.insert(moduleId, lib);
307 qCDebug(KDED) << "Successfully loaded module" << moduleId;
308 return kdedModule;
309 }
310 return nullptr;
311}
312
313bool Kded::unloadModule(const QString &obj)
314{
315 KDEDModule *module = m_modules.value(key: obj, defaultValue: nullptr);
316 if (!module) {
317 return false;
318 }
319 qCDebug(KDED) << "Unloading module" << obj;
320 m_modules.remove(key: obj);
321 delete module;
322 return true;
323}
324
325QStringList Kded::loadedModules()
326{
327 return m_modules.keys();
328}
329
330void Kded::slotApplicationRemoved(const QString &name)
331{
332#if 0 // see kdedmodule.cpp (KDED_OBJECTS)
333 foreach (KDEDModule *module, m_modules) {
334 module->removeAll(appId);
335 }
336#endif
337 m_serviceWatcher->removeWatchedService(service: name);
338 const QList<qlonglong> windowIds = m_windowIdList.value(key: name);
339 for (const auto id : windowIds) {
340 m_globalWindowIdList.remove(value: id);
341 for (KDEDModule *module : std::as_const(t&: m_modules)) {
342 Q_EMIT module->windowUnregistered(windowId: id);
343 }
344 }
345 m_windowIdList.remove(key: name);
346}
347
348void Kded::updateDirWatch()
349{
350 if (!bCheckUpdates) {
351 return;
352 }
353
354 delete m_pDirWatch;
355 m_pDirWatch = new KDirWatch(this);
356
357 QObject::connect(sender: m_pDirWatch, signal: &KDirWatch::dirty, context: this, slot: &Kded::update);
358 QObject::connect(sender: m_pDirWatch, signal: &KDirWatch::created, context: this, slot: &Kded::update);
359 QObject::connect(sender: m_pDirWatch, signal: &KDirWatch::deleted, context: this, slot: &Kded::dirDeleted);
360
361 // For each resource
362 for (const QString &dir : std::as_const(t&: m_allResourceDirs)) {
363 readDirectory(dir);
364 }
365
366 QStringList dataDirs = QStandardPaths::standardLocations(type: QStandardPaths::GenericDataLocation);
367 for (auto &dir : dataDirs) {
368 dir += QLatin1String("/icons");
369 if (!m_pDirWatch->contains(path: dir)) {
370 m_pDirWatch->addDir(path: dir, watchModes: KDirWatch::WatchDirOnly);
371 }
372 }
373}
374
375void Kded::updateResourceList()
376{
377 KSycoca::clearCaches();
378
379 if (!bCheckUpdates) {
380 return;
381 }
382
383 if (delayedCheck) {
384 return;
385 }
386
387 const QStringList dirs = KSycoca::self()->allResourceDirs();
388 // For each resource
389 for (const auto &dir : dirs) {
390 if (!m_allResourceDirs.contains(str: dir)) {
391 m_allResourceDirs.append(t: dir);
392 readDirectory(dir);
393 }
394 }
395}
396
397void Kded::recreate()
398{
399 recreate(initial: false);
400}
401
402void Kded::runDelayedCheck()
403{
404 if (m_needDelayedCheck) {
405 recreate(initial: false);
406 }
407 m_needDelayedCheck = false;
408}
409
410void Kded::recreate(bool initial)
411{
412 // Using KLauncher here is difficult since we might not have a
413 // database
414
415 if (!initial) {
416 updateDirWatch(); // Update tree first, to be sure to miss nothing.
417 KSycoca::self()->ensureCacheValid();
418 recreateDone();
419 } else {
420 if (!delayedCheck) {
421 updateDirWatch(); // this would search all the directories
422 }
423 if (bCheckSycoca) {
424 KSycoca::self()->ensureCacheValid();
425 }
426 recreateDone();
427 if (delayedCheck) {
428 // do a proper ksycoca check after a delay
429 QTimer::singleShot(interval: 60000, receiver: this, slot: &Kded::runDelayedCheck);
430 m_needDelayedCheck = true;
431 delayedCheck = false;
432 } else {
433 m_needDelayedCheck = false;
434 }
435 }
436}
437
438void Kded::recreateDone()
439{
440 updateResourceList();
441
442 initModules();
443}
444
445void Kded::dirDeleted(const QString &path)
446{
447 update(dir: path);
448}
449
450void Kded::update(const QString &path)
451{
452 if (path.endsWith(s: QLatin1String("/icons")) && m_pDirWatch->contains(path)) {
453 // If the dir was created or updated there could be new folders to merge into the active theme(s)
454 QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KIconLoader"), QStringLiteral("org.kde.KIconLoader"), QStringLiteral("iconChanged"));
455 message << 0;
456 QDBusConnection::sessionBus().send(message);
457 } else {
458 m_pTimer->start(msec: 1000);
459 }
460}
461
462void Kded::readDirectory(const QString &_path)
463{
464 QString path(_path);
465 if (!path.endsWith(c: QLatin1Char('/'))) {
466 path += QLatin1Char('/');
467 }
468
469 if (m_pDirWatch->contains(path)) { // Already seen this one?
470 return;
471 }
472
473 Q_ASSERT(path != QDir::homePath());
474 m_pDirWatch->addDir(path, watchModes: KDirWatch::WatchFiles | KDirWatch::WatchSubDirs); // add watch on this dir
475}
476
477void Kded::registerWindowId(qlonglong windowId, const QString &sender)
478{
479 if (!m_windowIdList.contains(key: sender)) {
480 m_serviceWatcher->addWatchedService(newService: sender);
481 }
482
483 m_globalWindowIdList.insert(value: windowId);
484 QList<qlonglong> windowIds = m_windowIdList.value(key: sender);
485 windowIds.append(t: windowId);
486 m_windowIdList.insert(key: sender, value: windowIds);
487
488 for (KDEDModule *module : std::as_const(t&: m_modules)) {
489 qCDebug(KDED) << module->moduleName();
490 Q_EMIT module->windowRegistered(windowId);
491 }
492}
493
494void Kded::unregisterWindowId(qlonglong windowId, const QString &sender)
495{
496 m_globalWindowIdList.remove(value: windowId);
497 QList<qlonglong> windowIds = m_windowIdList.value(key: sender);
498 if (!windowIds.isEmpty()) {
499 windowIds.removeAll(t: windowId);
500 if (windowIds.isEmpty()) {
501 m_serviceWatcher->removeWatchedService(service: sender);
502 m_windowIdList.remove(key: sender);
503 } else {
504 m_windowIdList.insert(key: sender, value: windowIds);
505 }
506 }
507
508 for (KDEDModule *module : std::as_const(t&: m_modules)) {
509 qCDebug(KDED) << module->moduleName();
510 Q_EMIT module->windowUnregistered(windowId);
511 }
512}
513
514static void sighandler(int /*sig*/)
515{
516 if (qApp) {
517 qApp->quit();
518 }
519}
520
521KUpdateD::KUpdateD()
522{
523 m_pDirWatch = new KDirWatch(this);
524 m_pTimer = new QTimer(this);
525 m_pTimer->setSingleShot(true);
526 connect(sender: m_pTimer, signal: &QTimer::timeout, context: this, slot: &KUpdateD::runKonfUpdate);
527 QObject::connect(sender: m_pDirWatch, signal: &KDirWatch::dirty, context: this, slot: &KUpdateD::slotNewUpdateFile);
528
529 QStringList dirs = QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, QStringLiteral("kconf_update"), options: QStandardPaths::LocateDirectory);
530 for (auto &path : dirs) {
531 Q_ASSERT(path != QDir::homePath());
532 if (!path.endsWith(c: QLatin1Char('/'))) {
533 path += QLatin1Char('/');
534 }
535
536 if (!m_pDirWatch->contains(path)) {
537 m_pDirWatch->addDir(path, watchModes: KDirWatch::WatchFiles);
538 }
539 }
540}
541
542KUpdateD::~KUpdateD()
543{
544}
545
546void KUpdateD::runKonfUpdate()
547{
548 ::runKonfUpdate();
549}
550
551void KUpdateD::slotNewUpdateFile(const QString &dirty)
552{
553 Q_UNUSED(dirty);
554 qCDebug(KDED) << dirty;
555 m_pTimer->start(msec: 500);
556}
557
558static void setupAppInfo(QApplication *app)
559{
560 app->setApplicationName(QStringLiteral("kded6"));
561 app->setOrganizationDomain(QStringLiteral("kde.org"));
562 app->setApplicationVersion(QStringLiteral(KDED_VERSION_STRING));
563}
564
565static bool detectPlatform(int argc, char **argv)
566{
567 if (qEnvironmentVariableIsSet(varName: "QT_QPA_PLATFORM")) {
568 return false;
569 }
570 for (int i = 0; i < argc; i++) {
571 /* clang-format off */
572 if (qstrcmp(str1: argv[i], str2: "-platform") == 0
573 || qstrcmp(str1: argv[i], str2: "--platform") == 0
574 || QByteArrayView(argv[i]).startsWith(other: "-platform=")
575 || QByteArrayView(argv[i]).startsWith(other: "--platform=")) { /* clang-format on */
576 return false;
577 }
578 }
579 const QByteArray sessionType = qgetenv(varName: "XDG_SESSION_TYPE");
580 if (sessionType.isEmpty()) {
581 return false;
582 }
583 if (qstrcmp(str1: sessionType.data(), str2: "wayland") == 0) {
584 qputenv(varName: "QT_QPA_PLATFORM", value: "wayland");
585 return true;
586 } else if (qstrcmp(str1: sessionType.data(), str2: "x11") == 0) {
587 qputenv(varName: "QT_QPA_PLATFORM", value: "xcb");
588 return true;
589 }
590 return false;
591}
592
593int main(int argc, char *argv[])
594{
595 // options.add("check", qi18n("Check Sycoca database only once"));
596
597 // WABA: Make sure not to enable session management.
598 qunsetenv(varName: "SESSION_MANAGER");
599
600 const bool unsetQpa = detectPlatform(argc, argv);
601
602 // In older versions, QApplication creation was postponed until after
603 // testing for --check, in which case, only a QCoreApplication was created.
604 // Since that option is no longer used at startup, we removed that speed
605 // optimization for code clarity and easier support of standard parameters.
606
607 QApplication app(argc, argv);
608 if (unsetQpa) {
609 qunsetenv(varName: "QT_QPA_PLATFORM");
610 }
611 setupAppInfo(&app);
612 app.setQuitOnLastWindowClosed(false);
613 app.setQuitLockEnabled(false);
614
615 QCommandLineParser parser;
616 parser.addHelpOption();
617 parser.addVersionOption();
618 parser.addOption(commandLineOption: QCommandLineOption(QStringLiteral("check"), QStringLiteral("Check cache validity")));
619 QCommandLineOption replaceOption({QStringLiteral("replace")}, QStringLiteral("Replace an existing instance"));
620 parser.addOption(commandLineOption: replaceOption);
621 parser.process(app);
622
623 // Parse command line before checking D-Bus
624 if (parser.isSet(QStringLiteral("check"))) {
625 // KDBusService not wanted here.
626 KSycoca::self()->ensureCacheValid();
627 runKonfUpdate();
628 return 0;
629 }
630
631 // Qt now has DBus in another thread, so we need to handle any messages
632 // between the service registration and our paths existing
633 // This means adding the spy now, so any received message gets
634 // posted to the main thread
635 qDBusAddSpyHook(Kded::messageFilter);
636
637 QDBusConnectionInterface *bus = QDBusConnection::sessionBus().interface();
638 // Also register as all the names we should respond to (org.kde.kcookiejar, org.kde.khotkeys etc.)
639 // so that the calling code is independent from the physical "location" of the service.
640 const QList<KPluginMetaData> plugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/kded"));
641 for (const KPluginMetaData &metaData : plugins) {
642 const QString serviceName = metaData.value(QStringLiteral("X-KDE-DBus-ServiceName"));
643 if (serviceName.isEmpty()) {
644 continue;
645 }
646 // register them queued as an old kded could be running at this point
647 if (!bus->registerService(serviceName, qoption: QDBusConnectionInterface::QueueService)) {
648 qCWarning(KDED) << "Couldn't register name" << serviceName << "with DBUS - another process owns it already!";
649 }
650 }
651 KDBusService service(KDBusService::Unique | KDBusService::StartupOption(parser.isSet(option: replaceOption) ? KDBusService::Replace : 0));
652
653 KSharedConfig::Ptr config = KSharedConfig::openConfig(QStringLiteral("kded5rc"));
654 KConfigGroup cg(config, QStringLiteral("General"));
655
656 bCheckSycoca = cg.readEntry(key: "CheckSycoca", defaultValue: true);
657 bCheckUpdates = cg.readEntry(key: "CheckUpdates", defaultValue: true);
658 delayedCheck = cg.readEntry(key: "DelayedCheck", defaultValue: false);
659
660 signal(SIGTERM, handler: sighandler);
661 signal(SIGHUP, handler: sighandler);
662
663 KCrash::setFlags(KCrash::AutoRestart);
664
665 std::unique_ptr<Kded> kded = std::make_unique<Kded>();
666
667 kded->recreate(initial: true); // initial
668
669 if (bCheckUpdates) {
670 (void)new KUpdateD; // Watch for updates
671 }
672
673 runKonfUpdate(); // Run it once.
674
675 return app.exec(); // keep running
676}
677
678#include "moc_kded.cpp"
679

source code of kded/src/kded.cpp