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::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}