//! 代理模块 Tauri 命令层。 use tauri::{AppHandle, State}; use super::system_proxy::{clear_system_proxy_windows, get_system_proxy_windows, set_system_proxy_windows}; use super::{ KernelInfo, KernelUpdateInfo, MihomoManager, ProfileMeta, ProxySettings, ProxyStatus, TrafficSnapshot, }; use crate::process_manager::{ProcessInfo, ProcessManager, ProcessStatus}; /// 判断 mihomo 进程是否处于运行状态 fn mihomo_running(pm: &ProcessManager) -> bool { matches!( pm.get_status("proxy").map(|p| p.status), Some(ProcessStatus::Running) ) } #[tauri::command] #[specta::specta] pub fn proxy_get_settings(state: State<'_, MihomoManager>) -> ProxySettings { state.load_settings() } #[tauri::command] #[specta::specta] pub fn proxy_save_settings( state: State<'_, MihomoManager>, settings: ProxySettings, ) -> Result<(), String> { state.save_settings(&settings) } #[tauri::command] #[specta::specta] pub fn proxy_kernel_info( state: State<'_, MihomoManager>, app: AppHandle, ) -> Result { state.prepare_kernel(&app) } #[tauri::command] #[specta::specta] pub async fn proxy_check_kernel_update( state: State<'_, MihomoManager>, ) -> Result { state.check_kernel_update().await } /// 取消内核下载/安装(设置取消标志,下载循环轮询后中止) #[tauri::command] #[specta::specta] pub fn proxy_cancel_kernel_install(state: State<'_, MihomoManager>) -> Result<(), String> { state.cancel_kernel_install(); Ok(()) } /// 前端确认 mihomo 已停止,唤醒等待中的安装流程继续解压替换。 /// (下载阶段允许 mihomo 运行以便走系统代理,解压替换前必须停止 mihomo,否则 exe 被占用) #[tauri::command] #[specta::specta] pub fn proxy_confirm_install(state: State<'_, MihomoManager>) -> Result<(), String> { state.confirm_install(); Ok(()) } /// 应用内核更新:下载阶段已由下载模块完成,本方法仅做 need_stop → 解压 → 替换。 #[tauri::command] #[specta::specta] pub async fn proxy_apply_kernel_update( state: State<'_, MihomoManager>, app: AppHandle, zip_path: String, ) -> Result { let path = std::path::PathBuf::from(zip_path); state.apply_kernel_update(&app, path).await } #[tauri::command] #[specta::specta] pub fn proxy_status(pm: State<'_, ProcessManager>) -> ProxyStatus { match pm.get_status("proxy") { Some(p) => ProxyStatus { running: matches!(p.status, crate::process_manager::ProcessStatus::Running), pid: p.pid, restart_count: p.restart_count, }, None => ProxyStatus { running: false, pid: None, restart_count: 0, }, } } #[tauri::command] #[specta::specta] pub fn proxy_start( state: State<'_, MihomoManager>, pm: State<'_, ProcessManager>, app: AppHandle, ) -> Result { let params = state.prepare_for_start(&app)?; let info = pm.start(params)?; // 手动启动也遵循「启动时自动开启系统代理」设置 state.apply_auto_system_proxy(); Ok(info) } #[tauri::command] #[specta::specta] pub fn proxy_stop(state: State<'_, MihomoManager>, pm: State<'_, ProcessManager>) -> Result<(), String> { // 关闭 mihomo 时同步关闭系统代理(若开启),避免系统代理指向已停止的端口导致断网 if get_system_proxy_windows() { let _ = state.disable_system_proxy(); } pm.stop("proxy") } #[tauri::command] #[specta::specta] pub async fn proxy_restart( state: State<'_, MihomoManager>, pm: State<'_, ProcessManager>, app: AppHandle, ) -> Result { let _ = pm.stop("proxy"); // 等待 TCP 端口释放(Windows 上 kill 后端口释放有延迟),在阻塞线程池中 sleep 避免阻塞主线程 tauri::async_runtime::spawn_blocking(|| { std::thread::sleep(std::time::Duration::from_millis(800)); }) .await .map_err(|e| format!("sleep 失败: {}", e))?; // 启动并等待 API 就绪,带有限重试(共 3 次): // - 冷加载大订阅(首次解析 + geo 下载)可能远超过前端 waitForApi 的 10s 预算, // 这里在命令内等满就绪,避免重启成功后仍被前端误判为「重启失败」导致节点不刷新。 // - 偶发的端口未及时释放 / 启动瞬间退出,通过重试自愈。 let mut last_err = "mihomo 启动失败".to_string(); for _ in 0..3 { match start_and_wait(state.inner(), pm.inner(), &app).await { Ok(info) => return Ok(info), Err(e) => { last_err = e; // 上个实例刚退出,多等一会儿释放端口再重试 tauri::async_runtime::spawn_blocking(|| { std::thread::sleep(std::time::Duration::from_millis(1200)); }) .await .map_err(|x| format!("sleep 失败: {}", x))?; } } } // 所有启动尝试均失败:mihomo 已停止,同步关闭系统代理(若开启),避免代理指向已停止端口导致断网 if get_system_proxy_windows() { let _ = state.disable_system_proxy(); } Err(last_err) } /// 启动 mihomo 并等待其 HTTP API 就绪(最多 20s)。 /// - 若启动后进程立即退出,快速返回(不白等满预算),便于外层尽早重试。 /// - 若 pm.start 返回「已在运行中」,说明崩溃监控已抢先拉起进程,同样等待其 API 就绪即可。 async fn start_and_wait( state: &MihomoManager, pm: &ProcessManager, app: &AppHandle, ) -> Result { let params = state.prepare_for_start(app)?; if let Err(e) = pm.start(params) { // "已在运行中" = 崩溃监控已拉起,不算失败;其余为上抛 if !e.contains("运行中") { return Err(e); } } let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(20); loop { // 进程已退出且 API 未就绪 → 启动失败(快速失败,交外层重试) if let Some(st) = pm.get_status("proxy") { if !matches!(st.status, ProcessStatus::Running) { return Err("mihomo 启动后立即退出".to_string()); } } if state.get_version().await.is_ok() { return pm.get_status("proxy").ok_or_else(|| "mihomo 进程不存在".to_string()); } if tokio::time::Instant::now() >= deadline { return Err("mihomo 启动超时,API 无响应".to_string()); } tokio::time::sleep(std::time::Duration::from_millis(500)).await; } } #[tauri::command] pub async fn proxy_version(state: State<'_, MihomoManager>) -> Result { state.get_version().await } #[tauri::command] pub async fn proxy_get_proxies(state: State<'_, MihomoManager>) -> Result { state.get_proxies().await } #[tauri::command] #[specta::specta] pub async fn proxy_select_proxy( state: State<'_, MihomoManager>, group: String, name: String, ) -> Result<(), String> { state.select_proxy(&group, &name).await } #[tauri::command] #[specta::specta] pub async fn proxy_test_delay( state: State<'_, MihomoManager>, name: String, url: Option, timeout: Option, ) -> Result { state .test_delay( &name, url.as_deref().unwrap_or("https://www.gstatic.com/generate_204"), timeout.unwrap_or(5000), ) .await } #[tauri::command] pub async fn proxy_get_connections( state: State<'_, MihomoManager>, ) -> Result { state.get_connections().await } #[tauri::command] #[specta::specta] pub async fn proxy_traffic( state: State<'_, MihomoManager>, ) -> Result { state.traffic_snapshot().await } #[tauri::command] #[specta::specta] pub async fn proxy_close_connection( state: State<'_, MihomoManager>, id: String, ) -> Result<(), String> { state.close_connection(&id).await } #[tauri::command] pub async fn proxy_patch_configs( state: State<'_, MihomoManager>, body: serde_json::Value, ) -> Result<(), String> { state.patch_configs(body).await } // ---------- 订阅 ---------- #[tauri::command] #[specta::specta] pub async fn proxy_import_profile( state: State<'_, MihomoManager>, url: String, name: String, ) -> Result { state.import_profile(&url, &name).await } #[tauri::command] #[specta::specta] pub async fn proxy_update_profile( state: State<'_, MihomoManager>, id: String, ) -> Result { state.update_profile(&id).await } #[tauri::command] #[specta::specta] pub fn proxy_delete_profile( state: State<'_, MihomoManager>, id: String, ) -> Result<(), String> { state.delete_profile(&id) } #[tauri::command] #[specta::specta] pub fn proxy_activate_profile( state: State<'_, MihomoManager>, id: String, ) -> Result<(), String> { state.activate_profile(&id) } // ---------- 系统代理 ---------- #[tauri::command] #[specta::specta] pub fn proxy_set_system_proxy( state: State<'_, MihomoManager>, pm: State<'_, ProcessManager>, ) -> Result<(), String> { // 停机时禁止开启系统代理:否则系统代理指向已停止的端口,会导致所有网络请求失败 if !mihomo_running(&pm) { return Err("mihomo 未运行,无法开启系统代理".into()); } let settings = state.load_settings(); let addr = format!("127.0.0.1:{}", settings.mixed_port); set_system_proxy_windows(&addr)?; let mut settings = settings; settings.system_proxy = true; state.save_settings(&settings) } #[tauri::command] #[specta::specta] pub fn proxy_clear_system_proxy( state: State<'_, MihomoManager>, ) -> Result<(), String> { clear_system_proxy_windows()?; let mut settings = state.load_settings(); settings.system_proxy = false; state.save_settings(&settings) } #[tauri::command] #[specta::specta] pub fn proxy_get_system_proxy() -> bool { get_system_proxy_windows() }