Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/daemon.zig
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,23 @@ pub const Daemon = struct {
self.alloc.destroy(c);
}
self.conns.deinit(self.alloc);
posix.close(self.opts.listen_fd);
std.fs.cwd().deleteFile(self.opts.socket_path) catch {};
self.retireListener();
if (self.owned_name) |n| self.alloc.free(n);
if (self.owned_socket_path) |p| self.alloc.free(p);
if (self.sig_read >= 0) posix.close(self.sig_read);
if (sigchld_pipe >= 0) posix.close(sigchld_pipe);
}

/// Close the listening socket and remove its file so new clients
/// resolve "no session" instead of connecting to a dying daemon
/// and reading EOF.
fn retireListener(self: *Daemon) void {
if (self.opts.listen_fd < 0) return;
posix.close(self.opts.listen_fd);
self.opts.listen_fd = -1;
std.fs.cwd().deleteFile(self.opts.socket_path) catch {};
}

fn loop(self: *Daemon) !void {
var fds: std.ArrayList(posix.pollfd) = .empty;
defer fds.deinit(self.alloc);
Expand Down Expand Up @@ -397,6 +406,11 @@ pub const Daemon = struct {
}
self.rename(conn, argv[1]);
} else if (std.mem.eql(u8, cmd, "quit")) {
// Retire the listener before acking: by the time the kill
// client sees the reply, the socket file is gone, so a
// follow-up command resolves "no session" instead of
// connecting to the dying daemon and reading EOF.
self.retireListener();
conn.send(.ok, "");
if (self.win) |w| {
posix.kill(w.child_pid, posix.SIG.HUP) catch {};
Expand Down
8 changes: 5 additions & 3 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,10 @@ pub fn sessionInfo(alloc: std.mem.Allocator, dir: []const u8, name: []const u8)
};
}

/// Run a control command against a session, mapping a missing daemon
/// to the documented exit code.
/// Run a control command against a session, mapping a missing or
/// mid-teardown daemon to the documented exit code. An EOF on the
/// control connection means the daemon died before replying, so it is
/// reported the same as a daemon that is already gone.
fn mustControl(
alloc: std.mem.Allocator,
dir: []const u8,
Expand All @@ -201,7 +203,7 @@ fn mustControl(
const sock = try paths.socketPath(alloc, dir, name);
defer alloc.free(sock);
return client.control(alloc, sock, argv) catch |err| switch (err) {
error.FileNotFound, error.ConnectionRefused => fail(
error.FileNotFound, error.ConnectionRefused, error.ConnectionLost => fail(
exit_no_session,
"no session named {s}",
.{name},
Expand Down
19 changes: 19 additions & 0 deletions test/integration.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,25 @@ test "agent loop: new, send, wait, peek, kill" {
try h.runExit(&.{ "peek", "agent" }, 3);
}

test "kill: peek immediately after kill reports no session" {
const alloc = std.testing.allocator;
var h = try Harness.init(alloc);
defer h.deinit();

// Once kill is acked the socket file is already unlinked, so a
// back-to-back peek must deterministically resolve "no session"
// (exit 3) and never observe EOF from the dying daemon. Repeat to
// amplify the former race between the kill ack and teardown.
var i: usize = 0;
var name_buf: [16]u8 = undefined;
while (i < 10) : (i += 1) {
const name = try std.fmt.bufPrint(&name_buf, "reap{d}", .{i});
try h.startDetached(name, &.{"sh"});
try h.runOk(&.{ "kill", name });
try h.runExit(&.{ "peek", name }, 3);
}
}

test "rename: moves a session to a new name" {
const alloc = std.testing.allocator;
var h = try Harness.init(alloc);
Expand Down
Loading