Commit 8ba312d

凡辞  ·  2026-07-22 12:02:23 -0400 EDT
parent 935c484
fix(send): preserve client leadership
3 files changed,  +34, -4
+1, -0
......@@ -19,6 +19,7 @@ Use spec: https://common-changelog.org/
1919 - `zmx run` will now detect heredocs and add the completion marker to a newline
2020 - Race between `zmx kill X; zmx run X`
2121 - Improved claim leader detection
22+- `zmx send` no longer claims client leadership or triggers a resize probe
2223 - Improved `ctrl+\` key detection
2324 - Support for linux kernel < 4.11 by avoiding statx calls
2425
+5, -3
......@@ -22,15 +22,16 @@ pub const Tag = enum(u8) {
2222 LabelSet = 15,
2323 LabelClear = 16,
2424 LabelData = 17,
25+ Send = 18,
2526 // Non-exhaustive: this enum comes off the wire via bytesToValue and
26- // @enumFromInt, so out-of-range values (14-255) are representable
27+ // @enumFromInt, so out-of-range values are representable
2728 // rather than UB. Switches must handle `_` (unknown tag).
2829 _,
2930 };
3031
3132 comptime {
3233 if (@typeInfo(Tag).@"enum".is_exhaustive) @compileError(
33- "ipc.Tag must stay non-exhaustive — old daemons rely on `_` to ignore unknown tags",
34+ "ipc.Tag must stay non-exhaustive -- old daemons rely on `_` to ignore unknown tags",
3435 );
3536 }
3637
......@@ -57,7 +58,7 @@ pub fn getTerminalSize(fd: i32) Resize {
5758 pub const MAX_CMD_LEN = 256;
5859 pub const MAX_CWD_LEN = 256;
5960
60-/// Frozen wire shape. Do NOT add fields — new stats go in new `Tag` values
61+/// Frozen wire shape. Do NOT add fields! New stats go in new `Tag` values
6162 /// so old daemons (whose `_` arm ignores unknown tags) stay reachable.
6263 /// Changing `@sizeOf(Info)` breaks `zmx list` against running daemons.
6364 pub const Info = extern struct {
......@@ -306,6 +307,7 @@ test "Tag wire values are frozen" {
306307 .{ Tag.Run, 9 }, .{ Tag.Ack, 10 }, .{ Tag.Switch, 11 },
307308 .{ Tag.Write, 12 }, .{ Tag.TaskComplete, 13 }, .{ Tag.LabelGet, 14 },
308309 .{ Tag.LabelSet, 15 }, .{ Tag.LabelClear, 16 }, .{ Tag.LabelData, 17 },
310+ .{ Tag.Send, 18 },
309311 }) |p| try std.testing.expectEqual(@as(u8, p[1]), @intFromEnum(p[0]));
310312 }
311313
+28, -1
......@@ -287,7 +287,7 @@ pub fn main() !void {
287287 error.NameTooLong => return socket.printSessionNameTooLong(sesh, cfg.socket_dir),
288288 error.OutOfMemory => return err,
289289 };
290- return send(&cfg, sesh, socket_path, text_parts.items, .Input);
290+ return send(&cfg, sesh, socket_path, text_parts.items, .Send);
291291 } else if (std.mem.eql(u8, cmd, "print") or std.mem.eql(u8, cmd, "p")) {
292292 const session_name = args.next() orelse "";
293293 if (std.mem.eql(u8, session_name, "--help") or std.mem.eql(u8, session_name, "-h")) {
......@@ -1046,6 +1046,11 @@ const Daemon = struct {
10461046 }
10471047 }
10481048
1049+ /// Queue input from `zmx send` without changing interactive client leadership.
1050+ pub fn handleSend(self: *Daemon, payload: []const u8) void {
1051+ self.queuePtyInput(payload);
1052+ }
1053+
10491054 pub fn handleSwitch(self: *Daemon, session_name: []const u8) !void {
10501055 for (self.clients.items) |client| {
10511056 if (self.leader_client_fd == client.socket_fd) {
......@@ -1371,6 +1376,27 @@ const Daemon = struct {
13711376 }
13721377 };
13731378
1379+test "send queues PTY input without changing leader" {
1380+ const alloc = std.testing.allocator;
1381+ var daemon = Daemon{
1382+ .cfg = undefined,
1383+ .alloc = alloc,
1384+ .clients = .empty,
1385+ .leader_client_fd = 42,
1386+ .session_name = "test",
1387+ .socket_path = "",
1388+ .running = true,
1389+ .pid = 0,
1390+ .created_at = 0,
1391+ };
1392+ defer daemon.pty_write_buf.deinit(alloc);
1393+
1394+ daemon.handleSend("hello");
1395+
1396+ try std.testing.expectEqual(@as(?i32, 42), daemon.leader_client_fd);
1397+ try std.testing.expectEqualStrings("hello", daemon.pty_write_buf.items);
1398+}
1399+
13741400 fn printVersion(cfg: *Cfg) !void {
13751401 var buf: [256]u8 = undefined;
13761402 var w = std.fs.File.stdout().writer(&buf);
......@@ -3042,6 +3068,7 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
30423068 while (client.read_buf.next()) |msg| {
30433069 switch (msg.header.tag) {
30443070 .Input => try daemon.handleInput(client, msg.payload),
3071+ .Send => daemon.handleSend(msg.payload),
30453072 .Output => try daemon.handleOutput(msg.payload, &vt_stream),
30463073 .Init => try daemon.handleInit(client, pty_fd, &term, msg.payload),
30473074 .Switch => try daemon.handleSwitch(msg.payload),