Commit 12a19ee

Ian Tay  ·  2026-03-08 12:54:06 -0400 EDT
parent 35f56e6
fix: use platform-correct O_NONBLOCK for fcntl; fix PTY write and double-close

The hardcoded value 0o4000 is Linux-specific; on macOS O_NONBLOCK is 0x4.
posix.SOCK.NONBLOCK is for socket()/accept4(), not fcntl(F_SETFL) — it only
worked on Linux by coincidence where both constants share the same value.
On macOS, non-blocking mode was never actually set on the PTY, client
socket, or stdin.

Setting O_NONBLOCK correctly exposes two latent issues, also fixed here:

- PTY writes in handleInput/handleRun did `_ = try posix.write()`,
  discarding short-write counts and propagating WouldBlock as an error
  that crashed the daemon. Added ptyWriteAll() that polls on WouldBlock
  and retries short writes — same blocking semantics macOS had implicitly.

- ensureSession's errdefer + defer both closed server_sock_fd when
  daemonLoop errored, causing EBADF (posix.close treats this as
  unreachable → panic in safe builds). Restructured so spawnPty failure
  is handled by an explicit catch; the defer is the sole owner after.

Additionally, O_NONBLOCK is set on the open file description (shared with
the parent shell), so stdin's original flags must be restored on exit to
avoid leaving the parent shell's stdin in non-blocking mode.
1 files changed,  +51, -16
+51, -16
......@@ -33,6 +33,9 @@ fn zmxLogFn(
3333 var sigwinch_received: std.atomic.Value(bool) = std.atomic.Value(bool).init(false);
3434 var sigterm_received: std.atomic.Value(bool) = std.atomic.Value(bool).init(false);
3535
36+// https://github.com/ziglang/zig/blob/738d2be9d6b6ef3ff3559130c05159ef53336224/lib/std/posix.zig#L3505
37+const O_NONBLOCK: usize = 1 << @bitOffsetOf(posix.O, "NONBLOCK");
38+
3639 pub fn main() !void {
3740 // use c_allocator to avoid "reached unreachable code" panic in DebugAllocator when forking
3841 const alloc = std.heap.c_allocator;
......@@ -334,7 +337,7 @@ const Daemon = struct {
334337
335338 // make pty non-blocking
336339 const flags = try posix.fcntl(master_fd, posix.F.GETFL, 0);
337- _ = try posix.fcntl(master_fd, posix.F.SETFL, flags | @as(u32, 0o4000));
340+ _ = try posix.fcntl(master_fd, posix.F.SETFL, flags | O_NONBLOCK);
338341 return master_fd;
339342 }
340343
......@@ -372,12 +375,19 @@ const Daemon = struct {
372375 defer self.alloc.free(session_log_path);
373376 try log_system.init(self.alloc, session_log_path);
374377
375- errdefer {
378+ // If spawnPty fails, clean up here. Once it succeeds,
379+ // the inner block's defer takes ownership of cleanup to
380+ // avoid double-closing server_sock_fd on daemonLoop error.
381+ const pty_fd = self.spawnPty() catch |err| {
376382 posix.close(server_sock_fd);
377383 dir.deleteFile(self.session_name) catch {};
378- }
379- const pty_fd = try self.spawnPty();
384+ return err;
385+ };
386+
380387 defer {
388+ self.handleKill();
389+ self.deinit();
390+ _ = posix.waitpid(self.pid, 0);
381391 posix.close(pty_fd);
382392 posix.close(server_sock_fd);
383393 std.log.info("deleting socket file session_name={s}", .{self.session_name});
......@@ -385,10 +395,8 @@ const Daemon = struct {
385395 std.log.warn("failed to delete socket file err={s}", .{@errorName(err)});
386396 };
387397 }
398+
388399 try daemonLoop(self, server_sock_fd, pty_fd);
389- self.handleKill();
390- _ = posix.waitpid(self.pid, 0);
391- self.deinit();
392400 return .{ .created = true, .is_daemon = true };
393401 }
394402 posix.close(server_sock_fd);
......@@ -399,10 +407,33 @@ const Daemon = struct {
399407 return .{ .created = false, .is_daemon = false };
400408 }
401409
402- pub fn handleInput(self: *Daemon, pty_fd: i32, payload: []const u8) !void {
410+ /// Best-effort write to the (non-blocking) PTY fd. Retries short writes
411+ /// until complete, but on WouldBlock (kernel buffer full) gives up and
412+ /// drops the remainder — the daemon is single-threaded, so blocking here
413+ /// to wait for POLLOUT would deadlock against a shell that's itself
414+ /// blocked writing echo to a full PTY output buffer that we're not
415+ /// draining. Dropping is the same trade-off the old code made implicitly
416+ /// (short writes were silently truncated), just without the crash.
417+ fn ptyWrite(pty_fd: i32, data: []const u8) void {
418+ var remaining = data;
419+ while (remaining.len > 0) {
420+ const n = posix.write(pty_fd, remaining) catch |err| {
421+ if (err == error.WouldBlock) {
422+ std.log.warn("pty write dropped {d}/{d} bytes (buffer full)", .{ remaining.len, data.len });
423+ } else {
424+ std.log.warn("pty write failed, {d} bytes lost: {s}", .{ remaining.len, @errorName(err) });
425+ }
426+ return;
427+ };
428+ if (n == 0) return;
429+ remaining = remaining[n..];
430+ }
431+ }
432+
433+ pub fn handleInput(self: *Daemon, pty_fd: i32, payload: []const u8) void {
403434 _ = self;
404435 if (payload.len > 0) {
405- _ = try posix.write(pty_fd, payload);
436+ ptyWrite(pty_fd, payload);
406437 }
407438 }
408439
......@@ -575,7 +606,7 @@ const Daemon = struct {
575606 self.is_task_mode = true;
576607
577608 if (payload.len > 0) {
578- _ = try posix.write(pty_fd, payload);
609+ ptyWrite(pty_fd, payload);
579610 }
580611 try ipc.appendMessage(self.alloc, &client.write_buf, .Ack, "");
581612 client.has_pending_output = true;
......@@ -1071,8 +1102,9 @@ fn clientLoop(client_sock_fd: i32) !void {
10711102 setupSigwinchHandler();
10721103
10731104 // Make socket non-blocking to avoid blocking on writes
1074- const sock_flags = try posix.fcntl(client_sock_fd, posix.F.GETFL, 0);
1075- _ = try posix.fcntl(client_sock_fd, posix.F.SETFL, sock_flags | posix.SOCK.NONBLOCK);
1105+ var sock_flags = try posix.fcntl(client_sock_fd, posix.F.GETFL, 0);
1106+ sock_flags |= O_NONBLOCK;
1107+ _ = try posix.fcntl(client_sock_fd, posix.F.SETFL, sock_flags);
10761108
10771109 // Buffer for outgoing socket writes
10781110 var sock_write_buf = try std.ArrayList(u8).initCapacity(alloc, 4096);
......@@ -1093,9 +1125,12 @@ fn clientLoop(client_sock_fd: i32) !void {
10931125
10941126 const stdin_fd = posix.STDIN_FILENO;
10951127
1096- // Make stdin non-blocking
1097- const flags = try posix.fcntl(stdin_fd, posix.F.GETFL, 0);
1098- _ = try posix.fcntl(stdin_fd, posix.F.SETFL, flags | posix.SOCK.NONBLOCK);
1128+ // Make stdin non-blocking. O_NONBLOCK is set on the open file description,
1129+ // which is shared with the parent shell; restore on exit to avoid
1130+ // corrupting the parent's stdin.
1131+ const stdin_orig_flags = try posix.fcntl(stdin_fd, posix.F.GETFL, 0);
1132+ _ = try posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags | O_NONBLOCK);
1133+ defer _ = posix.fcntl(stdin_fd, posix.F.SETFL, stdin_orig_flags) catch {};
10991134
11001135 while (true) {
11011136 // Check for pending SIGWINCH
......@@ -1369,7 +1404,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
13691404
13701405 while (client.read_buf.next()) |msg| {
13711406 switch (msg.header.tag) {
1372- .Input => try daemon.handleInput(pty_fd, msg.payload),
1407+ .Input => daemon.handleInput(pty_fd, msg.payload),
13731408 .Init => try daemon.handleInit(client, pty_fd, &term, msg.payload),
13741409 .Resize => try daemon.handleResize(pty_fd, &term, msg.payload),
13751410 .Detach => {