use super::{CollectMode, PlatformInfo};
use crate::collectors::command::{run_output, run_stdout, CommandTimeout};
use serde::Deserialize;
use std::env;
use wmi::{COMLibrary, WMIConnection};
#[derive(Deserialize)]
#[serde(rename = "Win32_OperatingSystem")]
#[serde(rename_all = "PascalCase")]
struct Win32OperatingSystem {
caption: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename = "Win32_ComputerSystem")]
#[serde(rename_all = "PascalCase")]
struct Win32ComputerSystem {
manufacturer: Option<String>,
model: Option<String>,
hypervisor_present: Option<bool>,
}
#[derive(Deserialize)]
#[serde(rename = "Win32_VideoController")]
#[serde(rename_all = "PascalCase")]
struct Win32VideoController {
name: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename = "Win32_Battery")]
#[serde(rename_all = "PascalCase")]
struct Win32Battery {
estimated_charge_remaining: Option<u16>,
battery_status: Option<u16>,
}
#[derive(Deserialize)]
#[serde(rename = "Win32_NetworkAdapterConfiguration")]
#[serde(rename_all = "PascalCase")]
struct Win32NetworkAdapterConfig {
#[serde(rename = "IPAddress")]
ip_address: Option<Vec<String>>,
#[serde(rename = "DNSServerSearchOrder")]
dns_server_search_order: Option<Vec<String>>,
interface_index: Option<u32>,
}
#[derive(Deserialize)]
#[serde(rename = "Win32_Processor")]
#[serde(rename_all = "PascalCase")]
#[allow(dead_code)]
struct Win32Processor {
socket_designation: Option<String>,
}
pub fn collect(mode: CollectMode) -> PlatformInfo {
if mode == CollectMode::Fast {
return PlatformInfo {
architecture: get_architecture(),
desktop_environment: Some("Windows Shell".to_string()),
display_server: Some("DWM".to_string()),
windows_edition: None,
boot_mode: None,
virtualization: None,
macos_codename: None,
gpus: get_gpus_fast(),
terminal: get_terminal_fast(),
shell: None,
machine_model: None,
cpu_core_topology: None,
display_resolution: None,
battery: None,
zfs_health: None,
motherboard: None,
bios: None,
ram_slots: None,
locale: None,
encryption: None,
};
}
let wmi_result = COMLibrary::new()
.ok()
.and_then(|com| WMIConnection::new(com).ok());
let (windows_edition, virtualization, gpus, battery) = if let Some(ref wmi) = wmi_result {
let edition = get_windows_edition_wmi(wmi).or_else(get_windows_edition_ps);
let virt = detect_virtualization_wmi(wmi).or_else(detect_virtualization_ps);
let gpu_list = {
let mut gpus = get_gpus_fast();
if gpus.is_empty() {
gpus = get_gpus_wmi(wmi);
}
if gpus.is_empty() {
gpus = get_gpus_ps();
}
filter_software_gpus(gpus)
};
let bat = get_battery_native()
.or_else(|| get_battery_wmi(wmi))
.or_else(get_battery_ps);
(edition, virt, gpu_list, bat)
} else {
let fallback = get_batched_powershell_fallback();
let gpus = if fallback.gpus.is_empty() {
get_gpus_ps()
} else {
fallback.gpus
};
(
fallback.windows_edition.or_else(get_windows_edition_ps),
fallback.virtualization.or_else(detect_virtualization_ps),
gpus,
fallback.battery.or_else(get_battery_ps),
)
};
PlatformInfo {
windows_edition,
boot_mode: detect_boot_mode(),
virtualization,
desktop_environment: Some("Windows Shell".to_string()),
display_server: Some("DWM".to_string()),
macos_codename: None,
gpus,
architecture: get_architecture(),
machine_model: None,
cpu_core_topology: None,
terminal: get_terminal(),
shell: get_shell(),
display_resolution: get_display_resolution(),
battery,
zfs_health: None,
motherboard: None,
bios: None,
ram_slots: None,
locale: get_locale(),
encryption: get_bitlocker_status(),
}
}
fn get_bitlocker_status() -> Option<String> {
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename = "Win32_EncryptableVolume")]
#[serde(rename_all = "PascalCase")]
struct EncryptableVolume {
drive_letter: Option<String>,
protection_status: Option<u32>,
}
#[derive(Deserialize)]
#[serde(rename = "Win32_EncryptableVolume")]
#[serde(rename_all = "PascalCase")]
struct EncryptableVolumeMethod {
drive_letter: Option<String>,
encryption_method: Option<u32>,
}
let com = COMLibrary::new().ok()?;
let wmi =
WMIConnection::with_namespace_path(r"ROOT\CIMV2\Security\MicrosoftVolumeEncryption", com)
.ok()?;
let volumes: Vec<EncryptableVolume> = wmi.query().ok()?;
let methods: Vec<EncryptableVolumeMethod> = wmi.query().ok().unwrap_or_default();
let system_drive = std::env::var("SystemDrive").unwrap_or_else(|_| "C:".to_string());
let target = volumes
.iter()
.find(|v| v.drive_letter.as_deref() == Some(system_drive.as_str()))?;
let protection = target.protection_status?;
let method_for_drive = methods
.iter()
.find(|m| m.drive_letter.as_deref() == Some(system_drive.as_str()))
.and_then(|m| m.encryption_method);
Some(format_bitlocker_status(protection, method_for_drive))
}
fn format_bitlocker_status(protection_status: u32, method: Option<u32>) -> String {
match protection_status {
0 => "BitLocker Off".to_string(),
1 => {
let method_name = method
.map(bitlocker_method_name)
.unwrap_or_else(|| "Unknown".to_string());
format!("BitLocker On ({})", method_name)
}
_ => "BitLocker (status unknown)".to_string(),
}
}
fn bitlocker_method_name(method: u32) -> String {
match method {
0 => "None".to_string(),
1 => "AES-128 + Diffuser".to_string(),
2 => "AES-256 + Diffuser".to_string(),
3 => "AES-128".to_string(),
4 => "AES-256".to_string(),
5 => "Hardware".to_string(),
6 => "XTS-AES-128".to_string(),
7 => "XTS-AES-256".to_string(),
_ => format!("Method #{}", method),
}
}
fn get_terminal_fast() -> Option<String> {
if env::var("WT_SESSION").is_ok() {
return Some("Windows Terminal".to_string());
}
if env::var("TERM_PROGRAM").ok().as_deref() == Some("vscode") {
return Some("VS Code".to_string());
}
Some("Console".to_string())
}
fn get_gpus_fast() -> Vec<String> {
let mut gpus = Vec::new();
if let Some(output) = run_output(
"reg",
[
"query",
r"HKLM\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}",
"/s",
"/v",
"DriverDesc",
],
CommandTimeout::Normal,
) {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("DriverDesc") {
if let Some(value) = line.split("REG_SZ").nth(1) {
let gpu = value.trim();
if !gpu.is_empty() && !gpus.contains(&gpu.to_string()) {
gpus.push(gpu.to_string());
}
}
}
}
}
gpus
}
fn get_windows_edition_wmi(wmi: &WMIConnection) -> Option<String> {
let results: Vec<Win32OperatingSystem> = wmi.query().ok()?;
results.into_iter().next()?.caption
}
fn detect_virtualization_wmi(wmi: &WMIConnection) -> Option<String> {
let cpuid_brand = cpuid_hypervisor_brand();
let results: Vec<Win32ComputerSystem> = wmi.query().ok()?;
let cs = results.into_iter().next()?;
let manufacturer = cs.manufacturer.unwrap_or_default().to_lowercase();
let model = cs.model.unwrap_or_default().to_lowercase();
let dmi = format!("{}|{}", manufacturer, model);
if dmi.contains("vmware") {
return Some("VMware".to_string());
}
if dmi.contains("virtualbox") || dmi.contains("vbox") {
return Some("VirtualBox".to_string());
}
if dmi.contains("qemu") {
return Some("QEMU".to_string());
}
if dmi.contains("xen") {
return Some("Xen".to_string());
}
if dmi.contains("parallels") {
return Some("Parallels".to_string());
}
if dmi.contains("microsoft") && dmi.contains("virtual") {
return Some("Hyper-V".to_string());
}
match cpuid_brand.as_deref() {
Some("Hyper-V") => {
return Some("Bare Metal (Hyper-V/VBS)".to_string());
}
Some(other) => return Some(other.to_string()),
None => {}
}
if cs.hypervisor_present == Some(true) {
return Some("Hypervisor Present".to_string());
}
None
}
fn cpuid_hypervisor_brand() -> Option<String> {
#[cfg(target_arch = "x86")]
use std::arch::x86::__cpuid;
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::__cpuid;
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
return None;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
{
let leaf1 = __cpuid(1);
if (leaf1.ecx & (1u32 << 31)) == 0 {
return None;
}
let leaf = __cpuid(0x4000_0000);
let mut bytes = [0u8; 12];
bytes[0..4].copy_from_slice(&leaf.ebx.to_le_bytes());
bytes[4..8].copy_from_slice(&leaf.ecx.to_le_bytes());
bytes[8..12].copy_from_slice(&leaf.edx.to_le_bytes());
let raw = std::str::from_utf8(&bytes).ok()?.trim_end_matches('\0');
if raw.is_empty() {
return None;
}
Some(map_hypervisor_vendor(raw))
}
}
fn map_hypervisor_vendor(raw: &str) -> String {
match raw {
"KVMKVMKVM" | "KVMKVMKVM\0\0\0" => "KVM".to_string(),
"Microsoft Hv" => "Hyper-V".to_string(),
"VMwareVMware" => "VMware".to_string(),
"VBoxVBoxVBox" => "VirtualBox".to_string(),
"XenVMMXenVMM" => "Xen".to_string(),
"TCGTCGTCGTCG" => "QEMU".to_string(),
"prl hyperv " | "prl hyperv" => "Parallels".to_string(),
"ACRNACRNACRN" => "ACRN".to_string(),
"bhyve bhyve " | "bhyve bhyve" => "bhyve".to_string(),
"QNXQVMBSQG" => "QNX".to_string(),
other => other.trim().to_string(),
}
}
fn get_gpus_wmi(wmi: &WMIConnection) -> Vec<String> {
let results: Vec<Win32VideoController> = wmi.query().unwrap_or_default();
results
.into_iter()
.filter_map(|v| v.name)
.filter(|n| !n.is_empty())
.collect()
}
fn get_battery_wmi(wmi: &WMIConnection) -> Option<String> {
let results: Vec<Win32Battery> = wmi.query().ok()?;
let bat = results.into_iter().next()?;
let charge = bat.estimated_charge_remaining?;
let status_code = bat.battery_status.unwrap_or(0);
let status = match status_code {
1 => "Discharging",
2 => "AC Power",
3 | 6 => "Charging",
4 => "Low",
5 => "Critical",
7 => "Charging High",
8 => "Charging Low",
9 => "Charging Critical",
_ => "Unknown",
};
Some(format!("{}% ({})", charge, status))
}
pub fn get_os_info_from_registry() -> Option<(String, String, String)> {
let mut display_version: Option<String> = None;
let mut current_build: Option<String> = None;
let mut ubr: Option<u32> = None;
let mut product_name: Option<String> = None;
let stdout = run_stdout(
"reg",
[
"query",
r"HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion",
],
CommandTimeout::Normal,
)?;
for line in stdout.lines() {
let mut parts = line.split_whitespace();
let name = match parts.next() {
Some(n) => n,
None => continue,
};
let _type_tok = match parts.next() {
Some(t) => t,
None => continue,
};
let value = parts.collect::<Vec<_>>().join(" ");
match name {
"DisplayVersion" => display_version = Some(value),
"CurrentBuild" => current_build = Some(value),
"UBR" => {
ubr = u32::from_str_radix(value.trim_start_matches("0x"), 16).ok();
}
"ProductName" => product_name = Some(value),
_ => {}
}
}
let build_num: u32 = current_build.as_deref()?.parse().ok()?;
let name = "Windows".to_string();
let release = if build_num >= 22000 {
"11"
} else if build_num >= 10240 {
"10"
} else {
return Some((
product_name
.clone()
.unwrap_or_else(|| "Windows".to_string()),
format!("(Build {})", build_num),
current_build.unwrap_or_default(),
));
};
let mut version = release.to_string();
if let Some(dv) = &display_version {
if !dv.is_empty() {
version.push(' ');
version.push_str(dv);
}
}
let kernel = match ubr {
Some(u) => format!("{}.{}", build_num, u),
None => build_num.to_string(),
};
Some((name, version, kernel))
}
pub fn get_socket_count_native() -> Option<usize> {
use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER;
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::sysinfoapi::GetLogicalProcessorInformationEx;
use winapi::um::winnt::RelationProcessorPackage;
let mut returned_length: u32 = 0;
let ok = unsafe {
GetLogicalProcessorInformationEx(
RelationProcessorPackage,
std::ptr::null_mut(),
&mut returned_length,
)
};
if ok != 0 || returned_length == 0 {
return None;
}
if unsafe { GetLastError() } != ERROR_INSUFFICIENT_BUFFER {
return None;
}
let mut buffer: Vec<u8> = vec![0u8; returned_length as usize];
let ok = unsafe {
GetLogicalProcessorInformationEx(
RelationProcessorPackage,
buffer.as_mut_ptr() as *mut _,
&mut returned_length,
)
};
if ok == 0 {
return None;
}
let mut offset: usize = 0;
let mut sockets: usize = 0;
while offset + 8 <= buffer.len() {
let mut header = [0u8; 8];
header.copy_from_slice(&buffer[offset..offset + 8]);
let relationship = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
let size = u32::from_le_bytes([header[4], header[5], header[6], header[7]]) as usize;
if size == 0 || offset + size > buffer.len() {
break; }
if relationship == RelationProcessorPackage {
sockets += 1;
}
offset += size;
}
if sockets == 0 {
None
} else {
Some(sockets)
}
}
#[allow(dead_code)] pub fn get_socket_count_wmi() -> usize {
let count = COMLibrary::new()
.ok()
.and_then(|com| WMIConnection::new(com).ok())
.and_then(|wmi| {
let results: Vec<Win32Processor> = wmi.query().ok()?;
Some(results.len().max(1))
});
if let Some(c) = count {
return c;
}
if let Some(stdout) = run_stdout(
"powershell",
[
"-NoProfile",
"-Command",
"(Get-CimInstance Win32_Processor).Count",
],
CommandTimeout::Slow,
) {
if let Ok(c) = stdout.trim().parse::<usize>() {
return c.max(1);
}
}
1
}
pub fn get_network_info_wmi() -> (Option<String>, Vec<String>) {
let result = COMLibrary::new()
.ok()
.and_then(|com| WMIConnection::new(com).ok())
.and_then(|wmi| {
let results: Vec<Win32NetworkAdapterConfig> = wmi
.raw_query("SELECT IPAddress, DNSServerSearchOrder, InterfaceIndex FROM Win32_NetworkAdapterConfiguration WHERE IPEnabled = TRUE")
.ok()?;
Some(results)
});
if let Some(results) = result {
let best_index = get_best_route_interface_index();
let mut ordered: Vec<&Win32NetworkAdapterConfig> = Vec::with_capacity(results.len());
if let Some(idx) = best_index {
ordered.extend(results.iter().filter(|a| a.interface_index == Some(idx)));
ordered.extend(results.iter().filter(|a| a.interface_index != Some(idx)));
} else {
ordered.extend(results.iter());
}
let mut machine_ip: Option<String> = None;
let mut dns_servers: Vec<String> = Vec::new();
for adapter in &ordered {
if machine_ip.is_none() {
if let Some(ref ips) = adapter.ip_address {
for ip in ips {
if ip.contains('.') && ip != "127.0.0.1" {
machine_ip = Some(ip.clone());
break;
}
}
}
}
if let Some(ref dns_list) = adapter.dns_server_search_order {
for dns in dns_list {
if !dns.is_empty() && !dns_servers.contains(dns) {
dns_servers.push(dns.clone());
if dns_servers.len() >= 5 {
break;
}
}
}
}
}
if machine_ip.is_some() || !dns_servers.is_empty() {
return (machine_ip, dns_servers);
}
}
let mut machine_ip: Option<String> = None;
if let Some(stdout) = run_stdout(
"powershell",
[
"-NoProfile", "-Command",
"(Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.InterfaceAlias -notmatch 'Loopback' -and $_.PrefixOrigin -ne 'WellKnown' } | Select-Object -First 1).IPAddress",
],
CommandTimeout::Slow,
)
{
let ip = stdout.trim().to_string();
if !ip.is_empty() && ip != "127.0.0.1" {
machine_ip = Some(ip);
}
}
let mut dns_servers = Vec::new();
if let Some(stdout) = run_stdout(
"powershell",
[
"-NoProfile", "-Command",
"(Get-DnsClientServerAddress -AddressFamily IPv4 | Where-Object { $_.ServerAddresses } | Select-Object -ExpandProperty ServerAddresses) -join \"`n\"",
],
CommandTimeout::Slow,
)
{
for line in stdout.lines() {
let ip = line.trim();
if !ip.is_empty() && !dns_servers.contains(&ip.to_string()) {
dns_servers.push(ip.to_string());
if dns_servers.len() >= 5 {
break;
}
}
}
}
(machine_ip, dns_servers)
}
#[link(name = "kernel32")]
extern "system" {
fn GetCurrentProcess() -> *mut std::ffi::c_void;
fn IsWow64Process2(
hProcess: *mut std::ffi::c_void,
pProcessMachine: *mut u16,
pNativeMachine: *mut u16,
) -> i32;
}
const IMAGE_FILE_MACHINE_UNKNOWN: u16 = 0;
const IMAGE_FILE_MACHINE_I386: u16 = 0x014C;
const IMAGE_FILE_MACHINE_AMD64: u16 = 0x8664;
const IMAGE_FILE_MACHINE_ARM: u16 = 0x01C0;
const IMAGE_FILE_MACHINE_ARM64: u16 = 0xAA64;
fn get_architecture() -> Option<String> {
let mut process_machine: u16 = 0;
let mut native_machine: u16 = 0;
let ok = unsafe {
IsWow64Process2(
GetCurrentProcess(),
&mut process_machine,
&mut native_machine,
)
};
if ok == 0 {
return Some(std::env::consts::ARCH.to_string());
}
let host = match native_machine {
IMAGE_FILE_MACHINE_AMD64 => "x86_64",
IMAGE_FILE_MACHINE_ARM64 => "aarch64",
IMAGE_FILE_MACHINE_I386 => "x86",
IMAGE_FILE_MACHINE_ARM => "arm",
IMAGE_FILE_MACHINE_UNKNOWN => return Some(std::env::consts::ARCH.to_string()),
_ => return Some(format!("unknown (0x{:x})", native_machine)),
};
if process_machine != IMAGE_FILE_MACHINE_UNKNOWN && process_machine != native_machine {
let proc_name = match process_machine {
IMAGE_FILE_MACHINE_AMD64 => "x86_64",
IMAGE_FILE_MACHINE_I386 => "x86",
IMAGE_FILE_MACHINE_ARM => "arm",
IMAGE_FILE_MACHINE_ARM64 => "aarch64",
_ => "unknown",
};
return Some(format!("{} ({} emulation)", host, proc_name));
}
Some(host.to_string())
}
fn detect_boot_mode() -> Option<String> {
if let Ok(firmware) = env::var("firmware_type") {
let upper = firmware.to_uppercase();
if upper.contains("UEFI") {
return Some("UEFI".to_string());
}
if upper.contains("LEGACY") || upper.contains("BIOS") {
return Some("Legacy BIOS".to_string());
}
}
if let Some(info) = run_stdout(
"cmd",
["/c", "bcdedit", "/enum", "{current}"],
CommandTimeout::Normal,
) {
let info = info.to_lowercase();
if info.contains("winload.efi") {
return Some("UEFI".to_string());
}
return Some("Legacy BIOS".to_string());
}
None
}
fn get_terminal() -> Option<String> {
if env::var("WT_SESSION").is_ok() {
return Some("Windows Terminal".to_string());
}
if env::var("TERM_PROGRAM").ok().as_deref() == Some("vscode") {
return Some("VS Code".to_string());
}
if env::var("CURSOR_TRACE_ID").is_ok() || env::var("CURSOR_AGENT").is_ok() {
return Some("Cursor".to_string());
}
if let Some(name) = detect_terminal_via_parent_walk() {
return Some(name);
}
Some("Console".to_string())
}
fn detect_terminal_via_parent_walk() -> Option<String> {
use std::collections::HashMap;
use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE};
use winapi::um::processthreadsapi::GetCurrentProcessId;
use winapi::um::tlhelp32::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
};
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
if snapshot == INVALID_HANDLE_VALUE {
return None;
}
let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() };
entry.dwSize = std::mem::size_of::<PROCESSENTRY32W>() as u32;
let mut pid_to_parent_name: HashMap<u32, (u32, String)> = HashMap::new();
if unsafe { Process32FirstW(snapshot, &mut entry) } != 0 {
loop {
let len = entry
.szExeFile
.iter()
.position(|&c| c == 0)
.unwrap_or(entry.szExeFile.len());
let name = String::from_utf16_lossy(&entry.szExeFile[..len]);
pid_to_parent_name.insert(entry.th32ProcessID, (entry.th32ParentProcessID, name));
if unsafe { Process32NextW(snapshot, &mut entry) } == 0 {
break;
}
}
}
unsafe { CloseHandle(snapshot) };
let mut current_pid = unsafe { GetCurrentProcessId() };
for _ in 0..10 {
let (parent_pid, name) = match pid_to_parent_name.get(¤t_pid) {
Some(v) => v.clone(),
None => break,
};
if let Some(label) = match_terminal_name(&name) {
return Some(label.to_string());
}
if parent_pid == 0 || parent_pid == current_pid {
break;
}
current_pid = parent_pid;
}
None
}
fn match_terminal_name(exe: &str) -> Option<&'static str> {
let lower = exe.to_lowercase();
match lower.as_str() {
"windowsterminal.exe" => Some("Windows Terminal"),
"wezterm-gui.exe" | "wezterm.exe" => Some("WezTerm"),
"alacritty.exe" => Some("Alacritty"),
"code.exe" => Some("VS Code"),
"cursor.exe" => Some("Cursor"),
"windsurf.exe" => Some("Windsurf"),
"hyper.exe" => Some("Hyper"),
"tabby.exe" => Some("Tabby"),
"ghostty.exe" => Some("Ghostty"),
"kitty.exe" => Some("Kitty"),
"mintty.exe" => Some("MinTTY"),
"claude.exe" => Some("Claude Code"),
"antigravity.exe" => Some("Antigravity"),
"conhost.exe" | "powershell.exe" | "pwsh.exe" | "cmd.exe" | "bash.exe" | "sh.exe"
| "zsh.exe" | "fish.exe" | "nu.exe" | "tr300.exe" | "node.exe" | "python.exe"
| "python3.exe" => None,
_ => None,
}
}
fn get_shell() -> Option<String> {
if let Ok(shell) = env::var("SHELL") {
if shell.contains("bash") {
if let Some(version) = run_stdout("bash", ["--version"], CommandTimeout::Normal) {
if let Some(line) = version.lines().next() {
if let Some(ver_start) = line.find("version ") {
let ver_part = &line[ver_start + 8..];
if let Some(ver_end) =
ver_part.find(|c: char| !c.is_ascii_digit() && c != '.')
{
return Some(format!("bash {}", &ver_part[..ver_end]));
}
}
}
}
return Some("bash".to_string());
}
}
if let Some(v) = get_powershell_core_version() {
return Some(format!("PowerShell {}", v));
}
if let Some(stdout) = run_stdout(
"reg",
[
"query",
r"HKLM\SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine",
"/v",
"PowerShellVersion",
],
CommandTimeout::Normal,
) {
for line in stdout.lines() {
if line.contains("PowerShellVersion") {
if let Some(version) = line.split_whitespace().last() {
return Some(format!("PowerShell {}", version));
}
}
}
}
Some("PowerShell".to_string())
}
fn get_powershell_core_version() -> Option<String> {
let stdout = run_stdout(
"reg",
[
"query",
r"HKLM\SOFTWARE\Microsoft\PowerShellCore\InstalledVersions",
"/s",
"/v",
"SemanticVersion",
],
CommandTimeout::Normal,
)?;
let mut best_tuple: Option<(u64, u64, u64)> = None;
let mut best_string: Option<String> = None;
for line in stdout.lines() {
if line.contains("SemanticVersion") {
if let Some(version) = line.split_whitespace().last() {
let version_clean = version.trim();
if let Some(tuple) = parse_semver_tuple(version_clean) {
if best_tuple.map(|b| tuple > b).unwrap_or(true) {
best_tuple = Some(tuple);
best_string = Some(version_clean.to_string());
}
}
}
}
}
best_string
}
fn parse_semver_tuple(s: &str) -> Option<(u64, u64, u64)> {
let mut parts = s.splitn(3, '.');
let major: u64 = parts.next()?.parse().ok()?;
let minor: u64 = parts.next()?.parse().ok()?;
let patch_raw = parts.next()?;
let patch_clean = patch_raw.split(['-', '+']).next().unwrap_or(patch_raw);
let patch: u64 = patch_clean.parse().ok()?;
Some((major, minor, patch))
}
fn get_display_resolution() -> Option<String> {
unsafe {
let cx = winapi::um::winuser::GetSystemMetrics(winapi::um::winuser::SM_CXSCREEN);
let cy = winapi::um::winuser::GetSystemMetrics(winapi::um::winuser::SM_CYSCREEN);
if cx > 0 && cy > 0 {
Some(format!("{}x{}", cx, cy))
} else {
None
}
}
}
fn get_locale() -> Option<String> {
let mut buf = [0u16; 85]; unsafe {
let len = winapi::um::winnls::GetUserDefaultLocaleName(buf.as_mut_ptr(), buf.len() as i32);
if len > 0 {
let name = String::from_utf16_lossy(&buf[..len as usize - 1]);
if !name.is_empty() {
return Some(name);
}
}
}
None
}
#[derive(Default)]
struct WindowsPowerShellFallback {
windows_edition: Option<String>,
virtualization: Option<String>,
gpus: Vec<String>,
battery: Option<String>,
}
fn get_batched_powershell_fallback() -> WindowsPowerShellFallback {
let script = r#"
$os = Get-CimInstance Win32_OperatingSystem
$cs = Get-CimInstance Win32_ComputerSystem
$gpu = @(Get-CimInstance Win32_VideoController | ForEach-Object { $_.Name })
$b = Get-CimInstance Win32_Battery
$battery = $null
if ($b) { $battery = "$($b.EstimatedChargeRemaining)% ($($b.BatteryStatus))" }
[pscustomobject]@{
edition = $os.Caption
computer = "$($cs.Manufacturer)|$($cs.Model)|$($cs.HypervisorPresent)"
gpus = $gpu
battery = $battery
} | ConvertTo-Json -Compress
"#;
let Some(stdout) = run_stdout(
"powershell",
["-NoProfile", "-Command", script],
CommandTimeout::Slow,
) else {
return WindowsPowerShellFallback::default();
};
parse_batched_powershell_fallback(&stdout).unwrap_or_default()
}
fn parse_batched_powershell_fallback(json: &str) -> Option<WindowsPowerShellFallback> {
let value: serde_json::Value = serde_json::from_str(json).ok()?;
let windows_edition = value["edition"]
.as_str()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string);
let virtualization = value["computer"]
.as_str()
.and_then(parse_virtualization_from_ps_computer_system);
let gpus = match &value["gpus"] {
serde_json::Value::Array(values) => values
.iter()
.filter_map(|v| v.as_str())
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect(),
serde_json::Value::String(s) if !s.trim().is_empty() => vec![s.trim().to_string()],
_ => Vec::new(),
};
let battery = value["battery"]
.as_str()
.map(normalize_powershell_battery_status)
.filter(|s| !s.is_empty() && s.contains('%'));
Some(WindowsPowerShellFallback {
windows_edition,
virtualization,
gpus,
battery,
})
}
fn get_windows_edition_ps() -> Option<String> {
let caption = run_stdout(
"powershell",
[
"-NoProfile",
"-Command",
"(Get-CimInstance Win32_OperatingSystem).Caption",
],
CommandTimeout::Slow,
)?
.trim()
.to_string();
if caption.is_empty() {
None
} else {
Some(caption)
}
}
fn detect_virtualization_ps() -> Option<String> {
let info = run_stdout(
"powershell",
[
"-NoProfile", "-Command",
"(Get-CimInstance Win32_ComputerSystem).Manufacturer + '|' + (Get-CimInstance Win32_ComputerSystem).Model + '|' + (Get-CimInstance Win32_ComputerSystem).HypervisorPresent",
],
CommandTimeout::Slow,
)?;
parse_virtualization_from_ps_computer_system(&info)
}
fn parse_virtualization_from_ps_computer_system(info: &str) -> Option<String> {
let info = info.to_lowercase();
if info.contains("vmware") {
return Some("VMware".to_string());
}
if info.contains("virtualbox") || info.contains("vbox") {
return Some("VirtualBox".to_string());
}
if info.contains("microsoft") && info.contains("virtual") {
return Some("Hyper-V".to_string());
}
if info.contains("qemu") {
return Some("QEMU".to_string());
}
if info.contains("xen") {
return Some("Xen".to_string());
}
if info.contains("parallels") {
return Some("Parallels".to_string());
}
if info.contains("true") {
return Some("Hypervisor Present".to_string());
}
None
}
fn get_gpus_ps() -> Vec<String> {
let mut gpus = Vec::new();
if let Some(stdout) = run_stdout(
"powershell",
[
"-NoProfile",
"-Command",
"(Get-CimInstance Win32_VideoController).Name -join \"`n\"",
],
CommandTimeout::Slow,
) {
for line in stdout.lines() {
let gpu = line.trim();
if !gpu.is_empty() {
gpus.push(gpu.to_string());
}
}
}
gpus
}
fn get_battery_ps() -> Option<String> {
let battery = run_stdout(
"powershell",
[
"-NoProfile", "-Command",
"$b = Get-CimInstance Win32_Battery; if ($b) { \"$($b.EstimatedChargeRemaining)% ($($b.BatteryStatus))\" }",
],
CommandTimeout::Slow,
)?
.trim()
.to_string();
if battery.is_empty() || !battery.contains('%') {
return None;
}
Some(normalize_powershell_battery_status(&battery))
}
fn normalize_powershell_battery_status(battery: &str) -> String {
battery
.replace("(1)", "(Discharging)")
.replace("(2)", "(AC Power)")
.replace("(3)", "(Charging)")
.replace("(4)", "(Low)")
.replace("(5)", "(Critical)")
.replace("(6)", "(Charging)")
.replace("(7)", "(Charging High)")
.replace("(8)", "(Charging Low)")
.replace("(9)", "(Charging Critical)")
}
#[cfg(test)]
mod powershell_fallback_tests {
use super::*;
#[test]
fn parses_batched_powershell_fallback_json() {
let json = r#"{
"edition": "Microsoft Windows 11 Pro",
"computer": "Microsoft Corporation|Virtual Machine|True",
"gpus": ["Intel Arc Graphics", "NVIDIA RTX"],
"battery": "87% (2)"
}"#;
let parsed = parse_batched_powershell_fallback(json).expect("valid fallback JSON");
assert_eq!(
parsed.windows_edition.as_deref(),
Some("Microsoft Windows 11 Pro")
);
assert_eq!(parsed.virtualization.as_deref(), Some("Hyper-V"));
assert_eq!(
parsed.gpus,
vec!["Intel Arc Graphics".to_string(), "NVIDIA RTX".to_string()]
);
assert_eq!(parsed.battery.as_deref(), Some("87% (AC Power)"));
}
#[test]
fn parses_single_gpu_string_from_batched_fallback() {
let json = r#"{
"edition": "",
"computer": "QEMU|Standard PC|True",
"gpus": "Virtio GPU",
"battery": null
}"#;
let parsed = parse_batched_powershell_fallback(json).expect("valid fallback JSON");
assert_eq!(parsed.windows_edition, None);
assert_eq!(parsed.virtualization.as_deref(), Some("QEMU"));
assert_eq!(parsed.gpus, vec!["Virtio GPU".to_string()]);
assert_eq!(parsed.battery, None);
}
}
fn filter_software_gpus(gpus: Vec<String>) -> Vec<String> {
const SOFTWARE_GPU_NEEDLES: &[&str] = &[
"Microsoft Basic Render Driver",
"Microsoft Basic Display",
"Microsoft Hyper-V Video",
"Microsoft Remote Display Adapter",
"Microsoft Indirect Display",
"RDPDD Chained DD",
"RDP Encoder Mirror",
];
gpus.into_iter()
.filter(|name| {
let lower = name.to_lowercase();
!SOFTWARE_GPU_NEEDLES
.iter()
.any(|n| lower.contains(&n.to_lowercase()))
})
.collect()
}
const BATTERY_FLAG_LOW: u8 = 0x02; const BATTERY_FLAG_CRITICAL: u8 = 0x04; const BATTERY_FLAG_CHARGING: u8 = 0x08;
const BATTERY_FLAG_NO_BATTERY: u8 = 0x80;
const BATTERY_FLAG_UNKNOWN: u8 = 0xFF;
fn get_battery_native() -> Option<String> {
let mut sps: winapi::um::winbase::SYSTEM_POWER_STATUS = unsafe { std::mem::zeroed() };
let ok = unsafe { winapi::um::winbase::GetSystemPowerStatus(&mut sps) };
if ok == 0 {
return None;
}
if sps.BatteryFlag == BATTERY_FLAG_NO_BATTERY {
return None;
}
let percent = sps.BatteryLifePercent;
if percent == 0xFF {
return None;
}
if sps.BatteryFlag == BATTERY_FLAG_UNKNOWN {
return Some(format!("{}% (Unknown)", percent));
}
let ac_status = sps.ACLineStatus;
let on_ac = ac_status == 1;
let ac_unknown = ac_status == 0xFF;
let charging = sps.BatteryFlag & BATTERY_FLAG_CHARGING != 0;
let critical = sps.BatteryFlag & BATTERY_FLAG_CRITICAL != 0;
let low = sps.BatteryFlag & BATTERY_FLAG_LOW != 0;
if ac_unknown {
return Some(format!("{}%", percent));
}
if on_ac {
if charging {
return Some(format!("{}% (Charging)", percent));
}
if percent >= 95 {
return Some("AC Power".to_string());
}
return Some(format!("{}% (Plugged in)", percent));
}
let label = if critical {
"Critical"
} else if low {
"Low"
} else {
"Discharging"
};
Some(format!("{}% ({})", percent, label))
}
#[link(name = "iphlpapi")]
extern "system" {
fn GetBestInterfaceEx(pDestAddr: *mut SockaddrIn, pdwBestIfIndex: *mut u32) -> u32;
}
#[repr(C)]
struct SockaddrIn {
sin_family: u16, sin_port: u16,
sin_addr: u32, sin_zero: [u8; 8],
}
const AF_INET: u16 = 2;
fn get_best_route_interface_index() -> Option<u32> {
let sin_addr: u32 = u32::from_le_bytes([1, 1, 1, 1]);
let mut sa = SockaddrIn {
sin_family: AF_INET,
sin_port: 0,
sin_addr,
sin_zero: [0u8; 8],
};
let mut best_index: u32 = 0;
let status = unsafe { GetBestInterfaceEx(&mut sa, &mut best_index) };
if status == 0 {
Some(best_index)
} else {
None
}
}
pub fn detect_fast_startup() -> bool {
let stdout = match run_stdout(
"reg",
[
"query",
r"HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Power",
"/v",
"HiberbootEnabled",
],
CommandTimeout::Normal,
) {
Some(stdout) => stdout,
None => return false,
};
for line in stdout.lines() {
if line.contains("HiberbootEnabled") {
if let Some(value) = line.split_whitespace().last() {
let trimmed = value.trim_start_matches("0x");
if let Ok(v) = u32::from_str_radix(trimmed, 16) {
return v == 1;
}
}
}
}
false
}
pub fn last_cold_boot_seconds() -> Option<u64> {
#[derive(Deserialize)]
#[serde(rename = "Win32_OperatingSystem")]
#[serde(rename_all = "PascalCase")]
struct OsBoot {
last_boot_up_time: Option<wmi::WMIDateTime>,
}
let com = COMLibrary::new().ok()?;
let wmi = WMIConnection::new(com).ok()?;
let results: Vec<OsBoot> = wmi.query().ok()?;
let boot = results.into_iter().next()?.last_boot_up_time?.0;
let elapsed = chrono::Utc::now()
.signed_duration_since(boot.with_timezone(&chrono::Utc))
.num_seconds();
if elapsed < 0 {
None
} else {
Some(elapsed as u64)
}
}