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
| ... | ... | @@ -1585,12 +1585,38 @@ fn wait(cfg: *Cfg, matchers: std.ArrayList(SessionMatch)) !void { | |
| 1585 | 1585 | } | |
| 1586 | 1586 | if (session.task_exit_code.? > 0) { | |
| 1587 | 1587 | try stdout.print("---\n", .{}); | |
| 1588 | - | try stdout.print("[{d}] failed task={s} exit_status={d}\n\n", .{ | |
| 1588 | + | try stdout.print("[{d}] failed task={s} exit_status={d}\n", .{ | |
| 1589 | 1589 | session.task_ended_at.?, | |
| 1590 | 1590 | session.name, | |
| 1591 | 1591 | session.task_exit_code.?, | |
| 1592 | 1592 | }); | |
| 1593 | - | try stdout.print("See the logs:\nzmx history {s}\nzmx attach {s}\n", .{ session.name, session.name }); | |
| 1593 | + | ||
| 1594 | + | // Fetch and print the last 20 lines of history for debugging | |
| 1595 | + | const history_lines: usize = 20; | |
| 1596 | + | const history_text = fetchHistory(alloc, cfg, session.name) catch null; | |
| 1597 | + | if (history_text) |text| { | |
| 1598 | + | defer alloc.free(text); | |
| 1599 | + | try stdout.print("\nLast {d} lines of {s} history:\n", .{ history_lines, session.name }); | |
| 1600 | + | ||
| 1601 | + | // Count lines and find the start of the last N lines | |
| 1602 | + | var total_lines: usize = 0; | |
| 1603 | + | var it = std.mem.splitScalar(u8, text, '\n'); | |
| 1604 | + | while (it.next()) |_| { | |
| 1605 | + | total_lines += 1; | |
| 1606 | + | } | |
| 1607 | + | ||
| 1608 | + | const skip = if (total_lines > history_lines) total_lines - history_lines else 0; | |
| 1609 | + | var current: usize = 0; | |
| 1610 | + | it = std.mem.splitScalar(u8, text, '\n'); | |
| 1611 | + | while (it.next()) |line| { | |
| 1612 | + | if (current >= skip) { | |
| 1613 | + | try stdout.print("{s}\n", .{line}); | |
| 1614 | + | } | |
| 1615 | + | current += 1; | |
| 1616 | + | } | |
| 1617 | + | } | |
| 1618 | + | ||
| 1619 | + | try stdout.print("\nSee the logs:\nzmx history {s}\nzmx attach {s}\n", .{ session.name, session.name }); | |
| 1594 | 1620 | try stdout.flush(); | |
| 1595 | 1621 | } | |
| 1596 | 1622 | } |
| ... | ... | @@ -1713,6 +1739,70 @@ fn kill(cfg: *Cfg, session_name: []const u8, force: bool) !void { | |
| 1713 | 1739 | try w.interface.flush(); | |
| 1714 | 1740 | } | |
| 1715 | 1741 | ||
| 1742 | + | /// Fetch terminal history from a session socket, returning it as an allocated | |
| 1743 | + | /// string. Caller owns the returned memory and must free it. | |
| 1744 | + | fn fetchHistory( | |
| 1745 | + | alloc: std.mem.Allocator, | |
| 1746 | + | cfg: *Cfg, | |
| 1747 | + | session_name: []const u8, | |
| 1748 | + | ) ![]const u8 { | |
| 1749 | + | const socket_path = socket.getSocketPath(alloc, cfg.socket_dir, session_name) catch |err| switch (err) { | |
| 1750 | + | error.NameTooLong => { | |
| 1751 | + | socket.printSessionNameTooLong(session_name, cfg.socket_dir); | |
| 1752 | + | return error.NameTooLong; | |
| 1753 | + | }, | |
| 1754 | + | error.OutOfMemory => return err, | |
| 1755 | + | }; | |
| 1756 | + | defer alloc.free(socket_path); | |
| 1757 | + | ||
| 1758 | + | var dir = try std.fs.openDirAbsolute(cfg.socket_dir, .{}); | |
| 1759 | + | defer dir.close(); | |
| 1760 | + | ||
| 1761 | + | const exists = try socket.sessionExists(dir, session_name); | |
| 1762 | + | if (!exists) { | |
| 1763 | + | return error.SessionNotFound; | |
| 1764 | + | } | |
| 1765 | + | ||
| 1766 | + | const fd = ipc.connectSession(socket_path) catch |err| { | |
| 1767 | + | if (err == error.ConnectionRefused) socket.cleanupStaleSocket(dir, session_name); | |
| 1768 | + | return err; | |
| 1769 | + | }; | |
| 1770 | + | defer posix.close(fd); | |
| 1771 | + | ||
| 1772 | + | const format_byte: u8 = @intFromEnum(util.HistoryFormat.plain); | |
| 1773 | + | const payload = [_]u8{format_byte}; | |
| 1774 | + | ipc.send(fd, .History, &payload) catch |err| switch (err) { | |
| 1775 | + | error.BrokenPipe, error.ConnectionResetByPeer => return error.SessionUnresponsive, | |
| 1776 | + | else => return err, | |
| 1777 | + | }; | |
| 1778 | + | ||
| 1779 | + | var sb = try ipc.SocketBuffer.init(alloc); | |
| 1780 | + | defer sb.deinit(); | |
| 1781 | + | ||
| 1782 | + | var result = std.ArrayList(u8).initCapacity(alloc, 4096) catch return error.OutOfMemory; | |
| 1783 | + | errdefer result.deinit(alloc); | |
| 1784 | + | ||
| 1785 | + | while (true) { | |
| 1786 | + | var poll_fds = [_]posix.pollfd{.{ .fd = fd, .events = posix.POLL.IN, .revents = 0 }}; | |
| 1787 | + | const poll_result = posix.poll(&poll_fds, 5000) catch return error.Timeout; | |
| 1788 | + | if (poll_result == 0) { | |
| 1789 | + | return error.Timeout; | |
| 1790 | + | } | |
| 1791 | + | ||
| 1792 | + | const n = sb.read(fd) catch return error.ReadFailed; | |
| 1793 | + | if (n == 0) break; | |
| 1794 | + | ||
| 1795 | + | while (sb.next()) |msg| { | |
| 1796 | + | if (msg.header.tag == .History) { | |
| 1797 | + | try result.appendSlice(alloc, msg.payload); | |
| 1798 | + | return result.toOwnedSlice(alloc); | |
| 1799 | + | } | |
| 1800 | + | } | |
| 1801 | + | } | |
| 1802 | + | ||
| 1803 | + | return error.NoHistoryResponse; | |
| 1804 | + | } | |
| 1805 | + | ||
| 1716 | 1806 | fn history(cfg: *Cfg, session_name: []const u8, format: util.HistoryFormat) !void { | |
| 1717 | 1807 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; | |
| 1718 | 1808 | defer _ = gpa.deinit(); |