404 lines
13 KiB
Rust
404 lines
13 KiB
Rust
use std::io::{BufRead, BufReader, Write};
|
|
use std::process::{Child, Command, Stdio};
|
|
use std::sync::Mutex;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use tauri::{App, AppHandle, Manager};
|
|
|
|
pub struct SensorChild(pub Mutex<Option<Child>>);
|
|
|
|
/// LHML 传感器单条数据(与 C# SensorReading 对齐)
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SensorReading {
|
|
pub hardware: String,
|
|
#[serde(rename = "hardwareType")]
|
|
pub hardware_type: String,
|
|
#[serde(rename = "subHardware")]
|
|
pub sub_hardware: Option<String>,
|
|
pub name: String,
|
|
#[serde(rename = "sensorType")]
|
|
pub sensor_type: String,
|
|
pub value: f64,
|
|
pub unit: Option<String>,
|
|
}
|
|
|
|
/// LHML 传感器响应(与 C# SensorsResponse 对齐)
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SensorsResponse {
|
|
#[serde(rename = "updatedAt")]
|
|
pub updated_at: String,
|
|
pub sensors: Vec<SensorReading>,
|
|
}
|
|
|
|
/// 管道桥接:管理 NexBoxMonitor 子进程的 stdin/stdout
|
|
pub struct SensorBridge {
|
|
child: Child,
|
|
reader: BufReader<std::process::ChildStdout>,
|
|
writer: std::process::ChildStdin,
|
|
}
|
|
|
|
impl SensorBridge {
|
|
/// 发送读取命令,返回传感器数据
|
|
pub fn read_sensors(&mut self) -> Result<SensorsResponse, String> {
|
|
// 发送命令
|
|
writeln!(self.writer, r#"{{"cmd":"read"}}"#)
|
|
.map_err(|e| format!("写入管道失败: {}", e))?;
|
|
self.writer
|
|
.flush()
|
|
.map_err(|e| format!("刷新管道失败: {}", e))?;
|
|
|
|
// 读取响应
|
|
let mut line = String::new();
|
|
self.reader
|
|
.read_line(&mut line)
|
|
.map_err(|e| format!("读取管道失败: {}", e))?;
|
|
|
|
if line.trim().is_empty() {
|
|
return Err("子进程返回空响应".to_string());
|
|
}
|
|
|
|
serde_json::from_str::<SensorsResponse>(&line)
|
|
.map_err(|e| format!("解析传感器JSON失败: {}", e))
|
|
}
|
|
|
|
/// 检查子进程是否仍然存活
|
|
pub fn is_alive(&mut self) -> bool {
|
|
match self.child.try_wait() {
|
|
Ok(None) => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
/// 优雅关闭子进程(最多等待 3 秒,超时则强制终止)
|
|
pub fn shutdown(&mut self) {
|
|
let _ = writeln!(self.writer, r#"{{"cmd":"exit"}}"#);
|
|
let _ = self.writer.flush();
|
|
let start = std::time::Instant::now();
|
|
loop {
|
|
match self.child.try_wait() {
|
|
Ok(Some(_)) => break,
|
|
Ok(None) => {
|
|
if start.elapsed() > std::time::Duration::from_secs(3) {
|
|
log::warn!("NexBoxMonitor 子进程未在 3s 内退出,强制终止");
|
|
let _ = self.child.kill();
|
|
let _ = self.child.wait();
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
}
|
|
Err(e) => {
|
|
log::warn!("等待 NexBoxMonitor 退出出错: {}", e);
|
|
let _ = self.child.kill();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 全局传感器桥接
|
|
static SENSOR_BRIDGE: Mutex<Option<SensorBridge>> = Mutex::new(None);
|
|
|
|
/// 启动传感器子进程
|
|
pub fn start_sensor_process(app: &App) {
|
|
match spawn_sensor() {
|
|
Ok(Some(bridge)) => {
|
|
log::info!("已启动 NexBoxMonitor 子进程 (pid={})", bridge.child.id());
|
|
*SENSOR_BRIDGE.lock().unwrap() = Some(bridge);
|
|
app.manage(SensorChild(Mutex::new(None))); // 保持兼容
|
|
}
|
|
Ok(None) => {
|
|
log::info!("NexBoxMonitor 未找到,跳过启动");
|
|
app.manage(SensorChild(Mutex::new(None)));
|
|
}
|
|
Err(e) => {
|
|
log::warn!("启动 NexBoxMonitor 失败: {e}");
|
|
app.manage(SensorChild(Mutex::new(None)));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 停止传感器子进程
|
|
pub fn stop_sensor_process(app: &AppHandle) {
|
|
// 先处理旧的 SensorChild(兼容)
|
|
if let Some(state) = app.try_state::<SensorChild>() {
|
|
let child = {
|
|
let mut guard = state
|
|
.0
|
|
.lock()
|
|
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
guard.take()
|
|
};
|
|
if let Some(mut child) = child {
|
|
log::info!("正在停止传感器子进程 (pid={})", child.id());
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
}
|
|
}
|
|
|
|
// 关闭新的 SensorBridge
|
|
if let Some(mut bridge) = SENSOR_BRIDGE.lock().unwrap().take() {
|
|
log::info!("正在关闭 NexBoxMonitor 子进程 (pid={})", bridge.child.id());
|
|
bridge.shutdown();
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn get_lhm_cpu_load() -> Result<Option<u16>, String> {
|
|
match read_lhm_sensors() {
|
|
Ok(response) => {
|
|
for s in &response.sensors {
|
|
if s.hardware_type.eq_ignore_ascii_case("CPU")
|
|
&& s.sensor_type == "Load"
|
|
&& (s.name == "CPU Total" || s.name == "Total")
|
|
{
|
|
return Ok(Some(s.value as u16));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
Err(e) => {
|
|
log::warn!("LHML CPU load 读取失败: {e}");
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn get_lhm_cpu_status() -> Result<(Option<u16>, Option<f64>), String> {
|
|
match read_lhm_sensors() {
|
|
Ok(response) => {
|
|
let mut load = None;
|
|
let mut temp = None;
|
|
for s in &response.sensors {
|
|
if s.hardware_type.eq_ignore_ascii_case("CPU") {
|
|
if s.sensor_type == "Load" && (s.name == "CPU Total" || s.name == "Total") {
|
|
load = Some(s.value as u16);
|
|
}
|
|
if s.sensor_type == "Temperature"
|
|
&& (s.name == "Core (Tctl/Tdie)" || s.name == "CPU Package" || s.name == "Tctl" || s.name == "Core")
|
|
{
|
|
temp = Some(s.value);
|
|
}
|
|
}
|
|
}
|
|
Ok((load, temp))
|
|
}
|
|
Err(e) => {
|
|
log::warn!("LHML CPU status 读取失败: {e}");
|
|
Ok((None, None))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub async fn get_lhm_gpu_status() -> Result<Vec<(Option<f64>, Option<u32>)>, String> {
|
|
match read_lhm_sensors() {
|
|
Ok(response) => {
|
|
let gpu_hardware_types: Vec<_> = {
|
|
let mut types: Vec<_> = response.sensors.iter()
|
|
.filter(|s| s.hardware_type.to_lowercase().starts_with("gpu"))
|
|
.map(|s| s.hardware_type.clone())
|
|
.collect();
|
|
types.dedup();
|
|
types
|
|
};
|
|
|
|
// 判断是否存在 NVIDIA 独显(仅 NVIDIA 明确是独显,AMD 可能是 APU 核显)
|
|
let has_nvidia = gpu_hardware_types.iter().any(|t| {
|
|
t.eq_ignore_ascii_case("GpuNvidia")
|
|
});
|
|
|
|
let mut results = Vec::new();
|
|
for hw_type in &gpu_hardware_types {
|
|
// NVIDIA 独显存在时跳过 Intel 核显,AMD 核显保留显示
|
|
if has_nvidia && hw_type.eq_ignore_ascii_case("GpuIntel") {
|
|
log::info!("跳过核显(LHML): 存在 NVIDIA 独显,忽略 GpuIntel");
|
|
continue;
|
|
}
|
|
let temp = response.sensors.iter()
|
|
.filter(|s| s.hardware_type == *hw_type && s.sensor_type == "Temperature" && s.name == "GPU Core")
|
|
.map(|s| s.value)
|
|
.next();
|
|
let usage = response.sensors.iter()
|
|
.filter(|s| s.hardware_type == *hw_type && s.sensor_type == "Load" && s.name == "GPU Core")
|
|
.map(|s| s.value as u32)
|
|
.next();
|
|
results.push((temp, usage));
|
|
}
|
|
Ok(results)
|
|
}
|
|
Err(e) => {
|
|
log::warn!("LHML GPU status 读取失败: {e}");
|
|
Ok(Vec::new())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 从 LHML 读取传感器数据(供 overlay_panel.rs 调用)
|
|
pub fn read_lhm_sensors() -> Result<SensorsResponse, String> {
|
|
let mut guard = SENSOR_BRIDGE
|
|
.lock()
|
|
.map_err(|e| format!("锁获取失败: {}", e))?;
|
|
|
|
match guard.as_mut() {
|
|
Some(bridge) => {
|
|
if !bridge.is_alive() {
|
|
log::warn!("NexBoxMonitor 子进程已退出,尝试重启...");
|
|
*guard = None;
|
|
drop(guard);
|
|
match spawn_sensor() {
|
|
Ok(Some(new_bridge)) => {
|
|
log::info!("NexBoxMonitor 重启成功 (pid={})", new_bridge.child.id());
|
|
*SENSOR_BRIDGE.lock().unwrap() = Some(new_bridge);
|
|
return Err("子进程已重启,请重试".to_string());
|
|
}
|
|
_ => return Err("NexBoxMonitor 不可用".to_string()),
|
|
}
|
|
}
|
|
bridge.read_sensors()
|
|
}
|
|
None => {
|
|
// 尝试启动
|
|
drop(guard);
|
|
match spawn_sensor() {
|
|
Ok(Some(bridge)) => {
|
|
log::info!("延迟启动 NexBoxMonitor (pid={})", bridge.child.id());
|
|
*SENSOR_BRIDGE.lock().unwrap() = Some(bridge);
|
|
Err("子进程已启动,请重试".to_string())
|
|
}
|
|
_ => Err("NexBoxMonitor 不可用".to_string()),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 查找 NexBoxMonitor.exe 路径
|
|
fn find_monitor_exe() -> Option<std::path::PathBuf> {
|
|
// 获取 exe 所在目录作为基准
|
|
let exe_dir = std::env::current_exe()
|
|
.ok()?
|
|
.parent()
|
|
.map(|p| p.to_path_buf())
|
|
.unwrap_or(std::path::PathBuf::from("."));
|
|
|
|
let exe_name = "NexBoxMonitor.exe";
|
|
|
|
// 定义所有要探测的候选路径生成器
|
|
// 参数 base: 基准目录, sub: 子路径后缀
|
|
let candidates: Vec<std::path::PathBuf> = {
|
|
let mut list = Vec::new();
|
|
let suffixes = [
|
|
// 方案A: 安装版 — {app}/monitor/NexBoxMonitor.exe
|
|
// Inno Setup 把 publish/* 复制到 {app}/monitor/
|
|
"monitor",
|
|
// 方案B: 用户描述的路径 — {app}/NexBox/monitor/NexBoxMonitor.exe
|
|
"NexBox/monitor",
|
|
// 方案C: Tauri 资源目录
|
|
"resources/monitor",
|
|
// 方案D: 旧版开发者构建路径
|
|
"monitor/bin/Release/net48",
|
|
// 方案E: publish 输出
|
|
"monitor/bin/Release/net48",
|
|
];
|
|
for suffix in &suffixes {
|
|
let p = exe_dir.join(suffix).join(exe_name);
|
|
list.push(p);
|
|
}
|
|
list
|
|
};
|
|
|
|
// 一次性检查所有候选路径
|
|
for path in &candidates {
|
|
if path.exists() {
|
|
log::info!("找到 NexBoxMonitor: {}", path.display());
|
|
return Some(path.clone());
|
|
}
|
|
}
|
|
|
|
// 方案F: 从 exe 目录向上回溯,查找项目根目录下的 monitor 构建产物
|
|
// 开发环境下 exe 位于 src-tauri/target/{debug,release}/nexbox.exe
|
|
let mut probe = exe_dir.clone();
|
|
for _ in 0..5 {
|
|
// 每层尝试: monitor/bin/Release/net48
|
|
let p1 = probe
|
|
.join("monitor")
|
|
.join("bin")
|
|
.join("Release")
|
|
.join("net48")
|
|
.join(exe_name);
|
|
if p1.exists() {
|
|
log::info!("找到 NexBoxMonitor (probe): {}", p1.display());
|
|
return Some(p1);
|
|
}
|
|
if !probe.pop() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 方案G: 通过 CARGO_MANIFEST_DIR 编译期路径(仅 debug 构建,纯备用)
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf();
|
|
let dev_path = base
|
|
.join("monitor")
|
|
.join("bin")
|
|
.join("Release")
|
|
.join("net48")
|
|
.join(exe_name);
|
|
if dev_path.exists() {
|
|
log::info!("找到 NexBoxMonitor (dev): {}", dev_path.display());
|
|
return Some(dev_path);
|
|
}
|
|
let pub_path = base
|
|
.join("monitor")
|
|
.join("bin")
|
|
.join("Release")
|
|
.join("net48")
|
|
.join(exe_name);
|
|
if pub_path.exists() {
|
|
log::info!("找到 NexBoxMonitor (publish): {}", pub_path.display());
|
|
return Some(pub_path);
|
|
}
|
|
}
|
|
|
|
log::warn!(
|
|
"未找到 NexBoxMonitor.exe (exe_dir: {}), 已尝试路径: {:?}",
|
|
exe_dir.display(),
|
|
candidates.iter().map(|p| p.display().to_string()).collect::<Vec<_>>()
|
|
);
|
|
None
|
|
}
|
|
|
|
fn spawn_sensor() -> std::io::Result<Option<SensorBridge>> {
|
|
let exe_path = match find_monitor_exe() {
|
|
Some(p) => p,
|
|
None => return Ok(None),
|
|
};
|
|
|
|
let mut cmd = Command::new(&exe_path);
|
|
cmd.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::null());
|
|
|
|
// Windows 下隐藏控制台窗口
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::process::CommandExt;
|
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
cmd.creation_flags(CREATE_NO_WINDOW);
|
|
}
|
|
|
|
let mut child = cmd.spawn()?;
|
|
|
|
let stdin = child.stdin.take().expect("无法获取子进程 stdin");
|
|
let stdout = child.stdout.take().expect("无法获取子进程 stdout");
|
|
|
|
let bridge = SensorBridge {
|
|
child,
|
|
reader: BufReader::new(stdout),
|
|
writer: stdin,
|
|
};
|
|
|
|
Ok(Some(bridge))
|
|
} |