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 516c780f84
commit b3145ad7c1
11 files changed
+1933 -36

No files matched your search

+7 -36
View File
@@ -11,52 +11,23 @@ use std::{
};
use ghostty_config::types_b::Duration;
pub use ghostty_crash::Report;
use crate::error::Error;
pub use crate::options::NoOptions as Options;
/// A crash report file (Zig: `crash.Report`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Report {
pub name: String,
/// Modification time in nanoseconds since the unix epoch.
pub mtime: i128,
}
/// The default crash directory (Zig: `crash.defaultDir`):
/// `$XDG_STATE_HOME/ghostty/crash`.
pub fn default_dir() -> Result<PathBuf, Error> {
let dir = ghostty_os::xdg::state(&ghostty_os::xdg::Options {
subdir: Some("ghostty/crash"),
home: None,
})
.map_err(|e| Error::Other(e.to_string()))?;
Ok(PathBuf::from(dir))
let dir = ghostty_crash::default_dir().map_err(|e| Error::Other(e.to_string()))?;
Ok(dir.path)
}
/// List the crash reports (regular files) in `dir`. A directory that cannot
/// be opened yields no reports.
/// List the crash reports (regular files) in `dir`, i.e. the
/// `.ghosttycrash` envelopes written by `ghostty-crash`. A directory that
/// cannot be opened yields no reports.
pub fn list_reports(dir: &std::path::Path) -> Result<Vec<Report>, Error> {
let Ok(rd) = std::fs::read_dir(dir) else {
return Ok(vec![]);
};
let mut reports = vec![];
for entry in rd {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let mtime = entry.metadata()?.modified()?;
let mtime = match mtime.duration_since(UNIX_EPOCH) {
Ok(d) => i128::try_from(d.as_nanos()).unwrap_or(i128::MAX),
Err(e) => -i128::try_from(e.duration().as_nanos()).unwrap_or(i128::MAX),
};
reports.push(Report {
name: entry.file_name().to_string_lossy().into_owned(),
mtime,
});
}
Ok(reports)
Ok(ghostty_crash::dir::list_reports(dir)?)
}
/// The `crash-report` command is used to inspect and send crash reports.