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::new().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::new().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 std::process::Command::new("vulkaninfo")
150 .args(&["--summary"])
151 .output()
152 .ok()
153 .map(|output| {
154 [
155 "<details><summary>`vulkaninfo --summary` output</summary>",
156 "",
157 "```",
158 String::from_utf8_lossy(&output.stdout).as_ref(),
159 "```",
160 "</details>",
161 ]
162 .join("\n")
163 })
164 .or(Some("Failed to run `vulkaninfo --summary`".to_string()))
165 }
166 #[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
167 {
168 None
169 }
170}
171
172#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)]
173pub struct GpuInfo {
174 pub device_name: Option<String>,
175 pub device_pci_id: u16,
176 pub vendor_name: Option<String>,
177 pub vendor_pci_id: u16,
178 pub driver_version: Option<String>,
179 pub driver_name: Option<String>,
180}
181
182#[cfg(any(target_os = "linux", target_os = "freebsd"))]
183pub fn read_gpu_info_from_sys_class_drm() -> anyhow::Result<Vec<GpuInfo>> {
184 use anyhow::Context as _;
185 use pciid_parser;
186 let dir_iter = std::fs::read_dir("/sys/class/drm").context("Failed to read /sys/class/drm")?;
187 let mut pci_addresses = vec![];
188 let mut gpus = Vec::<GpuInfo>::new();
189 let pci_db = pciid_parser::Database::read().ok();
190 for entry in dir_iter {
191 let Ok(entry) = entry else {
192 continue;
193 };
194
195 let device_path = entry.path().join("device");
196 let Some(pci_address) = device_path.read_link().ok().and_then(|pci_address| {
197 pci_address
198 .file_name()
199 .and_then(std::ffi::OsStr::to_str)
200 .map(str::trim)
201 .map(str::to_string)
202 }) else {
203 continue;
204 };
205 let Ok(device_pci_id) = read_pci_id_from_path(device_path.join("device")) else {
206 continue;
207 };
208 let Ok(vendor_pci_id) = read_pci_id_from_path(device_path.join("vendor")) else {
209 continue;
210 };
211 let driver_name = std::fs::read_link(device_path.join("driver"))
212 .ok()
213 .and_then(|driver_link| {
214 driver_link
215 .file_name()
216 .and_then(std::ffi::OsStr::to_str)
217 .map(str::trim)
218 .map(str::to_string)
219 });
220 let driver_version = driver_name
221 .as_ref()
222 .and_then(|driver_name| {
223 std::fs::read_to_string(format!("/sys/module/{driver_name}/version")).ok()
224 })
225 .as_deref()
226 .map(str::trim)
227 .map(str::to_string);
228
229 let already_found = gpus
230 .iter()
231 .zip(&pci_addresses)
232 .any(|(gpu, gpu_pci_address)| {
233 gpu_pci_address == &pci_address
234 && gpu.driver_version == driver_version
235 && gpu.driver_name == driver_name
236 });
237
238 if already_found {
239 continue;
240 }
241
242 let vendor = pci_db
243 .as_ref()
244 .and_then(|db| db.vendors.get(&vendor_pci_id));
245 let vendor_name = vendor.map(|vendor| vendor.name.clone());
246 let device_name = vendor
247 .and_then(|vendor| vendor.devices.get(&device_pci_id))
248 .map(|device| device.name.clone());
249
250 gpus.push(GpuInfo {
251 device_name,
252 device_pci_id,
253 vendor_name,
254 vendor_pci_id,
255 driver_version,
256 driver_name,
257 });
258 pci_addresses.push(pci_address);
259 }
260
261 Ok(gpus)
262}
263
264#[cfg(any(target_os = "linux", target_os = "freebsd"))]
265fn read_pci_id_from_path(path: impl AsRef<std::path::Path>) -> anyhow::Result<u16> {
266 use anyhow::Context as _;
267 let id = std::fs::read_to_string(path)?;
268 let id = id
269 .trim()
270 .strip_prefix("0x")
271 .context("Not a device ID")
272 .context(id.clone())?;
273 anyhow::ensure!(
274 id.len() == 4,
275 "Not a device id, expected 4 digits, found {}",
276 id.len()
277 );
278 u16::from_str_radix(id, 16).context("Failed to parse device ID")
279}
280
281/// Returns value of `ZED_BUNDLE_TYPE` set at compiletime or else at runtime.
282///
283/// The compiletime value is used by flatpak since it doesn't seem to have a way to provide a
284/// runtime environment variable.
285///
286/// The runtime value is used by snap since the Zed snaps use release binaries directly, and so
287/// cannot have this baked in.
288fn bundle_type() -> Option<String> {
289 option_env!("ZED_BUNDLE_TYPE")
290 .map(|bundle_type| bundle_type.to_string())
291 .or_else(|| env::var("ZED_BUNDLE_TYPE").ok())
292}