git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit74907c5ae626f242a3a8562a3b9a426030bbfe57
parenta43439006e
authorIsaac Freund <[email protected]>
date2026-01-12 17:33
WindowManager: disconnect wm if unresponsive

Currently a window manager entering an infinite loop and not
responding to a manage/render sequence causes river to lock up.

With this patch, river will now disconnect a window manager if it takes
more than 3 seconds to respond to a manage/render sequence.

Ideally, river would automatically start a fallback window manager in
this case, but we don't have one yet.

 protocol/river-window-management-v1.xml |  2 ++
 river/WindowManager.zig                 | 32 ++++++++++++++++++++++++--------
 2 files changed, 26 insertions(+), 8 deletions(-)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index cdb273e..db170d4 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -120,6 +120,8 @@
         summary="request violates manage/render sequence ordering"/>
       <entry name="role" value="1"
         summary="given wl_surface already has a role"/>
+      <entry name="unresponsive" value="2"
+        summary="window manager unresponsive"/>
     </enum>
 
     <event name="unavailable">
diff --git a/river/WindowManager.zig b/river/WindowManager.zig
index 8d39e60..c6025ef 100644
--- a/river/WindowManager.zig
+++ b/river/WindowManager.zig
@@ -310,6 +310,7 @@ fn manageStart(wm: *WindowManager) void {
 
     if (wm.object) |wm_v1| {
         wm_v1.sendManageStart();
+        wm.startTimeoutTimer(3000);
     } else {
         wm.manageFinish();
     }
@@ -317,6 +318,7 @@ fn manageStart(wm: *WindowManager) void {
 
 pub fn manageFinish(wm: *WindowManager) void {
     assert(wm.state == .manage);
+    wm.cancelTimeoutTimer();
 
     log.debug("manage sequence finish", .{});
 
@@ -345,14 +347,14 @@ pub fn manageFinish(wm: *WindowManager) void {
     log.debug("sent {} tracked configure(s)", .{wm.state.inflight_configures});
 
     if (wm.state.inflight_configures > 0) {
-        wm.startTimeoutTimer();
+        wm.startTimeoutTimer(100);
     } else {
         wm.renderStart();
     }
 }
 
-fn startTimeoutTimer(wm: *WindowManager) void {
-    wm.timeout.timerUpdate(100) catch {
+fn startTimeoutTimer(wm: *WindowManager, ms: u31) void {
+    wm.timeout.timerUpdate(ms) catch {
         log.err("failed to start timer", .{});
         _ = wm.handleTimeout();
     };
@@ -363,12 +365,23 @@ fn cancelTimeoutTimer(wm: *WindowManager) void {
 }
 
 fn handleTimeout(wm: *WindowManager) c_int {
-    log.err("timeout occurred, some imperfect frames may be shown", .{});
-
-    assert(wm.state.inflight_configures > 0);
-    wm.state.inflight_configures = 0;
+    switch (wm.state) {
+        .inflight_configures => {
+            log.err("timeout occurred, some imperfect frames may be shown", .{});
+            assert(wm.state.inflight_configures > 0);
+            wm.state.inflight_configures = 0;
 
-    wm.renderStart();
+            wm.renderStart();
+        },
+        .manage, .render => {
+            log.err("window manager unresponsive for more than 5 seconds, disconnecting", .{});
+            wm.object.?.postError(.unresponsive, "unresponsive for more than 5 seconds");
+            // Don't wait for the frozen client to receive the protocol error
+            // and exit of its own accord.
+            wm.object.?.getClient().destroy();
+        },
+        .idle => unreachable,
+    }
 
     return 0;
 }
@@ -401,6 +414,7 @@ fn renderStart(wm: *WindowManager) void {
 
     if (wm.object) |wm_v1| {
         wm_v1.sendRenderStart();
+        wm.startTimeoutTimer(3000);
     } else {
         wm.renderFinish();
     }
@@ -412,6 +426,8 @@ fn renderFinish(wm: *WindowManager) void {
     assert(wm.state == .render);
     wm.state = .idle;
 
+    wm.cancelTimeoutTimer();
+
     log.debug("render sequence finish", .{});
 
     {