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) {
2424 _,
2525 };
2626
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+
2733 pub const Header = packed struct {
2834 tag: Tag,
2935 len: u32,
......@@ -184,8 +190,8 @@ const ConnectError = error{
184190 Unexpected,
185191 };
186192
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.
189195 pub fn connectSession(socket_path: []const u8) ConnectError!i32 {
190196 return socket.sessionConnect(socket_path) catch |err| switch (err) {
191197 error.ConnectionRefused => return error.ConnectionRefused,
......@@ -239,14 +245,31 @@ pub fn probeSession(
239245 return error.Unexpected;
240246 }
241247
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+// ╚════════════════════════════════════════════════════════════════════╝
242257 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.
245258 try std.testing.expectEqual(@as(usize, 552), @sizeOf(Info));
246259 // packed struct{u8,u32} backs to u40 → @sizeOf rounds to 8, not 5.
247260 try std.testing.expectEqual(@as(usize, 8), @sizeOf(Header));
248261 }
249262
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+
250273 test "zeroed Info has no stack garbage in wire bytes" {
251274 var info = std.mem.zeroes(Info);
252275 info.clients_len = 3;
+3, -3
......@@ -1766,14 +1766,14 @@ fn switchSesh(daemon: *Daemon, current_sesh: []const u8) !void {
17661766 w.interface.flush() catch {};
17671767 return error.SessionNotFound;
17681768 }
1769- const result = ipc.probeSession(daemon.alloc, socket_path) catch |err| {
1769+ const fd = ipc.connectSession(socket_path) catch |err| {
17701770 std.log.err("session unresponsive: {s}", .{@errorName(err)});
17711771 if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, current_sesh);
17721772 return;
17731773 };
1774- defer posix.close(result.fd);
1774+ defer posix.close(fd);
17751775
1776- ipc.send(result.fd, .Switch, next_session) catch |err| switch (err) {
1776+ ipc.send(fd, .Switch, next_session) catch |err| switch (err) {
17771777 error.BrokenPipe, error.ConnectionResetByPeer => return,
17781778 else => return err,
17791779 };