535 lines
18 KiB
Rust
535 lines
18 KiB
Rust
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(¶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<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);
|
||
}
|
||
}
|
||
});
|
||
}
|