summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 21:35:50 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit4e5719a133030e8ed29799d4959bb0b917092d7c (patch)
treed933325f8bc15e4484385c5b00f64b63794ffa20 /src
parent20685b048fcb307840451471679339c9b9704b23 (diff)
downloadpardes-4e5719a133030e8ed29799d4959bb0b917092d7c.tar.gz
pardes-4e5719a133030e8ed29799d4959bb0b917092d7c.zip
An editor killed with SIGTERM or SIGHUP removes its 9P socket, and a start sweeps the pardes-9p-<pid>.sock files whose pid is gone
The runtime directory had gathered a hundred sockets of dead editors: only a clean exit removed its own, and a signal or a crash left it. The terminal's kill handler and, where nothing else handles the signals, a handler of the listener's own now unlink the socket before the signal's death. At listen, each pardes-9p-<pid>.sock whose pid kill(pid, 0) answers ESRCH for, that is a socket of this user's and that nothing answers on, is removed; a live pid's, a named session's and anything else are never touched. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/9p_io.zig124
-rw-r--r--src/tty/tty.zig2
2 files changed, 126 insertions, 0 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig
index e475416e..fe1a707f 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -825,6 +825,7 @@ pub const Listener = struct {
pardes.turn.wake_parked = null;
pardes.turn.answer_held = null;
pardes.turn.stop();
+ own_socket_len = 0; // removed here, not again by a signal
if (l.path_len != 0) {
var z: [sun_path_len:0]u8 = undefined;
@memcpy(z[0..l.path_len], l.path_buf[0..l.path_len]);
@@ -835,6 +836,90 @@ pub const Listener = struct {
}
};
+/// The socket this process listens on, kept where a fatal signal's handler
+/// can reach it without allocating (unlinkOwnSocket); empty when none.
+var own_socket: [sun_path_len:0]u8 = undefined;
+var own_socket_len: usize = 0;
+
+/// Removes the socket this process listens on. Async-signal-safe (one
+/// unlink): SIGTERM and SIGHUP call it before the signal's own death, so a
+/// killed editor leaves no socket behind.
+pub fn unlinkOwnSocket() void {
+ if (comptime !supported) return;
+ const n = own_socket_len;
+ if (n == 0) return;
+ own_socket_len = 0;
+ _ = libc.unlink(own_socket[0..n :0]);
+}
+
+/// SIGTERM and SIGHUP with no handler of their own (a GUI's): the socket
+/// goes, then the signal's own death. A host that handles them itself (the
+/// terminal's Killed, the detached server's quit) removes it its own way.
+fn armSocketCleanup() void {
+ const sig = std.posix.SIG;
+ const on: std.posix.Sigaction = .{ .handler = .{ .handler = onFatalSignal }, .mask = std.posix.sigemptyset(), .flags = std.posix.SA.RESETHAND };
+ for ([_]std.posix.SIG{ sig.TERM, sig.HUP }) |s| {
+ var was: std.posix.Sigaction = undefined;
+ std.posix.sigaction(s, null, &was);
+ if (was.handler.handler == sig.DFL) std.posix.sigaction(s, &on, null);
+ }
+}
+
+fn onFatalSignal(s: std.posix.SIG) callconv(.c) void {
+ unlinkOwnSocket();
+ // SA_RESETHAND put the default back: the same signal, now fatal.
+ _ = std.c.raise(s);
+}
+
+/// Removes `pardes-9p-<pid>.sock` files under `dir` whose pid is gone
+/// (kill(pid, 0) answers ESRCH): what an editor killed by a signal it could
+/// not catch, or a crash, left behind. A live pid's socket, a named
+/// session's, anything not a socket of this user's, and one something
+/// still answers on are left alone.
+pub fn sweepDeadSockets(io: std.Io, dir: [:0]const u8) void {
+ if (comptime !supported) return;
+ var names: [8192]u8 = undefined;
+ var staged: usize = 0;
+ {
+ const d = std.Io.Dir.openDirAbsolute(io, dir, .{ .iterate = true }) catch return;
+ defer d.close(io);
+ var read_buf: [std.Io.Dir.Iterator.reader_buffer_len]u8 align(@alignOf(usize)) = undefined;
+ var reader: std.Io.Dir.Reader = .init(d, &read_buf);
+ while (true) {
+ const listed = (reader.next(io) catch break) orelse break;
+ if (deadSocketPid(listed.name) == null) continue;
+ if (staged + 1 + listed.name.len > names.len) break;
+ names[staged] = @intCast(listed.name.len);
+ @memcpy(names[staged + 1 ..][0..listed.name.len], listed.name);
+ staged += 1 + listed.name.len;
+ }
+ }
+ var i: usize = 0;
+ while (i < staged) {
+ const name = names[i + 1 ..][0..names[i]];
+ i += 1 + name.len;
+ const pid = deadSocketPid(name).?;
+ if (pid == libc.getpid()) continue;
+ if (std.posix.errno(std.c.kill(pid, @enumFromInt(0))) != .SRCH) continue;
+ var path_buf: [sun_path_len:0]u8 = undefined;
+ const path = std.fmt.bufPrintSentinel(&path_buf, "{s}/{s}", .{ dir, name }, 0) catch continue;
+ const facts = statNoFollow(path) orelse continue;
+ if (facts.mode & 0o170000 != 0o140000 or facts.uid != libc.getuid()) continue;
+ if (probe(path) == .live) continue;
+ _ = libc.unlink(path);
+ }
+}
+
+/// The pid a `pardes-9p-<pid>.sock` name carries, or null for any other.
+fn deadSocketPid(name: []const u8) ?std.c.pid_t {
+ if (!std.mem.startsWith(u8, name, prefix) or !std.mem.endsWith(u8, name, ".sock")) return null;
+ const digits = name[prefix.len .. name.len - ".sock".len];
+ if (digits.len == 0 or digits.len > 10) return null;
+ for (digits) |c| if (!std.ascii.isDigit(c)) return null;
+ const pid = std.fmt.parseInt(std.c.pid_t, digits, 10) catch return null;
+ return if (pid > 0) pid else null;
+}
+
pub fn socketPath(buf: *[sun_path_len]u8, dir: []const u8, name: []const u8) ?[:0]const u8 {
if (name.len == 0) return null;
if (std.mem.indexOfAny(u8, name, "/\x00") != null) return null;
@@ -851,6 +936,8 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [
return null;
};
if (!ensureSocketDir(dir)) return null;
+ // What dead editors left behind goes first, never a live one's.
+ sweepDeadSockets(io, dir);
const entry_name = if (named.len != 0) named else fallback;
// Checked before anything is made: the turn's hooks point at the
// listener from here on, and a path too long for sun_path used to free
@@ -917,6 +1004,10 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [
l.deinit(gpa);
return null;
}
+ @memcpy(own_socket[0..p.len], p);
+ own_socket[p.len] = 0;
+ own_socket_len = p.len;
+ armSocketCleanup();
l.postToRegistry(entry_name);
if (tcp_dial) |dial| {
if (!std.mem.startsWith(u8, dial, "tcp!")) {
@@ -1134,6 +1225,39 @@ test "a name that is not one path component is no address at all" {
try testing.expect(socketPath(&buf, "/run", "a\x00b") == null);
}
+test "the startup sweep removes a dead pid's socket and leaves a live pid's, a named one and a plain file" {
+ if (comptime !supported) return error.SkipZigTest;
+ var dir_buf: [sun_path_len:0]u8 = undefined;
+ const base = socketDir(&dir_buf) orelse return error.SkipZigTest;
+ if (!ensureSocketDir(base)) return error.SkipZigTest;
+ var sub_buf: [sun_path_len:0]u8 = undefined;
+ const sub = std.fmt.bufPrintSentinel(&sub_buf, "{s}/deadsweep-{d}", .{ base, @as(u32, @intCast(libc.getpid())) }, 0) catch return error.SkipZigTest;
+ if (libc.mkdir(sub, 0o700) != 0) return error.SkipZigTest;
+ var paths: [4][sun_path_len:0]u8 = undefined;
+ // No process has this pid: kill(pid, 0) answers ESRCH.
+ const dead = try std.fmt.bufPrintSentinel(&paths[0], "{s}/" ++ prefix ++ "2147483647.sock", .{sub}, 0);
+ const live = try std.fmt.bufPrintSentinel(&paths[1], "{s}/" ++ prefix ++ "{d}.sock", .{ sub, @as(u32, @intCast(std.c.getppid())) }, 0);
+ const named = try std.fmt.bufPrintSentinel(&paths[2], "{s}/" ++ prefix ++ "work.sock", .{sub}, 0);
+ const plain = try std.fmt.bufPrintSentinel(&paths[3], "{s}/" ++ prefix ++ "2147483646.sock", .{sub}, 0);
+ defer {
+ for ([_][:0]const u8{ dead, live, named, plain }) |p| _ = libc.unlink(p);
+ _ = libc.rmdir(sub);
+ }
+ for ([_][:0]const u8{ dead, live, named }) |p| {
+ const fd = bindSocket(p);
+ if (fd < 0) return error.SkipZigTest;
+ _ = libc.close(fd); // closed: what a killed editor leaves
+ }
+ const fd = libc.open(plain, .{ .CREAT = true, .ACCMODE = .WRONLY }, @as(libc.mode_t, 0o600));
+ if (fd < 0) return error.SkipZigTest;
+ _ = libc.close(fd);
+ sweepDeadSockets(testing.io, sub);
+ try testing.expect(statNoFollow(dead) == null);
+ try testing.expect(statNoFollow(live) != null);
+ try testing.expect(statNoFollow(named) != null);
+ try testing.expect(statNoFollow(plain) != null);
+}
+
test "the registry sweep takes the dead entries and leaves everything else" {
if (comptime !supported) return error.SkipZigTest;
// Under the real runtime directory, because a sun_path is 108 bytes
diff --git a/src/tty/tty.zig b/src/tty/tty.zig
index eabbbe22..8d121b1f 100644
--- a/src/tty/tty.zig
+++ b/src/tty/tty.zig
@@ -1150,6 +1150,8 @@ const Killed = struct {
_ = std.c.write(fd, reset, reset.len);
_ = std.c.tcsetattr(fd, .FLUSH, &cooked);
}
+ // The 9P socket goes too: a killed editor leaves none behind.
+ ninep_io.unlinkOwnSocket();
// SA_RESETHAND put the default back: the same signal, now fatal.
_ = std.c.raise(sig);
}