From ffc8c1f6f19a5dc48e98f99938b438baf9a97165 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Thu, 3 Sep 2026 13:10:59 -0300 Subject: crash: a panic writes itself down beside the init file, where stderr cannot lose it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every crash this program has ever had went to stderr and nowhere else, and stderr is the one place it cannot keep anything. In the tty shell stderr IS the screen, so the trace lands on the grid the terminal is being reset out of; the SDL and AppKit shells have no terminal at all; a --detach session's goes wherever its launcher left it. src/crash.zig appends a record to /crashes first: one line naming the build (version, commit, UTC, os-arch, pid) and under it the panic message and the frames behind it. RETURN ADDRESSES and not the symbolised trace, which is measured rather than chosen. `std.debug.writeCurrentStackTrace` called from a panic handler BEFORE defaultPanic wedges the process at 0% CPU: symbolising reads DWARF, that read can itself panic, and the staging which turns a nested panic into "aborting due to recursive panic" is defaultPanic's own and private. Reproduced in a standalone build with this program's std_options_debug_io and inside a test binary. `captureCurrentStackTrace` only walks frames, so the addresses go in the file and `addr2line -e` finishes the job; stderr still gets the symbolised trace from defaultPanic, unchanged. The AppKit shell gets a panic handler of its own here too: the macOS build roots at macos.zig, so main.zig's had never run there — in the shell with the least useful stderr of the four. The config directory is COPIED rather than borrowed, because that host's lives in an arena its own errdefer frees. One record at a time, so two panicking threads cannot interleave into one buffer. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016Q4RATpafkwahrovHQLKRf --- src/macos.zig | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/macos.zig') diff --git a/src/macos.zig b/src/macos.zig index 4c7eb97f..a9c9d0f3 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -41,6 +41,19 @@ const lsp_host = @import("lsp_host.zig"); // the shared snapshot + worker body const host_api = @import("host.zig"); // LspRequest and the vtable's own types const tracy = @import("tracy.zig"); // no-op unless -Dtracy names a checkout const selection_pipe = @import("selection_pipe.zig"); // Job, runJob and Tasks +const crash = @import("crash.zig"); + +/// This file is the ROOT of the macOS build (build.zig: the AppKit shell is a +/// library whose host owns main()), so `std.builtin.panic` resolves here and +/// not in src/main.zig — a handler written only there would never run in the +/// app, which is the shell with the least useful stderr of the four. No +/// terminal to restore either, which is the rest of what main.zig's does. +pub const panic = std.debug.FullPanic(struct { + fn call(msg: []const u8, ret_addr: ?usize) noreturn { + crash.record(msg, ret_addr); + std.debug.defaultPanic(msg, ret_addr); + } +}.call); extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int; @@ -679,6 +692,10 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void { opts.startup_config = found.bytes; opts.startup_config_path = found.path; opts.config_dir = found.dir; + // This host has no terminal at all, so the panic trace stderr gets goes + // to a Console.app nobody has open. `panic` above writes it beside the + // init file too, and this is where it learns the directory. + if (found.dir) |d| crash.setDir(d); } pardes.image.start(io, allocs.image); -- cgit v1.3