wayland: Phase 40 — wire toplevel unmap to wUnmanageWindow

Guard all dpy==NULL / frame==NULL crash sites in the unmanage path:
  - wUnmanageWindow (window.c): frame ptr set to NULL-safe expression;
    window_unmap, raise_delay timer, and owner border change all
    guarded on frame != NULL
  - wSelectWindow (actions.c): early return if !wwin->frame
  - wNETCleanupFrameExtents (wmspec.c): early return if !dpy
  - wClientRestore (client.c): early return if !dpy

Wire handle_toplevel_unmap → context_find → wUnmanageWindow(destroyed=True)
so XDG toplevels are removed from scr->focused_window on client disconnect.

Smoke test: add Phase 40 check — grep "toplevel unmapped".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Levi Neely 2026-03-28 06:34:15 +01:00
parent ca09635129
commit 490ecb34ff
6 changed files with 31 additions and 6 deletions

View File

@ -2159,6 +2159,9 @@ void wSelectWindow(WWindow *wwin, Bool flag)
{
WScreen *scr = wwin->screen_ptr;
if (!wwin->frame)
return; /* Wayland: no frame window to set border on */
if (flag) {
wwin->flags.selected = 1;
if (wwin->frame->selected_border_pixel)

View File

@ -157,7 +157,7 @@ handle_toplevel_map(struct wl_listener *listener, void *data)
}
/*
* Phase 38: toplevel unmap — surface hidden (minimised or gone).
* Phase 40: toplevel unmap — surface hidden or destroyed; unmanage the window.
*/
static void
handle_toplevel_unmap(struct wl_listener *listener, void *data)
@ -166,7 +166,16 @@ handle_toplevel_unmap(struct wl_listener *listener, void *data)
(void)data;
wwarning("wl_backend: toplevel unmapped id=0x%lx", view->id);
/* TODO Phase 39: call wUnmanageWindow */
/* Phase 40: remove the WWindow from the WM's managed list. */
WScreen *scr = wScreenWithNumber(0);
if (scr) {
WObjDescriptor *desc = wm_backend->context_find(view->id, WM_CTX_CLIENT_WIN);
if (desc && desc->parent_type == WCLASS_WINDOW) {
WWindow *wwin = (WWindow *)desc->parent;
wUnmanageWindow(wwin, False, True);
}
}
}
/*

View File

@ -49,6 +49,8 @@
*/
void wClientRestore(WWindow * wwin)
{
if (!dpy)
return; /* Wayland: no X11 reparent/border-width */
#if 0
int gx, gy;

View File

@ -1472,7 +1472,8 @@ WWindow *wManageInternalWindow(WScreen *scr, WNativeWindow window, WNativeWindow
*/
void wUnmanageWindow(WWindow *wwin, Bool restore, Bool destroyed)
{
WCoreWindow *frame = wwin->frame->core;
/* Wayland: frame may be NULL when managing frameless surfaces */
WCoreWindow *frame = wwin->frame ? wwin->frame->core : NULL;
WWindow *owner = NULL;
WWindow *newFocusedWindow = NULL;
int wasFocused;
@ -1507,7 +1508,8 @@ void wUnmanageWindow(WWindow *wwin, Bool restore, Bool destroyed)
wm_backend->key_ungrab_all(wwin->client_win);
}
wm_backend->window_unmap(frame->window);
if (frame)
wm_backend->window_unmap(frame->window);
wm_backend->window_unmap(wwin->client_win);
@ -1516,7 +1518,7 @@ void wUnmanageWindow(WWindow *wwin, Bool restore, Bool destroyed)
/* remove all pending events on window */
/* I think this only matters for autoraise */
if (wPreferences.raise_delay)
if (wPreferences.raise_delay && wwin->frame)
WMDeleteTimerWithClientData(wwin->frame->core);
wm_backend->event_flush();
@ -1613,7 +1615,7 @@ void wUnmanageWindow(WWindow *wwin, Bool restore, Bool destroyed)
WMPostNotificationName(WMNUnmanaged, wwin, NULL);
if (wasFocused) {
if (newFocusedWindow != owner && owner) {
if (wwin->flags.is_gnustep == 0)
if (wwin->flags.is_gnustep == 0 && owner->frame)
wFrameWindowChangeState(owner->frame, WS_UNFOCUSED);
}
wSetFocusTo(scr, newFocusedWindow);

View File

@ -2021,5 +2021,7 @@ void wNETFrameExtents(WWindow *wwin)
void wNETCleanupFrameExtents(WWindow *wwin)
{
if (!dpy)
return; /* Wayland: no X11 _NET_FRAME_EXTENTS property */
XDeleteProperty(dpy, wwin->client_win, net_frame_extents);
}

View File

@ -20,6 +20,8 @@
# mapped" when weston-simple-shm creates its XDG toplevel surface
# 9. (Phase 39) wManageWindow called for mapped XDG toplevel: compositor
# logs "wManageWindow succeeded" for the synthetic surface ID
# 10. (Phase 40) wUnmanageWindow called when client disconnects: compositor
# logs "toplevel unmapped" and exits cleanly after cleanup
#
# This test grows as wl_backend vtable slots are implemented.
#
@ -155,6 +157,11 @@ if command -v weston-simple-shm >/dev/null 2>&1; then
grep -q 'wManageWindow succeeded' /tmp/wl-smoke-stderr \
|| die "compositor did not log 'wManageWindow succeeded' (wManageWindow call failed)"
echo " manage check: wManageWindow succeeded for weston-simple-shm toplevel"
# -- 10. Phase 40: wUnmanageWindow fired when client disconnects ------
grep -q 'toplevel unmapped' /tmp/wl-smoke-stderr \
|| die "compositor did not log 'toplevel unmapped' (wUnmanageWindow not triggered)"
echo " unmanage check: toplevel unmapped and wUnmanageWindow called"
else
echo " client check: skipped (weston-simple-shm not installed)"
fi