system_specs.rs

  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
146const fn 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}