Files
ghostty-rs/crates/ghostty-crash/src/context.rs
T
ddidderr a714fd9a65 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)
2026-10-12 06:02:28 +02:00

200 lines
6.3 KiB
Rust

//! Thread-local crash context (port of `ThreadState` in `crash/sentry.zig`).
//!
//! The native Sentry SDK has one global scope and no thread-local scopes, so
//! Ghostty keeps thread-specific data in a thread-local that the crash
//! callback reads. We do the same: thread entry functions install a
//! [`ThreadGuard`], and the panic hook / signal handler read it.
//!
//! Zig stores a `*Surface` and reads its size when the crash happens. The
//! Rust surface is not shareable across threads, so it instead publishes its
//! dimensions into a shared [`SurfaceDims`] (plain atomics, readable from a
//! signal handler) that the threads hold an `Arc` of.
#![allow(unsafe_code)]
use std::{
cell::Cell,
ptr,
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
};
/// The type of thread (Zig: `ThreadState.Type`). Used to tag the crash.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ThreadType {
Main = 1,
Renderer = 2,
Io = 3,
}
impl ThreadType {
/// The Zig `@tagName`.
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
ThreadType::Main => "main",
ThreadType::Renderer => "renderer",
ThreadType::Io => "io",
}
}
fn from_u8(v: u8) -> Option<ThreadType> {
match v {
1 => Some(ThreadType::Main),
2 => Some(ThreadType::Renderer),
3 => Some(ThreadType::Io),
_ => None,
}
}
}
/// The surface dimensions recorded in a crash (`Dimensions` context).
/// Updated by the owner of the surface whenever the size changes.
#[derive(Debug, Default)]
pub struct SurfaceDims {
screen_width: AtomicU32,
screen_height: AtomicU32,
grid_columns: AtomicU32,
grid_rows: AtomicU32,
cell_width: AtomicU32,
cell_height: AtomicU32,
}
/// A plain snapshot of [`SurfaceDims`].
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct DimsSnapshot {
pub screen_width: u32,
pub screen_height: u32,
pub grid_columns: u32,
pub grid_rows: u32,
pub cell_width: u32,
pub cell_height: u32,
}
impl SurfaceDims {
#[must_use]
pub fn new() -> Arc<SurfaceDims> {
Arc::new(SurfaceDims::default())
}
pub fn set(&self, d: DimsSnapshot) {
self.screen_width.store(d.screen_width, Ordering::Relaxed);
self.screen_height.store(d.screen_height, Ordering::Relaxed);
self.grid_columns.store(d.grid_columns, Ordering::Relaxed);
self.grid_rows.store(d.grid_rows, Ordering::Relaxed);
self.cell_width.store(d.cell_width, Ordering::Relaxed);
self.cell_height.store(d.cell_height, Ordering::Relaxed);
}
#[must_use]
pub fn get(&self) -> DimsSnapshot {
DimsSnapshot {
screen_width: self.screen_width.load(Ordering::Relaxed),
screen_height: self.screen_height.load(Ordering::Relaxed),
grid_columns: self.grid_columns.load(Ordering::Relaxed),
grid_rows: self.grid_rows.load(Ordering::Relaxed),
cell_width: self.cell_width.load(Ordering::Relaxed),
cell_height: self.cell_height.load(Ordering::Relaxed),
}
}
}
thread_local! {
// Const-initialized with a type that has no destructor so reading it
// from a signal handler is just a TLS load.
static STATE: Cell<(u8, *const SurfaceDims)> = const { Cell::new((0, ptr::null())) };
}
/// Sets the thread state for the current thread until dropped, then restores
/// whatever was set before (Zig: `thread_state = ...; defer thread_state = null`).
#[must_use = "the crash context is cleared when the guard is dropped"]
pub struct ThreadGuard {
prev: (u8, *const SurfaceDims),
// Keeps the pointer in `STATE` valid.
_dims: Option<Arc<SurfaceDims>>,
// Thread-local state must be restored on the same thread.
_not_send: std::marker::PhantomData<*const ()>,
}
impl ThreadGuard {
pub fn new(kind: ThreadType, dims: Option<Arc<SurfaceDims>>) -> ThreadGuard {
let p = dims.as_ref().map_or(ptr::null(), Arc::as_ptr);
let prev = STATE.with(|s| s.replace((kind as u8, p)));
ThreadGuard {
prev,
_dims: dims,
_not_send: std::marker::PhantomData,
}
}
}
impl Drop for ThreadGuard {
fn drop(&mut self) {
STATE.with(|s| s.set(self.prev));
}
}
/// The surface dimensions of the current thread's crash context, to hand to
/// threads it spawns (e.g. the termio read thread).
#[must_use]
pub fn current_dims() -> Option<Arc<SurfaceDims>> {
let (_, p) = STATE.with(Cell::get);
if p.is_null() {
return None;
}
// SAFETY: `p` came from `Arc::as_ptr` of an Arc owned by a live
// `ThreadGuard` on this thread (the guard restores `STATE` before it
// releases the Arc), so the strong count is >= 1 here.
unsafe {
Arc::increment_strong_count(p);
Some(Arc::from_raw(p))
}
}
/// Async-signal-safe read of the current thread state.
pub(crate) fn current() -> Option<(ThreadType, Option<DimsSnapshot>)> {
let (k, p) = STATE.try_with(Cell::get).ok()?;
let kind = ThreadType::from_u8(k)?;
let dims = if p.is_null() {
None
} else {
// SAFETY: see `current_dims`; the guard keeps the Arc alive while
// `STATE` points at it. Fields are atomics so a racing writer is
// fine (values may be stale, which upstream also tolerates).
Some(unsafe { (*p).get() })
};
Some((kind, dims))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn guard_sets_and_restores() {
assert!(current().is_none());
let dims = SurfaceDims::new();
dims.set(DimsSnapshot {
grid_columns: 80,
..Default::default()
});
{
let _g = ThreadGuard::new(ThreadType::Renderer, Some(dims));
let (k, d) = current().unwrap();
assert_eq!(ThreadType::Renderer, k);
assert_eq!(80, d.unwrap().grid_columns);
assert!(current_dims().is_some());
{
let _g2 = ThreadGuard::new(ThreadType::Io, None);
assert_eq!(ThreadType::Io, current().unwrap().0);
}
assert_eq!(ThreadType::Renderer, current().unwrap().0);
}
assert!(current().is_none());
assert!(current_dims().is_none());
}
}