Commit 8e168b6

Valentine Silvansky  ·  2026-07-27 09:32:02 -0400 EDT
parent 0266042
fix: attach hang against pre-0.7.0 daemons
2 files changed,  +44, -8
+41, -0
......@@ -132,6 +132,23 @@ pub fn appendMessage(
132132 }
133133 }
134134
135+/// Pre-0.7.0 daemons expect a 4-byte Init/Resize payload (rows+cols, no
136+/// pixel size) and silently drop the 8-byte form, hanging `zmx attach`
137+/// against a running old daemon (#211). Append both encodings: every daemon
138+/// drops the length it doesn't expect and processes the other exactly once.
139+pub const LEGACY_RESIZE_LEN = 4;
140+
141+pub fn appendSizeMessage(
142+ alloc: std.mem.Allocator,
143+ list: *std.ArrayList(u8),
144+ tag: Tag,
145+ size: Resize,
146+) !void {
147+ const bytes = std.mem.asBytes(&size);
148+ try appendMessage(alloc, list, tag, bytes);
149+ try appendMessage(alloc, list, tag, bytes[0..LEGACY_RESIZE_LEN]);
150+}
151+
135152 fn writeAll(fd: i32, data: []const u8) !void {
136153 var index: usize = 0;
137154 while (index < data.len) {
......@@ -362,6 +379,30 @@ pub fn roundTripForTag(
362379 return error.Unexpected;
363380 }
364381
382+test "appendSizeMessage emits current and legacy encodings" {
383+ const alloc = std.testing.allocator;
384+ var list = try std.ArrayList(u8).initCapacity(alloc, 64);
385+ defer list.deinit(alloc);
386+
387+ const size = Resize{ .rows = 45, .cols = 170, .xpixel = 900, .ypixel = 1800 };
388+ try appendSizeMessage(alloc, &list, .Init, size);
389+
390+ const h1 = std.mem.bytesToValue(Header, list.items[0..@sizeOf(Header)]);
391+ try std.testing.expectEqual(Tag.Init, h1.tag);
392+ try std.testing.expectEqual(@as(u32, @sizeOf(Resize)), h1.len);
393+ const p1 = list.items[@sizeOf(Header)..][0..@sizeOf(Resize)];
394+ try std.testing.expectEqual(size, std.mem.bytesToValue(Resize, p1));
395+
396+ const off2 = @sizeOf(Header) + @sizeOf(Resize);
397+ const h2 = std.mem.bytesToValue(Header, list.items[off2..][0..@sizeOf(Header)]);
398+ try std.testing.expectEqual(Tag.Init, h2.tag);
399+ try std.testing.expectEqual(@as(u32, LEGACY_RESIZE_LEN), h2.len);
400+ // Legacy payload is the rows+cols prefix of the current encoding.
401+ const p2 = list.items[off2 + @sizeOf(Header) ..][0..LEGACY_RESIZE_LEN];
402+ try std.testing.expectEqualSlices(u8, p1[0..LEGACY_RESIZE_LEN], p2);
403+ try std.testing.expectEqual(off2 + @sizeOf(Header) + LEGACY_RESIZE_LEN, list.items.len);
404+}
405+
365406 test "zeroed Info has no stack garbage in wire bytes" {
366407 var info = std.mem.zeroes(Info);
367408 info.clients_len = 3;
+3, -8
......@@ -47,7 +47,7 @@ pub fn clientLoop(client_sock_fd: i32, env_str: []const u8) !ClientResult {
4747
4848 // Send init message with terminal size (buffered)
4949 const size = ipc.getTerminalSize(lib_posix.STDOUT_FILENO);
50- try ipc.appendMessage(gpa, &sock_write_buf, .Init, std.mem.asBytes(&size));
50+ try ipc.appendSizeMessage(gpa, &sock_write_buf, .Init, size);
5151
5252 var poll_fds = try std.ArrayList(lib_posix.pollfd).initCapacity(gpa, 4);
5353 defer poll_fds.deinit(gpa);
......@@ -104,7 +104,7 @@ pub fn clientLoop(client_sock_fd: i32, env_str: []const u8) !ClientResult {
104104 if (poll_fds.items[2].revents & lib_posix.POLL.IN != 0) {
105105 signal.drainSignalPipe();
106106 const next_size = ipc.getTerminalSize(lib_posix.STDOUT_FILENO);
107- try ipc.appendMessage(gpa, &sock_write_buf, .Resize, std.mem.asBytes(&next_size));
107+ try ipc.appendSizeMessage(gpa, &sock_write_buf, .Resize, next_size);
108108 }
109109
110110 // Handle stdin -> socket (Input)
......@@ -160,12 +160,7 @@ pub fn clientLoop(client_sock_fd: i32, env_str: []const u8) !ClientResult {
160160 // daemon is asking for the client's window size usually in response
161161 // to this client being set as leader.
162162 const next_size = ipc.getTerminalSize(lib_posix.STDOUT_FILENO);
163- try ipc.appendMessage(
164- gpa,
165- &sock_write_buf,
166- .Resize,
167- std.mem.asBytes(&next_size),
168- );
163+ try ipc.appendSizeMessage(gpa, &sock_write_buf, .Resize, next_size);
169164 },
170165 .Switch => {
171166 std.log.info("switch session", .{});