diff --git a/package.json b/package.json index 74c37cc8..c321ee4d 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2.0.4-alpha", "license": "GPL-3.0-only", "scripts": { - "dev": "cross-env RUST_BACKTRACE=1 tauri dev", + "dev": "cross-env RUST_BACKTRACE=1 tauri dev --features verge-dev", "dev:diff": "cross-env RUST_BACKTRACE=1 tauri dev -f verge-dev", "build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' tauri build", "tauri": "tauri", diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index 4b0d4162..f1ed12f6 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -10,10 +10,14 @@ use std::{sync::Arc, time::Duration}; use tauri_plugin_shell::ShellExt; use tokio::sync::Mutex; use tokio::time::sleep; +use super::process_lock::ProcessLock; +use super::health_check::HealthChecker; #[derive(Debug)] pub struct CoreManager { running: Arc>, + process_lock: Arc>>, + health_checker: HealthChecker, } impl CoreManager { @@ -21,11 +25,19 @@ impl CoreManager { static CORE_MANAGER: OnceCell = OnceCell::new(); CORE_MANAGER.get_or_init(|| CoreManager { running: Arc::new(Mutex::new(false)), + process_lock: Arc::new(Mutex::new(None)), + health_checker: HealthChecker::new(), }) } pub async fn init(&self) -> Result<()> { log::trace!("run core start"); + + // 初始化进程锁 + let process_lock = ProcessLock::new()?; + process_lock.acquire()?; + *self.process_lock.lock().await = Some(process_lock); + // 启动clash log_err!(Self::global().start_core().await); log::trace!("run core end"); @@ -56,37 +68,38 @@ impl CoreManager { /// 停止核心运行 pub async fn stop_core(&self) -> Result<()> { - let mut running = self.running.lock().await; - - if !*running { - log::debug!("core is not running"); - return Ok(()); - } + log::info!(target: "app", "Stopping core"); // 关闭tun模式 let mut disable = Mapping::new(); let mut tun = Mapping::new(); tun.insert("enable".into(), false.into()); disable.insert("tun".into(), tun.into()); - log::debug!(target: "app", "disable tun mode"); - log_err!(clash_api::patch_configs(&disable).await); + log::debug!(target: "app", "Disabling TUN mode"); + let _ = clash_api::patch_configs(&disable).await; - // 服务模式 - if service::check_service().await.is_ok() { - log::info!(target: "app", "stop the core by service"); - service::stop_core_by_service().await?; + // 直接尝试停止服务,不预先检查状态 + log::info!(target: "app", "Attempting to stop service"); + let _ = service::stop_core_by_service().await; + + // 设置运行状态 + *self.running.lock().await = false; + + // 释放进程锁 + let mut process_lock = self.process_lock.lock().await; + if let Some(lock) = process_lock.take() { + log::info!(target: "app", "Releasing process lock"); + let _ = lock.release(); } - *running = false; + + log::info!(target: "app", "Core stopped successfully"); Ok(()) } /// 启动核心 pub async fn start_core(&self) -> Result<()> { - let mut running = self.running.lock().await; - if *running { - log::info!("core is running"); - return Ok(()); - } + // 检查端口占用 + self.health_checker.check_ports().await?; let config_path = Config::generate_file(ConfigType::Run)?; @@ -95,12 +108,23 @@ impl CoreManager { log::info!(target: "app", "try to run core in service mode"); service::run_core_by_service(&config_path).await?; } + + // 启动健康检查 + let checker = Arc::new(self.health_checker.clone()); + tokio::spawn(async move { + loop { + sleep(Duration::from_secs(30)).await; + if let Err(e) = checker.check_service_health().await { + log::error!(target: "app", "Health check failed: {}", e); + } + } + }); + // 流量订阅 #[cfg(target_os = "macos")] log_err!(Tray::global().subscribe_traffic().await); - *running = true; - + *self.running.lock().await = true; Ok(()) } diff --git a/src-tauri/src/core/health_check.rs b/src-tauri/src/core/health_check.rs new file mode 100644 index 00000000..9eac439e --- /dev/null +++ b/src-tauri/src/core/health_check.rs @@ -0,0 +1,53 @@ +use anyhow::{bail, Result}; +use sysinfo::{Pid, System}; +use crate::config::Config; +use crate::core::service; +use port_scanner::local_port_available; + +#[derive(Debug, Clone)] +pub struct HealthChecker; + +impl HealthChecker { + pub fn new() -> Self { + Self + } + + pub async fn check_ports(&self) -> Result<()> { + let verge = Config::verge(); + let verge_config = verge.latest(); + let mixed_port = verge_config.verge_mixed_port.unwrap_or(7897); + let socks_port = verge_config.verge_socks_port.unwrap_or(7890); + let http_port = verge_config.verge_port.unwrap_or(7891); + + if !local_port_available(mixed_port) { + bail!("Mixed port {} is already in use", mixed_port); + } + + if verge_config.verge_socks_enabled.unwrap_or(true) && !local_port_available(socks_port) { + bail!("Socks port {} is already in use", socks_port); + } + + if verge_config.verge_http_enabled.unwrap_or(true) && !local_port_available(http_port) { + bail!("Http port {} is already in use", http_port); + } + + Ok(()) + } + + pub async fn check_service_health(&self) -> Result<()> { + if let Ok(response) = service::check_service().await { + if let Some(body) = response.data { + let sys = System::new_all(); + if let Ok(pid) = body.bin_path.parse::() { + if let Some(process) = sys.process(Pid::from(pid as usize)) { + if !process.name().to_string_lossy().contains("mihomo") { + log::warn!(target: "app", "Found non-mihomo process using service port"); + return Ok(()); + } + } + } + } + } + Ok(()) + } +} \ No newline at end of file diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 098052b9..7f00e3a7 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -9,5 +9,6 @@ pub mod sysopt; pub mod timer; pub mod tray; pub mod win_uwp; - +pub mod process_lock; +pub mod health_check; pub use self::core::*; diff --git a/src-tauri/src/core/process_lock.rs b/src-tauri/src/core/process_lock.rs new file mode 100644 index 00000000..8d703867 --- /dev/null +++ b/src-tauri/src/core/process_lock.rs @@ -0,0 +1,191 @@ +use anyhow::{bail, Result}; +use std::fs; +use std::path::PathBuf; +use sysinfo::{Pid, System, Signal}; +use crate::utils::dirs; +use std::time::Duration; + +const TERM_WAIT: Duration = Duration::from_millis(500); +const KILL_WAIT: Duration = Duration::from_millis(500); +const FINAL_WAIT: Duration = Duration::from_millis(1000); + +#[derive(Debug)] +pub struct ProcessLock { + pid_file: PathBuf, +} + +impl ProcessLock { + pub fn new() -> Result { + let pid_file = dirs::app_home_dir()?.join("mihomo.pid"); + println!("Creating ProcessLock with PID file: {:?}", pid_file); + log::info!(target: "app", "Creating ProcessLock with PID file: {:?}", pid_file); + Ok(Self { pid_file }) + } + + fn is_target_process(process_name: &str) -> bool { + (process_name.contains("mihomo") || process_name.contains("clash")) + && !process_name.contains("clash-verge") + && !process_name.contains("clash.meta") + } + + fn kill_process(pid: Pid, process: &sysinfo::Process) -> bool { + let process_name = process.name().to_string_lossy().to_lowercase(); + let process_pid = pid.as_u32(); + + println!("Terminating clash core process (PID: {}, Name: {})", process_pid, process_name); + log::info!(target: "app", "Terminating clash core process (PID: {}, Name: {})", process_pid, process_name); + + // 首先尝试正常终止 + #[cfg(not(target_os = "windows"))] + { + println!("Sending SIGTERM to process {}", process_pid); + log::info!(target: "app", "Sending SIGTERM to process {}", process_pid); + let _ = process.kill_with(Signal::Term); + } + #[cfg(target_os = "windows")] + { + println!("Killing process {}", process_pid); + log::info!(target: "app", "Killing process {}", process_pid); + process.kill(); + } + + std::thread::sleep(TERM_WAIT); + + // 检查进程是否还在运行 + let mut new_sys = System::new(); + new_sys.refresh_all(); + if let Some(p) = new_sys.process(pid) { + println!("Process {} still running, trying force kill", process_pid); + log::info!(target: "app", "Process {} still running, trying force kill", process_pid); + + #[cfg(not(target_os = "windows"))] + { + println!("Sending SIGKILL to process {}", process_pid); + log::info!(target: "app", "Sending SIGKILL to process {}", process_pid); + let _ = p.kill_with(Signal::Kill); + } + #[cfg(target_os = "windows")] + { + println!("Force killing process {}", process_pid); + log::info!(target: "app", "Force killing process {}", process_pid); + p.kill(); + } + + std::thread::sleep(KILL_WAIT); + + // 再次检查进程是否存在 + new_sys.refresh_all(); + if new_sys.process(pid).is_some() { + println!("Failed to terminate process {}", process_pid); + log::error!(target: "app", "Failed to terminate process {}", process_pid); + return false; + } + } + + println!("Process {} has been terminated", process_pid); + log::info!(target: "app", "Process {} has been terminated", process_pid); + true + } + + fn kill_other_processes(include_current: bool) -> Result<()> { + println!("Starting process cleanup (include_current: {})", include_current); + log::info!(target: "app", "Starting process cleanup (include_current: {})", include_current); + + let mut sys = System::new(); + sys.refresh_all(); + + let current_pid = std::process::id(); + println!("Current process ID: {}", current_pid); + log::info!(target: "app", "Current process ID: {}", current_pid); + + let mut killed = false; + let mut failed_kills = Vec::new(); + + for (pid, process) in sys.processes() { + let process_name = process.name().to_string_lossy().to_lowercase(); + + if Self::is_target_process(&process_name) { + let process_pid = pid.as_u32(); + if include_current || process_pid != current_pid { + if !Self::kill_process(*pid, process) { + failed_kills.push((process_pid, process_name.clone())); + } + killed = true; + } + } + } + + if killed { + std::thread::sleep(FINAL_WAIT); + + // 最终检查 + let mut final_sys = System::new(); + final_sys.refresh_all(); + + let remaining: Vec<_> = final_sys.processes() + .iter() + .filter(|(_, process)| { + let name = process.name().to_string_lossy().to_lowercase(); + Self::is_target_process(&name) + }) + .map(|(pid, process)| { + (pid.as_u32(), process.name().to_string_lossy().to_string()) + }) + .collect(); + + if !remaining.is_empty() { + log::error!(target: "app", "Failed to terminate some processes: {:?}", remaining); + bail!("Failed to terminate processes: {:?}", remaining); + } + } + + println!("Process cleanup completed"); + log::info!(target: "app", "Process cleanup completed"); + Ok(()) + } + + pub fn acquire(&self) -> Result<()> { + println!("Attempting to acquire process lock"); + log::info!(target: "app", "Attempting to acquire process lock"); + + // 首先尝试终止其他进程 + Self::kill_other_processes(false)?; + + if self.pid_file.exists() { + fs::remove_file(&self.pid_file)?; + } + + fs::write(&self.pid_file, std::process::id().to_string())?; + println!("Process lock acquired successfully"); + log::info!(target: "app", "Process lock acquired successfully"); + Ok(()) + } + + pub fn release(&self) -> Result<()> { + println!("Starting release process"); + log::info!(target: "app", "Starting release process"); + + Self::kill_other_processes(true)?; + + if self.pid_file.exists() { + println!("Removing PID file"); + log::info!(target: "app", "Removing PID file"); + fs::remove_file(&self.pid_file)?; + } + + println!("Release process completed"); + log::info!(target: "app", "Release process completed"); + Ok(()) + } +} + +impl Drop for ProcessLock { + fn drop(&mut self) { + // 只在 PID 文件还存在时执行释放 + if self.pid_file.exists() { + println!("ProcessLock being dropped"); + log::info!(target: "app", "ProcessLock being dropped"); + let _ = self.release(); + } + } +} \ No newline at end of file diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index d4d5a93a..50b91fab 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -161,20 +161,49 @@ pub async fn reinstall_service() -> Result<()> { /// check the windows service status pub async fn check_service() -> Result { + log::info!(target: "app", "Checking service status"); + println!("Checking service status"); + let url = format!("{SERVICE_URL}/get_clash"); - let response = reqwest::ClientBuilder::new() + log::debug!(target: "app", "Sending request to {}", url); + println!("Sending request to {}", url); + + let client = reqwest::ClientBuilder::new() .no_proxy() .timeout(Duration::from_secs(3)) - .build()? - .get(url) - .send() - .await - .context("failed to connect to the Clash Verge Service")? - .json::() - .await - .context("failed to parse the Clash Verge Service response")?; - - Ok(response) + .build()?; + + // 重试3次 + for i in 0..3 { + match client.get(&url).send().await { + Ok(resp) => { + match resp.json::().await { + Ok(json) => { + log::info!(target: "app", "Service check response: {:?}", json); + println!("Service check response: {:?}", json); + return Ok(json); + } + Err(e) => { + log::error!(target: "app", "Failed to parse service response (attempt {}): {}", i + 1, e); + println!("Failed to parse service response (attempt {}): {}", i + 1, e); + if i == 2 { + return Err(e.into()); + } + } + } + } + Err(e) => { + log::error!(target: "app", "Failed to connect to service (attempt {}): {}", i + 1, e); + println!("Failed to connect to service (attempt {}): {}", i + 1, e); + if i == 2 { + return Err(e.into()); + } + } + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + + bail!("Failed to check service after 3 attempts") } /// start the clash by service @@ -219,14 +248,32 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> { /// stop the clash by service pub(super) async fn stop_core_by_service() -> Result<()> { + log::info!(target: "app", "Attempting to stop core through service"); + let url = format!("{SERVICE_URL}/stop_clash"); - let _ = reqwest::ClientBuilder::new() + let client = reqwest::ClientBuilder::new() .no_proxy() - .build()? - .post(url) - .send() - .await - .context("failed to connect to the Clash Verge Service")?; - - Ok(()) + .timeout(Duration::from_secs(3)) + .build()?; + + // 重试2次 + for i in 0..2 { + match client.post(&url).send().await { + Ok(_) => { + log::info!(target: "app", "Successfully sent stop request to service"); + // 等待服务停止 + tokio::time::sleep(Duration::from_millis(500)).await; + return Ok(()); + } + Err(e) => { + log::error!(target: "app", "Failed to send stop request (attempt {}): {}", i + 1, e); + if i == 1 { + return Err(e.into()); + } + } + } + tokio::time::sleep(Duration::from_millis(500)).await; + } + + bail!("Failed to stop service after 2 attempts") } diff --git a/src-tauri/src/feat.rs b/src-tauri/src/feat.rs index 7fee881b..b9a2e784 100644 --- a/src-tauri/src/feat.rs +++ b/src-tauri/src/feat.rs @@ -135,10 +135,36 @@ pub fn toggle_tun_mode() { } pub fn quit(code: Option) { + println!("Starting application quit process"); + log::info!(target: "app", "Starting application quit process"); + let app_handle = handle::Handle::global().app_handle().unwrap(); handle::Handle::global().set_is_exiting(); - resolve::resolve_reset(); - log_err!(handle::Handle::global().get_window().unwrap().close()); + + // 立即关闭窗口,让用户感知到退出开始 + if let Some(window) = handle::Handle::global().get_window() { + let _ = window.close(); + } + + // 后台执行所有清理工作 + let app_handle_clone = app_handle.clone(); + tauri::async_runtime::spawn(async move { + // 1. 发送停止内核指令 + let _ = CoreManager::global().stop_core().await; + + // 2. 重置系统代理 + resolve::resolve_reset(); + + // 3. 保存窗口状态 + let _ = app_handle_clone.save_window_state(StateFlags::default()); + + println!("Cleanup tasks completed in background"); + log::info!(target: "app", "Cleanup tasks completed in background"); + }); + + // 主线程立即退出 + println!("Exiting application with code: {:?}", code); + log::info!(target: "app", "Exiting application with code: {:?}", code); app_handle.exit(code.unwrap_or(0)); } diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index b46d1d0c..4d2f7690 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -15,47 +15,67 @@ use tauri_plugin_shell::ShellExt; /// initialize this instance's log file fn init_log() -> Result<()> { + println!("Starting log initialization..."); + let log_dir = dirs::app_logs_dir()?; if !log_dir.exists() { + println!("Creating log directory: {:?}", log_dir); let _ = fs::create_dir_all(&log_dir); } let log_level = Config::verge().data().get_log_level(); + println!("Current log level: {:?}", log_level); + if log_level == LevelFilter::Off { + println!("Logging is disabled"); return Ok(()); } - let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string(); - let log_file = format!("{}.log", local_time); - let log_file = log_dir.join(log_file); - let log_pattern = match log_level { LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}", _ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}", }; let encode = Box::new(PatternEncoder::new(log_pattern)); - let stdout = ConsoleAppender::builder().encoder(encode.clone()).build(); + + let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string(); + let log_file = format!("{}.log", local_time); + let log_file = log_dir.join(log_file); + println!("Log file path: {:?}", log_file); + let tofile = FileAppender::builder().encoder(encode).build(log_file)?; let mut logger_builder = Logger::builder(); let mut root_builder = Root::builder(); let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug; - - logger_builder = logger_builder.appenders(["file"]); + logger_builder = logger_builder.appenders(["stdout", "file"]); + root_builder = root_builder.appender("stdout"); if log_more { - root_builder = root_builder.appenders(["file"]); + root_builder = root_builder.appender("file"); } - let (config, _) = log4rs::config::Config::builder() + println!("Building log config..."); + let config = log4rs::config::Config::builder() .appender(Appender::builder().build("stdout", Box::new(stdout))) .appender(Appender::builder().build("file", Box::new(tofile))) .logger(logger_builder.additive(false).build("app", log_level)) - .build_lossy(root_builder.build(log_level)); + .build(root_builder.build(log_level)) + .map_err(|e| anyhow::anyhow!("Failed to build log config: {}", e))?; - log4rs::init_config(config)?; + println!("Initializing log config..."); + match log4rs::init_config(config) { + Ok(_) => println!("Log system initialized successfully"), + Err(e) => println!("Failed to initialize log system: {}", e), + } + + // 测试日志系统 + log::error!(target: "app", "Test error log message"); + log::warn!(target: "app", "Test warning log message"); + log::info!(target: "app", "Test info log message"); + log::debug!(target: "app", "Test debug log message"); + log::trace!(target: "app", "Test trace log message"); Ok(()) }