crashes.rs

  1use crash_handler::CrashHandler;
  2use log::info;
  3use minidumper::{Client, LoopAction, MinidumpBinary};
  4
  5use std::{
  6    env,
  7    fs::File,
  8    io,
  9    path::{Path, PathBuf},
 10    process::{self, Command},
 11    sync::{
 12        OnceLock,
 13        atomic::{AtomicBool, Ordering},
 14    },
 15    thread,
 16    time::Duration,
 17};
 18
 19// set once the crash handler has initialized and the client has connected to it
 20pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false);
 21// set when the first minidump request is made to avoid generating duplicate crash reports
 22pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false);
 23const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60);
 24
 25pub async fn init(id: String) {
 26    let exe = env::current_exe().expect("unable to find ourselves");
 27    let zed_pid = process::id();
 28    // TODO: we should be able to get away with using 1 crash-handler process per machine,
 29    // but for now we append the PID of the current process which makes it unique per remote
 30    // server or interactive zed instance. This solves an issue where occasionally the socket
 31    // used by the crash handler isn't destroyed correctly which causes it to stay on the file
 32    // system and block further attempts to initialize crash handlers with that socket path.
 33    let socket_name = paths::temp_dir().join(format!("zed-crash-handler-{zed_pid}"));
 34    #[allow(unused)]
 35    let server_pid = Command::new(exe)
 36        .arg("--crash-handler")
 37        .arg(&socket_name)
 38        .spawn()
 39        .expect("unable to spawn server process")
 40        .id();
 41    info!("spawning crash handler process");
 42
 43    let mut elapsed = Duration::ZERO;
 44    let retry_frequency = Duration::from_millis(100);
 45    let mut maybe_client = None;
 46    while maybe_client.is_none() {
 47        if let Ok(client) = Client::with_name(socket_name.as_path()) {
 48            maybe_client = Some(client);
 49            info!("connected to crash handler process after {elapsed:?}");
 50            break;
 51        }
 52        elapsed += retry_frequency;
 53        smol::Timer::after(retry_frequency).await;
 54    }
 55    let client = maybe_client.unwrap();
 56    client.send_message(1, id).unwrap(); // set session id on the server
 57
 58    let client = std::sync::Arc::new(client);
 59    let handler = crash_handler::CrashHandler::attach(unsafe {
 60        let client = client.clone();
 61        crash_handler::make_crash_event(move |crash_context: &crash_handler::CrashContext| {
 62            // only request a minidump once
 63            let res = if REQUESTED_MINIDUMP
 64                .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
 65                .is_ok()
 66            {
 67                client.send_message(2, "mistakes were made").unwrap();
 68                client.ping().unwrap();
 69                client.request_dump(crash_context).is_ok()
 70            } else {
 71                true
 72            };
 73            crash_handler::CrashEventResult::Handled(res)
 74        })
 75    })
 76    .expect("failed to attach signal handler");
 77
 78    #[cfg(target_os = "linux")]
 79    {
 80        handler.set_ptracer(Some(server_pid));
 81    }
 82    CRASH_HANDLER.store(true, Ordering::Release);
 83    std::mem::forget(handler);
 84    info!("crash handler registered");
 85
 86    loop {
 87        client.ping().ok();
 88        smol::Timer::after(Duration::from_secs(10)).await;
 89    }
 90}
 91
 92pub struct CrashServer {
 93    session_id: OnceLock<String>,
 94}
 95
 96impl minidumper::ServerHandler for CrashServer {
 97    fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> {
 98        let err_message = "Need to send a message with the ID upon starting the crash handler";
 99        let dump_path = paths::logs_dir()
100            .join(self.session_id.get().expect(err_message))
101            .with_extension("dmp");
102        let file = File::create(&dump_path)?;
103        Ok((file, dump_path))
104    }
105
106    fn on_minidump_created(&self, result: Result<MinidumpBinary, minidumper::Error>) -> LoopAction {
107        match result {
108            Ok(mut md_bin) => {
109                use io::Write;
110                let _ = md_bin.file.flush();
111                info!("wrote minidump to disk {:?}", md_bin.path);
112            }
113            Err(e) => {
114                info!("failed to write minidump: {:#}", e);
115            }
116        }
117        LoopAction::Exit
118    }
119
120    fn on_message(&self, kind: u32, buffer: Vec<u8>) {
121        let message = String::from_utf8(buffer).expect("invalid utf-8");
122        info!("kind: {kind}, message: {message}",);
123        if kind == 1 {
124            self.session_id
125                .set(message)
126                .expect("session id already initialized");
127        }
128    }
129
130    fn on_client_disconnected(&self, clients: usize) -> LoopAction {
131        info!("client disconnected, {clients} remaining");
132        if clients == 0 {
133            LoopAction::Exit
134        } else {
135            LoopAction::Continue
136        }
137    }
138}
139
140pub fn handle_panic() {
141    // wait 500ms for the crash handler process to start up
142    // if it's still not there just write panic info and no minidump
143    let retry_frequency = Duration::from_millis(100);
144    for _ in 0..5 {
145        if CRASH_HANDLER.load(Ordering::Acquire) {
146            log::error!("triggering a crash to generate a minidump...");
147            #[cfg(target_os = "linux")]
148            CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32);
149            #[cfg(not(target_os = "linux"))]
150            CrashHandler.simulate_exception(None);
151            break;
152        }
153        thread::sleep(retry_frequency);
154    }
155}
156
157pub fn crash_server(socket: &Path) {
158    let Ok(mut server) = minidumper::Server::with_name(socket) else {
159        log::info!("Couldn't create socket, there may already be a running crash server");
160        return;
161    };
162    let ab = AtomicBool::new(false);
163    server
164        .run(
165            Box::new(CrashServer {
166                session_id: OnceLock::new(),
167            }),
168            &ab,
169            Some(CRASH_HANDLER_TIMEOUT),
170        )
171        .expect("failed to run server");
172}