Commit 085fe3e
Ian Tay
·
2026-04-10 12:06:49 -0400 EDT
parent e889a18
refactor(ipc): freeze Tag wire values; switch via connectSession
2 files changed,
+30,
-7
+27,
-4
| ... | ... | @@ -24,6 +24,12 @@ pub const Tag = enum(u8) { | |
| 24 | 24 | _, | |
| 25 | 25 | }; | |
| 26 | 26 | ||
| 27 | + | comptime { | |
| 28 | + | if (@typeInfo(Tag).@"enum".is_exhaustive) @compileError( | |
| 29 | + | "ipc.Tag must stay non-exhaustive — old daemons rely on `_` to ignore unknown tags", | |
| 30 | + | ); | |
| 31 | + | } | |
| 32 | + | ||
| 27 | 33 | pub const Header = packed struct { | |
| 28 | 34 | tag: Tag, | |
| 29 | 35 | len: u32, |
| ... | ... | @@ -184,8 +190,8 @@ const ConnectError = error{ | |
| 184 | 190 | Unexpected, | |
| 185 | 191 | }; | |
| 186 | 192 | ||
| 187 | - | /// Unlike `probeSession`, does not round-trip `Info` — kill/detach/history/run | |
| 188 | - | /// stay usable against version-skewed daemons. | |
| 193 | + | /// Connect-only liveness check. Callers that don't read `Info` should use | |
| 194 | + | /// this (not `probeSession`) so they survive `Info` shape changes. | |
| 189 | 195 | pub fn connectSession(socket_path: []const u8) ConnectError!i32 { | |
| 190 | 196 | return socket.sessionConnect(socket_path) catch |err| switch (err) { | |
| 191 | 197 | error.ConnectionRefused => return error.ConnectionRefused, |
| ... | ... | @@ -239,14 +245,31 @@ pub fn probeSession( | |
| 239 | 245 | return error.Unexpected; | |
| 240 | 246 | } | |
| 241 | 247 | ||
| 248 | + | // ╔════════════════════════════════════════════════════════════════════╗ | |
| 249 | + | // ║ WIRE PROTOCOL FREEZE — read before "fixing" any test below. ║ | |
| 250 | + | // ║ ║ | |
| 251 | + | // ║ Changing these constants does not fix the test; it breaks every ║ | |
| 252 | + | // ║ running daemon for every user until they `pkill -f zmx`. ║ | |
| 253 | + | // ║ ║ | |
| 254 | + | // ║ Need a new field? → add a new `Tag` value (next free integer). ║ | |
| 255 | + | // ║ Need to remove one? → don't. Reserve the integer, stop sending it. ║ | |
| 256 | + | // ╚════════════════════════════════════════════════════════════════════╝ | |
| 242 | 257 | test "Info wire size is frozen" { | |
| 243 | - | // Bumping this means version-skewed `zmx list` breaks. See doc comment | |
| 244 | - | // on `Info` — add a new `Tag` instead of growing this struct. | |
| 245 | 258 | try std.testing.expectEqual(@as(usize, 552), @sizeOf(Info)); | |
| 246 | 259 | // packed struct{u8,u32} backs to u40 → @sizeOf rounds to 8, not 5. | |
| 247 | 260 | try std.testing.expectEqual(@as(usize, 8), @sizeOf(Header)); | |
| 248 | 261 | } | |
| 249 | 262 | ||
| 263 | + | test "Tag wire values are frozen" { | |
| 264 | + | inline for (.{ | |
| 265 | + | .{ Tag.Input, 0 }, .{ Tag.Output, 1 }, .{ Tag.Resize, 2 }, | |
| 266 | + | .{ Tag.Detach, 3 }, .{ Tag.DetachAll, 4 }, .{ Tag.Kill, 5 }, | |
| 267 | + | .{ Tag.Info, 6 }, .{ Tag.Init, 7 }, .{ Tag.History, 8 }, | |
| 268 | + | .{ Tag.Run, 9 }, .{ Tag.Ack, 10 }, .{ Tag.Switch, 11 }, | |
| 269 | + | .{ Tag.Write, 12 }, .{ Tag.TaskComplete, 13 }, | |
| 270 | + | }) |p| try std.testing.expectEqual(@as(u8, p[1]), @intFromEnum(p[0])); | |
| 271 | + | } | |
| 272 | + | ||
| 250 | 273 | test "zeroed Info has no stack garbage in wire bytes" { | |
| 251 | 274 | var info = std.mem.zeroes(Info); | |
| 252 | 275 | info.clients_len = 3; |
+3,
-3
| ... | ... | @@ -1766,14 +1766,14 @@ fn switchSesh(daemon: *Daemon, current_sesh: []const u8) !void { | |
| 1766 | 1766 | w.interface.flush() catch {}; | |
| 1767 | 1767 | return error.SessionNotFound; | |
| 1768 | 1768 | } | |
| 1769 | - | const result = ipc.probeSession(daemon.alloc, socket_path) catch |err| { | |
| 1769 | + | const fd = ipc.connectSession(socket_path) catch |err| { | |
| 1770 | 1770 | std.log.err("session unresponsive: {s}", .{@errorName(err)}); | |
| 1771 | 1771 | if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, current_sesh); | |
| 1772 | 1772 | return; | |
| 1773 | 1773 | }; | |
| 1774 | - | defer posix.close(result.fd); | |
| 1774 | + | defer posix.close(fd); | |
| 1775 | 1775 | ||
| 1776 | - | ipc.send(result.fd, .Switch, next_session) catch |err| switch (err) { | |
| 1776 | + | ipc.send(fd, .Switch, next_session) catch |err| switch (err) { | |
| 1777 | 1777 | error.BrokenPipe, error.ConnectionResetByPeer => return, | |
| 1778 | 1778 | else => return err, | |
| 1779 | 1779 | }; |