Files
Thing/src-tauri/src/process_manager.rs
T
2026-09-12 11:05:26 +08:00

535 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<usize> = std::sync::OnceLock::new();
#[cfg(windows)]
fn get_job_handle() -> Option<winapi::HANDLE> {
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::<winapi::JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() 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<u32>,
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<String>,
#[serde(default)]
pub cwd: Option<String>,
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<String>,
cwd: Option<String>,
}
/// 进程管理器 —— 管理子进程的完整生命周期
pub struct ProcessManager {
processes: Mutex<HashMap<String, ProcessEntry>>,
}
impl ProcessManager {
pub fn new() -> Self {
Self {
processes: Mutex::new(HashMap::new()),
}
}
/// 启动一个子进程
pub fn start(&self, params: StartProcessParams) -> Result<ProcessInfo, String> {
let mut processes = self.processes.lock().map_err(|e| e.to_string())?;
// 如果已有同名进程且仍在运行,返回错误
if let Some(entry) = processes.get_mut(&params.id) {
if entry.child.try_wait().map_err(|e| e.to_string())?.is_none() {
return Err(format!("进程 '{}' 已在运行中", params.id));
}
// 进程已退出,移除旧记录
processes.remove(&params.id);
}
let mut cmd = Command::new(&params.executable);
cmd.args(&params.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<ProcessInfo> {
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<ProcessInfo> {
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<ProcessInfo> {
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<String> = 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<ProcessInfo, String> {
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<ProcessInfo> {
state.get_status(&id)
}
#[tauri::command]
pub fn process_all_status(
state: tauri::State<'_, ProcessManager>,
) -> Vec<ProcessInfo> {
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::<ProcessManager>();
let changes = state.check_and_cleanup();
for change in changes {
let _ = app.emit(crate::constants::events::PROCESS_STATUS_CHANGED, &change);
}
}
});
}