Skip to content

fix: prevent gnome-shell crash when closing windows on Wayland - #586

Open
GilJSantana wants to merge 1 commit into
forge-ext:mainfrom
GilJSantana:fix/wayland-window-lifecycle-crash
Open

GilJSantana wants to merge 1 commit into
forge-ext:mainfrom
GilJSantana:fix/wayland-window-lifecycle-crash

Conversation

@GilJSantana

Copy link
Copy Markdown

Fix gnome-shell crash (session logout) when closing a window on Wayland

Problem

On GNOME Shell 51 / Mutter 51 (Wayland), opening a second application and then closing it crashes gnome-shell. The user is logged out and sent back to GDM.

Steps to reproduce:

  1. Log in to a Wayland session with Forge enabled and tiling on.
  2. Open one application, then open a second one (it gets tiled).
  3. Close the second application.
  4. gnome-shell crashes. GDM restarts, then a new user session starts.

The journal shows no JS exception before the crash. There is a burst of:

gnome-shell: (../mutter/src/core/window.c:4357):meta_window_move_resize_internal: runtime check failed: (priv->is_ready)

…then the process dies, and you see Running GNOME Shell (using mutter 51.0) again for GDM and then for the new session.

Environment: GNOME Shell 51.0, Mutter 51.0, Wayland, Forge main @ 1bf4b21.

Root cause

Forge calls Mutter APIs on MetaWindows that are not ready yet or are already being unmanaged:

  1. Late cleanup. On close, Mutter emits unmanaging and unmanaged. The actor emits destroy only after the close animation. Forge removed the node only in windowDestroy (the actor destroy), so during the animation the dead window stayed in the tree. Any renderTree() then called move_resize_frame() / unmaximize() on it. windowDestroy also called removeFloatOverride() (get_wm_class(), get_id()) on the unmanaged window. The per-window signals were never disconnected.
  2. Moving windows before the first frame. The create/render paths moved windows before their first buffer. That is the source of the priv->is_ready warnings.
  3. Focus-on-hover loop (every 16 ms). global.get_window_actors() includes actors of windows that are still animating closed. The loop called focus() / raise() on them. It also refocused the same window on every tick.

Changes (lib/extension/window.js only)

Window lifecycle

  • New helpers: _isAlive(), _isMaximized(), _unmaximize(), _destroyActorDecorations(), _disconnectWindowSignals().
  • Cleanup now happens on the window's unmanaging signal (new _onWindowUnmanaging(), replacing the unmanaged handler). It does the following:
    • marks the window dead;
    • disconnects its signals;
    • destroys the border and split border;
    • removes the float override while the window is still valid;
    • removes the node from the tree.
  • windowDestroy() is now only a safety net. It no longer calls into the MetaWindow.

Readiness gating

  • move() returns early unless the window is alive and ready. It also no longer calls move_frame() before move_resize_frame(), unmaximizes only when the window is maximized, and rounds the rect.
  • A window is marked ready by whichever comes first: the actor's first-frame, the window's shown, or a 500 ms safety timeout (_watchWindowReady()). Becoming ready triggers a renderTree(). Windows that already exist when Forge is enabled are marked ready in trackCurrentWindows().
  • Wayland: window-created is emitted before the compositor actor exists. trackWindow() now defers tracking until the actor appears (_deferTrackWindow(): shown signal plus a 50 ms poll, giving up after about 5 s) instead of dropping the window.
  • The live-resize neighbour loop (_liveResizeNeighbors() / _startLiveResizeLoop()) calls move_resize_frame() directly. It now skips windows that are not alive or not ready.

Focus on hover

  • _getMetaWindowAtPointer() ignores hidden, minimized, unmanaged and non-normal windows. It uses the frame rect.
  • focus() / raise() are called only when the window under the pointer changes.
  • this.grabOp is reset at the end of a grab, so the loop resumes after a drag.

Smaller fixes found along the way

  • showWindowBorders(): !maximized → !maximized(). It was a function reference, so it was always false and the split border never showed.
  • updateMetaWorkspaceMonitor(): !this.grabOp === Meta.GrabOp.WINDOW_BASE → this.grabOp !== …. The old check was always false.
  • SnapLayoutMove: break moved out of the if. Without a focused window it fell through into ShowTabDecorationToggle.
  • postProcessWindow() passed the MetaWindow to movePointerWith() instead of the node.
  • warpPointerToNodeWindow(): gets the seat from global.stage.context.get_backend(), falling back to Clutter.get_default_backend(), and is wrapped in try/catch.
  • Null guards in command(), rectForMonitor(), getWindowsOnWorkspace(), hideWindowBorders(), WindowSwapLastActive and others. case blocks with let declarations are wrapped in braces.

Testing

Tested on GNOME Shell 51.0 / Mutter 51.0, Wayland:

  • Open 2+ applications, close the second: no crash. Previously this logged the session out every time.
  • No more meta_window_move_resize_internal: runtime check failed: (priv->is_ready) in the journal.
  • New windows are tiled automatically again.
  • Close windows in tabbed and stacked containers.
  • Drag-and-drop tiling and live resize with the mouse.
  • Focus on hover enabled.
  • X11 session / older GNOME versions (not tested).

npm test (prettier --list-different) passes for lib/extension/window.js.

Happy to split this into smaller PRs, for example (1) the lifecycle and crash fix and (2) the small bug fixes, if that is easier to review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant