feat(crash): port Ghostty's local crash reporting

Ports ZIG/src/crash/* as crates/ghostty-crash. Like Ghostty (which uses
sentry-native only to write local files), crashes are written as Sentry
envelopes (<uuid>.ghosttycrash) to $XDG_STATE_HOME/ghostty/crash:
- envelope parse/serialize (all Zig tests ported)
- panic hook plus SIGSEGV/SIGBUS/SIGILL/SIGFPE/SIGABRT handlers on an
  alternate stack; the signal path is allocation-free and re-raises with
  the default action so exit status and core dumps are unchanged
- thread-local crash context (main/renderer/io/read thread, surface
  dimensions) set from the corresponding threads
- `sentry` cargo feature on the root binary, off by default like
  upstream's -Dsentry; +crash-report lists exactly what is written

Deviation: attachments always carry "type":"attachment" (Zig's
in-memory attachments omit it and become unparseable).

Test Plan:
- cargo test -p ghostty-crash (19)
- cargo test --test crash (child panics, SIGSEGV, stack overflow and
  abort each produce a valid envelope listed by +crash-report)
This commit is contained in:
ddidderr committed 2026-10-12 06:02:28 +02:00
1 parent a07b224869
commit a714fd9a65
11 files changed
+1933 -36

No files matched your search

@@ -0,0 +1,18 @@
//! Windows stand-in for `signal.rs`: Zig's `crash/sentry.zig` does not
//! initialise crash capture on Windows, so there are no fatal-signal
//! handlers. Only the helpers the report writer shares are provided.
use std::hash::{BuildHasher, Hasher, RandomState};
/// Fill `out` with random bytes (used for the event UUID). The standard
/// library's per-instance random hasher keys are good enough for an id.
pub(crate) fn random_bytes(out: &mut [u8; 16]) {
for chunk in out.chunks_mut(8) {
let mut h = RandomState::new().build_hasher();
h.write_usize(chunk.len());
chunk.copy_from_slice(&h.finish().to_ne_bytes()[..chunk.len()]);
}
}
/// No fatal-signal handlers on Windows.
pub(crate) fn install_handlers() {}