diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-30 15:14:00 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | 52f3e3615f77f690739c0a3c30ff400a2223ed34 (patch) | |
| tree | 41e3cd28b083bc37846d0a5023d12d7c4c483bdc /src/9p_io.zig | |
| parent | 864407b240b812743d6f57a56daf9706b36fc027 (diff) | |
| download | pardes-52f3e3615f77f690739c0a3c30ff400a2223ed34.tar.gz pardes-52f3e3615f77f690739c0a3c30ff400a2223ed34.zip | |
The 9P socket appears already listening: it is listened on under a name of its own and renamed into place
A client that waits for the socket file and then connects, as fs.py's
session helper and scripts do, was sometimes refused under load. The
listener bound the socket at its final name, so the file existed a moment
before listen(2), and a connect in that window got ECONNREFUSED. Now it
listens under `<name>.<pid>`, is chmodded, then renamed over the final
name, so that name only ever names a listening socket. A final name held
by a live listener is still refused, and a name with no room for the
suffix is bound in place as before. The registry test checks the socket
is listening where it appears and that the temporary name is gone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/9p_io.zig')
| -rw-r--r-- | src/9p_io.zig | 29 |
1 files changed, 28 insertions, 1 deletions
diff --git a/src/9p_io.zig b/src/9p_io.zig index 0eaa2d7e..e475416e 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -874,7 +874,29 @@ pub fn listen(io: std.Io, gpa: std.mem.Allocator, core: *pardes.Pardes, named: [ .greet_timeout_ms = Listener.greet_deadline_ms, }); const p = socketPath(&l.path_buf, dir, entry_name).?; - _ = l.runner.listen(.{ .unix = p }, max_conns) catch |err| retry: { + // Listened on under a name of its own, then renamed into place: a client + // that waits for the socket to appear finds it listening already. Bound + // at its own name, it was there a moment before listen(2), and a connect + // then was refused. (No room for the longer name: bound in place.) + var tmp_buf: [sun_path_len]u8 = undefined; + if (std.fmt.bufPrintSentinel(&tmp_buf, "{s}.{d}", .{ p, libc.getpid() }, 0) catch null) |tmp| { + const existing = @import("fs.zig").statPath(io, p, .{ .follow_symlinks = false }) catch null; + if (existing != null and (existing.?.kind != .unix_domain_socket or alive(p))) { + log.warn("something is already listening on {s}", .{p}); + l.deinit(gpa); + return null; + } + _ = libc.unlink(tmp); + _ = l.runner.listen(.{ .unix = tmp }, max_conns) catch { + l.deinit(gpa); + return null; + }; + if (libc.chmod(tmp, 0o600) != 0 or libc.rename(tmp, p) != 0) { + _ = libc.unlink(tmp); + l.deinit(gpa); + return null; + } + } else _ = l.runner.listen(.{ .unix = p }, max_conns) catch |err| retry: { const existing = @import("fs.zig").statPath(io, p, .{ .follow_symlinks = false }) catch null; if (err != error.AddressInUse or existing == null or existing.?.kind != .unix_domain_socket or alive(p)) { log.warn("something is already listening on {s}", .{p}); @@ -1338,6 +1360,11 @@ test "a listening editor posts itself into the 9P registry and unposts on stop" // The socket stays exactly where pardes has always bound it: // adopting the registry moves nothing, it only advertises. try testing.expect(statNoFollow(sock) != null); + // It appeared already listening, renamed into place from a name of + // its own, which is gone. + try testing.expect(alive(sock)); + var tmp_buf: [sun_path_len:0]u8 = undefined; + try testing.expect(statNoFollow(try std.fmt.bufPrintSentinel(&tmp_buf, "{s}.{d}", .{ sock, libc.getpid() }, 0)) == null); // And the registry holds a symlink to it one directory down, so // several editors group under /mnt/9p/pardes/ instead of // crowding the registry root — the layout zmx posts its |
