Commit 050a330
Kai Fronsdal
·
2026-06-10 19:47:51 -0400 EDT
parent fbbc6be
fix(kill): release socket before grace sleep so name is reusable `zmx kill X; zmx run X` raced: the daemon's shutdown defer ran handleKill() first and only then closed the listen socket and unlinked the socket file. Two changes: - Reorder the shutdown defer so the listen socket is closed and the socket file unlinked before handleKill()'s 500ms grace period. - Make `zmx kill` synchronous: after sending .Kill, drain-read the connection until EOF. The daemon closes client fds after unlinking the socket file, so when `kill` returns the name is guaranteed free. Avoids a client-side deleteFile, which would race with a fresh session created in between. Adds test/kill_run_race.bats which fails on main and passes here.
2 files changed,
+54,
-4
+19,
-4
1@@ -873,15 +873,19 @@ const Daemon = struct {
2 };
3
4 defer {
5- self.handleKill();
6- self.deinit();
7- posix.close(pty_fd);
8- _ = posix.waitpid(self.pid, 0);
9+ // Close and unlink the listen socket BEFORE handleKill()'s
10+ // 500ms SIGHUP->SIGKILL grace sleep. Otherwise a `zmx run`
11+ // for the same name issued in that window will hang waiting
12+ // for a connect.
13 posix.close(server_sock_fd);
14 std.log.info("deleting socket file session={s}", .{self.session_name});
15 dir.deleteFile(self.session_name) catch |err| {
16 std.log.warn("failed to delete socket file err={s}", .{@errorName(err)});
17 };
18+ self.handleKill();
19+ self.deinit();
20+ posix.close(pty_fd);
21+ _ = posix.waitpid(self.pid, 0);
22 }
23
24 try daemonLoop(self, server_sock_fd, pty_fd);
25@@ -1817,6 +1821,17 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
26 else => return err,
27 };
28
29+ // Block until the daemon hangs up. The daemon's shutdown defer closes
30+ // and unlinks the listen socket before it closes client connections,
31+ // so by the time we read EOF here the session name is free for reuse
32+ // and a subsequent `zmx run <name>` can't land in the dying daemon's
33+ // accept backlog.
34+ var drain: [256]u8 = undefined;
35+ while (true) {
36+ const n = posix.read(fd, &drain) catch break;
37+ if (n == 0) break;
38+ }
39+
40 var buf: [100]u8 = undefined;
41 var w = std.fs.File.stdout().writer(&buf);
42 try w.interface.print("killed session {s}\n", .{session_name});
+35,
-0
1@@ -0,0 +1,35 @@
2+#!/usr/bin/env bats
3+# Regression test for the `zmx kill X; zmx run X` race.
4+#
5+# Previously `zmx kill` returned immediately after sending the IPC .Kill,
6+# while the daemon's shutdown defer ran handleKill() -- SIGHUP, 500ms sleep,
7+# SIGKILL -- BEFORE closing/unlinking the listen socket. A `zmx run X`
8+# issued in that window would connect() into the kernel backlog of a
9+# socket the daemon would never accept() on again, then get RST'd
10+# (ConnectionResetByPeer) when the daemon finally closed the listen fd,
11+# exiting 1 with no output and no session created.
12+
13+load test_helper
14+
15+@test "kill then immediate run with same name succeeds" {
16+ for i in 1 2 3; do
17+ "$ZMX" run race-x -d echo first
18+ wait_for_session race-x
19+
20+ "$ZMX" kill race-x
21+
22+ # Immediately reuse the same session name. Must not land in the
23+ # dying daemon's listen backlog.
24+ run "$ZMX" run race-x -d echo second
25+ echo "iteration $i: status=$status output=$output"
26+ [ "$status" -eq 0 ]
27+ [[ "$output" == *"session \"race-x\" created"* ]]
28+
29+ # New session must be live and serving requests.
30+ wait_for_session race-x
31+ run "$ZMX" history race-x
32+ [ "$status" -eq 0 ]
33+
34+ "$ZMX" kill race-x
35+ done
36+}