Survive session loss by detaching listeners before wlroots tears down output and input devices

This commit is contained in:
Andrew Hamilton 2026-08-16 08:01:34 +10:00
parent 382809b126
commit 131950d6e0

View file

@ -132,6 +132,7 @@ struct tabbed_server {
struct wl_listener new_toplevel;
struct wl_listener new_popup;
struct wl_listener frame;
struct wl_listener output_destroy;
struct wl_event_source *ctl_source;
struct wlr_xdg_toplevel *active;
@ -415,6 +416,7 @@ struct standalone_kbdev {
struct wlr_keyboard *keyboard;
struct wl_listener key;
struct wl_listener modifiers;
struct wl_listener destroy;
struct wl_list link;
};
@ -458,6 +460,7 @@ static void shutdown_cleanup(void) {
wl_list_remove(&kd->link);
wl_list_remove(&kd->key.link);
wl_list_remove(&kd->modifiers.link);
wl_list_remove(&kd->destroy.link);
free(kd);
}
wl_list_init(&server.kbdevs);
@ -1536,6 +1539,31 @@ static void handle_output_frame(struct wl_listener *listener, void *data) {
frame_pending = true;
}
/* How many standalone outputs have been seen since the last (re)connect.
DRM destroys and re-creates the output when the session loses/regains its
VT, so this is reset to keep accepting the re-added connector. */
static int standalone_output_count;
/* DRM destroys the output when the session (VT) is lost. Clear our
references so nothing touches freed memory, detach the frame listener
(wlr_output_finish asserts it's empty), and let the connector re-added on
resume be accepted again. */
static void handle_output_destroy(struct wl_listener *listener, void *data) {
(void)listener;
(void)data;
wl_list_remove(&server.output_destroy.link);
wl_list_remove(&server.frame.link);
server.output = NULL;
server.scene_output = NULL; /* wlroots frees it via its output addon */
if (server.swapchain) {
wlr_swapchain_destroy(server.swapchain);
server.swapchain = NULL;
}
standalone_output_count = 0;
frame_pending = false;
needs_redraw = false;
}
static void handle_new_output(struct wl_listener *listener, void *data) {
(void)listener;
struct wlr_output *output = data;
@ -1543,7 +1571,6 @@ static void handle_new_output(struct wl_listener *listener, void *data) {
/* Standalone (multi-monitor allowed at the wlroots layer): drive only
the first output with the tabbed canvas, like the headless nested
single output. Later outputs are left disabled. */
static int standalone_output_count;
if (server.standalone && standalone_output_count++ > 0) {
tabbed_log(TABBED_LOG_INFO,
"standalone: extra output ignored (first output only)\n");
@ -1565,6 +1592,8 @@ server.output = output;
enter events even before any damage has accumulated. */
server.frame.notify = handle_output_frame;
wl_signal_add(&output->events.frame, &server.frame);
server.output_destroy.notify = handle_output_destroy;
wl_signal_add(&output->events.destroy, &server.output_destroy);
/* Enable the output up front so buffer commits succeed even before the
host window has been configured. A native output drives at its
@ -1599,11 +1628,17 @@ server.output = output;
backdrop so the nested frame is never transparent. (Direct scanout is
disabled separately above; a rect alone cannot prevent it because
wlroots culls a rect that a covering client surface occludes, leaving
a single render-list entry which is exactly what triggers scanout.) */
server.bg_rect = wlr_scene_rect_create(&server.scene->tree, w, h,
(float[]){ 0.0f, 0.0f, 0.0f, 1.0f });
if (server.bg_rect)
wlr_scene_node_lower_to_bottom(&server.bg_rect->node);
a single render-list entry which is exactly what triggers scanout.)
On a re-connected output (VT round-trip) the node already exists; just
resize it. */
if (server.bg_rect) {
wlr_scene_rect_set_size(server.bg_rect, w, h);
} else {
server.bg_rect = wlr_scene_rect_create(&server.scene->tree, w, h,
(float[]){ 0.0f, 0.0f, 0.0f, 1.0f });
if (server.bg_rect)
wlr_scene_node_lower_to_bottom(&server.bg_rect->node);
}
}
static void handle_toplevel_destroy(struct wl_listener *listener, void *data) {
@ -3399,6 +3434,21 @@ static void standalone_kbd_modifiers(struct wl_listener *listener, void *data) {
wlr_seat_keyboard_notify_modifiers(server.seat, &kb->modifiers);
}
/* The device is going away (libinput suspends all devices when the session
loses its VT). Drop our listeners so wlr_keyboard_finish's assert that
every event list is empty holds, then free the struct. wlroots emits the
input device's destroy signal from within wlr_keyboard_finish(), so this
runs before the assert. */
static void standalone_kbd_destroy(struct wl_listener *listener, void *data) {
(void)data;
struct standalone_kbdev *kd = wl_container_of(listener, kd, destroy);
wl_list_remove(&kd->destroy.link);
wl_list_remove(&kd->key.link);
wl_list_remove(&kd->modifiers.link);
wl_list_remove(&kd->link);
free(kd);
}
static struct wl_listener standalone_ptr_motion;
static struct wl_listener standalone_ptr_abs;
static struct wl_listener standalone_ptr_button;
@ -3468,6 +3518,8 @@ static void handle_new_input(struct wl_listener *listener, void *data) {
wl_signal_add(&kb->events.key, &kd->key);
kd->modifiers.notify = standalone_kbd_modifiers;
wl_signal_add(&kb->events.modifiers, &kd->modifiers);
kd->destroy.notify = standalone_kbd_destroy;
wl_signal_add(&kb->base.events.destroy, &kd->destroy);
wl_list_insert(server.kbdevs.prev, &kd->link);
tabbed_log(TABBED_LOG_INFO, "standalone: keyboard '%s'\n", device->name);
break;