Merge pull request #960 from zed-industries/crash-reporting

Antonio Scandurra created

Implement panic reporting

Change summary

Cargo.lock                            |  13 --
crates/auto_update/src/auto_update.rs |   6 
crates/client/src/client.rs           |   2 
crates/collab/src/api.rs              |  14 ++
crates/gpui/src/platform.rs           |   7 +
crates/zed/Cargo.toml                 |   3 
crates/zed/src/main.rs                | 155 ++++++++++++++++++++++++++--
7 files changed, 171 insertions(+), 29 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2499,16 +2499,6 @@ dependencies = [
  "value-bag",
 ]
 
-[[package]]
-name = "log-panics"
-version = "2.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae0136257df209261daa18d6c16394757c63e032e27aafd8b07788b051082bef"
-dependencies = [
- "backtrace",
- "log",
-]
-
 [[package]]
 name = "loom"
 version = "0.4.0"
@@ -5859,8 +5849,10 @@ dependencies = [
  "async-recursion",
  "async-trait",
  "auto_update",
+ "backtrace",
  "breadcrumbs",
  "chat_panel",
+ "chrono",
  "cli",
  "client",
  "clock",
@@ -5889,7 +5881,6 @@ dependencies = [
  "lazy_static",
  "libc",
  "log",
- "log-panics",
  "lsp",
  "num_cpus",
  "outline",

crates/auto_update/src/auto_update.rs 🔗

@@ -1,6 +1,5 @@
 use anyhow::{anyhow, Context, Result};
-use client::http::HttpClient;
-
+use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
 use gpui::{
     actions,
     elements::{Empty, MouseEventHandler, Text},
@@ -16,7 +15,6 @@ use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
 use workspace::{ItemHandle, StatusItemView};
 
 const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
-const ACCESS_TOKEN: &'static str = "618033988749894";
 
 lazy_static! {
     pub static ref ZED_APP_VERSION: Option<AppVersion> = env::var("ZED_APP_VERSION")
@@ -135,7 +133,7 @@ impl AutoUpdater {
         });
         let mut response = client
             .get(
-                &format!("{server_url}/api/releases/latest?token={ACCESS_TOKEN}&asset=Zed.dmg"),
+                &format!("{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg"),
                 Default::default(),
                 true,
             )

crates/client/src/client.rs 🔗

@@ -50,6 +50,8 @@ lazy_static! {
         .and_then(|s| if s.is_empty() { None } else { Some(s) });
 }
 
+pub const ZED_SECRET_CLIENT_TOKEN: &'static str = "618033988749894";
+
 actions!(client, [Authenticate]);
 
 pub fn init(rpc: Arc<Client>, cx: &mut MutableAppContext) {

crates/collab/src/api.rs 🔗

@@ -16,6 +16,7 @@ use axum::{
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
 use tower::ServiceBuilder;
+use tracing::instrument;
 
 pub fn routes(state: Arc<AppState>) -> Router<Body> {
     Router::new()
@@ -25,6 +26,7 @@ pub fn routes(state: Arc<AppState>) -> Router<Body> {
             put(update_user).delete(destroy_user).get(get_user),
         )
         .route("/users/:id/access_tokens", post(create_access_token))
+        .route("/panic", post(trace_panic))
         .layer(
             ServiceBuilder::new()
                 .layer(Extension(state))
@@ -129,6 +131,18 @@ async fn get_user(
     Ok(Json(user))
 }
 
+#[derive(Debug, Deserialize)]
+struct Panic {
+    version: String,
+    text: String,
+}
+
+#[instrument(skip(panic))]
+async fn trace_panic(panic: Json<Panic>) -> Result<()> {
+    tracing::error!(version = %panic.version, text = %panic.text, "panic report");
+    Ok(())
+}
+
 #[derive(Deserialize)]
 struct CreateAccessTokenQueryParams {
     public_key: String,

crates/gpui/src/platform.rs 🔗

@@ -24,6 +24,7 @@ use postage::oneshot;
 use serde::Deserialize;
 use std::{
     any::Any,
+    fmt::{self, Display},
     path::{Path, PathBuf},
     rc::Rc,
     str::FromStr,
@@ -167,6 +168,12 @@ impl FromStr for AppVersion {
     }
 }
 
+impl Display for AppVersion {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
+    }
+}
+
 #[derive(Copy, Clone, Debug)]
 pub enum RasterizationOptions {
     Alpha,

crates/zed/Cargo.toml 🔗

@@ -53,6 +53,8 @@ anyhow = "1.0.38"
 async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
 async-recursion = "0.3"
 async-trait = "0.1"
+backtrace = "0.3"
+chrono = "0.4"
 ctor = "0.1.20"
 dirs = "3.0"
 easy-parallel = "3.1.0"
@@ -66,7 +68,6 @@ isahc = "1.7"
 lazy_static = "1.4.0"
 libc = "0.2"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
-log-panics = { version = "2.0", features = ["with-backtrace"] }
 num_cpus = "1.13.0"
 parking_lot = "0.11.1"
 postage = { version = "0.4.1", features = ["futures-traits"] }

crates/zed/src/main.rs 🔗

@@ -3,25 +3,41 @@
 
 use anyhow::{anyhow, Context, Result};
 use assets::Assets;
+use auto_update::ZED_APP_VERSION;
+use backtrace::Backtrace;
 use cli::{
     ipc::{self, IpcSender},
     CliRequest, CliResponse, IpcHandshake,
 };
-use client::{self, http, ChannelList, UserStore};
+use client::{
+    self,
+    http::{self, HttpClient},
+    ChannelList, UserStore, ZED_SECRET_CLIENT_TOKEN,
+};
 use fs::OpenOptions;
 use futures::{
     channel::{mpsc, oneshot},
     FutureExt, SinkExt, StreamExt,
 };
-use gpui::{App, AssetSource, AsyncAppContext, Task};
+use gpui::{executor::Background, App, AssetSource, AsyncAppContext, Task};
+use isahc::{config::Configurable, AsyncBody, Request};
 use log::LevelFilter;
 use parking_lot::Mutex;
 use project::Fs;
+use serde_json::json;
 use settings::{self, KeymapFileContent, Settings, SettingsFileContent};
 use smol::process::Command;
-use std::{env, fs, path::PathBuf, sync::Arc, thread, time::Duration};
+use std::{
+    env,
+    ffi::OsStr,
+    fs, panic,
+    path::{Path, PathBuf},
+    sync::Arc,
+    thread,
+    time::Duration,
+};
 use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
-use util::ResultExt;
+use util::{ResultExt, TryFutureExt};
 use workspace::{self, AppState, OpenNew, OpenPaths};
 use zed::{
     self, build_window_options, build_workspace,
@@ -31,9 +47,16 @@ use zed::{
 };
 
 fn main() {
-    init_logger();
+    let http = http::client();
+    let logs_dir_path = dirs::home_dir()
+        .expect("could not find home dir")
+        .join("Library/Logs/Zed");
+    fs::create_dir_all(&logs_dir_path).expect("could not create logs path");
+    init_logger(&logs_dir_path);
 
     let mut app = gpui::App::new(Assets).unwrap();
+    init_panic_hook(logs_dir_path, http.clone(), app.background());
+
     load_embedded_fonts(&app);
 
     let fs = Arc::new(RealFs);
@@ -107,7 +130,6 @@ fn main() {
     });
 
     app.run(move |cx| {
-        let http = http::client();
         let client = client::Client::new(http.clone());
         let mut languages = languages::build_language_registry(login_shell_env_loaded);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
@@ -219,16 +241,12 @@ fn main() {
     });
 }
 
-fn init_logger() {
+fn init_logger(logs_dir_path: &Path) {
     if stdout_is_a_pty() {
         env_logger::init();
     } else {
         let level = LevelFilter::Info;
-        let log_dir_path = dirs::home_dir()
-            .expect("could not locate home directory for logging")
-            .join("Library/Logs/");
-        let log_file_path = log_dir_path.join("Zed.log");
-        fs::create_dir_all(&log_dir_path).expect("could not create log directory");
+        let log_file_path = logs_dir_path.join("Zed.log");
         let log_file = OpenOptions::new()
             .create(true)
             .append(true)
@@ -236,10 +254,121 @@ fn init_logger() {
             .expect("could not open logfile");
         simplelog::WriteLogger::init(level, simplelog::Config::default(), log_file)
             .expect("could not initialize logger");
-        log_panics::init();
     }
 }
 
+fn init_panic_hook(logs_dir_path: PathBuf, http: Arc<dyn HttpClient>, background: Arc<Background>) {
+    background
+        .spawn({
+            let logs_dir_path = logs_dir_path.clone();
+
+            async move {
+                let panic_report_url = format!("{}/api/panic", &*client::ZED_SERVER_URL);
+                let mut children = smol::fs::read_dir(&logs_dir_path).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;
+                    };
+
+                    let mut components = filename.split('-');
+                    if components.next() != Some("zed") {
+                        continue;
+                    }
+                    let version = if let Some(version) = components.next() {
+                        version
+                    } else {
+                        continue;
+                    };
+
+                    let text = smol::fs::read_to_string(&child_path)
+                        .await
+                        .context("error reading panic file")?;
+                    let body = serde_json::to_string(&json!({
+                        "text": text,
+                        "version": version,
+                        "token": ZED_SECRET_CLIENT_TOKEN,
+                    }))
+                    .unwrap();
+                    let request = Request::builder()
+                        .uri(&panic_report_url)
+                        .method(http::Method::POST)
+                        .redirect_policy(isahc::config::RedirectPolicy::Follow)
+                        .header("Content-Type", "application/json")
+                        .body(AsyncBody::from(body))?;
+                    let response = http.send(request).await.context("error sending panic")?;
+                    if response.status().is_success() {
+                        fs::remove_file(child_path)
+                            .context("error removing panic after sending it successfully")
+                            .log_err();
+                    } else {
+                        return Err(anyhow!(
+                            "error uploading panic to server: {}",
+                            response.status()
+                        ));
+                    }
+                }
+                Ok::<_, anyhow::Error>(())
+            }
+            .log_err()
+        })
+        .detach();
+
+    let app_version = ZED_APP_VERSION.map_or("dev".to_string(), |v| v.to_string());
+    let is_pty = stdout_is_a_pty();
+    panic::set_hook(Box::new(move |info| {
+        let backtrace = Backtrace::new();
+
+        let thread = thread::current();
+        let thread = thread.name().unwrap_or("<unnamed>");
+
+        let payload = match info.payload().downcast_ref::<&'static str>() {
+            Some(s) => *s,
+            None => match info.payload().downcast_ref::<String>() {
+                Some(s) => &**s,
+                None => "Box<Any>",
+            },
+        };
+
+        let message = match info.location() {
+            Some(location) => {
+                format!(
+                    "thread '{}' panicked at '{}': {}:{}{:?}",
+                    thread,
+                    payload,
+                    location.file(),
+                    location.line(),
+                    backtrace
+                )
+            }
+            None => format!(
+                "thread '{}' panicked at '{}'{:?}",
+                thread, payload, backtrace
+            ),
+        };
+
+        let panic_filename = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
+        fs::write(
+            logs_dir_path.join(format!("zed-{}-{}.panic", app_version, panic_filename)),
+            &message,
+        )
+        .context("error writing panic to disk")
+        .log_err();
+
+        if is_pty {
+            eprintln!("{}", message);
+        } else {
+            log::error!(target: "panic", "{}", message);
+        }
+    }));
+}
+
 async fn load_login_shell_environment() -> Result<()> {
     let marker = "ZED_LOGIN_SHELL_START";
     let shell = env::var("SHELL").context(