diff --git a/backend/Cargo.lock b/backend/Cargo.lock index b314c4c1e4..aec57bff53 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -113,9 +113,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -128,33 +128,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.9.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" dependencies = [ "memchr", "serde", @@ -925,9 +925,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "clash-nyanpasu" @@ -1085,9 +1085,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "colored" @@ -1623,7 +1623,7 @@ dependencies = [ [[package]] name = "dirs-utils" version = "0.1.0" -source = "git+https://github.com/LibNyanpasu/nyanpasu-utils.git#31f59134a7e94922f13b07b21047390b962ce3c4" +source = "git+https://github.com/LibNyanpasu/nyanpasu-utils.git#a4d554586049a548528ed88c3f33a44450610f51" dependencies = [ "dirs-next", "thiserror", @@ -1736,7 +1736,7 @@ dependencies = [ "cc", "memchr", "rustc_version 0.4.0", - "toml 0.8.15", + "toml 0.8.16", "vswhom", "winreg 0.52.0", ] @@ -1785,18 +1785,18 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6dc8c8ff84895b051f07a0e65f975cf225131742531338752abfb324e4449ff" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", ] [[package]] name = "env_logger" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06676b12debf7bba6903559720abca942d3a66b8acb88815fd2c7c6537e9ade1" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ "anstream", "anstyle", @@ -3132,9 +3132,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "isolang" @@ -3977,8 +3977,8 @@ dependencies = [ [[package]] name = "nyanpasu-ipc" -version = "1.0.2" -source = "git+https://github.com/LibNyanpasu/nyanpasu-service.git#67a46e899899147a695fe51d1fbf0111347195fa" +version = "1.0.3" +source = "git+https://github.com/LibNyanpasu/nyanpasu-service.git#10b455d0ee1c366f1ed3fcaa26b37764ab00c69b" dependencies = [ "anyhow", "derive_builder", @@ -4002,7 +4002,7 @@ dependencies = [ [[package]] name = "nyanpasu-utils" version = "0.1.0" -source = "git+https://github.com/LibNyanpasu/nyanpasu-utils.git#31f59134a7e94922f13b07b21047390b962ce3c4" +source = "git+https://github.com/LibNyanpasu/nyanpasu-utils.git#a4d554586049a548528ed88c3f33a44450610f51" dependencies = [ "constcat", "derive_builder", @@ -4284,7 +4284,7 @@ dependencies = [ [[package]] name = "os-utils" version = "0.1.0" -source = "git+https://github.com/LibNyanpasu/nyanpasu-utils.git#31f59134a7e94922f13b07b21047390b962ce3c4" +source = "git+https://github.com/LibNyanpasu/nyanpasu-utils.git#a4d554586049a548528ed88c3f33a44450610f51" dependencies = [ "nix 0.29.0", "shared_child", @@ -5619,9 +5619,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -6151,7 +6151,7 @@ dependencies = [ "cfg-expr 0.15.8", "heck 0.5.0", "pkg-config", - "toml 0.8.15", + "toml 0.8.16", "version-compare 0.2.0", ] @@ -6766,21 +6766,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" +checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.16", + "toml_edit 0.22.17", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" dependencies = [ "serde", ] @@ -6811,15 +6811,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.16" +version = "0.22.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" +checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.15", + "winnow 0.6.16", ] [[package]] @@ -7132,9 +7132,9 @@ checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vswhom" @@ -8125,9 +8125,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.15" +version = "0.6.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" +checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" dependencies = [ "memchr", ] @@ -8460,9 +8460,9 @@ dependencies = [ [[package]] name = "zip-extensions" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341" +checksum = "386508a00aae1d8218b9252a41f59bba739ccee3f8e420bb90bcb1c30d960d4a" dependencies = [ "zip 2.1.5", ] diff --git a/backend/tauri/src/config/nyanpasu/mod.rs b/backend/tauri/src/config/nyanpasu/mod.rs index 61d0472c48..3ac691cb21 100644 --- a/backend/tauri/src/config/nyanpasu/mod.rs +++ b/backend/tauri/src/config/nyanpasu/mod.rs @@ -9,6 +9,7 @@ pub mod logging; pub use self::clash_strategy::{ClashStrategy, ExternalControllerPortStrategy}; pub use logging::LoggingLevel; +// TODO: when support sing-box, remove this struct #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] pub enum ClashCore { #[serde(rename = "clash", alias = "clash-premium")] @@ -52,6 +53,41 @@ impl std::fmt::Display for ClashCore { } } +impl From<&ClashCore> for nyanpasu_utils::core::CoreType { + fn from(core: &ClashCore) -> Self { + match core { + ClashCore::ClashPremium => nyanpasu_utils::core::CoreType::Clash( + nyanpasu_utils::core::ClashCoreType::ClashPremium, + ), + ClashCore::ClashRs => nyanpasu_utils::core::CoreType::Clash( + nyanpasu_utils::core::ClashCoreType::ClashRust, + ), + ClashCore::Mihomo => { + nyanpasu_utils::core::CoreType::Clash(nyanpasu_utils::core::ClashCoreType::Mihomo) + } + ClashCore::MihomoAlpha => nyanpasu_utils::core::CoreType::Clash( + nyanpasu_utils::core::ClashCoreType::MihomoAlpha, + ), + } + } +} + +impl TryFrom<&nyanpasu_utils::core::CoreType> for ClashCore { + type Error = anyhow::Error; + + fn try_from(core: &nyanpasu_utils::core::CoreType) -> Result { + match core { + nyanpasu_utils::core::CoreType::Clash(clash) => match clash { + nyanpasu_utils::core::ClashCoreType::ClashPremium => Ok(ClashCore::ClashPremium), + nyanpasu_utils::core::ClashCoreType::ClashRust => Ok(ClashCore::ClashRs), + nyanpasu_utils::core::ClashCoreType::Mihomo => Ok(ClashCore::Mihomo), + nyanpasu_utils::core::ClashCoreType::MihomoAlpha => Ok(ClashCore::MihomoAlpha), + }, + _ => Err(anyhow::anyhow!("unsupported core type")), + } + } +} + /// ### `verge.yaml` schema #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct IVerge { diff --git a/backend/tauri/src/core/clash/core.rs b/backend/tauri/src/core/clash/core.rs index 1acaa3823a..3c9789f80e 100644 --- a/backend/tauri/src/core/clash/core.rs +++ b/backend/tauri/src/core/clash/core.rs @@ -6,11 +6,29 @@ use crate::{ utils::dirs, }; use anyhow::{bail, Context, Result}; +use nyanpasu_ipc::{api::status::CoreState, utils::get_current_ts}; +use nyanpasu_utils::{ + core::{ + instance::{CoreInstance, CoreInstanceBuilder}, + CommandEvent, + }, + runtime::{block_on, spawn}, +}; use once_cell::sync::OnceCell; use parking_lot::Mutex; -use std::{fs, io::Write, sync::Arc, time::Duration}; +use std::{ + borrow::Cow, + fs, + io::Write, + path::PathBuf, + sync::{ + atomic::{AtomicBool, AtomicI64, Ordering}, + Arc, + }, + time::Duration, +}; use sysinfo::{Pid, System}; -use tauri::api::process::{Command, CommandChild, CommandEvent}; +use tauri::api::process::{Command, CommandChild}; use tokio::time::sleep; pub enum RunType { @@ -23,43 +41,258 @@ pub enum RunType { Elevated, } +impl Default for RunType { + fn default() -> Self { + let enable_service = Config::verge() + .latest() + .enable_service_mode + .unwrap_or(false); + if enable_service { + RunType::Service + } else { + RunType::Normal + } + } +} + +#[derive(Debug)] +enum Instance { + Child { + child: Mutex>, + stated_changed_at: Arc, + kill_flag: Arc, + }, + Service, +} + +impl Instance { + pub fn try_new(run_type: RunType) -> Result { + let core_type: nyanpasu_utils::core::CoreType = { + (Config::verge() + .latest() + .clash_core + .as_ref() + .unwrap_or(&ClashCore::ClashPremium)) + .into() + }; + let data_dir = dirs::app_data_dir()?; + let binary = find_binary_path(&core_type)?; + let config_path = Config::generate_file(ConfigType::Run)?; + let pid_path = dirs::clash_pid_path()?; + match run_type { + RunType::Normal => { + let instance = Arc::new( + CoreInstanceBuilder::default() + .core_type(core_type) + .app_dir(data_dir) + .binary_path(binary) + .config_path(config_path.clone()) + .pid_path(pid_path) + .build()?, + ); + Ok(Instance::Child { + child: Mutex::new(instance), + kill_flag: Arc::new(AtomicBool::new(false)), + stated_changed_at: Arc::new(AtomicI64::new(get_current_ts())), + }) + } + RunType::Service => Ok(Instance::Service), + RunType::Elevated => { + todo!() + } + } + } + + pub async fn start(&self) -> Result<()> { + match self { + Instance::Child { + child, + kill_flag, + stated_changed_at, + } => { + let instance = { + let child = child.lock(); + child.clone() + }; + let is_premium = { + let child = child.lock(); + matches!( + child.core_type, + nyanpasu_utils::core::CoreType::Clash( + nyanpasu_utils::core::ClashCoreType::ClashPremium + ) + ) + }; + let (tx, mut rx) = tokio::sync::mpsc::channel::>(1); // use mpsc channel just to avoid type moved error, though it never fails + let stated_changed_at = stated_changed_at.clone(); + let kill_flag = kill_flag.clone(); + // This block below is to handle the stdio from the core process + tokio::spawn(async move { + match instance.run().await { + Ok((_, mut rx)) => { + kill_flag.store(false, Ordering::Relaxed); // reset kill flag + let mut err_buf: Vec = Vec::with_capacity(6); + loop { + if let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => { + if is_premium { + let log = api::parse_log(line.clone()); + log::info!(target: "app", "[clash]: {}", log); + } else { + log::info!(target: "app", "[clash]: {}", line); + } + Logger::global().set_log(line); + } + CommandEvent::Stderr(line) => { + log::error!(target: "app", "[clash]: {}", line); + err_buf.push(line.clone()); + Logger::global().set_log(line); + } + CommandEvent::Error(e) => { + log::error!(target: "app", "[clash]: {}", e); + let err = anyhow::anyhow!(format!( + "{}\n{}", + e, + err_buf.join("\n") + )); + Logger::global().set_log(e); + let _ = tx.send(Err(err)).await; + stated_changed_at + .store(get_current_ts(), Ordering::Relaxed); + break; + } + CommandEvent::Terminated(status) => { + log::error!( + target: "app", + "core terminated with status: {:?}", + status + ); + stated_changed_at + .store(get_current_ts(), Ordering::Relaxed); + if status.code != Some(0) + || !matches!(status.signal, Some(9) | Some(15)) + { + let err = anyhow::anyhow!(format!( + "core terminated with status: {:?}\n{}", + status, + err_buf.join("\n") + )); + tracing::error!("{}\n{}", err, err_buf.join("\n")); + if let Err(_) = tx.send(Err(err)).await { + if !kill_flag.load(Ordering::Relaxed) { + std::thread::spawn(move || { + block_on(async { + tracing::info!( + "Trying to recover core." + ); + let _ = CoreManager::global() + .recover_core() + .await; + }); + }); + } + } + } + break; + } + CommandEvent::DelayCheckpointPass => { + tracing::debug!("delay checkpoint pass"); + stated_changed_at + .store(get_current_ts(), Ordering::Relaxed); + tx.send(Ok(())).await.unwrap(); + } + } + } + } + } + Err(err) => { + spawn(async move { + tx.send(Err(err.into())).await.unwrap(); + }); + } + } + }); + rx.recv().await.unwrap()?; + Ok(()) + } + Instance::Service => { + todo!() + } + } + } + + pub async fn stop(&self) -> Result<()> { + let state = self.state(); + match self { + Instance::Child { + child, + stated_changed_at, + kill_flag, + } => { + if matches!(state.as_ref(), CoreState::Stopped(_)) { + anyhow::bail!("core is already stopped"); + } + kill_flag.store(true, Ordering::Relaxed); + let child = { + let child = child.lock(); + child.clone() + }; + child.kill().await?; + stated_changed_at.store(get_current_ts(), Ordering::Relaxed); + Ok(()) + } + Instance::Service => { + todo!() + } + } + } + + pub async fn restart(&self) -> Result<()> { + let state = self.state(); + if matches!(state.as_ref(), CoreState::Running) { + self.stop().await?; + } + self.start().await + } + + pub fn state<'a>(&self) -> Cow<'a, CoreState> { + match self { + Instance::Child { child, .. } => { + let this = child.lock(); + Cow::Owned(match this.state() { + nyanpasu_utils::core::instance::CoreInstanceState::Running => { + CoreState::Running + } + nyanpasu_utils::core::instance::CoreInstanceState::Stopped => { + CoreState::Stopped(None) + } + }) + } + Instance::Service => { + todo!() + } + } + } +} + #[cfg(target_os = "windows")] use crate::core::win_service; #[derive(Debug)] pub struct CoreManager { - sidecar: Arc>>, - - #[allow(unused)] - use_service_mode: Arc>, + instance: Mutex>>, } impl CoreManager { pub fn global() -> &'static CoreManager { static CORE_MANAGER: OnceCell = OnceCell::new(); - CORE_MANAGER.get_or_init(|| CoreManager { - sidecar: Arc::new(Mutex::new(None)), - use_service_mode: Arc::new(Mutex::new(false)), + instance: Mutex::new(None), }) } pub fn init(&self) -> Result<()> { - // kill old clash process - let _ = dirs::clash_pid_path() - .and_then(|path| fs::read(path).map(|p| p.to_vec()).context("")) - .and_then(|pid| String::from_utf8_lossy(&pid).parse().context("")) - .map(|pid| { - let mut system = System::new(); - system.refresh_all(); - if let Some(proc) = system.process(Pid::from_u32(pid)) { - if proc.name().contains("clash") { - log::debug!(target: "app", "kill old clash process"); - proc.kill(); - } - } - }); - tauri::async_runtime::spawn(async { // 启动clash log_err!(Self::global().run_core().await); @@ -98,34 +331,25 @@ impl CoreManager { /// 启动核心 pub async fn run_core(&self) -> Result<()> { - #[allow(unused_mut)] - let mut should_kill = match self.sidecar.lock().take() { - Some(child) => { - log::debug!(target: "app", "stop the core by sidecar"); - let _ = child.kill(); - true + { + let instance = { + let instance = self.instance.lock(); + instance.as_ref().cloned() + }; + if let Some(instance) = instance.as_ref() { + if matches!(instance.state().as_ref(), CoreState::Running) { + log::debug!(target: "app", "core is already running, stop it first..."); + instance.stop().await?; + } } - None => false, - }; - - #[cfg(target_os = "windows")] - if *self.use_service_mode.lock() { - log::debug!(target: "app", "stop the core by service"); - log_err!(win_service::stop_core_by_service().await); - should_kill = true; - } - - // 这里得等一会儿 - if should_kill { - sleep(Duration::from_millis(500)).await; } // 检查端口是否可用 + // TODO: 修复下面这个方法,从而允许 Fallback 到其他端口 Config::clash() .latest() .prepare_external_controller_port()?; - - let config_path = Config::generate_file(ConfigType::Run)?; + let instance = Arc::new(Instance::try_new(RunType::Normal)?); #[cfg(target_os = "macos")] { @@ -144,147 +368,81 @@ impl CoreManager { log::debug!(target: "app", "{event:?}"); } } - #[cfg(target_os = "windows")] + // FIXME: 重构服务模式 + // #[cfg(target_os = "windows")] + // { + // // 服务模式 + // let enable = { Config::verge().latest().enable_service_mode }; + // let enable = enable.unwrap_or(false); + + // *self.use_service_mode.lock() = enable; + + // if enable { + // // 服务模式启动失败就直接运行 sidecar + // log::debug!(target: "app", "try to run core in service mode"); + // let res = async { + // win_service::check_service().await?; + // win_service::run_core_by_service(&config_path).await + // } + // .await; + // match res { + // Ok(_) => return Ok(()), + // Err(err) => { + // // 修改这个值,免得stop出错 + // *self.use_service_mode.lock() = false; + // log::error!(target: "app", "{err}"); + // } + // } + // } + // } + { - // 服务模式 - let enable = { Config::verge().latest().enable_service_mode }; - let enable = enable.unwrap_or(false); - - *self.use_service_mode.lock() = enable; - - if enable { - // 服务模式启动失败就直接运行 sidecar - log::debug!(target: "app", "try to run core in service mode"); - let res = async { - win_service::check_service().await?; - win_service::run_core_by_service(&config_path).await - } - .await; - match res { - Ok(_) => return Ok(()), - Err(err) => { - // 修改这个值,免得stop出错 - *self.use_service_mode.lock() = false; - log::error!(target: "app", "{err}"); - } - } - } + let mut this = self.instance.lock(); + *this = Some(instance.clone()); } - - let app_dir = dirs::app_data_dir()?; - let app_dir = dirs::path_to_str(&app_dir)?; - - let clash_core = { Config::verge().latest().clash_core.clone() }; - let clash_core = clash_core.unwrap_or(ClashCore::ClashPremium); - let is_clash = matches!(&clash_core, ClashCore::ClashPremium); - - let config_path = dirs::path_to_str(&config_path)?; - - // fix #212 - let args = match &clash_core { - ClashCore::Mihomo | ClashCore::MihomoAlpha => { - vec!["-m", "-d", app_dir, "-f", config_path] - } - ClashCore::ClashRs => vec!["-d", app_dir, "-c", config_path], - ClashCore::ClashPremium => vec!["-d", app_dir, "-f", config_path], - }; - - let cmd = Command::new_sidecar(clash_core)?; - let (mut rx, cmd_child) = cmd.args(args).spawn()?; - - // 将pid写入文件中 - crate::log_err!((|| { - let pid = cmd_child.pid(); - let path = dirs::clash_pid_path()?; - fs::File::create(path) - .context("failed to create the pid file")? - .write(format!("{pid}").as_bytes()) - .context("failed to write pid to the file")?; - >::Ok(()) - })()); - - let mut sidecar = self.sidecar.lock(); - *sidecar = Some(cmd_child); - drop(sidecar); - - tauri::async_runtime::spawn(async move { - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Stdout(line) => { - if is_clash { - let stdout = api::parse_log(line.clone()); - log::info!(target: "app", "[clash]: {stdout}"); - } else { - log::info!(target: "app", "[clash]: {line}"); - }; - Logger::global().set_log(line); - } - CommandEvent::Stderr(err) => { - // let stdout = api::parse_log(err.clone()); - log::error!(target: "app", "[clash]: {err}"); - Logger::global().set_log(err); - } - CommandEvent::Error(err) => { - log::error!(target: "app", "[clash]: {err}"); - Logger::global().set_log(err); - } - CommandEvent::Terminated(_) => { - log::info!(target: "app", "clash core terminated"); - let _ = CoreManager::global().recover_core(); - break; - } - _ => {} - } - } - }); - - Ok(()) + instance.start().await } /// 重启内核 - pub fn recover_core(&'static self) -> Result<()> { - // 服务模式不管 - #[cfg(target_os = "windows")] - if *self.use_service_mode.lock() { - return Ok(()); + pub async fn recover_core(&'static self) -> Result<()> { + // 清除原来的实例 + { + let instance = { + let mut this = self.instance.lock(); + this.take() + }; + if let Some(instance) = instance { + if matches!(instance.state().as_ref(), CoreState::Running) { + log::debug!(target: "app", "core is running, stop it first..."); + instance.stop().await?; + } + } } - // 清空原来的sidecar值 - if let Some(sidecar) = self.sidecar.lock().take() { - let _ = sidecar.kill(); + if let Err(err) = self.run_core().await { + log::error!(target: "app", "failed to recover clash core"); + log::error!(target: "app", "{err}"); + tokio::time::sleep(Duration::from_secs(5)).await; // sleep 5s + std::thread::spawn(move || { + block_on(async { + let _ = CoreManager::global().recover_core().await; + }) + }); } - tauri::async_runtime::spawn(async move { - // 6秒之后再查看服务是否正常 (时间随便搞的) - // terminated 可能是切换内核 (切换内核已经有500ms的延迟) - sleep(Duration::from_millis(6666)).await; - - if self.sidecar.lock().is_none() { - log::info!(target: "app", "recover clash core"); - - // 重新启动app - if let Err(err) = self.run_core().await { - log::error!(target: "app", "failed to recover clash core"); - log::error!(target: "app", "{err}"); - - let _ = self.recover_core(); - } - } - }); - Ok(()) } /// 停止核心运行 - pub fn stop_core(&self) -> Result<()> { - #[cfg(target_os = "windows")] - if *self.use_service_mode.lock() { - log::debug!(target: "app", "stop the core by service"); - tauri::async_runtime::block_on(async move { - log_err!(win_service::stop_core_by_service().await); - }); - return Ok(()); - } + pub async fn stop_core(&self) -> Result<()> { + // #[cfg(target_os = "windows")] + // if *self.use_service_mode.lock() { + // log::debug!(target: "app", "stop the core by service"); + // tauri::async_runtime::block_on(async move { + // log_err!(win_service::stop_core_by_service().await); + // }); + // return Ok(()); + // } #[cfg(target_os = "macos")] { @@ -301,16 +459,18 @@ impl CoreManager { Ok(_) => return Ok(()), Err(err) => { // 修改这个值,免得stop出错 - *self.use_service_mode.lock() = false; + // *self.use_service_mode.lock() = false; log::error!(target: "app", "{err}"); } } } } - let mut sidecar = self.sidecar.lock(); - if let Some(child) = sidecar.take() { - log::debug!(target: "app", "stop the core by sidecar"); - let _ = child.kill(); + let instance = { + let instance = self.instance.lock(); + instance.as_ref().cloned() + }; + if let Some(instance) = instance.as_ref() { + instance.stop().await?; } Ok(()) } @@ -319,10 +479,6 @@ impl CoreManager { pub async fn change_core(&self, clash_core: Option) -> Result<()> { let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?; - // if &clash_core != "clash" && &clash_core != "clash-meta" && &clash_core != "clash-rs" { - // bail!("invalid clash core name \"{clash_core}\""); - // } - log::debug!(target: "app", "change core to `{clash_core}`"); Config::verge().draft().clash_core = Some(clash_core); @@ -345,6 +501,7 @@ impl CoreManager { Err(err) => { Config::verge().discard(); Config::runtime().discard(); + self.run_core().await?; Err(err) } } @@ -383,3 +540,25 @@ impl CoreManager { Ok(()) } } + +// TODO: support system path search via a config or flag +// FIXME: move this fn to nyanpasu-utils +/// Search the binary path of the core: Data Dir -> Sidecar Dir +pub fn find_binary_path(core_type: &nyanpasu_utils::core::CoreType) -> std::io::Result { + let data_dir = dirs::app_data_dir() + .map_err(|err| std::io::Error::new(std::io::ErrorKind::NotFound, err.to_string()))?; + let binary_path = data_dir.join(core_type.get_executable_name()); + if binary_path.exists() { + return Ok(binary_path); + } + let app_dir = dirs::app_install_dir() + .map_err(|err| std::io::Error::new(std::io::ErrorKind::NotFound, err.to_string()))?; + let binary_path = app_dir.join(core_type.get_executable_name()); + if binary_path.exists() { + return Ok(binary_path); + } + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + format!("{} not found", core_type.get_executable_name()), + )) +} diff --git a/backend/tauri/src/core/updater/instance.rs b/backend/tauri/src/core/updater/instance.rs index d39225aa8a..3a88e09e98 100644 --- a/backend/tauri/src/core/updater/instance.rs +++ b/backend/tauri/src/core/updater/instance.rs @@ -211,7 +211,7 @@ impl Updater { .clone() .unwrap_or_default(); if current_core == self.core_type { - tokio::task::spawn_blocking(move || CoreManager::global().stop_core()).await??; + CoreManager::global().stop_core().await?; return Ok(()); } #[cfg(target_os = "windows")] diff --git a/backend/tauri/src/main.rs b/backend/tauri/src/main.rs index fd6216a375..ff62a00389 100644 --- a/backend/tauri/src/main.rs +++ b/backend/tauri/src/main.rs @@ -53,6 +53,7 @@ fn deadlock_detection() { } fn main() -> std::io::Result<()> { + // share the tauri async runtime to nyanpasu-utils #[cfg(feature = "deadlock-detection")] deadlock_detection(); diff --git a/backend/tauri/src/utils/resolve.rs b/backend/tauri/src/utils/resolve.rs index 006faa00b7..efb745203a 100644 --- a/backend/tauri/src/utils/resolve.rs +++ b/backend/tauri/src/utils/resolve.rs @@ -15,7 +15,7 @@ use anyhow::Result; use semver::Version; use serde_yaml::Mapping; use std::net::TcpListener; -use tauri::{api::process::Command, App, AppHandle, Manager}; +use tauri::{api::process::Command, async_runtime::block_on, App, AppHandle, Manager}; #[cfg(target_os = "macos")] fn set_window_controls_pos(window: cocoa::base::id, x: f64, y: f64) { @@ -139,7 +139,7 @@ pub fn resolve_setup(app: &mut App) { /// reset system proxy pub fn resolve_reset() { log_err!(sysopt::Sysopt::global().reset_sysproxy()); - log_err!(CoreManager::global().stop_core()); + log_err!(block_on(CoreManager::global().stop_core())); } /// create main window diff --git a/frontend/nyanpasu/src/components/setting/setting-clash-core.tsx b/frontend/nyanpasu/src/components/setting/setting-clash-core.tsx index caedbb72d8..b30c0e82db 100644 --- a/frontend/nyanpasu/src/components/setting/setting-clash-core.tsx +++ b/frontend/nyanpasu/src/components/setting/setting-clash-core.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useMessage } from "@/hooks/use-notification"; import LoadingButton from "@mui/lab/LoadingButton"; -import { Box, List, ListItem, Tooltip } from "@mui/material"; +import { Box, List, ListItem } from "@mui/material"; import { ClashCore, useClash, useNyanpasu } from "@nyanpasu/interface"; import { BaseCard, ExpandMore } from "@nyanpasu/ui"; import { ClashCoreItem } from "./modules/clash-core"; @@ -55,7 +55,9 @@ export const SettingClashCore = () => { }); } catch (e) { useMessage( - "Switching failed, please check log and modify your profile file.", + `Switching failed, you could see the details in the log. \nError: ${ + e instanceof Error ? e.message : String(e) + }`, { type: "error", title: t("Error"), diff --git a/scripts/check.ts b/scripts/check.ts index 3b76da77e2..4ea88629ee 100644 --- a/scripts/check.ts +++ b/scripts/check.ts @@ -48,7 +48,6 @@ const tasks: { name: "nyanpasu-service", func: () => resolve.service(), retry: 5, - winOnly: true, }, { name: "mmdb", func: () => resolve.mmdb(), retry: 5 }, { name: "geoip", func: () => resolve.geoip(), retry: 5 },