crashes.rs

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