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

source code of kded/src/kded.cpp