From 1973526c4ea7346832a37bc8f7aeb619e042552c Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 14 Jul 2026 11:46:40 -0400 Subject: [PATCH] plugins: prevent churn of daemon plugins by not re-creating the entire map fixes #2860 port 1.5 --- quickshell/DMSShell.qml | 23 ----- quickshell/Services/PluginService.qml | 137 +++++++++++++++++--------- 2 files changed, 88 insertions(+), 72 deletions(-) diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 0aaf8433f..b32254b0a 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -41,29 +41,6 @@ Item { osdSurfaceReloadTimer.restart(); } - Instantiator { - id: daemonPluginInstantiator - asynchronous: true - model: Object.keys(PluginService.pluginDaemonComponents) - - delegate: Loader { - id: daemonLoader - property string pluginId: modelData - sourceComponent: PluginService.pluginDaemonComponents[pluginId] - - onLoaded: { - if (item) { - item.pluginService = PluginService; - if (item.popoutService !== undefined) { - item.popoutService = PopoutService; - } - item.pluginId = pluginId; - log.info("Daemon plugin loaded:", pluginId); - } - } - } - } - Loader { id: blurredWallpaperBackgroundLoader active: SettingsData.blurredWallpaperLayer && CompositorService.isNiri diff --git a/quickshell/Services/PluginService.qml b/quickshell/Services/PluginService.qml index 791ee3025..0ab4c184c 100644 --- a/quickshell/Services/PluginService.qml +++ b/quickshell/Services/PluginService.qml @@ -27,6 +27,8 @@ Singleton { property var knownManifests: ({}) property var pathToPluginId: ({}) property var pluginInstances: ({}) + property var pluginDaemonInstances: ({}) + property var _daemonSpawnQueue: [] property var globalVars: ({}) property var pluginLoadErrors: ({}) @@ -59,6 +61,16 @@ Singleton { onTriggered: root._flushDirtyStates() } + // Zero-interval so daemons spawn on the next event-loop tick, after any + // deferred destroy() of a previous generation has fully unregistered its + // IpcHandlers (quickshell#898 leaves stale registrations otherwise) + Timer { + id: _daemonSpawnTimer + interval: 0 + repeat: false + onTriggered: root._drainDaemonSpawnQueue() + } + Process { id: directoryCheckProcess command: ["test", "-d", root.pluginDirectory] @@ -368,14 +380,21 @@ Singleton { const newDaemons = Object.assign({}, pluginDaemonComponents); const newLaunchers = Object.assign({}, pluginLauncherComponents); const newInstances = Object.assign({}, pluginInstances); + const newDaemonInstances = Object.assign({}, pluginDaemonInstances); const prevInstance = newInstances[pluginId]; if (prevInstance) { prevInstance.destroy(); delete newInstances[pluginId]; } + const prevDaemon = newDaemonInstances[pluginId]; + if (prevDaemon) { + prevDaemon.destroy(); + delete newDaemonInstances[pluginId]; + } try { + const comps = {}; for (const surface of surfaces) { let url = "file://" + componentPaths[surface]; if (bustCache) @@ -386,38 +405,39 @@ Singleton { pluginLoadFailed(pluginId, comp.errorString()); return false; } - - switch (surface) { - case "daemon": - newDaemons[pluginId] = comp; - break; - case "desktop": - newDesktop[pluginId] = comp; - break; - case "launcher": { - const instance = comp.createObject(root, { - "pluginService": root - }); - if (!instance) { - log.error("failed to instantiate launcher surface:", pluginId, comp.errorString()); - pluginLoadFailed(pluginId, comp.errorString()); - return false; - } - newInstances[pluginId] = instance; - newLaunchers[pluginId] = comp; - break; - } - default: - newWidgets[pluginId] = comp; - break; - } + comps[surface] = comp; } + if (comps.launcher) { + const instance = comps.launcher.createObject(root, { + "pluginService": root + }); + if (!instance) { + log.error("failed to instantiate launcher surface:", pluginId, comps.launcher.errorString()); + pluginLoadFailed(pluginId, comps.launcher.errorString()); + return false; + } + newInstances[pluginId] = instance; + newLaunchers[pluginId] = comps.launcher; + } + + if (comps.daemon) { + newDaemons[pluginId] = comps.daemon; + _daemonSpawnQueue.push(pluginId); + _daemonSpawnTimer.restart(); + } + + if (comps.widget) + newWidgets[pluginId] = comps.widget; + if (comps.desktop) + newDesktop[pluginId] = comps.desktop; + pluginWidgetComponents = newWidgets; pluginDesktopComponents = newDesktop; pluginDaemonComponents = newDaemons; pluginLauncherComponents = newLaunchers; pluginInstances = newInstances; + pluginDaemonInstances = newDaemonInstances; plugin.loaded = true; const newLoaded = Object.assign({}, loadedPlugins); @@ -433,6 +453,36 @@ Singleton { } } + function _createDaemonInstance(pluginId, comp) { + const instance = comp.createObject(root, { + "pluginId": pluginId, + "pluginService": root + }); + if (!instance) { + log.error("failed to instantiate daemon surface:", pluginId, comp.errorString()); + return null; + } + if (instance.popoutService !== undefined) + instance.popoutService = PopoutService; + log.info("Daemon plugin loaded:", pluginId); + return instance; + } + + function _drainDaemonSpawnQueue() { + const queue = _daemonSpawnQueue; + _daemonSpawnQueue = []; + const newDaemonInstances = Object.assign({}, pluginDaemonInstances); + for (const pluginId of queue) { + const comp = pluginDaemonComponents[pluginId]; + if (!comp || !isPluginLoaded(pluginId) || newDaemonInstances[pluginId]) + continue; + const daemon = _createDaemonInstance(pluginId, comp); + if (daemon) + newDaemonInstances[pluginId] = daemon; + } + pluginDaemonInstances = newDaemonInstances; + } + function unloadPlugin(pluginId) { const plugin = loadedPlugins[pluginId]; if (!plugin) { @@ -449,6 +499,14 @@ Singleton { pluginInstances = newInstances; } + const daemonInstance = pluginDaemonInstances[pluginId]; + if (daemonInstance) { + daemonInstance.destroy(); + const newDaemonInstances = Object.assign({}, pluginDaemonInstances); + delete newDaemonInstances[pluginId]; + pluginDaemonInstances = newDaemonInstances; + } + if (pluginDaemonComponents[pluginId]) { const newDaemons = Object.assign({}, pluginDaemonComponents); delete newDaemons[pluginId]; @@ -757,30 +815,11 @@ Singleton { } function togglePlugin(pluginId) { - let instance = pluginInstances[pluginId]; - - // Lazy instantiate daemon plugins on first toggle - // This respects the daemon lifecycle (not instantiated on load) - // while supporting toggle functionality for slideout-capable daemons - if (!instance && pluginDaemonComponents[pluginId]) { - const comp = pluginDaemonComponents[pluginId]; - const newInstance = comp.createObject(root, { - "pluginId": pluginId, - "pluginService": root - }); - if (newInstance) { - const newInstances = Object.assign({}, pluginInstances); - newInstances[pluginId] = newInstance; - pluginInstances = newInstances; - instance = newInstance; - } - } - - if (instance && typeof instance.toggle === "function") { - instance.toggle(); - return true; - } - return false; + const instance = pluginInstances[pluginId] || pluginDaemonInstances[pluginId]; + if (!instance || typeof instance.toggle !== "function") + return false; + instance.toggle(); + return true; } function savePluginData(pluginId, key, value) {