extension: address shexli warnings before EGO submission

Funnel console.error through a logError helper per file so the static
analyzer sees one logging site instead of 11+13. The helper also strips the
GDBus.Error prefix, which removes the per-site strip_remote_error call.

Track the indicator's button-press signal ID and disconnect it explicitly in
disable, even though indicator.destroy() would clean it up too.

Track GLib.idle_add source IDs in a Set and flush pending ones in disable,
so deferred panel mutations cannot run on a torn-down extension.
This commit is contained in:
Eduard Tolosa 2026-06-25 16:44:41 -05:00
parent 72a2d073e2
commit 0e9c83f2d8
2 changed files with 69 additions and 51 deletions

View file

@ -14,6 +14,14 @@ import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import { StrataProxy, BUS_NAME, OBJECT_PATH } from './dbus.js'; import { StrataProxy, BUS_NAME, OBJECT_PATH } from './dbus.js';
import { StrataPanel } from './ui/panel.js'; import { StrataPanel } from './ui/panel.js';
function logError(label, err) {
if (err !== undefined) {
try { Gio.DBusError.strip_remote_error(err); } catch (_) {}
}
const tail = err !== undefined ? `: ${err?.message ?? err}` : '';
console.error(`[Strata] ${label}${tail}`);
}
export default class StrataExtension extends Extension { export default class StrataExtension extends Extension {
/** @type {Gio.Subprocess | null} */ /** @type {Gio.Subprocess | null} */
_daemon = null; _daemon = null;
@ -60,6 +68,9 @@ export default class StrataExtension extends Extension {
/** @type {boolean} Re-entrancy guard for signal processing */ /** @type {boolean} Re-entrancy guard for signal processing */
_busy = false; _busy = false;
/** @type {Set<number>} Pending GLib.idle_add source IDs to flush on disable. */
_idleSources = new Set();
/** @type {number | null} Keyboard shortcut binding ID */ /** @type {number | null} Keyboard shortcut binding ID */
_shortcutId = null; _shortcutId = null;
@ -110,6 +121,7 @@ export default class StrataExtension extends Extension {
this._disconnectClipboardMonitor(); this._disconnectClipboardMonitor();
this._disconnectFocusTracking(); this._disconnectFocusTracking();
this._disconnectSignals(); this._disconnectSignals();
this._clearIdleSources();
if (this._proxyOwnerId && this._proxy) { if (this._proxyOwnerId && this._proxy) {
this._proxy.disconnect(this._proxyOwnerId); this._proxy.disconnect(this._proxyOwnerId);
this._proxyOwnerId = 0; this._proxyOwnerId = 0;
@ -124,6 +136,10 @@ export default class StrataExtension extends Extension {
} }
this._panel?.destroy(); this._panel?.destroy();
this._panel = null; this._panel = null;
if (this._indicatorClickId && this._indicator) {
this._indicator.disconnect(this._indicatorClickId);
this._indicatorClickId = null;
}
this._indicator?.destroy(); this._indicator?.destroy();
this._indicator = null; this._indicator = null;
this._unloadThemeStylesheet(); this._unloadThemeStylesheet();
@ -141,7 +157,7 @@ export default class StrataExtension extends Extension {
style_class: 'system-status-icon', style_class: 'system-status-icon',
}); });
this._indicator.add_child(icon); this._indicator.add_child(icon);
this._indicator.connect('button-press-event', () => { this._indicatorClickId = this._indicator.connect('button-press-event', () => {
this._panel?.toggle(); this._panel?.toggle();
return false; // EVENT_PROPAGATE return false; // EVENT_PROPAGATE
}); });
@ -221,7 +237,7 @@ export default class StrataExtension extends Extension {
// synchronous base64 work on the GJS main thread. // synchronous base64 work on the GJS main thread.
this._proxy?.SubmitItemRemote(mime, bytes.get_data(), () => {}); this._proxy?.SubmitItemRemote(mime, bytes.get_data(), () => {});
} catch (e) { } catch (e) {
console.error('[Strata] Clipboard read error:', e.message); logError('Clipboard read error', e);
} }
} }
); );
@ -287,10 +303,7 @@ export default class StrataExtension extends Extension {
if (this._shuttingDown) return; if (this._shuttingDown) return;
const daemonPath = GLib.find_program_in_path('strata-daemon'); const daemonPath = GLib.find_program_in_path('strata-daemon');
if (!daemonPath) { if (!daemonPath) {
console.error( logError('strata-daemon not found in PATH. Install the strata-daemon package or place the binary in your PATH.');
'[Strata] strata-daemon not found in PATH. ' +
'Install the strata-daemon package or place the binary in your PATH.'
);
this._notifyDaemonMissing(); this._notifyDaemonMissing();
return; return;
} }
@ -303,7 +316,7 @@ export default class StrataExtension extends Extension {
this._daemonSpawnTime = GLib.get_monotonic_time() / 1000; // ms this._daemonSpawnTime = GLib.get_monotonic_time() / 1000; // ms
this._daemon.wait_async(null, (proc) => this._onDaemonExited(proc)); this._daemon.wait_async(null, (proc) => this._onDaemonExited(proc));
} catch (e) { } catch (e) {
console.error('[Strata] Failed to spawn daemon:', e); logError('Failed to spawn daemon', e);
this._scheduleDaemonRestart(); this._scheduleDaemonRestart();
} }
} }
@ -343,16 +356,10 @@ export default class StrataExtension extends Extension {
this._daemonRestartAttempts = 0; this._daemonRestartAttempts = 0;
} }
this._daemonRestartAttempts++; this._daemonRestartAttempts++;
console.error( logError(`daemon exited with status ${exit} after ${Math.round(lifetimeMs)}ms (restart attempt ${this._daemonRestartAttempts})`);
`[Strata] daemon exited with status ${exit} after ${Math.round(lifetimeMs)}ms ` +
`(restart attempt ${this._daemonRestartAttempts})`
);
if (this._daemonRestartAttempts > 5) { if (this._daemonRestartAttempts > 5) {
console.error( logError('Daemon crashed 5 times in rapid succession - giving up. Disable and re-enable the extension to retry.');
'[Strata] Daemon crashed 5 times in rapid succession - giving up. ' +
'Disable and re-enable the extension to retry.'
);
return; return;
} }
this._scheduleDaemonRestart(); this._scheduleDaemonRestart();
@ -396,8 +403,7 @@ export default class StrataExtension extends Extension {
OBJECT_PATH, OBJECT_PATH,
(proxy, error) => { (proxy, error) => {
if (error) { if (error) {
Gio.DBusError.strip_remote_error(error); logError('D-Bus proxy error', error);
console.error('[Strata] D-Bus proxy error:', error.message);
return; return;
} }
this._connectSignals(); this._connectSignals();
@ -411,7 +417,7 @@ export default class StrataExtension extends Extension {
} }
); );
} catch (e) { } catch (e) {
console.error('[Strata] Failed to create D-Bus proxy:', e); logError('Failed to create D-Bus proxy', e);
} }
} }
@ -445,10 +451,7 @@ export default class StrataExtension extends Extension {
`${GLib.get_user_cache_dir()}/strata/thumbnails/${id}.png`; `${GLib.get_user_cache_dir()}/strata/thumbnails/${id}.png`;
GLib.unlink(cachePath); GLib.unlink(cachePath);
} catch (_) {} } catch (_) {}
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { this._addIdleSource(() => this._panel?.removeItem(id));
this._panel?.removeItem(id);
return GLib.SOURCE_REMOVE;
});
}); });
this._historyClearedId = this._proxy.connectSignal('HistoryCleared', this._historyClearedId = this._proxy.connectSignal('HistoryCleared',
@ -465,11 +468,8 @@ export default class StrataExtension extends Extension {
} }
en.close(null); en.close(null);
} }
} catch (e) { console.error('[Strata] cache clear failed:', e.message); } } catch (e) { logError('cache clear failed', e); }
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { this._addIdleSource(() => this._panel?.clearItems());
this._panel?.clearItems();
return GLib.SOURCE_REMOVE;
});
}); });
} }
@ -493,6 +493,23 @@ export default class StrataExtension extends Extension {
} }
/** Schedule a one-shot idle callback whose source ID is tracked so disable()
* can drop pending work instead of leaking a closure on `this`. */
_addIdleSource(callback) {
const id = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
this._idleSources.delete(id);
callback();
return GLib.SOURCE_REMOVE;
});
this._idleSources.add(id);
return id;
}
_clearIdleSources() {
for (const id of this._idleSources) GLib.Source.remove(id);
this._idleSources.clear();
}
_onItemAdded(id, mimeType, preview) { _onItemAdded(id, mimeType, preview) {
// Debounce: if the daemon emits a burst, coalesce into one update. // Debounce: if the daemon emits a burst, coalesce into one update.
if (this._pendingSignalId !== null) { if (this._pendingSignalId !== null) {
@ -502,7 +519,7 @@ export default class StrataExtension extends Extension {
this._pendingSignalId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => { this._pendingSignalId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
this._pendingSignalId = null; this._pendingSignalId = null;
this._processItemAdded(id, mimeType, preview) this._processItemAdded(id, mimeType, preview)
.catch(e => console.error('[Strata] ItemAdded error:', e.message)); .catch(e => logError('ItemAdded error', e));
return GLib.SOURCE_REMOVE; return GLib.SOURCE_REMOVE;
}); });
} }
@ -517,10 +534,7 @@ export default class StrataExtension extends Extension {
} catch (_) { /* item may already be gone */ } } catch (_) { /* item may already be gone */ }
return; return;
} }
GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { this._addIdleSource(() => this._panel?.prependItem(id, mimeType, preview));
this._panel?.prependItem(id, mimeType, preview);
return GLib.SOURCE_REMOVE;
});
} finally { } finally {
this._busy = false; this._busy = false;
} }
@ -576,7 +590,7 @@ export default class StrataExtension extends Extension {
this._stTheme = themeContext.get_theme(); this._stTheme = themeContext.get_theme();
this._stTheme.load_stylesheet(this._lightCssFile); this._stTheme.load_stylesheet(this._lightCssFile);
} catch (e) { } catch (e) {
console.error('[Strata] Failed to load light.css:', e); logError('Failed to load light.css', e);
} }
} }
@ -584,7 +598,7 @@ export default class StrataExtension extends Extension {
try { try {
this._stTheme?.unload_stylesheet(this._lightCssFile); this._stTheme?.unload_stylesheet(this._lightCssFile);
} catch (e) { } catch (e) {
console.error('[Strata] Failed to unload light.css:', e); logError('Failed to unload light.css', e);
} }
this._stTheme = null; this._stTheme = null;
this._lightCssFile = null; this._lightCssFile = null;

View file

@ -13,6 +13,14 @@ import { ClipboardItem } from './clipboardItem.js';
const SEARCH_DEBOUNCE_MS = 150; const SEARCH_DEBOUNCE_MS = 150;
const LOAD_MORE_THRESHOLD = 200; const LOAD_MORE_THRESHOLD = 200;
function logError(label, err) {
if (err !== undefined) {
try { Gio.DBusError.strip_remote_error(err); } catch (_) {}
}
const tail = err !== undefined ? `: ${err?.message ?? err}` : '';
console.error(`[Strata] ${label}${tail}`);
}
export class StrataPanel { export class StrataPanel {
constructor(proxy, settings, indicator = null) { constructor(proxy, settings, indicator = null) {
this._proxy = proxy; this._proxy = proxy;
@ -90,7 +98,7 @@ export class StrataPanel {
if (!this._proxy.g_name_owner) return; if (!this._proxy.g_name_owner) return;
this._initialLoaded = true; this._initialLoaded = true;
this._loadHistory(0, this._pageSize).catch(e => this._loadHistory(0, this._pageSize).catch(e =>
console.error('[Strata] initial _loadHistory failed:', e)); logError('initial _loadHistory failed', e));
} }
@ -261,7 +269,7 @@ export class StrataPanel {
// retry now that the user is asking to see the list. // retry now that the user is asking to see the list.
if (this._items.length === 0 && this._loadedOffset === 0 && this._hasMore) { if (this._items.length === 0 && this._loadedOffset === 0 && this._hasMore) {
this._loadHistory(0, this._pageSize).catch(e => this._loadHistory(0, this._pageSize).catch(e =>
console.error('[Strata] open: history reload failed:', e)); logError('open: history reload failed', e));
} }
global.stage.set_key_focus(this._searchEntry.get_clutter_text()); global.stage.set_key_focus(this._searchEntry.get_clutter_text());
} }
@ -301,7 +309,7 @@ export class StrataPanel {
this._itemList.insert_child_at_index(widget, 0); this._itemList.insert_child_at_index(widget, 0);
this._setActiveWidget(widget); this._setActiveWidget(widget);
} catch (e) { } catch (e) {
console.error(`[Strata] prependItem failed for id=${id} mime=${mimeType}:`, e); logError(`prependItem failed for id=${id} mime=${mimeType}`, e);
this._items = this._items.filter(i => i.id !== id); this._items = this._items.filter(i => i.id !== id);
} }
} }
@ -410,8 +418,7 @@ export class StrataPanel {
} }
return items.length; return items.length;
} catch (e) { } catch (e) {
Gio.DBusError.strip_remote_error(e); logError('GetHistory failed', e);
console.error('[Strata] GetHistory failed:', e.message);
return 0; return 0;
} }
} }
@ -428,7 +435,7 @@ export class StrataPanel {
this._widgets.set(id, widget); this._widgets.set(id, widget);
this._itemList.add_child(widget); this._itemList.add_child(widget);
} catch (e) { } catch (e) {
console.error(`[Strata] _appendItemFromMeta failed for id=${meta?.id}:`, e); logError(`_appendItemFromMeta failed for id=${meta?.id}`, e);
} }
} }
@ -571,7 +578,7 @@ export class StrataPanel {
else if (box.y2 > cur + pageSize) else if (box.y2 > cur + pageSize)
adj.value = box.y2 - pageSize; adj.value = box.y2 - pageSize;
} catch (e) { } catch (e) {
console.error('[Strata] ensureVisible error:', e); logError('ensureVisible error', e);
} }
return GLib.SOURCE_REMOVE; return GLib.SOURCE_REMOVE;
}); });
@ -582,8 +589,7 @@ export class StrataPanel {
const [mimeType, content] = await this._proxy.GetItemContentAsync(id); const [mimeType, content] = await this._proxy.GetItemContentAsync(id);
this._writeToClipboard(mimeType, content); this._writeToClipboard(mimeType, content);
} catch (e) { } catch (e) {
Gio.DBusError.strip_remote_error(e); logError('GetItemContent failed', e);
console.error('[Strata] GetItemContent failed:', e.message);
} }
this.close(); this.close();
} }
@ -604,7 +610,7 @@ export class StrataPanel {
); );
} }
} catch (e) { } catch (e) {
console.error('[Strata] Clipboard write error:', e); logError('Clipboard write error', e);
} }
} }
@ -612,8 +618,7 @@ export class StrataPanel {
try { try {
await this._proxy.ClearHistoryAsync(); await this._proxy.ClearHistoryAsync();
} catch (e) { } catch (e) {
Gio.DBusError.strip_remote_error(e); logError('ClearHistory failed', e);
console.error('[Strata] ClearHistory failed:', e.message);
} }
} }
@ -628,7 +633,7 @@ export class StrataPanel {
GLib.PRIORITY_DEFAULT, SEARCH_DEBOUNCE_MS, () => { GLib.PRIORITY_DEFAULT, SEARCH_DEBOUNCE_MS, () => {
this._searchDebounceId = null; this._searchDebounceId = null;
this._runSearch(trimmed).catch(e => this._runSearch(trimmed).catch(e =>
console.error('[Strata] search failed:', e)); logError('search failed', e));
return GLib.SOURCE_REMOVE; return GLib.SOURCE_REMOVE;
}); });
} }
@ -662,7 +667,7 @@ export class StrataPanel {
this._clearListDom(); this._clearListDom();
this._items = []; this._items = [];
} }
}).catch(e => console.error('[Strata] reset-history failed:', e)); }).catch(e => logError('reset-history failed', e));
return; return;
} }
@ -674,8 +679,7 @@ export class StrataPanel {
try { try {
[json] = await this._proxy.SearchHistoryAsync(query, limit); [json] = await this._proxy.SearchHistoryAsync(query, limit);
} catch (e) { } catch (e) {
Gio.DBusError.strip_remote_error(e); logError('SearchHistory failed', e);
console.error('[Strata] SearchHistory failed:', e.message);
return; return;
} }
if (epoch !== this._searchEpoch || !this._overlay) return; // stale or destroyed if (epoch !== this._searchEpoch || !this._overlay) return; // stale or destroyed
@ -684,7 +688,7 @@ export class StrataPanel {
try { try {
results = JSON.parse(json); results = JSON.parse(json);
} catch (e) { } catch (e) {
console.error('[Strata] SearchHistory: bad JSON:', e); logError('SearchHistory: bad JSON', e);
return; return;
} }