Detailed changes
@@ -4050,6 +4050,7 @@ dependencies = [
name = "crashes"
version = "0.1.0"
dependencies = [
+ "bincode",
"crash-handler",
"log",
"mach2 0.5.0",
@@ -4059,6 +4060,7 @@ dependencies = [
"serde",
"serde_json",
"smol",
+ "system_specs",
"workspace-hack",
]
@@ -5738,14 +5740,10 @@ dependencies = [
name = "feedback"
version = "0.1.0"
dependencies = [
- "client",
"editor",
"gpui",
- "human_bytes",
"menu",
- "release_channel",
- "serde",
- "sysinfo",
+ "system_specs",
"ui",
"urlencoding",
"util",
@@ -11634,6 +11632,12 @@ dependencies = [
"hmac",
]
+[[package]]
+name = "pciid-parser"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61"
+
[[package]]
name = "pem"
version = "3.0.5"
@@ -16154,6 +16158,21 @@ dependencies = [
"winx",
]
+[[package]]
+name = "system_specs"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client",
+ "gpui",
+ "human_bytes",
+ "pciid-parser",
+ "release_channel",
+ "serde",
+ "sysinfo",
+ "workspace-hack",
+]
+
[[package]]
name = "tab_switcher"
version = "0.1.0"
@@ -20413,6 +20432,7 @@ dependencies = [
"auto_update",
"auto_update_ui",
"backtrace",
+ "bincode",
"breadcrumbs",
"call",
"channel",
@@ -20511,6 +20531,7 @@ dependencies = [
"supermaven",
"svg_preview",
"sysinfo",
+ "system_specs",
"tab_switcher",
"task",
"tasks_ui",
@@ -155,6 +155,7 @@ members = [
"crates/streaming_diff",
"crates/sum_tree",
"crates/supermaven",
+ "crates/system_specs",
"crates/supermaven_api",
"crates/svg_preview",
"crates/tab_switcher",
@@ -381,6 +382,7 @@ streaming_diff = { path = "crates/streaming_diff" }
sum_tree = { path = "crates/sum_tree" }
supermaven = { path = "crates/supermaven" }
supermaven_api = { path = "crates/supermaven_api" }
+system_specs = { path = "crates/system_specs" }
tab_switcher = { path = "crates/tab_switcher" }
task = { path = "crates/task" }
tasks_ui = { path = "crates/tasks_ui" }
@@ -450,6 +452,7 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [
aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] }
aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
base64 = "0.22"
+bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
@@ -493,6 +496,7 @@ handlebars = "4.3"
heck = "0.5"
heed = { version = "0.21.0", features = ["read-txn-no-tls"] }
hex = "0.4.3"
+human_bytes = "0.4.1"
html5ever = "0.27.0"
http = "1.1"
http-body = "1.0"
@@ -532,6 +536,7 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] }
parking_lot = "0.12.1"
partial-json-fixer = "0.5.3"
parse_int = "0.9"
+pciid-parser = "0.8.0"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" }
@@ -76,7 +76,7 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| {
pub static MINIDUMP_ENDPOINT: LazyLock<Option<String>> = LazyLock::new(|| {
option_env!("ZED_MINIDUMP_ENDPOINT")
- .map(|s| s.to_owned())
+ .map(str::to_string)
.or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok())
});
@@ -6,6 +6,7 @@ edition.workspace = true
license = "GPL-3.0-or-later"
[dependencies]
+bincode.workspace = true
crash-handler.workspace = true
log.workspace = true
minidumper.workspace = true
@@ -14,6 +15,7 @@ release_channel.workspace = true
smol.workspace = true
serde.workspace = true
serde_json.workspace = true
+system_specs.workspace = true
workspace-hack.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
@@ -127,6 +127,7 @@ unsafe fn suspend_all_other_threads() {
pub struct CrashServer {
initialization_params: OnceLock<InitCrashHandler>,
panic_info: OnceLock<CrashPanic>,
+ active_gpu: OnceLock<system_specs::GpuSpecs>,
has_connection: Arc<AtomicBool>,
}
@@ -135,6 +136,8 @@ pub struct CrashInfo {
pub init: InitCrashHandler,
pub panic: Option<CrashPanic>,
pub minidump_error: Option<String>,
+ pub gpus: Vec<system_specs::GpuInfo>,
+ pub active_gpu: Option<system_specs::GpuSpecs>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
@@ -143,7 +146,6 @@ pub struct InitCrashHandler {
pub zed_version: String,
pub release_channel: String,
pub commit_sha: String,
- // pub gpu: String,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
@@ -178,6 +180,18 @@ impl minidumper::ServerHandler for CrashServer {
Err(e) => Some(format!("{e:?}")),
};
+ #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
+ let gpus = vec![];
+
+ #[cfg(any(target_os = "linux", target_os = "freebsd"))]
+ let gpus = match system_specs::read_gpu_info_from_sys_class_drm() {
+ Ok(gpus) => gpus,
+ Err(err) => {
+ log::warn!("Failed to collect GPU information for crash report: {err}");
+ vec![]
+ }
+ };
+
let crash_info = CrashInfo {
init: self
.initialization_params
@@ -186,6 +200,8 @@ impl minidumper::ServerHandler for CrashServer {
.clone(),
panic: self.panic_info.get().cloned(),
minidump_error,
+ active_gpu: self.active_gpu.get().cloned(),
+ gpus,
};
let crash_data_path = paths::logs_dir()
@@ -211,6 +227,13 @@ impl minidumper::ServerHandler for CrashServer {
serde_json::from_slice::<CrashPanic>(&buffer).expect("invalid panic data");
self.panic_info.set(panic_data).expect("already panicked");
}
+ 3 => {
+ let gpu_specs: system_specs::GpuSpecs =
+ bincode::deserialize(&buffer).expect("gpu specs");
+ self.active_gpu
+ .set(gpu_specs)
+ .expect("already set active gpu");
+ }
_ => {
panic!("invalid message kind");
}
@@ -287,6 +310,7 @@ pub fn crash_server(socket: &Path) {
initialization_params: OnceLock::new(),
panic_info: OnceLock::new(),
has_connection,
+ active_gpu: OnceLock::new(),
}),
&shutdown,
Some(CRASH_HANDLER_PING_TIMEOUT),
@@ -15,13 +15,9 @@ path = "src/feedback.rs"
test-support = []
[dependencies]
-client.workspace = true
gpui.workspace = true
-human_bytes = "0.4.1"
menu.workspace = true
-release_channel.workspace = true
-serde.workspace = true
-sysinfo.workspace = true
+system_specs.workspace = true
ui.workspace = true
urlencoding.workspace = true
util.workspace = true
@@ -1,18 +1,14 @@
use gpui::{App, ClipboardItem, PromptLevel, actions};
-use system_specs::SystemSpecs;
+use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::feedback::FileBugReport;
pub mod feedback_modal;
-pub mod system_specs;
-
actions!(
zed,
[
- /// Copies system specifications to the clipboard for bug reports.
- CopySystemSpecsIntoClipboard,
/// Opens email client to send feedback to Zed support.
EmailZed,
/// Opens the Zed repository on GitHub.
@@ -352,7 +352,7 @@ impl<T> Flatten<T> for Result<T> {
}
/// Information about the GPU GPUI is running on.
-#[derive(Default, Debug)]
+#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)]
pub struct GpuSpecs {
/// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU.
pub is_software_emulated: bool,
@@ -0,0 +1,28 @@
+[package]
+name = "system_specs"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/system_specs.rs"
+
+[features]
+default = []
+
+[dependencies]
+anyhow.workspace = true
+client.workspace = true
+gpui.workspace = true
+human_bytes.workspace = true
+release_channel.workspace = true
+serde.workspace = true
+sysinfo.workspace = true
+workspace-hack.workspace = true
+
+[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
+pciid-parser.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -1,11 +1,22 @@
+//! # system_specs
+
use client::telemetry;
-use gpui::{App, AppContext as _, SemanticVersion, Task, Window};
+pub use gpui::GpuSpecs;
+use gpui::{App, AppContext as _, SemanticVersion, Task, Window, actions};
use human_bytes::human_bytes;
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use serde::Serialize;
use std::{env, fmt::Display};
use sysinfo::{MemoryRefreshKind, RefreshKind, System};
+actions!(
+ zed,
+ [
+ /// Copies system specifications to the clipboard for bug reports.
+ CopySystemSpecsIntoClipboard,
+ ]
+);
+
#[derive(Clone, Debug, Serialize)]
pub struct SystemSpecs {
app_version: String,
@@ -158,6 +169,115 @@ fn try_determine_available_gpus() -> Option<String> {
}
}
+#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)]
+pub struct GpuInfo {
+ pub device_name: Option<String>,
+ pub device_pci_id: u16,
+ pub vendor_name: Option<String>,
+ pub vendor_pci_id: u16,
+ pub driver_version: Option<String>,
+ pub driver_name: Option<String>,
+}
+
+#[cfg(any(target_os = "linux", target_os = "freebsd"))]
+pub fn read_gpu_info_from_sys_class_drm() -> anyhow::Result<Vec<GpuInfo>> {
+ use anyhow::Context as _;
+ use pciid_parser;
+ let dir_iter = std::fs::read_dir("/sys/class/drm").context("Failed to read /sys/class/drm")?;
+ let mut pci_addresses = vec![];
+ let mut gpus = Vec::<GpuInfo>::new();
+ let pci_db = pciid_parser::Database::read().ok();
+ for entry in dir_iter {
+ let Ok(entry) = entry else {
+ continue;
+ };
+
+ let device_path = entry.path().join("device");
+ let Some(pci_address) = device_path.read_link().ok().and_then(|pci_address| {
+ pci_address
+ .file_name()
+ .and_then(std::ffi::OsStr::to_str)
+ .map(str::trim)
+ .map(str::to_string)
+ }) else {
+ continue;
+ };
+ let Ok(device_pci_id) = read_pci_id_from_path(device_path.join("device")) else {
+ continue;
+ };
+ let Ok(vendor_pci_id) = read_pci_id_from_path(device_path.join("vendor")) else {
+ continue;
+ };
+ let driver_name = std::fs::read_link(device_path.join("driver"))
+ .ok()
+ .and_then(|driver_link| {
+ driver_link
+ .file_name()
+ .and_then(std::ffi::OsStr::to_str)
+ .map(str::trim)
+ .map(str::to_string)
+ });
+ let driver_version = driver_name
+ .as_ref()
+ .and_then(|driver_name| {
+ std::fs::read_to_string(format!("/sys/module/{driver_name}/version")).ok()
+ })
+ .as_deref()
+ .map(str::trim)
+ .map(str::to_string);
+
+ let already_found = gpus
+ .iter()
+ .zip(&pci_addresses)
+ .any(|(gpu, gpu_pci_address)| {
+ gpu_pci_address == &pci_address
+ && gpu.driver_version == driver_version
+ && gpu.driver_name == driver_name
+ });
+
+ if already_found {
+ continue;
+ }
+
+ let vendor = pci_db
+ .as_ref()
+ .and_then(|db| db.vendors.get(&vendor_pci_id));
+ let vendor_name = vendor.map(|vendor| vendor.name.clone());
+ let device_name = vendor
+ .and_then(|vendor| vendor.devices.get(&device_pci_id))
+ .map(|device| device.name.clone());
+
+ gpus.push(GpuInfo {
+ device_name,
+ device_pci_id,
+ vendor_name,
+ vendor_pci_id,
+ driver_version,
+ driver_name,
+ });
+ pci_addresses.push(pci_address);
+ }
+
+ Ok(gpus)
+}
+
+#[cfg(any(target_os = "linux", target_os = "freebsd"))]
+fn read_pci_id_from_path(path: impl AsRef<std::path::Path>) -> anyhow::Result<u16> {
+ use anyhow::Context as _;
+ let id = std::fs::read_to_string(path)?;
+ let id = id
+ .trim()
+ .strip_prefix("0x")
+ .context("Not a device ID")
+ .context(id.clone())?;
+ anyhow::ensure!(
+ id.len() == 4,
+ "Not a device id, expected 4 digits, found {}",
+ id.len()
+ );
+ u16::from_str_radix(id, 16).context("Failed to parse device ID")
+}
+
/// Returns value of `ZED_BUNDLE_TYPE` set at compiletime or else at runtime.
///
/// The compiletime value is used by flatpak since it doesn't seem to have a way to provide a
@@ -29,7 +29,7 @@ test-support = [
any_vec.workspace = true
anyhow.workspace = true
async-recursion.workspace = true
-bincode = "1.2.1"
+bincode.workspace = true
call.workspace = true
client.workspace = true
clock.workspace = true
@@ -33,6 +33,7 @@ audio.workspace = true
auto_update.workspace = true
auto_update_ui.workspace = true
backtrace = "0.3"
+bincode.workspace = true
breadcrumbs.workspace = true
call.workspace = true
channel.workspace = true
@@ -60,6 +61,7 @@ extensions_ui.workspace = true
feature_flags.workspace = true
feedback.workspace = true
file_finder.workspace = true
+system_specs.workspace = true
fs.workspace = true
futures.workspace = true
git.workspace = true
@@ -16,7 +16,7 @@ use extension_host::ExtensionStore;
use fs::{Fs, RealFs};
use futures::{StreamExt, channel::oneshot, future};
use git::GitHostingProviderRegistry;
-use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _};
+use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, UpdateGlobal as _};
use gpui_tokio::Tokio;
use http_client::{Url, read_proxy_from_env};
@@ -240,7 +240,7 @@ pub fn main() {
option_env!("ZED_COMMIT_SHA").map(|commit_sha| AppCommitSha::new(commit_sha.to_string()));
if args.system_specs {
- let system_specs = feedback::system_specs::SystemSpecs::new_stateless(
+ let system_specs = system_specs::SystemSpecs::new_stateless(
app_version,
app_commit_sha,
*release_channel::RELEASE_CHANNEL,
@@ -89,7 +89,9 @@ pub fn init_panic_hook(
},
backtrace,
);
- std::process::exit(-1);
+ if MINIDUMP_ENDPOINT.is_none() {
+ std::process::exit(-1);
+ }
}
let main_module_base_address = get_main_module_base_address();
@@ -148,7 +150,9 @@ pub fn init_panic_hook(
}
zlog::flush();
- if !is_pty && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() {
+ if (!is_pty || MINIDUMP_ENDPOINT.is_some())
+ && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err()
+ {
let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string();
let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic"));
let panic_file = fs::OpenOptions::new()
@@ -614,10 +618,9 @@ async fn upload_minidump(
let mut panic_message = "".to_owned();
if let Some(panic_info) = metadata.panic.as_ref() {
panic_message = panic_info.message.clone();
- form = form.text("sentry[logentry][formatted]", panic_info.message.clone());
- form = form.text("span", panic_info.span.clone());
- // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu
- // name, screen resolution, available ram, device model, etc
+ form = form
+ .text("sentry[logentry][formatted]", panic_info.message.clone())
+ .text("span", panic_info.span.clone());
}
if let Some(minidump_error) = metadata.minidump_error.clone() {
form = form.text("minidump_error", minidump_error);
@@ -633,6 +636,63 @@ async fn upload_minidump(
commit_sha = metadata.init.commit_sha.clone(),
);
+ let gpu_count = metadata.gpus.len();
+ for (index, gpu) in metadata.gpus.iter().cloned().enumerate() {
+ let system_specs::GpuInfo {
+ device_name,
+ device_pci_id,
+ vendor_name,
+ vendor_pci_id,
+ driver_version,
+ driver_name,
+ } = gpu;
+ let num = if gpu_count == 1 && metadata.active_gpu.is_none() {
+ String::new()
+ } else {
+ index.to_string()
+ };
+ let name = format!("gpu{num}");
+ let root = format!("sentry[contexts][{name}]");
+ form = form
+ .text(
+ format!("{root}[Description]"),
+ "A GPU found on the users system. May or may not be the GPU Zed is running on",
+ )
+ .text(format!("{root}[type]"), "gpu")
+ .text(format!("{root}[name]"), device_name.unwrap_or(name))
+ .text(format!("{root}[id]"), format!("{:#06x}", device_pci_id))
+ .text(
+ format!("{root}[vendor_id]"),
+ format!("{:#06x}", vendor_pci_id),
+ )
+ .text_if_some(format!("{root}[vendor_name]"), vendor_name)
+ .text_if_some(format!("{root}[driver_version]"), driver_version)
+ .text_if_some(format!("{root}[driver_name]"), driver_name);
+ }
+ if let Some(active_gpu) = metadata.active_gpu.clone() {
+ form = form
+ .text(
+ "sentry[contexts][Active_GPU][Description]",
+ "The GPU Zed is running on",
+ )
+ .text("sentry[contexts][Active_GPU][type]", "gpu")
+ .text("sentry[contexts][Active_GPU][name]", active_gpu.device_name)
+ .text(
+ "sentry[contexts][Active_GPU][driver_version]",
+ active_gpu.driver_info,
+ )
+ .text(
+ "sentry[contexts][Active_GPU][driver_name]",
+ active_gpu.driver_name,
+ )
+ .text(
+ "sentry[contexts][Active_GPU][is_software_emulated]",
+ active_gpu.is_software_emulated.to_string(),
+ );
+ }
+
+ // TODO: feature-flag-context, and more of device-context like screen resolution, available ram, device model, etc
+
let mut response_text = String::new();
let mut response = http.send_multipart_form(endpoint, form).await?;
response
@@ -646,6 +706,27 @@ async fn upload_minidump(
Ok(())
}
+trait FormExt {
+ fn text_if_some(
+ self,
+ label: impl Into<std::borrow::Cow<'static, str>>,
+ value: Option<impl Into<std::borrow::Cow<'static, str>>>,
+ ) -> Self;
+}
+
+impl FormExt for Form {
+ fn text_if_some(
+ self,
+ label: impl Into<std::borrow::Cow<'static, str>>,
+ value: Option<impl Into<std::borrow::Cow<'static, str>>>,
+ ) -> Self {
+ match value {
+ Some(value) => self.text(label.into(), value.into()),
+ None => self,
+ }
+ }
+}
+
async fn upload_panic(
http: &Arc<HttpClientWithUrl>,
panic_report_url: &Url,
@@ -344,7 +344,17 @@ pub fn initialize_workspace(
if let Some(specs) = window.gpu_specs() {
log::info!("Using GPU: {:?}", specs);
- show_software_emulation_warning_if_needed(specs, window, cx);
+ show_software_emulation_warning_if_needed(specs.clone(), window, cx);
+ if let Some((crash_server, message)) = crashes::CRASH_HANDLER
+ .get()
+ .zip(bincode::serialize(&specs).ok())
+ && let Err(err) = crash_server.send_message(3, message)
+ {
+ log::warn!(
+ "Failed to store active gpu info for crash reporting: {}",
+ err
+ );
+ }
}
let edit_prediction_menu_handle = PopoverMenuHandle::default();