├── .gitignore ├── Cross.toml ├── LICENSE ├── src ├── openwrt │ ├── sysinfo_ffi.rs │ └── mod.rs ├── android │ ├── sysinfo_ffi.rs │ ├── system_properties.rs │ └── mod.rs ├── linux │ ├── sysinfo_ffi.rs │ ├── pci_devices.rs │ └── mod.rs ├── dirs.rs ├── macos │ ├── mach_ffi.rs │ └── mod.rs ├── extra.rs ├── lib.rs ├── winman.rs ├── shared │ └── mod.rs ├── freebsd │ └── mod.rs ├── netbsd │ └── mod.rs ├── windows │ └── mod.rs └── traits.rs ├── CHANGELOG.md ├── README.md ├── Cargo.toml └── .github └── workflows └── libmacchina.yml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea/ 3 | Cargo.lock 4 | .vim 5 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-linux-android] 2 | # Workaround for https://github.com/cross-rs/cross/issues/1128 3 | # and https://github.com/rust-lang/rust/issues/103673 4 | image = "ghcr.io/cross-rs/aarch64-linux-android:edge" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Macchina CLI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/openwrt/sysinfo_ffi.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::*; 2 | 3 | #[repr(C)] 4 | #[derive(Debug, Copy, Clone)] 5 | pub struct sysinfo { 6 | pub uptime: c_ulong, 7 | pub loads: [c_ulong; 3], 8 | pub totalram: c_ulong, 9 | pub freeram: c_ulong, 10 | pub sharedram: c_ulong, 11 | pub bufferram: c_ulong, 12 | pub totalswap: c_ulong, 13 | pub freeswap: c_ulong, 14 | pub procs: c_ushort, 15 | pub pad: c_ushort, 16 | pub totalhigh: c_ulong, 17 | pub freehigh: c_ulong, 18 | pub mem_unit: c_uint, 19 | pub __reserved: [c_char; 256], 20 | } 21 | 22 | extern "C" { 23 | pub fn sysinfo(info: *mut sysinfo) -> c_int; 24 | } 25 | 26 | impl sysinfo { 27 | pub fn new() -> Self { 28 | sysinfo { 29 | uptime: 0, 30 | loads: [0; 3], 31 | totalram: 0, 32 | freeram: 0, 33 | sharedram: 0, 34 | bufferram: 0, 35 | totalswap: 0, 36 | freeswap: 0, 37 | procs: 0, 38 | pad: 0, 39 | totalhigh: 0, 40 | freehigh: 0, 41 | mem_unit: 0, 42 | __reserved: [0; 256], 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/android/sysinfo_ffi.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::*; 2 | 3 | #[repr(C)] 4 | #[derive(Debug, Copy, Clone)] 5 | pub struct sysinfo { 6 | pub uptime: c_long, 7 | pub loads: [c_ulong; 3], 8 | pub totalram: c_ulong, 9 | pub freeram: c_ulong, 10 | pub sharedram: c_ulong, 11 | pub bufferram: c_ulong, 12 | pub totalswap: c_ulong, 13 | pub freeswap: c_ulong, 14 | pub procs: c_ushort, 15 | pub pad: c_ushort, 16 | pub totalhigh: c_ulong, 17 | pub freehigh: c_ulong, 18 | pub mem_unit: c_uint, 19 | pub _f: [c_char; 20 - 2 * std::mem::size_of::() - std::mem::size_of::()], 20 | } 21 | 22 | extern "C" { 23 | pub fn sysinfo(info: *mut sysinfo) -> c_int; 24 | } 25 | 26 | impl sysinfo { 27 | pub fn new() -> Self { 28 | sysinfo { 29 | uptime: 0, 30 | loads: [0; 3], 31 | totalram: 0, 32 | freeram: 0, 33 | sharedram: 0, 34 | bufferram: 0, 35 | totalswap: 0, 36 | freeswap: 0, 37 | procs: 0, 38 | pad: 0, 39 | totalhigh: 0, 40 | freehigh: 0, 41 | mem_unit: 0, 42 | _f: [0; 20 - 2 * std::mem::size_of::() - std::mem::size_of::()], 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/linux/sysinfo_ffi.rs: -------------------------------------------------------------------------------- 1 | use std::os::raw::*; 2 | 3 | #[repr(C)] 4 | #[derive(Debug, Copy, Clone)] 5 | pub struct sysinfo { 6 | pub uptime: c_long, 7 | pub loads: [c_ulong; 3], 8 | pub totalram: c_ulong, 9 | pub freeram: c_ulong, 10 | pub sharedram: c_ulong, 11 | pub bufferram: c_ulong, 12 | pub totalswap: c_ulong, 13 | pub freeswap: c_ulong, 14 | pub procs: c_ushort, 15 | pub pad: c_ushort, 16 | pub totalhigh: c_ulong, 17 | pub freehigh: c_ulong, 18 | pub mem_unit: c_uint, 19 | pub _f: [c_char; 20 - 2 * std::mem::size_of::() - std::mem::size_of::()], 20 | } 21 | 22 | extern "C" { 23 | pub fn sysinfo(info: *mut sysinfo) -> c_int; 24 | } 25 | 26 | impl sysinfo { 27 | pub fn new() -> Self { 28 | sysinfo { 29 | uptime: 0, 30 | loads: [0; 3], 31 | totalram: 0, 32 | freeram: 0, 33 | sharedram: 0, 34 | bufferram: 0, 35 | totalswap: 0, 36 | freeswap: 0, 37 | procs: 0, 38 | pad: 0, 39 | totalhigh: 0, 40 | freehigh: 0, 41 | mem_unit: 0, 42 | _f: [0; 20 - 2 * std::mem::size_of::() - std::mem::size_of::()], 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/android/system_properties.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(dead_code)] 5 | use std::ffi::{CStr, CString}; 6 | use std::os::raw::c_char; 7 | 8 | extern "C" { 9 | pub fn __system_property_get( 10 | __name: *const ::std::os::raw::c_char, 11 | __value: *mut ::std::os::raw::c_char, 12 | ) -> ::std::os::raw::c_int; 13 | } 14 | 15 | pub fn to_string_safe(param: *mut c_char) -> String { 16 | unsafe { CStr::from_ptr(param).to_string_lossy().into_owned() } 17 | } 18 | 19 | // Takes a property name, and returns its value. 20 | pub fn getprop(name: T) -> Option 21 | where 22 | T: ToString, 23 | { 24 | let name = name.to_string(); 25 | if !name.is_ascii() { 26 | return None; 27 | } 28 | // Property name 29 | let __name: *const c_char = CString::new(name).unwrap().into_raw(); 30 | 31 | // Property value 32 | let mut __value: *mut c_char = CString::new("").unwrap().into_raw(); 33 | 34 | // making them mut / const doesn't matter in rust. 35 | // I'm keeping them like that since it is idiomatic. 36 | let ret = unsafe { __system_property_get(__name, __value) }; 37 | 38 | if ret == -1 { 39 | None 40 | } else { 41 | Some(to_string_safe(__value)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/dirs.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::{self, BufRead}; 3 | use std::path::{Path, PathBuf}; 4 | 5 | // https://doc.rust-lang.org/rust-by-example/std_misc/file/read_lines.html 6 | pub fn read_lines

(filename: P) -> io::Result>> 7 | where 8 | P: AsRef, 9 | { 10 | let file = File::open(filename)?; 11 | Ok(io::BufReader::new(file).lines()) 12 | } 13 | 14 | /// Returns the value of PKG_DBDIR if exists or a default if not. 15 | pub fn pkgdb_dir() -> Option { 16 | if let Ok(lines) = read_lines("/etc/mk.conf") { 17 | let line = lines 18 | .map_while(Result::ok) 19 | .find(|l| l.starts_with("PKG_DBDIR")); 20 | 21 | if let Some(pkg_dbdir) = line { 22 | if let Some(value) = pkg_dbdir.split('=').nth(1) { 23 | return Some(PathBuf::from(value.trim())); 24 | } 25 | }; 26 | } 27 | 28 | Some(PathBuf::from("/usr/pkg/pkgdb")) 29 | } 30 | 31 | /// Returns the value of LOCALBASE if exists or a default if not. 32 | pub fn localbase_dir() -> Option { 33 | if let Ok(lines) = read_lines("/etc/mk.conf") { 34 | let line = lines 35 | .map_while(Result::ok) 36 | .find(|l| l.starts_with("LOCALBASE")); 37 | 38 | if let Some(pkg_dbdir) = line { 39 | if let Some(value) = pkg_dbdir.split('=').nth(1) { 40 | return Some(PathBuf::from(value.trim())); 41 | } 42 | }; 43 | } 44 | 45 | Some(PathBuf::from("/usr/pkg")) 46 | } 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Unreleased 4 | 5 | ## `8.1.0` 6 | 7 | Adrian Groh: 8 | - Fix a doctest (#183) 9 | - Add support for winget and chocolatey package counts on Windows (#182) 10 | 11 | Bnyro (first-time contributor): 12 | - implement swap usage for linux (#175) 13 | 14 | ## `8.0.0` 15 | 16 | Rolv Apneseth: 17 | - BREAKING CHANGE: Allow disk_space function to accept a path argument (#156) 18 | 19 | ## `7.0.0` 20 | 21 | Rolv Apneseth: 22 | - BREAKING CHANGE: Change disk_space return value to u64 (#153) 23 | 24 | ## `6.4.1` 25 | 26 | - Default to GPU device name if subdevice name is not found 27 | - Detect VGA compatible controllers 28 | - Correctly filter battery devices when retrieving their status 29 | 30 | ## `6.4.0` 31 | 32 | Adrian Groh: 33 | - Use the correct kernel parameters when initializing FreeBSD `KernelReadout` (#148) 34 | - Implement uptime readout for FreeBSD systems (#138) 35 | - Use `MemAvailable` to calculate used memory (#134) 36 | - Prioritize detecting window managers with xprop (#133) 37 | 38 | Rolv Apneseth: Implement GPU readout for Linux systems (#140) 39 | 40 | Matthias Baer: Use a singleton for `COMLibrary` (#143) 41 | 42 | Xarblu: Change Flatpak package-counting method (#125) 43 | 44 | Kian-Meng Ang: Fix a typo in the documentation 45 | 46 | ## `6.3.5` 47 | 48 | - Ignore clippy unnecessary_cast warnings in shared module 49 | 50 | ## `6.3.4` 51 | 52 | - Add missing bang in clippy allow macros 53 | 54 | ## `6.3.3` 55 | 56 | - Ignore clippy unnecessary_cast warnings 57 | 58 | ## `6.3.2` 59 | 60 | @TheCactusVert: 61 | - Fix cargo package count (#128) 62 | 63 | @123marvin123 and @Markos-Th09: 64 | - Fix brew package count (#127) 65 | 66 | @xarblu: 67 | - Fix portage package count (#124) 68 | 69 | ## `6.3.1` 70 | 71 | @123marvin123: 72 | - Fix a bug that returns a nil framerate on certain macOS systems 73 | 74 | ## `6.3.0` 75 | 76 | @123marvin123: 77 | - Implement backlight readout for macOS 78 | 79 | ## `6.2.0` 80 | 81 | - Update dependencies where needed, bringing us up to speed with the 82 | latest and greatest stuff from the libraries we use. 83 | 84 | @DemonInTheCloset: 85 | - Fix armv7 compilation issues 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

libmacchina

3 | 4 | A library providing access to all sorts of system information. 5 | 6 | Linux • macOS • Windows • NetBSD • FreeBSD • Android • OpenWrt 7 | 8 | 9 | version 10 | 11 | 12 | 13 | docs 14 | 15 | 16 |
17 | 18 | ### Disclaimer 19 | 20 | _libmacchina_ utilizes **unsafe** code in the form of calls to system libraries 21 | that haven't been natively implemented in Rust, we do this for performance 22 | reasons. 23 | 24 | ### Usage 25 | 26 | Add the following to your project's _Cargo.toml_ file: 27 | 28 | ```toml 29 | libmacchina = "7" 30 | ``` 31 | 32 | ### Notes 33 | 34 | On distributions like openSUSE that use the `ndb` RPM database format, `librpm` 35 | (which is usually provided by the `rpm-devel` package) is required for the RPM 36 | package count readout to work. 37 | 38 | ### Examples 39 | 40 | ```rust 41 | // Let's import two of the several available types. 42 | use libmacchina::{GeneralReadout, MemoryReadout}; 43 | 44 | fn main() { 45 | // Let's import the GeneralReadout trait so we 46 | // can fetch some general information about the host. 47 | use libmacchina::traits::GeneralReadout as _; 48 | 49 | let general_readout = GeneralReadout::new(); 50 | 51 | // There are many more metrics we can query 52 | // i.e. username, distribution, terminal, shell, etc. 53 | let cpu_cores = general_readout.cpu_cores().unwrap(); // 8 [logical cores] 54 | let cpu = general_readout.cpu_model_name().unwrap(); // Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz 55 | let uptime = general_readout.uptime().unwrap(); // 1500 [in seconds] 56 | 57 | // Now we'll import the MemoryReadout trait to get an 58 | // idea of what the host's memory usage looks like. 59 | use libmacchina::traits::MemoryReadout as _; 60 | 61 | let memory_readout = MemoryReadout::new(); 62 | 63 | let total_mem = memory_readout.total(); // 20242204 [in kB] 64 | let used_mem = memory_readout.used(); // 3894880 [in kB] 65 | } 66 | 67 | ``` 68 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libmacchina" 3 | version = "8.1.0" 4 | authors = ["grtcdr ", "Marvin Haschker ", "Uttarayan Mondal "] 5 | edition = "2021" 6 | description = "A library that can fetch all sorts of system information." 7 | keywords = ["system", "fetch", "library"] 8 | repository = "https://github.com/Macchina-CLI/libmacchina" 9 | readme = "README.md" 10 | license = "MIT" 11 | build = "build.rs" 12 | 13 | [dependencies] 14 | cfg-if = "1.0.0" 15 | libc = "0.2.148" 16 | home = "0.5.5" 17 | pciid-parser = "0.6.3" 18 | 19 | [build-dependencies.vergen] 20 | version = "8.2.6" 21 | optional = true 22 | default-features = false 23 | features = ["build","cargo","git","gitcl","rustc"] 24 | 25 | [target.'cfg(target_os = "linux")'.dependencies] 26 | dirs = "5.0.1" 27 | walkdir = "2.4.0" 28 | os-release = "0.1" 29 | regex = "1.9.2" 30 | rpm-pkg-count = { version = "0.2.1", features = ["runtime"] } 31 | nix = { version = "0.26.2", features = ["socket"], default-features = false } 32 | wayland-sys = { version = "0.31.1", features = ["dlopen", "client"] } 33 | 34 | [target.'cfg(target_os = "netbsd")'.dependencies] 35 | nix = { version = "0.26.2", default-features = false, features = ["hostname"] } 36 | regex = "1.9.2" 37 | 38 | [target.'cfg(target_os = "macos")'.dependencies] 39 | core-foundation = "0.9.3" 40 | core-graphics = "0.23.1" 41 | core-video-sys = "0.1.4" 42 | mach2 = "0.4.1" 43 | 44 | [target.'cfg(target_family = "unix")'.dependencies] 45 | num_cpus = "1.16.0" 46 | 47 | [target.'cfg(target_os = "windows")'.dependencies] 48 | local-ip-address = "0.5.6" 49 | wmi = "0.12.0" 50 | winreg = "0.10.1" 51 | windows = { version = "0.39.0", features = [ 52 | "Win32_Foundation", 53 | "Win32_System_Power", 54 | "Win32_System_SystemInformation", 55 | "Win32_System_WindowsProgramming" 56 | ]} 57 | 58 | [target.'cfg(not(target_os = "windows"))'.dependencies] 59 | if-addrs = "0.10.2" 60 | 61 | [target.'cfg(any(target_os="freebsd", target_os = "linux", target_os = "windows"))'.dependencies] 62 | sqlite = "0.36.0" 63 | 64 | [target.'cfg(any(target_os="freebsd", target_os = "netbsd"))'.dependencies] 65 | x11rb = "0.12.0" 66 | 67 | [target.'cfg(any(target_os = "linux", target_os = "netbsd", target_os = "android"))'.dependencies] 68 | itertools = "0.11.0" 69 | 70 | [target.'cfg(not(any(target_os = "netbsd", target_os = "windows")))'.dependencies] 71 | sysctl = "0.5.4" 72 | 73 | [target.'cfg(any(target_os = "linux", target_os = "netbsd"))'.build-dependencies] 74 | pkg-config = { version = "0.3.27", optional = true} 75 | 76 | [features] 77 | openwrt = [] 78 | version = ["vergen"] 79 | -------------------------------------------------------------------------------- /src/macos/mach_ffi.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_camel_case_types, non_upper_case_globals, dead_code, unused)] 2 | 3 | use mach2::boolean; 4 | use mach2::kern_return; 5 | use mach2::kern_return::kern_return_t; 6 | use mach2::mach_types::{host_name_port_t, host_t}; 7 | use mach2::message::mach_msg_type_number_t; 8 | use mach2::vm_types::{integer_t, natural_t}; 9 | 10 | use core_foundation::array::CFArrayRef; 11 | use core_foundation::base::{mach_port_t, CFAllocatorRef, CFRelease, CFTypeRef, TCFTypeRef}; 12 | use core_foundation::dictionary::{CFDictionaryRef, CFMutableDictionaryRef}; 13 | use core_foundation::string::CFStringRef; 14 | use core_graphics::display::CGDirectDisplayID; 15 | use libc::c_char; 16 | use std::os::raw::c_uint; 17 | 18 | type host_flavor_t = integer_t; 19 | type host_info64_t = *mut integer_t; 20 | pub type io_object_t = mach_port_t; 21 | pub type io_service_t = io_object_t; 22 | pub type IOOptionBits = c_uint; 23 | pub type io_registry_entry_t = io_object_t; 24 | 25 | #[repr(C)] 26 | #[derive(Debug, Copy, Clone, Default)] 27 | pub(crate) struct vm_statistics64 { 28 | pub free_count: natural_t, 29 | pub active_count: natural_t, 30 | pub inactive_count: natural_t, 31 | pub wire_count: natural_t, 32 | pub zero_fill_count: u64, 33 | pub reactivations: u64, 34 | pub pageins: u64, 35 | pub pageouts: u64, 36 | pub faults: u64, 37 | pub cow_faults: u64, 38 | pub lookups: u64, 39 | pub hits: u64, 40 | pub purges: u64, 41 | pub purgeable_count: natural_t, 42 | pub speculative_count: natural_t, 43 | pub decompressions: u64, 44 | pub compressions: u64, 45 | pub swapins: u64, 46 | pub swapouts: u64, 47 | pub compressor_page_count: natural_t, 48 | pub throttled_count: natural_t, 49 | pub external_page_count: natural_t, 50 | pub internal_page_count: natural_t, 51 | pub total_uncompressed_pages_in_compressor: u64, 52 | } 53 | 54 | extern "C" { 55 | pub fn host_statistics64( 56 | host_priv: host_t, 57 | flavor: host_flavor_t, 58 | host_info64_out: host_info64_t, 59 | host_info64_out_cnt: *mut mach_msg_type_number_t, 60 | ) -> kern_return_t; 61 | 62 | pub fn mach_host_self() -> host_name_port_t; 63 | 64 | #[link_name = "kIOMasterPortDefault"] 65 | pub static kIOMasterPortDefault: mach_port_t; 66 | 67 | pub fn IOServiceMatching(name: *const c_char) -> CFMutableDictionaryRef; 68 | 69 | pub fn IOServiceGetMatchingService( 70 | master_port: mach_port_t, 71 | matching: CFDictionaryRef, 72 | ) -> io_service_t; 73 | 74 | pub fn IORegistryEntryCreateCFProperties( 75 | entry: io_registry_entry_t, 76 | properties: *mut CFMutableDictionaryRef, 77 | allocator: CFAllocatorRef, 78 | options: IOOptionBits, 79 | ) -> kern_return_t; 80 | 81 | pub fn IOObjectRelease(object: io_object_t) -> kern_return_t; 82 | 83 | pub fn DisplayServicesGetBrightness(id: CGDirectDisplayID, brightness: *mut f32) -> i32; 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/libmacchina.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | name: Lint 9 | env: 10 | RUSTFLAGS: "-Dwarnings" 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Bootstrap 16 | uses: dtolnay/rust-toolchain@stable 17 | with: 18 | components: rustfmt, clippy 19 | 20 | - name: Formatting 21 | run: cargo fmt --all -- --check 22 | 23 | - name: Clippy 24 | run: cargo clippy --all-targets --all-features 25 | 26 | checks: 27 | name: ${{ matrix.name }} (${{ matrix.target }}) 28 | runs-on: ${{ matrix.os }} 29 | env: 30 | PROGRAM: ${{ matrix.cross && 'cross' || 'cargo' }} 31 | strategy: 32 | fail-fast: false 33 | matrix: 34 | target: 35 | - x86_64-unknown-linux-gnu 36 | - x86_64-apple-darwin 37 | - x86_64-pc-windows-msvc 38 | - x86_64-unknown-netbsd 39 | - x86_64-unknown-freebsd 40 | - aarch64-linux-android 41 | - aarch64-unknown-linux-gnu 42 | - armv7-unknown-linux-gnueabihf 43 | 44 | include: 45 | - os: ubuntu-latest 46 | name: Linux 47 | target: x86_64-unknown-linux-gnu 48 | cross: false 49 | test: true 50 | 51 | - os: macos-latest 52 | name: macOS 53 | target: x86_64-apple-darwin 54 | cross: false 55 | test: true 56 | 57 | - os: windows-latest 58 | name: Windows 59 | target: x86_64-pc-windows-msvc 60 | cross: false 61 | test: true 62 | 63 | - os: ubuntu-latest 64 | name: NetBSD 65 | target: x86_64-unknown-netbsd 66 | cross: true 67 | test: false 68 | 69 | - os: ubuntu-latest 70 | name: FreeBSD 71 | target: x86_64-unknown-freebsd 72 | cross: true 73 | test: false 74 | 75 | - os: ubuntu-latest 76 | name: Android 77 | target: aarch64-linux-android 78 | cross: true 79 | test: true 80 | 81 | - os: ubuntu-latest 82 | name: OpenWrt 83 | target: aarch64-unknown-linux-gnu 84 | cross: true 85 | test: true 86 | cargo_args: --features "openwrt" 87 | 88 | - os: ubuntu-latest 89 | name: Linux ARMv7 90 | target: armv7-unknown-linux-gnueabihf 91 | cross: true 92 | test: true 93 | 94 | steps: 95 | - name: Checkout 96 | uses: actions/checkout@v4 97 | 98 | - name: Bootstrap 99 | uses: dtolnay/rust-toolchain@stable 100 | with: 101 | targets: ${{ matrix.target }} 102 | 103 | - name: Install cross 104 | run: cargo install cross 105 | if: ${{ matrix.cross }} 106 | 107 | - name: Build 108 | run: ${{ env.PROGRAM }} build --target=${{ matrix.target }} ${{ matrix.cargo_args }} 109 | 110 | - name: Test 111 | run: ${{ env.PROGRAM }} test --target=${{ matrix.target }} ${{ matrix.cargo_args }} 112 | if: ${{ matrix.test }} 113 | -------------------------------------------------------------------------------- /src/extra.rs: -------------------------------------------------------------------------------- 1 | //! This module provides additional functionalities 2 | #![allow(dead_code)] 3 | 4 | use std::env; 5 | use std::ffi::OsStr; 6 | use std::path::{Path, PathBuf}; 7 | 8 | /** 9 | This function pops `\n` from the end of a given `String` if it is found. 10 | 11 | This can come in handy when reading the contents of a file that might 12 | contain a newline control character at the end of the line. 13 | 14 | Files of this kind are very common on GNU/Linux systems. 15 | */ 16 | pub fn pop_newline(input: T) -> String 17 | where 18 | T: std::string::ToString, 19 | { 20 | let mut output = input.to_string(); 21 | if output.ends_with('\n') { 22 | output.pop(); 23 | } 24 | 25 | output 26 | } 27 | 28 | /// Uppercase the first letter of a `String` or `&str`. 29 | pub fn ucfirst>(s: S) -> String { 30 | let mut c = s.as_ref().chars(); 31 | match c.next() { 32 | None => String::new(), 33 | Some(f) => f.to_uppercase().collect::() + c.as_str(), 34 | } 35 | } 36 | 37 | /** 38 | Search all directories in __PATH__ for a program e.g. _ps_, _grep_, etc. 39 | 40 | This can be used to check if a particular program exists 41 | before running the command associated with said program. 42 | 43 | - Returns `true` if a given program is in __PATH__, and `false` if it isn't. 44 | */ 45 | pub fn which

(input: P) -> bool 46 | where 47 | P: AsRef, 48 | { 49 | env::var_os("PATH") 50 | .and_then(|paths| env::split_paths(&paths).find(|dir| dir.join(&input).is_file())) 51 | .is_some() 52 | } 53 | 54 | // Returns the number of newlines in a buffer 55 | pub fn count_lines(buffer: T) -> Option 56 | where 57 | T: std::string::ToString, 58 | { 59 | let buf = buffer.to_string().trim().to_owned(); 60 | 61 | if !buf.is_empty() { 62 | return Some(buf.as_bytes().iter().filter(|&&c| c == b'\n').count() + 1); 63 | } 64 | 65 | None 66 | } 67 | 68 | /** 69 | Returns the entries of a given `Path`. 70 | 71 | - If `Path` is not a directory, the function will return an empty `Vec`. 72 | */ 73 | pub fn get_entries(path: &Path) -> Option> { 74 | if let Ok(dir) = std::fs::read_dir(path) { 75 | let mut entries: Vec = Vec::new(); 76 | dir.map_while(Result::ok) 77 | .for_each(|x| entries.push(x.path())); 78 | return Some(entries); 79 | } 80 | 81 | None 82 | } 83 | 84 | /// Returns the extension of a given path. 85 | pub fn path_extension(path: &Path) -> Option<&str> { 86 | path.extension().and_then(OsStr::to_str) 87 | } 88 | 89 | pub fn common_shells() -> [&'static str; 10] { 90 | [ 91 | "sh", "su", "nu", "bash", "fish", "dash", "tcsh", "zsh", "ksh", "csh", 92 | ] 93 | } 94 | 95 | #[cfg(test)] 96 | #[cfg(not(target_os = "netbsd"))] 97 | mod tests { 98 | use super::*; 99 | 100 | #[test] 101 | fn test_ucfirst() { 102 | assert_eq!(ucfirst("lorem"), "Lorem"); 103 | assert_eq!(ucfirst("Ipsum"), "Ipsum"); 104 | } 105 | 106 | #[test] 107 | fn test_pop_newline() { 108 | assert_eq!(pop_newline(String::from("Lorem ipsum\n")), "Lorem ipsum"); 109 | } 110 | 111 | #[test] 112 | fn test_path_extension() { 113 | assert_eq!(path_extension(Path::new("foo.rs")).unwrap(), "rs"); 114 | assert!(path_extension(Path::new("bar")) 115 | .unwrap_or_default() 116 | .is_empty()); 117 | } 118 | 119 | #[test] 120 | #[cfg(not(feature = "openwrt"))] 121 | fn test_which() { 122 | assert!(which("sh")); 123 | assert!(!which("not_a_real_command")); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/linux/pci_devices.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{read_dir, read_to_string}, 3 | io, 4 | path::PathBuf, 5 | }; 6 | 7 | use pciid_parser::{schema::SubDeviceId, Database}; 8 | 9 | use crate::extra::pop_newline; 10 | 11 | fn parse_device_hex(hex_str: &str) -> String { 12 | pop_newline(hex_str).chars().skip(2).collect::() 13 | } 14 | 15 | pub enum PciDeviceReadableValues { 16 | Class, 17 | Vendor, 18 | Device, 19 | SubVendor, 20 | SubDevice, 21 | } 22 | 23 | impl PciDeviceReadableValues { 24 | fn as_str(&self) -> &'static str { 25 | match self { 26 | PciDeviceReadableValues::Class => "class", 27 | PciDeviceReadableValues::Vendor => "vendor", 28 | PciDeviceReadableValues::Device => "device", 29 | PciDeviceReadableValues::SubVendor => "subsystem_vendor", 30 | PciDeviceReadableValues::SubDevice => "subsystem_device", 31 | } 32 | } 33 | } 34 | 35 | #[derive(Debug)] 36 | pub struct PciDevice { 37 | base_path: PathBuf, 38 | } 39 | 40 | impl PciDevice { 41 | fn new(base_path: PathBuf) -> PciDevice { 42 | PciDevice { base_path } 43 | } 44 | 45 | fn read_value(&self, readable_value: PciDeviceReadableValues) -> String { 46 | let value_path = self.base_path.join(readable_value.as_str()); 47 | 48 | match read_to_string(&value_path) { 49 | Ok(hex_string) => parse_device_hex(&hex_string), 50 | _ => panic!("Could not find value: {:?}", value_path), 51 | } 52 | } 53 | 54 | pub fn is_gpu(&self, db: &Database) -> bool { 55 | let class_value = self.read_value(PciDeviceReadableValues::Class); 56 | let first_pair = class_value.chars().take(2).collect::(); 57 | let classes = ["Display controller", "VGA compatible controller"]; 58 | 59 | match db.classes.get(&first_pair) { 60 | Some(class) => classes.contains(&class.name.as_str()), 61 | _ => false, 62 | } 63 | } 64 | 65 | pub fn get_device_name(&self, db: &Database) -> Option { 66 | let vendor_value = self.read_value(PciDeviceReadableValues::Vendor); 67 | let sub_vendor_value = self.read_value(PciDeviceReadableValues::SubVendor); 68 | let device_value = self.read_value(PciDeviceReadableValues::Device); 69 | let sub_device_value = self.read_value(PciDeviceReadableValues::SubDevice); 70 | 71 | let Some(vendor) = db.vendors.get(&vendor_value) else { 72 | return None; 73 | }; 74 | 75 | let Some(device) = vendor.devices.get(&device_value) else { 76 | return None; 77 | }; 78 | // To return device name if no valid subdevice name is found 79 | let device_name = device.name.to_owned(); 80 | 81 | let sub_device_id = SubDeviceId { 82 | subvendor: sub_vendor_value, 83 | subdevice: sub_device_value, 84 | }; 85 | 86 | if let Some(sub_device) = device.subdevices.get(&sub_device_id) { 87 | let start = match sub_device.find('[') { 88 | Some(i) => i + 1, 89 | _ => return Some(device_name), 90 | }; 91 | let end = sub_device.len() - 1; 92 | 93 | Some(sub_device.chars().take(end).skip(start).collect::()) 94 | } else { 95 | Some(device_name) 96 | } 97 | } 98 | } 99 | pub fn get_pci_devices() -> Result, io::Error> { 100 | let devices_dir = read_dir("/sys/bus/pci/devices/")?; 101 | 102 | let mut devices = vec![]; 103 | for device_entry in devices_dir.map_while(Result::ok) { 104 | devices.push(PciDevice::new(device_entry.path())); 105 | } 106 | 107 | Ok(devices) 108 | } 109 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | 3 | cfg_if! { 4 | if #[cfg(all(target_os = "linux", feature = "openwrt"))] { 5 | mod extra; 6 | mod openwrt; 7 | 8 | pub type BatteryReadout = openwrt::OpenWrtBatteryReadout; 9 | pub type KernelReadout = openwrt::OpenWrtKernelReadout; 10 | pub type MemoryReadout = openwrt::OpenWrtMemoryReadout; 11 | pub type GeneralReadout = openwrt::OpenWrtGeneralReadout; 12 | pub type ProductReadout = openwrt::OpenWrtProductReadout; 13 | pub type PackageReadout = openwrt::OpenWrtPackageReadout; 14 | pub type NetworkReadout = openwrt::OpenWrtNetworkReadout; 15 | } else if #[cfg(all(target_os = "linux", not(feature = "openwrt")))] { 16 | mod extra; 17 | mod linux; 18 | mod winman; 19 | 20 | pub type BatteryReadout = linux::LinuxBatteryReadout; 21 | pub type KernelReadout = linux::LinuxKernelReadout; 22 | pub type MemoryReadout = linux::LinuxMemoryReadout; 23 | pub type GeneralReadout = linux::LinuxGeneralReadout; 24 | pub type ProductReadout = linux::LinuxProductReadout; 25 | pub type PackageReadout = linux::LinuxPackageReadout; 26 | pub type NetworkReadout = linux::LinuxNetworkReadout; 27 | } else if #[cfg(target_os = "macos")] { 28 | mod extra; 29 | mod macos; 30 | 31 | pub type BatteryReadout = macos::MacOSBatteryReadout; 32 | pub type KernelReadout = macos::MacOSKernelReadout; 33 | pub type MemoryReadout = macos::MacOSMemoryReadout; 34 | pub type GeneralReadout = macos::MacOSGeneralReadout; 35 | pub type ProductReadout = macos::MacOSProductReadout; 36 | pub type PackageReadout = macos::MacOSPackageReadout; 37 | pub type NetworkReadout = macos::MacOSNetworkReadout; 38 | } else if #[cfg(target_os = "netbsd")] { 39 | mod extra; 40 | mod netbsd; 41 | mod winman; 42 | pub mod dirs; 43 | 44 | pub type BatteryReadout = netbsd::NetBSDBatteryReadout; 45 | pub type KernelReadout = netbsd::NetBSDKernelReadout; 46 | pub type MemoryReadout = netbsd::NetBSDMemoryReadout; 47 | pub type GeneralReadout = netbsd::NetBSDGeneralReadout; 48 | pub type ProductReadout = netbsd::NetBSDProductReadout; 49 | pub type PackageReadout = netbsd::NetBSDPackageReadout; 50 | pub type NetworkReadout = netbsd::NetBSDNetworkReadout; 51 | } else if #[cfg(target_os = "windows")] { 52 | mod windows; 53 | 54 | pub type BatteryReadout = windows::WindowsBatteryReadout; 55 | pub type KernelReadout = windows::WindowsKernelReadout; 56 | pub type MemoryReadout = windows::WindowsMemoryReadout; 57 | pub type GeneralReadout = windows::WindowsGeneralReadout; 58 | pub type ProductReadout = windows::WindowsProductReadout; 59 | pub type PackageReadout = windows::WindowsPackageReadout; 60 | pub type NetworkReadout = windows::WindowsNetworkReadout; 61 | } else if #[cfg(target_os = "android")] { 62 | mod android; 63 | mod extra; 64 | 65 | pub type BatteryReadout = android::AndroidBatteryReadout; 66 | pub type KernelReadout = android::AndroidKernelReadout; 67 | pub type MemoryReadout = android::AndroidMemoryReadout; 68 | pub type GeneralReadout = android::AndroidGeneralReadout; 69 | pub type ProductReadout = android::AndroidProductReadout; 70 | pub type PackageReadout = android::AndroidPackageReadout; 71 | pub type NetworkReadout = android::AndroidNetworkReadout; 72 | } else if #[cfg(target_os = "freebsd")] { 73 | mod extra; 74 | mod freebsd; 75 | mod winman; 76 | 77 | pub type BatteryReadout = freebsd::FreeBSDBatteryReadout; 78 | pub type KernelReadout = freebsd::FreeBSDKernelReadout; 79 | pub type MemoryReadout = freebsd::FreeBSDMemoryReadout; 80 | pub type GeneralReadout = freebsd::FreeBSDGeneralReadout; 81 | pub type ProductReadout = freebsd::FreeBSDProductReadout; 82 | pub type PackageReadout = freebsd::FreeBSDPackageReadout; 83 | pub type NetworkReadout = freebsd::FreeBSDNetworkReadout; 84 | } else { 85 | compiler_error!("This platform is currently not supported by libmacchina."); 86 | } 87 | } 88 | 89 | pub struct Readouts { 90 | pub battery: BatteryReadout, 91 | pub kernel: KernelReadout, 92 | pub memory: MemoryReadout, 93 | pub general: GeneralReadout, 94 | pub product: ProductReadout, 95 | pub packages: PackageReadout, 96 | pub network: NetworkReadout, 97 | } 98 | 99 | #[cfg(feature = "version")] 100 | pub fn version() -> &'static str { 101 | if let Some(git_sha) = option_env!("VERGEN_GIT_SHA_SHORT") { 102 | Box::leak(format!("{} ({})", env!("CARGO_PKG_VERSION"), git_sha).into_boxed_str()) 103 | } else { 104 | env!("CARGO_PKG_VERSION") 105 | } 106 | } 107 | 108 | mod shared; 109 | pub mod traits; 110 | -------------------------------------------------------------------------------- /src/winman.rs: -------------------------------------------------------------------------------- 1 | //! This module provides a set of functions that detect the name of the window manager the host is 2 | //! running. 3 | 4 | use crate::extra; 5 | use crate::traits::ReadoutError; 6 | 7 | use std::process::{Command, Stdio}; 8 | 9 | #[cfg(target_os = "linux")] 10 | use wayland_sys::{client::*, ffi_dispatch}; 11 | 12 | #[cfg(target_os = "linux")] 13 | use nix::sys::socket::{sockopt, GetSockOpt}; 14 | 15 | #[cfg(target_os = "linux")] 16 | use std::os::fd::AsRawFd; 17 | 18 | #[cfg(target_os = "linux")] 19 | pub fn detect_wayland_window_manager() -> Result { 20 | if !is_lib_available() { 21 | return Err(ReadoutError::MetricNotAvailable); 22 | } 23 | 24 | let display_ptr = unsafe { 25 | ffi_dispatch!( 26 | wayland_client_handle(), 27 | wl_display_connect, 28 | ::std::ptr::null() 29 | ) 30 | }; 31 | 32 | if display_ptr.is_null() { 33 | return Err(ReadoutError::MetricNotAvailable); 34 | } 35 | 36 | let display_fd = 37 | unsafe { ffi_dispatch!(wayland_client_handle(), wl_display_get_fd, display_ptr) } 38 | .as_raw_fd(); 39 | 40 | let pid = sockopt::PeerCredentials 41 | .get(display_fd) 42 | .map_err(|_| ReadoutError::MetricNotAvailable)? 43 | .pid(); 44 | 45 | Ok(extra::pop_newline(std::fs::read_to_string(format!( 46 | "/proc/{}/comm", 47 | pid 48 | ))?)) 49 | } 50 | 51 | pub fn detect_xorg_window_manager() -> Result { 52 | if extra::which("xprop") { 53 | let xprop_id = Command::new("xprop") 54 | .args(["-root", "-notype", "_NET_SUPPORTING_WM_CHECK"]) 55 | .stdout(Stdio::piped()) 56 | .stderr(Stdio::piped()) 57 | .spawn() 58 | .expect("ERROR: failed to spawn \"xprop\" process"); 59 | 60 | let xprop_id_output = xprop_id 61 | .wait_with_output() 62 | .expect("ERROR: failed to wait for \"xprop\" process to exit"); 63 | 64 | let window_manager_id_info = String::from_utf8(xprop_id_output.stdout) 65 | .expect("ERROR: \"xprop -root -notype _NET_SUPPORTING_WM_CHECK\" process stdout was not valid UTF-8"); 66 | 67 | let window_manager_id = window_manager_id_info.split(' ').last().unwrap_or_default(); 68 | 69 | let xprop_property = Command::new("xprop") 70 | .args([ 71 | "-id", 72 | window_manager_id, 73 | "-notype", 74 | "-len", 75 | "25", 76 | "-f", 77 | "_NET_WM_NAME", 78 | "8t", 79 | ]) 80 | .stdout(Stdio::piped()) 81 | .stderr(Stdio::piped()) 82 | .spawn() 83 | .expect("ERROR: failed to spawn \"xprop\" process"); 84 | 85 | let xprop_property_output = xprop_property 86 | .wait_with_output() 87 | .expect("ERROR: failed to wait for \"xprop\" process to exit"); 88 | 89 | let window_manager_name_info = String::from_utf8(xprop_property_output.stdout) 90 | .unwrap_or_else(|_| { 91 | panic!( 92 | "ERROR: \"xprop -id {window_manager_id} -notype -len 25 93 | -f _NET_WM_NAME 8t\" process stdout was not valid UTF-8" 94 | ) 95 | }); 96 | 97 | if let Some(line) = window_manager_name_info 98 | .lines() 99 | .find(|line| line.starts_with("_NET_WM_NAME")) 100 | { 101 | return Ok(line 102 | .split_once('=') 103 | .unwrap_or_default() 104 | .1 105 | .trim() 106 | .replace(['\"', '\''], "")); 107 | }; 108 | } 109 | 110 | if extra::which("wmctrl") { 111 | let wmctrl = Command::new("wmctrl") 112 | .arg("-m") 113 | .stdout(Stdio::piped()) 114 | .stderr(Stdio::piped()) 115 | .spawn() 116 | .expect("ERROR: failed to spawn \"wmctrl\" process"); 117 | 118 | let wmctrl_out = wmctrl 119 | .stdout 120 | .expect("ERROR: failed to open \"wmctrl\" stdout"); 121 | 122 | let head = Command::new("head") 123 | .args(["-n", "1"]) 124 | .stdin(Stdio::from(wmctrl_out)) 125 | .stdout(Stdio::piped()) 126 | .spawn() 127 | .expect("ERROR: failed to spawn \"head\" process"); 128 | 129 | let output = head 130 | .wait_with_output() 131 | .expect("ERROR: failed to wait for \"head\" process to exit"); 132 | 133 | let window_manager = String::from_utf8(output.stdout) 134 | .expect("ERROR: \"wmctrl -m | head -n1\" process stdout was not valid UTF-8"); 135 | 136 | let winman_name = 137 | extra::pop_newline(String::from(window_manager.replace("Name:", "").trim())); 138 | 139 | if winman_name == "N/A" || winman_name.is_empty() { 140 | return Err(ReadoutError::Other( 141 | "Window manager not available — perhaps it's not EWMH-compliant.".to_string(), 142 | )); 143 | } 144 | 145 | return Ok(winman_name); 146 | } 147 | 148 | Err(ReadoutError::Other( 149 | "\"wmctrl\" must be installed to display your window manager.".to_string(), 150 | )) 151 | } 152 | -------------------------------------------------------------------------------- /src/shared/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_imports)] 3 | #![allow(clippy::unnecessary_cast)] 4 | 5 | use crate::traits::{ReadoutError, ShellFormat, ShellKind}; 6 | 7 | use std::fs::read_dir; 8 | use std::fs::read_to_string; 9 | use std::io::Error; 10 | use std::path::Path; 11 | use std::process::{Command, Stdio}; 12 | use std::{env, fs}; 13 | use std::{ffi::CStr, path::PathBuf}; 14 | 15 | use std::ffi::CString; 16 | #[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] 17 | use sysctl::SysctlError; 18 | 19 | #[cfg(any(target_os = "linux", target_os = "macos", target_os = "android"))] 20 | impl From for ReadoutError { 21 | fn from(e: SysctlError) -> Self { 22 | ReadoutError::Other(format!("Could not access sysctl: {e:?}")) 23 | } 24 | } 25 | 26 | impl From for ReadoutError { 27 | fn from(e: Error) -> Self { 28 | ReadoutError::Other(e.to_string()) 29 | } 30 | } 31 | 32 | #[cfg(not(any(target_os = "freebsd", target_os = "macos", target_os = "windows")))] 33 | pub(crate) fn uptime() -> Result { 34 | let uptime_buf = fs::read_to_string("/proc/uptime")?; 35 | let uptime_str = uptime_buf.split_whitespace().next().unwrap(); 36 | let uptime_val = uptime_str.parse::(); 37 | 38 | match uptime_val { 39 | Ok(s) => Ok(s as usize), 40 | Err(e) => Err(ReadoutError::Other(format!( 41 | "Could not convert '{uptime_str}' to a digit: {e:?}", 42 | ))), 43 | } 44 | } 45 | 46 | #[cfg(not(any( 47 | feature = "openwrt", 48 | target_os = "android", 49 | target_os = "macos", 50 | target_os = "windows" 51 | )))] 52 | pub(crate) fn desktop_environment() -> Result { 53 | let desktop_env = env::var("XDG_CURRENT_DESKTOP").or_else(|_| env::var("DESKTOP_SESSION")); 54 | match desktop_env { 55 | Ok(de) => { 56 | if de.to_lowercase() == "xinitrc" { 57 | return Err(ReadoutError::Other(String::from( 58 | "You appear to be only running a window manager.", 59 | ))); 60 | } 61 | 62 | Ok(crate::extra::ucfirst(de)) 63 | } 64 | Err(_) => Err(ReadoutError::Other(String::from( 65 | "You appear to be only running a window manager.", 66 | ))), 67 | } 68 | } 69 | 70 | #[cfg(not(any( 71 | feature = "openwrt", 72 | target_os = "android", 73 | target_os = "macos", 74 | target_os = "windows" 75 | )))] 76 | pub(crate) fn session() -> Result { 77 | match env::var("XDG_SESSION_TYPE") { 78 | Ok(s) => Ok(crate::extra::ucfirst(s)), 79 | Err(_) => Err(ReadoutError::Other(String::from( 80 | "No graphical session detected.", 81 | ))), 82 | } 83 | } 84 | 85 | #[cfg(all(target_os = "linux", not(feature = "openwrt")))] 86 | pub(crate) fn window_manager() -> Result { 87 | use crate::winman::*; 88 | 89 | match session()?.as_str() { 90 | "Wayland" => detect_wayland_window_manager(), 91 | "X11" => detect_xorg_window_manager(), 92 | _ => Err(ReadoutError::MetricNotAvailable), 93 | } 94 | } 95 | 96 | #[cfg(any(target_os = "netbsd", target_os = "freebsd"))] 97 | pub(crate) fn resolution() -> Result { 98 | use x11rb::connection::Connection; 99 | 100 | let mut resolution: Vec = vec![]; 101 | if let Ok(conn) = x11rb::connect(None) { 102 | let screens = &conn.0.setup().roots; 103 | for s in screens { 104 | let width = s.width_in_pixels; 105 | let height = s.height_in_pixels; 106 | resolution.push(width.to_string() + "x" + &height.to_string()) 107 | } 108 | 109 | return Ok(resolution.join(", ")); 110 | } 111 | 112 | Err(ReadoutError::Warning(String::from( 113 | "Could not open a connection to the X11 server.", 114 | ))) 115 | } 116 | 117 | #[cfg(target_family = "unix")] 118 | fn get_passwd_struct() -> Result<*mut libc::passwd, ReadoutError> { 119 | let uid: libc::uid_t = unsafe { libc::geteuid() }; 120 | 121 | // Do not call free on passwd pointer according to man page. 122 | let passwd = unsafe { libc::getpwuid(uid) }; 123 | 124 | if !passwd.is_null() { 125 | return Ok(passwd); 126 | } 127 | 128 | Err(ReadoutError::Other(String::from( 129 | "Unable to read account information.", 130 | ))) 131 | } 132 | 133 | #[cfg(target_family = "unix")] 134 | pub(crate) fn username() -> Result { 135 | let passwd = get_passwd_struct()?; 136 | 137 | let name = unsafe { CStr::from_ptr((*passwd).pw_name) }; 138 | if let Ok(str) = name.to_str() { 139 | return Ok(String::from(str)); 140 | } 141 | 142 | Err(ReadoutError::Other(String::from( 143 | "Unable to read username for the current UID.", 144 | ))) 145 | } 146 | 147 | #[cfg(target_family = "unix")] 148 | pub(crate) fn shell(shorthand: ShellFormat, kind: ShellKind) -> Result { 149 | match kind { 150 | ShellKind::Default => { 151 | let passwd = get_passwd_struct()?; 152 | let shell_name = unsafe { CStr::from_ptr((*passwd).pw_shell) }; 153 | 154 | if let Ok(str) = shell_name.to_str() { 155 | let path = String::from(str); 156 | 157 | match shorthand { 158 | ShellFormat::Relative => { 159 | let path = Path::new(&path); 160 | if let Some(relative) = path.file_name() { 161 | if let Some(shell) = relative.to_str() { 162 | return Ok(shell.to_owned()); 163 | } 164 | } 165 | } 166 | _ => { 167 | return Ok(path); 168 | } 169 | } 170 | } 171 | 172 | Err(ReadoutError::Other(String::from( 173 | "Unable to read default shell for the current UID.", 174 | ))) 175 | } 176 | ShellKind::Current => { 177 | if cfg!(target_os = "macos") { 178 | Err(ReadoutError::Other(String::from( 179 | "Retrieving the current shell is not supported on macOS.", 180 | ))) 181 | } else { 182 | let path = PathBuf::from("/proc") 183 | .join(unsafe { libc::getppid() }.to_string()) 184 | .join("comm"); 185 | 186 | if let Ok(shell) = read_to_string(path) { 187 | return Ok(shell); 188 | } 189 | 190 | Err(ReadoutError::Other(String::from( 191 | "Unable to read current shell.", 192 | ))) 193 | } 194 | } 195 | } 196 | } 197 | 198 | #[cfg(not(any(target_os = "macos", target_os = "windows")))] 199 | pub(crate) fn cpu_model_name() -> String { 200 | use std::io::{BufRead, BufReader}; 201 | let file = fs::File::open("/proc/cpuinfo"); 202 | match file { 203 | Ok(content) => { 204 | let reader = BufReader::new(content); 205 | for line in reader.lines().map_while(Result::ok) { 206 | if line.starts_with("model name") { 207 | return line 208 | .replace("model name", "") 209 | .replace(':', "") 210 | .trim() 211 | .to_string(); 212 | } 213 | } 214 | String::new() 215 | } 216 | Err(_e) => String::new(), 217 | } 218 | } 219 | 220 | #[cfg(any(target_os = "freebsd", target_os = "macos", target_os = "netbsd"))] 221 | pub(crate) fn cpu_usage() -> Result { 222 | let nelem: i32 = 1; 223 | let mut value: f64 = 0.0; 224 | let value_ptr: *mut f64 = &mut value; 225 | let cpu_load = unsafe { libc::getloadavg(value_ptr, nelem) }; 226 | if cpu_load != -1 { 227 | if let Ok(logical_cores) = cpu_cores() { 228 | return Ok((value as f64 / logical_cores as f64 * 100.0).round() as usize); 229 | } 230 | } 231 | Err(ReadoutError::Other(format!( 232 | "getloadavg failed with return code: {cpu_load}" 233 | ))) 234 | } 235 | 236 | #[cfg(target_family = "unix")] 237 | pub(crate) fn cpu_cores() -> Result { 238 | Ok(num_cpus::get()) 239 | } 240 | 241 | #[cfg(target_family = "unix")] 242 | pub(crate) fn cpu_physical_cores() -> Result { 243 | Ok(num_cpus::get_physical()) 244 | } 245 | 246 | #[cfg(not(any(target_os = "netbsd", target_os = "windows")))] 247 | pub(crate) fn disk_space(path: &Path) -> Result<(u64, u64), ReadoutError> { 248 | use std::os::unix::ffi::OsStrExt; 249 | 250 | if !path.is_dir() || !path.is_absolute() { 251 | return Err(ReadoutError::Other(format!( 252 | "The provided path is not valid: {:?}", 253 | path 254 | ))); 255 | } 256 | 257 | let mut s: std::mem::MaybeUninit = std::mem::MaybeUninit::uninit(); 258 | let path = CString::new(path.as_os_str().as_bytes()) 259 | .expect("Could not create C string for disk usage path."); 260 | 261 | if unsafe { libc::statfs(path.as_ptr(), s.as_mut_ptr()) } == 0 { 262 | #[cfg(target_pointer_width = "32")] 263 | type UInt = u32; 264 | #[cfg(target_pointer_width = "64")] 265 | type UInt = u64; 266 | 267 | let stats: libc::statfs = unsafe { s.assume_init() }; 268 | 269 | let disk_size = stats.f_blocks as UInt * stats.f_bsize as UInt; 270 | let free = stats.f_bavail as UInt * stats.f_bsize as UInt; 271 | 272 | let used_byte = disk_size - free; 273 | let disk_size_byte = disk_size; 274 | 275 | #[cfg(target_pointer_width = "32")] 276 | return Ok((used_byte.into(), disk_size_byte.into())); 277 | #[cfg(target_pointer_width = "64")] 278 | return Ok((used_byte, disk_size_byte)); 279 | } 280 | 281 | Err(ReadoutError::Other(String::from( 282 | "Error while trying to get statfs structure.", 283 | ))) 284 | } 285 | 286 | /// Obtain the value of a specified field from `/proc/meminfo` needed to calculate memory usage 287 | #[cfg(not(any(target_os = "macos", target_os = "windows")))] 288 | pub(crate) fn get_meminfo_value(value: &str) -> u64 { 289 | use std::io::{BufRead, BufReader}; 290 | let file = fs::File::open("/proc/meminfo"); 291 | match file { 292 | Ok(content) => { 293 | let reader = BufReader::new(content); 294 | for line in reader.lines().map_while(Result::ok) { 295 | if line.starts_with(value) { 296 | let s_mem_kb: String = line.chars().filter(|c| c.is_ascii_digit()).collect(); 297 | return s_mem_kb.parse::().unwrap_or(0); 298 | } 299 | } 300 | 0 301 | } 302 | Err(_e) => 0, 303 | } 304 | } 305 | 306 | #[cfg(not(target_os = "windows"))] 307 | pub(crate) fn logical_address(interface: Option<&str>) -> Result { 308 | if let Some(ifname) = interface { 309 | if let Some(addr) = if_addrs::get_if_addrs()?.into_iter().find_map(|i| { 310 | if i.name.ne(ifname) { 311 | return None; 312 | } 313 | 314 | if i.addr.is_loopback() { 315 | return None; 316 | } 317 | 318 | if let if_addrs::IfAddr::V4(v4_addr) = i.addr { 319 | return Some(v4_addr); 320 | } 321 | 322 | None 323 | }) { 324 | return Ok(addr.ip.to_string()); 325 | }; 326 | } 327 | Err(ReadoutError::Other(String::from( 328 | "Unable to get local IPv4 address.", 329 | ))) 330 | } 331 | 332 | pub(crate) fn count_cargo() -> Option { 333 | let bin = home::cargo_home().ok()?.join("bin"); 334 | let read_dir = read_dir(bin).ok()?; 335 | 336 | match read_dir.count() { 337 | 0 => None, 338 | pkgs => Some(pkgs), 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /src/openwrt/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unnecessary_cast)] 2 | mod sysinfo_ffi; 3 | 4 | use crate::shared; 5 | use crate::traits::*; 6 | use std::fs; 7 | use std::io::{BufRead, BufReader}; 8 | use std::path::Path; 9 | use sysctl::{Ctl, Sysctl}; 10 | use sysinfo_ffi::sysinfo; 11 | 12 | pub struct OpenWrtBatteryReadout; 13 | 14 | pub struct OpenWrtKernelReadout { 15 | os_release_ctl: Option, 16 | os_type_ctl: Option, 17 | } 18 | 19 | pub struct OpenWrtGeneralReadout { 20 | hostname_ctl: Option, 21 | sysinfo: sysinfo, 22 | } 23 | 24 | pub struct OpenWrtMemoryReadout { 25 | sysinfo: sysinfo, 26 | } 27 | 28 | pub struct OpenWrtProductReadout; 29 | pub struct OpenWrtPackageReadout; 30 | pub struct OpenWrtNetworkReadout; 31 | 32 | impl BatteryReadout for OpenWrtBatteryReadout { 33 | fn new() -> Self { 34 | OpenWrtBatteryReadout 35 | } 36 | 37 | fn percentage(&self) -> Result { 38 | Err(ReadoutError::NotImplemented) 39 | } 40 | 41 | fn status(&self) -> Result { 42 | Err(ReadoutError::NotImplemented) 43 | } 44 | 45 | fn health(&self) -> Result { 46 | Err(ReadoutError::NotImplemented) 47 | } 48 | } 49 | 50 | impl KernelReadout for OpenWrtKernelReadout { 51 | fn new() -> Self { 52 | OpenWrtKernelReadout { 53 | os_release_ctl: Ctl::new("kernel.osrelease").ok(), 54 | os_type_ctl: Ctl::new("kernel.ostype").ok(), 55 | } 56 | } 57 | 58 | fn os_release(&self) -> Result { 59 | Ok(self 60 | .os_release_ctl 61 | .as_ref() 62 | .ok_or(ReadoutError::MetricNotAvailable)? 63 | .value_string()?) 64 | } 65 | 66 | fn os_type(&self) -> Result { 67 | Ok(self 68 | .os_type_ctl 69 | .as_ref() 70 | .ok_or(ReadoutError::MetricNotAvailable)? 71 | .value_string()?) 72 | } 73 | } 74 | 75 | impl GeneralReadout for OpenWrtGeneralReadout { 76 | fn new() -> Self { 77 | OpenWrtGeneralReadout { 78 | hostname_ctl: Ctl::new("kernel.hostname").ok(), 79 | sysinfo: sysinfo::new(), 80 | } 81 | } 82 | 83 | fn backlight(&self) -> Result { 84 | Err(ReadoutError::NotImplemented) 85 | } 86 | 87 | fn resolution(&self) -> Result { 88 | Err(ReadoutError::NotImplemented) 89 | } 90 | 91 | fn machine(&self) -> Result { 92 | use std::io::{BufRead, BufReader}; 93 | let file = fs::File::open("/proc/cpuinfo"); 94 | if let Ok(content) = file { 95 | let reader = BufReader::new(content); 96 | for line in reader.lines().map_while(Result::ok) { 97 | if line.starts_with("machine") { 98 | return Ok(line 99 | .replace("machine", "") 100 | .replace(':', "") 101 | .trim() 102 | .to_string()); 103 | } 104 | } 105 | } 106 | 107 | Err(ReadoutError::Other(String::from( 108 | "Machine information not available in /proc/cpuinfo", 109 | ))) 110 | } 111 | 112 | fn username(&self) -> Result { 113 | shared::username() 114 | } 115 | 116 | fn hostname(&self) -> Result { 117 | Ok(self 118 | .hostname_ctl 119 | .as_ref() 120 | .ok_or(ReadoutError::MetricNotAvailable)? 121 | .value_string()?) 122 | } 123 | 124 | fn distribution(&self) -> Result { 125 | use os_release::OsRelease; 126 | let content = OsRelease::new()?; 127 | if !content.version_id.is_empty() { 128 | return Ok(format!("{} {}", content.name, content.version_id)); 129 | } 130 | 131 | Ok(content.name) 132 | } 133 | 134 | fn desktop_environment(&self) -> Result { 135 | Err(ReadoutError::NotImplemented) 136 | } 137 | 138 | fn session(&self) -> Result { 139 | Err(ReadoutError::NotImplemented) 140 | } 141 | 142 | fn window_manager(&self) -> Result { 143 | Err(ReadoutError::NotImplemented) 144 | } 145 | 146 | fn terminal(&self) -> Result { 147 | Err(ReadoutError::NotImplemented) 148 | } 149 | 150 | fn shell(&self, format: ShellFormat, kind: ShellKind) -> Result { 151 | shared::shell(format, kind) 152 | } 153 | 154 | fn cpu_model_name(&self) -> Result { 155 | let file = fs::File::open("/proc/cpuinfo"); 156 | if let Ok(content) = file { 157 | let reader = BufReader::new(content); 158 | for line in reader.lines().map_while(Result::ok) { 159 | if line.starts_with("cpu model") { 160 | return Ok(line 161 | .replace("cpu model", "") 162 | .replace(':', "") 163 | .trim() 164 | .to_string()); 165 | } 166 | } 167 | } 168 | 169 | Err(ReadoutError::Other(String::from( 170 | "Cannot read model from /proc/cpuinfo", 171 | ))) 172 | } 173 | 174 | fn cpu_cores(&self) -> Result { 175 | shared::cpu_cores() 176 | } 177 | 178 | fn cpu_physical_cores(&self) -> Result { 179 | shared::cpu_physical_cores() 180 | } 181 | 182 | fn cpu_usage(&self) -> Result { 183 | let mut info = self.sysinfo; 184 | let info_ptr: *mut sysinfo = &mut info; 185 | let ret = unsafe { sysinfo(info_ptr) }; 186 | if ret != -1 { 187 | let f_load = 1f64 / (1 << libc::SI_LOAD_SHIFT) as f64; 188 | let cpu_usage = info.loads[0] as f64 * f_load; 189 | let cpu_usage_u = (cpu_usage / num_cpus::get() as f64 * 100.0).round() as usize; 190 | Ok(cpu_usage_u as usize) 191 | } else { 192 | Err(ReadoutError::Other(String::from( 193 | "sysinfo struct returned an error.", 194 | ))) 195 | } 196 | } 197 | 198 | fn uptime(&self) -> Result { 199 | let mut info = self.sysinfo; 200 | let info_ptr: *mut sysinfo = &mut info; 201 | let ret = unsafe { sysinfo(info_ptr) }; 202 | if ret != -1 { 203 | Ok(info.uptime as usize) 204 | } else { 205 | Err(ReadoutError::Other(String::from( 206 | "sysinfo struct returned an error.", 207 | ))) 208 | } 209 | } 210 | 211 | fn os_name(&self) -> Result { 212 | Err(ReadoutError::NotImplemented) 213 | } 214 | 215 | fn disk_space(&self, path: &Path) -> Result<(u64, u64), ReadoutError> { 216 | shared::disk_space(path) 217 | } 218 | 219 | fn gpus(&self) -> Result, ReadoutError> { 220 | Err(ReadoutError::NotImplemented) 221 | } 222 | } 223 | 224 | impl MemoryReadout for OpenWrtMemoryReadout { 225 | fn new() -> Self { 226 | OpenWrtMemoryReadout { 227 | sysinfo: sysinfo::new(), 228 | } 229 | } 230 | 231 | fn total(&self) -> Result { 232 | let mut info = self.sysinfo; 233 | let info_ptr: *mut sysinfo = &mut info; 234 | let ret = unsafe { sysinfo(info_ptr) }; 235 | if ret != -1 { 236 | Ok(info.totalram as u64 * info.mem_unit as u64 / 1024) 237 | } else { 238 | Err(ReadoutError::Other(String::from( 239 | "sysinfo struct returned an error.", 240 | ))) 241 | } 242 | } 243 | 244 | fn free(&self) -> Result { 245 | let mut info = self.sysinfo; 246 | let info_ptr: *mut sysinfo = &mut info; 247 | let ret = unsafe { sysinfo(info_ptr) }; 248 | if ret != -1 { 249 | Ok(info.freeram as u64 * info.mem_unit as u64 / 1024) 250 | } else { 251 | Err(ReadoutError::Other(String::from( 252 | "sysinfo struct returned an error.", 253 | ))) 254 | } 255 | } 256 | 257 | fn buffers(&self) -> Result { 258 | let mut info = self.sysinfo; 259 | let info_ptr: *mut sysinfo = &mut info; 260 | let ret = unsafe { sysinfo(info_ptr) }; 261 | if ret != -1 { 262 | Ok(info.bufferram as u64 * info.mem_unit as u64 / 1024) 263 | } else { 264 | Err(ReadoutError::Other(String::from( 265 | "Failed to get system statistics", 266 | ))) 267 | } 268 | } 269 | 270 | fn cached(&self) -> Result { 271 | Ok(shared::get_meminfo_value("Cached")) 272 | } 273 | 274 | fn reclaimable(&self) -> Result { 275 | Ok(shared::get_meminfo_value("SReclaimable")) 276 | } 277 | 278 | fn used(&self) -> Result { 279 | let total = self.total().unwrap(); 280 | let free = self.free().unwrap(); 281 | let cached = self.cached().unwrap(); 282 | let reclaimable = self.reclaimable().unwrap(); 283 | let buffers = self.buffers().unwrap(); 284 | 285 | if reclaimable != 0 { 286 | return Ok(total - free - cached - reclaimable - buffers); 287 | } 288 | 289 | Ok(total - free - cached - buffers) 290 | } 291 | 292 | fn swap_total(&self) -> Result { 293 | Err(ReadoutError::NotImplemented) 294 | } 295 | 296 | fn swap_free(&self) -> Result { 297 | Err(ReadoutError::NotImplemented) 298 | } 299 | 300 | fn swap_used(&self) -> Result { 301 | Err(ReadoutError::NotImplemented) 302 | } 303 | } 304 | 305 | impl PackageReadout for OpenWrtPackageReadout { 306 | fn new() -> Self { 307 | OpenWrtPackageReadout 308 | } 309 | 310 | fn count_pkgs(&self) -> Vec<(PackageManager, usize)> { 311 | let mut packages = Vec::new(); 312 | 313 | if let Some(c) = OpenWrtPackageReadout::count_opkg() { 314 | packages.push((PackageManager::Opkg, c)); 315 | } 316 | 317 | packages 318 | } 319 | } 320 | 321 | impl OpenWrtPackageReadout { 322 | /// Returns the number of installed packages for systems 323 | /// that utilize `opkg` as their package manager. \ 324 | /// Including but not limited to: 325 | /// - [OpenWrt](https://openwrt.org) 326 | fn count_opkg() -> Option { 327 | let mut count: usize = 0; 328 | let file = fs::File::open("/usr/lib/opkg/status"); 329 | if let Ok(content) = file { 330 | let reader = BufReader::new(content); 331 | for line in reader.lines().map_while(Result::ok) { 332 | if line.starts_with("Package:") { 333 | count += 1 334 | } 335 | } 336 | 337 | return Some(count); 338 | } 339 | 340 | None 341 | } 342 | } 343 | 344 | impl NetworkReadout for OpenWrtNetworkReadout { 345 | fn new() -> Self { 346 | OpenWrtNetworkReadout 347 | } 348 | 349 | fn tx_bytes(&self, _: Option<&str>) -> Result { 350 | Err(ReadoutError::NotImplemented) 351 | } 352 | 353 | fn tx_packets(&self, _: Option<&str>) -> Result { 354 | Err(ReadoutError::NotImplemented) 355 | } 356 | 357 | fn rx_bytes(&self, _: Option<&str>) -> Result { 358 | Err(ReadoutError::NotImplemented) 359 | } 360 | 361 | fn rx_packets(&self, _: Option<&str>) -> Result { 362 | Err(ReadoutError::NotImplemented) 363 | } 364 | 365 | fn logical_address(&self, interface: Option<&str>) -> Result { 366 | shared::logical_address(interface) 367 | } 368 | 369 | fn physical_address(&self, _: Option<&str>) -> Result { 370 | Err(ReadoutError::NotImplemented) 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/freebsd/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unnecessary_cast)] 2 | use crate::extra; 3 | use crate::shared; 4 | use crate::traits::*; 5 | use crate::winman; 6 | use std::fs; 7 | use std::path::{Path, PathBuf}; 8 | use sysctl::{Ctl, Sysctl}; 9 | 10 | impl From for ReadoutError { 11 | fn from(e: sqlite::Error) -> Self { 12 | ReadoutError::Other(e.to_string()) 13 | } 14 | } 15 | 16 | pub struct FreeBSDBatteryReadout { 17 | battery_state_ctl: Option, 18 | battery_life_ctl: Option, 19 | } 20 | 21 | pub struct FreeBSDKernelReadout { 22 | os_release_ctl: Option, 23 | os_type_ctl: Option, 24 | } 25 | 26 | pub struct FreeBSDGeneralReadout { 27 | hostname_ctl: Option, 28 | model_ctl: Option, 29 | } 30 | 31 | pub struct FreeBSDMemoryReadout { 32 | // available memory 33 | physmem_ctl: Option, 34 | // used memory 35 | usermem_ctl: Option, 36 | } 37 | 38 | pub struct FreeBSDProductReadout; 39 | pub struct FreeBSDPackageReadout; 40 | pub struct FreeBSDNetworkReadout; 41 | 42 | impl BatteryReadout for FreeBSDBatteryReadout { 43 | fn new() -> Self { 44 | FreeBSDBatteryReadout { 45 | battery_state_ctl: Ctl::new("hw.acpi.battery.state").ok(), 46 | battery_life_ctl: Ctl::new("hw.acpi.battery.life").ok(), 47 | } 48 | } 49 | 50 | fn percentage(&self) -> Result { 51 | if let Some(ctl) = &self.battery_life_ctl { 52 | if let Ok(val) = ctl.value_string() { 53 | if let Ok(to_int) = val.parse::() { 54 | return Ok(to_int); 55 | } 56 | } 57 | } 58 | 59 | Err(ReadoutError::MetricNotAvailable) 60 | } 61 | 62 | fn status(&self) -> Result { 63 | if let Some(ctl) = &self.battery_state_ctl { 64 | if let Ok(val) = ctl.value_string() { 65 | if let Ok(to_int) = val.parse::() { 66 | match to_int { 67 | // https://lists.freebsd.org/pipermail/freebsd-acpi/2019-October/009753.html 68 | 1 => return Ok(BatteryState::Discharging), 69 | 2 => return Ok(BatteryState::Charging), 70 | _ => { 71 | return Err(ReadoutError::Other( 72 | "An unsupported battery state was reported.".to_string(), 73 | )) 74 | } 75 | }; 76 | } 77 | } 78 | } 79 | 80 | Err(ReadoutError::MetricNotAvailable) 81 | } 82 | 83 | fn health(&self) -> Result { 84 | Err(ReadoutError::NotImplemented) 85 | } 86 | } 87 | 88 | impl KernelReadout for FreeBSDKernelReadout { 89 | fn new() -> Self { 90 | FreeBSDKernelReadout { 91 | os_release_ctl: Ctl::new("kern.osrelease").ok(), 92 | os_type_ctl: Ctl::new("kern.ostype").ok(), 93 | } 94 | } 95 | 96 | fn os_release(&self) -> Result { 97 | self.os_release_ctl 98 | .as_ref() 99 | .ok_or(ReadoutError::MetricNotAvailable)? 100 | .value_string() 101 | .map_err(|e| ReadoutError::Other(e.to_string())) 102 | } 103 | 104 | fn os_type(&self) -> Result { 105 | self.os_type_ctl 106 | .as_ref() 107 | .ok_or(ReadoutError::MetricNotAvailable)? 108 | .value_string() 109 | .map_err(|e| ReadoutError::Other(e.to_string())) 110 | } 111 | 112 | fn pretty_kernel(&self) -> Result { 113 | Err(ReadoutError::MetricNotAvailable) 114 | } 115 | } 116 | 117 | impl GeneralReadout for FreeBSDGeneralReadout { 118 | fn new() -> Self { 119 | FreeBSDGeneralReadout { 120 | hostname_ctl: Ctl::new("kern.hostname").ok(), 121 | model_ctl: Ctl::new("hw.model").ok(), 122 | } 123 | } 124 | 125 | fn resolution(&self) -> Result { 126 | shared::resolution() 127 | } 128 | 129 | fn backlight(&self) -> Result { 130 | Err(ReadoutError::MetricNotAvailable) 131 | } 132 | 133 | fn machine(&self) -> Result { 134 | Err(ReadoutError::MetricNotAvailable) 135 | } 136 | 137 | fn username(&self) -> Result { 138 | shared::username() 139 | } 140 | 141 | fn hostname(&self) -> Result { 142 | self.hostname_ctl 143 | .as_ref() 144 | .ok_or(ReadoutError::MetricNotAvailable)? 145 | .value_string() 146 | .map_err(|e| ReadoutError::Other(e.to_string())) 147 | } 148 | 149 | fn distribution(&self) -> Result { 150 | Err(ReadoutError::MetricNotAvailable) 151 | } 152 | 153 | fn desktop_environment(&self) -> Result { 154 | shared::desktop_environment() 155 | } 156 | 157 | fn session(&self) -> Result { 158 | shared::session() 159 | } 160 | 161 | fn window_manager(&self) -> Result { 162 | winman::detect_xorg_window_manager() 163 | } 164 | 165 | fn terminal(&self) -> Result { 166 | // This function returns the PPID of a given PID: 167 | // - The file used to extract this data: /proc//status 168 | // - The format of the file is: command_name command_pid command_ppid ... 169 | fn get_parent(pid: i32) -> i32 { 170 | let process_path = PathBuf::from("/proc").join(pid.to_string()).join("status"); 171 | if let Ok(content) = fs::read_to_string(process_path) { 172 | if let Some(val) = content.split_whitespace().nth(2) { 173 | if let Ok(c) = val.parse::() { 174 | return c; 175 | } 176 | } 177 | 178 | return -1; 179 | } 180 | 181 | -1 182 | } 183 | 184 | // This function returns the name associated with a given PPID 185 | fn terminal_name() -> String { 186 | let mut terminal_pid = get_parent(unsafe { libc::getppid() }); 187 | 188 | let path = PathBuf::from("/proc") 189 | .join(terminal_pid.to_string()) 190 | .join("status"); 191 | 192 | // The below loop will traverse /proc to find the 193 | // terminal inside of which the user is operating 194 | if let Ok(mut terminal_name) = fs::read_to_string(path) { 195 | terminal_name = terminal_name.split_whitespace().next().unwrap().to_owned(); 196 | 197 | // Any command_name we find that matches 198 | // one of the elements in common_shells() 199 | // is effectively ignored 200 | while extra::common_shells().contains(&terminal_name.as_str()) { 201 | let ppid = get_parent(terminal_pid); 202 | terminal_pid = ppid; 203 | 204 | let path = PathBuf::from("/proc") 205 | .join(terminal_pid.to_string()) 206 | .join("status"); 207 | 208 | if let Ok(status) = fs::read_to_string(path) { 209 | if let Some(name) = status.split_whitespace().next() { 210 | terminal_name = name.to_string(); 211 | } 212 | } 213 | } 214 | 215 | return terminal_name; 216 | } 217 | 218 | String::new() 219 | } 220 | 221 | let terminal = terminal_name(); 222 | 223 | if terminal.is_empty() { 224 | return Err(ReadoutError::Other( 225 | "Could not to fetch terminal.".to_string(), 226 | )); 227 | } 228 | 229 | Ok(terminal) 230 | } 231 | 232 | fn shell(&self, shorthand: ShellFormat, kind: ShellKind) -> Result { 233 | shared::shell(shorthand, kind) 234 | } 235 | 236 | fn cpu_model_name(&self) -> Result { 237 | self.model_ctl 238 | .as_ref() 239 | .ok_or(ReadoutError::MetricNotAvailable)? 240 | .value_string() 241 | .map_err(|e| ReadoutError::Other(e.to_string())) 242 | } 243 | 244 | fn cpu_cores(&self) -> Result { 245 | shared::cpu_cores() 246 | } 247 | 248 | fn cpu_physical_cores(&self) -> Result { 249 | shared::cpu_physical_cores() 250 | } 251 | 252 | fn cpu_usage(&self) -> Result { 253 | shared::cpu_usage() 254 | } 255 | 256 | fn uptime(&self) -> Result { 257 | let ctl = match sysctl::Ctl::new("kern.boottime") { 258 | Ok(ctl) => ctl, 259 | Err(_) => { 260 | return Err(ReadoutError::Other( 261 | "Could not get sysctl: kern.boottime".to_string(), 262 | )); 263 | } 264 | }; 265 | let boot_time = match ctl.value_as::() { 266 | Ok(boot_time) => boot_time, 267 | Err(_) => { 268 | return Err(ReadoutError::Other( 269 | "Could not parse sysctl output".to_string(), 270 | )); 271 | } 272 | }; 273 | 274 | match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { 275 | Ok(unix_epoch) => Ok(unix_epoch.as_secs() as usize - boot_time.tv_sec as usize), 276 | Err(_) => Err(ReadoutError::MetricNotAvailable), 277 | } 278 | } 279 | 280 | fn os_name(&self) -> Result { 281 | let kernel_readout = FreeBSDKernelReadout::new(); 282 | 283 | let os_type = kernel_readout.os_type()?; 284 | let os_release = kernel_readout.os_release()?; 285 | 286 | if !(os_type.is_empty() || os_release.is_empty()) { 287 | return Ok(format!("{os_type} {os_release}")); 288 | } 289 | 290 | Err(ReadoutError::MetricNotAvailable) 291 | } 292 | 293 | fn disk_space(&self, path: &Path) -> Result<(u64, u64), ReadoutError> { 294 | shared::disk_space(path) 295 | } 296 | 297 | fn gpus(&self) -> Result, ReadoutError> { 298 | Err(ReadoutError::NotImplemented) 299 | } 300 | } 301 | 302 | impl MemoryReadout for FreeBSDMemoryReadout { 303 | fn new() -> Self { 304 | FreeBSDMemoryReadout { 305 | physmem_ctl: Ctl::new("hw.physmem").ok(), 306 | usermem_ctl: Ctl::new("hw.usermem").ok(), 307 | } 308 | } 309 | 310 | fn total(&self) -> Result { 311 | Ok(self 312 | .physmem_ctl 313 | .as_ref() 314 | .ok_or(ReadoutError::MetricNotAvailable)? 315 | .value_string() 316 | .unwrap() 317 | .parse::() 318 | .unwrap() 319 | / 1024) 320 | } 321 | 322 | fn free(&self) -> Result { 323 | Ok(self 324 | .usermem_ctl 325 | .as_ref() 326 | .ok_or(ReadoutError::MetricNotAvailable)? 327 | .value_string() 328 | .unwrap() 329 | .parse::() 330 | .unwrap() 331 | / 1024) 332 | } 333 | 334 | fn buffers(&self) -> Result { 335 | Err(ReadoutError::NotImplemented) 336 | } 337 | 338 | fn cached(&self) -> Result { 339 | Err(ReadoutError::NotImplemented) 340 | } 341 | 342 | fn reclaimable(&self) -> Result { 343 | Err(ReadoutError::NotImplemented) 344 | } 345 | 346 | fn used(&self) -> Result { 347 | let total = self.total().unwrap(); 348 | let free = self.free().unwrap(); 349 | 350 | Ok(total - free) 351 | } 352 | 353 | fn swap_total(&self) -> Result { 354 | return Err(ReadoutError::NotImplemented); 355 | } 356 | 357 | fn swap_free(&self) -> Result { 358 | return Err(ReadoutError::NotImplemented); 359 | } 360 | 361 | fn swap_used(&self) -> Result { 362 | return Err(ReadoutError::NotImplemented); 363 | } 364 | } 365 | 366 | impl ProductReadout for FreeBSDProductReadout { 367 | fn new() -> Self { 368 | FreeBSDProductReadout 369 | } 370 | 371 | fn family(&self) -> Result { 372 | Err(ReadoutError::MetricNotAvailable) 373 | } 374 | 375 | fn vendor(&self) -> Result { 376 | Err(ReadoutError::MetricNotAvailable) 377 | } 378 | 379 | fn product(&self) -> Result { 380 | Err(ReadoutError::MetricNotAvailable) 381 | } 382 | } 383 | 384 | impl PackageReadout for FreeBSDPackageReadout { 385 | fn new() -> Self { 386 | FreeBSDPackageReadout 387 | } 388 | 389 | fn count_pkgs(&self) -> Vec<(PackageManager, usize)> { 390 | let mut packages = Vec::new(); 391 | 392 | if let Some(c) = FreeBSDPackageReadout::count_pkg() { 393 | packages.push((PackageManager::Pkg, c)); 394 | } 395 | 396 | if let Some(c) = FreeBSDPackageReadout::count_cargo() { 397 | packages.push((PackageManager::Cargo, c)); 398 | } 399 | 400 | packages 401 | } 402 | } 403 | 404 | impl FreeBSDPackageReadout { 405 | fn count_pkg() -> Option { 406 | let db = "/var/db/pkg/local.sqlite"; 407 | if !Path::new(db).exists() { 408 | return None; 409 | } 410 | 411 | let connection = sqlite::open(db); 412 | if let Ok(con) = connection { 413 | let statement = con.prepare("SELECT COUNT(*) FROM packages"); 414 | if let Ok(mut s) = statement { 415 | if s.next().is_ok() { 416 | return match s.read::, _>(0) { 417 | Ok(Some(count)) => Some(count as usize), 418 | _ => None, 419 | }; 420 | } 421 | } 422 | } 423 | 424 | None 425 | } 426 | 427 | fn count_cargo() -> Option { 428 | shared::count_cargo() 429 | } 430 | } 431 | 432 | impl NetworkReadout for FreeBSDNetworkReadout { 433 | fn new() -> Self { 434 | FreeBSDNetworkReadout 435 | } 436 | 437 | fn tx_bytes(&self, _: Option<&str>) -> Result { 438 | Err(ReadoutError::NotImplemented) 439 | } 440 | 441 | fn tx_packets(&self, _: Option<&str>) -> Result { 442 | Err(ReadoutError::NotImplemented) 443 | } 444 | 445 | fn rx_bytes(&self, _: Option<&str>) -> Result { 446 | Err(ReadoutError::NotImplemented) 447 | } 448 | 449 | fn rx_packets(&self, _: Option<&str>) -> Result { 450 | Err(ReadoutError::NotImplemented) 451 | } 452 | 453 | fn logical_address(&self, interface: Option<&str>) -> Result { 454 | shared::logical_address(interface) 455 | } 456 | 457 | fn physical_address(&self, _: Option<&str>) -> Result { 458 | Err(ReadoutError::NotImplemented) 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /src/android/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unnecessary_cast)] 2 | mod sysinfo_ffi; 3 | mod system_properties; 4 | 5 | use crate::extra; 6 | use crate::shared; 7 | use crate::traits::*; 8 | use itertools::Itertools; 9 | use std::ffi::{CStr, CString}; 10 | use std::fs; 11 | use std::path::{Path, PathBuf}; 12 | use std::process::{Command, Stdio}; 13 | use sysinfo_ffi::sysinfo; 14 | use system_properties::getprop; 15 | 16 | impl From for ReadoutError { 17 | fn from(e: std::str::Utf8Error) -> Self { 18 | ReadoutError::Other(e.to_string()) 19 | } 20 | } 21 | impl From for ReadoutError { 22 | fn from(e: std::num::ParseFloatError) -> Self { 23 | ReadoutError::Other(e.to_string()) 24 | } 25 | } 26 | 27 | pub struct AndroidBatteryReadout; 28 | 29 | pub struct AndroidKernelReadout { 30 | utsname: Option, 31 | } 32 | 33 | pub struct AndroidGeneralReadout { 34 | sysinfo: sysinfo, 35 | } 36 | 37 | pub struct AndroidMemoryReadout { 38 | sysinfo: sysinfo, 39 | } 40 | 41 | pub struct AndroidProductReadout; 42 | pub struct AndroidPackageReadout; 43 | pub struct AndroidNetworkReadout; 44 | 45 | impl BatteryReadout for AndroidBatteryReadout { 46 | fn new() -> Self { 47 | AndroidBatteryReadout 48 | } 49 | 50 | fn percentage(&self) -> Result { 51 | let bat_path = Path::new("/sys/class/power_supply/battery/capacity"); 52 | let percentage_text = extra::pop_newline(fs::read_to_string(bat_path)?); 53 | let percentage_parsed = percentage_text.parse::(); 54 | 55 | match percentage_parsed { 56 | Ok(p) => Ok(p), 57 | Err(e) => Err(ReadoutError::Other(format!( 58 | "Could not parse the value '{}' of {} into a \ 59 | digit: {:?}", 60 | percentage_text, 61 | bat_path.to_str().unwrap_or_default(), 62 | e 63 | ))), 64 | } 65 | } 66 | 67 | fn status(&self) -> Result { 68 | let bat_path = Path::new("/sys/class/power_supply/battery/status"); 69 | 70 | let status_text = extra::pop_newline(fs::read_to_string(bat_path)?).to_lowercase(); 71 | match &status_text[..] { 72 | "charging" => Ok(BatteryState::Charging), 73 | "discharging" | "full" => Ok(BatteryState::Discharging), 74 | s => Err(ReadoutError::Other(format!( 75 | "Got unexpected value '{}' from {}.", 76 | s, 77 | bat_path.to_str().unwrap_or_default() 78 | ))), 79 | } 80 | } 81 | 82 | fn health(&self) -> Result { 83 | Err(ReadoutError::NotImplemented) 84 | } 85 | } 86 | 87 | impl KernelReadout for AndroidKernelReadout { 88 | fn new() -> Self { 89 | let mut __utsname: libc::utsname = unsafe { std::mem::zeroed() }; 90 | let utsname: Option = if unsafe { libc::uname(&mut __utsname) } == -1 { 91 | None 92 | } else { 93 | Some(__utsname) 94 | }; 95 | 96 | AndroidKernelReadout { utsname } 97 | } 98 | 99 | fn os_release(&self) -> Result { 100 | if let Some(utsname) = self.utsname { 101 | return Ok(unsafe { CStr::from_ptr(utsname.release.as_ptr()) } 102 | .to_str() 103 | .unwrap() 104 | .to_owned()); 105 | } else { 106 | Err(ReadoutError::Other(String::from( 107 | "Failed to get os_release", 108 | ))) 109 | } 110 | } 111 | 112 | fn os_type(&self) -> Result { 113 | if let Some(utsname) = self.utsname { 114 | return Ok(unsafe { CStr::from_ptr(utsname.sysname.as_ptr()) } 115 | .to_str() 116 | .unwrap() 117 | .to_owned()); 118 | } else { 119 | Err(ReadoutError::Other(String::from("Failed to get os_type"))) 120 | } 121 | } 122 | } 123 | 124 | impl GeneralReadout for AndroidGeneralReadout { 125 | fn new() -> Self { 126 | AndroidGeneralReadout { 127 | sysinfo: sysinfo::new(), 128 | } 129 | } 130 | 131 | fn backlight(&self) -> Result { 132 | Err(ReadoutError::NotImplemented) 133 | } 134 | 135 | fn resolution(&self) -> Result { 136 | Err(ReadoutError::NotImplemented) 137 | } 138 | 139 | fn machine(&self) -> Result { 140 | let product_readout = AndroidProductReadout::new(); 141 | 142 | let family = product_readout.family()?; 143 | let vendor = product_readout.vendor()?; 144 | let product = product_readout.product()?; 145 | 146 | let new_product = format!("{vendor} {family} {product}"); 147 | 148 | if product.is_empty() || product.len() <= 15 { 149 | return Ok(new_product.split_whitespace().unique().join(" ")); 150 | } 151 | 152 | Ok(product) 153 | } 154 | 155 | fn username(&self) -> Result { 156 | shared::username() 157 | } 158 | 159 | fn hostname(&self) -> Result { 160 | let __name: *mut std::os::raw::c_char = CString::new("").unwrap().into_raw(); 161 | let __len: usize = libc::_SC_HOST_NAME_MAX as usize; 162 | let ret = unsafe { libc::gethostname(__name, __len) }; 163 | if ret == -1 { 164 | Err(ReadoutError::Other(String::from("Failed to get hostname"))) 165 | } else { 166 | Ok(unsafe { CStr::from_ptr(__name).to_string_lossy().into_owned() }) 167 | } 168 | } 169 | 170 | fn distribution(&self) -> Result { 171 | Err(ReadoutError::NotImplemented) 172 | } 173 | 174 | fn desktop_environment(&self) -> Result { 175 | Err(ReadoutError::NotImplemented) 176 | } 177 | 178 | fn session(&self) -> Result { 179 | Err(ReadoutError::NotImplemented) 180 | } 181 | 182 | fn window_manager(&self) -> Result { 183 | Err(ReadoutError::NotImplemented) 184 | } 185 | 186 | fn terminal(&self) -> Result { 187 | Err(ReadoutError::NotImplemented) 188 | } 189 | 190 | fn shell(&self, format: ShellFormat, kind: ShellKind) -> Result { 191 | if let Some(shell) = std::env::var_os("SHELL") { 192 | if let Some(relative) = PathBuf::from(shell).file_name() { 193 | if let Some(str) = relative.to_str() { 194 | return Ok(str.to_owned()); 195 | } 196 | } 197 | } 198 | 199 | shared::shell(format, kind) 200 | } 201 | 202 | fn cpu_model_name(&self) -> Result { 203 | use std::io::{BufRead, BufReader}; 204 | let file = fs::File::open("/proc/cpuinfo"); 205 | let mut model: Option = None; 206 | let mut hardware: Option = None; 207 | let mut processor: Option = None; 208 | 209 | let get_value_from_line = |input: String, option: &str| -> String { 210 | input 211 | .replace(option, "") 212 | .replace(':', "") 213 | .trim() 214 | .to_string() 215 | }; 216 | 217 | if let Ok(content) = file { 218 | let reader = BufReader::new(content); 219 | for line in reader.lines().map_while(Result::ok) { 220 | if line.starts_with("Hardware") { 221 | hardware = Some(get_value_from_line(line, "Hardware")); 222 | break; // If "Hardware" information is present, the rest is not needed. 223 | } else if line.starts_with("Processor") { 224 | processor = Some(get_value_from_line(line, "Processor")); 225 | } else if line.starts_with("model name") && model.is_none() { 226 | model = Some(get_value_from_line(line, "model name")); 227 | } 228 | } 229 | } 230 | match (hardware, model, processor) { 231 | (Some(hardware), _, _) => Ok(hardware), 232 | (_, Some(model), _) => Ok(model), 233 | (_, _, Some(processor)) => Ok(processor), 234 | (_, _, _) => Err(ReadoutError::Other(String::from( 235 | "Failed to get processor model name", 236 | ))), 237 | } 238 | } 239 | 240 | fn cpu_physical_cores(&self) -> Result { 241 | shared::cpu_physical_cores() 242 | } 243 | 244 | fn cpu_cores(&self) -> Result { 245 | shared::cpu_cores() 246 | } 247 | 248 | fn cpu_usage(&self) -> Result { 249 | let mut info = self.sysinfo; 250 | let info_ptr: *mut sysinfo = &mut info; 251 | let ret = unsafe { sysinfo(info_ptr) }; 252 | if ret != -1 { 253 | let f_load = 1f64 / (1 << libc::SI_LOAD_SHIFT) as f64; 254 | let cpu_usage = info.loads[0] as f64 * f_load; 255 | let cpu_usage_u = (cpu_usage / num_cpus::get() as f64 * 100.0).round() as usize; 256 | if cpu_usage_u != 0 { 257 | return Ok(cpu_usage_u as usize); 258 | } 259 | Err(ReadoutError::Other("Processor usage is null.".to_string())) 260 | } else { 261 | Err(ReadoutError::Other( 262 | "Failed to get system statistics".to_string(), 263 | )) 264 | } 265 | } 266 | 267 | fn uptime(&self) -> Result { 268 | let mut info = self.sysinfo; 269 | let info_ptr: *mut sysinfo = &mut info; 270 | let ret = unsafe { sysinfo(info_ptr) }; 271 | if ret != -1 { 272 | Ok(info.uptime as usize) 273 | } else { 274 | Err(ReadoutError::Other( 275 | "Failed to get system statistics".to_string(), 276 | )) 277 | } 278 | } 279 | 280 | fn os_name(&self) -> Result { 281 | match getprop("ro.build.version.release") { 282 | Some(version) => Ok("Android ".to_string() + &version), 283 | None => Err(ReadoutError::Other( 284 | "Failed to get Android version".to_string(), 285 | )), 286 | } 287 | } 288 | 289 | fn disk_space(&self, path: &Path) -> Result<(u64, u64), ReadoutError> { 290 | Err(ReadoutError::NotImplemented) 291 | } 292 | 293 | fn gpus(&self) -> Result, ReadoutError> { 294 | Err(ReadoutError::NotImplemented) 295 | } 296 | } 297 | 298 | impl MemoryReadout for AndroidMemoryReadout { 299 | fn new() -> Self { 300 | AndroidMemoryReadout { 301 | sysinfo: sysinfo::new(), 302 | } 303 | } 304 | 305 | fn total(&self) -> Result { 306 | let mut info = self.sysinfo; 307 | let info_ptr: *mut sysinfo = &mut info; 308 | let ret = unsafe { sysinfo(info_ptr) }; 309 | if ret != -1 { 310 | Ok(info.totalram * info.mem_unit as u64 / 1024) 311 | } else { 312 | Err(ReadoutError::Other( 313 | "Failed to get system statistics".to_string(), 314 | )) 315 | } 316 | } 317 | 318 | fn free(&self) -> Result { 319 | let mut info = self.sysinfo; 320 | let info_ptr: *mut sysinfo = &mut info; 321 | let ret = unsafe { sysinfo(info_ptr) }; 322 | if ret != -1 { 323 | Ok(info.freeram * info.mem_unit as u64 / 1024) 324 | } else { 325 | Err(ReadoutError::Other( 326 | "Failed to get system statistics".to_string(), 327 | )) 328 | } 329 | } 330 | 331 | fn buffers(&self) -> Result { 332 | let mut info = self.sysinfo; 333 | let info_ptr: *mut sysinfo = &mut info; 334 | let ret = unsafe { sysinfo(info_ptr) }; 335 | if ret != -1 { 336 | Ok(info.bufferram * info.mem_unit as u64 / 1024) 337 | } else { 338 | Err(ReadoutError::Other( 339 | "Failed to get system statistics".to_string(), 340 | )) 341 | } 342 | } 343 | 344 | fn cached(&self) -> Result { 345 | Ok(shared::get_meminfo_value("Cached")) 346 | } 347 | 348 | fn reclaimable(&self) -> Result { 349 | Ok(shared::get_meminfo_value("SReclaimable")) 350 | } 351 | 352 | fn used(&self) -> Result { 353 | let total = self.total().unwrap(); 354 | let free = self.free().unwrap(); 355 | let cached = self.cached().unwrap(); 356 | let reclaimable = self.reclaimable().unwrap(); 357 | let buffers = self.buffers().unwrap(); 358 | 359 | Ok(total - free - cached - reclaimable - buffers) 360 | } 361 | 362 | fn swap_total(&self) -> Result { 363 | return Err(ReadoutError::NotImplemented); 364 | } 365 | 366 | fn swap_free(&self) -> Result { 367 | return Err(ReadoutError::NotImplemented); 368 | } 369 | 370 | fn swap_used(&self) -> Result { 371 | return Err(ReadoutError::NotImplemented); 372 | } 373 | } 374 | 375 | impl ProductReadout for AndroidProductReadout { 376 | fn new() -> Self { 377 | AndroidProductReadout 378 | } 379 | 380 | fn family(&self) -> Result { 381 | getprop("ro.product.model") 382 | .ok_or_else(|| ReadoutError::Other("Failed to get device family property".to_string())) 383 | } 384 | 385 | fn vendor(&self) -> Result { 386 | getprop("ro.product.brand") 387 | .ok_or_else(|| ReadoutError::Other("Failed to get device vendor property".to_string())) 388 | } 389 | 390 | fn product(&self) -> Result { 391 | getprop("ro.build.product") 392 | .ok_or_else(|| ReadoutError::Other("Failed to get device product property".to_string())) 393 | } 394 | } 395 | 396 | impl PackageReadout for AndroidPackageReadout { 397 | fn new() -> Self { 398 | AndroidPackageReadout 399 | } 400 | 401 | /// Supports: pm, dpkg, cargo 402 | fn count_pkgs(&self) -> Vec<(PackageManager, usize)> { 403 | let mut packages = Vec::new(); 404 | // Since the target is Android we can assume that pm is available 405 | if let Some(c) = AndroidPackageReadout::count_pm() { 406 | packages.push((PackageManager::Android, c)); 407 | } 408 | 409 | if extra::which("dpkg") { 410 | if let Some(c) = AndroidPackageReadout::count_dpkg() { 411 | packages.push((PackageManager::Dpkg, c)); 412 | } 413 | } 414 | 415 | if extra::which("cargo") { 416 | if let Some(c) = AndroidPackageReadout::count_cargo() { 417 | packages.push((PackageManager::Cargo, c)); 418 | } 419 | } 420 | 421 | packages 422 | } 423 | } 424 | 425 | impl AndroidPackageReadout { 426 | /// Returns the number of installed apps for the system 427 | /// Includes all apps ( user + system ) 428 | fn count_pm() -> Option { 429 | let pm_output = Command::new("pm") 430 | .args(["list", "packages"]) 431 | .stdout(Stdio::piped()) 432 | .output() 433 | .unwrap(); 434 | 435 | extra::count_lines( 436 | String::from_utf8(pm_output.stdout) 437 | .expect("ERROR: \"pm list packages\" output was not valid UTF-8"), 438 | ) 439 | } 440 | /// Return the number of installed packages for systems 441 | /// that have `dpkg` installed. 442 | /// In android that's mainly termux. 443 | fn count_dpkg() -> Option { 444 | let prefix = match std::env::var_os("PREFIX") { 445 | None => return None, 446 | Some(prefix) => prefix, 447 | }; 448 | 449 | let dpkg_dir = Path::new(&prefix).join("var/lib/dpkg/info"); 450 | 451 | extra::get_entries(&dpkg_dir).map(|entries| { 452 | entries 453 | .iter() 454 | .filter(|x| extra::path_extension(x).unwrap_or_default() == "list") 455 | .count() 456 | }) 457 | } 458 | 459 | /// Returns the number of installed packages for systems 460 | /// that have `cargo` installed. 461 | fn count_cargo() -> Option { 462 | shared::count_cargo() 463 | } 464 | } 465 | 466 | impl NetworkReadout for AndroidNetworkReadout { 467 | fn new() -> Self { 468 | AndroidNetworkReadout 469 | } 470 | 471 | fn tx_bytes(&self, _: Option<&str>) -> Result { 472 | Err(ReadoutError::NotImplemented) 473 | } 474 | 475 | fn tx_packets(&self, _: Option<&str>) -> Result { 476 | Err(ReadoutError::NotImplemented) 477 | } 478 | 479 | fn rx_bytes(&self, _: Option<&str>) -> Result { 480 | Err(ReadoutError::NotImplemented) 481 | } 482 | 483 | fn rx_packets(&self, _: Option<&str>) -> Result { 484 | Err(ReadoutError::NotImplemented) 485 | } 486 | 487 | fn logical_address(&self, interface: Option<&str>) -> Result { 488 | shared::logical_address(interface) 489 | } 490 | 491 | fn physical_address(&self, _: Option<&str>) -> Result { 492 | Err(ReadoutError::NotImplemented) 493 | } 494 | } 495 | -------------------------------------------------------------------------------- /src/netbsd/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unnecessary_cast)] 2 | use crate::dirs; 3 | use crate::extra; 4 | use crate::shared; 5 | use crate::traits::*; 6 | use itertools::Itertools; 7 | use nix::unistd; 8 | use regex::Regex; 9 | use std::ffi::CString; 10 | use std::fs; 11 | use std::fs::read_dir; 12 | use std::path::{Path, PathBuf}; 13 | use std::process::{Command, Stdio}; 14 | 15 | pub struct NetBSDBatteryReadout; 16 | pub struct NetBSDKernelReadout; 17 | pub struct NetBSDGeneralReadout; 18 | pub struct NetBSDMemoryReadout; 19 | pub struct NetBSDProductReadout; 20 | pub struct NetBSDPackageReadout; 21 | pub struct NetBSDNetworkReadout; 22 | 23 | impl BatteryReadout for NetBSDBatteryReadout { 24 | fn new() -> Self { 25 | NetBSDBatteryReadout 26 | } 27 | 28 | fn percentage(&self) -> Result { 29 | if extra::which("envstat") { 30 | let envstat = Command::new("envstat") 31 | .args(["-s", "acpibat0:charge"]) 32 | .stdout(Stdio::piped()) 33 | .output() 34 | .expect("ERROR: failed to spawn \"envstat\" process"); 35 | 36 | let envstat_out = String::from_utf8(envstat.stdout) 37 | .expect("ERROR: \"envstat\" process stdout was not valid UTF-8"); 38 | if envstat_out.is_empty() { 39 | return Err(ReadoutError::MetricNotAvailable); 40 | } else { 41 | let re = Regex::new(r"\(([^()]*)\)").unwrap(); 42 | let caps = re.captures(&envstat_out); 43 | match caps { 44 | Some(c) => { 45 | let percentage = c 46 | .get(1) 47 | .map_or("", |m| m.as_str()) 48 | .to_string() 49 | .replace([' ', '%'], ""); 50 | let percentage_f = percentage.parse::().unwrap(); 51 | let percentage_i = percentage_f.round() as u8; 52 | return Ok(percentage_i); 53 | } 54 | None => return Err(ReadoutError::MetricNotAvailable), 55 | } 56 | } 57 | } 58 | 59 | Err(ReadoutError::MetricNotAvailable) 60 | } 61 | 62 | fn status(&self) -> Result { 63 | if extra::which("envstat") { 64 | let envstat = Command::new("envstat") 65 | .args(["-s", "acpibat0:charging"]) 66 | .stdout(Stdio::piped()) 67 | .output() 68 | .expect("ERROR: failed to spawn \"envstat\" process"); 69 | 70 | let envstat_out = String::from_utf8(envstat.stdout) 71 | .expect("ERROR: \"envstat\" process stdout was not valid UTF-8"); 72 | 73 | if envstat_out.is_empty() { 74 | return Err(ReadoutError::MetricNotAvailable); 75 | } else if envstat_out.contains("TRUE") { 76 | return Ok(BatteryState::Charging); 77 | } else { 78 | return Ok(BatteryState::Discharging); 79 | } 80 | } 81 | 82 | Err(ReadoutError::Other("envstat is not installed".to_owned())) 83 | } 84 | 85 | fn health(&self) -> Result { 86 | Err(ReadoutError::NotImplemented) 87 | } 88 | } 89 | 90 | impl KernelReadout for NetBSDKernelReadout { 91 | fn new() -> Self { 92 | NetBSDKernelReadout 93 | } 94 | 95 | fn os_release(&self) -> Result { 96 | let output = Command::new("sysctl") 97 | .args(["-n", "-b", "kern.osrelease"]) 98 | .output() 99 | .expect("ERROR: failed to fetch \"kernel.osrelease\" using \"sysctl\""); 100 | 101 | let osrelease = String::from_utf8(output.stdout) 102 | .expect("ERROR: \"sysctl\" process stdout was not valid UTF-8"); 103 | 104 | Ok(osrelease) 105 | } 106 | 107 | fn os_type(&self) -> Result { 108 | let output = Command::new("sysctl") 109 | .args(["-n", "-b", "kern.ostype"]) 110 | .output() 111 | .expect("ERROR: failed to fetch \"kernel.ostype\" using \"sysctl\""); 112 | 113 | let osrelease = String::from_utf8(output.stdout) 114 | .expect("ERROR: \"sysctl\" process stdout was not valid UTF-8"); 115 | 116 | Ok(osrelease) 117 | } 118 | 119 | fn pretty_kernel(&self) -> Result { 120 | Err(ReadoutError::Warning(String::from( 121 | "This information is provided by the OperatingSystem readout on NetBSD.", 122 | ))) 123 | } 124 | } 125 | 126 | impl GeneralReadout for NetBSDGeneralReadout { 127 | fn new() -> Self { 128 | NetBSDGeneralReadout 129 | } 130 | 131 | fn resolution(&self) -> Result { 132 | shared::resolution() 133 | } 134 | 135 | fn backlight(&self) -> Result { 136 | let output = Command::new("sysctl") 137 | .args(["-n", "hw.acpi.acpiout0.brightness"]) 138 | .output() 139 | .expect("ERROR: failed to fetch \"hw.acpi.acpiout0.brightness\" using \"sysctl\""); 140 | 141 | let backlight = String::from_utf8(output.stdout) 142 | .expect("ERROR: \"sysctl\" process stdout was not valid UTF-8"); 143 | 144 | if backlight.is_empty() { 145 | return Err(ReadoutError::Other(String::from( 146 | "Could not obtain backlight value through sysctl, is ACPIVGA driver installed?", 147 | ))); 148 | } 149 | 150 | if let Ok(val) = extra::pop_newline(backlight).parse::() { 151 | return Ok(val); 152 | } 153 | 154 | Err(ReadoutError::Other(String::from( 155 | "Could not parse the obtained backlight value.", 156 | ))) 157 | } 158 | 159 | fn machine(&self) -> Result { 160 | let product_readout = NetBSDProductReadout::new(); 161 | 162 | let family = product_readout.family()?; 163 | let vendor = product_readout.vendor()?; 164 | let product = product_readout.product()?; 165 | 166 | let new_product = 167 | format!("{vendor} {family} {product}").replace("To be filled by O.E.M.", ""); 168 | 169 | if product == new_product && product == vendor { 170 | return Ok(vendor); 171 | } 172 | 173 | Ok(new_product.split_whitespace().unique().join(" ")) 174 | } 175 | 176 | fn username(&self) -> Result { 177 | shared::username() 178 | } 179 | 180 | fn hostname(&self) -> Result { 181 | let hostname_cstr = unistd::gethostname(); 182 | match hostname_cstr { 183 | Ok(hostname_cstr) => { 184 | let hostname = hostname_cstr.to_str().unwrap_or("Unknown"); 185 | Ok(String::from(hostname)) 186 | } 187 | Err(_e) => Err(ReadoutError::Other(String::from( 188 | "Failed to retrieve hostname from 'gethostname'.", 189 | ))), 190 | } 191 | } 192 | 193 | fn distribution(&self) -> Result { 194 | Err(ReadoutError::Warning(String::from( 195 | "This information is provided by the OperatingSystem readout on NetBSD.", 196 | ))) 197 | } 198 | 199 | fn desktop_environment(&self) -> Result { 200 | shared::desktop_environment() 201 | } 202 | 203 | fn session(&self) -> Result { 204 | shared::session() 205 | } 206 | 207 | fn window_manager(&self) -> Result { 208 | crate::winman::detect_xorg_window_manager() 209 | } 210 | 211 | fn terminal(&self) -> Result { 212 | // This function returns the PPID of a given PID: 213 | // - The file used to extract this data: /proc//status 214 | // - The format of the file is: command_name command_pid command_ppid ... 215 | fn get_parent(pid: i32) -> i32 { 216 | let process_path = PathBuf::from("/proc").join(pid.to_string()).join("status"); 217 | if let Ok(content) = fs::read_to_string(process_path) { 218 | if let Some(val) = content.split_whitespace().nth(2) { 219 | if let Ok(c) = val.parse::() { 220 | return c; 221 | } 222 | } 223 | 224 | return -1; 225 | } 226 | 227 | -1 228 | } 229 | 230 | // This function returns the name associated with a given PPID 231 | fn terminal_name() -> String { 232 | let mut terminal_pid = get_parent(unsafe { libc::getppid() }); 233 | 234 | let path = PathBuf::from("/proc") 235 | .join(terminal_pid.to_string()) 236 | .join("status"); 237 | 238 | // The below loop will traverse /proc to find the 239 | // terminal inside of which the user is operating 240 | if let Ok(mut terminal_name) = fs::read_to_string(path) { 241 | terminal_name = terminal_name.split_whitespace().next().unwrap().to_owned(); 242 | 243 | // Any command_name we find that matches 244 | // one of the elements within this table 245 | // is effectively ignored 246 | while extra::common_shells().contains(&terminal_name.as_str()) { 247 | let ppid = get_parent(terminal_pid); 248 | terminal_pid = ppid; 249 | 250 | let path = PathBuf::from("/proc") 251 | .join(terminal_pid.to_string()) 252 | .join("status"); 253 | 254 | if let Ok(status) = fs::read_to_string(path) { 255 | if let Some(name) = status.split_whitespace().next() { 256 | terminal_name = name.to_string(); 257 | } 258 | } 259 | } 260 | 261 | return terminal_name; 262 | } 263 | 264 | String::new() 265 | } 266 | 267 | let terminal = terminal_name(); 268 | 269 | if terminal.is_empty() { 270 | return Err(ReadoutError::Other( 271 | "Could not to fetch terminal.".to_owned(), 272 | )); 273 | } 274 | 275 | Ok(terminal) 276 | } 277 | 278 | fn shell(&self, shorthand: ShellFormat, kind: ShellKind) -> Result { 279 | shared::shell(shorthand, kind) 280 | } 281 | 282 | fn cpu_model_name(&self) -> Result { 283 | Ok(shared::cpu_model_name()) 284 | } 285 | 286 | fn cpu_cores(&self) -> Result { 287 | shared::cpu_cores() 288 | } 289 | 290 | fn cpu_physical_cores(&self) -> Result { 291 | shared::cpu_physical_cores() 292 | } 293 | 294 | fn cpu_usage(&self) -> Result { 295 | shared::cpu_usage() 296 | } 297 | 298 | fn uptime(&self) -> Result { 299 | shared::uptime() 300 | } 301 | 302 | fn os_name(&self) -> Result { 303 | let kernel_readout = NetBSDKernelReadout::new(); 304 | 305 | let os_type = kernel_readout.os_type()?; 306 | let os_release = kernel_readout.os_release()?; 307 | 308 | if !(os_type.is_empty() || os_release.is_empty()) { 309 | return Ok(format!("{os_type} {os_release}")); 310 | } 311 | 312 | Err(ReadoutError::MetricNotAvailable) 313 | } 314 | 315 | fn disk_space(&self, path: &Path) -> Result<(u64, u64), ReadoutError> { 316 | use std::os::unix::ffi::OsStrExt; 317 | 318 | if !path.is_dir() || !path.is_absolute() { 319 | return Err(ReadoutError::Other(format!( 320 | "The provided path is not valid: {:?}", 321 | path 322 | ))); 323 | } 324 | 325 | let mut s: std::mem::MaybeUninit = std::mem::MaybeUninit::uninit(); 326 | let path = CString::new(path.as_os_str().as_bytes()) 327 | .expect("Could not create C string for disk usage path."); 328 | 329 | if unsafe { libc::statvfs(path.as_ptr(), s.as_mut_ptr()) } == 0 { 330 | let stats: libc::statvfs = unsafe { s.assume_init() }; 331 | 332 | let disk_size = stats.f_blocks * stats.f_bsize as u64; 333 | let free = stats.f_bavail * stats.f_bsize as u64; 334 | 335 | let used_byte = disk_size - free; 336 | let disk_size_byte = disk_size; 337 | 338 | return Ok((used_byte, disk_size_byte)); 339 | } 340 | 341 | Err(ReadoutError::Other(String::from( 342 | "Error while trying to get statfs structure.", 343 | ))) 344 | } 345 | 346 | fn gpus(&self) -> Result, ReadoutError> { 347 | Err(ReadoutError::NotImplemented) 348 | } 349 | } 350 | 351 | impl MemoryReadout for NetBSDMemoryReadout { 352 | fn new() -> Self { 353 | NetBSDMemoryReadout 354 | } 355 | 356 | fn total(&self) -> Result { 357 | Ok(shared::get_meminfo_value("MemTotal")) 358 | } 359 | 360 | fn free(&self) -> Result { 361 | Ok(shared::get_meminfo_value("MemFree")) 362 | } 363 | 364 | fn buffers(&self) -> Result { 365 | Err(ReadoutError::NotImplemented) 366 | } 367 | 368 | fn cached(&self) -> Result { 369 | Err(ReadoutError::NotImplemented) 370 | } 371 | 372 | fn reclaimable(&self) -> Result { 373 | Err(ReadoutError::NotImplemented) 374 | } 375 | 376 | fn used(&self) -> Result { 377 | let total = self.total().unwrap(); 378 | let free = self.free().unwrap(); 379 | 380 | Ok(total - free) 381 | } 382 | 383 | fn swap_total(&self) -> Result { 384 | return Err(ReadoutError::NotImplemented); 385 | } 386 | 387 | fn swap_free(&self) -> Result { 388 | return Err(ReadoutError::NotImplemented); 389 | } 390 | 391 | fn swap_used(&self) -> Result { 392 | return Err(ReadoutError::NotImplemented); 393 | } 394 | } 395 | 396 | impl ProductReadout for NetBSDProductReadout { 397 | fn new() -> Self { 398 | NetBSDProductReadout 399 | } 400 | 401 | fn product(&self) -> Result { 402 | let output = Command::new("sysctl") 403 | .args(["-n", "-b", "machdep.dmi.system-version"]) 404 | .output() 405 | .expect("ERROR: failed to start \"sysctl\" process"); 406 | 407 | let sysver = String::from_utf8(output.stdout) 408 | .expect("ERROR: \"sysctl\" process stdout was not valid UTF-8"); 409 | 410 | Ok(sysver) 411 | } 412 | 413 | fn vendor(&self) -> Result { 414 | let output = Command::new("sysctl") 415 | .args(["-n", "-b", "machdep.dmi.system-vendor"]) 416 | .output() 417 | .expect("ERROR: failed to start \"sysctl\" process"); 418 | 419 | let sysven = String::from_utf8(output.stdout) 420 | .expect("ERROR: \"sysctl\" process stdout was not valid UTF-8"); 421 | 422 | Ok(sysven) 423 | } 424 | 425 | fn family(&self) -> Result { 426 | let output = Command::new("sysctl") 427 | .args(["-n", "-b", "machdep.dmi.system-product"]) 428 | .output() 429 | .expect("ERROR: failed to start \"sysctl\" process"); 430 | 431 | let sysprod = String::from_utf8(output.stdout) 432 | .expect("ERROR: \"sysctl\" process stdout was not valid UTF-8"); 433 | 434 | Ok(sysprod) 435 | } 436 | } 437 | 438 | impl PackageReadout for NetBSDPackageReadout { 439 | fn new() -> Self { 440 | NetBSDPackageReadout 441 | } 442 | 443 | fn count_pkgs(&self) -> Vec<(PackageManager, usize)> { 444 | let mut packages = Vec::new(); 445 | 446 | if let Some(c) = NetBSDPackageReadout::count_pkgin() { 447 | packages.push((PackageManager::Pkgsrc, c)); 448 | } 449 | 450 | if let Some(c) = NetBSDPackageReadout::count_cargo() { 451 | packages.push((PackageManager::Cargo, c)); 452 | } 453 | 454 | packages 455 | } 456 | } 457 | 458 | impl NetBSDPackageReadout { 459 | fn count_pkgin() -> Option { 460 | if let Some(pkg_dbdir) = dirs::pkgdb_dir() { 461 | if let Ok(read_dir) = read_dir(pkg_dbdir) { 462 | return Some(read_dir.count() - 1); 463 | }; 464 | } 465 | 466 | if let Some(localbase_dir) = dirs::localbase_dir() { 467 | if let Ok(read_dir) = read_dir(localbase_dir.join("pkgdb")) { 468 | return Some(read_dir.count() - 1); 469 | } 470 | } 471 | 472 | None 473 | } 474 | 475 | fn count_cargo() -> Option { 476 | shared::count_cargo() 477 | } 478 | } 479 | 480 | impl NetworkReadout for NetBSDNetworkReadout { 481 | fn new() -> Self { 482 | NetBSDNetworkReadout 483 | } 484 | 485 | fn tx_bytes(&self, _: Option<&str>) -> Result { 486 | Err(ReadoutError::NotImplemented) 487 | } 488 | 489 | fn tx_packets(&self, _: Option<&str>) -> Result { 490 | Err(ReadoutError::NotImplemented) 491 | } 492 | 493 | fn rx_bytes(&self, _: Option<&str>) -> Result { 494 | Err(ReadoutError::NotImplemented) 495 | } 496 | 497 | fn rx_packets(&self, _: Option<&str>) -> Result { 498 | Err(ReadoutError::NotImplemented) 499 | } 500 | 501 | fn logical_address(&self, interface: Option<&str>) -> Result { 502 | shared::logical_address(interface) 503 | } 504 | 505 | fn physical_address(&self, _: Option<&str>) -> Result { 506 | Err(ReadoutError::NotImplemented) 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /src/windows/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::traits::*; 2 | use std::collections::HashMap; 3 | use std::env; 4 | use std::fs::read_dir; 5 | use std::path::{Path, PathBuf}; 6 | use winreg::enums::*; 7 | use winreg::RegKey; 8 | use wmi::WMIResult; 9 | use wmi::{COMLibrary, Variant, WMIConnection}; 10 | 11 | use windows::{ 12 | core::PSTR, Win32::System::Power::GetSystemPowerStatus, 13 | Win32::System::Power::SYSTEM_POWER_STATUS, 14 | Win32::System::SystemInformation::GetComputerNameExA, 15 | Win32::System::SystemInformation::GetTickCount64, 16 | Win32::System::SystemInformation::GlobalMemoryStatusEx, 17 | Win32::System::SystemInformation::MEMORYSTATUSEX, 18 | Win32::System::WindowsProgramming::GetUserNameA, 19 | }; 20 | 21 | impl From for ReadoutError { 22 | fn from(e: wmi::WMIError) -> Self { 23 | ReadoutError::Other(e.to_string()) 24 | } 25 | } 26 | 27 | pub struct WindowsBatteryReadout; 28 | 29 | impl BatteryReadout for WindowsBatteryReadout { 30 | fn new() -> Self { 31 | WindowsBatteryReadout {} 32 | } 33 | 34 | fn percentage(&self) -> Result { 35 | let power_state = WindowsBatteryReadout::get_power_status()?; 36 | 37 | match power_state.BatteryLifePercent { 38 | s if s != 255 => Ok(s), 39 | s => Err(ReadoutError::Warning(format!( 40 | "Windows reported a battery percentage of {s}, which means there is \ 41 | no battery available. Are you on a desktop system?" 42 | ))), 43 | } 44 | } 45 | 46 | fn status(&self) -> Result { 47 | let power_state = WindowsBatteryReadout::get_power_status()?; 48 | 49 | match power_state.ACLineStatus { 50 | 0 => Ok(BatteryState::Discharging), 51 | 1 => Ok(BatteryState::Charging), 52 | a => Err(ReadoutError::Other(format!( 53 | "Unexpected value for ac_line_status from win32 api: {a}" 54 | ))), 55 | } 56 | } 57 | 58 | fn health(&self) -> Result { 59 | Err(ReadoutError::NotImplemented) 60 | } 61 | } 62 | 63 | impl WindowsBatteryReadout { 64 | fn get_power_status() -> Result { 65 | let mut power_state = SYSTEM_POWER_STATUS::default(); 66 | 67 | if unsafe { GetSystemPowerStatus(&mut power_state) }.as_bool() { 68 | return Ok(power_state); 69 | } 70 | 71 | Err(ReadoutError::Other(String::from( 72 | "Call to GetSystemPowerStatus failed.", 73 | ))) 74 | } 75 | } 76 | 77 | pub struct WindowsKernelReadout; 78 | 79 | impl KernelReadout for WindowsKernelReadout { 80 | fn new() -> Self { 81 | WindowsKernelReadout {} 82 | } 83 | 84 | fn os_release(&self) -> Result { 85 | let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); 86 | let current_windows_not = 87 | hklm.open_subkey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")?; 88 | 89 | let nt_build: String = current_windows_not.get_value("CurrentBuild")?; 90 | 91 | Ok(nt_build) 92 | } 93 | 94 | fn os_type(&self) -> Result { 95 | Ok(String::from("Windows NT")) 96 | } 97 | 98 | fn pretty_kernel(&self) -> Result { 99 | Ok(format!("{} {}", self.os_type()?, self.os_release()?)) 100 | } 101 | } 102 | 103 | pub struct WindowsMemoryReadout; 104 | 105 | impl MemoryReadout for WindowsMemoryReadout { 106 | fn new() -> Self { 107 | WindowsMemoryReadout {} 108 | } 109 | 110 | fn total(&self) -> Result { 111 | let memory_status = WindowsMemoryReadout::get_memory_status()?; 112 | Ok(memory_status.ullTotalPhys / 1024u64) 113 | } 114 | 115 | fn free(&self) -> Result { 116 | Err(ReadoutError::NotImplemented) 117 | } 118 | 119 | fn buffers(&self) -> Result { 120 | Err(ReadoutError::NotImplemented) 121 | } 122 | 123 | fn cached(&self) -> Result { 124 | Err(ReadoutError::NotImplemented) 125 | } 126 | 127 | fn reclaimable(&self) -> Result { 128 | Err(ReadoutError::NotImplemented) 129 | } 130 | 131 | fn used(&self) -> Result { 132 | let memory_status = WindowsMemoryReadout::get_memory_status()?; 133 | Ok((memory_status.ullTotalPhys - memory_status.ullAvailPhys) / 1024u64) 134 | } 135 | 136 | fn swap_total(&self) -> Result { 137 | return Err(ReadoutError::NotImplemented); 138 | } 139 | 140 | fn swap_free(&self) -> Result { 141 | return Err(ReadoutError::NotImplemented); 142 | } 143 | 144 | fn swap_used(&self) -> Result { 145 | return Err(ReadoutError::NotImplemented); 146 | } 147 | } 148 | 149 | impl WindowsMemoryReadout { 150 | fn get_memory_status() -> Result { 151 | let mut memory_status = MEMORYSTATUSEX::default(); 152 | memory_status.dwLength = std::mem::size_of_val(&memory_status) as u32; 153 | 154 | if !unsafe { GlobalMemoryStatusEx(&mut memory_status) }.as_bool() { 155 | return Err(ReadoutError::Other(String::from( 156 | "GlobalMemoryStatusEx returned a zero \ 157 | return \ 158 | code.", 159 | ))); 160 | } 161 | 162 | Ok(memory_status) 163 | } 164 | } 165 | 166 | thread_local! { 167 | static COM_LIB: COMLibrary = COMLibrary::new().unwrap(); 168 | } 169 | 170 | fn wmi_connection() -> WMIResult { 171 | let com_lib = COM_LIB.with(|com| *com); 172 | WMIConnection::new(com_lib) 173 | } 174 | 175 | pub struct WindowsGeneralReadout; 176 | 177 | impl GeneralReadout for WindowsGeneralReadout { 178 | fn new() -> Self { 179 | WindowsGeneralReadout 180 | } 181 | 182 | fn backlight(&self) -> Result { 183 | Err(ReadoutError::NotImplemented) 184 | } 185 | 186 | fn resolution(&self) -> Result { 187 | Err(ReadoutError::NotImplemented) 188 | } 189 | 190 | fn username(&self) -> Result { 191 | let mut size = 0; 192 | unsafe { GetUserNameA(PSTR(std::ptr::null_mut()), &mut size) }; 193 | 194 | if size == 0 { 195 | return Err(ReadoutError::Other( 196 | "Call to \"GetUserNameA\" failed.".to_string(), 197 | )); 198 | } 199 | 200 | let mut username = Vec::with_capacity(size as usize); 201 | if !unsafe { GetUserNameA(PSTR(username.as_mut_ptr()), &mut size) }.as_bool() { 202 | return Err(ReadoutError::Other( 203 | "Call to \"GetUserNameA\" failed.".to_string(), 204 | )); 205 | } 206 | 207 | unsafe { 208 | username.set_len(size as usize); 209 | } 210 | 211 | let mut str = match String::from_utf8(username) { 212 | Ok(str) => str, 213 | Err(e) => { 214 | return Err(ReadoutError::Other(format!( 215 | "String from \"GetUserNameA\" \ 216 | was not valid UTF-8: {e}" 217 | ))) 218 | } 219 | }; 220 | 221 | str.pop(); //remove null terminator from string. 222 | 223 | Ok(str) 224 | } 225 | 226 | fn hostname(&self) -> Result { 227 | use windows::Win32::System::SystemInformation::ComputerNameDnsHostname; 228 | 229 | let mut size = 0; 230 | unsafe { 231 | GetComputerNameExA( 232 | ComputerNameDnsHostname, 233 | PSTR(std::ptr::null_mut()), 234 | &mut size, 235 | ) 236 | }; 237 | 238 | if size == 0 { 239 | return Err(ReadoutError::Other(String::from( 240 | "Call to \"GetComputerNameExA\" failed.", 241 | ))); 242 | } 243 | 244 | let mut hostname = Vec::with_capacity(size as usize); 245 | if unsafe { 246 | GetComputerNameExA( 247 | ComputerNameDnsHostname, 248 | PSTR(hostname.as_mut_ptr()), 249 | &mut size, 250 | ) 251 | } == false 252 | { 253 | return Err(ReadoutError::Other(String::from( 254 | "Call to \"GetComputerNameExA\" failed.", 255 | ))); 256 | } 257 | 258 | unsafe { hostname.set_len(size as usize) }; 259 | 260 | let str = match String::from_utf8(hostname) { 261 | Ok(str) => str, 262 | Err(e) => { 263 | return Err(ReadoutError::Other(format!( 264 | "String from \"GetComputerNameExA\" \ 265 | was not valid UTF-8: {e}" 266 | ))) 267 | } 268 | }; 269 | 270 | Ok(str) 271 | } 272 | 273 | fn distribution(&self) -> Result { 274 | Err(ReadoutError::NotImplemented) 275 | } 276 | 277 | fn desktop_environment(&self) -> Result { 278 | Err(ReadoutError::NotImplemented) 279 | } 280 | 281 | fn session(&self) -> Result { 282 | Err(ReadoutError::NotImplemented) 283 | } 284 | 285 | fn window_manager(&self) -> Result { 286 | Err(ReadoutError::NotImplemented) 287 | } 288 | 289 | fn terminal(&self) -> Result { 290 | Err(ReadoutError::NotImplemented) 291 | } 292 | 293 | fn shell(&self, _shorthand: ShellFormat, _: ShellKind) -> Result { 294 | Err(ReadoutError::NotImplemented) 295 | } 296 | 297 | fn cpu_model_name(&self) -> Result { 298 | let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); 299 | let central_processor = 300 | hklm.open_subkey("HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0")?; 301 | 302 | let processor_name: String = central_processor.get_value("ProcessorNameString")?; 303 | 304 | Ok(processor_name) 305 | } 306 | 307 | fn cpu_usage(&self) -> Result { 308 | Err(ReadoutError::NotImplemented) 309 | } 310 | 311 | fn cpu_physical_cores(&self) -> Result { 312 | Err(ReadoutError::NotImplemented) 313 | } 314 | 315 | fn cpu_cores(&self) -> Result { 316 | Err(ReadoutError::NotImplemented) 317 | } 318 | 319 | fn uptime(&self) -> Result { 320 | let tick_count = unsafe { GetTickCount64() }; 321 | let duration = std::time::Duration::from_millis(tick_count); 322 | 323 | Ok(duration.as_secs() as usize) 324 | } 325 | 326 | fn machine(&self) -> Result { 327 | let product_readout = WindowsProductReadout::new(); 328 | 329 | Ok(format!( 330 | "{} {}", 331 | product_readout.vendor()?, 332 | product_readout.product()? 333 | )) 334 | } 335 | 336 | fn os_name(&self) -> Result { 337 | let wmi_con = wmi_connection()?; 338 | 339 | let results: Vec> = 340 | wmi_con.raw_query("SELECT Caption FROM Win32_OperatingSystem")?; 341 | 342 | if let Some(os) = results.first() { 343 | if let Some(Variant::String(caption)) = os.get("Caption") { 344 | return Ok(caption.to_string()); 345 | } 346 | } 347 | 348 | Err(ReadoutError::Other( 349 | "Trying to get the operating system name \ 350 | from WMI failed" 351 | .to_string(), 352 | )) 353 | } 354 | 355 | fn disk_space(&self, path: &Path) -> Result<(u64, u64), ReadoutError> { 356 | Err(ReadoutError::NotImplemented) 357 | } 358 | 359 | fn gpus(&self) -> Result, ReadoutError> { 360 | Err(ReadoutError::NotImplemented) 361 | } 362 | } 363 | 364 | pub struct WindowsProductReadout { 365 | manufacturer: Option, 366 | model: Option, 367 | } 368 | 369 | impl ProductReadout for WindowsProductReadout { 370 | fn new() -> Self { 371 | let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); 372 | let sys_info = hklm 373 | .open_subkey("SYSTEM\\CurrentControlSet\\Control\\SystemInformation") 374 | .unwrap(); 375 | 376 | WindowsProductReadout { 377 | manufacturer: sys_info.get_value("SystemManufacturer").ok(), 378 | model: sys_info.get_value("SystemProductName").ok(), 379 | } 380 | } 381 | 382 | fn vendor(&self) -> Result { 383 | match &self.manufacturer { 384 | Some(v) => Ok(v.clone()), 385 | None => Err(ReadoutError::Other( 386 | "Trying to get the system manufacturer \ 387 | from the registry failed" 388 | .to_string(), 389 | )), 390 | } 391 | } 392 | 393 | fn family(&self) -> Result { 394 | Err(ReadoutError::NotImplemented) 395 | } 396 | 397 | fn product(&self) -> Result { 398 | match &self.model { 399 | Some(v) => Ok(v.clone()), 400 | None => Err(ReadoutError::Other( 401 | "Trying to get the system product name \ 402 | from the registry failed" 403 | .to_string(), 404 | )), 405 | } 406 | } 407 | } 408 | 409 | pub struct WindowsPackageReadout; 410 | 411 | impl PackageReadout for WindowsPackageReadout { 412 | fn new() -> Self { 413 | WindowsPackageReadout {} 414 | } 415 | 416 | /// Returns the __number of installed packages__ for the following package managers: 417 | /// - cargo 418 | fn count_pkgs(&self) -> Vec<(PackageManager, usize)> { 419 | let mut packages = Vec::new(); 420 | if let Some(c) = WindowsPackageReadout::count_cargo() { 421 | packages.push((PackageManager::Cargo, c)); 422 | } 423 | if let Some(c) = WindowsPackageReadout::count_scoop() { 424 | packages.push((PackageManager::Scoop, c)); 425 | } 426 | if let Some(c) = WindowsPackageReadout::count_winget() { 427 | packages.push((PackageManager::Winget, c)); 428 | } 429 | if let Some(c) = WindowsPackageReadout::count_chocolatey() { 430 | packages.push((PackageManager::Chocolatey, c)); 431 | } 432 | packages 433 | } 434 | } 435 | 436 | impl WindowsPackageReadout { 437 | fn count_cargo() -> Option { 438 | crate::shared::count_cargo() 439 | } 440 | 441 | fn count_scoop() -> Option { 442 | let scoop = match std::env::var("SCOOP") { 443 | Ok(scoop_var) => PathBuf::from(scoop_var), 444 | _ => home::home_dir().unwrap().join("scoop"), 445 | }; 446 | match scoop.join("apps").read_dir() { 447 | Ok(dir) => Some(dir.count() - 1), // One entry belongs to scoop itself 448 | _ => None, 449 | } 450 | } 451 | 452 | fn count_winget() -> Option { 453 | if let Ok(username) = env::var("USERNAME") { 454 | let db = format!("C:\\Users\\{username}\\AppData\\Local\\Packages\\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\\LocalState\\Microsoft.Winget.Source_8wekyb3d8bbwe\\installed.db"); 455 | if !Path::new(&db).is_file() { 456 | return None; 457 | } 458 | let connection = sqlite::open(db); 459 | if let Ok(con) = connection { 460 | let statement = con.prepare("SELECT COUNT(*) FROM ids"); 461 | if let Ok(mut s) = statement { 462 | if s.next().is_ok() { 463 | return match s.read::, _>(0) { 464 | Ok(Some(count)) => Some(count as usize), 465 | _ => None, 466 | }; 467 | } 468 | } 469 | } 470 | } 471 | None 472 | } 473 | 474 | fn count_chocolatey() -> Option { 475 | let chocolatey_dir = Path::new("C:\\ProgramData\\chocolatey\\lib"); 476 | if chocolatey_dir.is_dir() { 477 | if let Ok(read_dir) = read_dir(chocolatey_dir) { 478 | return Some(read_dir.count()); 479 | } 480 | } 481 | None 482 | } 483 | } 484 | 485 | pub struct WindowsNetworkReadout; 486 | 487 | impl NetworkReadout for WindowsNetworkReadout { 488 | fn new() -> Self { 489 | WindowsNetworkReadout 490 | } 491 | 492 | fn tx_bytes(&self, _: Option<&str>) -> Result { 493 | Err(ReadoutError::NotImplemented) 494 | } 495 | 496 | fn tx_packets(&self, _: Option<&str>) -> Result { 497 | Err(ReadoutError::NotImplemented) 498 | } 499 | 500 | fn rx_bytes(&self, _: Option<&str>) -> Result { 501 | Err(ReadoutError::NotImplemented) 502 | } 503 | 504 | fn rx_packets(&self, _: Option<&str>) -> Result { 505 | Err(ReadoutError::NotImplemented) 506 | } 507 | 508 | fn logical_address(&self, interface: Option<&str>) -> Result { 509 | match interface { 510 | Some(interface) => { 511 | if let Ok(addresses) = local_ip_address::list_afinet_netifas() { 512 | if let Some((_, ip)) = addresses.iter().find(|(name, _)| name == interface) { 513 | return Ok(ip.to_string()); 514 | } 515 | } 516 | } 517 | None => { 518 | if let Ok(local_ip) = local_ip_address::local_ip() { 519 | return Ok(local_ip.to_string()); 520 | } 521 | } 522 | }; 523 | 524 | Err(ReadoutError::Other( 525 | "Unable to get local IP address.".to_string(), 526 | )) 527 | } 528 | 529 | fn physical_address(&self, _: Option<&str>) -> Result { 530 | Err(ReadoutError::NotImplemented) 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | //! This module contains all the traits and types for creating a cross-platform API to query 2 | //! different readouts from various operating systems. For each operating system, there must be an implementation of these traits. 3 | #![allow(unused_variables)] 4 | 5 | use std::path::Path; 6 | 7 | /// This enum contains possible error types when doing sensor & variable readouts. 8 | #[derive(Debug, Clone)] 9 | pub enum ReadoutError { 10 | /// A specific metric might not be available on all systems (e. g. battery percentage on a 11 | /// desktop). \ 12 | /// If you encounter this error, it means that the requested value is not available. 13 | MetricNotAvailable, 14 | 15 | /// The default error for any readout that is not implemented by a particular platform. 16 | NotImplemented, 17 | 18 | /// A readout for a metric might be available, but fails due to missing dependencies or other 19 | /// unsatisfied requirements. 20 | Other(String), 21 | 22 | /// Getting a readout on a specific operating system might not make sense or causes some other 23 | /// kind of warning. This is not necessarily an error. 24 | Warning(String), 25 | } 26 | 27 | impl std::fmt::Display for ReadoutError { 28 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 29 | match self { 30 | ReadoutError::MetricNotAvailable => { 31 | write!(f, "Metric is not available on this system.") 32 | } 33 | ReadoutError::NotImplemented => { 34 | write!(f, "This metric is not available on this platform or is not yet implemented by libmacchina.") 35 | } 36 | ReadoutError::Other(s) => write!(f, "{}", s), 37 | ReadoutError::Warning(s) => write!(f, "{}", s), 38 | } 39 | } 40 | } 41 | 42 | impl From<&ReadoutError> for ReadoutError { 43 | fn from(r: &ReadoutError) -> Self { 44 | r.to_owned() 45 | } 46 | } 47 | 48 | /** 49 | This trait provides the necessary functions for querying battery statistics from the host 50 | computer. A desktop computer might not be able to provide values such as `percentage` and 51 | `status`, which means a `ReadoutError` can be returned. 52 | 53 | # Example 54 | 55 | ``` 56 | use libmacchina::traits::BatteryReadout; 57 | use libmacchina::traits::ReadoutError; 58 | use libmacchina::traits::BatteryState; 59 | 60 | //You can add fields to this struct which will then need to be initialized in the 61 | //BatteryReadout::new() function. 62 | pub struct MacOSBatteryReadout; 63 | 64 | impl BatteryReadout for MacOSBatteryReadout { 65 | fn new() -> Self { 66 | MacOSBatteryReadout {} 67 | } 68 | 69 | fn percentage(&self) -> Result { 70 | //get the battery percentage somehow... 71 | Ok(100u8) //always fully charged 72 | } 73 | 74 | fn status(&self) -> Result { 75 | //check if battery is being charged... 76 | Ok(BatteryState::Charging) //always charging. 77 | } 78 | 79 | fn health(&self) -> Result{ 80 | //check the battery health... 81 | Ok(100) //totally healtyh 82 | } 83 | } 84 | ``` 85 | */ 86 | pub trait BatteryReadout { 87 | /// Creates a new instance of the structure which implements this trait. 88 | fn new() -> Self; 89 | 90 | /// This function is used for querying the current battery percentage. The expected value is 91 | /// a u8 in the range of `0` to `100`. 92 | fn percentage(&self) -> Result; 93 | 94 | /// This function is used for querying the current battery charging state. If the battery is 95 | /// currently being charged, we expect a return value of `BatteryState::Charging`, otherwise 96 | /// `BatteryState::Discharging`. 97 | fn status(&self) -> Result; 98 | 99 | /// This function is used for querying the current battery's health in percentage. 100 | fn health(&self) -> Result; 101 | } 102 | 103 | /** 104 | This trait is used for implementing common functions for reading kernel properties, such as 105 | kernel name and version. 106 | 107 | # Example 108 | 109 | ``` 110 | use libmacchina::traits::KernelReadout; 111 | use libmacchina::traits::ReadoutError; 112 | 113 | pub struct MacOSKernelReadout; 114 | 115 | impl KernelReadout for MacOSKernelReadout { 116 | fn new() -> Self { 117 | MacOSKernelReadout {} 118 | } 119 | 120 | fn os_release(&self) -> Result { 121 | // Get kernel version 122 | Ok(String::from("20.0.1")) 123 | } 124 | 125 | fn os_type(&self) -> Result { 126 | // Get kernel name 127 | Ok(String::from("Darwin")) 128 | } 129 | } 130 | ``` 131 | */ 132 | pub trait KernelReadout { 133 | /// Creates a new instance of the structure which implements this trait. 134 | fn new() -> Self; 135 | 136 | /// This function should return the version of the kernel (e. g. `20.3.0` on macOS for Darwin). 137 | fn os_release(&self) -> Result; 138 | 139 | /// This function should return the kernel name as a string (e. g. `Darwin` on macOS). 140 | fn os_type(&self) -> Result; 141 | 142 | /// This function is used for getting the kernel name and version in a pretty format. 143 | fn pretty_kernel(&self) -> Result { 144 | let os_type = self.os_type().unwrap_or_default(); 145 | let os_release = self.os_release().unwrap_or_default(); 146 | 147 | if !(os_type.is_empty() || os_release.is_empty()) { 148 | return Ok(format!("{os_type} {os_release}")); 149 | } 150 | 151 | Err(ReadoutError::MetricNotAvailable) 152 | } 153 | } 154 | 155 | /** 156 | This trait provides common functions for _querying the current memory state_ of the host device, 157 | most notably `total` and `used`. All other methods exposed by this trait are there in case you're 158 | intending to calculate memory usage on your own. 159 | 160 | # Example 161 | 162 | ``` 163 | use libmacchina::traits::MemoryReadout; 164 | use libmacchina::traits::ReadoutError; 165 | 166 | pub struct MacOSMemoryReadout; 167 | 168 | impl MemoryReadout for MacOSMemoryReadout { 169 | fn new() -> Self { 170 | MacOSMemoryReadout {} 171 | } 172 | 173 | fn total(&self) -> Result { 174 | // Get the total physical memory for the machine 175 | Ok(512 * 1024) // Return 512mb in kilobytes. 176 | } 177 | 178 | fn free(&self) -> Result { 179 | // Get the amount of free memory 180 | Ok(256 * 1024) // Return 256mb in kilobytes. 181 | } 182 | 183 | fn buffers(&self) -> Result { 184 | // Get the current memory value for buffers 185 | Ok(64 * 1024) // Return 64mb in kilobytes. 186 | } 187 | 188 | fn cached(&self) -> Result { 189 | // Get the amount of cached content in memory 190 | Ok(128 * 1024) // Return 128mb in kilobytes. 191 | } 192 | 193 | fn reclaimable(&self) -> Result { 194 | // Get the amount of reclaimable memory 195 | Ok(64 * 1024) // Return 64mb in kilobytes. 196 | } 197 | 198 | fn used(&self) -> Result { 199 | // Get the currently used memory. 200 | Ok(256 * 1024) // Return 256mb in kilobytes. 201 | } 202 | 203 | fn swap_total(&self) -> Result { 204 | // Get the total amount of swap. 205 | Ok(4096 * 1024) // Return 4096mb in kilobytes. 206 | } 207 | 208 | fn swap_free(&self) -> Result { 209 | // Get the amount of free swap. 210 | Ok(277 * 1024) // Return 277mb in kilobytes. 211 | } 212 | 213 | fn swap_used(&self) -> Result { 214 | // Get the currently used swap. 215 | Ok(4 * 1024) // Return 4mb in kilobytes. 216 | } 217 | } 218 | 219 | ``` 220 | */ 221 | pub trait MemoryReadout { 222 | /// Creates a new instance of the structure which implements this trait. 223 | fn new() -> Self; 224 | 225 | /// This function should return the total available memory in kilobytes. 226 | fn total(&self) -> Result; 227 | 228 | /// This function should return the free available memory in kilobytes. 229 | fn free(&self) -> Result; 230 | 231 | /// This function should return the current memory value for buffers in kilobytes. 232 | fn buffers(&self) -> Result; 233 | 234 | /// This function should return the amount of cached content in memory in kilobytes. 235 | fn cached(&self) -> Result; 236 | 237 | /// This function should return the amount of reclaimable memory in kilobytes. 238 | fn reclaimable(&self) -> Result; 239 | 240 | /// This function should return the amount of currently used memory in kilobytes. 241 | fn used(&self) -> Result; 242 | 243 | /// This function should return of the total available swap in kilobytes. 244 | fn swap_total(&self) -> Result; 245 | 246 | /// This function should return the amount of the free available swap in kilobytes. 247 | fn swap_free(&self) -> Result; 248 | 249 | /// This function should return the amount of currently used swap in kilobytes. 250 | fn swap_used(&self) -> Result; 251 | } 252 | 253 | /** 254 | This trait provides an interface to various functions used to _count packages_ on 255 | the host system. Almost all modern operating systems use some kind of package manager. 256 | 257 | # Example 258 | 259 | ``` 260 | use libmacchina::traits::{PackageReadout, PackageManager}; 261 | use libmacchina::traits::ReadoutError; 262 | 263 | pub struct MacOSPackageReadout; 264 | 265 | impl PackageReadout for MacOSPackageReadout { 266 | fn new() -> Self { 267 | MacOSPackageReadout {} 268 | } 269 | 270 | fn count_pkgs(&self) -> Vec<(PackageManager, usize)> { 271 | // Check if homebrew 🍻 is installed and count installed packages... 272 | vec![(PackageManager::Homebrew, 120)] 273 | } 274 | } 275 | ``` 276 | */ 277 | pub trait PackageReadout { 278 | /// Creates a new instance of the structure which implements this trait. 279 | fn new() -> Self; 280 | 281 | /// This function should return the number of installed packages. 282 | fn count_pkgs(&self) -> Vec<(PackageManager, usize)> { 283 | Vec::new() 284 | } 285 | } 286 | 287 | /** 288 | This trait provides an interface to various networking statistics about the host system. 289 | 290 | # Example 291 | 292 | ``` 293 | use libmacchina::traits::NetworkReadout; 294 | use libmacchina::traits::ReadoutError; 295 | 296 | pub struct MacOSNetworkReadout; 297 | 298 | impl NetworkReadout for MacOSNetworkReadout { 299 | fn new() -> Self { 300 | MacOSNetworkReadout {} 301 | } 302 | 303 | fn tx_bytes(&self, interface: Option<&str>) -> Result { 304 | todo!() 305 | } 306 | 307 | fn tx_packets(&self, interface: Option<&str>) -> Result { 308 | todo!() 309 | } 310 | 311 | fn rx_bytes(&self, interface: Option<&str>) -> Result { 312 | todo!() 313 | } 314 | 315 | fn rx_packets(&self, interface: Option<&str>) -> Result { 316 | todo!() 317 | } 318 | 319 | fn logical_address(&self, interface: Option<&str>) -> Result { 320 | todo!() 321 | } 322 | 323 | fn physical_address(&self, interface: Option<&str>) -> Result { 324 | todo!() 325 | } 326 | } 327 | ``` 328 | 329 | */ 330 | pub trait NetworkReadout { 331 | /// Creates a new instance of the structure which implements this trait. 332 | fn new() -> Self; 333 | 334 | /// This function should return the number of bytes 335 | /// transmitted by the interface of the host. 336 | fn tx_bytes(&self, interface: Option<&str>) -> Result; 337 | 338 | /// This function should return the number of packets 339 | /// transmitted by the interface of the host. 340 | fn tx_packets(&self, interface: Option<&str>) -> Result; 341 | 342 | /// This function should return the number of bytes 343 | /// received by the interface of the host. 344 | fn rx_bytes(&self, interface: Option<&str>) -> Result; 345 | 346 | /// This function should return the number of packets 347 | /// received by the interface of the host. 348 | fn rx_packets(&self, interface: Option<&str>) -> Result; 349 | 350 | /// This function should return the logical address, i.e. _local IPv4/6 address_ of the 351 | /// specified interface. 352 | /// 353 | /// _e.g._ `192.168.1.2` 354 | fn logical_address(&self, interface: Option<&str>) -> Result; 355 | 356 | /// This function should return the physical address, i.e. _MAC address_ of the 357 | /// specified interface. 358 | /// 359 | /// _e.g._ `52:9a:d2:d3:b5:fd` 360 | fn physical_address(&self, interface: Option<&str>) -> Result; 361 | } 362 | 363 | /** 364 | This trait provides the interface for implementing functionality used for getting _product information_ 365 | about the host machine. 366 | 367 | # Example 368 | 369 | ``` 370 | use libmacchina::traits::ProductReadout; 371 | use libmacchina::traits::ReadoutError; 372 | 373 | pub struct MacOSProductReadout; 374 | 375 | impl ProductReadout for MacOSProductReadout { 376 | fn new() -> Self { 377 | MacOSProductReadout {} 378 | } 379 | 380 | fn vendor(&self) -> Result { 381 | Ok(String::from("Apple")) 382 | } 383 | 384 | fn family(&self) -> Result { 385 | Ok(String::from("MacBook Pro")) 386 | } 387 | 388 | fn product(&self) -> Result { 389 | Ok(String::from("MacBookPro16,1")) 390 | } 391 | } 392 | ``` 393 | */ 394 | pub trait ProductReadout { 395 | /// Creates a new instance of the structure which implements this trait. 396 | fn new() -> Self; 397 | 398 | /// This function should return the vendor name of the host's machine. 399 | /// 400 | /// _e.g._ `Lenovo` 401 | /// 402 | /// This is set by the machine's manufacturer. 403 | fn vendor(&self) -> Result; 404 | 405 | /// This function should return the family name of the host's machine. 406 | /// 407 | /// _e.g._ `IdeaPad S540-15IWL GTX` 408 | /// 409 | /// This is set by the machine's manufacturer. 410 | fn family(&self) -> Result; 411 | 412 | /// This function should return the product name of the host's machine. 413 | /// 414 | /// _e.g._ `81SW` 415 | /// 416 | /// This is set by the machine's manufacturer. 417 | fn product(&self) -> Result; 418 | } 419 | 420 | /** 421 | This trait provides the interface for implementing functionality used for querying general 422 | information about the running operating system and current user. 423 | 424 | # Example 425 | 426 | ``` 427 | use std::path::Path; 428 | use libmacchina::traits::GeneralReadout; 429 | use libmacchina::traits::ReadoutError; 430 | use libmacchina::traits::ShellFormat; 431 | use libmacchina::traits::ShellKind; 432 | 433 | pub struct MacOSGeneralReadout; 434 | 435 | impl GeneralReadout for MacOSGeneralReadout { 436 | 437 | fn new() -> Self { 438 | MacOSGeneralReadout {} 439 | } 440 | 441 | fn backlight(&self) -> Result { 442 | Ok(100) // Brightness is at its maximum 443 | } 444 | 445 | fn resolution(&self) -> Result { 446 | Ok("1920x1080".to_string()) 447 | } 448 | 449 | fn username(&self) -> Result { 450 | //let username = NSUserName(); 451 | Ok(String::from("johndoe")) 452 | } 453 | 454 | fn hostname(&self) -> Result { 455 | Ok("supercomputer".to_string()) 456 | } 457 | 458 | fn distribution(&self) -> Result { 459 | Ok("Arch Linux".to_string()) 460 | } 461 | 462 | fn desktop_environment(&self) -> Result { 463 | Ok("Plasma".to_string()) 464 | } 465 | 466 | fn session(&self) -> Result { 467 | Ok("Wayland".to_string()) 468 | } 469 | 470 | fn window_manager(&self) -> Result { 471 | Ok("KWin".to_string()) 472 | } 473 | 474 | fn terminal(&self) -> Result { 475 | Ok("kitty".to_string()) 476 | } 477 | 478 | fn shell(&self, _shorthand: ShellFormat, kind: ShellKind) -> Result { 479 | Ok("bash".to_string()) 480 | } 481 | 482 | fn cpu_model_name(&self) -> Result { 483 | Ok("Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz".to_string()) 484 | } 485 | 486 | fn cpu_usage(&self) -> Result { 487 | Ok(20) //20% CPU usage 488 | } 489 | 490 | fn cpu_physical_cores(&self) -> Result { 491 | Ok(4) 492 | } 493 | 494 | fn cpu_cores(&self) -> Result { 495 | Ok(8) 496 | } 497 | 498 | fn uptime(&self) -> Result { 499 | Ok(24 * 60 * 60) //1 day 500 | } 501 | 502 | fn machine(&self) -> Result { 503 | Ok("MacBookPro11,5".to_string()) 504 | } 505 | 506 | fn os_name(&self) -> Result { 507 | Ok("macOS 11.2.2 Big Sur".to_string()) 508 | } 509 | 510 | fn disk_space(&self, path: &Path) -> Result<(u64, u64), ReadoutError> { 511 | Ok((50000000,1000000000)) // Used / Total 512 | } 513 | 514 | fn gpus(&self) -> Result, ReadoutError> { 515 | // Get gpu(s) from list of connected pci devices 516 | Ok(vec!(String::from("gpu1"), String::from("gpu2"))) // Return gpu sub-device names 517 | } 518 | } 519 | 520 | ``` 521 | */ 522 | pub trait GeneralReadout { 523 | /// Creates a new instance of the structure which implements this trait. 524 | fn new() -> Self; 525 | 526 | /// This function should return the backlight (brightness) value of the machine. 527 | /// 528 | /// _e.g._ `100` 529 | fn backlight(&self) -> Result; 530 | 531 | /// This function should return the display resolution of the machine. 532 | /// 533 | /// _e.g. `1920x1080` 534 | fn resolution(&self) -> Result; 535 | 536 | /// This function should return the username of the currently logged on user. 537 | /// 538 | /// _e.g._ `johndoe` 539 | fn username(&self) -> Result; 540 | 541 | /// This function should return the hostname of the host's computer. 542 | /// 543 | /// _e.g._ `supercomputer` 544 | fn hostname(&self) -> Result; 545 | 546 | /// This function should return the name of the distribution of the operating system. 547 | /// 548 | /// _e.g._ `Arch Linux` 549 | fn distribution(&self) -> Result; 550 | 551 | /// This function should return the name of the used desktop environment. 552 | /// 553 | /// _e.g._ `Plasma` 554 | fn desktop_environment(&self) -> Result; 555 | 556 | /// This function should return the type of session that's in use. 557 | /// 558 | /// _e.g._ `Wayland` 559 | fn session(&self) -> Result; 560 | 561 | /// This function should return the name of the used window manager. 562 | /// 563 | /// _e.g._ `KWin` 564 | fn window_manager(&self) -> Result; 565 | 566 | /// This function should return the name of the used terminal emulator. 567 | /// 568 | /// _e.g._ `kitty` 569 | fn terminal(&self) -> Result; 570 | 571 | /// This function should return the currently running shell depending on the `_shorthand` 572 | /// value, for example: /bin/bash, /bin/zsh, etc. 573 | /// 574 | /// - If `_shorthand` is `ShellFormat::Relative` the basename of the shell will be returned, 575 | /// _e.g._ bash, zsh, etc. 576 | /// 577 | /// - If `_shorthand` is `ShellFormat::Absolute` the absolute path of the shell will be 578 | /// returned. 579 | fn shell(&self, _shorthand: ShellFormat, kind: ShellKind) -> Result; 580 | 581 | /// This function should return the model name of the CPU \ 582 | /// 583 | /// _e.g._ `Intel(R) Core(TM) i5-8265U CPU @ 1.60GHz` 584 | fn cpu_model_name(&self) -> Result; 585 | 586 | /// This function should return the average CPU usage over the last minute. 587 | fn cpu_usage(&self) -> Result; 588 | 589 | /// This function should return the number of physical cores of the host's processor. 590 | fn cpu_physical_cores(&self) -> Result; 591 | 592 | /// This function should return the number of logical cores of the host's processor. 593 | fn cpu_cores(&self) -> Result; 594 | 595 | /// This function should return the uptime of the OS in seconds. 596 | fn uptime(&self) -> Result; 597 | 598 | /// This function should return the name of the physical machine. 599 | /// 600 | /// _e.g._ `MacBookPro11,5` 601 | fn machine(&self) -> Result; 602 | 603 | /// This function should return the name of the OS in a pretty format. 604 | /// 605 | /// _e.g._ `macOS 11.2.2 Big Sur` 606 | fn os_name(&self) -> Result; 607 | 608 | /// This function should return a tuple with the number values representing used and total 609 | /// bytes of disk space. 610 | /// 611 | /// _e.g._ '(50000000, 1000000000)' 612 | fn disk_space(&self, path: &Path) -> Result<(u64, u64), ReadoutError>; 613 | 614 | /// This function should return the device names of any _GPU(s)_ connected to the host machine. 615 | fn gpus(&self) -> Result, ReadoutError>; 616 | } 617 | 618 | /// Holds the possible variants for battery status. 619 | pub enum BatteryState { 620 | Charging, 621 | Discharging, 622 | } 623 | 624 | impl std::fmt::Display for BatteryState { 625 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 626 | match *self { 627 | BatteryState::Charging => { 628 | write!(f, "Charging") 629 | } 630 | BatteryState::Discharging => { 631 | write!(f, "Discharging") 632 | } 633 | } 634 | } 635 | } 636 | 637 | impl From for &'static str { 638 | fn from(state: BatteryState) -> &'static str { 639 | match state { 640 | BatteryState::Charging => "Charging", 641 | BatteryState::Discharging => "Discharging", 642 | } 643 | } 644 | } 645 | 646 | /// The currently running shell is a program, whose path 647 | /// can be _relative_, or _absolute_. 648 | #[derive(Debug)] 649 | pub enum ShellFormat { 650 | Relative, 651 | Absolute, 652 | } 653 | 654 | #[derive(Debug)] 655 | /// There are two distinct kinds of shells, a so called *"current"* shell, i.e. the shell the user is currently using. 656 | /// And a default shell, i.e. that the user sets for themselves using the `chsh` tool. 657 | pub enum ShellKind { 658 | Current, 659 | Default, 660 | } 661 | 662 | /// The supported package managers whose packages can be extracted. 663 | pub enum PackageManager { 664 | Homebrew, 665 | MacPorts, 666 | Pacman, 667 | Portage, 668 | Dpkg, 669 | Opkg, 670 | Xbps, 671 | Pkgsrc, 672 | Apk, 673 | Eopkg, 674 | Rpm, 675 | Cargo, 676 | Flatpak, 677 | Snap, 678 | Android, 679 | Pkg, 680 | Scoop, 681 | Nix, 682 | Winget, 683 | Chocolatey, 684 | } 685 | 686 | impl std::fmt::Display for PackageManager { 687 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 688 | match self { 689 | PackageManager::Homebrew => write!(f, "Homebrew"), 690 | PackageManager::MacPorts => write!(f, "MacPorts"), 691 | PackageManager::Pacman => write!(f, "pacman"), 692 | PackageManager::Portage => write!(f, "portage"), 693 | PackageManager::Dpkg => write!(f, "dpkg"), 694 | PackageManager::Opkg => write!(f, "opkg"), 695 | PackageManager::Xbps => write!(f, "xbps"), 696 | PackageManager::Pkgsrc => write!(f, "pkgsrc"), 697 | PackageManager::Apk => write!(f, "apk"), 698 | PackageManager::Eopkg => write!(f, "eopkg"), 699 | PackageManager::Rpm => write!(f, "rpm"), 700 | PackageManager::Cargo => write!(f, "cargo"), 701 | PackageManager::Flatpak => write!(f, "flatpak"), 702 | PackageManager::Snap => write!(f, "snap"), 703 | PackageManager::Android => write!(f, "Android"), 704 | PackageManager::Pkg => write!(f, "pkg"), 705 | PackageManager::Scoop => write!(f, "Scoop"), 706 | PackageManager::Nix => write!(f, "nix"), 707 | PackageManager::Winget => write!(f, "winget"), 708 | PackageManager::Chocolatey => write!(f, "choco"), 709 | } 710 | } 711 | } 712 | -------------------------------------------------------------------------------- /src/macos/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unnecessary_cast)] 2 | use crate::extra; 3 | use crate::macos::mach_ffi::{io_registry_entry_t, DisplayServicesGetBrightness, IOObjectRelease}; 4 | use crate::macos::mach_ffi::{ 5 | kIOMasterPortDefault, vm_statistics64, IORegistryEntryCreateCFProperties, 6 | IOServiceGetMatchingService, IOServiceMatching, 7 | }; 8 | use crate::shared; 9 | use crate::traits::ReadoutError::MetricNotAvailable; 10 | use crate::traits::*; 11 | use core_foundation::base::{TCFType, ToVoid}; 12 | use core_foundation::dictionary::{CFMutableDictionary, CFMutableDictionaryRef}; 13 | use core_foundation::number::{CFNumber, CFNumberRef}; 14 | use core_foundation::string::CFString; 15 | use core_graphics::display::{CGDisplay, CGMainDisplayID}; 16 | use core_video_sys::{ 17 | kCVTimeIsIndefinite, CVDisplayLinkCreateWithCGDisplay, 18 | CVDisplayLinkGetNominalOutputVideoRefreshPeriod, CVDisplayLinkRef, CVDisplayLinkRelease, 19 | }; 20 | use mach2::kern_return::KERN_SUCCESS; 21 | use std::ffi::CString; 22 | use std::fs::DirEntry; 23 | use std::path::Path; 24 | use sysctl::{Ctl, Sysctl}; 25 | 26 | mod mach_ffi; 27 | 28 | pub struct MacOSBatteryReadout { 29 | power_info: Result, 30 | } 31 | 32 | pub struct MacOSProductReadout { 33 | hw_model_ctl: Option, 34 | } 35 | 36 | pub struct MacOSKernelReadout { 37 | os_type_ctl: Option, 38 | os_release_ctl: Option, 39 | } 40 | 41 | pub struct MacOSGeneralReadout { 42 | cpu_brand_ctl: Option, 43 | boot_time_ctl: Option, 44 | hostname_ctl: Option, 45 | os_product_version_ctl: Option, 46 | } 47 | 48 | pub struct MacOSMemoryReadout { 49 | page_size: i64, 50 | physical_memory: i64, 51 | } 52 | 53 | #[derive(Debug, Default)] 54 | struct MacOSIOPMPowerSource { 55 | battery_installed: Option, 56 | state_of_charge: Option, 57 | charging: Option, 58 | } 59 | 60 | pub struct MacOSPackageReadout; 61 | 62 | pub struct MacOSNetworkReadout; 63 | 64 | impl BatteryReadout for MacOSBatteryReadout { 65 | fn new() -> Self { 66 | MacOSBatteryReadout { 67 | power_info: MacOSIOPMPowerSource::new(), 68 | } 69 | } 70 | 71 | fn percentage(&self) -> Result { 72 | let power_info = self.power_info.as_ref()?; 73 | 74 | Ok(power_info 75 | .state_of_charge 76 | .ok_or_else(|| ReadoutError::Other(String::from( 77 | "Percentage property was not present in the dictionary that was returned from IOKit.", 78 | )))? as u8) 79 | } 80 | 81 | fn status(&self) -> Result { 82 | let power_info = self.power_info.as_ref()?; 83 | 84 | if let Some(charging) = power_info.charging { 85 | return Ok(if charging { 86 | BatteryState::Charging 87 | } else { 88 | BatteryState::Discharging 89 | }); 90 | } 91 | 92 | Err(ReadoutError::Other(String::from( 93 | "Status property was not present in the dictionary that was returned from IOKit.", 94 | ))) 95 | } 96 | 97 | fn health(&self) -> Result { 98 | Err(ReadoutError::NotImplemented) 99 | } 100 | } 101 | 102 | impl MacOSIOPMPowerSource { 103 | fn new() -> Result { 104 | let battery_data_key = CFString::new("BatteryData"); 105 | let power_source_dict = MacOSIOPMPowerSource::get_power_source_dict()?; 106 | 107 | if !power_source_dict.contains_key(battery_data_key.to_void()) { 108 | return Err(ReadoutError::Other(String::from("Dictionary does not contain information about the battery. Are you using a third-party battery?"))); 109 | } 110 | 111 | let battery_data_dict = 112 | (*power_source_dict.get(&battery_data_key.to_void())) as CFMutableDictionaryRef; 113 | 114 | let battery_data_dict: CFMutableDictionary<_> = 115 | unsafe { CFMutableDictionary::wrap_under_get_rule(battery_data_dict) }; 116 | 117 | let mut instance: MacOSIOPMPowerSource = std::default::Default::default(); 118 | 119 | unsafe { 120 | if let Some(battery_installed) = 121 | power_source_dict.find(&CFString::new("BatteryInstalled").to_void()) 122 | { 123 | let number = CFNumber::wrap_under_get_rule((*battery_installed) as CFNumberRef); 124 | instance.battery_installed = Some(number.to_i32() != Some(0)); 125 | } 126 | 127 | if let Some(state_of_charge) = 128 | battery_data_dict.find(&CFString::new("StateOfCharge").to_void()) 129 | { 130 | let number = CFNumber::wrap_under_get_rule((*state_of_charge) as CFNumberRef); 131 | instance.state_of_charge = Some(number.to_i32().unwrap() as usize); 132 | } 133 | 134 | if let Some(charging) = power_source_dict.find(&CFString::new("IsCharging").to_void()) { 135 | let number = CFNumber::wrap_under_get_rule((*charging) as CFNumberRef); 136 | instance.charging = Some(number.to_i32() != Some(0)); 137 | } 138 | } 139 | 140 | Ok(instance) 141 | } 142 | 143 | fn get_power_source_dict() -> Result { 144 | let io_service_name = CString::new("IOPMPowerSource").expect("Unable to create c string"); 145 | let service = unsafe { IOServiceMatching(io_service_name.as_ptr()) }; 146 | let entry: io_registry_entry_t = 147 | unsafe { IOServiceGetMatchingService(kIOMasterPortDefault, service) }; 148 | let mut dict_data: Option = None; 149 | 150 | if entry != 0 { 151 | let mut dict: CFMutableDictionaryRef = std::ptr::null_mut(); 152 | let dict_ptr = (&mut dict) as *mut CFMutableDictionaryRef; 153 | 154 | let kern_return = 155 | unsafe { IORegistryEntryCreateCFProperties(entry, dict_ptr, std::ptr::null(), 0) }; 156 | 157 | if kern_return == KERN_SUCCESS { 158 | dict_data = Some(unsafe { CFMutableDictionary::wrap_under_create_rule(dict) }); 159 | } 160 | 161 | unsafe { 162 | IOObjectRelease(entry); 163 | } 164 | 165 | if kern_return != KERN_SUCCESS { 166 | return Err(ReadoutError::Other(format!( 167 | "Creating the dictionary for the IOService failed with return code: {kern_return}" 168 | ))); 169 | } 170 | } 171 | 172 | dict_data.ok_or_else(|| ReadoutError::Other(String::from( 173 | "Unable to get the 'IOPMPowerSource' service from IOKit :( Are you on a desktop system?", 174 | ))) 175 | } 176 | } 177 | 178 | impl KernelReadout for MacOSKernelReadout { 179 | fn new() -> Self { 180 | MacOSKernelReadout { 181 | os_type_ctl: Ctl::new("kern.ostype").ok(), 182 | os_release_ctl: Ctl::new("kern.osrelease").ok(), 183 | } 184 | } 185 | 186 | fn os_release(&self) -> Result { 187 | Ok(self 188 | .os_release_ctl 189 | .as_ref() 190 | .ok_or(MetricNotAvailable)? 191 | .value_string()?) 192 | } 193 | 194 | fn os_type(&self) -> Result { 195 | Ok(self 196 | .os_type_ctl 197 | .as_ref() 198 | .ok_or(MetricNotAvailable)? 199 | .value_string()?) 200 | } 201 | 202 | fn pretty_kernel(&self) -> Result { 203 | Ok(format!("{} {}", self.os_type()?, self.os_release()?)) 204 | } 205 | } 206 | 207 | impl GeneralReadout for MacOSGeneralReadout { 208 | fn new() -> Self { 209 | MacOSGeneralReadout { 210 | cpu_brand_ctl: Ctl::new("machdep.cpu.brand_string").ok(), 211 | boot_time_ctl: Ctl::new("kern.boottime").ok(), 212 | hostname_ctl: Ctl::new("kern.hostname").ok(), 213 | os_product_version_ctl: Ctl::new("kern.osproductversion").ok(), 214 | } 215 | } 216 | 217 | fn backlight(&self) -> Result { 218 | let main_display = unsafe { CGMainDisplayID() }; 219 | let mut display_brightness: f32 = 0.0; 220 | 221 | let return_value = 222 | unsafe { DisplayServicesGetBrightness(main_display, &mut display_brightness) }; 223 | 224 | if return_value == 0 { 225 | Ok((display_brightness * 100.0) as usize) 226 | } else { 227 | Err(ReadoutError::Other(format!( 228 | "Could not query display brightness of main display, got return code {return_value}" 229 | ))) 230 | } 231 | } 232 | 233 | fn resolution(&self) -> Result { 234 | let displays = CGDisplay::active_displays(); 235 | if let Err(e) = displays { 236 | return Err(ReadoutError::Other(format!( 237 | "Error while querying active displays: {e}" 238 | ))); 239 | } 240 | 241 | let displays: Vec = displays 242 | .unwrap() 243 | .iter() 244 | .map(|id| CGDisplay::new(*id)) 245 | .filter(|d| d.is_active()) 246 | .collect(); 247 | 248 | let mut output: Vec = Vec::with_capacity(displays.len()); 249 | 250 | for display in displays { 251 | let (ui_width, ui_height) = (display.pixels_wide(), display.pixels_high()); 252 | let mut out_string: String = format!("{ui_width}x{ui_height}"); 253 | 254 | if let Some(mode) = display.display_mode() { 255 | let (real_width, real_height) = (mode.pixel_width(), mode.pixel_height()); 256 | 257 | let mut refresh_rate: i32 = mode.refresh_rate().round() as i32; 258 | if refresh_rate == 0 { 259 | unsafe { 260 | let mut link: CVDisplayLinkRef = std::mem::zeroed(); 261 | CVDisplayLinkCreateWithCGDisplay(display.id, &mut link); 262 | 263 | let time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod(link); 264 | if (time.flags & kCVTimeIsIndefinite) == 0 { 265 | refresh_rate = 266 | ((time.timeScale as f64) / (time.timeValue as f64)) as i32; 267 | } 268 | 269 | CVDisplayLinkRelease(link); 270 | } 271 | } 272 | 273 | if real_width != ui_width || real_height != ui_height { 274 | out_string = format!( 275 | "{real_width}x{real_height}@{refresh_rate}fps (as {ui_width}x{ui_height})" 276 | ); 277 | } 278 | } 279 | 280 | output.push(out_string); 281 | } 282 | 283 | Ok(output.join("\n")) 284 | } 285 | 286 | fn username(&self) -> Result { 287 | shared::username() 288 | } 289 | 290 | fn hostname(&self) -> Result { 291 | Ok(self 292 | .hostname_ctl 293 | .as_ref() 294 | .ok_or(MetricNotAvailable)? 295 | .value_string()?) 296 | } 297 | 298 | fn distribution(&self) -> Result { 299 | Err(ReadoutError::Warning(String::from( 300 | "Since you're on macOS, there is no distribution to be read from the system.", 301 | ))) 302 | } 303 | 304 | fn desktop_environment(&self) -> Result { 305 | Ok(String::from("Aqua")) 306 | } 307 | 308 | fn session(&self) -> Result { 309 | Err(ReadoutError::NotImplemented) 310 | } 311 | 312 | fn window_manager(&self) -> Result { 313 | Ok(String::from("Quartz Compositor")) 314 | } 315 | 316 | fn terminal(&self) -> Result { 317 | use std::env::var; 318 | 319 | let mut terminal: Option = None; 320 | if let Ok(mut terminal_str) = var("TERM_PROGRAM") { 321 | terminal_str = terminal_str.to_lowercase(); 322 | terminal = match terminal_str.as_str() { 323 | "iterm.app" => Some(String::from("iTerm2")), 324 | "apple_terminal" => Some(String::from("Apple Terminal")), 325 | "hyper" => Some(String::from("HyperTerm")), 326 | s => Some(String::from(s)), 327 | } 328 | } 329 | 330 | if let Some(terminal) = terminal { 331 | if let Ok(version) = var("TERM_PROGRAM_VERSION") { 332 | return Ok(format!("{terminal} (Version {version})")); 333 | } 334 | 335 | return Ok(terminal); 336 | } 337 | 338 | if let Ok(terminal_env) = var("TERM") { 339 | return Ok(terminal_env); 340 | } 341 | 342 | Err(MetricNotAvailable) 343 | } 344 | 345 | fn shell(&self, shorthand: ShellFormat, kind: ShellKind) -> Result { 346 | shared::shell(shorthand, kind) 347 | } 348 | 349 | fn cpu_model_name(&self) -> Result { 350 | Ok(self 351 | .cpu_brand_ctl 352 | .as_ref() 353 | .ok_or(MetricNotAvailable)? 354 | .value_string()?) 355 | } 356 | 357 | fn cpu_usage(&self) -> Result { 358 | shared::cpu_usage() 359 | } 360 | 361 | fn cpu_physical_cores(&self) -> Result { 362 | shared::cpu_physical_cores() 363 | } 364 | 365 | fn cpu_cores(&self) -> Result { 366 | shared::cpu_cores() 367 | } 368 | 369 | fn uptime(&self) -> Result { 370 | use libc::timeval; 371 | use std::time::{Duration, SystemTime, UNIX_EPOCH}; 372 | 373 | let time = self 374 | .boot_time_ctl 375 | .as_ref() 376 | .ok_or(MetricNotAvailable)? 377 | .value_as::()?; 378 | let duration = Duration::new(time.tv_sec as u64, (time.tv_usec * 1000) as u32); 379 | let bootup_timestamp = UNIX_EPOCH + duration; 380 | 381 | if let Ok(duration) = SystemTime::now().duration_since(bootup_timestamp) { 382 | let seconds_since_boot = duration.as_secs(); 383 | return Ok(seconds_since_boot as usize); 384 | } 385 | 386 | Err(ReadoutError::Other(String::from( 387 | "Error calculating boot time since unix \ 388 | epoch.", 389 | ))) 390 | } 391 | 392 | fn machine(&self) -> Result { 393 | let product_readout = MacOSProductReadout::new(); 394 | product_readout.product() 395 | } 396 | 397 | fn os_name(&self) -> Result { 398 | let version: String = self.operating_system_version()?.into(); 399 | let major_version_name = macos_version_to_name(&self.operating_system_version()?); 400 | 401 | Ok(format!("macOS {version} {major_version_name}")) 402 | } 403 | 404 | fn disk_space(&self, path: &Path) -> Result<(u64, u64), ReadoutError> { 405 | shared::disk_space(path) 406 | } 407 | 408 | fn gpus(&self) -> Result, ReadoutError> { 409 | Err(ReadoutError::NotImplemented) 410 | } 411 | } 412 | 413 | impl MacOSGeneralReadout { 414 | fn operating_system_version(&self) -> Result { 415 | let os_string = self 416 | .os_product_version_ctl 417 | .as_ref() 418 | .ok_or(ReadoutError::MetricNotAvailable)? 419 | .value_string()?; 420 | 421 | let mut string_parts = os_string.split('.'); 422 | 423 | let mut operating_system_version = NSOperatingSystemVersion::default(); 424 | 425 | if let Some(major) = string_parts.next() { 426 | operating_system_version.major_version = major.parse().unwrap_or_default() 427 | } 428 | 429 | if let Some(minor) = string_parts.next() { 430 | operating_system_version.minor_version = minor.parse().unwrap_or_default() 431 | } 432 | 433 | if let Some(patch) = string_parts.next() { 434 | operating_system_version.patch_version = patch.parse().unwrap_or_default() 435 | } 436 | 437 | Ok(operating_system_version) 438 | } 439 | } 440 | 441 | impl MemoryReadout for MacOSMemoryReadout { 442 | fn new() -> Self { 443 | let page_size = match Ctl::new("hw.pagesize").unwrap().value().unwrap() { 444 | sysctl::CtlValue::S64(s) => s, 445 | _ => panic!("Could not get vm page size."), 446 | }; 447 | 448 | let physical_mem = match Ctl::new("hw.memsize").unwrap().value().unwrap() { 449 | sysctl::CtlValue::S64(s) => s, 450 | _ => panic!("Could not get physical memory size."), 451 | }; 452 | 453 | MacOSMemoryReadout { 454 | page_size, 455 | physical_memory: physical_mem, 456 | } 457 | } 458 | 459 | fn total(&self) -> Result { 460 | Ok(self.physical_memory as u64 / 1024) 461 | } 462 | 463 | fn free(&self) -> Result { 464 | let vm_stats = MacOSMemoryReadout::mach_vm_stats()?; 465 | let free_count: u64 = 466 | (vm_stats.free_count + vm_stats.inactive_count - vm_stats.speculative_count) as u64; 467 | 468 | Ok(((free_count * self.page_size as u64) / 1024) as u64) 469 | } 470 | 471 | fn buffers(&self) -> Result { 472 | Err(ReadoutError::NotImplemented) 473 | } 474 | 475 | fn cached(&self) -> Result { 476 | Err(ReadoutError::NotImplemented) 477 | } 478 | 479 | fn reclaimable(&self) -> Result { 480 | let vm_stats = MacOSMemoryReadout::mach_vm_stats()?; 481 | Ok((vm_stats.purgeable_count as u64 * self.page_size as u64 / 1024) as u64) 482 | } 483 | 484 | fn used(&self) -> Result { 485 | let vm_stats = MacOSMemoryReadout::mach_vm_stats()?; 486 | let used: u64 = ((vm_stats.active_count + vm_stats.wire_count) as u64 487 | * self.page_size as u64 488 | / 1024) as u64; 489 | 490 | Ok(used) 491 | } 492 | 493 | fn swap_total(&self) -> Result { 494 | return Err(ReadoutError::NotImplemented); 495 | } 496 | 497 | fn swap_free(&self) -> Result { 498 | return Err(ReadoutError::NotImplemented); 499 | } 500 | 501 | fn swap_used(&self) -> Result { 502 | return Err(ReadoutError::NotImplemented); 503 | } 504 | } 505 | 506 | impl MacOSMemoryReadout { 507 | fn mach_vm_stats() -> Result { 508 | use mach2::kern_return::KERN_SUCCESS; 509 | use mach2::message::mach_msg_type_number_t; 510 | use mach2::vm_types::integer_t; 511 | use mach_ffi::*; 512 | 513 | const HOST_VM_INFO_COUNT: mach_msg_type_number_t = 514 | (std::mem::size_of::() / std::mem::size_of::()) as u32; 515 | 516 | const HOST_VM_INFO64: integer_t = 4; 517 | 518 | let mut vm_stat: vm_statistics64 = std::default::Default::default(); 519 | let vm_stat_ptr: *mut vm_statistics64 = &mut vm_stat; 520 | let mut count: mach_msg_type_number_t = HOST_VM_INFO_COUNT; 521 | 522 | let ret_val = unsafe { 523 | host_statistics64( 524 | mach_host_self(), 525 | HOST_VM_INFO64, 526 | vm_stat_ptr as *mut integer_t, 527 | &mut count as *mut mach_msg_type_number_t, 528 | ) 529 | }; 530 | 531 | if ret_val == KERN_SUCCESS { 532 | return Ok(vm_stat); 533 | } 534 | 535 | Err(ReadoutError::Other(String::from( 536 | "Could not retrieve vm statistics from host.", 537 | ))) 538 | } 539 | } 540 | 541 | #[derive(Copy, Clone, Default)] 542 | #[repr(C)] 543 | struct NSOperatingSystemVersion { 544 | major_version: u64, 545 | minor_version: u64, 546 | patch_version: u64, 547 | } 548 | 549 | impl From for String { 550 | fn from(s: NSOperatingSystemVersion) -> String { 551 | format!( 552 | "{}.{}.{}", 553 | s.major_version, s.minor_version, s.patch_version 554 | ) 555 | } 556 | } 557 | 558 | impl ProductReadout for MacOSProductReadout { 559 | fn new() -> Self { 560 | MacOSProductReadout { 561 | hw_model_ctl: Ctl::new("hw.model").ok(), 562 | } 563 | } 564 | 565 | fn vendor(&self) -> Result { 566 | Ok(String::from("Apple")) 567 | } 568 | 569 | fn family(&self) -> Result { 570 | Err(ReadoutError::NotImplemented) 571 | } 572 | 573 | fn product(&self) -> Result { 574 | let mac_model = self 575 | .hw_model_ctl 576 | .as_ref() 577 | .ok_or(MetricNotAvailable)? 578 | .value_string()?; 579 | 580 | Ok(mac_model) 581 | } 582 | } 583 | 584 | impl PackageReadout for MacOSPackageReadout { 585 | fn new() -> Self { 586 | MacOSPackageReadout 587 | } 588 | 589 | fn count_pkgs(&self) -> Vec<(PackageManager, usize)> { 590 | let mut packages = Vec::new(); 591 | if extra::which("brew") { 592 | if let Some(c) = MacOSPackageReadout::count_homebrew() { 593 | packages.push((PackageManager::Homebrew, c)) 594 | } 595 | } 596 | 597 | if let Some(c) = MacOSPackageReadout::count_cargo() { 598 | packages.push((PackageManager::Cargo, c)) 599 | } 600 | 601 | packages 602 | } 603 | } 604 | 605 | impl MacOSPackageReadout { 606 | /// This method returns the total entries of `/usr/local/Cellar` and `/usr/local/Caskroom` directories 607 | /// which contain all installed packages of the Homebrew package manager. 608 | /// A manual call via `homebrew list` would be too expensive, since it is pretty slow. 609 | fn count_homebrew() -> Option { 610 | use std::fs::read_dir; 611 | use std::path::Path; 612 | 613 | // Homebrew stores packages in /usr/local on older-generation Apple hardware. 614 | let homebrew_root = Path::new("/usr/local"); 615 | let cellar_folder = homebrew_root.join("Cellar"); 616 | let caskroom_folder = homebrew_root.join("Caskroom"); 617 | 618 | let hidden_files_filter = |f: &Result| match f { 619 | Ok(entry) => !entry.file_name().to_str().unwrap().starts_with('.'), 620 | Err(_) => false, 621 | }; 622 | 623 | let cellar_count = match read_dir(cellar_folder) { 624 | Ok(read_dir) => read_dir.filter(hidden_files_filter).count(), 625 | Err(_) => 0, 626 | }; 627 | 628 | let caskroom_count = match read_dir(caskroom_folder) { 629 | Ok(read_dir) => read_dir.filter(hidden_files_filter).count(), 630 | Err(_) => 0, 631 | }; 632 | 633 | // Homebrew stores packages in /opt/homebrew on Apple Silicon machines. 634 | let opt_homebrew_root = Path::new("/opt/homebrew"); 635 | let opt_cellar_folder = opt_homebrew_root.join("Cellar"); 636 | let opt_caskroom_folder = opt_homebrew_root.join("Caskroom"); 637 | 638 | let opt_cellar_count = match read_dir(opt_cellar_folder) { 639 | Ok(read_dir) => read_dir.filter(hidden_files_filter).count(), 640 | Err(_) => 0, 641 | }; 642 | 643 | let opt_caskroom_count = match read_dir(opt_caskroom_folder) { 644 | Ok(read_dir) => read_dir.filter(hidden_files_filter).count(), 645 | Err(_) => 0, 646 | }; 647 | 648 | Some(cellar_count + caskroom_count + opt_cellar_count + opt_caskroom_count) 649 | } 650 | 651 | fn count_cargo() -> Option { 652 | shared::count_cargo() 653 | } 654 | } 655 | 656 | impl NetworkReadout for MacOSNetworkReadout { 657 | fn new() -> Self { 658 | MacOSNetworkReadout 659 | } 660 | 661 | fn tx_bytes(&self, _: Option<&str>) -> Result { 662 | Err(ReadoutError::NotImplemented) 663 | } 664 | 665 | fn tx_packets(&self, _: Option<&str>) -> Result { 666 | Err(ReadoutError::NotImplemented) 667 | } 668 | 669 | fn rx_bytes(&self, _: Option<&str>) -> Result { 670 | Err(ReadoutError::NotImplemented) 671 | } 672 | 673 | fn rx_packets(&self, _: Option<&str>) -> Result { 674 | Err(ReadoutError::NotImplemented) 675 | } 676 | 677 | fn logical_address(&self, interface: Option<&str>) -> Result { 678 | shared::logical_address(interface) 679 | } 680 | 681 | fn physical_address(&self, _: Option<&str>) -> Result { 682 | Err(ReadoutError::NotImplemented) 683 | } 684 | } 685 | 686 | fn macos_version_to_name(version: &NSOperatingSystemVersion) -> &'static str { 687 | match (version.major_version, version.minor_version) { 688 | (10, 1) => "Puma", 689 | (10, 2) => "Jaguar", 690 | (10, 3) => "Panther", 691 | (10, 4) => "Tiger", 692 | (10, 5) => "Leopard", 693 | (10, 6) => "Snow Leopard", 694 | (10, 7) => "Lion", 695 | (10, 8) => "Mountain Lion", 696 | (10, 9) => "Mavericks", 697 | (10, 10) => "Yosemite", 698 | (10, 11) => "El Capitan", 699 | (10, 12) => "Sierra", 700 | (10, 13) => "High Sierra", 701 | (10, 14) => "Mojave", 702 | (10, 15) => "Catalina", 703 | (11, _) | (10, 16) => "Big Sur", 704 | (12, _) => "Monterey", 705 | (13, _) => "Ventura", 706 | (14, _) => "Sonoma", 707 | (15, _) => "Sequoia", 708 | (26, _) => "Tahoe", 709 | _ => "Unknown", 710 | } 711 | } 712 | -------------------------------------------------------------------------------- /src/linux/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::unnecessary_cast)] 2 | mod pci_devices; 3 | mod sysinfo_ffi; 4 | 5 | use self::pci_devices::get_pci_devices; 6 | use crate::extra; 7 | use crate::extra::get_entries; 8 | use crate::extra::path_extension; 9 | use crate::shared; 10 | use crate::traits::*; 11 | use itertools::Itertools; 12 | use pciid_parser::Database; 13 | use regex::Regex; 14 | use std::ffi::OsStr; 15 | use std::fs; 16 | use std::fs::read_dir; 17 | use std::fs::File; 18 | use std::io::{BufRead, BufReader}; 19 | use std::path::{Path, PathBuf}; 20 | use std::process::{Command, Stdio}; 21 | use sysctl::{Ctl, Sysctl}; 22 | use sysinfo_ffi::sysinfo; 23 | 24 | impl From for ReadoutError { 25 | fn from(e: sqlite::Error) -> Self { 26 | ReadoutError::Other(e.to_string()) 27 | } 28 | } 29 | 30 | pub struct LinuxKernelReadout { 31 | os_release_ctl: Option, 32 | os_type_ctl: Option, 33 | } 34 | 35 | pub struct LinuxGeneralReadout { 36 | hostname_ctl: Option, 37 | sysinfo: sysinfo, 38 | } 39 | 40 | pub struct LinuxMemoryReadout { 41 | sysinfo: sysinfo, 42 | } 43 | 44 | pub struct LinuxBatteryReadout; 45 | pub struct LinuxProductReadout; 46 | pub struct LinuxPackageReadout; 47 | pub struct LinuxNetworkReadout; 48 | 49 | impl BatteryReadout for LinuxBatteryReadout { 50 | fn new() -> Self { 51 | LinuxBatteryReadout 52 | } 53 | 54 | fn percentage(&self) -> Result { 55 | if let Some(entries) = get_entries(Path::new("/sys/class/power_supply")) { 56 | let dirs: Vec = entries 57 | .into_iter() 58 | .filter(|x| { 59 | x.components() 60 | .last() 61 | .unwrap() 62 | .as_os_str() 63 | .to_string_lossy() 64 | .starts_with("BAT") 65 | }) 66 | .collect(); 67 | 68 | if let Some(battery) = dirs.first() { 69 | let path_to_capacity = battery.join("capacity"); 70 | let percentage_text = extra::pop_newline(fs::read_to_string(path_to_capacity)?); 71 | let percentage_parsed = percentage_text.parse::(); 72 | 73 | match percentage_parsed { 74 | Ok(p) => return Ok(p), 75 | Err(e) => { 76 | return Err(ReadoutError::Other(format!( 77 | "Could not parse the value '{percentage_text}' into a digit: {e:?}" 78 | ))) 79 | } 80 | }; 81 | } 82 | }; 83 | 84 | Err(ReadoutError::Other("No batteries detected.".to_string())) 85 | } 86 | 87 | fn status(&self) -> Result { 88 | if let Some(entries) = get_entries(Path::new("/sys/class/power_supply")) { 89 | let dirs: Vec = entries 90 | .into_iter() 91 | .filter(|x| { 92 | x.components() 93 | .last() 94 | .unwrap() 95 | .as_os_str() 96 | .to_string_lossy() 97 | .starts_with("BAT") 98 | }) 99 | .collect(); 100 | 101 | if let Some(battery) = dirs.first() { 102 | let path_to_status = battery.join("status"); 103 | let status_text = 104 | extra::pop_newline(fs::read_to_string(path_to_status)?).to_lowercase(); 105 | 106 | match &status_text[..] { 107 | "charging" => return Ok(BatteryState::Charging), 108 | "discharging" | "full" => return Ok(BatteryState::Discharging), 109 | s => { 110 | return Err(ReadoutError::Other(format!( 111 | "Got an unexpected value \"{s}\" reading battery status" 112 | ))) 113 | } 114 | } 115 | } 116 | } 117 | 118 | Err(ReadoutError::Other("No batteries detected.".to_string())) 119 | } 120 | 121 | fn health(&self) -> Result { 122 | if let Some(entries) = get_entries(Path::new("/sys/class/power_supply")) { 123 | let dirs: Vec = entries 124 | .into_iter() 125 | .filter(|x| { 126 | !x.components() 127 | .last() 128 | .unwrap() 129 | .as_os_str() 130 | .to_string_lossy() 131 | .starts_with("ADP") 132 | }) 133 | .collect(); 134 | 135 | if let Some(battery) = dirs.first() { 136 | let energy_full = 137 | extra::pop_newline(fs::read_to_string(battery.join("energy_full"))?) 138 | .parse::(); 139 | 140 | let energy_full_design = 141 | extra::pop_newline(fs::read_to_string(battery.join("energy_full_design"))?) 142 | .parse::(); 143 | 144 | match (energy_full, energy_full_design) { 145 | (Ok(mut ef), Ok(efd)) => { 146 | if ef > efd { 147 | ef = efd; 148 | return Ok((ef as f32 / efd as f32 * 100_f32).ceil() as u8); 149 | } 150 | 151 | return Ok((ef as f32 / efd as f32 * 100_f32).ceil() as u8); 152 | } 153 | _ => { 154 | return Err(ReadoutError::Other( 155 | "Error calculating battery health.".to_string(), 156 | )) 157 | } 158 | } 159 | } 160 | } 161 | 162 | Err(ReadoutError::Other("No batteries detected.".to_string())) 163 | } 164 | } 165 | 166 | impl KernelReadout for LinuxKernelReadout { 167 | fn new() -> Self { 168 | LinuxKernelReadout { 169 | os_release_ctl: Ctl::new("kernel.osrelease").ok(), 170 | os_type_ctl: Ctl::new("kernel.ostype").ok(), 171 | } 172 | } 173 | 174 | fn os_release(&self) -> Result { 175 | Ok(self 176 | .os_release_ctl 177 | .as_ref() 178 | .ok_or(ReadoutError::MetricNotAvailable)? 179 | .value_string()?) 180 | } 181 | 182 | fn os_type(&self) -> Result { 183 | Ok(self 184 | .os_type_ctl 185 | .as_ref() 186 | .ok_or(ReadoutError::MetricNotAvailable)? 187 | .value_string()?) 188 | } 189 | } 190 | 191 | impl NetworkReadout for LinuxNetworkReadout { 192 | fn new() -> Self { 193 | LinuxNetworkReadout 194 | } 195 | 196 | fn tx_bytes(&self, interface: Option<&str>) -> Result { 197 | if let Some(ifname) = interface { 198 | let rx_file = PathBuf::from("/sys/class/net") 199 | .join(ifname) 200 | .join("statistics/tx_bytes"); 201 | let content = std::fs::read_to_string(rx_file)?; 202 | let bytes = extra::pop_newline(content) 203 | .parse::() 204 | .unwrap_or_default(); 205 | Ok(bytes) 206 | } else { 207 | Err(ReadoutError::Other(String::from( 208 | "Please specify a network interface to query.", 209 | ))) 210 | } 211 | } 212 | 213 | fn tx_packets(&self, interface: Option<&str>) -> Result { 214 | if let Some(ifname) = interface { 215 | let rx_file = PathBuf::from("/sys/class/net") 216 | .join(ifname) 217 | .join("statistics/tx_packets"); 218 | let content = std::fs::read_to_string(rx_file)?; 219 | let packets = extra::pop_newline(content) 220 | .parse::() 221 | .unwrap_or_default(); 222 | Ok(packets) 223 | } else { 224 | Err(ReadoutError::Other(String::from( 225 | "Please specify a network interface to query.", 226 | ))) 227 | } 228 | } 229 | 230 | fn rx_bytes(&self, interface: Option<&str>) -> Result { 231 | if let Some(ifname) = interface { 232 | let rx_file = PathBuf::from("/sys/class/net") 233 | .join(ifname) 234 | .join("statistics/rx_bytes"); 235 | let content = std::fs::read_to_string(rx_file)?; 236 | let bytes = extra::pop_newline(content) 237 | .parse::() 238 | .unwrap_or_default(); 239 | Ok(bytes) 240 | } else { 241 | Err(ReadoutError::Other(String::from( 242 | "Please specify a network interface to query.", 243 | ))) 244 | } 245 | } 246 | 247 | fn rx_packets(&self, interface: Option<&str>) -> Result { 248 | if let Some(ifname) = interface { 249 | let rx_file = PathBuf::from("/sys/class/net") 250 | .join(ifname) 251 | .join("statistics/rx_packets"); 252 | let content = std::fs::read_to_string(rx_file)?; 253 | let packets = extra::pop_newline(content) 254 | .parse::() 255 | .unwrap_or_default(); 256 | Ok(packets) 257 | } else { 258 | Err(ReadoutError::Other(String::from( 259 | "Please specify a network interface to query.", 260 | ))) 261 | } 262 | } 263 | 264 | fn physical_address(&self, interface: Option<&str>) -> Result { 265 | if let Some(ifname) = interface { 266 | let rx_file = PathBuf::from("/sys/class/net").join(ifname).join("address"); 267 | let content = std::fs::read_to_string(rx_file)?; 268 | Ok(content) 269 | } else { 270 | Err(ReadoutError::Other(String::from( 271 | "Please specify a network interface to query.", 272 | ))) 273 | } 274 | } 275 | 276 | fn logical_address(&self, interface: Option<&str>) -> Result { 277 | shared::logical_address(interface) 278 | } 279 | } 280 | 281 | impl GeneralReadout for LinuxGeneralReadout { 282 | fn new() -> Self { 283 | LinuxGeneralReadout { 284 | hostname_ctl: Ctl::new("kernel.hostname").ok(), 285 | sysinfo: sysinfo::new(), 286 | } 287 | } 288 | 289 | fn backlight(&self) -> Result { 290 | if let Some(base) = get_entries(Path::new("/sys/class/backlight/")) { 291 | if let Some(backlight_path) = base.into_iter().next() { 292 | let max_brightness_path = backlight_path.join("max_brightness"); 293 | let current_brightness_path = backlight_path.join("brightness"); 294 | 295 | let max_brightness_value = 296 | extra::pop_newline(fs::read_to_string(max_brightness_path)?) 297 | .parse::() 298 | .ok(); 299 | 300 | let current_brightness_value = 301 | extra::pop_newline(fs::read_to_string(current_brightness_path)?) 302 | .parse::() 303 | .ok(); 304 | 305 | match (current_brightness_value, max_brightness_value) { 306 | (Some(c), Some(m)) => { 307 | let brightness = c as f64 / m as f64 * 100f64; 308 | return Ok(brightness.round() as usize); 309 | } 310 | _ => { 311 | return Err(ReadoutError::Other(String::from( 312 | "Error occurred while calculating backlight (brightness) value.", 313 | ))); 314 | } 315 | } 316 | } 317 | } 318 | 319 | Err(ReadoutError::Other(String::from( 320 | "Could not obtain backlight information.", 321 | ))) 322 | } 323 | 324 | fn resolution(&self) -> Result { 325 | let drm = Path::new("/sys/class/drm"); 326 | 327 | if let Some(entries) = get_entries(drm) { 328 | let mut resolutions: Vec = Vec::new(); 329 | entries.into_iter().for_each(|entry| { 330 | // Append "modes" to /sys/class/drm// 331 | let modes = entry.join("modes"); 332 | if let Ok(file) = File::open(modes) { 333 | // Push the resolution to the resolutions vector. 334 | if let Some(Ok(res)) = BufReader::new(file).lines().next() { 335 | resolutions.push(res); 336 | } 337 | } 338 | }); 339 | 340 | return Ok(resolutions.join(", ")); 341 | } 342 | 343 | Err(ReadoutError::Other( 344 | "Could not obtain screen resolution from /sys/class/drm".to_string(), 345 | )) 346 | } 347 | 348 | fn username(&self) -> Result { 349 | shared::username() 350 | } 351 | 352 | fn hostname(&self) -> Result { 353 | Ok(self 354 | .hostname_ctl 355 | .as_ref() 356 | .ok_or(ReadoutError::MetricNotAvailable)? 357 | .value_string()?) 358 | } 359 | 360 | fn distribution(&self) -> Result { 361 | use os_release::OsRelease; 362 | let content = OsRelease::new()?; 363 | 364 | if !content.version.is_empty() { 365 | return Ok(format!("{} {}", content.name, content.version)); 366 | } else if !content.version_id.is_empty() { 367 | return Ok(format!("{} {}", content.name, content.version_id)); 368 | } 369 | 370 | Ok(content.name) 371 | } 372 | 373 | fn desktop_environment(&self) -> Result { 374 | shared::desktop_environment() 375 | } 376 | 377 | fn session(&self) -> Result { 378 | shared::session() 379 | } 380 | 381 | fn window_manager(&self) -> Result { 382 | shared::window_manager() 383 | } 384 | 385 | fn terminal(&self) -> Result { 386 | // This function returns the PPID of a given PID: 387 | // - The file used to extract this data: /proc//status 388 | // - This function parses and returns the value of the ppid line. 389 | fn get_parent(pid: i32) -> i32 { 390 | let process_path = PathBuf::from("/proc").join(pid.to_string()).join("status"); 391 | let file = File::open(process_path); 392 | match file { 393 | Ok(content) => { 394 | let reader = BufReader::new(content); 395 | for line in reader.lines().map_while(Result::ok) { 396 | if line.to_uppercase().starts_with("PPID") { 397 | let s_mem_kb: String = 398 | line.chars().filter(|c| c.is_ascii_digit()).collect(); 399 | return s_mem_kb.parse::().unwrap_or(-1); 400 | } 401 | } 402 | 403 | -1 404 | } 405 | 406 | Err(_) => -1, 407 | } 408 | } 409 | 410 | // This function returns the name associated with a given PPID 411 | fn terminal_name() -> String { 412 | let mut terminal_pid = get_parent(unsafe { libc::getppid() }); 413 | 414 | let path = PathBuf::from("/proc") 415 | .join(terminal_pid.to_string()) 416 | .join("comm"); 417 | 418 | // The below loop will traverse /proc to find the 419 | // terminal inside of which the user is operating 420 | if let Ok(mut terminal_name) = fs::read_to_string(path) { 421 | // Any command_name we find that matches 422 | // one of the elements within this table 423 | // is effectively ignored 424 | while extra::common_shells().contains(&terminal_name.replace('\n', "").as_str()) { 425 | let ppid = get_parent(terminal_pid); 426 | terminal_pid = ppid; 427 | 428 | let path = PathBuf::from("/proc").join(ppid.to_string()).join("comm"); 429 | 430 | if let Ok(comm) = fs::read_to_string(path) { 431 | terminal_name = comm; 432 | } 433 | } 434 | 435 | return terminal_name; 436 | } 437 | 438 | String::new() 439 | } 440 | 441 | let terminal = terminal_name(); 442 | 443 | if terminal.is_empty() { 444 | return Err(ReadoutError::Other( 445 | "Querying terminal information failed".to_string(), 446 | )); 447 | } 448 | 449 | Ok(terminal) 450 | } 451 | 452 | fn shell(&self, format: ShellFormat, kind: ShellKind) -> Result { 453 | shared::shell(format, kind) 454 | } 455 | 456 | fn cpu_model_name(&self) -> Result { 457 | Ok(shared::cpu_model_name()) 458 | } 459 | 460 | fn cpu_usage(&self) -> Result { 461 | let mut info = self.sysinfo; 462 | let info_ptr: *mut sysinfo = &mut info; 463 | let ret = unsafe { sysinfo(info_ptr) }; 464 | 465 | if ret != -1 { 466 | let f_load = 1f64 / (1 << libc::SI_LOAD_SHIFT) as f64; 467 | let cpu_usage = info.loads[0] as f64 * f_load; 468 | let cpu_usage_u = 469 | (cpu_usage / self.cpu_cores().unwrap() as f64 * 100.0).round() as usize; 470 | return Ok(cpu_usage_u as usize); 471 | } 472 | 473 | Err(ReadoutError::Other( 474 | "Something went wrong during the initialization of the sysinfo struct.".to_string(), 475 | )) 476 | } 477 | 478 | fn cpu_physical_cores(&self) -> Result { 479 | use std::io::{BufRead, BufReader}; 480 | if let Ok(content) = File::open("/proc/cpuinfo") { 481 | let reader = BufReader::new(content); 482 | for line in reader.lines().map_while(Result::ok) { 483 | if line.to_lowercase().starts_with("cpu cores") { 484 | return Ok(line 485 | .split(':') 486 | .nth(1) 487 | .unwrap() 488 | .trim() 489 | .parse::() 490 | .unwrap()); 491 | } 492 | } 493 | } 494 | 495 | Err(ReadoutError::MetricNotAvailable) 496 | } 497 | 498 | fn cpu_cores(&self) -> Result { 499 | Ok(unsafe { libc::sysconf(libc::_SC_NPROCESSORS_CONF) } as usize) 500 | } 501 | 502 | fn uptime(&self) -> Result { 503 | let mut info = self.sysinfo; 504 | let info_ptr: *mut sysinfo = &mut info; 505 | let ret = unsafe { sysinfo(info_ptr) }; 506 | 507 | if ret != -1 { 508 | return Ok(info.uptime as usize); 509 | } 510 | 511 | Err(ReadoutError::Other( 512 | "Something went wrong during the initialization of the sysinfo struct.".to_string(), 513 | )) 514 | } 515 | 516 | fn machine(&self) -> Result { 517 | let product_readout = LinuxProductReadout::new(); 518 | 519 | let vendor = product_readout.vendor()?; 520 | let family = product_readout.family()?; 521 | let product = product_readout.product()?; 522 | let version = extra::pop_newline(fs::read_to_string("/sys/class/dmi/id/product_version")?); 523 | 524 | // If one field is generic, the others are likely the same, so fail the readout. 525 | if vendor.eq_ignore_ascii_case("system manufacturer") { 526 | return Err(ReadoutError::Other(String::from( 527 | "Your manufacturer may have not specified your machine's product information.", 528 | ))); 529 | } 530 | 531 | let new_product = 532 | format!("{vendor} {family} {product} {version}").replace("To be filled by O.E.M.", ""); 533 | 534 | if family == product && family == version { 535 | return Ok(family); 536 | } else if version.is_empty() || version.len() <= 22 { 537 | return Ok(new_product.split_whitespace().unique().join(" ")); 538 | } 539 | 540 | Ok(version) 541 | } 542 | 543 | fn os_name(&self) -> Result { 544 | Err(ReadoutError::NotImplemented) 545 | } 546 | 547 | fn disk_space(&self, path: &Path) -> Result<(u64, u64), ReadoutError> { 548 | shared::disk_space(path) 549 | } 550 | 551 | fn gpus(&self) -> Result, ReadoutError> { 552 | let db = match Database::read() { 553 | Ok(db) => db, 554 | _ => return Err(ReadoutError::MetricNotAvailable), 555 | }; 556 | 557 | let devices = get_pci_devices()?; 558 | let mut gpus = vec![]; 559 | 560 | for device in devices { 561 | if !device.is_gpu(&db) { 562 | continue; 563 | }; 564 | 565 | if let Some(sub_device_name) = device.get_device_name(&db) { 566 | gpus.push(sub_device_name); 567 | }; 568 | } 569 | 570 | if gpus.is_empty() { 571 | Err(ReadoutError::MetricNotAvailable) 572 | } else { 573 | Ok(gpus) 574 | } 575 | } 576 | } 577 | 578 | impl MemoryReadout for LinuxMemoryReadout { 579 | fn new() -> Self { 580 | LinuxMemoryReadout { 581 | sysinfo: sysinfo::new(), 582 | } 583 | } 584 | 585 | fn total(&self) -> Result { 586 | let mut info = self.sysinfo; 587 | let info_ptr: *mut sysinfo = &mut info; 588 | let ret = unsafe { sysinfo(info_ptr) }; 589 | if ret != -1 { 590 | Ok(info.totalram as u64 * info.mem_unit as u64 / 1024) 591 | } else { 592 | Err(ReadoutError::Other( 593 | "Something went wrong during the initialization of the sysinfo struct.".to_string(), 594 | )) 595 | } 596 | } 597 | 598 | fn free(&self) -> Result { 599 | let mut info = self.sysinfo; 600 | let info_ptr: *mut sysinfo = &mut info; 601 | let ret = unsafe { sysinfo(info_ptr) }; 602 | if ret != -1 { 603 | Ok(info.freeram as u64 * info.mem_unit as u64 / 1024) 604 | } else { 605 | Err(ReadoutError::Other( 606 | "Something went wrong during the initialization of the sysinfo struct.".to_string(), 607 | )) 608 | } 609 | } 610 | 611 | fn buffers(&self) -> Result { 612 | let mut info = self.sysinfo; 613 | let info_ptr: *mut sysinfo = &mut info; 614 | let ret = unsafe { sysinfo(info_ptr) }; 615 | if ret != -1 { 616 | Ok(info.bufferram as u64 * info.mem_unit as u64 / 1024) 617 | } else { 618 | Err(ReadoutError::Other( 619 | "Something went wrong during the initialization of the sysinfo struct.".to_string(), 620 | )) 621 | } 622 | } 623 | 624 | fn cached(&self) -> Result { 625 | Ok(shared::get_meminfo_value("Cached")) 626 | } 627 | 628 | fn reclaimable(&self) -> Result { 629 | Ok(shared::get_meminfo_value("SReclaimable")) 630 | } 631 | 632 | fn used(&self) -> Result { 633 | let total = self.total().unwrap(); 634 | match shared::get_meminfo_value("MemAvailable") { 635 | 0 => { 636 | let free = self.free().unwrap(); 637 | let cached = self.cached().unwrap(); 638 | let reclaimable = self.reclaimable().unwrap(); 639 | let buffers = self.buffers().unwrap(); 640 | Ok(total - free - cached - reclaimable - buffers) 641 | } 642 | available => Ok(total - available), 643 | } 644 | } 645 | 646 | fn swap_total(&self) -> Result { 647 | let mut info = self.sysinfo; 648 | let info_ptr: *mut sysinfo = &mut info; 649 | let ret = unsafe { sysinfo(info_ptr) }; 650 | if ret != -1 { 651 | Ok(info.totalswap as u64 * info.mem_unit as u64 / 1024) 652 | } else { 653 | Err(ReadoutError::Other( 654 | "Something went wrong during the initialization of the sysinfo struct.".to_string(), 655 | )) 656 | } 657 | } 658 | 659 | fn swap_free(&self) -> Result { 660 | let mut info = self.sysinfo; 661 | let info_ptr: *mut sysinfo = &mut info; 662 | let ret = unsafe { sysinfo(info_ptr) }; 663 | if ret != -1 { 664 | Ok(info.freeswap as u64 * info.mem_unit as u64 / 1024) 665 | } else { 666 | Err(ReadoutError::Other( 667 | "Something went wrong during the initialization of the sysinfo struct.".to_string(), 668 | )) 669 | } 670 | } 671 | 672 | fn swap_used(&self) -> Result { 673 | let mut info = self.sysinfo; 674 | let info_ptr: *mut sysinfo = &mut info; 675 | let ret = unsafe { sysinfo(info_ptr) }; 676 | if ret != -1 { 677 | Ok((info.totalswap as u64 - info.freeswap as u64) * info.mem_unit as u64 / 1024) 678 | } else { 679 | Err(ReadoutError::Other( 680 | "Something went wrong during the initialization of the sysinfo struct.".to_string(), 681 | )) 682 | } 683 | } 684 | } 685 | 686 | impl ProductReadout for LinuxProductReadout { 687 | fn new() -> Self { 688 | LinuxProductReadout 689 | } 690 | 691 | fn vendor(&self) -> Result { 692 | Ok(extra::pop_newline(fs::read_to_string( 693 | "/sys/class/dmi/id/sys_vendor", 694 | )?)) 695 | } 696 | 697 | fn family(&self) -> Result { 698 | Ok(extra::pop_newline(fs::read_to_string( 699 | "/sys/class/dmi/id/product_family", 700 | )?)) 701 | } 702 | 703 | fn product(&self) -> Result { 704 | Ok(extra::pop_newline(fs::read_to_string( 705 | "/sys/class/dmi/id/product_name", 706 | )?)) 707 | } 708 | } 709 | 710 | impl PackageReadout for LinuxPackageReadout { 711 | fn new() -> Self { 712 | LinuxPackageReadout 713 | } 714 | 715 | fn count_pkgs(&self) -> Vec<(PackageManager, usize)> { 716 | let mut packages = Vec::new(); 717 | let mut home = PathBuf::new(); 718 | 719 | // Acquire the value of HOME early on to avoid 720 | // doing it multiple times. 721 | if let Ok(path) = std::env::var("HOME") { 722 | home = PathBuf::from(path); 723 | } 724 | 725 | if let Some(c) = LinuxPackageReadout::count_pacman() { 726 | packages.push((PackageManager::Pacman, c)); 727 | } 728 | 729 | if let Some(c) = LinuxPackageReadout::count_dpkg() { 730 | packages.push((PackageManager::Dpkg, c)); 731 | } 732 | 733 | if let Some(c) = LinuxPackageReadout::count_rpm() { 734 | packages.push((PackageManager::Rpm, c)); 735 | } 736 | 737 | if let Some(c) = LinuxPackageReadout::count_portage() { 738 | packages.push((PackageManager::Portage, c)); 739 | } 740 | 741 | if let Some(c) = LinuxPackageReadout::count_cargo() { 742 | packages.push((PackageManager::Cargo, c)); 743 | } 744 | 745 | if let Some(c) = LinuxPackageReadout::count_xbps() { 746 | packages.push((PackageManager::Xbps, c)); 747 | } 748 | 749 | if let Some(c) = LinuxPackageReadout::count_eopkg() { 750 | packages.push((PackageManager::Eopkg, c)); 751 | } 752 | 753 | if let Some(c) = LinuxPackageReadout::count_apk() { 754 | packages.push((PackageManager::Apk, c)); 755 | } 756 | 757 | if let Some(c) = LinuxPackageReadout::count_flatpak(&home) { 758 | packages.push((PackageManager::Flatpak, c)); 759 | } 760 | 761 | if let Some(c) = LinuxPackageReadout::count_snap() { 762 | packages.push((PackageManager::Snap, c)); 763 | } 764 | 765 | if let Some(c) = LinuxPackageReadout::count_homebrew(&home) { 766 | packages.push((PackageManager::Homebrew, c)); 767 | } 768 | 769 | if let Some(c) = LinuxPackageReadout::count_nix() { 770 | packages.push((PackageManager::Nix, c)); 771 | } 772 | 773 | packages 774 | } 775 | } 776 | 777 | impl LinuxPackageReadout { 778 | /// Returns the number of installed packages for systems 779 | /// that utilize `rpm` as their package manager. 780 | fn count_rpm() -> Option { 781 | // Return the number of installed packages using sqlite (~1ms) 782 | // as directly calling rpm or dnf is too expensive (~500ms) 783 | let count_sqlite = 'sqlite: { 784 | let db = "/var/lib/rpm/rpmdb.sqlite"; 785 | if !Path::new(db).is_file() { 786 | break 'sqlite None; 787 | } 788 | 789 | let connection = sqlite::open(db); 790 | if let Ok(con) = connection { 791 | let statement = con.prepare("SELECT COUNT(*) FROM Installtid"); 792 | if let Ok(mut s) = statement { 793 | if s.next().is_ok() { 794 | break 'sqlite match s.read::, _>(0) { 795 | Ok(Some(count)) => Some(count as usize), 796 | _ => None, 797 | }; 798 | } 799 | } 800 | } 801 | 802 | None 803 | }; 804 | 805 | // If counting with sqlite failed, try using librpm instead 806 | count_sqlite.or_else(|| unsafe { rpm_pkg_count::count() }.map(|count| count as usize)) 807 | } 808 | 809 | /// Returns the number of installed packages for systems 810 | /// that utilize `pacman` as their package manager. 811 | fn count_pacman() -> Option { 812 | let pacman_dir = Path::new("/var/lib/pacman/local"); 813 | if pacman_dir.is_dir() { 814 | if let Ok(read_dir) = read_dir(pacman_dir) { 815 | return Some(read_dir.count() - 1); // Ignore ALPM_DB_VERSION 816 | }; 817 | } 818 | 819 | None 820 | } 821 | 822 | /// Returns the number of installed packages for systems 823 | /// that utilize `eopkg` as their package manager. 824 | fn count_eopkg() -> Option { 825 | let eopkg_dir = Path::new("/var/lib/eopkg/package"); 826 | if eopkg_dir.is_dir() { 827 | if let Ok(read_dir) = read_dir(eopkg_dir) { 828 | return Some(read_dir.count()); 829 | }; 830 | } 831 | 832 | None 833 | } 834 | 835 | /// Returns the number of installed packages for systems 836 | /// that utilize `portage` as their package manager. 837 | fn count_portage() -> Option { 838 | let pkg_dir = Path::new("/var/db/pkg"); 839 | if pkg_dir.exists() { 840 | return Some( 841 | walkdir::WalkDir::new(pkg_dir) 842 | .min_depth(2) 843 | .max_depth(2) 844 | .into_iter() 845 | .count(), 846 | ); 847 | } 848 | 849 | None 850 | } 851 | 852 | /// Returns the number of installed packages for systems 853 | /// that utilize `dpkg` as their package manager. 854 | fn count_dpkg() -> Option { 855 | let dpkg_dir = Path::new("/var/lib/dpkg/info"); 856 | 857 | get_entries(dpkg_dir).map(|entries| { 858 | entries 859 | .iter() 860 | .filter(|x| extra::path_extension(x).unwrap_or_default() == "list") 861 | .count() 862 | }) 863 | } 864 | 865 | /// Returns the number of installed packages for systems 866 | /// that have `homebrew` installed. 867 | fn count_homebrew(home: &Path) -> Option { 868 | let keepme = OsStr::new(".keepme"); 869 | let mut base = home.join(".linuxbrew"); 870 | 871 | if !base.is_dir() { 872 | base = PathBuf::from("/home/linuxbrew/.linuxbrew"); 873 | } 874 | 875 | if !base.is_dir() { 876 | return None; 877 | } 878 | 879 | match read_dir(base.join("Cellar")) { 880 | Ok(dir) => Some( 881 | dir.filter(|entry| match entry { 882 | Err(_) => false, 883 | Ok(file) => file.file_name() != keepme, 884 | }) 885 | .count(), 886 | ), 887 | Err(_) => None, 888 | } 889 | } 890 | 891 | /// Returns the number of installed packages for systems 892 | /// that utilize `xbps` as their package manager. 893 | fn count_xbps() -> Option { 894 | if !extra::which("xbps-query") { 895 | return None; 896 | } 897 | 898 | let xbps_output = Command::new("xbps-query") 899 | .arg("-l") 900 | .stdout(Stdio::piped()) 901 | .output() 902 | .unwrap(); 903 | 904 | extra::count_lines( 905 | String::from_utf8(xbps_output.stdout) 906 | .expect("ERROR: \"xbps-query -l\" output was not valid UTF-8"), 907 | ) 908 | } 909 | 910 | /// Returns the number of installed packages for systems 911 | /// that utilize `apk` as their package manager. 912 | fn count_apk() -> Option { 913 | // faster method for alpine: count empty lines in /lib/apk/db/installed 914 | if let Ok(content) = fs::read_to_string(Path::new("/lib/apk/db/installed")) { 915 | return Some(content.lines().filter(|l| l.is_empty()).count()); 916 | } 917 | 918 | // fallback to command invocation 919 | if !extra::which("apk") { 920 | return None; 921 | } 922 | 923 | let apk_output = Command::new("apk") 924 | .arg("info") 925 | .stdout(Stdio::piped()) 926 | .output() 927 | .unwrap(); 928 | 929 | extra::count_lines( 930 | String::from_utf8(apk_output.stdout) 931 | .expect("ERROR: \"apk info\" output was not valid UTF-8"), 932 | ) 933 | } 934 | 935 | /// Returns the number of installed packages for systems 936 | /// that have `cargo` installed. 937 | fn count_cargo() -> Option { 938 | shared::count_cargo() 939 | } 940 | 941 | /// Returns the number of installed packages for systems 942 | /// that have `flatpak` installed. 943 | fn count_flatpak(home: &Path) -> Option { 944 | let mut total: usize = 0; 945 | let filter = Regex::new(r".*\.(Locale|Debug)").unwrap(); 946 | for install in [ 947 | Path::new("/var/lib/flatpak"), 948 | &home.join(".local/share/flatpak"), 949 | ] { 950 | if install.exists() { 951 | for dir in ["app", "runtime"] { 952 | let pkgdir = install.join(dir); 953 | if pkgdir.exists() { 954 | for package in walkdir::WalkDir::new(&pkgdir) 955 | .min_depth(1) 956 | .max_depth(1) 957 | .into_iter() 958 | .filter_entry(|e| !filter.is_match(&e.path().to_string_lossy())) 959 | { 960 | total += walkdir::WalkDir::new(package.ok()?.path()) 961 | .min_depth(2) 962 | .max_depth(2) 963 | .into_iter() 964 | .count(); 965 | } 966 | } 967 | } 968 | } 969 | } 970 | if total > 0 { 971 | return Some(total); 972 | } 973 | 974 | None 975 | } 976 | 977 | /// Returns the number of installed packages for systems 978 | /// that have `snap` installed. 979 | fn count_snap() -> Option { 980 | let snap_dir = Path::new("/var/lib/snapd/snaps"); 981 | if let Some(entries) = get_entries(snap_dir) { 982 | return Some( 983 | entries 984 | .iter() 985 | .filter(|&x| path_extension(x).unwrap_or_default() == "snap") 986 | .count(), 987 | ); 988 | } 989 | 990 | None 991 | } 992 | 993 | /// Returns the number of installed packages for systems 994 | /// that utilize `nix` as their package manager. 995 | fn count_nix() -> Option { 996 | return 'sqlite: { 997 | let db = "/nix/var/nix/db/db.sqlite"; 998 | if !Path::new(db).is_file() { 999 | break 'sqlite None; 1000 | } 1001 | 1002 | let connection = sqlite::Connection::open_with_flags( 1003 | // The nix store is immutable, so we need to inform sqlite about it 1004 | "file:".to_owned() + db + "?immutable=1", 1005 | sqlite::OpenFlags::new().with_read_only().with_uri(), 1006 | ); 1007 | 1008 | if let Ok(con) = connection { 1009 | let statement = 1010 | con.prepare("SELECT COUNT(path) FROM ValidPaths WHERE sigs IS NOT NULL"); 1011 | 1012 | if let Ok(mut s) = statement { 1013 | if s.next().is_ok() { 1014 | break 'sqlite match s.read::, _>(0) { 1015 | Ok(Some(count)) => Some(count as usize), 1016 | _ => None, 1017 | }; 1018 | } 1019 | } 1020 | } 1021 | 1022 | None 1023 | }; 1024 | } 1025 | } 1026 | --------------------------------------------------------------------------------