mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-06-15 12:12:01 +08:00
feat: 初步增加 Windows 支持
This commit is contained in:
280
src/diagnostics/linux.rs
Normal file
280
src/diagnostics/linux.rs
Normal file
@@ -0,0 +1,280 @@
|
||||
use super::{DeviceInfo, DiskSpaceInfo, NetworkAddress};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::utils::hostname_uname;
|
||||
|
||||
pub fn get_disk_space(path: &std::path::Path) -> Result<DiskSpaceInfo> {
|
||||
let stat = nix::sys::statvfs::statvfs(path)
|
||||
.map_err(|e| AppError::Internal(format!("Failed to get disk space: {}", e)))?;
|
||||
|
||||
let block_size = stat.block_size() as u64;
|
||||
let total = stat.blocks() as u64 * block_size;
|
||||
let available = stat.blocks_available() as u64 * block_size;
|
||||
let used = total - available;
|
||||
|
||||
Ok(DiskSpaceInfo {
|
||||
total,
|
||||
available,
|
||||
used,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_device_info() -> DeviceInfo {
|
||||
let mem_info = get_meminfo();
|
||||
|
||||
DeviceInfo {
|
||||
hostname: hostname_uname(),
|
||||
cpu_model: get_cpu_model(),
|
||||
cpu_usage: get_cpu_usage(),
|
||||
memory_total: mem_info.total,
|
||||
memory_used: mem_info.total.saturating_sub(mem_info.available),
|
||||
network_addresses: get_network_addresses(),
|
||||
serial_ports: crate::utils::list_serial_ports(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cpu_model() -> String {
|
||||
let cpuinfo = std::fs::read_to_string("/proc/cpuinfo").ok();
|
||||
|
||||
if let Some(model) = parse_cpu_model_from_cpuinfo_content(cpuinfo.as_deref()) {
|
||||
return model;
|
||||
}
|
||||
|
||||
if let Some(model) = read_device_tree_model() {
|
||||
return model;
|
||||
}
|
||||
|
||||
if let Some(content) = cpuinfo.as_deref() {
|
||||
let cores = content
|
||||
.lines()
|
||||
.filter(|line| line.starts_with("processor"))
|
||||
.count();
|
||||
if cores > 0 {
|
||||
return format!("{} {}C", std::env::consts::ARCH, cores);
|
||||
}
|
||||
}
|
||||
|
||||
std::env::consts::ARCH.to_string()
|
||||
}
|
||||
|
||||
fn parse_cpu_model_from_cpuinfo_content(content: Option<&str>) -> Option<String> {
|
||||
let content = content?;
|
||||
|
||||
content
|
||||
.lines()
|
||||
.find(|line| line.starts_with("model name") || line.starts_with("Model"))
|
||||
.and_then(|line| line.split(':').nth(1))
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
fn read_device_tree_model() -> Option<String> {
|
||||
std::fs::read("/proc/device-tree/model")
|
||||
.ok()
|
||||
.and_then(|bytes| parse_device_tree_model_bytes(bytes.as_slice()))
|
||||
}
|
||||
|
||||
fn parse_device_tree_model_bytes(bytes: &[u8]) -> Option<String> {
|
||||
let model = String::from_utf8_lossy(bytes)
|
||||
.trim_matches(|c: char| c == '\0' || c.is_whitespace())
|
||||
.to_string();
|
||||
|
||||
if model.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(model)
|
||||
}
|
||||
}
|
||||
|
||||
static CPU_PREV_STATS: std::sync::OnceLock<std::sync::Mutex<(u64, u64)>> =
|
||||
std::sync::OnceLock::new();
|
||||
|
||||
fn get_cpu_usage() -> f32 {
|
||||
let content = match std::fs::read_to_string("/proc/stat") {
|
||||
Ok(c) => c,
|
||||
Err(_) => return 0.0,
|
||||
};
|
||||
|
||||
let cpu_line = match content.lines().next() {
|
||||
Some(line) if line.starts_with("cpu ") => line,
|
||||
_ => return 0.0,
|
||||
};
|
||||
|
||||
let parts: Vec<u64> = cpu_line
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.take(8)
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
|
||||
if parts.len() < 4 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let idle = parts[3] + parts.get(4).unwrap_or(&0);
|
||||
let total: u64 = parts.iter().sum();
|
||||
|
||||
let prev_mutex = CPU_PREV_STATS.get_or_init(|| std::sync::Mutex::new((0, 0)));
|
||||
let mut prev = prev_mutex.lock().unwrap();
|
||||
let (prev_idle, prev_total) = *prev;
|
||||
|
||||
let idle_delta = idle.saturating_sub(prev_idle);
|
||||
let total_delta = total.saturating_sub(prev_total);
|
||||
*prev = (idle, total);
|
||||
|
||||
if total_delta == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let usage = 100.0 * (1.0 - (idle_delta as f64 / total_delta as f64));
|
||||
usage as f32
|
||||
}
|
||||
|
||||
struct MemInfo {
|
||||
total: u64,
|
||||
available: u64,
|
||||
}
|
||||
|
||||
fn get_meminfo() -> MemInfo {
|
||||
let content = match std::fs::read_to_string("/proc/meminfo") {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return MemInfo {
|
||||
total: 0,
|
||||
available: 0,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut total = 0u64;
|
||||
let mut available = 0u64;
|
||||
|
||||
for line in content.lines() {
|
||||
if line.starts_with("MemTotal:") {
|
||||
if let Some(kb) = line
|
||||
.split_whitespace()
|
||||
.nth(1)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
{
|
||||
total = kb * 1024;
|
||||
}
|
||||
} else if line.starts_with("MemAvailable:") {
|
||||
if let Some(kb) = line
|
||||
.split_whitespace()
|
||||
.nth(1)
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
{
|
||||
available = kb * 1024;
|
||||
}
|
||||
}
|
||||
|
||||
if total > 0 && available > 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MemInfo { total, available }
|
||||
}
|
||||
|
||||
fn get_network_addresses() -> Vec<NetworkAddress> {
|
||||
let all_addrs = match nix::ifaddrs::getifaddrs() {
|
||||
Ok(addrs) => addrs,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let mut up_ifaces = std::collections::HashSet::new();
|
||||
let net_dir = match std::fs::read_dir("/sys/class/net") {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
for entry in net_dir.flatten() {
|
||||
let iface_name = match entry.file_name().into_string() {
|
||||
Ok(name) => name,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if iface_name == "lo" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let operstate_path = entry.path().join("operstate");
|
||||
let is_up = std::fs::read_to_string(&operstate_path)
|
||||
.map(|s| s.trim() == "up")
|
||||
.unwrap_or(false);
|
||||
|
||||
if is_up {
|
||||
up_ifaces.insert(iface_name);
|
||||
}
|
||||
}
|
||||
|
||||
let mut addresses = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for ifaddr in all_addrs {
|
||||
let iface_name = &ifaddr.interface_name;
|
||||
if iface_name == "lo" || !up_ifaces.contains(iface_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(addr) = ifaddr.address {
|
||||
if let Some(sockaddr_in) = addr.as_sockaddr_in() {
|
||||
let ip = sockaddr_in.ip();
|
||||
if ip.is_loopback() {
|
||||
continue;
|
||||
}
|
||||
let ip_str = ip.to_string();
|
||||
if seen.insert((iface_name.clone(), ip_str.clone())) {
|
||||
addresses.push(NetworkAddress {
|
||||
interface: iface_name.clone(),
|
||||
ip: ip_str,
|
||||
});
|
||||
}
|
||||
} else if let Some(sockaddr_in6) = addr.as_sockaddr_in6() {
|
||||
let ip = sockaddr_in6.ip();
|
||||
if ip.is_loopback() || ip.is_unspecified() || ip.is_unicast_link_local() {
|
||||
continue;
|
||||
}
|
||||
let ip_str = ip.to_string();
|
||||
if seen.insert((iface_name.clone(), ip_str.clone())) {
|
||||
addresses.push(NetworkAddress {
|
||||
interface: iface_name.clone(),
|
||||
ip: ip_str,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addresses
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_cpu_model_from_cpuinfo_content, parse_device_tree_model_bytes};
|
||||
|
||||
#[test]
|
||||
fn parse_cpu_model_from_model_name_field() {
|
||||
let input = "processor\t: 0\nmodel name\t: Intel(R) Xeon(R)\n";
|
||||
assert_eq!(
|
||||
parse_cpu_model_from_cpuinfo_content(input),
|
||||
Some("Intel(R) Xeon(R)".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_cpu_model_from_model_field() {
|
||||
let input = "processor\t: 0\nModel\t\t: Raspberry Pi 4 Model B Rev 1.4\n";
|
||||
assert_eq!(
|
||||
parse_cpu_model_from_cpuinfo_content(input),
|
||||
Some("Raspberry Pi 4 Model B Rev 1.4".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_device_tree_model_trimmed() {
|
||||
let input = b"Onething OEC Box\0\n";
|
||||
assert_eq!(
|
||||
parse_device_tree_model_bytes(input),
|
||||
Some("Onething OEC Box".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
47
src/diagnostics/mod.rs
Normal file
47
src/diagnostics/mod.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Host diagnostics used by the web status API.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::error::Result;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DeviceInfo {
|
||||
pub hostname: String,
|
||||
pub cpu_model: String,
|
||||
pub cpu_usage: f32,
|
||||
pub memory_total: u64,
|
||||
pub memory_used: u64,
|
||||
pub network_addresses: Vec<NetworkAddress>,
|
||||
pub serial_ports: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct NetworkAddress {
|
||||
pub interface: String,
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DiskSpaceInfo {
|
||||
pub total: u64,
|
||||
pub available: u64,
|
||||
pub used: u64,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
mod linux;
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
#[cfg(unix)]
|
||||
use linux as platform;
|
||||
#[cfg(windows)]
|
||||
use windows as platform;
|
||||
|
||||
pub fn get_disk_space(path: &std::path::Path) -> Result<DiskSpaceInfo> {
|
||||
platform::get_disk_space(path)
|
||||
}
|
||||
|
||||
pub fn get_device_info() -> DeviceInfo {
|
||||
platform::get_device_info()
|
||||
}
|
||||
249
src/diagnostics/windows.rs
Normal file
249
src/diagnostics/windows.rs
Normal file
@@ -0,0 +1,249 @@
|
||||
use super::{DeviceInfo, DiskSpaceInfo, NetworkAddress};
|
||||
use crate::error::{AppError, Result};
|
||||
use crate::utils::hostname_uname;
|
||||
use std::ffi::CStr;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use windows_sys::Win32::Foundation::{ERROR_BUFFER_OVERFLOW, ERROR_SUCCESS, FILETIME};
|
||||
use windows_sys::Win32::NetworkManagement::IpHelper::{
|
||||
GetAdaptersAddresses, GAA_FLAG_SKIP_ANYCAST, GAA_FLAG_SKIP_DNS_SERVER, GAA_FLAG_SKIP_MULTICAST,
|
||||
IP_ADAPTER_ADDRESSES_LH,
|
||||
};
|
||||
use windows_sys::Win32::NetworkManagement::Ndis::IfOperStatusUp;
|
||||
use windows_sys::Win32::Networking::WinSock::{
|
||||
AF_INET, AF_INET6, SOCKADDR, SOCKADDR_IN, SOCKADDR_IN6,
|
||||
};
|
||||
use windows_sys::Win32::System::SystemInformation::{
|
||||
GetNativeSystemInfo, GlobalMemoryStatusEx, MEMORYSTATUSEX, PROCESSOR_ARCHITECTURE_AMD64,
|
||||
PROCESSOR_ARCHITECTURE_ARM64, PROCESSOR_ARCHITECTURE_INTEL, SYSTEM_INFO,
|
||||
};
|
||||
use windows_sys::Win32::System::Threading::GetSystemTimes;
|
||||
|
||||
pub fn get_disk_space(_path: &std::path::Path) -> Result<DiskSpaceInfo> {
|
||||
Err(AppError::Internal(
|
||||
"Disk space reporting is unavailable on Windows".to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_device_info() -> DeviceInfo {
|
||||
let (memory_total, memory_used) = get_memory_usage();
|
||||
|
||||
DeviceInfo {
|
||||
hostname: hostname_uname(),
|
||||
cpu_model: get_cpu_model(),
|
||||
cpu_usage: get_cpu_usage(),
|
||||
memory_total,
|
||||
memory_used,
|
||||
network_addresses: get_network_addresses(),
|
||||
serial_ports: crate::utils::list_serial_ports(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cpu_model() -> String {
|
||||
std::env::var("PROCESSOR_IDENTIFIER")
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(get_cpu_arch_label)
|
||||
}
|
||||
|
||||
fn get_cpu_arch_label() -> String {
|
||||
let mut info = std::mem::MaybeUninit::<SYSTEM_INFO>::zeroed();
|
||||
unsafe {
|
||||
GetNativeSystemInfo(info.as_mut_ptr());
|
||||
let info = info.assume_init();
|
||||
match info.Anonymous.Anonymous.wProcessorArchitecture {
|
||||
PROCESSOR_ARCHITECTURE_AMD64 => "x86_64".to_string(),
|
||||
PROCESSOR_ARCHITECTURE_ARM64 => "aarch64".to_string(),
|
||||
PROCESSOR_ARCHITECTURE_INTEL => "x86".to_string(),
|
||||
_ => std::env::consts::ARCH.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_memory_usage() -> (u64, u64) {
|
||||
let mut status = MEMORYSTATUSEX {
|
||||
dwLength: std::mem::size_of::<MEMORYSTATUSEX>() as u32,
|
||||
..unsafe { std::mem::zeroed() }
|
||||
};
|
||||
|
||||
let ok = unsafe { GlobalMemoryStatusEx(&mut status) };
|
||||
if ok == 0 {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
(
|
||||
status.ullTotalPhys,
|
||||
status.ullTotalPhys.saturating_sub(status.ullAvailPhys),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_cpu_usage() -> f32 {
|
||||
static LAST_SAMPLE: OnceLock<Mutex<Option<CpuTimes>>> = OnceLock::new();
|
||||
|
||||
let Some(current) = read_cpu_times() else {
|
||||
return 0.0;
|
||||
};
|
||||
let sample = LAST_SAMPLE.get_or_init(|| Mutex::new(None));
|
||||
let Ok(mut last) = sample.lock() else {
|
||||
return 0.0;
|
||||
};
|
||||
|
||||
let (previous, current) = if let Some(previous) = last.replace(current) {
|
||||
(previous, current)
|
||||
} else {
|
||||
drop(last);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
let Some(next) = read_cpu_times() else {
|
||||
return 0.0;
|
||||
};
|
||||
if let Ok(mut last) = sample.lock() {
|
||||
*last = Some(next);
|
||||
}
|
||||
(current, next)
|
||||
};
|
||||
|
||||
let idle = current.idle.saturating_sub(previous.idle);
|
||||
let kernel = current.kernel.saturating_sub(previous.kernel);
|
||||
let user = current.user.saturating_sub(previous.user);
|
||||
let total = kernel.saturating_add(user);
|
||||
|
||||
if total == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
((total.saturating_sub(idle)) as f64 * 100.0 / total as f64).clamp(0.0, 100.0) as f32
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct CpuTimes {
|
||||
idle: u64,
|
||||
kernel: u64,
|
||||
user: u64,
|
||||
}
|
||||
|
||||
fn read_cpu_times() -> Option<CpuTimes> {
|
||||
let mut idle = FILETIME {
|
||||
dwLowDateTime: 0,
|
||||
dwHighDateTime: 0,
|
||||
};
|
||||
let mut kernel = idle;
|
||||
let mut user = idle;
|
||||
|
||||
let ok = unsafe { GetSystemTimes(&mut idle, &mut kernel, &mut user) };
|
||||
if ok == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(CpuTimes {
|
||||
idle: filetime_to_u64(idle),
|
||||
kernel: filetime_to_u64(kernel),
|
||||
user: filetime_to_u64(user),
|
||||
})
|
||||
}
|
||||
|
||||
fn filetime_to_u64(time: FILETIME) -> u64 {
|
||||
((time.dwHighDateTime as u64) << 32) | time.dwLowDateTime as u64
|
||||
}
|
||||
|
||||
fn get_network_addresses() -> Vec<NetworkAddress> {
|
||||
let mut buffer_len = 15_000u32;
|
||||
let flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER;
|
||||
|
||||
for _ in 0..2 {
|
||||
let mut buffer = vec![0u8; buffer_len as usize];
|
||||
let ret = unsafe {
|
||||
GetAdaptersAddresses(
|
||||
0,
|
||||
flags,
|
||||
std::ptr::null_mut(),
|
||||
buffer.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH,
|
||||
&mut buffer_len,
|
||||
)
|
||||
};
|
||||
|
||||
if ret == ERROR_BUFFER_OVERFLOW {
|
||||
continue;
|
||||
}
|
||||
if ret != ERROR_SUCCESS {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut addresses = Vec::new();
|
||||
let mut adapter = buffer.as_ptr() as *const IP_ADAPTER_ADDRESSES_LH;
|
||||
while !adapter.is_null() {
|
||||
let adapter_ref = unsafe { &*adapter };
|
||||
if adapter_ref.OperStatus != IfOperStatusUp {
|
||||
adapter = adapter_ref.Next;
|
||||
continue;
|
||||
}
|
||||
|
||||
let interface = adapter_name(adapter_ref);
|
||||
let mut unicast = adapter_ref.FirstUnicastAddress;
|
||||
|
||||
while !unicast.is_null() {
|
||||
let unicast_ref = unsafe { &*unicast };
|
||||
if let Some(ip) = sockaddr_to_ip(unicast_ref.Address.lpSockaddr) {
|
||||
addresses.push(NetworkAddress {
|
||||
interface: interface.clone(),
|
||||
ip,
|
||||
});
|
||||
}
|
||||
unicast = unicast_ref.Next;
|
||||
}
|
||||
|
||||
adapter = adapter_ref.Next;
|
||||
}
|
||||
|
||||
addresses.sort_by(|a, b| a.interface.cmp(&b.interface).then(a.ip.cmp(&b.ip)));
|
||||
addresses.dedup_by(|a, b| a.interface == b.interface && a.ip == b.ip);
|
||||
return addresses;
|
||||
}
|
||||
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn adapter_name(adapter: &IP_ADAPTER_ADDRESSES_LH) -> String {
|
||||
unsafe {
|
||||
if !adapter.FriendlyName.is_null() {
|
||||
let mut len = 0usize;
|
||||
while *adapter.FriendlyName.add(len) != 0 {
|
||||
len += 1;
|
||||
}
|
||||
let name =
|
||||
String::from_utf16_lossy(std::slice::from_raw_parts(adapter.FriendlyName, len));
|
||||
if !name.trim().is_empty() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
if !adapter.AdapterName.is_null() {
|
||||
return CStr::from_ptr(adapter.AdapterName.cast())
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
}
|
||||
}
|
||||
|
||||
"unknown".to_string()
|
||||
}
|
||||
|
||||
fn sockaddr_to_ip(sockaddr: *const SOCKADDR) -> Option<String> {
|
||||
if sockaddr.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let family = unsafe { (*sockaddr).sa_family };
|
||||
match family {
|
||||
AF_INET => {
|
||||
let addr = unsafe { *(sockaddr as *const SOCKADDR_IN) };
|
||||
let bytes = unsafe { addr.sin_addr.S_un.S_addr.to_ne_bytes() };
|
||||
Some(Ipv4Addr::from(bytes).to_string())
|
||||
}
|
||||
AF_INET6 => {
|
||||
let addr = unsafe { *(sockaddr as *const SOCKADDR_IN6) };
|
||||
let bytes = unsafe { addr.sin6_addr.u.Byte };
|
||||
Some(Ipv6Addr::from(bytes).to_string())
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user