From 373294e2b99c7c9b7dd4f2028306e1d44ce45b96 Mon Sep 17 00:00:00 2001 From: Bjorn Blomberg Date: Wed, 15 Jul 2026 19:08:04 +0200 Subject: [PATCH] Fix frozen client on ctrl+q: send bye instead of half-closing Dropping the client's channel sender never closed the socket (the server's reader thread keeps the connection open), so the client froze waiting for frames. The server now sends a bye message; the client exits cleanly on it and its socket close lets the server clean up. Messages from already-disconnected clients are ignored. Verified end-to-end with the real client in a PTY (exits 0, daemon survives). Co-Authored-By: Claude Fable 5 --- src/client.rs | 2 ++ src/ipc.rs | 4 ++++ src/main.rs | 12 +++++++++++- tests/integration.py | 10 ++++++---- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/client.rs b/src/client.rs index 63fe598..228e633 100644 --- a/src/client.rs +++ b/src/client.rs @@ -100,6 +100,8 @@ fn client_loop( } stdout.flush()?; } + // Servern har kopplat ner oss (t.ex. ctrl+q) — avsluta snyggt. + Ok(ServerMessage::Bye) => return Ok(()), Ok(_) => {} Err(mpsc::TryRecvError::Empty) => break, Err(mpsc::TryRecvError::Disconnected) => return Ok(()), diff --git a/src/ipc.rs b/src/ipc.rs index 0923e3d..0bebdeb 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -111,6 +111,10 @@ pub enum ClientRole { #[serde(tag = "type", rename_all = "snake_case")] pub enum ServerMessage { HelloOk { version: String, socket_path: String }, + /// Servern kopplar ner klienten kontrollerat (t.ex. efter ctrl+q + /// från klienten). Klienten ska återställa terminalen och avsluta — + /// det är klientens socket-stängning som låter servern städa upp. + Bye, Frame { width: u16, height: u16, diff --git a/src/main.rs b/src/main.rs index f4fb1a2..0a3c073 100644 --- a/src/main.rs +++ b/src/main.rs @@ -438,6 +438,11 @@ fn handle_ipc_event( all_clients.remove(&client_id); } IpcEvent::Message { client_id, msg } => { + // Ignorera meddelanden från klienter som redan kopplats ner + // (t.ex. efter bye) men vars socket ännu inte hunnit stängas. + if !all_clients.contains_key(&client_id) { + return; + } match msg { ClientMessage::Input { data } => { let bytes = ipc::from_hex(&data); @@ -540,7 +545,12 @@ fn disconnect_instead_of_quit( ) { if app.should_quit { app.should_quit = false; - // Sändaren droppas → klientens ström stängs → klienten avslutar. + // Skicka bye så klienten avslutar och stänger socketen — att bara + // droppa sändaren räcker inte, eftersom serverns läsartråd håller + // anslutningen öppen och klienten annars fryser i väntan på frames. + if let Some((_, _, _, tx)) = all_clients.get(&client_id) { + let _ = tx.try_send(ServerMessage::Bye); + } all_clients.remove(&client_id); } } diff --git a/tests/integration.py b/tests/integration.py index 483297e..0df10e9 100644 --- a/tests/integration.py +++ b/tests/integration.py @@ -148,13 +148,15 @@ def main(): disp2.recv_until("frame") disp2.send(key_event(ch="q", modifiers="CONTROL")) try: + got_bye = False deadline = time.time() + 3 while time.time() < deadline: - disp2.recv() - disconnected = False + if disp2.recv().get("type") == "bye": + got_bye = True + break except (EOFError, TimeoutError, OSError): - disconnected = True - check("ctrl+q från klient kopplar ner klienten", disconnected) + pass # EOF duger också som nedkoppling + check("ctrl+q från klient ger bye till klienten", got_bye) frame = disp.recv_until("frame") check("daemonen överlever klientens exit", frame.get("width", 0) > 0)