29d88ab2
refactor: sockpath follow-through — the seam gets its pin, the optional goes, the vestigial ordering is named
a73x 2026-08-12 19:25
Commit message
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -191,8 +191,8 @@ pub fn build(b: *std.Build) void { | |||
| 191 | .root_source_file = b.path("src/sockpath.zig"), | 191 | .root_source_file = b.path("src/sockpath.zig"), |
| 192 | .target = target, | 192 | .target = target, |
| 193 | .optimize = optimize, | 193 | .optimize = optimize, |
| 194 | .link_libc = true, | ||
| 195 | }); | 194 | }); |
| 195 | sockpath_mod.addImport("testtmp", testtmp_mod); | ||
| 196 | 196 | ||
| 197 | const server_mod = b.createModule(.{ | 197 | const server_mod = b.createModule(.{ |
| 198 | .root_source_file = b.path("src/server.zig"), | 198 | .root_source_file = b.path("src/server.zig"), |
src/delta.zig
| Old | New | ||
|---|---|---|---|
| @@ -1,7 +1,8 @@ | |||
| 1 | //! Row-level change tracking behind the delta stream: one content hash per | 1 | //! The daemon's half of the delta stream. This side holds the authoritative |
| 2 | //! viewport row, so an engine update can be sent as just the rows that | 2 | //! grid and decides WHICH rows a client is missing; the far side is a |
| 3 | //! actually changed, and a reattach can be answered with a delta instead of | 3 | //! replica that applies whatever payload arrives without knowing how it was |
| 4 | //! a full snapshot. | 4 | //! chosen. protocol.zig owns the bytes those payloads are made of — this |
| 5 | //! owns which rows go into them. | ||
| 5 | //! | 6 | //! |
| 6 | //! Engine and protocol are the whole of its world — no daemon, no clients, | 7 | //! Engine and protocol are the whole of its world — no daemon, no clients, |
| 7 | //! no sockets — which is what lets the tracker be driven directly by a test | 8 | //! no sockets — which is what lets the tracker be driven directly by a test |
| @@ -134,6 +135,8 @@ pub const DeltaTracker = struct { | |||
| 134 | /// the tracker does not know which daemon instance it belongs to. | 135 | /// the tracker does not know which daemon instance it belongs to. |
| 135 | /// | 136 | /// |
| 136 | /// 0 is never serviceable: it is what a client says when it holds | 137 | /// 0 is never serviceable: it is what a client says when it holds |
| 138 | /// nothing. Neither is any seq against an unbuilt tracker — one that has | ||
| 139 | /// never been built, or whose rebuild failed partway, can describe | ||
| 137 | /// nothing. | 140 | /// nothing. |
| 138 | pub fn canServe(self: *const DeltaTracker, have_seq: u64) bool { | 141 | pub fn canServe(self: *const DeltaTracker, have_seq: u64) bool { |
| 139 | return have_seq != 0 and | 142 | return have_seq != 0 and |
src/server.zig
| Old | New | ||
|---|---|---|---|
| @@ -218,8 +218,10 @@ pub const Server = struct { | |||
| 218 | listener: std.net.Server, | 218 | listener: std.net.Server, |
| 219 | sock_path: []const u8, | 219 | sock_path: []const u8, |
| 220 | /// What the socket file was when we bound it, so teardown can tell our | 220 | /// What the socket file was when we bound it, so teardown can tell our |
| 221 | /// socket from one that replaced it. See sockpath.PathId. | 221 | /// socket from one that replaced it. See sockpath.PathId. Not optional: |
| 222 | path_id: ?sockpath.PathId, | 222 | /// init cannot return without one, and an absent-means-false arm is the |
| 223 | /// silent no-unlink 6090604 fixed. | ||
| 224 | path_id: sockpath.PathId, | ||
| 223 | /// The attached interactive clients. All of them see every update. | 225 | /// The attached interactive clients. All of them see every update. |
| 224 | clients: [max_clients]?ClientSlot = @splat(null), | 226 | clients: [max_clients]?ClientSlot = @splat(null), |
| 225 | /// How much unsent output one client may accumulate before the daemon | 227 | /// How much unsent output one client may accumulate before the daemon |
| @@ -331,7 +333,10 @@ pub const Server = struct { | |||
| 331 | // newer daemon may have replaced the file since we bound it, and | 333 | // newer daemon may have replaced the file since we bound it, and |
| 332 | // deleting that one would hand its clients the same field incident | 334 | // deleting that one would hand its clients the same field incident |
| 333 | // the socket-steal fix exists to prevent. | 335 | // the socket-steal fix exists to prevent. |
| 334 | const ours: bool = if (self.path_id) |id| id.stillAt(self.sock_path) else false; | 336 | // Statting before the close is vestigial: 8a82225 fstat'd the live |
| 337 | // descriptor, 6090604 removed that stat. After the close would narrow | ||
| 338 | // the successor-claims-the-path window — an M16 change, not an M15 one. | ||
| 339 | const ours: bool = self.path_id.stillAt(self.sock_path); | ||
| 335 | self.listener.deinit(); | 340 | self.listener.deinit(); |
| 336 | if (ours) std.fs.cwd().deleteFile(self.sock_path) catch {}; | 341 | if (ours) std.fs.cwd().deleteFile(self.sock_path) catch {}; |
| 337 | self.tracker.deinit(self.alloc); | 342 | self.tracker.deinit(self.alloc); |
src/sockpath.zig
| Old | New | ||
|---|---|---|---|
| @@ -8,6 +8,7 @@ | |||
| 8 | //! of the daemon. Nothing here knows a Server exists; a path is all it | 8 | //! of the daemon. Nothing here knows a Server exists; a path is all it |
| 9 | //! takes. | 9 | //! takes. |
| 10 | const std = @import("std"); | 10 | const std = @import("std"); |
| 11 | const TmpDir = @import("testtmp").TmpDir; | ||
| 11 | 12 | ||
| 12 | /// A socket file's identity at the moment it was bound, so teardown can | 13 | /// A socket file's identity at the moment it was bound, so teardown can |
| 13 | /// tell our socket from one that replaced it. | 14 | /// tell our socket from one that replaced it. |
| @@ -84,3 +85,38 @@ pub fn claim(path: []const u8) !void { | |||
| 84 | else => |e| return e, | 85 | else => |e| return e, |
| 85 | }; | 86 | }; |
| 86 | } | 87 | } |
| 88 | |||
| 89 | test "PathId: names the file it was taken from, not the path, and not a successor" { | ||
| 90 | var tmp = try TmpDir.make(); | ||
| 91 | defer tmp.cleanup(); | ||
| 92 | |||
| 93 | var buf: [64]u8 = undefined; | ||
| 94 | const sock_path = try std.fmt.bufPrint(&buf, "{s}/id.sock", .{tmp.path()}); | ||
| 95 | |||
| 96 | const addr = try std.net.Address.initUnix(sock_path); | ||
| 97 | var listener = try addr.listen(.{}); | ||
| 98 | const id = try PathId.of(sock_path); | ||
| 99 | |||
| 100 | // A live bound socket answers for itself — which is the whole reason | ||
| 101 | // this type takes the PATH's dev+ino. The guard it replaced stat'd the | ||
| 102 | // LISTENING DESCRIPTOR, which lives in sockfs, against the path's | ||
| 103 | // ordinary filesystem inode: never equal, so this case was false too, | ||
| 104 | // and the daemon silently stopped unlinking its own socket on every | ||
| 105 | // clean exit (6090604). | ||
| 106 | try std.testing.expect(id.stillAt(sock_path)); | ||
| 107 | |||
| 108 | // Nothing at the path: the stat fails, and the answer callers need is | ||
| 109 | // "not ours" rather than an error to handle. The catch arm, which no | ||
| 110 | // test reached before this one. | ||
| 111 | listener.deinit(); | ||
| 112 | try std.fs.cwd().deleteFile(sock_path); | ||
| 113 | try std.testing.expect(!id.stillAt(sock_path)); | ||
| 114 | |||
| 115 | // A successor binds the same NAME. Same path, different file, and | ||
| 116 | // deleting it would strand the daemon that owns it — the field incident | ||
| 117 | // the whole module exists for, in one line. | ||
| 118 | const addr2 = try std.net.Address.initUnix(sock_path); | ||
| 119 | var successor = try addr2.listen(.{}); | ||
| 120 | defer successor.deinit(); | ||
| 121 | try std.testing.expect(!id.stillAt(sock_path)); | ||
| 122 | } | ||