Commit 9d1e6dd
Eric Bower
·
2026-05-16 11:02:50 -0400 EDT
parent 936c1fa
feat(wait): print last 20 lines of session history on failure
1 files changed,
+92,
-2
+92,
-2
1@@ -1585,12 +1585,38 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void {
2 }
3 if (session.task_exit_code.? > 0) {
4 try stdout.print("---\n", .{});
5- try stdout.print("[{d}] failed task={s} exit_status={d}\n\n", .{
6+ try stdout.print("[{d}] failed task={s} exit_status={d}\n", .{
7 session.task_ended_at.?,
8 session.name,
9 session.task_exit_code.?,
10 });
11- try stdout.print("See the logs:\nzmx history {s}\nzmx attach {s}\n", .{ session.name, session.name });
12+
13+ // Fetch and print the last 20 lines of history for debugging
14+ const history_lines: usize = 20;
15+ const history_text = fetchHistory(alloc, cfg, session.name) catch null;
16+ if (history_text) |text| {
17+ defer alloc.free(text);
18+ try stdout.print("\nLast {d} lines of {s} history:\n", .{ history_lines, session.name });
19+
20+ // Count lines and find the start of the last N lines
21+ var total_lines: usize = 0;
22+ var it = std.mem.splitScalar(u8, text, '\n');
23+ while (it.next()) |_| {
24+ total_lines += 1;
25+ }
26+
27+ const skip = if (total_lines > history_lines) total_lines - history_lines else 0;
28+ var current: usize = 0;
29+ it = std.mem.splitScalar(u8, text, '\n');
30+ while (it.next()) |line| {
31+ if (current >= skip) {
32+ try stdout.print("{s}\n", .{line});
33+ }
34+ current += 1;
35+ }
36+ }
37+
38+ try stdout.print("\nSee the logs:\nzmx history {s}\nzmx attach {s}\n", .{ session.name, session.name });
39 try stdout.flush();
40 }
41 }
42@@ -1713,6 +1739,70 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void {
43 try w.interface.flush();
44 }
45
46+/// Fetch terminal history from a session socket, returning it as an allocated
47+/// string. Caller owns the returned memory and must free it.
48+fn fetchHistory(
49+ alloc: std.mem.Allocator,
50+ cfg: *Cfg,
51+ session_name: []const u8,
52+) ![]const u8 {
53+ const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) {
54+ error.NameTooLong => {
55+ socket.printSessionNameTooLong(session_name, cfg.socket_dir);
56+ return error.NameTooLong;
57+ },
58+ error.OutOfMemory => return err,
59+ };
60+ defer alloc.free(socket_path);
61+
62+ var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{});
63+ defer dir.close();
64+
65+ const exists = try socket.sessionExists(dir, session_name);
66+ if (!exists) {
67+ return error.SessionNotFound;
68+ }
69+
70+ const fd = ipc.connectSession(socket_path) catch |err| {
71+ if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name);
72+ return err;
73+ };
74+ defer posix.close(fd);
75+
76+ const format_byte: u8 = @intFromEnum(util.HistoryFormat.plain);
77+ const payload = [_]u8{format_byte};
78+ ipc.send(fd, .History, &payload) catch |err| switch (err) {
79+ error.BrokenPipe, error.ConnectionResetByPeer => return error.SessionUnresponsive,
80+ else => return err,
81+ };
82+
83+ var sb = try ipc.SocketBuffer.init(alloc);
84+ defer sb.deinit();
85+
86+ var result = std.ArrayList(u8).initCapacity(alloc, 4096) catch return error.OutOfMemory;
87+ errdefer result.deinit(alloc);
88+
89+ while (true) {
90+ var poll_fds = [_]posix.pollfd{.{ .fd = fd, .events = posix.POLL.IN, .revents = 0 }};
91+ const poll_result = posix.poll(&poll_fds, 5000) catch return error.Timeout;
92+ if (poll_result == 0) {
93+ return error.Timeout;
94+ }
95+
96+ const n = sb.read(fd) catch return error.ReadFailed;
97+ if (n == 0) break;
98+
99+ while (sb.next()) |msg| {
100+ if (msg.header.tag == .History) {
101+ try result.appendSlice(alloc, msg.payload);
102+ return result.toOwnedSlice(alloc);
103+ }
104+ }
105+ }
106+
107+ return error.NoHistoryResponse;
108+}
109+
110 fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !void {
111 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
112 defer _ = gpa.deinit();