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}