zed: Add `--system-specs` arg (#27285)

Ben Kunkle created

Adds the `--system-specs` flag to the Zed binary, so that users who wish
to report issues can retrieve their system specs, even if Zed is failing
to launch

Still TODO:
- [x] Test and do best effort GPU info detection on Linux
- [ ] Modify GitHub issue templates to tell users that the flag is
available if they are unable to launch Zed

Release Notes:

- Added the `--system-specs` flag to the Zed binary (not the cli!), to
retrieve the system specs we ask for in GitHub issues without needing to
open Zed

Change summary

crates/cli/src/main.rs              | 30 +++++++++++++
crates/feedback/src/feedback.rs     |  2 
crates/feedback/src/system_specs.rs | 69 +++++++++++++++++++++++++++---
crates/zed/src/main.rs              | 15 ++++++
crates/zlog/src/zlog.rs             |  1 
5 files changed, 108 insertions(+), 9 deletions(-)

Detailed changes

crates/cli/src/main.rs 🔗

@@ -27,6 +27,7 @@ trait InstalledApp {
     fn zed_version_string(&self) -> String;
     fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
     fn run_foreground(&self, ipc_url: String) -> io::Result<ExitStatus>;
+    fn path(&self) -> PathBuf;
 }
 
 #[derive(Parser, Debug)]
@@ -73,6 +74,10 @@ struct Args {
     /// Run zed in dev-server mode
     #[arg(long)]
     dev_server_token: Option<String>,
+    /// Not supported in Zed CLI, only supported on Zed binary
+    /// Will attempt to give the correct command to run
+    #[arg(long)]
+    system_specs: bool,
     /// Uninstall Zed from user system
     #[cfg(all(
         any(target_os = "linux", target_os = "macos"),
@@ -140,6 +145,16 @@ fn main() -> Result<()> {
         return Ok(());
     }
 
+    if args.system_specs {
+        let path = app.path();
+        let msg = [
+            "The `--system-specs` argument is not supported in the Zed CLI, only on Zed binary.",
+            "To retrieve the system specs on the command line, run the following command:",
+            &format!("{} --system-specs", path.display()),
+        ];
+        return Err(anyhow::anyhow!(msg.join("\n")));
+    }
+
     #[cfg(all(
         any(target_os = "linux", target_os = "macos"),
         not(feature = "no-bundled-uninstall")
@@ -437,6 +452,10 @@ mod linux {
                 .arg(ipc_url)
                 .status()
         }
+
+        fn path(&self) -> PathBuf {
+            self.0.clone()
+        }
     }
 
     impl App {
@@ -674,6 +693,10 @@ mod windows {
                 .spawn()?
                 .wait()
         }
+
+        fn path(&self) -> PathBuf {
+            self.0.clone()
+        }
     }
 
     impl Detect {
@@ -876,6 +899,13 @@ mod mac_os {
 
             std::process::Command::new(path).arg(ipc_url).status()
         }
+
+        fn path(&self) -> PathBuf {
+            match self {
+                Bundle::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed").clone(),
+                Bundle::LocalPath { executable, .. } => executable.clone(),
+            }
+        }
     }
 
     impl Bundle {

crates/feedback/src/system_specs.rs 🔗

@@ -1,5 +1,5 @@
 use client::telemetry;
-use gpui::{App, AppContext as _, Task, Window};
+use gpui::{App, AppContext as _, SemanticVersion, Task, Window};
 use human_bytes::human_bytes;
 use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
 use serde::Serialize;
@@ -35,14 +35,12 @@ impl SystemSpecs {
             _ => None,
         };
 
-        let gpu_specs = if let Some(specs) = window.gpu_specs() {
-            Some(format!(
+        let gpu_specs = window.gpu_specs().map(|specs| {
+            format!(
                 "{} || {} || {}",
                 specs.device_name, specs.driver_name, specs.driver_info
-            ))
-        } else {
-            None
-        };
+            )
+        });
 
         cx.background_spawn(async move {
             let os_version = telemetry::os_version();
@@ -58,6 +56,37 @@ impl SystemSpecs {
             }
         })
     }
+
+    pub fn new_stateless(
+        app_version: SemanticVersion,
+        app_commit_sha: Option<AppCommitSha>,
+        release_channel: ReleaseChannel,
+    ) -> Self {
+        let os_name = telemetry::os_name();
+        let os_version = telemetry::os_version();
+        let system = System::new_with_specifics(
+            RefreshKind::new().with_memory(MemoryRefreshKind::everything()),
+        );
+        let memory = system.total_memory();
+        let architecture = env::consts::ARCH;
+        let commit_sha = match release_channel {
+            ReleaseChannel::Dev | ReleaseChannel::Nightly => {
+                app_commit_sha.map(|sha| sha.0.clone())
+            }
+            _ => None,
+        };
+
+        Self {
+            app_version: app_version.to_string(),
+            release_channel: release_channel.display_name(),
+            os_name,
+            os_version,
+            memory,
+            architecture,
+            commit_sha,
+            gpu_specs: try_determine_available_gpus(),
+        }
+    }
 }
 
 impl Display for SystemSpecs {
@@ -94,3 +123,29 @@ impl Display for SystemSpecs {
         write!(f, "{system_specs}")
     }
 }
+
+fn try_determine_available_gpus() -> Option<String> {
+    #[cfg(target_os = "linux")]
+    {
+        return std::process::Command::new("vulkaninfo")
+            .args(&["--summary"])
+            .output()
+            .ok()
+            .map(|output| {
+                [
+                    "<details><summary>`vulkaninfo --summary` output</summary>",
+                    "",
+                    "```",
+                    String::from_utf8_lossy(&output.stdout).as_ref(),
+                    "```",
+                    "</details>",
+                ]
+                .join("\n")
+            })
+            .or(Some("Failed to run `vulkaninfo --summary`".to_string()));
+    }
+    #[cfg(not(target_os = "linux"))]
+    {
+        return None;
+    }
+}

crates/zed/src/main.rs 🔗

@@ -215,6 +215,16 @@ fn main() {
         session_id.clone(),
     );
 
+    if args.system_specs {
+        let system_specs = feedback::system_specs::SystemSpecs::new_stateless(
+            app_version,
+            app_commit_sha.clone(),
+            *release_channel::RELEASE_CHANNEL,
+        );
+        println!("Zed System Specs (from CLI):\n{}", system_specs);
+        return;
+    }
+
     let (open_listener, mut open_rx) = OpenListener::new();
 
     let failed_single_instance_check = if *db::ZED_STATELESS
@@ -953,6 +963,11 @@ struct Args {
     #[arg(long)]
     dev_server_token: Option<String>,
 
+    /// Prints system specs. Useful for submitting issues on GitHub when encountering a bug
+    /// that prevents Zed from starting, so you can't run `zed: copy system specs to clipboard`
+    #[arg(long)]
+    system_specs: bool,
+
     /// Run zed in the foreground, only used on Windows, to match the behavior of the behavior on macOS.
     #[arg(long)]
     #[cfg(target_os = "windows")]

crates/zlog/src/zlog.rs 🔗

@@ -400,7 +400,6 @@ pub mod scope_map {
             *map = Some(map_new.clone());
             // note: hash update done here to ensure consistency with scope map
         }
-        eprintln!("Updated log scope settings :: map = {:?}", map_new);
     }
 }