- commit
- 085fe3e
- parent
- e889a18
- author
- Ian Tay
- date
- 2026-04-10 12:06:49 -0400 EDT
refactor(ipc): freeze Tag wire values; switch via connectSession
2 files changed,
+30,
-7
+27,
-4
1@@ -24,6 +24,12 @@ pub const Tag = enum(u8) {
2 _,
3 };
4
5+comptime {
6+ if (@typeInfo(Tag).@"enum".is_exhaustive) @compileError(
7+ "ipc.Tag must stay non-exhaustive — old daemons rely on `_` to ignore unknown tags",
8+ );
9+}
10+
11 pub const Header = packed struct {
12 tag: Tag,
13 len: u32,
14@@ -184,8 +190,8 @@ const ConnectError = error{
15 Unexpected,
16 };
17
18-/// Unlike `probeSession`, does not round-trip `Info` — kill/detach/history/run
19-/// stay usable against version-skewed daemons.
20+/// Connect-only liveness check. Callers that don't read `Info` should use
21+/// this (not `probeSession`) so they survive `Info` shape changes.
22 pub fn connectSession(socket_path: []const u8) ConnectError!i32 {
23 return socket.sessionConnect(socket_path) catch |err| switch (err) {
24 error.ConnectionRefused => return error.ConnectionRefused,
25@@ -239,14 +245,31 @@ pub fn probeSession(
26 return error.Unexpected;
27 }
28
29+// ╔════════════════════════════════════════════════════════════════════╗
30+// ║ WIRE PROTOCOL FREEZE — read before "fixing" any test below. ║
31+// ║ ║
32+// ║ Changing these constants does not fix the test; it breaks every ║
33+// ║ running daemon for every user until they `pkill -f zmx`. ║
34+// ║ ║
35+// ║ Need a new field? → add a new `Tag` value (next free integer). ║
36+// ║ Need to remove one? → don't. Reserve the integer, stop sending it. ║
37+// ╚════════════════════════════════════════════════════════════════════╝
38 test "Info wire size is frozen" {
39- // Bumping this means version-skewed `zmx list` breaks. See doc comment
40- // on `Info` — add a new `Tag` instead of growing this struct.
41 try std.testing.expectEqual(@as(usize, 552), @sizeOf(Info));
42 // packed struct{u8,u32} backs to u40 → @sizeOf rounds to 8, not 5.
43 try std.testing.expectEqual(@as(usize, 8), @sizeOf(Header));
44 }
45
46+test "Tag wire values are frozen" {
47+ inline for (.{
48+ .{ Tag.Input, 0 }, .{ Tag.Output, 1 }, .{ Tag.Resize, 2 },
49+ .{ Tag.Detach, 3 }, .{ Tag.DetachAll, 4 }, .{ Tag.Kill, 5 },
50+ .{ Tag.Info, 6 }, .{ Tag.Init, 7 }, .{ Tag.History, 8 },
51+ .{ Tag.Run, 9 }, .{ Tag.Ack, 10 }, .{ Tag.Switch, 11 },
52+ .{ Tag.Write, 12 }, .{ Tag.TaskComplete, 13 },
53+ }) |p| try std.testing.expectEqual(@as(u8, p[1]), @intFromEnum(p[0]));
54+}
55+
56 test "zeroed Info has no stack garbage in wire bytes" {
57 var info = std.mem.zeroes(Info);
58 info.clients_len = 3;
+3,
-3
1@@ -1766,14 +1766,14 @@ fn switchSesh(daemon: *Daemon, current_sesh: []const u8) !void {
2 w.interface.flush() catch {};
3 return error.SessionNotFound;
4 }
5- const result = ipc.probeSession(daemon.alloc, socket_path) catch |err| {
6+ const fd = ipc.connectSession(socket_path) catch |err| {
7 std.log.err("session unresponsive: {s}", .{@errorName(err)});
8 if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, current_sesh);
9 return;
10 };
11- defer posix.close(result.fd);
12+ defer posix.close(fd);
13
14- ipc.send(result.fd, .Switch, next_session) catch |err| switch (err) {
15+ ipc.send(fd, .Switch, next_session) catch |err| switch (err) {
16 error.BrokenPipe, error.ConnectionResetByPeer => return,
17 else => return err,
18 };