use serde::Serialize; use specta::Type; use std::collections::HashMap; use std::process::{Child, Command, Stdio}; use std::sync::Mutex; use std::thread; use std::time::Duration; use tauri::{AppHandle, Emitter, Manager}; // Windows 平台用于隐藏控制台窗口的标志位 // CREATE_NO_WINDOW = 0x08000000,阻止子进程创建新的控制台窗口 #[cfg(windows)] pub const CREATE_NO_WINDOW: u32 = 0x08000000; // CREATE_NEW_CONSOLE = 0x00000010,强制为控制台类子进程新开一个可见控制台窗口。 // 从 GUI 宿主(无控制台)直接 spawn cmd/powershell 等控制台程序时若不设置, // 子进程会挂到隐藏控制台/不显示窗口,表现为"点击没反应"。 #[cfg(windows)] pub const CREATE_NEW_CONSOLE: u32 = 0x00000010; // Windows Job Object 相关常量,用于异常退出时自动清理子进程 #[cfg(windows)] #[allow(non_snake_case, non_upper_case_globals, non_camel_case_types)] mod winapi { pub const JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE: u32 = 0x2000; pub type HANDLE = *mut std::ffi::c_void; pub type BOOL = i32; pub type DWORD = u32; pub type ULONG_PTR = usize; #[repr(C)] pub struct IO_COUNTERS { pub ReadOperationCount: ULONG_PTR, pub WriteOperationCount: ULONG_PTR, pub OtherOperationCount: ULONG_PTR, pub ReadTransferCount: ULONG_PTR, pub WriteTransferCount: ULONG_PTR, pub OtherTransferCount: ULONG_PTR, } #[repr(C)] pub struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { pub BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION, pub IoInfo: IO_COUNTERS, pub ProcessMemoryLimit: ULONG_PTR, pub JobMemoryLimit: ULONG_PTR, pub PeakProcessMemoryUsed: ULONG_PTR, pub PeakJobMemoryUsed: ULONG_PTR, } #[repr(C)] pub struct JOBOBJECT_BASIC_LIMIT_INFORMATION { pub PerProcessUserTimeLimit: i64, pub PerJobUserTimeLimit: i64, pub LimitFlags: DWORD, pub MinimumWorkingSetSize: ULONG_PTR, pub MaximumWorkingSetSize: ULONG_PTR, pub ActiveProcessLimit: DWORD, pub Affinity: ULONG_PTR, pub PriorityClass: DWORD, pub SchedulingClass: DWORD, } pub const JobObjectExtendedLimitInformation: DWORD = 9; extern "system" { pub fn CreateJobObjectW(lpJobAttributes: *mut std::ffi::c_void, lpName: *const u16) -> HANDLE; pub fn SetInformationJobObject( hJob: HANDLE, JobObjectInformationClass: DWORD, lpJobObjectInformation: *mut std::ffi::c_void, cbJobObjectInformationLength: DWORD, ) -> BOOL; pub fn AssignProcessToJobObject(hJob: HANDLE, hProcess: HANDLE) -> BOOL; } } /// 全局 Job Object 句柄(lazy 初始化,所有子进程都加入此 job) /// 当主进程退出(包括崩溃)时,OS 自动终止 job 内所有子进程 /// 用 usize 存储指针以绕过 Send 约束(句柄本身是进程级资源,线程间共享安全) #[cfg(windows)] static JOB_HANDLE: std::sync::OnceLock = std::sync::OnceLock::new(); #[cfg(windows)] fn get_job_handle() -> Option { let addr = *JOB_HANDLE.get_or_init(|| { unsafe { let h = winapi::CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()); if h.is_null() { crate::logger::log_error("process", "CreateJobObjectW 失败,异常退出时子进程可能残留"); return 0; } // 设置 KILL_ON_JOB_CLOSE:主进程退出时自动终止所有子进程 let mut info: winapi::JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed(); info.BasicLimitInformation.LimitFlags = winapi::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; let ok = winapi::SetInformationJobObject( h, winapi::JobObjectExtendedLimitInformation, &mut info as *mut _ as *mut std::ffi::c_void, std::mem::size_of::() as u32, ); if ok == 0 { crate::logger::log_error("process", "SetInformationJobObject 失败"); return 0; } h as usize } }); if addr == 0 { None } else { Some(addr as winapi::HANDLE) } } /// 为 Command 设置平台特定的创建标志(Windows 上隐藏控制台窗口) /// 公开以便其他模块(如 mihomo_manager 调用 mihomo -v 查询版本)复用 #[cfg(windows)] pub fn setup_creation_flags(cmd: &mut Command) { use std::os::windows::process::CommandExt; cmd.creation_flags(CREATE_NO_WINDOW); } #[cfg(not(windows))] pub fn setup_creation_flags(_cmd: &mut Command) { // 非 Windows 平台无需处理 } /// 将已启动的子进程加入 Job Object(异常退出时自动清理) /// 在 Windows 上调用,非 Windows 平台为空操作 /// pub(crate):音乐模块的桥接进程(自管 stdio,不走 ProcessManager)也需加入 Job #[cfg(windows)] pub(crate) fn assign_to_job(child: &Child) { use std::os::windows::io::AsRawHandle; if let Some(job) = get_job_handle() { let child_handle = child.as_raw_handle() as winapi::HANDLE; unsafe { let _ = winapi::AssignProcessToJobObject(job, child_handle); } } } #[cfg(not(windows))] fn assign_to_job(_child: &Child) {} /// 进程状态枚举 #[derive(Serialize, Clone, Debug, Type)] #[serde(rename_all = "lowercase")] pub enum ProcessStatus { Running, Stopped, Crashed, #[allow(dead_code)] Starting, } /// 进程信息(返回给前端) #[derive(Serialize, Clone, Type)] #[serde(rename_all = "camelCase")] pub struct ProcessInfo { pub id: String, pub name: String, pub status: ProcessStatus, pub pid: Option, pub restart_count: u32, } /// 进程启动参数(从前端传入) #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] pub struct StartProcessParams { pub id: String, pub executable: String, #[serde(default)] pub args: Vec, #[serde(default)] pub cwd: Option, pub name: String, #[serde(default)] pub restart_on_crash: bool, #[serde(default = "default_max_restarts")] pub max_restarts: u32, } fn default_max_restarts() -> u32 { 3 } /// 内部进程条目 struct ProcessEntry { child: Child, name: String, restart_count: u32, max_restarts: u32, restart_on_crash: bool, executable: String, args: Vec, cwd: Option, } /// 进程管理器 —— 管理子进程的完整生命周期 pub struct ProcessManager { processes: Mutex>, } impl ProcessManager { pub fn new() -> Self { Self { processes: Mutex::new(HashMap::new()), } } /// 启动一个子进程 pub fn start(&self, params: StartProcessParams) -> Result { let mut processes = self.processes.lock().map_err(|e| e.to_string())?; // 如果已有同名进程且仍在运行,返回错误 if let Some(entry) = processes.get_mut(¶ms.id) { if entry.child.try_wait().map_err(|e| e.to_string())?.is_none() { return Err(format!("进程 '{}' 已在运行中", params.id)); } // 进程已退出,移除旧记录 processes.remove(¶ms.id); } let mut cmd = Command::new(¶ms.executable); cmd.args(¶ms.args); if let Some(ref dir) = params.cwd { cmd.current_dir(dir); } // 子进程的 stdout/stderr/stdin 不继承主进程 cmd.stdout(Stdio::null()) .stderr(Stdio::null()) .stdin(Stdio::null()); // Windows 上隐藏控制台窗口(mihomo.exe 是控制台程序,否则会弹黑框) setup_creation_flags(&mut cmd); let child = cmd .spawn() .map_err(|e| format!("启动进程 '{}' 失败: {}", params.id, e))?; let pid = child.id(); // 将子进程加入 Job Object,主进程异常退出时由 OS 自动清理 assign_to_job(&child); let entry = ProcessEntry { child, name: params.name.clone(), restart_count: 0, max_restarts: params.max_restarts, restart_on_crash: params.restart_on_crash, executable: params.executable, args: params.args, cwd: params.cwd, }; processes.insert(params.id.clone(), entry); Ok(ProcessInfo { id: params.id, name: params.name, status: ProcessStatus::Running, pid: Some(pid), restart_count: 0, }) } /// 停止指定进程 /// kill 在主线程执行(快速),wait 移到后台线程执行避免阻塞前端 /// Windows 上 kill 后 wait 可能需要等待子进程清理资源,有几十毫秒到几百毫秒延迟 pub fn stop(&self, id: &str) -> Result<(), String> { let mut processes = self.processes.lock().map_err(|e| e.to_string())?; if let Some(mut entry) = processes.remove(id) { entry .child .kill() .map_err(|e| format!("终止进程 '{}' 失败: {}", id, e))?; // 后台等待子进程退出,避免阻塞当前调用线程(前端会感知卡顿) // child 已 move 进闭包,wait 在后台完成 thread::spawn(move || { let _ = entry.child.wait(); }); Ok(()) } else { Err(format!("进程 '{}' 不存在", id)) } } /// 停止所有进程(应用退出时调用) /// 同步 kill + 带超时的 wait,确保子进程在主进程退出前真正终止 pub fn stop_all(&self) { if let Ok(mut processes) = self.processes.lock() { for (id, mut entry) in processes.drain() { let _ = entry.child.kill(); // 带超时的 wait,避免子进程卡住导致主进程无法退出 // 最长等 3 秒,超时则放弃等待(Job Object 兜底会清理) let deadline = std::time::Instant::now() + Duration::from_secs(3); loop { match entry.child.try_wait() { Ok(Some(_)) => break, Ok(None) => { if std::time::Instant::now() >= deadline { crate::logger::log_warn("process", &format!("进程 {} 等待退出超时(3s),放弃等待", id)); break; } std::thread::sleep(Duration::from_millis(50)); } Err(_) => break, } } crate::logger::log_info("process", &format!("已停止进程: {}", id)); } } } /// 获取单个进程状态 pub fn get_status(&self, id: &str) -> Option { let mut processes = self.processes.lock().ok()?; let entry = processes.get_mut(id)?; let (status, pid) = match entry.child.try_wait() { Ok(None) => (ProcessStatus::Running, Some(entry.child.id())), Ok(Some(_)) => (ProcessStatus::Crashed, None), Err(_) => (ProcessStatus::Stopped, None), }; Some(ProcessInfo { id: id.to_string(), name: entry.name.clone(), status, pid, restart_count: entry.restart_count, }) } /// 获取所有进程状态 pub fn get_all_status(&self) -> Vec { let mut result = Vec::new(); if let Ok(mut processes) = self.processes.lock() { for (id, entry) in processes.iter_mut() { let (status, pid) = match entry.child.try_wait() { Ok(None) => (ProcessStatus::Running, Some(entry.child.id())), Ok(Some(_)) => (ProcessStatus::Crashed, None), Err(_) => (ProcessStatus::Stopped, None), }; result.push(ProcessInfo { id: id.clone(), name: entry.name.clone(), status, pid, restart_count: entry.restart_count, }); } } result } /// 检查所有进程,处理崩溃的进程(自动重启或移除) /// 返回状态发生变化的进程列表 /// 注意:kill/wait/sleep(800ms) 等阻塞操作一律在锁外执行, /// 锁内仅做非阻塞的 try_wait 判定,避免阻塞其他进程的状态查询 pub fn check_and_cleanup(&self) -> Vec { let mut changes = Vec::new(); // 需要重启的条目(锁外执行 kill + sleep + spawn) let mut restarts: Vec<(String, ProcessEntry)> = Vec::new(); // 阶段 1:锁内快速判定(仅非阻塞 try_wait),收集重启/移除决策 { let mut processes = match self.processes.lock() { Ok(p) => p, Err(_) => return changes, }; let ids: Vec = processes.keys().cloned().collect(); for id in ids { let Some(entry) = processes.get_mut(&id) else { continue }; match entry.child.try_wait() { Ok(None) => { // 仍在运行,无需处理 } Ok(Some(_)) => { // 进程已退出 if entry.restart_on_crash && (entry.max_restarts == 0 || entry.restart_count < entry.max_restarts) { // 需要自动重启:从 map 移除,锁外执行 if let Some(mut e) = processes.remove(&id) { e.restart_count += 1; restarts.push((id.clone(), e)); } } else { // 不自动重启,移除记录 let name = entry.name.clone(); let restart_count = entry.restart_count; processes.remove(&id); changes.push(ProcessInfo { id: id.clone(), name, status: ProcessStatus::Stopped, pid: None, restart_count, }); } } Err(_) => { // try_wait 出错 let name = entry.name.clone(); let restart_count = entry.restart_count; processes.remove(&id); changes.push(ProcessInfo { id: id.clone(), name, status: ProcessStatus::Stopped, pid: None, restart_count, }); } } } } // 锁在此释放 // 阶段 2:锁外执行重启(kill + wait + 端口释放等待 + spawn,不阻塞进程状态查询) for (id, mut entry) in restarts { // 先终止旧进程 let _ = entry.child.kill(); let _ = entry.child.wait(); // 等待 TCP 端口释放(Windows 上 kill 后端口释放有延迟) std::thread::sleep(std::time::Duration::from_millis(800)); // 重新启动 let mut cmd = Command::new(&entry.executable); cmd.args(&entry.args); if let Some(ref dir) = entry.cwd { cmd.current_dir(dir); } cmd.stdout(Stdio::null()) .stderr(Stdio::null()) .stdin(Stdio::null()); setup_creation_flags(&mut cmd); let name = entry.name.clone(); let restart_count = entry.restart_count; match cmd.spawn() { Ok(new_child) => { let pid = new_child.id(); entry.child = new_child; if let Ok(mut processes) = self.processes.lock() { processes.insert(id.clone(), entry); } changes.push(ProcessInfo { id: id.clone(), name, status: ProcessStatus::Running, pid: Some(pid), restart_count, }); } Err(e) => { crate::logger::log_error( "process", &format!("重启进程 '{}' 失败: {}", id, e), ); changes.push(ProcessInfo { id: id.clone(), name, status: ProcessStatus::Crashed, pid: None, restart_count, }); } } } changes } } // ===== Tauri 命令 ===== #[tauri::command] pub fn process_start( state: tauri::State<'_, ProcessManager>, params: StartProcessParams, ) -> Result { state.start(params) } #[tauri::command] pub fn process_stop( state: tauri::State<'_, ProcessManager>, id: String, ) -> Result<(), String> { state.stop(&id) } #[tauri::command] pub fn process_status( state: tauri::State<'_, ProcessManager>, id: String, ) -> Option { state.get_status(&id) } #[tauri::command] pub fn process_all_status( state: tauri::State<'_, ProcessManager>, ) -> Vec { state.get_all_status() } #[tauri::command] pub fn process_stop_all(state: tauri::State<'_, ProcessManager>) { state.stop_all() } /// 启动后台监控线程,定期检查进程状态并向前端发送事件 pub fn start_monitoring_thread(app: AppHandle) { thread::spawn(move || { loop { thread::sleep(Duration::from_secs(3)); let state = app.state::(); let changes = state.check_and_cleanup(); for change in changes { let _ = app.emit(crate::constants::events::PROCESS_STATUS_CHANGED, &change); } } }); }