From 107de1c3ffe9fa1ad1c9dc57af65a36e3f371b9c Mon Sep 17 00:00:00 2001 From: Eduard Tolosa Date: Tue, 26 May 2026 21:33:04 -0500 Subject: [PATCH] fix: extension supervisor + panel hardening - Daemon supervisor: a _spawnPending guard so an in-flight GetNameOwner check plus a firing backoff timer can't spawn two daemons; wait_async now binds the subprocess and _onDaemonExited ignores foreign/stale exits, so backoff accounting can't be corrupted by an overlapping spawn. - Panel: clearItems() invalidates the search snapshot (_resultsEpoch = -1) so a scroll after HistoryCleared can't render stale rows; _ensureVisible's idle callback null-guards the overlay like the other idle callbacks. Co-Authored-By: Claude Opus 4.7 --- strata@edu4rdshl.dev/extension.js | 22 ++++++++++++++++++---- strata@edu4rdshl.dev/ui/panel.js | 2 ++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/strata@edu4rdshl.dev/extension.js b/strata@edu4rdshl.dev/extension.js index f6cb37e..43f7dea 100644 --- a/strata@edu4rdshl.dev/extension.js +++ b/strata@edu4rdshl.dev/extension.js @@ -21,6 +21,10 @@ export default class StrataExtension extends Extension { _daemonRestartAttempts = 0; _shuttingDown = false; _daemonRestartTimerId = null; + _daemonKillTimerId = null; + /** True while a GetNameOwner check before spawning is in flight, so an + * overlapping respawn attempt can't spawn a second daemon. */ + _spawnPending = false; /** @type {StrataPanel | null} */ _panel = null; @@ -266,6 +270,11 @@ export default class StrataExtension extends Extension { _spawnDaemon() { if (this._shuttingDown) return; + // Coalesce overlapping spawn attempts: the async GetNameOwner check below + // can still be in flight when the backoff timer fires _spawnDaemon again. + // Without this guard both could call _doSpawnDaemon and spawn two daemons. + if (this._spawnPending) return; + this._spawnPending = true; // Check if a daemon is already running (e.g. via systemd user service). // If the D-Bus name is already owned we must not spawn a second instance. @@ -275,6 +284,8 @@ export default class StrataExtension extends Extension { new GLib.Variant('(s)', [BUS_NAME]), null, Gio.DBusCallFlags.NONE, 2000, null, (_conn, result) => { + this._spawnPending = false; + if (this._shuttingDown) return; try { _conn.call_finish(result); // Name already owned - daemon managed externally (systemd etc). @@ -305,7 +316,7 @@ export default class StrataExtension extends Extension { }); this._daemon.init(null); this._daemonSpawnTime = GLib.get_monotonic_time() / 1000; // ms - this._daemon.wait_async(null, () => this._onDaemonExited()); + this._daemon.wait_async(null, (proc) => this._onDaemonExited(proc)); } catch (e) { console.error('[Strata] Failed to spawn daemon:', e); this._scheduleDaemonRestart(); @@ -331,9 +342,12 @@ export default class StrataExtension extends Extension { } } - _onDaemonExited() { - if (!this._daemon) return; // shutdown initiated by _stopDaemon - const exit = this._daemon.get_exit_status(); + _onDaemonExited(proc) { + // Ignore exits from anything that is not the current daemon: a stop + // initiated by _stopDaemon (which nulls _daemon), or a stray subprocess + // from an overlapping spawn. Otherwise we'd misattribute its exit. + if (!this._daemon || proc !== this._daemon) return; + const exit = proc.get_exit_status(); const lifetimeMs = (GLib.get_monotonic_time() / 1000) - this._daemonSpawnTime; this._daemon = null; diff --git a/strata@edu4rdshl.dev/ui/panel.js b/strata@edu4rdshl.dev/ui/panel.js index c9b4f39..b0a3d5d 100644 --- a/strata@edu4rdshl.dev/ui/panel.js +++ b/strata@edu4rdshl.dev/ui/panel.js @@ -347,6 +347,7 @@ export class StrataPanel { this._items = []; this._searchResults = []; this._searchRendered = 0; + this._resultsEpoch = -1; // invalidate the search snapshot so a scroll can't render stale rows this._hoveredWidget = null; this._activeWidget = null; this._widgets.clear(); @@ -558,6 +559,7 @@ export class StrataPanel { _ensureVisible(widget) { GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + if (!this._overlay) return GLib.SOURCE_REMOVE; // panel destroyed before idle ran try { const adj = this._scrollView.get_vadjustment(); if (!adj || adj.page_size === 0) return GLib.SOURCE_REMOVE;