- commit
- fbcec11
- parent
- 969ef90
- author
- Eric Bower
- date
- 2026-02-08 21:26:56 -0500 EST
feat(run): track task completion with a marker and record exit code
2 files changed,
+101,
-9
+3,
-0
1@@ -35,6 +35,9 @@ pub const Info = extern struct {
2 cwd_len: u16,
3 cmd: [MAX_CMD_LEN]u8,
4 cwd: [MAX_CWD_LEN]u8,
5+ created_at: u64,
6+ task_ended_at: u64,
7+ task_exit_code: i32,
8 };
9
10 pub fn expectedLength(data: []const u8) ?usize {
+98,
-9
1@@ -121,6 +121,13 @@ const Cfg = struct {
2 }
3 };
4
5+const SessionMetadata = struct {
6+ created_at: u64, // unix timestamp (ns) - all sessions
7+ task_exit_code: ?i32 = null, // null = running, set when task completes
8+ task_end_time: ?u64 = null, // timestamp when task exited
9+ task_command: []const u8 = "", // original task command string
10+};
11+
12 const Daemon = struct {
13 cfg: *Cfg,
14 alloc: std.mem.Allocator,
15@@ -133,6 +140,11 @@ const Daemon = struct {
16 cwd: []const u8 = "",
17 has_pty_output: bool = false,
18 has_had_client: bool = false,
19+ created_at: u64, // unix timestamp (ns)
20+ is_task_mode: bool = false, // flag for when session is run as a task
21+ task_exit_code: ?i32 = null, // null = running or n/a, set when task completes
22+ task_ended_at: ?u64 = null, // timestamp when task exited
23+ task_command: ?[]const []const u8 = null,
24
25 pub fn deinit(self: *Daemon) void {
26 self.clients.deinit(self.alloc);
27@@ -265,7 +277,8 @@ const Daemon = struct {
28 // Build command string from args
29 var cmd_buf: [ipc.MAX_CMD_LEN]u8 = undefined;
30 var cmd_len: u16 = 0;
31- if (self.command) |args| {
32+ const cur_cmd = self.command orelse self.task_command;
33+ if (cur_cmd) |args| {
34 for (args, 0..) |arg, i| {
35 if (i > 0) {
36 if (cmd_len < ipc.MAX_CMD_LEN) {
37@@ -292,6 +305,9 @@ const Daemon = struct {
38 .cwd_len = cwd_len,
39 .cmd = cmd_buf,
40 .cwd = cwd_buf,
41+ .created_at = self.created_at,
42+ .task_ended_at = self.task_ended_at orelse 0,
43+ .task_exit_code = self.task_exit_code orelse 0,
44 };
45 try ipc.appendMessage(self.alloc, &client.write_buf, .Info, std.mem.asBytes(&info));
46 client.has_pending_output = true;
47@@ -407,6 +423,7 @@ pub fn main() !void {
48 .pid = undefined,
49 .command = command,
50 .cwd = cwd,
51+ .created_at = @intCast(std.time.nanoTimestamp()),
52 };
53 daemon.socket_path = try getSocketPath(alloc, cfg.socket_dir, session_name);
54 std.log.info("socket path={s}", .{daemon.socket_path});
55@@ -416,12 +433,23 @@ pub fn main() !void {
56 return error.SessionNameRequired;
57 };
58
59- var command_args: std.ArrayList([]const u8) = .empty;
60- defer command_args.deinit(alloc);
61+ var cmd_args_raw: std.ArrayList([]const u8) = .empty;
62+ defer cmd_args_raw.deinit(alloc);
63 while (args.next()) |arg| {
64- try command_args.append(alloc, arg);
65+ try cmd_args_raw.append(alloc, arg);
66+ }
67+ var cmd_args = try cmd_args_raw.clone(alloc);
68+ defer cmd_args.deinit(alloc);
69+
70+ const shell = detectShell();
71+ // add a task completed marker so we know when the cmd is finished
72+ // we also capture the exit status
73+ if (std.mem.eql(u8, std.fs.path.basename(shell), "fish")) {
74+ // fish has special handling for capturing exit status
75+ try cmd_args.append(alloc, "; echo ZMX_TASK_COMPLETED:$status");
76+ } else {
77+ try cmd_args.append(alloc, "; echo ZMX_TASK_COMPLETED:$?");
78 }
79-
80 const clients = try std.ArrayList(*Client).initCapacity(alloc, 10);
81
82 var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
83@@ -437,10 +465,13 @@ pub fn main() !void {
84 .pid = undefined,
85 .command = null,
86 .cwd = cwd,
87+ .created_at = @intCast(std.time.nanoTimestamp()),
88+ .is_task_mode = true,
89+ .task_command = cmd_args_raw.items,
90 };
91 daemon.socket_path = try getSocketPath(alloc, cfg.socket_dir, session_name);
92 std.log.info("socket path={s}", .{daemon.socket_path});
93- return run(&daemon, command_args.items);
94+ return run(&daemon, cmd_args.items);
95 } else {
96 return help();
97 }
98@@ -500,6 +531,9 @@ const SessionEntry = struct {
99 error_name: ?[]const u8,
100 cmd: ?[]const u8 = null,
101 cwd: ?[]const u8 = null,
102+ created_at: u64,
103+ task_ended_at: ?u64,
104+ task_exit_code: ?i32,
105
106 fn lessThan(_: void, a: SessionEntry, b: SessionEntry) bool {
107 return std.mem.order(u8, a.name, b.name) == .lt;
108@@ -551,8 +585,11 @@ fn list(cfg: *Cfg, short: bool) !void {
109 .clients_len = null,
110 .is_error = true,
111 .error_name = @errorName(err),
112+ .created_at = 0,
113+ .task_exit_code = 1,
114+ .task_ended_at = 0,
115 });
116- cleanupStaleSocket(dir, entry.name);
117+ // cleanupStaleSocket(dir, entry.name);
118 continue;
119 };
120 posix.close(result.fd);
121@@ -575,6 +612,9 @@ fn list(cfg: *Cfg, short: bool) !void {
122 .error_name = null,
123 .cmd = cmd,
124 .cwd = cwd,
125+ .created_at = result.info.created_at,
126+ .task_ended_at = result.info.task_ended_at,
127+ .task_exit_code = result.info.task_exit_code,
128 });
129 }
130 }
131@@ -1098,6 +1138,33 @@ fn clientLoop(_: *Cfg, client_sock_fd: i32) !void {
132 }
133 }
134
135+fn findTaskExitMarker(output: []const u8) ?i32 {
136+ const marker = "ZMX_TASK_COMPLETED:";
137+
138+ // Search for marker in output
139+ if (std.mem.indexOf(u8, output, marker)) |idx| {
140+ const after_marker = output[idx + marker.len ..];
141+
142+ // Find the exit code number and newline
143+ var end_idx: usize = 0;
144+ while (end_idx < after_marker.len and after_marker[end_idx] != '\n' and after_marker[end_idx] != '\r') {
145+ end_idx += 1;
146+ }
147+
148+ const exit_code_str = after_marker[0..end_idx];
149+
150+ // Parse exit code
151+ if (std.fmt.parseInt(i32, exit_code_str, 10)) |exit_code| {
152+ return exit_code;
153+ } else |_| {
154+ std.log.warn("failed to parse task exit code from: {s}", .{exit_code_str});
155+ return null;
156+ }
157+ }
158+
159+ return null;
160+}
161+
162 fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
163 std.log.info("daemon started session={s} pty_fd={d}", .{ daemon.session_name, pty_fd });
164 setupSigtermHandler();
165@@ -1185,6 +1252,17 @@ fn daemonLoop(daemon: *Daemon, server_sock_fd: i32, pty_fd: i32) !void {
166 try vt_stream.nextSlice(buf[0..n]);
167 daemon.has_pty_output = true;
168
169+ // In run mode, scan output for exit code marker
170+ if (daemon.is_task_mode and daemon.task_exit_code == null) {
171+ if (findTaskExitMarker(buf[0..n])) |exit_code| {
172+ daemon.task_exit_code = exit_code;
173+ daemon.task_ended_at = @intCast(std.time.nanoTimestamp());
174+
175+ std.log.info("task completed exit_code={d}", .{exit_code});
176+ // Shell continues running - no break here
177+ }
178+ }
179+
180 // Broadcast data to all clients
181 for (daemon.clients.items) |client| {
182 ipc.appendMessage(daemon.alloc, &client.write_buf, .Output, buf[0..n]) catch |err| {
183@@ -1312,7 +1390,7 @@ fn spawnPty(daemon: *Daemon) !c_int {
184 std.log.err("execvpe failed: cmd={s} err={s}", .{ cmd_args[0], @errorName(err) });
185 std.posix.exit(1);
186 } else {
187- const shell = std.posix.getenv("SHELL") orelse "/bin/sh";
188+ const shell = detectShell();
189 // Use "-shellname" as argv[0] to signal login shell (traditional method)
190 var buf: [64]u8 = undefined;
191 const login_shell = try std.fmt.bufPrintZ(&buf, "-{s}", .{std.fs.path.basename(shell)});
192@@ -1332,6 +1410,10 @@ fn spawnPty(daemon: *Daemon) !c_int {
193 return master_fd;
194 }
195
196+fn detectShell() [:0]const u8 {
197+ return std.posix.getenv("SHELL") orelse "/bin/sh";
198+}
199+
200 fn sessionConnect(fname: []const u8) !i32 {
201 var unix_addr = try std.net.Address.initUnix(fname);
202 const socket_fd = try posix.socket(posix.AF.UNIX, posix.SOCK.STREAM | posix.SOCK.CLOEXEC, 0);
203@@ -1483,12 +1565,19 @@ fn writeSessionLine(writer: *std.Io.Writer, session: SessionEntry, short: bool,
204 return;
205 }
206
207- try writer.print("{s}session_name={s}\tpid={d}\tclients={d}", .{
208+ try writer.print("{s}session_name={s}\tpid={d}\tclients={d}\tcreated_at={d}", .{
209 prefix,
210 session.name,
211 session.pid.?,
212 session.clients_len.?,
213+ session.created_at,
214 });
215+ if (session.task_ended_at) |ended_at| {
216+ try writer.print("\ttask_ended_at={d}", .{ended_at});
217+ }
218+ if (session.task_exit_code) |exit_code| {
219+ try writer.print("\ttask_exit_code={d}", .{exit_code});
220+ }
221 if (session.cwd) |cwd| {
222 try writer.print("\tstarted_in={s}", .{cwd});
223 }