浏览器下载插件
This commit is contained in:
@@ -758,9 +758,10 @@ impl Aria2Manager {
|
||||
Err(_) => return,
|
||||
};
|
||||
let _ = rt.block_on(async {
|
||||
// 给一个短超时,避免退出卡住
|
||||
// 给 2 秒超时,让 aria2 有足够时间保存 session
|
||||
// (aria2 保存 session 可能涉及磁盘 I/O,500ms 太短)
|
||||
let _ = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(500),
|
||||
std::time::Duration::from_millis(2000),
|
||||
self.rpc_call("aria2.shutdown", vec![]),
|
||||
)
|
||||
.await;
|
||||
|
||||
+24
-5
@@ -38,14 +38,16 @@ fn quit_app(
|
||||
state: tauri::State<'_, ProcessManager>,
|
||||
mihomo: tauri::State<'_, MihomoManager>,
|
||||
aria2: tauri::State<'_, Aria2Manager>,
|
||||
app: tauri::AppHandle,
|
||||
) {
|
||||
// 退出前清理系统代理,避免遗留导致网络问题
|
||||
mihomo.cleanup_on_exit();
|
||||
// 退出前让 aria2 优雅关闭(保存 session)
|
||||
// 退出前让 aria2 优雅关闭(保存 session,2s 超时)
|
||||
aria2.cleanup_on_exit();
|
||||
// 停止所有子进程
|
||||
// 停止所有子进程(同步 kill + 带超时的 wait,确保进程真正终止)
|
||||
state.stop_all();
|
||||
std::process::exit(0);
|
||||
// 通过 app.exit 触发 RunEvent::ExitRequested,统一退出路径
|
||||
app.exit(0);
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
@@ -154,6 +156,7 @@ pub fn run() {
|
||||
}
|
||||
"quit" => {
|
||||
// 退出前清理系统代理 + 优雅关闭 aria2 + 停止所有子进程
|
||||
// 直接调用 cleanup + stop_all + exit(quit_app 命令是给前端用的)
|
||||
if let Some(mihomo) = app.try_state::<MihomoManager>() {
|
||||
mihomo.cleanup_on_exit();
|
||||
}
|
||||
@@ -207,6 +210,22 @@ pub fn run() {
|
||||
api.prevent_close();
|
||||
}
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app, event| {
|
||||
// 退出请求兜底:捕获所有退出路径(app.exit、窗口全部关闭、系统信号等)
|
||||
// 确保 mihomo/aria2 子进程在任何情况下都被清理
|
||||
// 注:quit_app 命令和托盘菜单已主动调用 cleanup,这里作为二次保险
|
||||
if let tauri::RunEvent::ExitRequested { .. } = event {
|
||||
if let Some(mihomo) = app.try_state::<MihomoManager>() {
|
||||
mihomo.cleanup_on_exit();
|
||||
}
|
||||
if let Some(aria2) = app.try_state::<Aria2Manager>() {
|
||||
aria2.cleanup_on_exit();
|
||||
}
|
||||
if let Some(pm) = app.try_state::<ProcessManager>() {
|
||||
pm.stop_all();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,101 @@ use tauri::{AppHandle, Emitter, Manager};
|
||||
#[cfg(windows)]
|
||||
pub const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// 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() {
|
||||
eprintln!("[ProcessManager] 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 {
|
||||
eprintln!("[ProcessManager] SetInformationJobObject 失败");
|
||||
return 0;
|
||||
}
|
||||
h as usize
|
||||
}
|
||||
});
|
||||
if addr == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(addr as winapi::HANDLE)
|
||||
}
|
||||
}
|
||||
|
||||
/// 为 Command 设置平台特定的创建标志(Windows 上隐藏控制台窗口)
|
||||
/// 公开以便其他模块(如 mihomo_manager 调用 mihomo -v 查询版本)复用
|
||||
#[cfg(windows)]
|
||||
@@ -24,6 +119,22 @@ pub fn setup_creation_flags(_cmd: &mut Command) {
|
||||
// 非 Windows 平台无需处理
|
||||
}
|
||||
|
||||
/// 将已启动的子进程加入 Job Object(异常退出时自动清理)
|
||||
/// 在 Windows 上调用,非 Windows 平台为空操作
|
||||
#[cfg(windows)]
|
||||
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)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -119,6 +230,8 @@ impl ProcessManager {
|
||||
.spawn()
|
||||
.map_err(|e| format!("启动进程 '{}' 失败: {}", params.id, e))?;
|
||||
let pid = child.id();
|
||||
// 将子进程加入 Job Object,主进程异常退出时由 OS 自动清理
|
||||
assign_to_job(&child);
|
||||
|
||||
let entry = ProcessEntry {
|
||||
child,
|
||||
@@ -165,11 +278,27 @@ impl ProcessManager {
|
||||
}
|
||||
|
||||
/// 停止所有进程(应用退出时调用)
|
||||
/// 同步 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();
|
||||
let _ = entry.child.wait();
|
||||
// 带超时的 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 {
|
||||
println!("[ProcessManager] 进程 {} 等待退出超时(3s),放弃等待", id);
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
println!("[ProcessManager] 已停止进程: {}", id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user