system_specs.rs

  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}