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}