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