From 23a7d728647ce44fec3e75031359d75e230ea3d5 Mon Sep 17 00:00:00 2001 From: Maycon Date: Wed, 29 Apr 2026 13:31:09 -0300 Subject: [PATCH] fix: guard raise/focus/activate against unmanaged windows Forge calls metaWindow.raise()/.focus()/.activate() on windows held by its tree after Mutter has unmanaged them, which fires assertion warnings on every churn (alt-tab, app close, swap): meta_window_set_stack_position_no_sync: assertion 'window->stack_position >= 0' failed The compositor private of an unmanaged window is null, so a single get_compositor_private() check is enough to short-circuit before the call hits Mutter. Add lib/extension/mutter-safe.js with isWindowAlive() + safeRaise/safeFocus/safeActivate wrappers, and route the call sites in tree.js and window.js through them. Behaviour for live windows is unchanged. For dead windows, the operation is silently skipped instead of warning. Complementary to 8716a05 ("guard tree rendering against destroyed window actors"), which already guards the *render* path (move + border draw); the focus path remained unguarded. Verified by churn test: 20 cycles of nautilus open/close (~5s), journal stays silent on stack_position warnings (was 1-2/min during normal use before the patch). --- lib/extension/mutter-safe.js | 34 ++++++++++++++++++++++++++++++++++ lib/extension/tree.js | 16 ++++++++++------ lib/extension/window.js | 31 +++++++++++++++++-------------- 3 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 lib/extension/mutter-safe.js diff --git a/lib/extension/mutter-safe.js b/lib/extension/mutter-safe.js new file mode 100644 index 0000000..5a205ac --- /dev/null +++ b/lib/extension/mutter-safe.js @@ -0,0 +1,34 @@ +// Guard helpers around Meta.Window operations that fault when the window +// has been unmanaged but Forge still holds a reference to it. +// +// Mutter clears `compositor_private` and resets `stack_position` to -1 +// the moment a window is unmanaged. Calling `raise()`, `activate()`, or +// `focus()` after that fires assertions like +// meta_window_set_stack_position_no_sync: assertion 'window->stack_position >= 0' failed +// and bumps the gnome-shell main-loop cost on every churn (alt-tab, +// app close, workspace move). The checks here cost a single C call and +// short-circuit before Forge hands a dead window back to Mutter. + +export function isWindowAlive(metaWindow) { + if (!metaWindow) return false; + if (typeof metaWindow.get_compositor_private !== "function") return false; + return metaWindow.get_compositor_private() !== null; +} + +export function safeRaise(metaWindow) { + if (!isWindowAlive(metaWindow)) return false; + metaWindow.raise(); + return true; +} + +export function safeFocus(metaWindow, time) { + if (!isWindowAlive(metaWindow)) return false; + metaWindow.focus(time); + return true; +} + +export function safeActivate(metaWindow, time) { + if (!isWindowAlive(metaWindow)) return false; + metaWindow.activate(time); + return true; +} diff --git a/lib/extension/tree.js b/lib/extension/tree.js index 7d2c169..3b061b9 100644 --- a/lib/extension/tree.js +++ b/lib/extension/tree.js @@ -29,6 +29,7 @@ import { Logger } from "../shared/logger.js"; // App imports import * as Utils from "./utils.js"; import * as Window from "./window.js"; +import { safeRaise, safeFocus, safeActivate } from "./mutter-safe.js"; export const NODE_TYPES = Utils.createEnum([ "ROOT", @@ -490,7 +491,7 @@ export class Node extends GObject.Object { } }); tabContents.add_style_class_name("window-tabbed-tab-active"); - metaWin.activate(global.display.get_current_time()); + safeActivate(metaWin, global.display.get_current_time()); }; let closeFn = () => { @@ -852,9 +853,10 @@ export class Tree extends Node { if (metaWindow.minimized) { next = this.focus(next, direction); } else { - metaWindow.raise(); - metaWindow.focus(global.display.get_current_time()); - metaWindow.activate(global.display.get_current_time()); + const t = global.display.get_current_time(); + if (!safeRaise(metaWindow)) return null; + safeFocus(metaWindow, t); + safeActivate(metaWindow, t); const monitorArea = metaWindow.get_work_area_current_monitor(); const ptr = this.extWm.getPointer(); @@ -1208,8 +1210,10 @@ export class Tree extends Node { if (focus) { // The fromNode is now on the parent-target - fromNode.nodeValue.raise(); - fromNode.nodeValue.focus(global.get_current_time()); + const t = global.get_current_time(); + if (safeRaise(fromNode.nodeValue)) { + safeFocus(fromNode.nodeValue, t); + } } } } diff --git a/lib/extension/window.js b/lib/extension/window.js index 61820e1..8a0ead1 100644 --- a/lib/extension/window.js +++ b/lib/extension/window.js @@ -33,6 +33,7 @@ import { Logger } from "../shared/logger.js"; // App imports import * as Utils from "./utils.js"; +import { safeRaise, safeFocus, safeActivate } from "./mutter-safe.js"; import { Keybindings } from "./keybindings.js"; import { Tree, @@ -517,15 +518,18 @@ export class WindowManager extends GObject.Object { callback: () => { if (this.eventQueue.length <= 0) { this.unfreezeRender(); + const t = global.display.get_current_time(); if (focusNodeWindow.parentNode.layout === LAYOUT_TYPES.STACKED) { focusNodeWindow.parentNode.appendChild(focusNodeWindow); - focusNodeWindow.nodeValue.raise(); - focusNodeWindow.nodeValue.activate(global.display.get_current_time()); + if (safeRaise(focusNodeWindow.nodeValue)) { + safeActivate(focusNodeWindow.nodeValue, t); + } this.renderTree("move-stacked-queue"); } if (focusNodeWindow.parentNode.layout === LAYOUT_TYPES.TABBED) { - focusNodeWindow.nodeValue.raise(); - focusNodeWindow.nodeValue.activate(global.display.get_current_time()); + if (safeRaise(focusNodeWindow.nodeValue)) { + safeActivate(focusNodeWindow.nodeValue, t); + } if (prev) prev.parentNode.lastTabFocus = prev.nodeValue; this.renderTree("move-tabbed-queue"); } @@ -551,7 +555,7 @@ export class WindowManager extends GObject.Object { this.unfreezeRender(); let swapDirection = Utils.resolveDirection(action.direction); this.tree.swap(focusNodeWindow, swapDirection); - focusNodeWindow.nodeValue.raise(); + safeRaise(focusNodeWindow.nodeValue); this.updateTabbedFocus(focusNodeWindow); this.updateStackedFocus(focusNodeWindow); this.movePointerWith(focusNodeWindow); @@ -656,7 +660,7 @@ export class WindowManager extends GObject.Object { focusNodeWindow.parentNode.layout = LAYOUT_TYPES.STACKED; let lastChild = focusNodeWindow.parentNode.lastChild; if (lastChild.nodeType === NODE_TYPES.WINDOW) { - lastChild.nodeValue.activate(global.display.get_current_time()); + safeActivate(lastChild.nodeValue, global.display.get_current_time()); } } this.unfreezeRender(); @@ -1586,7 +1590,7 @@ export class WindowManager extends GObject.Object { parentNode.appendChild(focusNodeWindow); parentNode.childNodes .filter((child) => child.isWindow()) - .forEach((child) => child.nodeValue.raise()); + .forEach((child) => safeRaise(child.nodeValue)); this.queueEvent({ name: "render-focus-stack", callback: () => { @@ -1599,8 +1603,7 @@ export class WindowManager extends GObject.Object { updateTabbedFocus(focusNodeWindow) { if (!focusNodeWindow) return; if (focusNodeWindow.parentNode.layout === LAYOUT_TYPES.TABBED && !this._freezeRender) { - const metaWindow = focusNodeWindow.nodeValue; - metaWindow.raise(); + safeRaise(focusNodeWindow.nodeValue); } } @@ -1739,7 +1742,7 @@ export class WindowManager extends GObject.Object { this.updateStackedFocus(existNodeWindow); } else { if (this.floatingWindow(existNodeWindow)) { - existNodeWindow.nodeValue.raise(); + safeRaise(existNodeWindow.nodeValue); } } } @@ -2364,10 +2367,10 @@ export class WindowManager extends GObject.Object { const metaWindow = this._getMetaWindowAtPointer(pointer); if (metaWindow) { - // If window is not null, focus it - metaWindow.focus(global.get_current_time()); - // Raise it to the top - metaWindow.raise(); + const t = global.get_current_time(); + if (safeFocus(metaWindow, t)) { + safeRaise(metaWindow); + } } // Continue polling