Commit 00897d8

Eric Bower  ·  2026-08-11 10:16:10 -0400 EDT
parent b7033a8
refactor: ZMX_TASK_COMPLETE now uses a 4ch hex id for task detection

This ensures we are correctly scanning for the task that was actually triggered
from the current daemon.  A zmx session is allowed to `zmx run` to another session
and then it immediately tails that command.  That command will also output
the ZMX_TASK_COMPLETE marker which could mark the parent session as complete
erroneously.

We now look for `ZMX_TASK_COMPLETE:wxyz:0`
3 files changed,  +32, -14
+11, -5
......@@ -362,7 +362,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
362362 @memcpy(scan_buf[marker_carry_len..][0..n], buf[0..n]);
363363 const scan_len = marker_carry_len + n;
364364
365- if (util.findTaskExitMarker(scan_buf[0..scan_len])) |exit_code| {
365+ if (try util.findTaskExitMarker(scan_buf[0..scan_len], daemon.task_id)) |exit_code| {
366366 daemon.task_exit_code = exit_code;
367367 daemon.task_ended_at = @intCast(std.Io.Timestamp.now(io, .real).toSeconds());
368368
......@@ -474,7 +474,7 @@ fn daemonLoop(daemon: *Daemon, gpa: std.mem.Allocator, io: std.Io, server_sock_f
474474 .LabelSet => try daemon.handleLabelSet(gpa, client, msg.payload),
475475 .LabelClear => try daemon.handleLabelClear(gpa, client),
476476 .History => try daemon.handleHistory(gpa, client, &term, msg.payload),
477- .Run => try daemon.handleRun(gpa, client, msg.payload),
477+ .Run => try daemon.handleRun(gpa, io, client, msg.payload),
478478 .Ack, .TaskComplete, .LabelData => {},
479479 .Write => try daemon.handleWrite(gpa, client, msg.payload),
480480 _ => std.log.warn(
......@@ -566,6 +566,7 @@ pub const Daemon = struct {
566566 has_terminal_client: bool = false, // true only after a real attach (.Init received)
567567 created_at: u64, // unix timestamp (ns)
568568 is_task_mode: bool = false, // flag for when session is run as a task
569+ task_id: [4]u8 = undefined,
569570 task_exit_code: ?u8 = null, // null = running or n/a, set when task completes
570571 task_ended_at: ?u64 = null, // timestamp when task exited
571572 pty_fd: i32 = -1, // set by daemonLoop so handleRun can probe the foreground process
......@@ -1102,13 +1103,14 @@ pub const Daemon = struct {
11021103 }
11031104 }
11041105
1105- pub fn handleRun(self: *Daemon, gpa: std.mem.Allocator, client: *Client, payload: []const u8) !void {
1106+ pub fn handleRun(self: *Daemon, gpa: std.mem.Allocator, io: std.Io, client: *Client, payload: []const u8) !void {
11061107 // Reset task tracking so the new command's exit marker is detected.
11071108 // Without this, a second `zmx run` on the same session is ignored
11081109 // because task_exit_code is still set from the first run.
11091110 self.task_exit_code = null;
11101111 self.task_ended_at = null;
11111112 self.is_task_mode = true;
1113+ self.task_id = util.generateTaskId(io);
11121114
11131115 if (payload.len == 0) return;
11141116
......@@ -1118,8 +1120,12 @@ pub const Daemon = struct {
11181120 // exit code of the command (not the `;`). The sole exception is when
11191121 // the command contains a heredoc (`<<`), the delimiter must be alone
11201122 // on its line, so the marker goes on the next line instead.
1121- const single_line_marker = "; echo ZMX_TASK_COMPLETED:$?\r";
1122- const heredoc_marker = "\r\necho ZMX_TASK_COMPLETED:$?\r";
1123+ var buf: [1024]u8 = undefined;
1124+ const marker = try util.getTaskExitMarker(&buf, self.task_id);
1125+ var single_buf: [1024]u8 = undefined;
1126+ const single_line_marker = try std.fmt.bufPrint(&single_buf, "; echo {s}$?\r", .{marker});
1127+ var here_buf: [1024]u8 = undefined;
1128+ const heredoc_marker = try std.fmt.bufPrint(&here_buf, "\r\necho {s}$?\r", .{marker});
11231129 const uses_heredoc = std.mem.indexOf(u8, cmd, "<<") != null;
11241130
11251131 if (cmd.len > 0 and cmd[cmd.len - 1] == '\r') {
+6, -5
......@@ -642,15 +642,16 @@ fn tail(alloc: std.mem.Allocator, client_socket_fds: std.ArrayList(i32), detache
642642 },
643643 .Output => {
644644 if (msg.payload.len > 0) {
645+ // TODO: figure out how to bring this back
645646 // Fallback: scan output for task exit marker in case
646647 // .TaskComplete was lost (e.g. daemon exited before
647648 // flushing). This ensures we detect completion even
648649 // when the IPC message doesn't arrive.
649- if (task_complete_code == null and is_run_cmd) {
650- if (util.findTaskExitMarker(msg.payload)) |ec| {
651- task_complete_code = ec;
652- }
653- }
650+ // if (task_complete_code == null and is_run_cmd) {
651+ // if (util.findTaskExitMarker(msg.payload)) |ec| {
652+ // task_complete_code = ec;
653+ // }
654+ // }
654655
655656 // Strip the first line (command echo) for run mode.
656657 var payload = msg.payload;
+15, -4
......@@ -350,13 +350,24 @@ test "rewritePromptRedraw: embedded in larger output" {
350350 try std.testing.expectEqualStrings("some output\r\n\x1b]133;A;redraw=0\x07prompt$ \x1b]133;B\x07", result);
351351 }
352352
353-pub fn findTaskExitMarker(output: []const u8) ?u8 {
354- const marker = "ZMX_TASK_COMPLETED:";
353+pub fn generateTaskId(io: std.Io) [4]u8 {
354+ var bytes: [2]u8 = undefined;
355+ io.random(&bytes);
356+ return std.fmt.bytesToHex(bytes, .lower);
357+}
358+
359+pub fn getTaskExitMarker(buf: []u8, id_marker: [4]u8) ![]u8 {
360+ return std.fmt.bufPrint(buf, "ZMX_TASK_COMPLETED:{s}:", .{id_marker});
361+}
362+
363+pub fn findTaskExitMarker(output: []const u8, id_marker: [4]u8) !?u8 {
364+ var buf: [1024]u8 = undefined;
365+ const marker = try getTaskExitMarker(&buf, id_marker);
355366
356367 // The command line is echoed back by the PTY (canonical mode) before the
357368 // shell evaluates it, so the *first* occurrence of the marker in the
358- // output is often the literal, unexpanded "ZMX_TASK_COMPLETED:$?" from
359- // the echo, not the real "ZMX_TASK_COMPLETED:<code>" written once the
369+ // output is often the literal, unexpanded "ZMX_TASK_COMPLETED:{id}:$?" from
370+ // the echo, not the real "ZMX_TASK_COMPLETED:{id}:<code>" written once the
360371 // shell actually runs it. Keep scanning past unparseable occurrences
361372 // instead of giving up on the first one.
362373 var search_start: usize = 0;