监控模块 优化

This commit is contained in:
zhongluofeng
2026-07-24 18:16:34 +08:00
parent e8267c244f
commit 7e6149355e
15 changed files with 2148 additions and 234 deletions
+503 -8
View File
@@ -12,6 +12,7 @@
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
@@ -35,6 +36,155 @@ const READY_POLL_INTERVAL_MS: u64 = 500;
/// 冷启动就绪总超时(阶段一实测冷启动约 5s,留 5s 余量)
const READY_TIMEOUT_MS: u64 = 10_000;
// ===================== Windows 提权启动 FFI =====================
/// ShellExecuteW 的 nShowCmd 取值:隐藏窗口。
/// ThingHK.exe 是控制台程序,提权启动时隐藏控制台窗口避免黑框。
#[cfg(windows)]
const SW_HIDE: i32 = 0;
// ShellExecuteW 返回值 <= 32 表示错误(HINSTANCE 被强制为 isize)。
// 1223 = ERROR_CANCELLED(用户取消了 UAC 弹窗)。
#[cfg(windows)]
#[link(name = "shell32")]
extern "system" {
fn ShellExecuteW(
hwnd: *mut std::ffi::c_void,
lp_operation: *const u16,
lp_file: *const u16,
lp_parameters: *const u16,
lp_directory: *const u16,
n_show_cmd: i32,
) -> isize;
}
/// 以管理员权限启动可执行文件(弹 UAC)。
/// 通过 ShellExecuteW "runas" 动词实现,返回的进程句柄不可用(跨权限级别),
/// 因此提权后的进程不加入 ProcessManager,停止时通过 Kernel 的 /shutdown 接口。
#[cfg(windows)]
fn shell_execute_elevated(file: &str, params: &str, dir: Option<&str>) -> Result<(), String> {
use std::os::windows::ffi::OsStrExt;
fn to_wide(s: &str) -> Vec<u16> {
std::ffi::OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
}
let operation = to_wide("runas");
let file_w = to_wide(file);
let params_w = to_wide(params);
let dir_w = dir.map(to_wide);
let dir_ptr = dir_w.as_ref().map_or(std::ptr::null(), |d| d.as_ptr());
let result = unsafe {
ShellExecuteW(
std::ptr::null_mut(),
operation.as_ptr(),
file_w.as_ptr(),
params_w.as_ptr(),
dir_ptr,
SW_HIDE,
)
};
if result <= 32 {
// 1223 = ERROR_CANCELLED:用户点了"否"或关闭了 UAC 弹窗
if result == 1223 {
return Err("用户取消了 UAC 提权".into());
}
Err(format!("ShellExecuteW 失败,错误码: {}", result))
} else {
Ok(())
}
}
/// 检测当前 Thing 进程是否以管理员权限运行。
/// 用于"永久提权"模式:Thing 本身是管理员时,ThingHK 子进程自动继承权限,
/// 无需 ShellExecuteW "runas"ProcessManager 可直接管理(同权限级别可 kill)。
#[cfg(windows)]
pub fn is_thing_elevated() -> bool {
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
use windows_sys::Win32::Security::{
GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY,
};
use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
unsafe {
let mut token: HANDLE = 0;
if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
return false;
}
let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };
let mut ret_len = 0u32;
let success = GetTokenInformation(
token,
TokenElevation,
&mut elevation as *mut _ as *mut _,
std::mem::size_of::<TOKEN_ELEVATION>() as u32,
&mut ret_len,
);
CloseHandle(token);
success != 0 && elevation.TokenIsElevated != 0
}
}
#[cfg(not(windows))]
pub fn is_thing_elevated() -> bool {
false
}
// ===================== 永久提权标志持久化 =====================
/// 永久提权标志文件路径: {app_data_dir}/monitor/elevate.json
/// 内容: { "enabled": true }
fn elevate_flag_path(app_data_dir: &std::path::Path) -> PathBuf {
app_data_dir.join("monitor").join("elevate.json")
}
/// 读取永久提权标志
fn read_elevate_flag(flag_path: &std::path::Path) -> bool {
match fs::read_to_string(flag_path) {
Ok(content) => serde_json::from_str::<serde_json::Value>(&content)
.ok()
.and_then(|v| v.get("enabled").and_then(|e| e.as_bool()))
.unwrap_or(false),
Err(_) => false,
}
}
/// 写入永久提权标志
fn write_elevate_flag(flag_path: &std::path::Path, enabled: bool) -> Result<(), String> {
if let Some(parent) = flag_path.parent() {
fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?;
}
let content = serde_json::json!({ "enabled": enabled }).to_string();
fs::write(flag_path, content).map_err(|e| format!("写入提权标志失败: {}", e))
}
/// 启动时检查永久提权标志,如果已设置且当前非管理员,则 ShellExecute "runas" 重启自身。
/// 在 `setup` 早期调用(MonitorKernel 初始化之前)。
/// 返回 true 表示已触发提权重启,调用方应立即退出当前进程。
pub fn check_and_relaunch_if_needed(app_data_dir: &std::path::Path) -> bool {
let flag_path = elevate_flag_path(app_data_dir);
if !read_elevate_flag(&flag_path) {
return false;
}
if is_thing_elevated() {
return false; // 已是管理员,无需重启
}
let exe = match std::env::current_exe() {
Ok(e) => e,
Err(_) => return false,
};
eprintln!("[monitor] 检测到永久提权标志,以管理员权限重启 Thing");
match shell_execute_elevated(&exe.to_string_lossy(), "", None) {
Ok(()) => true,
Err(e) => {
eprintln!("[monitor] 永久提权重启失败(用户可能取消了 UAC): {}", e);
false
}
}
}
// ===================== 数据结构 =====================
/// Kernel /status 响应(与 ThingHK Contracts.cs KernelStatus 对应)
@@ -73,6 +223,10 @@ pub struct MonitorStatus {
pub ready: bool,
pub sensor_count: u32,
pub restart_count: u32,
/// 是否为提权模式(通过 UAC 以管理员权限启动)
pub elevated: bool,
/// Thing 自身是否以管理员权限运行(永久提权模式)
pub thing_elevated: bool,
}
// ===================== MonitorKernel =====================
@@ -91,6 +245,8 @@ pub struct MonitorKernel {
sub_handle: Arc<Mutex<Option<tauri::async_runtime::JoinHandle<()>>>>,
/// 监听 process-status-changed 的句柄,用于在 stop 时取消监听
listener_ids: Arc<Mutex<Vec<tauri::EventId>>>,
/// 是否以管理员权限运行(提权模式下进程不受 ProcessManager 管控,停止走 /shutdown
elevated: Arc<AtomicBool>,
}
impl MonitorKernel {
@@ -108,9 +264,25 @@ impl MonitorKernel {
.unwrap_or_else(|_| Client::new()),
sub_handle: Arc::new(Mutex::new(None)),
listener_ids: Arc::new(Mutex::new(Vec::new())),
elevated: Arc::new(AtomicBool::new(false)),
}
}
/// 当前是否处于提权模式
pub fn is_elevated(&self) -> bool {
self.elevated.load(Ordering::SeqCst)
}
/// 永久提权标志是否已启用(后续启动自动触发 UAC)
pub fn elevate_on_launch(&self) -> bool {
read_elevate_flag(&self.root.join("elevate.json"))
}
/// 设置/清除永久提权标志
pub fn set_elevate_on_launch(&self, enabled: bool) -> Result<(), String> {
write_elevate_flag(&self.root.join("elevate.json"), enabled)
}
fn cores_dir(&self) -> PathBuf {
self.root.join("cores")
}
@@ -120,15 +292,23 @@ impl MonitorKernel {
fn kernel_url(&self) -> String {
format!("http://127.0.0.1:{}", KERNEL_PORT)
}
/// 硬件监控配置文件路径:{app_data_dir}/monitor/hardware-config.json
pub fn hardware_config_path(&self) -> PathBuf {
self.root.join("hardware-config.json")
}
/// 确保内核就位:若 cores/ 无内核,从资源目录复制
/// 确保内核就位:若 cores/ 无内核或版本过期(源文件较新),从资源目录复制
pub fn prepare_kernel(&self, app: &AppHandle) -> Result<MonitorKernelInfo, String> {
let kernel = self.kernel_path();
if !kernel.exists() {
if let Ok(res) = app.path().resolve("binaries/ThingHK.exe", BaseDirectory::Resource) {
if res.exists() {
if let Ok(src) = app.path().resolve("binaries/ThingHK.exe", BaseDirectory::Resource) {
if src.exists() {
let need_copy = !kernel.exists()
|| fs::metadata(&src)
.and_then(|s| fs::metadata(&kernel).map(|d| s.len() != d.len()))
.unwrap_or(true);
if need_copy {
fs::create_dir_all(self.cores_dir()).ok();
fs::copy(&res, &kernel).map_err(|e| format!("复制 Kernel 失败: {}", e))?;
fs::copy(&src, &kernel).map_err(|e| format!("复制 Kernel 失败: {}", e))?;
}
}
}
@@ -155,7 +335,8 @@ impl MonitorKernel {
"serve".into(),
"--port".into(),
KERNEL_PORT.to_string(),
"--basic".into(),
"--config".into(),
self.hardware_config_path().to_string_lossy().to_string(),
],
cwd: Some(self.cores_dir().to_string_lossy().to_string()),
name: "ThingHK".into(),
@@ -273,6 +454,12 @@ impl MonitorKernel {
// 重连前先确认 Kernel 是否还活着(可能已被 stop)
if !self.is_kernel_alive().await {
eprintln!("[monitor] Kernel 已停止,退出 SSE 循环");
// 提权模式下 Kernel 崩溃/退出后重置 elevated 标志,
// 否则 monitor_status 会一直认为提权模式但 Kernel 已死,用户无法重启
if self.is_elevated() {
self.elevated.store(false, Ordering::SeqCst);
eprintln!("[monitor] 提权 Kernel 已退出,重置 elevated 标志");
}
break;
}
}
@@ -345,7 +532,14 @@ impl MonitorKernel {
/// 注册 process-status-changed 监听:当 Kernel 进程被自动重启恢复 Running 时,
/// 自动重新启动 SSE 订阅(实现崩溃恢复后的自愈)。
/// 注册前先清理旧 listener,防止重复注册导致多次 spawn SSE 订阅。
pub async fn register_auto_reconnect(self: Self, app: AppHandle) {
// 清理旧 listener(防止重复注册)
let old_ids = self.listener_ids.lock().await.drain(..).collect::<Vec<_>>();
for id in old_ids {
app.unlisten(id);
}
let app_clone = app.clone();
let this = self.clone();
let id = app.listen("process-status-changed", move |event| {
@@ -371,14 +565,67 @@ impl MonitorKernel {
/// 启动 Kernel 进程并开始 SSE 订阅(命令和 setup 自动启动共用)。
/// ProcessManager 通过 app.state 获取,无需外部传入。
///
/// 启动前先检测端口 8730 是否已有 Kernel 在运行(可能是上次提权遗留的进程):
/// - 如果是提权 KernelisAdmin=true):直接接管,设置 elevated=true,跳过 ProcessManager 启动
/// - 如果是普通权限 Kernel:调用 /shutdown 停止后重新启动
///
/// 永久提权模式(Thing 自身是管理员):
/// - ThingHK 通过 ProcessManager 启动,子进程继承管理员权限
/// - 设置 elevated=true 表示 ThingHK 在管理员权限下运行
/// - ProcessManager 可直接 kill(同权限级别),无需 /shutdown 接口
pub async fn start_with_subscription(
&self,
app: &AppHandle,
) -> Result<crate::process_manager::ProcessInfo, String> {
let thing_elevated = is_thing_elevated();
// 检测是否已有 Kernel 在运行(端口被占用)
if let Ok(status) = self.get_status().await {
if status.ready {
if thing_elevated {
// Thing 是管理员:停止已有 Kernel(无论什么权限),用 ProcessManager 重启以继承权限
eprintln!("[monitor] Thing 已提权,重启 ThingHK 以继承管理员权限");
let _ = self.shutdown_kernel().await;
tokio::time::sleep(Duration::from_millis(500)).await;
} else if status.is_admin {
// Thing 非管理员,但已有提权 Kernel:直接接管
eprintln!("[monitor] 检测到已有提权 Kernel 运行中,直接接管");
self.elevated.store(true, Ordering::SeqCst);
let kernel = self.clone();
kernel.clone().register_auto_reconnect(app.clone()).await;
let app_clone = app.clone();
let handle = tauri::async_runtime::spawn(async move {
kernel.start_subscription(app_clone).await;
});
*self.sub_handle.lock().await = Some(handle);
// 提权模式下无 ProcessInfo,返回一个占位的
return Ok(crate::process_manager::ProcessInfo {
id: PROCESS_ID.into(),
name: "ThingHK".into(),
status: crate::process_manager::ProcessStatus::Running,
pid: None,
restart_count: 0,
});
} else {
// Thing 非管理员,已有普通 Kernel:先停止
eprintln!("[monitor] 检测到已有普通权限 Kernel 运行中,先停止再重启");
let _ = self.shutdown_kernel().await;
tokio::time::sleep(Duration::from_millis(500)).await;
}
}
}
let pm = app.state::<ProcessManager>();
let params = self.prepare_for_start(app)?;
let info = pm.start(params)?;
// Thing 是管理员时,ThingHK 继承权限,标记 elevated(仍由 ProcessManager 管理)
if thing_elevated {
self.elevated.store(true, Ordering::SeqCst);
eprintln!("[monitor] ThingHK 已以管理员权限启动(继承自 Thing)");
}
// 用 self 的 clone(共享 Arc<Mutex> 状态)启动订阅,
// 确保 register_auto_reconnect 注册的 listener 与 sub_handle 共享,
// stop_subscription 时才能正确清理 listener。
@@ -392,6 +639,117 @@ impl MonitorKernel {
Ok(info)
}
/// 以管理员权限启动 Kernel(弹 UAC)。
///
/// 流程:
/// 1. 停止当前普通权限 Kernel(如果在运行)+ 取消 SSE 订阅
/// 2. 等待端口释放
/// 3. ShellExecuteW "runas" 启动提权 Kernel(隐藏控制台窗口)
/// 4. 标记 elevated=true,注册自动重连 + 启动 SSE 订阅
///
/// 提权模式限制(与普通模式的差异):
/// - 进程不加入 ProcessManager(跨权限级别句柄不可用)
/// - 停止通过 Kernel 的 POST /shutdown 接口(普通权限无法 TerminateProcess 管理员进程)
/// - 崩溃不支持自动重启(ProcessManager 看不到此进程)
/// - pid 不可查询(返回 None
pub async fn start_elevated(&self, app: &AppHandle) -> Result<(), String> {
// 1. 停止当前 Kernel(可能是普通权限,也可能是已提权的)
self.stop_subscription(app).await;
let pm = app.state::<ProcessManager>();
if pm.get_status(PROCESS_ID).is_some() {
let _ = pm.stop(PROCESS_ID);
}
if self.elevated.load(Ordering::SeqCst) {
// 已是提权模式,先通过 /shutdown 停止旧进程
let _ = self.shutdown_kernel().await;
self.elevated.store(false, Ordering::SeqCst);
}
// 2. 准备 Kernel 二进制
let info = self.prepare_kernel(app)?;
if !info.exists {
return Err(format!(
"ThingHK Kernel 未安装。请将 ThingHK.exe 放置到 src-tauri/binaries/ 后重新构建。"
));
}
// 3. 等待端口释放(pm.stop 异步 kill 后 Windows 端口释放有延迟)
tokio::time::sleep(Duration::from_millis(800)).await;
// 4. ShellExecute "runas" 启动提权 Kernel
// 提权模式下控制台窗口隐藏(SW_HIDE),stderr 不可见,
// 通过 --log-file 将日志写入文件,便于排查崩溃问题
let log_path = self.root.join("kernel-elevated.log");
let config_path = self.hardware_config_path();
let params = format!(
"serve --port {} --config \"{}\" --log-file \"{}\"",
KERNEL_PORT,
config_path.to_string_lossy(),
log_path.to_string_lossy()
);
#[cfg(windows)]
{
shell_execute_elevated(
&info.path,
&params,
Some(&self.cores_dir().to_string_lossy()),
)?;
}
#[cfg(not(windows))]
{
let _ = params;
return Err("提权启动仅支持 Windows".into());
}
self.elevated.store(true, Ordering::SeqCst);
eprintln!("[monitor] 提权启动已发起,等待 Kernel ready");
// 5. 启动 SSE 订阅(提权模式不注册 register_auto_reconnect
// 提权进程不归 ProcessManager 管,process-status-changed 事件不会触发,
// 注册了反而可能在其他进程状态变化时误触发 SSE 重连)
let kernel = self.clone();
let app_clone = app.clone();
let handle = tauri::async_runtime::spawn(async move {
kernel.start_subscription(app_clone).await;
});
*self.sub_handle.lock().await = Some(handle);
Ok(())
}
/// 调用 Kernel 的 POST /shutdown 接口,让提权 Kernel 自行退出。
/// 普通权限 Tauri 无法 TerminateProcess 管理员进程,必须通过 HTTP 优雅关闭。
async fn shutdown_kernel(&self) -> Result<(), String> {
let url = format!("{}/shutdown", self.kernel_url());
let resp = self
.client
.post(&url)
.timeout(Duration::from_secs(3))
.send()
.await
.map_err(|e| format!("请求 /shutdown 失败: {}", e))?;
if !resp.status().is_success() {
return Err(format!("/shutdown 返回 HTTP {}", resp.status()));
}
// 等待 Kernel 进程退出 + 端口释放
tokio::time::sleep(Duration::from_millis(500)).await;
Ok(())
}
/// 程序退出时清理:取消 SSE 订阅 + 停止提权 Kernel。
/// 普通权限 Kernel 和永久提权模式(Thing 也是管理员)由 ProcessManager.stop_all 统一清理;
/// 仅"提权 ThingHK"模式(Thing 普通、ThingHK 管理员)需通过 /shutdown 接口停止,
/// 否则程序关闭后提权 Kernel 会残留并占用端口,导致下次启动端口冲突。
pub async fn cleanup_on_exit(&self, app: &AppHandle) {
self.stop_subscription(app).await;
// 仅当 ThingHK 提权但 Thing 未提权时才需要 /shutdownProcessManager 无法 kill 管理员进程)
if self.is_elevated() && !is_thing_elevated() {
eprintln!("[monitor] 退出清理:停止提权 Kernel/shutdown");
let _ = self.shutdown_kernel().await;
self.elevated.store(false, Ordering::SeqCst);
}
}
}
/// 解析 SSE 事件文本,提取 data: 字段的 JSON 内容。
@@ -425,12 +783,29 @@ pub async fn monitor_status(
state: tauri::State<'_, MonitorKernel>,
pm: tauri::State<'_, ProcessManager>,
) -> Result<MonitorStatus, String> {
let thing_elevated = is_thing_elevated();
// 提权模式(仅 ThingHK 提权,Thing 普通):进程不归 ProcessManager 管
if state.is_elevated() && !thing_elevated {
let kernel_status = state.get_status().await.ok();
let running = kernel_status.is_some();
return Ok(MonitorStatus {
running,
pid: None,
ready: kernel_status.as_ref().map(|s| s.ready).unwrap_or(false),
sensor_count: kernel_status.as_ref().map(|s| s.sensor_count).unwrap_or(0),
restart_count: 0,
elevated: true,
thing_elevated: false,
});
}
// 普通权限模式 或 永久提权模式(Thing 管理员):通过 ProcessManager 获取进程状态
let info = pm.get_status(PROCESS_ID);
let running = info
.as_ref()
.map(|i| matches!(i.status, crate::process_manager::ProcessStatus::Running))
.unwrap_or(false);
// 只在运行时查询 Kernel /status(避免未运行时发起无意义的 HTTP 请求)
let kernel_status = if running { state.get_status().await.ok() } else { None };
Ok(MonitorStatus {
running,
@@ -438,6 +813,9 @@ pub async fn monitor_status(
ready: kernel_status.as_ref().map(|s| s.ready).unwrap_or(false),
sensor_count: kernel_status.as_ref().map(|s| s.sensor_count).unwrap_or(0),
restart_count: info.as_ref().map(|i| i.restart_count).unwrap_or(0),
// 永久提权模式下 elevated=trueThingHK 在管理员权限运行)
elevated: thing_elevated,
thing_elevated,
})
}
@@ -449,6 +827,50 @@ pub async fn monitor_start(
state.start_with_subscription(&app).await
}
/// 以管理员权限启动 Kernel(弹 UAC)。
/// 提权后的进程不归 ProcessManager 管,停止需通过 monitor_stop(内部走 /shutdown)。
#[tauri::command]
pub async fn monitor_start_elevated(
state: tauri::State<'_, MonitorKernel>,
app: AppHandle,
) -> Result<(), String> {
state.start_elevated(&app).await
}
/// 永久提权:以管理员权限重启 Thing 自身,并持久化标志使后续启动自动提权。
/// - 已是管理员:仅设置标志,不重启
/// - 非管理员:设置标志 + ShellExecute "runas" 重启 + 退出当前进程
#[tauri::command]
pub async fn monitor_elevate_self(
state: tauri::State<'_, MonitorKernel>,
app: AppHandle,
) -> Result<(), String> {
#[cfg(windows)]
{
// 持久化标志:后续启动时 setup 检测到此标志会自动 ShellExecute "runas"
state.set_elevate_on_launch(true)?;
if is_thing_elevated() {
// 已是管理员,只需设置标志,无需重启
eprintln!("[monitor] Thing 已是管理员,仅设置永久提权标志");
return Ok(());
}
let exe = std::env::current_exe().map_err(|e| format!("获取当前路径失败: {}", e))?;
let exe_str = exe.to_string_lossy().to_string();
eprintln!("[monitor] 永久提权:以管理员权限重启 Thing");
shell_execute_elevated(&exe_str, "", None)?;
// 退出当前进程(非提权),新的提权进程会接管
app.exit(0);
Ok(())
}
#[cfg(not(windows))]
{
let _ = (state, app);
Err("永久提权仅支持 Windows".into())
}
}
#[tauri::command]
pub async fn monitor_stop(
state: tauri::State<'_, MonitorKernel>,
@@ -456,7 +878,15 @@ pub async fn monitor_stop(
app: AppHandle,
) -> Result<(), String> {
state.stop_subscription(&app).await;
pm.stop(PROCESS_ID)
// 仅"提权 ThingHK"模式(Thing 普通、ThingHK 管理员)需要 /shutdown
// 永久提权模式(Thing 管理员)和普通模式都用 ProcessManager kill
if state.is_elevated() && !is_thing_elevated() {
state.shutdown_kernel().await?;
state.elevated.store(false, Ordering::SeqCst);
Ok(())
} else {
pm.stop(PROCESS_ID)
}
}
#[tauri::command]
@@ -468,3 +898,68 @@ pub async fn monitor_get_status(state: tauri::State<'_, MonitorKernel>) -> Resul
pub async fn monitor_get_snapshot(state: tauri::State<'_, MonitorKernel>) -> Result<SensorSnapshot, String> {
state.get_snapshot().await
}
/// 查询永久提权标志是否已启用
#[tauri::command]
pub fn monitor_get_elevate_on_launch(
state: tauri::State<'_, MonitorKernel>,
) -> bool {
state.elevate_on_launch()
}
/// 设置/清除永久提权标志。
/// 清除后,下次启动不再触发 UAC(当前会话权限不变)。
#[tauri::command]
pub fn monitor_set_elevate_on_launch(
state: tauri::State<'_, MonitorKernel>,
enabled: bool,
) -> Result<(), String> {
state.set_elevate_on_launch(enabled)
}
/// 查询硬件监控配置(透传 Kernel GET /config/hardware)。
/// 返回当前配置 + 可用硬件/传感器类型清单,供前端 Dialog 渲染。
#[tauri::command]
pub async fn monitor_get_hardware_config(
state: tauri::State<'_, MonitorKernel>,
) -> Result<serde_json::Value, String> {
let url = format!("{}/config/hardware", state.kernel_url());
let resp = state
.client
.get(&url)
.timeout(Duration::from_secs(5))
.send()
.await
.map_err(|e| format!("请求 /config/hardware 失败: {}", e))?;
if !resp.status().is_success() {
return Err(format!("/config/hardware 返回 HTTP {}", resp.status()));
}
resp.json().await.map_err(|e| format!("解析配置失败: {}", e))
}
/// 更新硬件监控配置(透传 Kernel POST /config/hardware)。
/// 传感器类型过滤热生效;硬件开关变化返回 restartRequired=true,需重启 Kernel。
#[tauri::command]
pub async fn monitor_set_hardware_config(
state: tauri::State<'_, MonitorKernel>,
hardware: Option<std::collections::HashMap<String, bool>>,
sensor_types: Option<std::collections::HashMap<String, bool>>,
) -> Result<serde_json::Value, String> {
let url = format!("{}/config/hardware", state.kernel_url());
let body = serde_json::json!({
"hardware": hardware,
"sensorTypes": sensor_types,
});
let resp = state
.client
.post(&url)
.timeout(Duration::from_secs(5))
.json(&body)
.send()
.await
.map_err(|e| format!("请求 POST /config/hardware 失败: {}", e))?;
if !resp.status().is_success() {
return Err(format!("POST /config/hardware 返回 HTTP {}", resp.status()));
}
resp.json().await.map_err(|e| format!("解析响应失败: {}", e))
}