监控模块 优化
This commit is contained in:
Generated
+136
-14
@@ -711,6 +711,25 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.22"
|
||||
@@ -1043,6 +1062,12 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "3.0.11"
|
||||
@@ -2394,6 +2419,15 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
@@ -3021,6 +3055,26 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
@@ -3824,6 +3878,20 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c33cd241af0f2e9e3b5c32163b873b29956890b5342e6745b917ce9d490f4af"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"memchr",
|
||||
"ntapi",
|
||||
"rayon",
|
||||
"windows 0.57.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
@@ -3892,7 +3960,7 @@ dependencies = [
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
@@ -3963,7 +4031,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"window-vibrancy",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4118,7 +4186,7 @@ dependencies = [
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
@@ -4160,7 +4228,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4185,7 +4253,7 @@ dependencies = [
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"wry",
|
||||
]
|
||||
|
||||
@@ -4272,6 +4340,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"sysinfo",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
@@ -5023,10 +5092,10 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
|
||||
dependencies = [
|
||||
"webview2-com-macros",
|
||||
"webview2-com-sys",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5047,7 +5116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
@@ -5097,6 +5166,16 @@ dependencies = [
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
||||
dependencies = [
|
||||
"windows-core 0.57.0",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.3"
|
||||
@@ -5119,14 +5198,26 @@ dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||
dependencies = [
|
||||
"windows-implement 0.57.0",
|
||||
"windows-interface 0.57.0",
|
||||
"windows-result 0.1.2",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link 0.1.3",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
@@ -5138,8 +5229,8 @@ version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings 0.5.1",
|
||||
@@ -5156,6 +5247,17 @@ dependencies = [
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
@@ -5167,6 +5269,17 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
@@ -5211,6 +5324,15 @@ dependencies = [
|
||||
"windows-strings 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
@@ -5665,7 +5787,7 @@ dependencies = [
|
||||
"webkit2gtk",
|
||||
"webkit2gtk-sys",
|
||||
"webview2-com",
|
||||
"windows",
|
||||
"windows 0.61.3",
|
||||
"windows-core 0.61.2",
|
||||
"windows-version",
|
||||
"x11-dl",
|
||||
|
||||
@@ -33,10 +33,11 @@ axum = "0.7"
|
||||
url = "2"
|
||||
zip = "2"
|
||||
dirs = "5"
|
||||
sysinfo = "0.32"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = "0.52"
|
||||
windows-sys = { version = "0.52", features = ["Win32_Networking_WinInet", "Win32_Foundation"] }
|
||||
windows-sys = { version = "0.52", features = ["Win32_Networking_WinInet", "Win32_Foundation", "Win32_Security", "Win32_System_Threading"] }
|
||||
|
||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||
tauri-plugin-autostart = "2"
|
||||
|
||||
Binary file not shown.
+34
-1
@@ -1,9 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
use tauri::Manager;
|
||||
|
||||
mod download_engine;
|
||||
mod logger;
|
||||
mod mihomo_manager;
|
||||
mod monitor_kernel;
|
||||
mod network_monitor;
|
||||
mod process_manager;
|
||||
|
||||
use download_engine::{
|
||||
@@ -23,9 +25,12 @@ use mihomo_manager::{
|
||||
proxy_test_delay, proxy_update_kernel, proxy_update_profile, proxy_version, MihomoManager,
|
||||
};
|
||||
use monitor_kernel::{
|
||||
monitor_get_snapshot, monitor_get_status, monitor_kernel_info, monitor_start, monitor_status,
|
||||
monitor_elevate_self, monitor_get_elevate_on_launch, monitor_get_hardware_config,
|
||||
monitor_get_snapshot, monitor_get_status, monitor_kernel_info, monitor_set_elevate_on_launch,
|
||||
monitor_set_hardware_config, monitor_start, monitor_start_elevated, monitor_status,
|
||||
monitor_stop, MonitorKernel,
|
||||
};
|
||||
use network_monitor::{network_monitor_status, NetworkMonitor};
|
||||
use process_manager::{
|
||||
get_all_process_status, get_process_status, start_monitoring_thread, start_process,
|
||||
stop_all_processes, stop_process, ProcessManager,
|
||||
@@ -41,12 +46,15 @@ fn quit_app(
|
||||
state: tauri::State<'_, ProcessManager>,
|
||||
mihomo: tauri::State<'_, MihomoManager>,
|
||||
engine: tauri::State<'_, DownloadEngine>,
|
||||
monitor: tauri::State<'_, MonitorKernel>,
|
||||
app: tauri::AppHandle,
|
||||
) {
|
||||
// 退出前清理系统代理,避免遗留导致网络问题
|
||||
mihomo.cleanup_on_exit();
|
||||
// 退出前保存下载引擎状态
|
||||
engine.cleanup_on_exit();
|
||||
// 停止提权 Kernel(普通权限由 stop_all 统一清理,提权 Kernel 需通过 /shutdown)
|
||||
tauri::async_runtime::block_on(monitor.cleanup_on_exit(&app));
|
||||
// 停止所有子进程(同步 kill + 带超时的 wait,确保进程真正终止)
|
||||
state.stop_all();
|
||||
// 通过 app.exit 触发 RunEvent::ExitRequested,统一退出路径
|
||||
@@ -104,9 +112,16 @@ pub fn run() {
|
||||
monitor_kernel_info,
|
||||
monitor_status,
|
||||
monitor_start,
|
||||
monitor_start_elevated,
|
||||
monitor_elevate_self,
|
||||
monitor_stop,
|
||||
monitor_get_status,
|
||||
monitor_get_snapshot,
|
||||
monitor_get_elevate_on_launch,
|
||||
monitor_set_elevate_on_launch,
|
||||
monitor_get_hardware_config,
|
||||
monitor_set_hardware_config,
|
||||
network_monitor_status,
|
||||
downloader_get_tasks,
|
||||
downloader_add_task,
|
||||
downloader_pause_task,
|
||||
@@ -134,6 +149,12 @@ pub fn run() {
|
||||
.app_data_dir()
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
|
||||
// 永久提权检查:如果标志已设置且当前非管理员,以管理员权限重启自身并退出
|
||||
// 必须在所有模块初始化之前执行(此时无资源需要清理)
|
||||
if monitor_kernel::check_and_relaunch_if_needed(&app_data_dir) {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let mihomo = MihomoManager::new(app_data_dir.clone());
|
||||
app.manage(mihomo);
|
||||
|
||||
@@ -141,6 +162,12 @@ pub fn run() {
|
||||
let monitor = MonitorKernel::new(app_data_dir.clone());
|
||||
app.manage(monitor);
|
||||
|
||||
// 初始化 NetworkMonitor(网速监控,独立于 ThingHK Kernel)
|
||||
// 网速采样不依赖提权,应用启动即开始
|
||||
let network_monitor = Arc::new(NetworkMonitor::new());
|
||||
app.manage(network_monitor.clone());
|
||||
network_monitor.start(app.handle().clone());
|
||||
|
||||
// 初始化 DownloadEngine,数据目录: {app_data_dir}/downloader/
|
||||
let engine = DownloadEngine::new(
|
||||
app_data_dir.join("downloader"),
|
||||
@@ -179,6 +206,9 @@ pub fn run() {
|
||||
if let Some(engine) = app.try_state::<DownloadEngine>() {
|
||||
engine.cleanup_on_exit();
|
||||
}
|
||||
if let Some(monitor) = app.try_state::<MonitorKernel>() {
|
||||
tauri::async_runtime::block_on(monitor.cleanup_on_exit(app));
|
||||
}
|
||||
if let Some(pm) = app.try_state::<ProcessManager>() {
|
||||
pm.stop_all();
|
||||
}
|
||||
@@ -241,6 +271,9 @@ pub fn run() {
|
||||
if let Some(engine) = app.try_state::<DownloadEngine>() {
|
||||
engine.cleanup_on_exit();
|
||||
}
|
||||
if let Some(monitor) = app.try_state::<MonitorKernel>() {
|
||||
tauri::async_runtime::block_on(monitor.cleanup_on_exit(app));
|
||||
}
|
||||
if let Some(pm) = app.try_state::<ProcessManager>() {
|
||||
pm.stop_all();
|
||||
}
|
||||
|
||||
@@ -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 在运行(可能是上次提权遗留的进程):
|
||||
/// - 如果是提权 Kernel(isAdmin=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,
|
||||
¶ms,
|
||||
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 未提权时才需要 /shutdown(ProcessManager 无法 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=true(ThingHK 在管理员权限运行)
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// network_monitor.rs — 网速监控模块
|
||||
//
|
||||
// 职责:
|
||||
// - 后台每秒采样所有网络接口的收发字节数
|
||||
// - 计算与上一次采样的差值(速率,bytes/s)
|
||||
// - 聚合所有接口得到总下载/上传速率
|
||||
// - 通过 Tauri 事件 "monitor-network" 推送给前端
|
||||
//
|
||||
// 设计说明:
|
||||
// - 不依赖 ThingHK(ThingHK 当前不支持网络传感器扩展)
|
||||
// - 使用 sysinfo crate 的 Networks(轻量、跨平台)
|
||||
// - 采样间隔与 Kernel 快通道一致(1s),保证 UI 曲线流畅
|
||||
// - 前端通过监听事件更新 UI,无需主动 invoke
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::Serialize;
|
||||
use sysinfo::Networks;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
/// 推送给前端的网速数据
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NetworkSpeed {
|
||||
/// 下载速率(bytes/s),所有接口聚合
|
||||
pub download_bps: u64,
|
||||
/// 上传速率(bytes/s),所有接口聚合
|
||||
pub upload_bps: u64,
|
||||
/// 时间戳(ms)
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
/// 网速监控器:后台任务采样并 emit 事件。
|
||||
///
|
||||
/// 使用方式:
|
||||
/// 1. app.manage(NetworkMonitor::new()) 注册到 Tauri State
|
||||
/// 2. 在 setup 中调用 NetworkMonitor::start(&app) 启动后台采样
|
||||
/// 3. 前端监听 "monitor-network" 事件
|
||||
pub struct NetworkMonitor {
|
||||
/// 是否正在运行(用于 start/stop 幂等控制)
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl NetworkMonitor {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
running: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动后台网速采样任务。幂等:重复调用不会启动多个任务。
|
||||
pub fn start(self: Arc<Self>, app: AppHandle) {
|
||||
if self.running.swap(true, Ordering::SeqCst) {
|
||||
return; // 已在运行
|
||||
}
|
||||
|
||||
let running = self.running.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// sysinfo Networks 需要可变借用,放在独立线程的本地变量
|
||||
let mut networks = Networks::new_with_refreshed_list();
|
||||
// 首次采样仅记录基线,不 emit(速率需要两次采样差值)
|
||||
let mut last_rx: u64 = networks.iter().map(|(_, data)| data.total_received()).sum();
|
||||
let mut last_tx: u64 = networks.iter().map(|(_, data)| data.total_transmitted()).sum();
|
||||
|
||||
// 1s 间隔采样(与 Kernel 快通道一致)
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
||||
// 跳过第一次立即触发(已记录基线)
|
||||
interval.tick().await;
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if !running.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 刷新网络数据(sysinfo 0.32: refresh() 刷新接口列表和统计数据)
|
||||
networks.refresh();
|
||||
|
||||
let cur_rx: u64 = networks.iter().map(|(_, data)| data.total_received()).sum();
|
||||
let cur_tx: u64 = networks.iter().map(|(_, data)| data.total_transmitted()).sum();
|
||||
|
||||
// 速率 = 当前累计 - 上次累计(sysinfo 的 total_received 是单调递增的累计值)
|
||||
// 若接口移除/添加可能导致差值为负或异常大,做饱和减法保护
|
||||
let download_bps = cur_rx.saturating_sub(last_rx);
|
||||
let upload_bps = cur_tx.saturating_sub(last_tx);
|
||||
last_rx = cur_rx;
|
||||
last_tx = cur_tx;
|
||||
|
||||
let speed = NetworkSpeed {
|
||||
download_bps,
|
||||
upload_bps,
|
||||
timestamp: chrono::Local::now().timestamp_millis() as u64,
|
||||
};
|
||||
|
||||
// emit 给前端(失败忽略:窗口可能未就绪)
|
||||
let _ = app.emit("monitor-network", &speed);
|
||||
}
|
||||
eprintln!("[network] 网速采样任务已退出");
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止后台采样
|
||||
#[allow(dead_code)]
|
||||
pub fn stop(&self) {
|
||||
self.running.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for NetworkMonitor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Tauri 命令:获取网速监控是否运行
|
||||
#[tauri::command]
|
||||
pub fn network_monitor_status(
|
||||
state: tauri::State<'_, NetworkMonitor>,
|
||||
) -> bool {
|
||||
state.running.load(Ordering::SeqCst)
|
||||
}
|
||||
Reference in New Issue
Block a user