From 680929081d252bb3c41c535704d8b096d4b5de34 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Jan 2024 19:41:02 -0700 Subject: [PATCH 1/5] Send crash reports to Slack automatically --- Cargo.lock | 11 ++ Procfile | 1 - crates/client/src/client.rs | 10 ++ crates/collab/.env.toml | 3 + crates/collab/Cargo.toml | 1 + crates/collab/k8s/collab.template.yml | 15 ++ crates/collab/src/api.rs | 127 +++++++++++++- crates/collab/src/lib.rs | 3 + crates/collab/src/tests/test_server.rs | 3 + crates/util/src/paths.rs | 2 + crates/zed/src/main.rs | 222 +++++++++++++++++-------- 11 files changed, 324 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c3a4e8592..ba85eb1486 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1474,6 +1474,7 @@ dependencies = [ "env_logger", "envy", "file_finder", + "form-data-builder", "fs", "futures 0.3.28", "git", @@ -2690,6 +2691,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form-data-builder" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ff8fb4527b05539a9f573ba2831a1127038a7b45eea385a338a63dc5ab6829" +dependencies = [ + "base64 0.13.1", + "rand 0.8.5", +] + [[package]] name = "form_urlencoded" version = "1.2.0" diff --git a/Procfile b/Procfile index 3f42c3a967..122ad5139f 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,3 @@ -web: cd ../zed.dev && PORT=3000 npm run dev collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve livekit: livekit-server --dev postgrest: postgrest crates/collab/admin_api.conf diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1894023084..da695f6dc4 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -969,6 +969,16 @@ impl Client { Url::parse(&collab_url).context("invalid rpc url") } + // todo: this should probably be cached (And/or done better) + pub async fn get_collab_server_url( + http: Arc, + release_channel: Option, + ) -> Result { + let mut url = Self::get_rpc_url(http, release_channel).await?; + url.set_path(""); + Ok(url) + } + fn establish_websocket_connection( self: &Arc, credentials: &Credentials, diff --git a/crates/collab/.env.toml b/crates/collab/.env.toml index 01866012ea..46140ae9b3 100644 --- a/crates/collab/.env.toml +++ b/crates/collab/.env.toml @@ -2,11 +2,14 @@ DATABASE_URL = "postgres://postgres@localhost/zed" DATABASE_MAX_CONNECTIONS = 5 HTTP_PORT = 8080 API_TOKEN = "secret" +CLIENT_TOKEN = "618033988749894" INVITE_LINK_PREFIX = "http://localhost:3000/invites/" ZED_ENVIRONMENT = "development" LIVE_KIT_SERVER = "http://localhost:7880" LIVE_KIT_KEY = "devkey" LIVE_KIT_SECRET = "secret" +# SLACK_PANIC_CHANNEL = +# SLACK_API_KEY = # RUST_LOG=info # LOG_JSON=true diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index bc273cb12a..f703c88bc0 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -58,6 +58,7 @@ tracing = "0.1.34" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } uuid.workspace = true +form-data-builder = "1.0.1" [dev-dependencies] audio = { path = "../audio" } diff --git a/crates/collab/k8s/collab.template.yml b/crates/collab/k8s/collab.template.yml index 120e5f592f..eb9f4d9172 100644 --- a/crates/collab/k8s/collab.template.yml +++ b/crates/collab/k8s/collab.template.yml @@ -90,6 +90,11 @@ spec: secretKeyRef: name: api key: token + - name: CLIENT_TOKEN + valueFrom: + secretKeyRef: + name: api + key: client_token - name: LIVE_KIT_SERVER valueFrom: secretKeyRef: @@ -105,6 +110,16 @@ spec: secretKeyRef: name: livekit key: secret + - name: SLACK_PANIC_CHANNEL + valueFrom: + secretKeyRef: + name: slack + key: api_key + - name: SLACK_API_KEY + valueFrom: + secretKeyRef: + name: slack + key: panic_channel - name: INVITE_LINK_PREFIX value: ${INVITE_LINK_PREFIX} - name: RUST_BACKTRACE diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index a28aeac9ab..daca13fa85 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -18,19 +18,28 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use tower::ServiceBuilder; use tracing::instrument; +use util::{async_maybe, http::AsyncBody, ResultExt}; pub fn routes(rpc_server: Arc, state: Arc) -> Router { - Router::new() + let called_from_website = Router::new() .route("/user", get(get_authenticated_user)) .route("/users/:id/access_tokens", post(create_access_token)) .route("/panic", post(trace_panic)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .layer( ServiceBuilder::new() - .layer(Extension(state)) + .layer(Extension(state.clone())) .layer(Extension(rpc_server)) .layer(middleware::from_fn(validate_api_token)), - ) + ); + + let called_from_client = Router::new().route("/crash", post(trace_crash)).layer( + ServiceBuilder::new() + .layer(Extension(state)) + .layer(middleware::from_fn(validate_client_secret)), + ); + + called_from_website.merge(called_from_client) } pub async fn validate_api_token(req: Request, next: Next) -> impl IntoResponse { @@ -64,6 +73,37 @@ pub async fn validate_api_token(req: Request, next: Next) -> impl IntoR Ok::<_, Error>(next.run(req).await) } +pub async fn validate_client_secret(req: Request, next: Next) -> impl IntoResponse { + let token = req + .headers() + .get(http::header::AUTHORIZATION) + .and_then(|header| header.to_str().ok()) + .ok_or_else(|| { + Error::Http( + StatusCode::BAD_REQUEST, + "missing authorization header".to_string(), + ) + })? + .strip_prefix("token ") + .ok_or_else(|| { + Error::Http( + StatusCode::BAD_REQUEST, + "invalid authorization header".to_string(), + ) + })?; + + let state = req.extensions().get::>().unwrap(); + + if token != state.config.client_token { + Err(Error::Http( + StatusCode::UNAUTHORIZED, + "invalid client secret".to_string(), + ))? + } + + Ok::<_, Error>(next.run(req).await) +} + #[derive(Debug, Deserialize)] struct AuthenticatedUserParams { github_user_id: Option, @@ -127,6 +167,87 @@ async fn trace_panic(panic: Json) -> Result<()> { Ok(()) } +/// IPSHeader is the first line of an .ips file (in JSON format) +/// https://developer.apple.com/documentation/xcode/interpreting-the-json-format-of-a-crash-report +#[derive(Debug, Serialize, Deserialize)] +struct IPSHeader { + timestamp: Option, + name: Option, + app_name: Option, + app_version: Option, + slice_uuid: Option, + build_version: Option, + platform: Option, + #[serde(rename = "bundleID")] + bundle_id: Option, + share_with_app_devs: Option, + is_first_party: Option, + bug_type: Option, + os_version: Option, + roots_installed: Option, + incident_id: Option, +} + +#[instrument(skip(content, app))] +async fn trace_crash(content: String, Extension(app): Extension>) -> Result<()> { + let Some(header) = content.split("\n").next() else { + return Err(Error::Http( + StatusCode::BAD_REQUEST, + "invalid .ips file".to_string(), + )); + }; + let header: IPSHeader = serde_json::from_slice(&header.as_bytes())?; + let text = content.as_str(); + + tracing::error!(app_version = %header.app_version.clone().unwrap_or_default(), + build_version = %header.build_version.unwrap_or_default(), + os_version = %header.os_version.unwrap_or_default(), + bundle_id = %header.bundle_id.clone().unwrap_or_default(), + text = %text, + "crash report"); + + async_maybe!({ + let api_key = app.config.slack_api_key.clone()?; + let channel = app.config.slack_panic_channel.clone()?; + + let mut body = form_data_builder::FormData::new(Vec::new()); + body.write_field("content", text).log_err()?; + body.write_field("channels", channel.as_str()).log_err()?; + body.write_field( + "filename", + format!("zed-crash-{}.ips", header.incident_id.unwrap_or_default()).as_str(), + ) + .log_err()?; + body.write_field( + "initial_comment", + format!( + "New crash in {} ({})", + header.bundle_id.unwrap_or_default(), + header.app_version.unwrap_or_default() + ) + .as_str(), + ) + .log_err()?; + let content_type = body.content_type_header(); + let body = AsyncBody::from(body.finish().log_err()?); + + let request = Request::post("https://slack.com/api/files.upload") + .header("Content-Type", content_type) + .header("Authorization", format!("Bearer {}", api_key)) + .body(body) + .log_err()?; + + let response = util::http::client().send(request).await.log_err()?; + if !response.status().is_success() { + tracing::error!(response = ?response, "failed to send crash report to slack"); + } + + Some(()) + }) + .await; + Ok(()) +} + async fn get_rpc_server_snapshot( Extension(rpc_server): Extension>, ) -> Result { diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index aba9bd75d1..bf4270512e 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -101,6 +101,9 @@ pub struct Config { pub rust_log: Option, pub log_json: Option, pub zed_environment: Arc, + pub slack_api_key: Option, + pub slack_panic_channel: Option, + pub client_token: String, } impl Config { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index cda0621cb3..d6ea536dd1 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -465,6 +465,7 @@ impl TestServer { database_url: "".into(), database_max_connections: 0, api_token: "".into(), + client_token: "".into(), invite_link_prefix: "".into(), live_kit_server: None, live_kit_key: None, @@ -472,6 +473,8 @@ impl TestServer { rust_log: None, log_json: None, zed_environment: "test".into(), + slack_api_key: None, + slack_panic_channel: None, }, }) } 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 821668001c..ce39549016 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}; @@ -227,14 +228,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() @@ -597,77 +598,158 @@ 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); - + let release_channel = cx.global::().clone(); 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, release_channel) + .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, + release_channel: ReleaseChannel, +) -> Result<()> { + if !telemetry_settings.diagnostics { + return Ok(()); + } + let last_uploaded = KEY_VALUE_STORE + .read_kvp(LAST_CRASH_UPLOADED)? + .unwrap_or("zed-2024-01-17-000000.ips".to_string()); // don't upload old crash reports from before we had this. + let mut uploaded = last_uploaded.clone(); + + let mut crash_report_url = + client::Client::get_collab_server_url(http.clone(), Some(release_channel)).await?; + crash_report_url.set_path("/crash"); + + 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.to_string()) + .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<()> { From 345b983c8edd18acc346845dcdd1266e144a05a9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Jan 2024 20:46:20 -0700 Subject: [PATCH 2/5] Improve panic logging Send along every symbol for inlined frames, and also include the address (and if we have it filename + lineno) --- crates/zed/src/main.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ce39549016..6924b955f8 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -542,7 +542,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. From ef6f39d09002afbea8b870ee5b237424d97e1131 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Jan 2024 22:53:53 -0700 Subject: [PATCH 3/5] Upload panics via zed.dev instead --- Cargo.lock | 11 --- Procfile | 1 + crates/client/src/client.rs | 10 -- crates/collab/.env.toml | 3 - crates/collab/Cargo.toml | 1 - crates/collab/k8s/collab.template.yml | 15 --- crates/collab/src/api.rs | 127 +------------------------ crates/collab/src/lib.rs | 3 - crates/collab/src/tests/test_server.rs | 3 - crates/zed/src/main.rs | 8 +- 10 files changed, 7 insertions(+), 175 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba85eb1486..4c3a4e8592 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1474,7 +1474,6 @@ dependencies = [ "env_logger", "envy", "file_finder", - "form-data-builder", "fs", "futures 0.3.28", "git", @@ -2691,16 +2690,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "form-data-builder" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ff8fb4527b05539a9f573ba2831a1127038a7b45eea385a338a63dc5ab6829" -dependencies = [ - "base64 0.13.1", - "rand 0.8.5", -] - [[package]] name = "form_urlencoded" version = "1.2.0" diff --git a/Procfile b/Procfile index 122ad5139f..3f42c3a967 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,4 @@ +web: cd ../zed.dev && PORT=3000 npm run dev collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve livekit: livekit-server --dev postgrest: postgrest crates/collab/admin_api.conf diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index da695f6dc4..1894023084 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -969,16 +969,6 @@ impl Client { Url::parse(&collab_url).context("invalid rpc url") } - // todo: this should probably be cached (And/or done better) - pub async fn get_collab_server_url( - http: Arc, - release_channel: Option, - ) -> Result { - let mut url = Self::get_rpc_url(http, release_channel).await?; - url.set_path(""); - Ok(url) - } - fn establish_websocket_connection( self: &Arc, credentials: &Credentials, diff --git a/crates/collab/.env.toml b/crates/collab/.env.toml index 46140ae9b3..01866012ea 100644 --- a/crates/collab/.env.toml +++ b/crates/collab/.env.toml @@ -2,14 +2,11 @@ DATABASE_URL = "postgres://postgres@localhost/zed" DATABASE_MAX_CONNECTIONS = 5 HTTP_PORT = 8080 API_TOKEN = "secret" -CLIENT_TOKEN = "618033988749894" INVITE_LINK_PREFIX = "http://localhost:3000/invites/" ZED_ENVIRONMENT = "development" LIVE_KIT_SERVER = "http://localhost:7880" LIVE_KIT_KEY = "devkey" LIVE_KIT_SECRET = "secret" -# SLACK_PANIC_CHANNEL = -# SLACK_API_KEY = # RUST_LOG=info # LOG_JSON=true diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index f703c88bc0..bc273cb12a 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -58,7 +58,6 @@ tracing = "0.1.34" tracing-log = "0.1.3" tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] } uuid.workspace = true -form-data-builder = "1.0.1" [dev-dependencies] audio = { path = "../audio" } diff --git a/crates/collab/k8s/collab.template.yml b/crates/collab/k8s/collab.template.yml index eb9f4d9172..120e5f592f 100644 --- a/crates/collab/k8s/collab.template.yml +++ b/crates/collab/k8s/collab.template.yml @@ -90,11 +90,6 @@ spec: secretKeyRef: name: api key: token - - name: CLIENT_TOKEN - valueFrom: - secretKeyRef: - name: api - key: client_token - name: LIVE_KIT_SERVER valueFrom: secretKeyRef: @@ -110,16 +105,6 @@ spec: secretKeyRef: name: livekit key: secret - - name: SLACK_PANIC_CHANNEL - valueFrom: - secretKeyRef: - name: slack - key: api_key - - name: SLACK_API_KEY - valueFrom: - secretKeyRef: - name: slack - key: panic_channel - name: INVITE_LINK_PREFIX value: ${INVITE_LINK_PREFIX} - name: RUST_BACKTRACE diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index daca13fa85..a28aeac9ab 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -18,28 +18,19 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use tower::ServiceBuilder; use tracing::instrument; -use util::{async_maybe, http::AsyncBody, ResultExt}; pub fn routes(rpc_server: Arc, state: Arc) -> Router { - let called_from_website = Router::new() + Router::new() .route("/user", get(get_authenticated_user)) .route("/users/:id/access_tokens", post(create_access_token)) .route("/panic", post(trace_panic)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .layer( ServiceBuilder::new() - .layer(Extension(state.clone())) + .layer(Extension(state)) .layer(Extension(rpc_server)) .layer(middleware::from_fn(validate_api_token)), - ); - - let called_from_client = Router::new().route("/crash", post(trace_crash)).layer( - ServiceBuilder::new() - .layer(Extension(state)) - .layer(middleware::from_fn(validate_client_secret)), - ); - - called_from_website.merge(called_from_client) + ) } pub async fn validate_api_token(req: Request, next: Next) -> impl IntoResponse { @@ -73,37 +64,6 @@ pub async fn validate_api_token(req: Request, next: Next) -> impl IntoR Ok::<_, Error>(next.run(req).await) } -pub async fn validate_client_secret(req: Request, next: Next) -> impl IntoResponse { - let token = req - .headers() - .get(http::header::AUTHORIZATION) - .and_then(|header| header.to_str().ok()) - .ok_or_else(|| { - Error::Http( - StatusCode::BAD_REQUEST, - "missing authorization header".to_string(), - ) - })? - .strip_prefix("token ") - .ok_or_else(|| { - Error::Http( - StatusCode::BAD_REQUEST, - "invalid authorization header".to_string(), - ) - })?; - - let state = req.extensions().get::>().unwrap(); - - if token != state.config.client_token { - Err(Error::Http( - StatusCode::UNAUTHORIZED, - "invalid client secret".to_string(), - ))? - } - - Ok::<_, Error>(next.run(req).await) -} - #[derive(Debug, Deserialize)] struct AuthenticatedUserParams { github_user_id: Option, @@ -167,87 +127,6 @@ async fn trace_panic(panic: Json) -> Result<()> { Ok(()) } -/// IPSHeader is the first line of an .ips file (in JSON format) -/// https://developer.apple.com/documentation/xcode/interpreting-the-json-format-of-a-crash-report -#[derive(Debug, Serialize, Deserialize)] -struct IPSHeader { - timestamp: Option, - name: Option, - app_name: Option, - app_version: Option, - slice_uuid: Option, - build_version: Option, - platform: Option, - #[serde(rename = "bundleID")] - bundle_id: Option, - share_with_app_devs: Option, - is_first_party: Option, - bug_type: Option, - os_version: Option, - roots_installed: Option, - incident_id: Option, -} - -#[instrument(skip(content, app))] -async fn trace_crash(content: String, Extension(app): Extension>) -> Result<()> { - let Some(header) = content.split("\n").next() else { - return Err(Error::Http( - StatusCode::BAD_REQUEST, - "invalid .ips file".to_string(), - )); - }; - let header: IPSHeader = serde_json::from_slice(&header.as_bytes())?; - let text = content.as_str(); - - tracing::error!(app_version = %header.app_version.clone().unwrap_or_default(), - build_version = %header.build_version.unwrap_or_default(), - os_version = %header.os_version.unwrap_or_default(), - bundle_id = %header.bundle_id.clone().unwrap_or_default(), - text = %text, - "crash report"); - - async_maybe!({ - let api_key = app.config.slack_api_key.clone()?; - let channel = app.config.slack_panic_channel.clone()?; - - let mut body = form_data_builder::FormData::new(Vec::new()); - body.write_field("content", text).log_err()?; - body.write_field("channels", channel.as_str()).log_err()?; - body.write_field( - "filename", - format!("zed-crash-{}.ips", header.incident_id.unwrap_or_default()).as_str(), - ) - .log_err()?; - body.write_field( - "initial_comment", - format!( - "New crash in {} ({})", - header.bundle_id.unwrap_or_default(), - header.app_version.unwrap_or_default() - ) - .as_str(), - ) - .log_err()?; - let content_type = body.content_type_header(); - let body = AsyncBody::from(body.finish().log_err()?); - - let request = Request::post("https://slack.com/api/files.upload") - .header("Content-Type", content_type) - .header("Authorization", format!("Bearer {}", api_key)) - .body(body) - .log_err()?; - - let response = util::http::client().send(request).await.log_err()?; - if !response.status().is_success() { - tracing::error!(response = ?response, "failed to send crash report to slack"); - } - - Some(()) - }) - .await; - Ok(()) -} - async fn get_rpc_server_snapshot( Extension(rpc_server): Extension>, ) -> Result { diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index bf4270512e..aba9bd75d1 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -101,9 +101,6 @@ pub struct Config { pub rust_log: Option, pub log_json: Option, pub zed_environment: Arc, - pub slack_api_key: Option, - pub slack_panic_channel: Option, - pub client_token: String, } impl Config { diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index d6ea536dd1..cda0621cb3 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -465,7 +465,6 @@ impl TestServer { database_url: "".into(), database_max_connections: 0, api_token: "".into(), - client_token: "".into(), invite_link_prefix: "".into(), live_kit_server: None, live_kit_key: None, @@ -473,8 +472,6 @@ impl TestServer { rust_log: None, log_json: None, zed_environment: "test".into(), - slack_api_key: None, - slack_panic_channel: None, }, }) } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 6924b955f8..f67ba6cddb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -710,12 +710,10 @@ async fn upload_previous_crashes( } let last_uploaded = KEY_VALUE_STORE .read_kvp(LAST_CRASH_UPLOADED)? - .unwrap_or("zed-2024-01-17-000000.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. let mut uploaded = last_uploaded.clone(); - let mut crash_report_url = - client::Client::get_collab_server_url(http.clone(), Some(release_channel)).await?; - crash_report_url.set_path("/crash"); + 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?; @@ -741,7 +739,7 @@ async fn upload_previous_crashes( .await .context("error reading crash file")?; - let request = Request::post(&crash_report_url.to_string()) + let request = Request::post(&crash_report_url) .redirect_policy(isahc::config::RedirectPolicy::Follow) .header("Content-Type", "text/plain") .header( From 4d9ff5c4ff3714fc4a8c0f86df7c57d8df799f01 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Jan 2024 23:02:36 -0700 Subject: [PATCH 4/5] Simplify service discovery in development --- crates/client/src/client.rs | 15 +++++---------- script/zed-local | 3 ++- 2 files changed, 7 insertions(+), 11 deletions(-) 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/script/zed-local b/script/zed-local index 090fbd5876..f9a07dda6f 100755 --- a/script/zed-local +++ b/script/zed-local @@ -111,7 +111,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, From f1df6318ba7fdba9a448aa18a8aa083e1d5bb4aa Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Jan 2024 09:06:13 -0700 Subject: [PATCH 5/5] Remove unused variable --- crates/zed/src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f67ba6cddb..92435f68d2 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -615,13 +615,12 @@ fn init_panic_hook(app: &App, installation_id: Option, session_id: Strin fn upload_panics_and_crashes(http: Arc, cx: &mut AppContext) { let telemetry_settings = *client::TelemetrySettings::get_global(cx); - let release_channel = cx.global::().clone(); cx.background_executor() .spawn(async move { upload_previous_panics(http.clone(), telemetry_settings) .await .log_err(); - upload_previous_crashes(http, telemetry_settings, release_channel) + upload_previous_crashes(http, telemetry_settings) .await .log_err() }) @@ -703,7 +702,6 @@ static LAST_CRASH_UPLOADED: &'static str = "LAST_CRASH_UPLOADED"; async fn upload_previous_crashes( http: Arc, telemetry_settings: client::TelemetrySettings, - release_channel: ReleaseChannel, ) -> Result<()> { if !telemetry_settings.diagnostics { return Ok(());