1use client::telemetry;
2pub use gpui::GpuSpecs;
3use gpui::{App, AppContext as _, Task, Window, actions};
4use human_bytes::human_bytes;
5use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
6use semver::Version;
7use serde::Serialize;
8use std::{env, fmt::Display};
9use sysinfo::{MemoryRefreshKind, RefreshKind, System};
10
11actions!(
12 zed,
13 [
14 /// Copies system specifications to the clipboard for bug reports.
15 CopySystemSpecsIntoClipboard,
16 ]
17);
18
19#[derive(Clone, Debug, Serialize)]
20pub struct SystemSpecs {
21 app_version: String,
22 release_channel: &'static str,
23 os_name: String,
24 os_version: String,
25 memory: u64,
26 architecture: &'static str,
27 commit_sha: Option<String>,
28 bundle_type: Option<String>,
29 gpu_specs: Option<String>,
30}
31
32impl SystemSpecs {
33 pub fn new(window: &mut Window, cx: &mut App) -> Task<Self> {
34 let app_version = AppVersion::global(cx).to_string();
35 let release_channel = ReleaseChannel::global(cx);
36 let os_name = telemetry::os_name();
37 let system = System::new_with_specifics(
38 RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()),
39 );
40 let memory = system.total_memory();
41 let architecture = env::consts::ARCH;
42 let commit_sha = match release_channel {
43 ReleaseChannel::Dev | ReleaseChannel::Nightly => {
44 AppCommitSha::try_global(cx).map(|sha| sha.full())
45 }
46 _ => None,
47 };
48 let bundle_type = bundle_type();
49
50 let gpu_specs = window.gpu_specs().map(|specs| {
51 format!(
52 "{} || {} || {}",
53 specs.device_name, specs.driver_name, specs.driver_info
54 )
55 });
56
57 cx.background_spawn(async move {
58 let os_version = telemetry::os_version();
59 SystemSpecs {
60 app_version,
61 release_channel: release_channel.display_name(),
62 bundle_type,
63 os_name,
64 os_version,
65 memory,
66 architecture,
67 commit_sha,
68 gpu_specs,
69 }
70 })
71 }
72
73 pub fn new_stateless(
74 app_version: Version,
75 app_commit_sha: Option<AppCommitSha>,
76 release_channel: ReleaseChannel,
77 ) -> Self {
78 let os_name = telemetry::os_name();
79 let os_version = telemetry::os_version();
80 let system = System::new_with_specifics(
81 RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()),
82 );
83 let memory = system.total_memory();
84 let architecture = env::consts::ARCH;
85 let commit_sha = match release_channel {
86 ReleaseChannel::Dev | ReleaseChannel::Nightly => app_commit_sha.map(|sha| sha.full()),
87 _ => None,
88 };
89 let bundle_type = bundle_type();
90
91 Self {
92 app_version: app_version.to_string(),
93 release_channel: release_channel.display_name(),
94 os_name,
95 os_version,
96 memory,
97 architecture,
98 commit_sha,
99 bundle_type,
100 gpu_specs: try_determine_available_gpus(),
101 }
102 }
103}
104
105impl Display for SystemSpecs {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 let os_information = format!("OS: {} {}", self.os_name, self.os_version);
108 let app_version_information = format!(
109 "Zed: v{} ({}) {}{}",
110 self.app_version,
111 match &self.commit_sha {
112 Some(commit_sha) => format!("{} {}", self.release_channel, commit_sha),
113 None => self.release_channel.to_string(),
114 },
115 if let Some(bundle_type) = &self.bundle_type {
116 format!("({bundle_type})")
117 } else {
118 "".to_string()
119 },
120 if cfg!(debug_assertions) {
121 "(Taylor's Version)"
122 } else {
123 ""
124 },
125 );
126 let system_specs = [
127 app_version_information,
128 os_information,
129 format!("Memory: {}", human_bytes(self.memory as f64)),
130 format!("Architecture: {}", self.architecture),
131 ]
132 .into_iter()
133 .chain(
134 self.gpu_specs
135 .as_ref()
136 .map(|specs| format!("GPU: {}", specs)),
137 )
138 .collect::<Vec<String>>()
139 .join("\n");
140
141 write!(f, "{system_specs}")
142 }
143}
144
145fn try_determine_available_gpus() -> Option<String> {
146 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
147 {
148 #[allow(
149 clippy::disallowed_methods,
150 reason = "we are not running in an executor"
151 )]
152 std::process::Command::new("vulkaninfo")
153 .args(&["--summary"])
154 .output()
155 .ok()
156 .map(|output| {
157 [
158 "<details><summary>`vulkaninfo --summary` output</summary>",
159 "",
160 "```",
161 String::from_utf8_lossy(&output.stdout).as_ref(),
162 "```",
163 "</details>",
164 ]
165 .join("\n")
166 })
167 .or(Some("Failed to run `vulkaninfo --summary`".to_string()))
168 }
169 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
170 {
171 None
172 }
173}
174
175#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)]
176pub struct GpuInfo {
177 pub device_name: Option<String>,
178 pub device_pci_id: u16,
179 pub vendor_name: Option<String>,
180 pub vendor_pci_id: u16,
181 pub driver_version: Option<String>,
182 pub driver_name: Option<String>,
183}
184
185#[cfg(any(target_os = "linux", target_os = "freebsd"))]
186pub fn read_gpu_info_from_sys_class_drm() -> anyhow::Result<Vec<GpuInfo>> {
187 use anyhow::Context as _;
188 use pciid_parser;
189 let dir_iter = std::fs::read_dir("/sys/class/drm").context("Failed to read /sys/class/drm")?;
190 let mut pci_addresses = vec![];
191 let mut gpus = Vec::<GpuInfo>::new();
192 let pci_db = pciid_parser::Database::read().ok();
193 for entry in dir_iter {
194 let Ok(entry) = entry else {
195 continue;
196 };
197
198 let device_path = entry.path().join("device");
199 let Some(pci_address) = device_path.read_link().ok().and_then(|pci_address| {
200 pci_address
201 .file_name()
202 .and_then(std::ffi::OsStr::to_str)
203 .map(str::trim)
204 .map(str::to_string)
205 }) else {
206 continue;
207 };
208 let Ok(device_pci_id) = read_pci_id_from_path(device_path.join("device")) else {
209 continue;
210 };
211 let Ok(vendor_pci_id) = read_pci_id_from_path(device_path.join("vendor")) else {
212 continue;
213 };
214 let driver_name = std::fs::read_link(device_path.join("driver"))
215 .ok()
216 .and_then(|driver_link| {
217 driver_link
218 .file_name()
219 .and_then(std::ffi::OsStr::to_str)
220 .map(str::trim)
221 .map(str::to_string)
222 });
223 let driver_version = driver_name
224 .as_ref()
225 .and_then(|driver_name| {
226 std::fs::read_to_string(format!("/sys/module/{driver_name}/version")).ok()
227 })
228 .as_deref()
229 .map(str::trim)
230 .map(str::to_string);
231
232 let already_found = gpus
233 .iter()
234 .zip(&pci_addresses)
235 .any(|(gpu, gpu_pci_address)| {
236 gpu_pci_address == &pci_address
237 && gpu.driver_version == driver_version
238 && gpu.driver_name == driver_name
239 });
240
241 if already_found {
242 continue;
243 }
244
245 let vendor = pci_db
246 .as_ref()
247 .and_then(|db| db.vendors.get(&vendor_pci_id));
248 let vendor_name = vendor.map(|vendor| vendor.name.clone());
249 let device_name = vendor
250 .and_then(|vendor| vendor.devices.get(&device_pci_id))
251 .map(|device| device.name.clone());
252
253 gpus.push(GpuInfo {
254 device_name,
255 device_pci_id,
256 vendor_name,
257 vendor_pci_id,
258 driver_version,
259 driver_name,
260 });
261 pci_addresses.push(pci_address);
262 }
263
264 Ok(gpus)
265}
266
267#[cfg(any(target_os = "linux", target_os = "freebsd"))]
268fn read_pci_id_from_path(path: impl AsRef<std::path::Path>) -> anyhow::Result<u16> {
269 use anyhow::Context as _;
270 let id = std::fs::read_to_string(path)?;
271 let id = id
272 .trim()
273 .strip_prefix("0x")
274 .context("Not a device ID")
275 .context(id.clone())?;
276 anyhow::ensure!(
277 id.len() == 4,
278 "Not a device id, expected 4 digits, found {}",
279 id.len()
280 );
281 u16::from_str_radix(id, 16).context("Failed to parse device ID")
282}
283
284/// Returns value of `ZED_BUNDLE_TYPE` set at compiletime or else at runtime.
285///
286/// The compiletime value is used by flatpak since it doesn't seem to have a way to provide a
287/// runtime environment variable.
288///
289/// The runtime value is used by snap since the Zed snaps use release binaries directly, and so
290/// cannot have this baked in.
291fn bundle_type() -> Option<String> {
292 option_env!("ZED_BUNDLE_TYPE")
293 .map(|bundle_type| bundle_type.to_string())
294 .or_else(|| env::var("ZED_BUNDLE_TYPE").ok())
295}