From f1a89bdf9d9fac2cd7d6c1479e5bc99c8dd4c904 Mon Sep 17 00:00:00 2001 From: CJ van den Berg Date: Wed, 26 Jun 2024 23:28:17 +0200 Subject: [PATCH] feat: add stack traces to thespian errors in debug builds --- src/subprocess.zig | 36 +++++++++++------------ src/subprocess_windows.zig | 26 ++++++++--------- src/thespian.zig | 58 +++++++++++++++++++++++++++----------- 3 files changed, 73 insertions(+), 47 deletions(-) diff --git a/src/subprocess.zig b/src/subprocess.zig index bdaa3ef..8a34c23 100644 --- a/src/subprocess.zig +++ b/src/subprocess.zig @@ -31,7 +31,7 @@ pub fn write(self: *Self, bytes: []const u8) error{Exit}!usize { pub fn send(self: *const Self, bytes_: []const u8) tp.result { if (self.stdin_behavior != .Pipe) return tp.exit("cannot send to closed stdin"); - const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed); + const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed, null); var bytes = bytes_; while (bytes.len > 0) bytes = loop: { @@ -134,11 +134,11 @@ const Proc = struct { _ = self.args.reset(.free_all); if (self.child.stdin_behavior == .Pipe) - self.fd_stdin = tp.file_descriptor.init("stdin", self.child.stdin.?.handle) catch |e| return tp.exit_error(e); - self.fd_stdout = tp.file_descriptor.init("stdout", self.child.stdout.?.handle) catch |e| return tp.exit_error(e); - self.fd_stderr = tp.file_descriptor.init("stderr", self.child.stderr.?.handle) catch |e| return tp.exit_error(e); - if (self.fd_stdout) |fd_stdout| fd_stdout.wait_read() catch |e| return tp.exit_error(e); - if (self.fd_stderr) |fd_stderr| fd_stderr.wait_read() catch |e| return tp.exit_error(e); + self.fd_stdin = tp.file_descriptor.init("stdin", self.child.stdin.?.handle) catch |e| return tp.exit_error(e, @errorReturnTrace()); + self.fd_stdout = tp.file_descriptor.init("stdout", self.child.stdout.?.handle) catch |e| return tp.exit_error(e, @errorReturnTrace()); + self.fd_stderr = tp.file_descriptor.init("stderr", self.child.stderr.?.handle) catch |e| return tp.exit_error(e, @errorReturnTrace()); + if (self.fd_stdout) |fd_stdout| fd_stdout.wait_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); + if (self.fd_stderr) |fd_stderr| fd_stderr.wait_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); tp.receive(&self.receiver); } @@ -150,22 +150,22 @@ const Proc = struct { var err_msg: []u8 = ""; if (try m.match(.{ "fd", "stdout", "read_ready" })) { try self.dispatch_stdout(); - if (self.fd_stdout) |fd_stdout| fd_stdout.wait_read() catch |e| return tp.exit_error(e); + if (self.fd_stdout) |fd_stdout| fd_stdout.wait_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); } else if (try m.match(.{ "fd", "stderr", "read_ready" })) { try self.dispatch_stderr(); - if (self.fd_stderr) |fd_stderr| fd_stderr.wait_read() catch |e| return tp.exit_error(e); + if (self.fd_stderr) |fd_stderr| fd_stderr.wait_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); } else if (try m.match(.{ "fd", "stdin", "write_ready" })) { if (self.stdin_buffer.items.len > 0) { if (self.child.stdin) |stdin| { const written = stdin.write(self.stdin_buffer.items) catch |e| switch (e) { error.WouldBlock => { if (self.fd_stdin) |fd_stdin| { - fd_stdin.wait_write() catch |e_| return tp.exit_error(e_); + fd_stdin.wait_write() catch |e_| return tp.exit_error(e_, @errorReturnTrace()); self.write_pending = true; return; - } else return tp.exit_error(error.WouldBlock); + } else return tp.exit_error(error.WouldBlock, @errorReturnTrace()); }, - else => return tp.exit_error(e), + else => return tp.exit_error(e, @errorReturnTrace()), }; self.write_pending = false; defer { @@ -178,7 +178,7 @@ const Proc = struct { std.mem.copyForwards(u8, self.stdin_buffer.items, self.stdin_buffer.items[written..]); self.stdin_buffer.items.len = self.stdin_buffer.items.len - written; if (self.fd_stdin) |fd_stdin| { - fd_stdin.wait_write() catch |e| return tp.exit_error(e); + fd_stdin.wait_write() catch |e| return tp.exit_error(e, @errorReturnTrace()); self.write_pending = true; } } @@ -186,8 +186,8 @@ const Proc = struct { } } else if (try m.match(.{ "stdin", tp.extract(&bytes) })) { if (self.fd_stdin) |fd_stdin| { - self.stdin_buffer.appendSlice(bytes) catch |e| return tp.exit_error(e); - fd_stdin.wait_write() catch |e| return tp.exit_error(e); + self.stdin_buffer.appendSlice(bytes) catch |e| return tp.exit_error(e, @errorReturnTrace()); + fd_stdin.wait_write() catch |e| return tp.exit_error(e, @errorReturnTrace()); self.write_pending = true; } } else if (try m.match(.{"stdin_close"})) { @@ -207,7 +207,7 @@ const Proc = struct { self.child.stderr = null; } } else if (try m.match(.{"term"})) { - const term_ = self.child.kill() catch |e| return tp.exit_error(e); + const term_ = self.child.kill() catch |e| return tp.exit_error(e, @errorReturnTrace()); return self.handle_term(term_); } else if (try m.match(.{ "fd", tp.any, "read_error", tp.extract(&err), tp.extract(&err_msg) })) { return tp.exit(err_msg); @@ -227,7 +227,7 @@ const Proc = struct { const stdout = self.child.stdout orelse return tp.exit("cannot read closed stdout"); const bytes = stdout.read(&buffer) catch |e| switch (e) { error.WouldBlock => return, - else => return tp.exit_error(e), + else => return tp.exit_error(e, @errorReturnTrace()), }; if (bytes == 0) return self.handle_terminate(); @@ -239,7 +239,7 @@ const Proc = struct { const stderr = self.child.stderr orelse return tp.exit("cannot read closed stderr"); const bytes = stderr.read(&buffer) catch |e| switch (e) { error.WouldBlock => return, - else => return tp.exit_error(e), + else => return tp.exit_error(e, @errorReturnTrace()), }; if (bytes == 0) return; @@ -247,7 +247,7 @@ const Proc = struct { } fn handle_terminate(self: *Proc) tp.result { - return self.handle_term(self.child.wait() catch |e| return tp.exit_error(e)); + return self.handle_term(self.child.wait() catch |e| return tp.exit_error(e, @errorReturnTrace())); } fn handle_term(self: *Proc, term_: std.process.Child.Term) tp.result { diff --git a/src/subprocess_windows.zig b/src/subprocess_windows.zig index a32c006..33a7f9a 100644 --- a/src/subprocess_windows.zig +++ b/src/subprocess_windows.zig @@ -31,7 +31,7 @@ pub fn write(self: *Self, bytes: []const u8) error{Exit}!usize { pub fn send(self: *const Self, bytes_: []const u8) tp.result { if (self.stdin_behavior != .Pipe) return tp.exit("cannot send to closed stdin"); - const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed); + const pid = if (self.pid) |pid| pid else return tp.exit_error(error.Closed, null); var bytes = bytes_; while (bytes.len > 0) bytes = loop: { @@ -134,11 +134,11 @@ const Proc = struct { _ = self.args.reset(.free_all); if (self.child.stdin_behavior == .Pipe) - self.stream_stdin = tp.file_stream.init("stdin", self.child.stdin.?.handle) catch |e| return tp.exit_error(e); - self.stream_stdout = tp.file_stream.init("stdout", self.child.stdout.?.handle) catch |e| return tp.exit_error(e); - self.stream_stderr = tp.file_stream.init("stderr", self.child.stderr.?.handle) catch |e| return tp.exit_error(e); - if (self.stream_stdout) |stream| stream.start_read() catch |e| return tp.exit_error(e); - if (self.stream_stderr) |stream| stream.start_read() catch |e| return tp.exit_error(e); + self.stream_stdin = tp.file_stream.init("stdin", self.child.stdin.?.handle) catch |e| return tp.exit_error(e, @errorReturnTrace()); + self.stream_stdout = tp.file_stream.init("stdout", self.child.stdout.?.handle) catch |e| return tp.exit_error(e, @errorReturnTrace()); + self.stream_stderr = tp.file_stream.init("stderr", self.child.stderr.?.handle) catch |e| return tp.exit_error(e, @errorReturnTrace()); + if (self.stream_stdout) |stream| stream.start_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); + if (self.stream_stderr) |stream| stream.start_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); tp.receive(&self.receiver); } @@ -151,12 +151,12 @@ const Proc = struct { var err_msg: []u8 = ""; if (try m.match(.{ "stream", "stdout", "read_complete", tp.extract(&bytes) })) { try self.dispatch_stdout(bytes); - if (self.stream_stdout) |stream| stream.start_read() catch |e| return tp.exit_error(e); + if (self.stream_stdout) |stream| stream.start_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); } else if (try m.match(.{ "stream", "stderr", "read_complete", tp.extract(&bytes) })) { try self.dispatch_stderr(bytes); - if (self.stream_stderr) |stream| stream.start_read() catch |e| return tp.exit_error(e); + if (self.stream_stderr) |stream| stream.start_read() catch |e| return tp.exit_error(e, @errorReturnTrace()); } else if (try m.match(.{ "stream", "stdin", "write_complete", tp.extract(&bytes_written) })) { - const old_buf = self.stdin_buffer.toOwnedSlice() catch |e| return tp.exit_error(e); + const old_buf = self.stdin_buffer.toOwnedSlice() catch |e| return tp.exit_error(e, @errorReturnTrace()); defer self.stdin_buffer.allocator.free(old_buf); const bytes_left = old_buf[bytes_written..]; if (bytes_left.len > 0) { @@ -185,7 +185,7 @@ const Proc = struct { self.child.stderr = null; } } else if (try m.match(.{"term"})) { - const term_ = self.child.kill() catch |e| return tp.exit_error(e); + const term_ = self.child.kill() catch |e| return tp.exit_error(e, @errorReturnTrace()); return self.handle_term(term_); } else if (try m.match(.{ "stream", tp.any, "read_error", tp.extract(&err), tp.extract(&err_msg) })) { return tp.exit(err_msg); @@ -194,8 +194,8 @@ const Proc = struct { fn start_write(self: *Proc, bytes: []const u8) tp.result { if (self.stream_stdin) |stream_stdin| { - self.stdin_buffer.appendSlice(bytes) catch |e| return tp.exit_error(e); - stream_stdin.start_write(self.stdin_buffer.items) catch |e| return tp.exit_error(e); + self.stdin_buffer.appendSlice(bytes) catch |e| return tp.exit_error(e, @errorReturnTrace()); + stream_stdin.start_write(self.stdin_buffer.items) catch |e| return tp.exit_error(e, @errorReturnTrace()); self.write_pending = true; } } @@ -221,7 +221,7 @@ const Proc = struct { } fn handle_terminate(self: *Proc) tp.result { - return self.handle_term(self.child.wait() catch |e| return tp.exit_error(e)); + return self.handle_term(self.child.wait() catch |e| return tp.exit_error(e, @errorReturnTrace())); } fn handle_term(self: *Proc, term_: std.process.Child.Term) tp.result { diff --git a/src/thespian.zig b/src/thespian.zig index 8bb0e35..3994c59 100644 --- a/src/thespian.zig +++ b/src/thespian.zig @@ -79,7 +79,7 @@ fn Pid(comptime own: Ownership) type { } pub fn delay_send(self: Self, a: std.mem.Allocator, delay_us: u64, m: anytype) result { - var h = self.delay_send_cancellable(a, delay_us, m) catch |e| return exit_error(e); + var h = self.delay_send_cancellable(a, delay_us, m) catch |e| return exit_error(e, @errorReturnTrace()); h.deinit(); } @@ -88,10 +88,10 @@ fn Pid(comptime own: Ownership) type { return Cancellable.init(try DelayedSender.send(self, a, delay_us, msg)); } - pub fn forward_error(self: Self, e: anyerror) result { + pub fn forward_error(self: Self, e: anyerror, stack_trace: ?*std.builtin.StackTrace) result { return self.send_raw(switch (e) { error.Exit => .{ .buf = error_message() }, - else => exit_message(e), + else => exit_message(e, stack_trace), }); } @@ -199,32 +199,58 @@ pub const message = struct { c.cbor_to_json(self.to(c_buffer_type), callback); } pub fn match(self: Self, m: anytype) error{Exit}!bool { - return if (cbor.match(self.buf, m)) |ret| ret else |e| exit_error(e); + return if (cbor.match(self.buf, m)) |ret| ret else |e| exit_error(e, @errorReturnTrace()); } }; -pub fn exit_message(e: anytype) message { - return message.fmtbuf(&error_message_buffer, .{ "exit", e }) catch unreachable; +pub fn exit_message(e: anytype, stack_trace: ?*std.builtin.StackTrace) message { + if (stack_trace) |stack_trace_| { + var debug_info_arena_allocator: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer debug_info_arena_allocator.deinit(); + const a = debug_info_arena_allocator.allocator(); + var out = std.ArrayList(u8).init(a); + store_stack_trace(a, stack_trace_.*, out.writer()); + return message.fmtbuf(&error_message_buffer, .{ "exit", e, out.items }) catch unreachable; + } else { + return message.fmtbuf(&error_message_buffer, .{ "exit", e }) catch unreachable; + } +} + +fn store_stack_trace(a: std.mem.Allocator, stack_trace: std.builtin.StackTrace, writer: anytype) void { + nosuspend { + if (builtin.strip_debug_info) { + writer.print("Unable to store stack trace: debug info stripped\n", .{}) catch return; + return; + } + const debug_info = std.debug.getSelfDebugInfo() catch |err| { + writer.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; + return; + }; + std.debug.writeStackTrace(stack_trace, writer, a, debug_info, .no_color) catch |err| { + writer.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; + return; + }; + } } pub fn exit_normal() result { - return set_error_msg(exit_message("normal")); + return set_error_msg(exit_message("normal", null)); } pub fn exit(e: []const u8) error{Exit} { - return set_error_msg(exit_message(e)); + return set_error_msg(exit_message(e, null)); } pub fn exit_fmt(comptime fmt: anytype, args: anytype) result { var buf: [1024]u8 = undefined; const msg = std.fmt.bufPrint(&buf, fmt, args) catch "FMTERROR"; - return set_error_msg(exit_message(msg)); + return set_error_msg(exit_message(msg, null)); } -pub fn exit_error(e: anyerror) error{Exit} { +pub fn exit_error(e: anyerror, stack_trace: ?*std.builtin.StackTrace) error{Exit} { return switch (e) { error.Exit => error.Exit, - else => set_error_msg(exit_message(e)), + else => set_error_msg(exit_message(e, stack_trace)), }; } @@ -232,14 +258,14 @@ pub fn unexpected(b: message) error{Exit} { const txt = "UNEXPECTED_MESSAGE: "; var buf: [max_message_size]u8 = undefined; @memcpy(buf[0..txt.len], txt); - const json = b.to_json(buf[txt.len..]) catch |e| return exit_error(e); + const json = b.to_json(buf[txt.len..]) catch |e| return exit_error(e, @errorReturnTrace()); return set_error_msg(message.fmt(.{ "exit", buf[0..(txt.len + json.len)] })); } pub fn error_message() []const u8 { if (error_buffer_tl.base) |base| return base[0..error_buffer_tl.len]; - set_error_msg(exit_message("NOERROR")) catch {}; + set_error_msg(exit_message("NOERROR", null)) catch {}; return error_message(); } @@ -775,7 +801,7 @@ const CallContext = struct { fn receive_(self: *Self, _: pid_ref, m: message) result { defer self.done.set(); - self.response.* = m.clone(self.a) catch |e| return exit_error(e); + self.response.* = m.clone(self.a) catch |e| return exit_error(e, @errorReturnTrace()); return exit_normal(); } }; @@ -804,7 +830,7 @@ const DelayedSender = struct { fn start(self: *DelayedSender) result { self.receiver = ReceiverT.init(receive_, self); const m_ = self.message.?; - self.timeout = timeout.init(self.delay_us, m_) catch |e| return exit_error(e); + self.timeout = timeout.init(self.delay_us, m_) catch |e| return exit_error(e, @errorReturnTrace()); self.a.free(m_.buf); _ = set_trap(true); receive(&self.receiver); @@ -817,7 +843,7 @@ const DelayedSender = struct { fn receive_(self: *DelayedSender, _: pid_ref, m_: message) result { if (try m_.match(.{"CANCEL"})) { - self.timeout.cancel() catch |e| return exit_error(e); + self.timeout.cancel() catch |e| return exit_error(e, @errorReturnTrace()); return; } defer self.deinit();