diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1894023084..75bcc2512a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -52,6 +52,7 @@ pub use user::*; lazy_static! { pub static ref ZED_SERVER_URL: String = std::env::var("ZED_SERVER_URL").unwrap_or_else(|_| "https://zed.dev".to_string()); + pub static ref ZED_RPC_URL: Option = std::env::var("ZED_RPC_URL").ok(); pub static ref IMPERSONATE_LOGIN: Option = std::env::var("ZED_IMPERSONATE") .ok() .and_then(|s| if s.is_empty() { None } else { Some(s) }); @@ -933,6 +934,10 @@ impl Client { http: Arc, release_channel: Option, ) -> Result { + if let Some(url) = &*ZED_RPC_URL { + return Url::parse(url).context("invalid rpc url"); + } + let mut url = format!("{}/rpc", *ZED_SERVER_URL); if let Some(preview_param) = release_channel.and_then(|channel| channel.release_query_param()) @@ -941,14 +946,6 @@ impl Client { url += preview_param; } let response = http.get(&url, Default::default(), false).await?; - - // Normally, ZED_SERVER_URL is set to the URL of zed.dev website. - // The website's /rpc endpoint redirects to a collab server's /rpc endpoint, - // which requires authorization via an HTTP header. - // - // For testing purposes, ZED_SERVER_URL can also set to the direct URL of - // of a collab server. In that case, a request to the /rpc endpoint will - // return an 'unauthorized' response. let collab_url = if response.status().is_redirection() { response .headers() @@ -957,8 +954,6 @@ impl Client { .to_str() .map_err(EstablishConnectionError::other)? .to_string() - } else if response.status() == StatusCode::UNAUTHORIZED { - url } else { Err(anyhow!( "unexpected /rpc response status {}", diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index ff1f6080f7..2df28def4c 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -15,6 +15,8 @@ lazy_static::lazy_static! { pub static ref COPILOT_DIR: PathBuf = HOME.join("Library/Application Support/Zed/copilot"); pub static ref DEFAULT_PRETTIER_DIR: PathBuf = HOME.join("Library/Application Support/Zed/prettier"); pub static ref DB_DIR: PathBuf = HOME.join("Library/Application Support/Zed/db"); + pub static ref CRASHES_DIR: PathBuf = HOME.join("Library/Logs/DiagnosticReports"); + pub static ref CRASHES_RETIRED_DIR: PathBuf = HOME.join("Library/Logs/DiagnosticReports/Retired"); pub static ref SETTINGS: PathBuf = CONFIG_DIR.join("settings.json"); pub static ref KEYMAP: PathBuf = CONFIG_DIR.join("keymap.json"); pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt"); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0c9f3d371e..be07ba6a5f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -43,7 +43,8 @@ use util::{ async_maybe, channel::{parse_zed_link, AppCommitSha, ReleaseChannel, RELEASE_CHANNEL}, http::{self, HttpClient}, - paths, ResultExt, + paths::{self, CRASHES_DIR, CRASHES_RETIRED_DIR}, + ResultExt, }; use uuid::Uuid; use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN}; @@ -229,14 +230,14 @@ fn main() { initialize_workspace(app_state.clone(), cx); if stdout_is_a_pty() { + upload_panics_and_crashes(http.clone(), cx); cx.activate(true); let urls = collect_url_args(); if !urls.is_empty() { listener.open_urls(&urls) } } else { - upload_previous_panics(http.clone(), cx); - + upload_panics_and_crashes(http.clone(), cx); // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead // of an *app, hence gets no specific callbacks run. Emulate them here, if needed. if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() @@ -543,7 +544,22 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin let mut backtrace = backtrace .frames() .iter() - .filter_map(|frame| Some(format!("{:#}", frame.symbols().first()?.name()?))) + .flat_map(|frame| { + frame.symbols().iter().filter_map(|symbol| { + let name = symbol.name()?; + let addr = symbol.addr()? as usize; + let position = if let (Some(path), Some(lineno)) = ( + symbol.filename().and_then(|path| path.file_name()), + symbol.lineno(), + ) { + format!("{}:{}", path.to_string_lossy(), lineno) + } else { + "?".to_string() + }; + + Some(format!("{:} ({:#x}) at {}", name, addr, position)) + }) + }) .collect::>(); // Strip out leading stack frames for rust panic-handling. @@ -599,77 +615,154 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin })); } -fn upload_previous_panics(http: Arc, cx: &mut AppContext) { +fn upload_panics_and_crashes(http: Arc, cx: &mut AppContext) { let telemetry_settings = *client::TelemetrySettings::get_global(cx); - cx.background_executor() .spawn(async move { - let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL); - let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?; - while let Some(child) = children.next().await { - let child = child?; - let child_path = child.path(); - - if child_path.extension() != Some(OsStr::new("panic")) { - continue; - } - let filename = if let Some(filename) = child_path.file_name() { - filename.to_string_lossy() - } else { - continue; - }; - - if !filename.starts_with("zed") { - continue; - } - - if telemetry_settings.diagnostics { - let panic_file_content = smol::fs::read_to_string(&child_path) - .await - .context("error reading panic file")?; - - let panic = serde_json::from_str(&panic_file_content) - .ok() - .or_else(|| { - panic_file_content - .lines() - .next() - .and_then(|line| serde_json::from_str(line).ok()) - }) - .unwrap_or_else(|| { - log::error!( - "failed to deserialize panic file {:?}", - panic_file_content - ); - None - }); - - if let Some(panic) = panic { - let body = serde_json::to_string(&PanicRequest { - panic, - token: client::ZED_SECRET_CLIENT_TOKEN.into(), - }) - .unwrap(); - - let request = Request::post(&panic_report_url) - .redirect_policy(isahc::config::RedirectPolicy::Follow) - .header("Content-Type", "application/json") - .body(body.into())?; - let response = http.send(request).await.context("error sending panic")?; - if !response.status().is_success() { - log::error!("Error uploading panic to server: {}", response.status()); - } - } - } - - // We've done what we can, delete the file - std::fs::remove_file(child_path) - .context("error removing panic") - .log_err(); - } - Ok::<_, anyhow::Error>(()) + upload_previous_panics(http.clone(), telemetry_settings) + .await + .log_err(); + upload_previous_crashes(http, telemetry_settings) + .await + .log_err() }) - .detach_and_log_err(cx); + .detach() +} + +/// upload panics to us (via zed.dev) +async fn upload_previous_panics( + http: Arc, + telemetry_settings: client::TelemetrySettings, +) -> Result<()> { + let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL); + let mut children = smol::fs::read_dir(&*paths::LOGS_DIR).await?; + while let Some(child) = children.next().await { + let child = child?; + let child_path = child.path(); + + if child_path.extension() != Some(OsStr::new("panic")) { + continue; + } + let filename = if let Some(filename) = child_path.file_name() { + filename.to_string_lossy() + } else { + continue; + }; + + if !filename.starts_with("zed") { + continue; + } + + if telemetry_settings.diagnostics { + let panic_file_content = smol::fs::read_to_string(&child_path) + .await + .context("error reading panic file")?; + + let panic = serde_json::from_str(&panic_file_content) + .ok() + .or_else(|| { + panic_file_content + .lines() + .next() + .and_then(|line| serde_json::from_str(line).ok()) + }) + .unwrap_or_else(|| { + log::error!("failed to deserialize panic file {:?}", panic_file_content); + None + }); + + if let Some(panic) = panic { + let body = serde_json::to_string(&PanicRequest { + panic, + token: client::ZED_SECRET_CLIENT_TOKEN.into(), + }) + .unwrap(); + + let request = Request::post(&panic_report_url) + .redirect_policy(isahc::config::RedirectPolicy::Follow) + .header("Content-Type", "application/json") + .body(body.into())?; + let response = http.send(request).await.context("error sending panic")?; + if !response.status().is_success() { + log::error!("Error uploading panic to server: {}", response.status()); + } + } + } + + // We've done what we can, delete the file + std::fs::remove_file(child_path) + .context("error removing panic") + .log_err(); + } + Ok::<_, anyhow::Error>(()) +} + +static LAST_CRASH_UPLOADED: &'static str = "LAST_CRASH_UPLOADED"; + +/// upload crashes from apple's diagnostic reports to our server. +/// (only if telemetry is enabled) +async fn upload_previous_crashes( + http: Arc, + telemetry_settings: client::TelemetrySettings, +) -> Result<()> { + if !telemetry_settings.diagnostics { + return Ok(()); + } + let last_uploaded = KEY_VALUE_STORE + .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. + let mut uploaded = last_uploaded.clone(); + + let crash_report_url = format!("{}/api/crash", &*client::ZED_SERVER_URL); + + for dir in [&*CRASHES_DIR, &*CRASHES_RETIRED_DIR] { + let mut children = smol::fs::read_dir(&dir).await?; + while let Some(child) = children.next().await { + let child = child?; + let Some(filename) = child + .path() + .file_name() + .map(|f| f.to_string_lossy().to_lowercase()) + else { + continue; + }; + + if !filename.starts_with("zed-") || !filename.ends_with(".ips") { + continue; + } + + if filename <= last_uploaded { + continue; + } + + let body = smol::fs::read_to_string(&child.path()) + .await + .context("error reading crash file")?; + + let request = Request::post(&crash_report_url) + .redirect_policy(isahc::config::RedirectPolicy::Follow) + .header("Content-Type", "text/plain") + .header( + "Authorization", + format!("token {}", client::ZED_SECRET_CLIENT_TOKEN), + ) + .body(body.into())?; + + let response = http.send(request).await.context("error sending crash")?; + if !response.status().is_success() { + log::error!("Error uploading crash to server: {}", response.status()); + } + + if uploaded < filename { + uploaded = filename.clone(); + KEY_VALUE_STORE + .write_kvp(LAST_CRASH_UPLOADED.to_string(), filename) + .await?; + } + } + } + + Ok(()) } async fn load_login_shell_environment() -> Result<()> { diff --git a/script/zed-local b/script/zed-local index 4ae4013a4c..5fa73bbbb2 100755 --- a/script/zed-local +++ b/script/zed-local @@ -116,7 +116,8 @@ setTimeout(() => { ZED_WINDOW_POSITION: positions[i], ZED_STATELESS: "1", ZED_ALWAYS_ACTIVE: "1", - ZED_SERVER_URL: "http://localhost:8080", + ZED_SERVER_URL: "http://localhost:3000", + ZED_RPC_URL: "http://localhost:8080/rpc", ZED_ADMIN_API_TOKEN: "secret", ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`, PATH: process.env.PATH,