Commit 0a18bee

matt chow  ·  2026-10-16 11:03:38 -0400 EDT
parent 35b8783
fix(daemon): don't drop a client's unread input on hangup

A client that writes and exits (e.g. `zmx send`, `zmx run -d`) can be
polled with IN and HUP together while part of its message is still in
the socket. Each pass reads at most 4 KiB, so the HUP check in the same
pass closed the client and the rest of the message was discarded: no
log line, and the sender had already exited 0.

Leave such a client for the next pass instead of closing it; the reads
carry on until EOF, which closes it through the existing n == 0 path.
ERR/NVAL still close at once.
2 files changed,  +7, -0
+1, -0
......@@ -25,6 +25,7 @@ Use spec: https://common-changelog.org/
2525 - Prevent infinite loop in `mkdirAll` when directory path contains a dangling symlink
2626 - Prevent terminal and kitty keyboard queries from taking leadership control
2727 - Fixed missing terminal snapshot when attaching to a session originally started detached or via `zmx run`
28+- Don't drop a client's unread input on hangup
2829
2930 ## v0.8.1 - 2026-09-05
3031
+6, -0
......@@ -536,6 +536,12 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
536536 }
537537
538538 if (revents & (lib_posix.POLL.HUP | lib_posix.POLL.ERR | lib_posix.POLL.NVAL) != 0) {
539+ // A client that writes and exits (`zmx send`, `zmx run -d`) is
540+ // reported HUP with part of its message still unread: a pass
541+ // reads at most 4 KiB. Closing now drops the rest. Leave it to
542+ // the next pass, which reads on until EOF closes the client.
543+ if (revents & lib_posix.POLL.IN != 0 and
544+ revents & (lib_posix.POLL.ERR | lib_posix.POLL.NVAL) == 0) continue;
539545 const last = daemon.closeClient(gpa, client, i, false);
540546 if (last) break :daemon_loop;
541547 }