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 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 19:08:04 +02:00
parent db18c42481
commit 373294e2b9
4 changed files with 23 additions and 5 deletions

View File

@@ -100,6 +100,8 @@ fn client_loop(
} }
stdout.flush()?; stdout.flush()?;
} }
// Servern har kopplat ner oss (t.ex. ctrl+q) — avsluta snyggt.
Ok(ServerMessage::Bye) => return Ok(()),
Ok(_) => {} Ok(_) => {}
Err(mpsc::TryRecvError::Empty) => break, Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => return Ok(()), Err(mpsc::TryRecvError::Disconnected) => return Ok(()),

View File

@@ -111,6 +111,10 @@ pub enum ClientRole {
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerMessage { pub enum ServerMessage {
HelloOk { version: String, socket_path: String }, 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 { Frame {
width: u16, width: u16,
height: u16, height: u16,

View File

@@ -438,6 +438,11 @@ fn handle_ipc_event(
all_clients.remove(&client_id); all_clients.remove(&client_id);
} }
IpcEvent::Message { client_id, msg } => { 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 { match msg {
ClientMessage::Input { data } => { ClientMessage::Input { data } => {
let bytes = ipc::from_hex(&data); let bytes = ipc::from_hex(&data);
@@ -540,7 +545,12 @@ fn disconnect_instead_of_quit(
) { ) {
if app.should_quit { if app.should_quit {
app.should_quit = false; 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); all_clients.remove(&client_id);
} }
} }

View File

@@ -148,13 +148,15 @@ def main():
disp2.recv_until("frame") disp2.recv_until("frame")
disp2.send(key_event(ch="q", modifiers="CONTROL")) disp2.send(key_event(ch="q", modifiers="CONTROL"))
try: try:
got_bye = False
deadline = time.time() + 3 deadline = time.time() + 3
while time.time() < deadline: while time.time() < deadline:
disp2.recv() if disp2.recv().get("type") == "bye":
disconnected = False got_bye = True
break
except (EOFError, TimeoutError, OSError): except (EOFError, TimeoutError, OSError):
disconnected = True pass # EOF duger också som nedkoppling
check("ctrl+q från klient kopplar ner klienten", disconnected) check("ctrl+q från klient ger bye till klienten", got_bye)
frame = disp.recv_until("frame") frame = disp.recv_until("frame")
check("daemonen överlever klientens exit", frame.get("width", 0) > 0) check("daemonen överlever klientens exit", frame.get("width", 0) > 0)