repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
87cc98f
parent
d12b7cc
author
Eric Bower
date
2026-03-03 20:32:47 -0500 EST
fix(run): stdin regression with ZMX_TASK_COMPLETED

We were not actually running the command sent into stdin properly.

Closes: https://github.com/neurosnap/zmx/issues/71
1 files changed,  +29, -27
M src/main.zig
+29, -27
  1@@ -445,18 +445,6 @@ pub fn main() !void {
  2         while (args.next()) |arg| {
  3             try cmd_args_raw.append(alloc, arg);
  4         }
  5-        var cmd_args = try cmd_args_raw.clone(alloc);
  6-        defer cmd_args.deinit(alloc);
  7-
  8-        const shell = detectShell();
  9-        // add a task completed marker so we know when the cmd is finished
 10-        // we also capture the exit status
 11-        if (std.mem.eql(u8, std.fs.path.basename(shell), "fish")) {
 12-            // fish has special handling for capturing exit status
 13-            try cmd_args.append(alloc, "; echo ZMX_TASK_COMPLETED:$status");
 14-        } else {
 15-            try cmd_args.append(alloc, "; echo ZMX_TASK_COMPLETED:$?");
 16-        }
 17         const clients = try std.ArrayList(*Client).initCapacity(alloc, 10);
 18 
 19         var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
 20@@ -480,7 +468,7 @@ pub fn main() !void {
 21         };
 22         daemon.socket_path = try getSocketPath(alloc, cfg.socket_dir, sesh);
 23         std.log.info("socket path={s}", .{daemon.socket_path});
 24-        return run(&daemon, cmd_args.items);
 25+        return run(&daemon, cmd_args_raw.items);
 26     } else if (std.mem.eql(u8, cmd, "wait") or std.mem.eql(u8, cmd, "w")) {
 27         var args_raw: std.ArrayList([]const u8) = .empty;
 28         defer {
 29@@ -1055,6 +1043,10 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
 30     var buf: [4096]u8 = undefined;
 31     var w = std.fs.File.stdout().writer(&buf);
 32 
 33+    var cmd_to_send: ?[]const u8 = null;
 34+    var allocated_cmd: ?[]u8 = null;
 35+    defer if (allocated_cmd) |cmd| alloc.free(cmd);
 36+
 37     const result = try ensureSession(daemon);
 38     if (result.is_daemon) return;
 39 
 40@@ -1063,9 +1055,16 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
 41         try w.interface.flush();
 42     }
 43 
 44-    var cmd_to_send: ?[]const u8 = null;
 45-    var allocated_cmd: ?[]u8 = null;
 46-    defer if (allocated_cmd) |cmd| alloc.free(cmd);
 47+    const shell = detectShell();
 48+    const shell_basename = std.fs.path.basename(shell);
 49+    const inline_task_marker = if (std.mem.eql(u8, shell_basename, "fish"))
 50+        "; echo ZMX_TASK_COMPLETED:$status"
 51+    else
 52+        "; echo ZMX_TASK_COMPLETED:$?";
 53+    const stdin_task_marker = if (std.mem.eql(u8, shell_basename, "fish"))
 54+        "echo ZMX_TASK_COMPLETED:$status"
 55+    else
 56+        "echo ZMX_TASK_COMPLETED:$?";
 57 
 58     if (command_args.len > 0) {
 59         var parts: std.ArrayList([]const u8) = .empty;
 60@@ -1075,11 +1074,8 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
 61         }
 62 
 63         var total_len: usize = 0;
 64-        for (command_args, 0..) |arg, i| {
 65-            // Last arg is the sentinel (e.g. "; echo ZMX_TASK_COMPLETED:$?")
 66-            // which must not be quoted so the shell interprets it.
 67-            const is_last = i == command_args.len - 1;
 68-            if (!is_last and shellNeedsQuoting(arg)) {
 69+        for (command_args) |arg| {
 70+            if (shellNeedsQuoting(arg)) {
 71                 const quoted = try shellQuote(alloc, arg);
 72                 try parts.append(alloc, quoted);
 73                 total_len += quoted.len + 1;
 74@@ -1090,20 +1086,22 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
 75             }
 76         }
 77 
 78+        total_len += inline_task_marker.len + 1;
 79+
 80         const cmd_buf = try alloc.alloc(u8, total_len);
 81         allocated_cmd = cmd_buf;
 82 
 83         var offset: usize = 0;
 84-        for (parts.items, 0..) |part, i| {
 85+        for (parts.items) |part| {
 86             @memcpy(cmd_buf[offset .. offset + part.len], part);
 87             offset += part.len;
 88-            if (i < parts.items.len - 1) {
 89-                cmd_buf[offset] = ' ';
 90-            } else {
 91-                cmd_buf[offset] = '\n';
 92-            }
 93+            cmd_buf[offset] = ' ';
 94             offset += 1;
 95         }
 96+
 97+        @memcpy(cmd_buf[offset .. offset + inline_task_marker.len], inline_task_marker);
 98+        offset += inline_task_marker.len;
 99+        cmd_buf[offset] = '\n';
100         cmd_to_send = cmd_buf;
101     } else {
102         const stdin_fd = posix.STDIN_FILENO;
103@@ -1126,6 +1124,10 @@ fn run(daemon: *Daemon, command_args: [][]const u8) !void {
104                 if (needs_newline) {
105                     try stdin_buf.append(alloc, '\n');
106                 }
107+
108+                try stdin_buf.appendSlice(alloc, stdin_task_marker);
109+                try stdin_buf.append(alloc, '\n');
110+
111                 cmd_to_send = try alloc.dupe(u8, stdin_buf.items);
112                 allocated_cmd = @constCast(cmd_to_send.?);
113             }