Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GPU support for mac, multi gpu for linux #7

Merged
merged 11 commits into from
Apr 10, 2023
55 changes: 55 additions & 0 deletions .github/workflows/cargo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
on: [push, pull_request]

name: Continuous integration

jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust (Stable)
run: |
curl -sSL https://sh.rustup.rs | sh -s -- -y
rustup default stable
- name: Cargo Check
run: cargo check

test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust (Stable)
run: |
curl -sSL https://sh.rustup.rs | sh -s -- -y
rustup default stable
- name: Cargo Test
run: cargo test

fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust (Stable)
run: |
curl -sSL https://sh.rustup.rs | sh -s -- -y
rustup default stable
- name: Cargo Fmt
run: cargo fmt --all -- --check

clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust (Stable)
run: |
curl -sSL https://sh.rustup.rs | sh -s -- -y
rustup default stable
- name: Install Clippy
run: rustup component add clippy
- name: Cargo Clippy
# TODO: fix warnings and enable
run: cargo clippy # -- -D warnings
55 changes: 55 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ homepage = "https://github.com/nidnogg/zeitfetch"
readme = "README.md"

[dependencies]
sysinfo = "0.28.2"
home = "0.5.3"
lazy_static = "1.4.0"
prettytable-rs = "^0.10"
regex = "1.7.3"
serde = { version = "1.0.159", features = ["derive"] }
serde_json = "1.0.95"
sysinfo = "0.28.2"
212 changes: 175 additions & 37 deletions src/scanner.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use home;
use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
use std::process::Command;
use std::process::Stdio;
Expand All @@ -7,6 +10,24 @@ use sysinfo::{CpuExt, System, SystemExt};

use crate::logo::*;

/// Data for a single display from the `system_profiler` command
#[derive(Serialize, Deserialize, Debug)]
struct SPDisplay {
sppci_model: String,
}

/// Output of macOS's `system_profiler SPDisplayDataType` command
#[derive(Serialize, Deserialize, Debug)]
struct SPDisplaysDataTypeOutput {
#[serde(rename = "SPDisplaysDataType")]
sp_displays_data_type: Vec<SPDisplay>,
}

lazy_static! {
static ref GPU_RE: Regex =
Regex::new(r#"^\S+? "(VGA|3D|Display).*?" ".*?" "(?P<gpu>.*?)""#).unwrap();
Copy link
Owner

@nidnogg nidnogg Apr 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking unwrap() here might not be the best option - even though it does seem unlikely to fail since the regex has been tested already. If it does, however, omitting routines dependent on GPU_RE could be an option.

Copy link
Contributor Author

@triarius triarius Apr 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, I too am averse to unwrap() - the docs discourage its use, although most people in the rust community don't seem to heed this.

However, in this case, the error will only occur if the regex: r#"^\S+? "(VGA|3D|Display).*?" ".*?" "(?P<gpu>.*?)""# is not well-defined, not if there is an error when matching any particular string. Because the regex is a constant string, the value of Regex::new(...) will either always be an Err, or always be an Ok. There is no variation in runtime behaviour. So pushing the error handling down to the consumers of GPU_RE at runtime is a waste. (Incidentally, the fact that the regex is constant is also why I'm using the lazy_static macro to ensure it's only compiled once.)

Ideally, this is something that could be detected at compile time, but it might be outside the design of the compiler to check this. I think this is the next best thing: any attempt to use this module will break, even if no methods from the module are invoked. One way to make this more robust is to have a test that asserts that the regex does not panic. I suspect any test that has use crate::scanner in it will achieve this. I think I might verify this and add one.

LMK what you think.

}

pub fn get_logo(sys: &System) -> Option<String> {
sys.name().map(|sys_name| {
if sys_name.contains("Windows") {
Expand Down Expand Up @@ -243,10 +264,9 @@ pub fn get_cpu_name(sys: &System) -> Option<String> {
}

pub fn get_gpu_name(sys: &System) -> Option<String> {
// works on wsl, needs formatting and grep: lspci | grep -i --color 'vga\|3d\|2d'
let gpu_name = sys.name().map(|sys_name| {
// Windows
if sys_name.contains("Windows") {
// works on wsl, needs formatting and search on linux
sys.name().and_then(|sys_name| {
let gpus = if sys_name.contains("Windows") {
let win_fetch_gpu = Command::new("wmic")
.args(["path", "win32_VideoController", "get", "name"])
.output()
Expand All @@ -261,46 +281,56 @@ pub fn get_gpu_name(sys: &System) -> Option<String> {
.take(0)
.chain(processed_gpu_name.chars().skip(4))
.collect();
let final_gpu_name = format!("\x1b[93;1m{}\x1b[0m: {}", "GPU", trimmed_gpu_name.trim());
Some(final_gpu_name)
// TO-DO MacOS for ARM and Intel x86 versions
// Linux
} else {
// lspci | grep -i --color 'vga\|3d\|2d'
let mut cmd_lspci = Command::new("lspci")
vec![trimmed_gpu_name.trim().into()]
} else if sys_name.contains("Darwin") || sys_name.contains("Mac") {
let cmd = Command::new("system_profiler")
.args(["-json", "SPDisplaysDataType"])
.stdout(Stdio::piped())
.spawn()
.ok()?;
let mut cmd_grep = Command::new("grep")
.args(["-i", "--color", "\'vga\\|3d\\|2d\'"])
.stdin(Stdio::piped())
parse_system_profiler_json_output(std::io::BufReader::new(cmd.stdout?))
} else {
// Linux
let lspci = Command::new("lspci")
.args(["-mm"])
.stdout(Stdio::piped())
.spawn()
.ok()?;
if let Some(ref mut stdout) = cmd_lspci.stdout {
if let Some(ref mut stdin) = cmd_grep.stdin {
let mut buf: Vec<u8> = Vec::new();
if let Err(_) = stdout.read_to_end(&mut buf) {
return None;
}
if let Err(_) = stdin.write_all(&buf) {
return None;
}
}
}
let gpu_name_buf = cmd_grep.wait_with_output().ok()?.stdout;
let processed_gpu_name = match str::from_utf8(&gpu_name_buf) {
Ok(result) => result,
Err(_) => return None,
};
let mut processed_gpu_no_newline = String::from(processed_gpu_name);
processed_gpu_no_newline.pop();
let final_sys_name = format!("\x1b[93;1m{}\x1b[0m: {}", "GPU", processed_gpu_no_newline);
Some(final_sys_name)
}
});
parse_lspci_mm_output(lspci.stdout?)
};
Some(
gpus.iter()
.map(|g| format!("\x1b[93;1m{}\x1b[0m: {}", "GPU", g))
.collect::<Vec<_>>()
.join("\n"),
)
})
}

fn parse_lspci_mm_output(reader: impl Read) -> Vec<String> {
use std::io::{BufRead, BufReader};
BufReader::new(reader)
.lines()
.flat_map(|l| {
l.ok().and_then(|l| {
GPU_RE
.captures(&l)
.and_then(|c| c.name("gpu").map(|c| c.as_str().to_owned()))
})
})
.collect()
}

gpu_name.flatten()
fn parse_system_profiler_json_output(reader: impl Read) -> Vec<String> {
let displays: SPDisplaysDataTypeOutput = match serde_json::from_reader(reader) {
Ok(o) => o,
Err(_) => return vec![],
};
displays
.sp_displays_data_type
.iter()
.map(|d| d.sppci_model.to_owned())
.collect::<Vec<_>>()
}

pub fn get_mem_info(sys: &System) -> Option<String> {
Expand Down Expand Up @@ -353,3 +383,111 @@ fn get_mac_friendly_name(untrimmed_ver_num: String) -> String {

format!("{} {}", friendly_name, ver_num)
}

#[cfg(test)]
mod tests {
#[test]
fn gpu_regex_empty() {
use crate::scanner::GPU_RE;
let cap = GPU_RE.captures("");
assert!(cap.is_none());
}

#[test]
fn gpu_regex_not_gpu() {
use crate::scanner::GPU_RE;
let cap = GPU_RE.captures(r#"00:00.0 "Host bridge" "Intel Corporation" "12th Gen Core Processor Host Bridge/DRAM Registers" -r02 -p00 "Dell" "Device 0b19""#);
assert!(cap.is_none());
}

#[test]
fn gpu_regex_nvidia() {
use crate::scanner::GPU_RE;
let cap = GPU_RE.captures(r#"01:00.0 "3D controller" "NVIDIA Corporation" "GA107M [GeForce RTX 3050 Ti Mobile]" -ra1 -p00 "Dell" "Device 0b19""#);
let gpu = cap.and_then(|c| c.name("gpu").map(|m| m.as_str()));
assert_eq!(gpu, Some("GA107M [GeForce RTX 3050 Ti Mobile]"));
}

#[test]
fn parse_lspci_mm_output() {
let input = r#"00:00.0 "Host bridge" "Intel Corporation" "12th Gen Core Processor Host Bridge/DRAM Registers" -r02 -p00 "Dell" "Device 0b19"
00:01.0 "PCI bridge" "Intel Corporation" "12th Gen Core Processor PCI Express x16 Controller #1" -r02 -p00 "Dell" "Device 0b19"
00:02.0 "VGA compatible controller" "Intel Corporation" "Alder Lake-P Integrated Graphics Controller" -r0c -p00 "Dell" "Device 0b19"
00:04.0 "Signal processing controller" "Intel Corporation" "Alder Lake Innovation Platform Framework Processor Participant" -r02 -p00 "Dell" "Device 0b19"
00:06.0 "PCI bridge" "Intel Corporation" "12th Gen Core Processor PCI Express x4 Controller #0" -r02 -p00 "Dell" "Device 0b19"
00:07.0 "PCI bridge" "Intel Corporation" "Alder Lake-P Thunderbolt 4 PCI Express Root Port #0" -r02 -p00 "Dell" "Device 0b19"
00:07.1 "PCI bridge" "Intel Corporation" "Alder Lake-P Thunderbolt 4 PCI Express Root Port #1" -r02 -p00 "Dell" "Device 0b19"
00:08.0 "System peripheral" "Intel Corporation" "12th Gen Core Processor Gaussian & Neural Accelerator" -r02 -p00 "Dell" "Device 0b19"
00:0d.0 "USB controller" "Intel Corporation" "Alder Lake-P Thunderbolt 4 USB Controller" -r02 -p30 "Dell" "Device 0b19"
00:0d.2 "USB controller" "Intel Corporation" "Alder Lake-P Thunderbolt 4 NHI #0" -r02 -p40 "Dell" "Device 0b19"
00:12.0 "Serial controller" "Intel Corporation" "Alder Lake-P Integrated Sensor Hub" -r01 -p00 "Dell" "Device 0b19"
00:14.0 "USB controller" "Intel Corporation" "Alder Lake PCH USB 3.2 xHCI Host Controller" -r01 -p30 "Dell" "Device 0b19"
00:14.2 "RAM memory" "Intel Corporation" "Alder Lake PCH Shared SRAM" -r01 -p00 "Dell" "Device 0b19"
00:14.3 "Network controller" "Intel Corporation" "Alder Lake-P PCH CNVi WiFi" -r01 -p00 "Intel Corporation" "Wi-Fi 6E AX211 160MHz"
00:15.0 "Serial bus controller" "Intel Corporation" "Alder Lake PCH Serial IO I2C Controller #0" -r01 -p00 "Dell" "Device 0b19"
00:15.1 "Serial bus controller" "Intel Corporation" "Alder Lake PCH Serial IO I2C Controller #1" -r01 -p00 "Dell" "Device 0b19"
00:16.0 "Communication controller" "Intel Corporation" "Alder Lake PCH HECI Controller" -r01 -p00 "Dell" "Device 0b19"
00:1c.0 "PCI bridge" "Intel Corporation" "Device 51bb" -r01 -p00 "Dell" "Device 0b19"
00:1f.0 "ISA bridge" "Intel Corporation" "Alder Lake PCH eSPI Controller" -r01 -p00 "Dell" "Device 0b19"
00:1f.3 "Audio device" "Intel Corporation" "Alder Lake PCH-P High Definition Audio Controller" -r01 -p80 "Dell" "Device 0b19"
00:1f.4 "SMBus" "Intel Corporation" "Alder Lake PCH-P SMBus Host Controller" -r01 -p00 "Dell" "Device 0b19"
00:1f.5 "Serial bus controller" "Intel Corporation" "Alder Lake-P PCH SPI Controller" -r01 -p00 "Dell" "Device 0b19"
01:00.0 "3D controller" "NVIDIA Corporation" "GA107M [GeForce RTX 3050 Ti Mobile]" -ra1 -p00 "Dell" "Device 0b19"
02:00.0 "Non-Volatile memory controller" "Samsung Electronics Co Ltd" "NVMe SSD Controller PM9A1/PM9A3/980PRO" -p02 "Samsung Electronics Co Ltd" "Device a801"
54:00.0 "PCI bridge" "Intel Corporation" "JHL7540 Thunderbolt 3 Bridge [Titan Ridge DD 2018]" -r06 -p00 "Intel Corporation" "Device 0000"
55:02.0 "PCI bridge" "Intel Corporation" "JHL7540 Thunderbolt 3 Bridge [Titan Ridge DD 2018]" -r06 -p00 "Intel Corporation" "Device 0000"
55:04.0 "PCI bridge" "Intel Corporation" "JHL7540 Thunderbolt 3 Bridge [Titan Ridge DD 2018]" -r06 -p00 "Intel Corporation" "Device 0000"
56:00.0 "USB controller" "Intel Corporation" "JHL7540 Thunderbolt 3 USB Controller [Titan Ridge DD 2018]" -r06 -p30 "Intel Corporation" "Device 0000"
a5:00.0 "Unassigned class [ff00]" "Realtek Semiconductor Co., Ltd." "RTS5260 PCI Express Card Reader" -r01 -p00 "Dell" "Device 0b19"
"#;
assert_eq!(
super::parse_lspci_mm_output(input.as_bytes()),
vec![
"Alder Lake-P Integrated Graphics Controller",
"GA107M [GeForce RTX 3050 Ti Mobile]"
]
);
}

#[test]
fn parse_system_profiler_json_output() {
let input = r#"{
"SPDisplaysDataType" : [
{
"_name" : "kHW_AppleM1Item",
"spdisplays_mtlgpufamilysupport" : "spdisplays_metal3",
"spdisplays_ndrvs" : [
{
"_name" : "Color LCD",
"_spdisplays_display-product-id" : "a047",
"_spdisplays_display-serial-number" : "---redacted---",
"_spdisplays_display-vendor-id" : "610",
"_spdisplays_display-week" : "0",
"_spdisplays_display-year" : "0",
"_spdisplays_displayID" : "1",
"_spdisplays_pixels" : "2880 x 1800",
"_spdisplays_resolution" : "1440 x 900 @ 60.00Hz",
"spdisplays_ambient_brightness" : "spdisplays_yes",
"spdisplays_connection_type" : "spdisplays_internal",
"spdisplays_display_type" : "spdisplays_built-in_retinaLCD",
"spdisplays_main" : "spdisplays_yes",
"spdisplays_mirror" : "spdisplays_off",
"spdisplays_online" : "spdisplays_yes",
"spdisplays_pixelresolution" : "spdisplays_2560x1600Retina"
}
],
"spdisplays_vendor" : "sppci_vendor_Apple",
"sppci_bus" : "spdisplays_builtin",
"sppci_cores" : "7",
"sppci_device_type" : "spdisplays_gpu",
"sppci_model" : "Apple M1"
}
]
}
"#;
assert_eq!(
super::parse_system_profiler_json_output(input.as_bytes()),
vec!["Apple M1"],
);
}
}