Separate minidump crashes from panics (#36267)

The minidump-based crash reporting is now entirely separate from our
legacy panic_hook-based reporting. This should improve the association
of minidumps with their metadata and give us more consistent crash
reports.

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Julia Ryan 2025-08-16 01:33:32 -05:00 committed by GitHub
parent f5f14111ef
commit 7784fac288
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 315 additions and 248 deletions

2
Cargo.lock generated
View file

@ -4038,6 +4038,8 @@ dependencies = [
"minidumper", "minidumper",
"paths", "paths",
"release_channel", "release_channel",
"serde",
"serde_json",
"smol", "smol",
"workspace-hack", "workspace-hack",
] ]

View file

@ -12,6 +12,8 @@ minidumper.workspace = true
paths.workspace = true paths.workspace = true
release_channel.workspace = true release_channel.workspace = true
smol.workspace = true smol.workspace = true
serde.workspace = true
serde_json.workspace = true
workspace-hack.workspace = true workspace-hack.workspace = true
[lints] [lints]

View file

@ -2,15 +2,17 @@ use crash_handler::CrashHandler;
use log::info; use log::info;
use minidumper::{Client, LoopAction, MinidumpBinary}; use minidumper::{Client, LoopAction, MinidumpBinary};
use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
use serde::{Deserialize, Serialize};
use std::{ use std::{
env, env,
fs::File, fs::{self, File},
io, io,
panic::Location,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{self, Command}, process::{self, Command},
sync::{ sync::{
LazyLock, OnceLock, Arc, OnceLock,
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
}, },
thread, thread,
@ -18,19 +20,17 @@ use std::{
}; };
// set once the crash handler has initialized and the client has connected to it // set once the crash handler has initialized and the client has connected to it
pub static CRASH_HANDLER: AtomicBool = AtomicBool::new(false); pub static CRASH_HANDLER: OnceLock<Arc<Client>> = OnceLock::new();
// set when the first minidump request is made to avoid generating duplicate crash reports // set when the first minidump request is made to avoid generating duplicate crash reports
pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false); pub static REQUESTED_MINIDUMP: AtomicBool = AtomicBool::new(false);
const CRASH_HANDLER_TIMEOUT: Duration = Duration::from_secs(60); const CRASH_HANDLER_PING_TIMEOUT: Duration = Duration::from_secs(60);
const CRASH_HANDLER_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
pub static GENERATE_MINIDUMPS: LazyLock<bool> = LazyLock::new(|| { pub async fn init(crash_init: InitCrashHandler) {
*RELEASE_CHANNEL != ReleaseChannel::Dev || env::var("ZED_GENERATE_MINIDUMPS").is_ok() if *RELEASE_CHANNEL == ReleaseChannel::Dev && env::var("ZED_GENERATE_MINIDUMPS").is_err() {
});
pub async fn init(id: String) {
if !*GENERATE_MINIDUMPS {
return; return;
} }
let exe = env::current_exe().expect("unable to find ourselves"); let exe = env::current_exe().expect("unable to find ourselves");
let zed_pid = process::id(); let zed_pid = process::id();
// TODO: we should be able to get away with using 1 crash-handler process per machine, // TODO: we should be able to get away with using 1 crash-handler process per machine,
@ -61,9 +61,11 @@ pub async fn init(id: String) {
smol::Timer::after(retry_frequency).await; smol::Timer::after(retry_frequency).await;
} }
let client = maybe_client.unwrap(); let client = maybe_client.unwrap();
client.send_message(1, id).unwrap(); // set session id on the server client
.send_message(1, serde_json::to_vec(&crash_init).unwrap())
.unwrap();
let client = std::sync::Arc::new(client); let client = Arc::new(client);
let handler = crash_handler::CrashHandler::attach(unsafe { let handler = crash_handler::CrashHandler::attach(unsafe {
let client = client.clone(); let client = client.clone();
crash_handler::make_crash_event(move |crash_context: &crash_handler::CrashContext| { crash_handler::make_crash_event(move |crash_context: &crash_handler::CrashContext| {
@ -72,7 +74,6 @@ pub async fn init(id: String) {
.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
.is_ok() .is_ok()
{ {
client.send_message(2, "mistakes were made").unwrap();
client.ping().unwrap(); client.ping().unwrap();
client.request_dump(crash_context).is_ok() client.request_dump(crash_context).is_ok()
} else { } else {
@ -87,7 +88,7 @@ pub async fn init(id: String) {
{ {
handler.set_ptracer(Some(server_pid)); handler.set_ptracer(Some(server_pid));
} }
CRASH_HANDLER.store(true, Ordering::Release); CRASH_HANDLER.set(client.clone()).ok();
std::mem::forget(handler); std::mem::forget(handler);
info!("crash handler registered"); info!("crash handler registered");
@ -98,14 +99,43 @@ pub async fn init(id: String) {
} }
pub struct CrashServer { pub struct CrashServer {
session_id: OnceLock<String>, initialization_params: OnceLock<InitCrashHandler>,
panic_info: OnceLock<CrashPanic>,
has_connection: Arc<AtomicBool>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CrashInfo {
pub init: InitCrashHandler,
pub panic: Option<CrashPanic>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct InitCrashHandler {
pub session_id: String,
pub zed_version: String,
pub release_channel: String,
pub commit_sha: String,
// pub gpu: String,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct CrashPanic {
pub message: String,
pub span: String,
} }
impl minidumper::ServerHandler for CrashServer { impl minidumper::ServerHandler for CrashServer {
fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> { fn create_minidump_file(&self) -> Result<(File, PathBuf), io::Error> {
let err_message = "Need to send a message with the ID upon starting the crash handler"; let err_message = "Missing initialization data";
let dump_path = paths::logs_dir() let dump_path = paths::logs_dir()
.join(self.session_id.get().expect(err_message)) .join(
&self
.initialization_params
.get()
.expect(err_message)
.session_id,
)
.with_extension("dmp"); .with_extension("dmp");
let file = File::create(&dump_path)?; let file = File::create(&dump_path)?;
Ok((file, dump_path)) Ok((file, dump_path))
@ -122,38 +152,71 @@ impl minidumper::ServerHandler for CrashServer {
info!("failed to write minidump: {:#}", e); info!("failed to write minidump: {:#}", e);
} }
} }
let crash_info = CrashInfo {
init: self
.initialization_params
.get()
.expect("not initialized")
.clone(),
panic: self.panic_info.get().cloned(),
};
let crash_data_path = paths::logs_dir()
.join(&crash_info.init.session_id)
.with_extension("json");
fs::write(crash_data_path, serde_json::to_vec(&crash_info).unwrap()).ok();
LoopAction::Exit LoopAction::Exit
} }
fn on_message(&self, kind: u32, buffer: Vec<u8>) { fn on_message(&self, kind: u32, buffer: Vec<u8>) {
let message = String::from_utf8(buffer).expect("invalid utf-8"); match kind {
info!("kind: {kind}, message: {message}",); 1 => {
if kind == 1 { let init_data =
self.session_id serde_json::from_slice::<InitCrashHandler>(&buffer).expect("invalid init data");
.set(message) self.initialization_params
.expect("session id already initialized"); .set(init_data)
.expect("already initialized");
}
2 => {
let panic_data =
serde_json::from_slice::<CrashPanic>(&buffer).expect("invalid panic data");
self.panic_info.set(panic_data).expect("already panicked");
}
_ => {
panic!("invalid message kind");
}
} }
} }
fn on_client_disconnected(&self, clients: usize) -> LoopAction { fn on_client_disconnected(&self, _clients: usize) -> LoopAction {
info!("client disconnected, {clients} remaining"); LoopAction::Exit
if clients == 0 { }
LoopAction::Exit
} else { fn on_client_connected(&self, _clients: usize) -> LoopAction {
LoopAction::Continue self.has_connection.store(true, Ordering::SeqCst);
} LoopAction::Continue
} }
} }
pub fn handle_panic() { pub fn handle_panic(message: String, span: Option<&Location>) {
if !*GENERATE_MINIDUMPS { let span = span
return; .map(|loc| format!("{}:{}", loc.file(), loc.line()))
} .unwrap_or_default();
// wait 500ms for the crash handler process to start up // wait 500ms for the crash handler process to start up
// if it's still not there just write panic info and no minidump // if it's still not there just write panic info and no minidump
let retry_frequency = Duration::from_millis(100); let retry_frequency = Duration::from_millis(100);
for _ in 0..5 { for _ in 0..5 {
if CRASH_HANDLER.load(Ordering::Acquire) { if let Some(client) = CRASH_HANDLER.get() {
client
.send_message(
2,
serde_json::to_vec(&CrashPanic { message, span }).unwrap(),
)
.ok();
log::error!("triggering a crash to generate a minidump..."); log::error!("triggering a crash to generate a minidump...");
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32); CrashHandler.simulate_signal(crash_handler::Signal::Trap as u32);
@ -170,14 +233,30 @@ pub fn crash_server(socket: &Path) {
log::info!("Couldn't create socket, there may already be a running crash server"); log::info!("Couldn't create socket, there may already be a running crash server");
return; return;
}; };
let ab = AtomicBool::new(false);
let shutdown = Arc::new(AtomicBool::new(false));
let has_connection = Arc::new(AtomicBool::new(false));
std::thread::spawn({
let shutdown = shutdown.clone();
let has_connection = has_connection.clone();
move || {
std::thread::sleep(CRASH_HANDLER_CONNECT_TIMEOUT);
if !has_connection.load(Ordering::SeqCst) {
shutdown.store(true, Ordering::SeqCst);
}
}
});
server server
.run( .run(
Box::new(CrashServer { Box::new(CrashServer {
session_id: OnceLock::new(), initialization_params: OnceLock::new(),
panic_info: OnceLock::new(),
has_connection,
}), }),
&ab, &shutdown,
Some(CRASH_HANDLER_TIMEOUT), Some(CRASH_HANDLER_PING_TIMEOUT),
) )
.expect("failed to run server"); .expect("failed to run server");
} }

View file

@ -28,11 +28,13 @@ message GetCrashFiles {
message GetCrashFilesResponse { message GetCrashFilesResponse {
repeated CrashReport crashes = 1; repeated CrashReport crashes = 1;
repeated string legacy_panics = 2;
} }
message CrashReport { message CrashReport {
optional string panic_contents = 1; reserved 1, 2;
optional bytes minidump_contents = 2; string metadata = 3;
bytes minidump_contents = 4;
} }
message Extension { message Extension {

View file

@ -1490,20 +1490,17 @@ impl RemoteConnection for SshRemoteConnection {
identifier = &unique_identifier, identifier = &unique_identifier,
); );
if let Some(rust_log) = std::env::var("RUST_LOG").ok() { for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
start_proxy_command = format!( if let Some(value) = std::env::var(env_var).ok() {
"RUST_LOG={} {}", start_proxy_command = format!(
shlex::try_quote(&rust_log).unwrap(), "{}={} {} ",
start_proxy_command env_var,
) shlex::try_quote(&value).unwrap(),
} start_proxy_command,
if let Some(rust_backtrace) = std::env::var("RUST_BACKTRACE").ok() { );
start_proxy_command = format!( }
"RUST_BACKTRACE={} {}",
shlex::try_quote(&rust_backtrace).unwrap(),
start_proxy_command
)
} }
if reconnect { if reconnect {
start_proxy_command.push_str(" --reconnect"); start_proxy_command.push_str(" --reconnect");
} }
@ -2241,8 +2238,7 @@ impl SshRemoteConnection {
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
{ {
run_cmd(Command::new("gzip").args(["-9", "-f", &bin_path.to_string_lossy()])) run_cmd(Command::new("gzip").args(["-f", &bin_path.to_string_lossy()])).await?;
.await?;
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
@ -2474,7 +2470,7 @@ impl ChannelClient {
}, },
async { async {
smol::Timer::after(timeout).await; smol::Timer::after(timeout).await;
anyhow::bail!("Timeout detected") anyhow::bail!("Timed out resyncing remote client")
}, },
) )
.await .await
@ -2488,7 +2484,7 @@ impl ChannelClient {
}, },
async { async {
smol::Timer::after(timeout).await; smol::Timer::after(timeout).await;
anyhow::bail!("Timeout detected") anyhow::bail!("Timed out pinging remote client")
}, },
) )
.await .await

View file

@ -34,10 +34,10 @@ use smol::io::AsyncReadExt;
use smol::Async; use smol::Async;
use smol::{net::unix::UnixListener, stream::StreamExt as _}; use smol::{net::unix::UnixListener, stream::StreamExt as _};
use std::collections::HashMap;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::ops::ControlFlow; use std::ops::ControlFlow;
use std::str::FromStr; use std::str::FromStr;
use std::sync::LazyLock;
use std::{env, thread}; use std::{env, thread};
use std::{ use std::{
io::Write, io::Write,
@ -48,6 +48,13 @@ use std::{
use telemetry_events::LocationData; use telemetry_events::LocationData;
use util::ResultExt; use util::ResultExt;
pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL {
ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION"),
ReleaseChannel::Nightly | ReleaseChannel::Dev => {
option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha")
}
});
fn init_logging_proxy() { fn init_logging_proxy() {
env_logger::builder() env_logger::builder()
.format(|buf, record| { .format(|buf, record| {
@ -113,7 +120,6 @@ fn init_logging_server(log_file_path: PathBuf) -> Result<Receiver<Vec<u8>>> {
fn init_panic_hook(session_id: String) { fn init_panic_hook(session_id: String) {
std::panic::set_hook(Box::new(move |info| { std::panic::set_hook(Box::new(move |info| {
crashes::handle_panic();
let payload = info let payload = info
.payload() .payload()
.downcast_ref::<&str>() .downcast_ref::<&str>()
@ -121,6 +127,8 @@ fn init_panic_hook(session_id: String) {
.or_else(|| info.payload().downcast_ref::<String>().cloned()) .or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Box<Any>".to_string()); .unwrap_or_else(|| "Box<Any>".to_string());
crashes::handle_panic(payload.clone(), info.location());
let backtrace = backtrace::Backtrace::new(); let backtrace = backtrace::Backtrace::new();
let mut backtrace = backtrace let mut backtrace = backtrace
.frames() .frames()
@ -150,14 +158,6 @@ fn init_panic_hook(session_id: String) {
(&backtrace).join("\n") (&backtrace).join("\n")
); );
let release_channel = *RELEASE_CHANNEL;
let version = match release_channel {
ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION"),
ReleaseChannel::Nightly | ReleaseChannel::Dev => {
option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha")
}
};
let panic_data = telemetry_events::Panic { let panic_data = telemetry_events::Panic {
thread: thread_name.into(), thread: thread_name.into(),
payload: payload.clone(), payload: payload.clone(),
@ -165,9 +165,9 @@ fn init_panic_hook(session_id: String) {
file: location.file().into(), file: location.file().into(),
line: location.line(), line: location.line(),
}), }),
app_version: format!("remote-server-{version}"), app_version: format!("remote-server-{}", *VERSION),
app_commit_sha: option_env!("ZED_COMMIT_SHA").map(|sha| sha.into()), app_commit_sha: option_env!("ZED_COMMIT_SHA").map(|sha| sha.into()),
release_channel: release_channel.dev_name().into(), release_channel: RELEASE_CHANNEL.dev_name().into(),
target: env!("TARGET").to_owned().into(), target: env!("TARGET").to_owned().into(),
os_name: telemetry::os_name(), os_name: telemetry::os_name(),
os_version: Some(telemetry::os_version()), os_version: Some(telemetry::os_version()),
@ -204,8 +204,8 @@ fn handle_crash_files_requests(project: &Entity<HeadlessProject>, client: &Arc<C
client.add_request_handler( client.add_request_handler(
project.downgrade(), project.downgrade(),
|_, _: TypedEnvelope<proto::GetCrashFiles>, _cx| async move { |_, _: TypedEnvelope<proto::GetCrashFiles>, _cx| async move {
let mut legacy_panics = Vec::new();
let mut crashes = Vec::new(); let mut crashes = Vec::new();
let mut minidumps_by_session_id = HashMap::new();
let mut children = smol::fs::read_dir(paths::logs_dir()).await?; let mut children = smol::fs::read_dir(paths::logs_dir()).await?;
while let Some(child) = children.next().await { while let Some(child) = children.next().await {
let child = child?; let child = child?;
@ -227,41 +227,31 @@ fn handle_crash_files_requests(project: &Entity<HeadlessProject>, client: &Arc<C
.await .await
.context("error reading panic file")?; .context("error reading panic file")?;
crashes.push(proto::CrashReport { legacy_panics.push(file_contents);
panic_contents: Some(file_contents), smol::fs::remove_file(&child_path)
minidump_contents: None, .await
}); .context("error removing panic")
.log_err();
} else if extension == Some(OsStr::new("dmp")) { } else if extension == Some(OsStr::new("dmp")) {
let session_id = child_path.file_stem().unwrap().to_string_lossy(); let mut json_path = child_path.clone();
minidumps_by_session_id json_path.set_extension("json");
.insert(session_id.to_string(), smol::fs::read(&child_path).await?); if let Ok(json_content) = smol::fs::read_to_string(&json_path).await {
} crashes.push(CrashReport {
metadata: json_content,
// We've done what we can, delete the file minidump_contents: smol::fs::read(&child_path).await?,
smol::fs::remove_file(&child_path) });
.await smol::fs::remove_file(&child_path).await.log_err();
.context("error removing panic") smol::fs::remove_file(&json_path).await.log_err();
.log_err(); } else {
} log::error!("Couldn't find json metadata for crash: {child_path:?}");
}
for crash in &mut crashes {
let panic: telemetry_events::Panic =
serde_json::from_str(crash.panic_contents.as_ref().unwrap())?;
if let dump @ Some(_) = minidumps_by_session_id.remove(&panic.session_id) {
crash.minidump_contents = dump;
} }
} }
crashes.extend( anyhow::Ok(proto::GetCrashFilesResponse {
minidumps_by_session_id crashes,
.into_values() legacy_panics,
.map(|dmp| CrashReport { })
panic_contents: None,
minidump_contents: Some(dmp),
}),
);
anyhow::Ok(proto::GetCrashFilesResponse { crashes })
}, },
); );
} }
@ -442,7 +432,12 @@ pub fn execute_run(
let app = gpui::Application::headless(); let app = gpui::Application::headless();
let id = std::process::id().to_string(); let id = std::process::id().to_string();
app.background_executor() app.background_executor()
.spawn(crashes::init(id.clone())) .spawn(crashes::init(crashes::InitCrashHandler {
session_id: id.clone(),
zed_version: VERSION.to_owned(),
release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(),
commit_sha: option_env!("ZED_COMMIT_SHA").unwrap_or("no_sha").to_owned(),
}))
.detach(); .detach();
init_panic_hook(id); init_panic_hook(id);
let log_rx = init_logging_server(log_file)?; let log_rx = init_logging_server(log_file)?;
@ -569,7 +564,13 @@ pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> {
let server_paths = ServerPaths::new(&identifier)?; let server_paths = ServerPaths::new(&identifier)?;
let id = std::process::id().to_string(); let id = std::process::id().to_string();
smol::spawn(crashes::init(id.clone())).detach(); smol::spawn(crashes::init(crashes::InitCrashHandler {
session_id: id.clone(),
zed_version: VERSION.to_owned(),
release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(),
commit_sha: option_env!("ZED_COMMIT_SHA").unwrap_or("no_sha").to_owned(),
}))
.detach();
init_panic_hook(id); init_panic_hook(id);
log::info!("starting proxy process. PID: {}", std::process::id()); log::info!("starting proxy process. PID: {}", std::process::id());

View file

@ -8,6 +8,7 @@ use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::{Client, ProxySettings, UserStore, parse_zed_link}; use client::{Client, ProxySettings, UserStore, parse_zed_link};
use collab_ui::channel_view::ChannelView; use collab_ui::channel_view::ChannelView;
use collections::HashMap; use collections::HashMap;
use crashes::InitCrashHandler;
use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
use editor::Editor; use editor::Editor;
use extension::ExtensionHostProxy; use extension::ExtensionHostProxy;
@ -269,7 +270,15 @@ pub fn main() {
let session = app.background_executor().block(Session::new()); let session = app.background_executor().block(Session::new());
app.background_executor() app.background_executor()
.spawn(crashes::init(session_id.clone())) .spawn(crashes::init(InitCrashHandler {
session_id: session_id.clone(),
zed_version: app_version.to_string(),
release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(),
commit_sha: app_commit_sha
.as_ref()
.map(|sha| sha.full())
.unwrap_or_else(|| "no sha".to_owned()),
}))
.detach(); .detach();
reliability::init_panic_hook( reliability::init_panic_hook(
app_version, app_version,

View file

@ -12,6 +12,7 @@ use gpui::{App, AppContext as _, SemanticVersion};
use http_client::{self, HttpClient, HttpClientWithUrl, HttpRequestExt, Method}; use http_client::{self, HttpClient, HttpClientWithUrl, HttpRequestExt, Method};
use paths::{crashes_dir, crashes_retired_dir}; use paths::{crashes_dir, crashes_retired_dir};
use project::Project; use project::Project;
use proto::{CrashReport, GetCrashFilesResponse};
use release_channel::{AppCommitSha, RELEASE_CHANNEL, ReleaseChannel}; use release_channel::{AppCommitSha, RELEASE_CHANNEL, ReleaseChannel};
use reqwest::multipart::{Form, Part}; use reqwest::multipart::{Form, Part};
use settings::Settings; use settings::Settings;
@ -51,10 +52,6 @@ pub fn init_panic_hook(
thread::yield_now(); thread::yield_now();
} }
} }
crashes::handle_panic();
let thread = thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>");
let payload = info let payload = info
.payload() .payload()
@ -63,6 +60,11 @@ pub fn init_panic_hook(
.or_else(|| info.payload().downcast_ref::<String>().cloned()) .or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "Box<Any>".to_string()); .unwrap_or_else(|| "Box<Any>".to_string());
crashes::handle_panic(payload.clone(), info.location());
let thread = thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>");
if *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev { if *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev {
let location = info.location().unwrap(); let location = info.location().unwrap();
let backtrace = Backtrace::new(); let backtrace = Backtrace::new();
@ -214,45 +216,53 @@ pub fn init(
let installation_id = installation_id.clone(); let installation_id = installation_id.clone();
let system_id = system_id.clone(); let system_id = system_id.clone();
if let Some(ssh_client) = project.ssh_client() { let Some(ssh_client) = project.ssh_client() else {
ssh_client.update(cx, |client, cx| { return;
if TelemetrySettings::get_global(cx).diagnostics { };
let request = client.proto_client().request(proto::GetCrashFiles {}); ssh_client.update(cx, |client, cx| {
cx.background_spawn(async move { if !TelemetrySettings::get_global(cx).diagnostics {
let crash_files = request.await?; return;
for crash in crash_files.crashes { }
let mut panic: Option<Panic> = crash let request = client.proto_client().request(proto::GetCrashFiles {});
.panic_contents cx.background_spawn(async move {
.and_then(|s| serde_json::from_str(&s).log_err()); let GetCrashFilesResponse {
legacy_panics,
crashes,
} = request.await?;
if let Some(panic) = panic.as_mut() { for panic in legacy_panics {
panic.session_id = session_id.clone(); if let Some(mut panic) = serde_json::from_str::<Panic>(&panic).log_err() {
panic.system_id = system_id.clone(); panic.session_id = session_id.clone();
panic.installation_id = installation_id.clone(); panic.system_id = system_id.clone();
} panic.installation_id = installation_id.clone();
upload_panic(&http_client, &panic_report_url, panic, &mut None).await?;
if let Some(minidump) = crash.minidump_contents { }
upload_minidump(
http_client.clone(),
minidump.clone(),
panic.as_ref(),
)
.await
.log_err();
}
if let Some(panic) = panic {
upload_panic(&http_client, &panic_report_url, panic, &mut None)
.await?;
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
} }
let Some(endpoint) = MINIDUMP_ENDPOINT.as_ref() else {
return Ok(());
};
for CrashReport {
metadata,
minidump_contents,
} in crashes
{
if let Some(metadata) = serde_json::from_str(&metadata).log_err() {
upload_minidump(
http_client.clone(),
endpoint,
minidump_contents,
&metadata,
)
.await
.log_err();
}
}
anyhow::Ok(())
}) })
} .detach_and_log_err(cx);
})
}) })
.detach(); .detach();
} }
@ -466,16 +476,18 @@ fn upload_panics_and_crashes(
installation_id: Option<String>, installation_id: Option<String>,
cx: &App, cx: &App,
) { ) {
let telemetry_settings = *client::TelemetrySettings::get_global(cx); if !client::TelemetrySettings::get_global(cx).diagnostics {
return;
}
cx.background_spawn(async move { cx.background_spawn(async move {
let most_recent_panic = upload_previous_minidumps(http.clone()).await.warn_on_err();
upload_previous_panics(http.clone(), &panic_report_url, telemetry_settings) let most_recent_panic = upload_previous_panics(http.clone(), &panic_report_url)
.await
.log_err()
.flatten();
upload_previous_crashes(http, most_recent_panic, installation_id, telemetry_settings)
.await .await
.log_err() .log_err()
.flatten();
upload_previous_crashes(http, most_recent_panic, installation_id)
.await
.log_err();
}) })
.detach() .detach()
} }
@ -484,7 +496,6 @@ fn upload_panics_and_crashes(
async fn upload_previous_panics( async fn upload_previous_panics(
http: Arc<HttpClientWithUrl>, http: Arc<HttpClientWithUrl>,
panic_report_url: &Url, panic_report_url: &Url,
telemetry_settings: client::TelemetrySettings,
) -> anyhow::Result<Option<(i64, String)>> { ) -> anyhow::Result<Option<(i64, String)>> {
let mut children = smol::fs::read_dir(paths::logs_dir()).await?; let mut children = smol::fs::read_dir(paths::logs_dir()).await?;
@ -507,58 +518,41 @@ async fn upload_previous_panics(
continue; continue;
} }
if telemetry_settings.diagnostics { let panic_file_content = smol::fs::read_to_string(&child_path)
let panic_file_content = smol::fs::read_to_string(&child_path) .await
.await .context("error reading panic file")?;
.context("error reading panic file")?;
let panic: Option<Panic> = serde_json::from_str(&panic_file_content) let panic: Option<Panic> = serde_json::from_str(&panic_file_content)
.log_err() .log_err()
.or_else(|| { .or_else(|| {
panic_file_content panic_file_content
.lines() .lines()
.next() .next()
.and_then(|line| serde_json::from_str(line).ok()) .and_then(|line| serde_json::from_str(line).ok())
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
log::error!("failed to deserialize panic file {:?}", panic_file_content); log::error!("failed to deserialize panic file {:?}", panic_file_content);
None None
}); });
if let Some(panic) = panic { if let Some(panic) = panic
let minidump_path = paths::logs_dir() && upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await?
.join(&panic.session_id) {
.with_extension("dmp"); // We've done what we can, delete the file
if minidump_path.exists() { fs::remove_file(child_path)
let minidump = smol::fs::read(&minidump_path) .context("error removing panic")
.await .log_err();
.context("Failed to read minidump")?;
if upload_minidump(http.clone(), minidump, Some(&panic))
.await
.log_err()
.is_some()
{
fs::remove_file(minidump_path).ok();
}
}
if !upload_panic(&http, &panic_report_url, panic, &mut most_recent_panic).await? {
continue;
}
}
} }
// We've done what we can, delete the file
fs::remove_file(child_path)
.context("error removing panic")
.log_err();
} }
if MINIDUMP_ENDPOINT.is_none() { Ok(most_recent_panic)
return Ok(most_recent_panic); }
}
pub async fn upload_previous_minidumps(http: Arc<HttpClientWithUrl>) -> anyhow::Result<()> {
let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else {
return Err(anyhow::anyhow!("Minidump endpoint not set"));
};
// loop back over the directory again to upload any minidumps that are missing panics
let mut children = smol::fs::read_dir(paths::logs_dir()).await?; let mut children = smol::fs::read_dir(paths::logs_dir()).await?;
while let Some(child) = children.next().await { while let Some(child) = children.next().await {
let child = child?; let child = child?;
@ -566,33 +560,35 @@ async fn upload_previous_panics(
if child_path.extension() != Some(OsStr::new("dmp")) { if child_path.extension() != Some(OsStr::new("dmp")) {
continue; continue;
} }
if upload_minidump( let mut json_path = child_path.clone();
http.clone(), json_path.set_extension("json");
smol::fs::read(&child_path) if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) {
.await if upload_minidump(
.context("Failed to read minidump")?, http.clone(),
None, &minidump_endpoint,
) smol::fs::read(&child_path)
.await .await
.log_err() .context("Failed to read minidump")?,
.is_some() &metadata,
{ )
fs::remove_file(child_path).ok(); .await
.log_err()
.is_some()
{
fs::remove_file(child_path).ok();
fs::remove_file(json_path).ok();
}
} }
} }
Ok(())
Ok(most_recent_panic)
} }
async fn upload_minidump( async fn upload_minidump(
http: Arc<HttpClientWithUrl>, http: Arc<HttpClientWithUrl>,
endpoint: &str,
minidump: Vec<u8>, minidump: Vec<u8>,
panic: Option<&Panic>, metadata: &crashes::CrashInfo,
) -> Result<()> { ) -> Result<()> {
let minidump_endpoint = MINIDUMP_ENDPOINT
.to_owned()
.ok_or_else(|| anyhow::anyhow!("Minidump endpoint not set"))?;
let mut form = Form::new() let mut form = Form::new()
.part( .part(
"upload_file_minidump", "upload_file_minidump",
@ -600,38 +596,22 @@ async fn upload_minidump(
.file_name("minidump.dmp") .file_name("minidump.dmp")
.mime_str("application/octet-stream")?, .mime_str("application/octet-stream")?,
) )
.text(
"sentry[tags][channel]",
metadata.init.release_channel.clone(),
)
.text("sentry[tags][version]", metadata.init.zed_version.clone())
.text("sentry[release]", metadata.init.commit_sha.clone())
.text("platform", "rust"); .text("platform", "rust");
if let Some(panic) = panic { if let Some(panic_info) = metadata.panic.as_ref() {
form = form form = form.text("sentry[logentry][formatted]", panic_info.message.clone());
.text("sentry[tags][channel]", panic.release_channel.clone()) form = form.text("span", panic_info.span.clone());
.text("sentry[tags][version]", panic.app_version.clone())
.text("sentry[context][os][name]", panic.os_name.clone())
.text(
"sentry[context][device][architecture]",
panic.architecture.clone(),
)
.text("sentry[logentry][formatted]", panic.payload.clone());
if let Some(sha) = panic.app_commit_sha.clone() {
form = form.text("sentry[release]", sha)
} else {
form = form.text(
"sentry[release]",
format!("{}-{}", panic.release_channel, panic.app_version),
)
}
if let Some(v) = panic.os_version.clone() {
form = form.text("sentry[context][os][release]", v);
}
if let Some(location) = panic.location_data.as_ref() {
form = form.text("span", format!("{}:{}", location.file, location.line))
}
// TODO: add gpu-context, feature-flag-context, and more of device-context like gpu // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu
// name, screen resolution, available ram, device model, etc // name, screen resolution, available ram, device model, etc
} }
let mut response_text = String::new(); let mut response_text = String::new();
let mut response = http.send_multipart_form(&minidump_endpoint, form).await?; let mut response = http.send_multipart_form(endpoint, form).await?;
response response
.body_mut() .body_mut()
.read_to_string(&mut response_text) .read_to_string(&mut response_text)
@ -681,11 +661,7 @@ async fn upload_previous_crashes(
http: Arc<HttpClientWithUrl>, http: Arc<HttpClientWithUrl>,
most_recent_panic: Option<(i64, String)>, most_recent_panic: Option<(i64, String)>,
installation_id: Option<String>, installation_id: Option<String>,
telemetry_settings: client::TelemetrySettings,
) -> Result<()> { ) -> Result<()> {
if !telemetry_settings.diagnostics {
return Ok(());
}
let last_uploaded = KEY_VALUE_STORE let last_uploaded = KEY_VALUE_STORE
.read_kvp(LAST_CRASH_UPLOADED)? .read_kvp(LAST_CRASH_UPLOADED)?
.unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this. .unwrap_or("zed-2024-01-17-221900.ips".to_string()); // don't upload old crash reports from before we had this.