diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6ccfafa..f7aed53 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -488,9 +488,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", @@ -985,9 +985,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer 0.10.3", "crypto-common", @@ -1860,9 +1860,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.4" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd8e4fb07cf672b1642304e731ef8a6a4c7891d67bb4fd4f5ce58cd6ed86803c" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" dependencies = [ "bytemuck", "byteorder", @@ -3235,9 +3235,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ "base64", "bytes", @@ -3670,7 +3670,7 @@ checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.5", + "digest 0.10.6", ] [[package]] @@ -3681,7 +3681,7 @@ checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.5", + "digest 0.10.6", ] [[package]] @@ -3705,7 +3705,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if 1.0.0", "cpufeatures", - "digest 0.10.5", + "digest 0.10.6", ] [[package]] @@ -3955,9 +3955,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42c460173627564bde252ca5ebf346ba5b37c5cee1a445782bacc8e9b8d38b5e" +checksum = "a0382fcc02b87420a0d024ff6ec2dbfccdccafb46de8c03fb07dbf2f36810a42" dependencies = [ "bitflags", "cairo-rs", @@ -3995,7 +3995,7 @@ dependencies = [ "scopeguard", "serde", "unicode-segmentation", - "uuid 1.2.1", + "uuid 1.2.2", "windows 0.39.0", "windows-implement", "x11-dl", @@ -4060,7 +4060,7 @@ dependencies = [ "time 0.3.17", "tokio", "url", - "uuid 1.2.1", + "uuid 1.2.2", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -4105,7 +4105,7 @@ dependencies = [ "tauri-utils", "thiserror", "time 0.3.17", - "uuid 1.2.1", + "uuid 1.2.2", "walkdir", ] @@ -4138,7 +4138,7 @@ dependencies = [ "serde_json", "tauri-utils", "thiserror", - "uuid 1.2.1", + "uuid 1.2.2", "webview2-com", "windows 0.39.0", ] @@ -4156,7 +4156,7 @@ dependencies = [ "raw-window-handle", "tauri-runtime", "tauri-utils", - "uuid 1.2.1", + "uuid 1.2.2", "webkit2gtk", "webview2-com", "windows 0.39.0", @@ -4708,9 +4708,9 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" [[package]] name = "uuid" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb41e78f93363bb2df8b0e86a2ca30eed7806ea16ea0c790d757cf93f79be83" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" dependencies = [ "getrandom 0.2.8", ] @@ -5358,9 +5358,9 @@ dependencies = [ [[package]] name = "wry" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d297b203eae65b095af16c02978b7932be1968012b4da7138390edf34dea5" +checksum = "e0fd80bb2bd8e8eae26d59c5164e70233f29f7593cb886a958024a4fd8b5cd21" dependencies = [ "base64", "block", diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index e8ac49f..fb05e43 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -1,258 +1,179 @@ use crate::{ - core::Core, - data::{ClashInfo, Data, PrfItem, PrfOption, Profiles, Verge}, + config::*, + core::*, + feat, utils::{dirs, help}, }; -use crate::{log_if_err, ret_err, wrap_err}; -use anyhow::Result; +use crate::{ret_err, wrap_err}; +use anyhow::{Context, Result}; use serde_yaml::Mapping; use std::collections::{HashMap, VecDeque}; use sysproxy::Sysproxy; type CmdResult = Result; -/// get all profiles from `profiles.yaml` #[tauri::command] -pub fn get_profiles() -> CmdResult { - let global = Data::global(); - let profiles = global.profiles.lock(); - Ok(profiles.clone()) +pub fn get_profiles() -> CmdResult { + Ok(Config::profiles().data().clone()) } -/// manually exec enhanced profile #[tauri::command] -pub fn enhance_profiles() -> CmdResult { - let core = Core::global(); - wrap_err!(core.activate()) -} - -/// import the profile from url -/// and save to `profiles.yaml` -#[tauri::command] -pub async fn import_profile(url: String, option: Option) -> CmdResult { - let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; - - let global = Data::global(); - let mut profiles = global.profiles.lock(); - wrap_err!(profiles.append_item(item)) -} - -/// new a profile -/// append a temp profile item file to the `profiles` dir -/// view the temp profile file by using vscode or other editor -#[tauri::command] -pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResult { - let item = wrap_err!(PrfItem::from(item, file_data).await)?; - - let global = Data::global(); - let mut profiles = global.profiles.lock(); - wrap_err!(profiles.append_item(item)) -} - -/// Update the profile -#[tauri::command] -pub async fn update_profile(index: String, option: Option) -> CmdResult { - let core = Core::global(); - wrap_err!(core.update_profile_item(index, option).await) -} - -/// change the current profile -#[tauri::command] -pub fn select_profile(index: String) -> CmdResult { - let global = Data::global(); - let mut profiles = global.profiles.lock(); - wrap_err!(profiles.put_current(index))?; - drop(profiles); - - let core = Core::global(); - wrap_err!(core.activate()) -} - -/// change the profile chain -#[tauri::command] -pub fn change_profile_chain(chain: Option>) -> CmdResult { - let global = Data::global(); - let mut profiles = global.profiles.lock(); - wrap_err!(profiles.put_chain(chain))?; - drop(profiles); - - let core = Core::global(); - wrap_err!(core.activate()) -} - -/// change the profile valid fields -#[tauri::command] -pub fn change_profile_valid(valid: Option>) -> CmdResult { - let global = Data::global(); - let mut profiles = global.profiles.lock(); - wrap_err!(profiles.put_valid(valid))?; - drop(profiles); - - let core = Core::global(); - wrap_err!(core.activate()) -} - -/// delete profile item -#[tauri::command] -pub fn delete_profile(index: String) -> CmdResult { - let global = Data::global(); - let mut profiles = global.profiles.lock(); - if wrap_err!(profiles.delete_item(index))? { - drop(profiles); - - let core = Core::global(); - log_if_err!(core.activate()); - } +pub async fn enhance_profiles() -> CmdResult { + wrap_err!(CoreManager::global().update_config().await)?; + handle::Handle::refresh_clash(); Ok(()) } -/// patch the profile config +#[deprecated] #[tauri::command] -pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult { - let global = Data::global(); - let mut profiles = global.profiles.lock(); - wrap_err!(profiles.patch_item(index, profile))?; - drop(profiles); - - // update cron task - let core = Core::global(); - let mut timer = core.timer.lock(); - wrap_err!(timer.refresh()) +pub async fn import_profile(url: String, option: Option) -> CmdResult { + let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; + wrap_err!(Config::profiles().data().append_item(item)) } -/// run vscode command to edit the profile #[tauri::command] -pub fn view_profile(index: String) -> CmdResult { - let global = Data::global(); - let profiles = global.profiles.lock(); - let item = wrap_err!(profiles.get_item(&index))?; +pub async fn create_profile(item: PrfItem, file_data: Option) -> CmdResult { + let item = wrap_err!(PrfItem::from(item, file_data).await)?; + wrap_err!(Config::profiles().data().append_item(item)) +} - let file = item.file.clone(); - if file.is_none() { - ret_err!("file is null"); +#[tauri::command] +pub async fn update_profile(index: String, option: Option) -> CmdResult { + wrap_err!(feat::update_profile(index, option).await) +} + +#[tauri::command] +pub async fn delete_profile(index: String) -> CmdResult { + let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?; + if should_update { + wrap_err!(CoreManager::global().update_config().await)?; + handle::Handle::refresh_clash(); } - let path = dirs::app_profiles_dir().join(file.unwrap()); + Ok(()) +} + +/// 修改profiles的 +#[tauri::command] +pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { + wrap_err!({ Config::profiles().draft().patch_config(profiles) })?; + + match CoreManager::global().update_config().await { + Ok(_) => { + handle::Handle::refresh_clash(); + Config::profiles().apply(); + wrap_err!(Config::profiles().data().save_file())?; + Ok(()) + } + Err(err) => { + Config::profiles().discard(); + log::error!(target: "app", "{err}"); + Err(format!("{err}")) + } + } +} + +/// 修改某个profile item的 +#[tauri::command] +pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult { + wrap_err!(Config::profiles().data().patch_item(index, profile))?; + wrap_err!(timer::Timer::global().refresh()) +} + +#[tauri::command] +pub fn view_profile(index: String) -> CmdResult { + let file = { + wrap_err!(Config::profiles().latest().get_item(&index))? + .file + .clone() + .ok_or("the file field is null") + }?; + + let path = wrap_err!(dirs::app_profiles_dir())?.join(file); if !path.exists() { - ret_err!("file not found"); + ret_err!("the file not found"); } wrap_err!(help::open_file(path)) } -/// read the profile item file data #[tauri::command] pub fn read_profile_file(index: String) -> CmdResult { - let global = Data::global(); - let profiles = global.profiles.lock(); + let profiles = Config::profiles(); + let profiles = profiles.latest(); let item = wrap_err!(profiles.get_item(&index))?; let data = wrap_err!(item.read_file())?; Ok(data) } -/// save the profile item file data #[tauri::command] pub fn save_profile_file(index: String, file_data: Option) -> CmdResult { if file_data.is_none() { return Ok(()); } - let global = Data::global(); - let profiles = global.profiles.lock(); + let profiles = Config::profiles(); + let profiles = profiles.latest(); let item = wrap_err!(profiles.get_item(&index))?; wrap_err!(item.save_file(file_data.unwrap())) } -/// get the clash core info from the state -/// the caller can also get the infomation by clash's api #[tauri::command] -pub fn get_clash_info() -> CmdResult { - let global = Data::global(); - let clash = global.clash.lock(); - Ok(clash.info.clone()) +pub fn get_clash_info() -> CmdResult { + wrap_err!(Config::clash().latest().get_info()) } -/// get the runtime clash config mapping #[tauri::command] pub fn get_runtime_config() -> CmdResult> { - let core = Core::global(); - let rt = core.runtime.lock(); - Ok(rt.config.clone()) + Ok(Config::runtime().latest().config.clone()) } -/// get the runtime clash config yaml string #[tauri::command] -pub fn get_runtime_yaml() -> CmdResult> { - let core = Core::global(); - let rt = core.runtime.lock(); - Ok(rt.config_yaml.clone()) +pub fn get_runtime_yaml() -> CmdResult { + let runtime = Config::runtime(); + let runtime = runtime.latest(); + let config = runtime.config.as_ref(); + wrap_err!(config + .ok_or(anyhow::anyhow!("failed to parse config to yaml file")) + .and_then( + |config| serde_yaml::to_string(config).context("failed to convert config to yaml") + )) } -/// get the runtime config exists keys #[tauri::command] pub fn get_runtime_exists() -> CmdResult> { - let core = Core::global(); - let rt = core.runtime.lock(); - Ok(rt.exists_keys.clone()) + Ok(Config::runtime().latest().exists_keys.clone()) } -/// get the runtime enhanced chain log #[tauri::command] pub fn get_runtime_logs() -> CmdResult>> { - let core = Core::global(); - let rt = core.runtime.lock(); - Ok(rt.chain_logs.clone()) -} - -/// update the clash core config -/// after putting the change to the clash core -/// then we should save the latest config -#[tauri::command] -pub fn patch_clash_config(payload: Mapping) -> CmdResult { - let core = Core::global(); - wrap_err!(core.patch_clash(payload)) + Ok(Config::runtime().latest().chain_logs.clone()) } #[tauri::command] -pub fn get_verge_config() -> CmdResult { - let global = Data::global(); - let verge = global.verge.lock(); - Ok(verge.clone()) -} - -/// patch the verge config -/// this command only save the config and not responsible for other things -#[tauri::command] -pub fn patch_verge_config(payload: Verge) -> CmdResult { - let core = Core::global(); - wrap_err!(core.patch_verge(payload)) +pub async fn patch_clash_config(payload: Mapping) -> CmdResult { + wrap_err!(feat::patch_clash(payload).await) } #[tauri::command] -pub fn update_hotkeys(hotkeys: Vec) -> CmdResult { - let core = Core::global(); - let mut hotkey = core.hotkey.lock(); - wrap_err!(hotkey.update(hotkeys)) +pub fn get_verge_config() -> CmdResult { + Ok(Config::verge().data().clone()) } -/// change clash core #[tauri::command] -pub fn change_clash_core(clash_core: Option) -> CmdResult { - let core = Core::global(); - wrap_err!(core.change_core(clash_core)) +pub async fn patch_verge_config(payload: IVerge) -> CmdResult { + wrap_err!(feat::patch_verge(payload).await) +} + +#[tauri::command] +pub async fn change_clash_core(clash_core: Option) -> CmdResult { + wrap_err!(CoreManager::global().change_core(clash_core).await) } /// restart the sidecar #[tauri::command] -pub fn restart_sidecar() -> CmdResult { - let core = Core::global(); - wrap_err!(core.restart_clash()) -} - -/// kill all sidecars when update app -#[tauri::command] -pub fn kill_sidecar() { - tauri::api::process::kill_children(); +pub async fn restart_sidecar() -> CmdResult { + wrap_err!(CoreManager::global().run_core().await) } /// get the system proxy @@ -273,64 +194,44 @@ pub fn get_sys_proxy() -> CmdResult { #[tauri::command] pub fn get_clash_logs() -> CmdResult> { - let core = Core::global(); - let service = core.service.lock(); - Ok(service.get_logs()) + Ok(logger::Logger::global().get_log()) } -/// open app config dir #[tauri::command] pub fn open_app_dir() -> CmdResult<()> { - let app_dir = dirs::app_home_dir(); + let app_dir = wrap_err!(dirs::app_home_dir())?; wrap_err!(open::that(app_dir)) } -/// open logs dir #[tauri::command] pub fn open_logs_dir() -> CmdResult<()> { - let log_dir = dirs::app_logs_dir(); + let log_dir = wrap_err!(dirs::app_logs_dir())?; wrap_err!(open::that(log_dir)) } -/// open url #[tauri::command] pub fn open_web_url(url: String) -> CmdResult<()> { wrap_err!(open::that(url)) } -/// service mode #[cfg(windows)] pub mod service { use super::*; - use crate::core::win_service::JsonResponse; + use crate::core::win_service; #[tauri::command] - pub async fn start_service() -> CmdResult<()> { - wrap_err!(crate::core::Service::start_service().await) + pub async fn check_service() -> CmdResult { + wrap_err!(win_service::check_service().await) } #[tauri::command] - pub async fn stop_service() -> CmdResult<()> { - wrap_err!(crate::core::Service::stop_service().await) + pub async fn install_service() -> CmdResult { + wrap_err!(win_service::install_service().await) } #[tauri::command] - pub async fn check_service() -> CmdResult { - // no log - match crate::core::Service::check_service().await { - Ok(res) => Ok(res), - Err(err) => Err(err.to_string()), - } - } - - #[tauri::command] - pub async fn install_service() -> CmdResult<()> { - wrap_err!(crate::core::Service::install_service().await) - } - - #[tauri::command] - pub async fn uninstall_service() -> CmdResult<()> { - wrap_err!(crate::core::Service::uninstall_service().await) + pub async fn uninstall_service() -> CmdResult { + wrap_err!(win_service::uninstall_service().await) } } @@ -339,23 +240,15 @@ pub mod service { use super::*; #[tauri::command] - pub async fn start_service() -> CmdResult<()> { + pub async fn check_service() -> CmdResult { Ok(()) } #[tauri::command] - pub async fn stop_service() -> CmdResult<()> { + pub async fn install_service() -> CmdResult { Ok(()) } #[tauri::command] - pub async fn check_service() -> CmdResult<()> { - Ok(()) - } - #[tauri::command] - pub async fn install_service() -> CmdResult<()> { - Ok(()) - } - #[tauri::command] - pub async fn uninstall_service() -> CmdResult<()> { + pub async fn uninstall_service() -> CmdResult { Ok(()) } } diff --git a/src-tauri/src/data/clash.rs b/src-tauri/src/config/clash.rs similarity index 53% rename from src-tauri/src/data/clash.rs rename to src-tauri/src/config/clash.rs index 73ecd35..549ccfc 100644 --- a/src-tauri/src/data/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -1,27 +1,70 @@ -use crate::utils::{config, dirs}; +use crate::utils::{dirs, help}; use anyhow::Result; use serde::{Deserialize, Serialize}; use serde_yaml::{Mapping, Value}; +#[derive(Default, Debug, Clone)] +pub struct IClashTemp(pub Mapping); + +impl IClashTemp { + pub fn new() -> Self { + match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) { + Ok(map) => Self(map), + Err(err) => { + log::error!(target: "app", "{err}"); + Self::template() + } + } + } + + pub fn template() -> Self { + let mut map = Mapping::new(); + + map.insert("mixed-port".into(), 7890.into()); + map.insert("log-level".into(), "info".into()); + map.insert("allow-lan".into(), false.into()); + map.insert("mode".into(), "rule".into()); + map.insert("external-controller".into(), "127.0.0.1:9090".into()); + map.insert("secret".into(), "".into()); + + Self(map) + } + + pub fn patch_config(&mut self, patch: Mapping) { + for (key, value) in patch.into_iter() { + self.0.insert(key, value); + } + } + + pub fn save_config(&self) -> Result<()> { + help::save_yaml( + &dirs::clash_path()?, + &self.0, + Some("# Generated by Clash Verge"), + ) + } + + pub fn get_info(&self) -> Result { + Ok(ClashInfoN::from(&self.0)) + } +} + #[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct ClashInfo { +pub struct ClashInfoN { /// clash sidecar status pub status: String, - /// clash core port pub port: Option, - /// same as `external-controller` pub server: Option, - /// clash secret pub secret: Option, } -impl ClashInfo { +impl ClashInfoN { /// parse the clash's config.yaml /// get some information - pub fn from(config: &Mapping) -> ClashInfo { + pub fn from(config: &Mapping) -> ClashInfoN { let key_port_1 = Value::from("mixed-port"); let key_port_2 = Value::from("port"); let key_server = Value::from("external-controller"); @@ -97,7 +140,7 @@ impl ClashInfo { _ => None, }; - ClashInfo { + ClashInfoN { status: format!("{status}"), port, server, @@ -106,62 +149,52 @@ impl ClashInfo { } } -#[derive(Debug)] -pub struct Clash { - /// maintain the clash config - pub config: Mapping, - - /// some info - pub info: ClashInfo, +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct IClash { + pub mixed_port: Option, + pub allow_lan: Option, + pub log_level: Option, + pub ipv6: Option, + pub mode: Option, + pub external_controller: Option, + pub secret: Option, + pub dns: Option, + pub tun: Option, + pub interface_name: Option, } -impl Clash { - pub fn new() -> Clash { - let config = Clash::read_config(); - let info = ClashInfo::from(&config); - - Clash { config, info } - } - - /// get clash config - pub fn read_config() -> Mapping { - config::read_merge_mapping(dirs::clash_path()) - } - - /// save the clash config - pub fn save_config(&self) -> Result<()> { - config::save_yaml( - dirs::clash_path(), - &self.config, - Some("# Default Config For Clash Core\n\n"), - ) - } - - /// patch update the clash config - /// if the port is changed then return true - pub fn patch_config(&mut self, patch: Mapping) -> Result<()> { - let port_key = Value::from("mixed-port"); - let server_key = Value::from("external-controller"); - let secret_key = Value::from("secret"); - - let change_info = patch.contains_key(&port_key) - || patch.contains_key(&server_key) - || patch.contains_key(&secret_key); - - for (key, value) in patch.into_iter() { - self.config.insert(key, value); - } - - if change_info { - self.info = ClashInfo::from(&self.config); - } - - self.save_config() - } +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct IClashTUN { + pub enable: Option, + pub stack: Option, + pub auto_route: Option, + pub auto_detect_interface: Option, + pub dns_hijack: Option>, } -impl Default for Clash { - fn default() -> Self { - Clash::new() - } +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct IClashDNS { + pub enable: Option, + pub listen: Option, + pub default_nameserver: Option>, + pub enhanced_mode: Option, + pub fake_ip_range: Option, + pub use_hosts: Option, + pub fake_ip_filter: Option>, + pub nameserver: Option>, + pub fallback: Option>, + pub fallback_filter: Option, + pub nameserver_policy: Option>, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct IClashFallbackFilter { + pub geoip: Option, + pub geoip_code: Option, + pub ipcidr: Option>, + pub domain: Option>, } diff --git a/src-tauri/src/config/config.rs b/src-tauri/src/config/config.rs new file mode 100644 index 0000000..1ba7a1e --- /dev/null +++ b/src-tauri/src/config/config.rs @@ -0,0 +1,111 @@ +use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge}; +use crate::{ + enhance, + utils::{dirs, help}, +}; +use anyhow::{anyhow, Result}; +use once_cell::sync::OnceCell; +use std::{env::temp_dir, path::PathBuf}; + +pub const RUNTIME_CONFIG: &str = "clash-verge.yaml"; +pub const CHECK_CONFIG: &str = "clash-verge-check.yaml"; + +pub struct Config { + clash_config: Draft, + verge_config: Draft, + profiles_config: Draft, + runtime_config: Draft, +} + +impl Config { + pub fn global() -> &'static Config { + static CONFIG: OnceCell = OnceCell::new(); + + CONFIG.get_or_init(|| Config { + clash_config: Draft::from(IClashTemp::new()), + verge_config: Draft::from(IVerge::new()), + profiles_config: Draft::from(IProfiles::new()), + runtime_config: Draft::from(IRuntime::new()), + }) + } + + pub fn clash() -> Draft { + Self::global().clash_config.clone() + } + + pub fn verge() -> Draft { + Self::global().verge_config.clone() + } + + pub fn profiles() -> Draft { + Self::global().profiles_config.clone() + } + + pub fn runtime() -> Draft { + Self::global().runtime_config.clone() + } + + /// 初始化配置 + pub fn init_config() -> Result<()> { + crate::log_err!(Self::generate()); + if let Err(err) = Self::generate_file(ConfigType::Run) { + log::error!(target: "app", "{err}"); + + let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG); + // 如果不存在就将默认的clash文件拿过来 + if !runtime_path.exists() { + help::save_yaml( + &runtime_path, + &Config::clash().latest().0, + Some("# Clash Verge Runtime"), + )?; + } + } + Ok(()) + } + + /// 将配置丢到对应的文件中 + pub fn generate_file(typ: ConfigType) -> Result { + let path = match typ { + ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG), + ConfigType::Check => temp_dir().join(CHECK_CONFIG), + }; + + let runtime = Config::runtime(); + let runtime = runtime.latest(); + let config = runtime + .config + .as_ref() + .ok_or(anyhow!("failed to get runtime config"))?; + + help::save_yaml(&path, &config, Some("# Generated by Clash Verge"))?; + Ok(path) + } + + /// 生成配置存好 + pub fn generate() -> Result<()> { + let clash_config = { Config::clash().latest().clone() }; + + let tun_mode = { Config::verge().latest().enable_tun_mode.clone() }; + let tun_mode = tun_mode.unwrap_or(false); + + let pa = { Config::profiles().latest().gen_activate()? }; + + let (config, exists_keys, logs) = + enhance::enhance_config(clash_config.0, pa.current, pa.chain, pa.valid, tun_mode); + + *Config::runtime().draft() = IRuntime { + config: Some(config), + exists_keys, + chain_logs: logs, + }; + + Ok(()) + } +} + +#[derive(Debug)] +pub enum ConfigType { + Run, + Check, +} diff --git a/src-tauri/src/config/draft.rs b/src-tauri/src/config/draft.rs new file mode 100644 index 0000000..f1edf67 --- /dev/null +++ b/src-tauri/src/config/draft.rs @@ -0,0 +1,127 @@ +use super::{IClashTemp, IProfiles, IRuntime, IVerge}; +use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; +use std::sync::Arc; + +#[derive(Debug, Clone)] +pub struct Draft { + inner: Arc)>>, +} + +macro_rules! draft_define { + ($id: ident) => { + impl Draft<$id> { + #[allow(unused)] + pub fn data(&self) -> MappedMutexGuard<$id> { + MutexGuard::map(self.inner.lock(), |guard| &mut guard.0) + } + + pub fn latest(&self) -> MappedMutexGuard<$id> { + MutexGuard::map(self.inner.lock(), |inner| { + if inner.1.is_none() { + &mut inner.0 + } else { + inner.1.as_mut().unwrap() + } + }) + } + + pub fn draft(&self) -> MappedMutexGuard<$id> { + MutexGuard::map(self.inner.lock(), |mut inner| { + if inner.1.is_none() { + inner.1 = Some(inner.0.clone()); + } + + inner.1.as_mut().unwrap() + }) + } + + pub fn apply(&self) -> Option<$id> { + let mut inner = self.inner.lock(); + + match inner.1.take() { + Some(draft) => { + let old_value = inner.0.to_owned(); + inner.0 = draft.to_owned(); + Some(old_value) + } + None => None, + } + } + + pub fn discard(&self) -> Option<$id> { + let mut inner = self.inner.lock(); + inner.1.take() + } + } + + impl From<$id> for Draft<$id> { + fn from(data: $id) -> Self { + Draft { + inner: Arc::new(Mutex::new((data, None))), + } + } + } + }; +} + +// draft_define!(IClash); +draft_define!(IClashTemp); +draft_define!(IProfiles); +draft_define!(IRuntime); +draft_define!(IVerge); + +#[test] +fn test_draft() { + let verge = IVerge { + enable_auto_launch: Some(true), + enable_tun_mode: Some(false), + ..IVerge::default() + }; + + let draft = Draft::from(verge); + + assert_eq!(draft.data().enable_auto_launch, Some(true)); + assert_eq!(draft.data().enable_tun_mode, Some(false)); + + assert_eq!(draft.draft().enable_auto_launch, Some(true)); + assert_eq!(draft.draft().enable_tun_mode, Some(false)); + + let mut d = draft.draft(); + d.enable_auto_launch = Some(false); + d.enable_tun_mode = Some(true); + drop(d); + + assert_eq!(draft.data().enable_auto_launch, Some(true)); + assert_eq!(draft.data().enable_tun_mode, Some(false)); + + assert_eq!(draft.draft().enable_auto_launch, Some(false)); + assert_eq!(draft.draft().enable_tun_mode, Some(true)); + + assert_eq!(draft.latest().enable_auto_launch, Some(false)); + assert_eq!(draft.latest().enable_tun_mode, Some(true)); + + assert!(draft.apply().is_some()); + assert!(draft.apply().is_none()); + + assert_eq!(draft.data().enable_auto_launch, Some(false)); + assert_eq!(draft.data().enable_tun_mode, Some(true)); + + assert_eq!(draft.draft().enable_auto_launch, Some(false)); + assert_eq!(draft.draft().enable_tun_mode, Some(true)); + + let mut d = draft.draft(); + d.enable_auto_launch = Some(true); + drop(d); + + assert_eq!(draft.data().enable_auto_launch, Some(false)); + + assert_eq!(draft.draft().enable_auto_launch, Some(true)); + + assert!(draft.discard().is_some()); + + assert_eq!(draft.data().enable_auto_launch, Some(false)); + + assert!(draft.discard().is_none()); + + assert_eq!(draft.draft().enable_auto_launch, Some(false)); +} diff --git a/src-tauri/src/config/mod.rs b/src-tauri/src/config/mod.rs index 7020020..b246a76 100644 --- a/src-tauri/src/config/mod.rs +++ b/src-tauri/src/config/mod.rs @@ -1,69 +1,15 @@ -mod field; -mod merge; -mod script; -mod tun; +mod clash; +mod config; +mod draft; +mod prfitem; +mod profiles; +mod runtime; +mod verge; -pub(self) use self::field::*; -use self::merge::*; -use self::script::*; -use self::tun::*; -use crate::data::ChainItem; -use crate::data::ChainType; -use serde_yaml::Mapping; -use std::collections::HashMap; -use std::collections::HashSet; - -type ResultLog = Vec<(String, String)>; - -pub fn enhance_config( - clash_config: Mapping, - profile_config: Mapping, - chain: Vec, - valid: Vec, - tun_mode: bool, -) -> (Mapping, Vec, HashMap) { - let mut config = profile_config; - let mut result_map = HashMap::new(); - let mut exists_keys = use_keys(&config); - - let valid = use_valid_fields(valid); - - chain.into_iter().for_each(|item| match item.data { - ChainType::Merge(merge) => { - exists_keys.extend(use_keys(&merge)); - config = use_merge(merge, config.to_owned()); - config = use_filter(config.to_owned(), &valid); - } - ChainType::Script(script) => { - let mut logs = vec![]; - - match use_script(script, config.to_owned()) { - Ok((res_config, res_logs)) => { - exists_keys.extend(use_keys(&res_config)); - config = use_filter(res_config, &valid); - logs.extend(res_logs); - } - Err(err) => logs.push(("exception".into(), err.to_string())), - } - - result_map.insert(item.uid, logs); - } - }); - - config = use_filter(config, &valid); - - for (key, value) in clash_config.into_iter() { - config.insert(key, value); - } - - let clash_fields = use_clash_fields(); - config = use_filter(config, &clash_fields); - config = use_tun(config, tun_mode); - config = use_sort(config); - - let mut exists_set = HashSet::new(); - exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s))); - exists_keys = exists_set.into_iter().collect(); - - (config, exists_keys, result_map) -} +pub use self::clash::*; +pub use self::config::*; +pub use self::draft::*; +pub use self::prfitem::*; +pub use self::profiles::*; +pub use self::runtime::*; +pub use self::verge::*; diff --git a/src-tauri/src/data/prfitem.rs b/src-tauri/src/config/prfitem.rs similarity index 94% rename from src-tauri/src/data/prfitem.rs rename to src-tauri/src/config/prfitem.rs index 90b08f3..664ad05 100644 --- a/src-tauri/src/data/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -1,4 +1,4 @@ -use crate::utils::{config, dirs, help, tmpl}; +use crate::utils::{dirs, help, tmpl}; use anyhow::{bail, Context, Result}; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; @@ -6,6 +6,8 @@ use serde_yaml::Mapping; use std::fs; use sysproxy::Sysproxy; +use super::Config; + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct PrfItem { pub uid: Option, @@ -29,7 +31,7 @@ pub struct PrfItem { #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, - /// selected infomation + /// selected information #[serde(skip_serializing_if = "Option::is_none")] pub selected: Option>, @@ -170,7 +172,7 @@ impl PrfItem { selected: None, extra: None, option: None, - updated: Some(help::get_now()), + updated: Some(chrono::Local::now().timestamp() as usize), file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())), }) } @@ -192,9 +194,9 @@ impl PrfItem { // 使用软件自己的代理 if self_proxy { - let data = super::Data::global(); - let port = data.clash.lock().info.port.clone(); + let port = Config::clash().data().get_info()?.port; let port = port.ok_or(anyhow::anyhow!("failed to get clash info port"))?; + let proxy_scheme = format!("http://127.0.0.1:{port}"); if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) { @@ -299,7 +301,7 @@ impl PrfItem { selected: None, extra, option, - updated: Some(help::get_now()), + updated: Some(chrono::Local::now().timestamp() as usize), file_data: Some(data), }) } @@ -320,13 +322,13 @@ impl PrfItem { selected: None, extra: None, option: None, - updated: Some(help::get_now()), + updated: Some(chrono::Local::now().timestamp() as usize), file_data: Some(tmpl::ITEM_MERGE.into()), }) } /// ## Script type (enhance) - /// create the enhanced item by using javascript(browserjs) + /// create the enhanced item by using javascript quick.js pub fn from_script(name: String, desc: String) -> Result { let uid = help::get_uid("s"); let file = format!("{uid}.js"); // js ext @@ -341,7 +343,7 @@ impl PrfItem { selected: None, extra: None, option: None, - updated: Some(help::get_now()), + updated: Some(chrono::Local::now().timestamp() as usize), file_data: Some(tmpl::ITEM_SCRIPT.into()), }) } @@ -353,7 +355,7 @@ impl PrfItem { } let file = self.file.clone().unwrap(); - let path = dirs::app_profiles_dir().join(file); + let path = dirs::app_profiles_dir()?.join(file); fs::read_to_string(path).context("failed to read the file") } @@ -364,7 +366,7 @@ impl PrfItem { } let file = self.file.clone().unwrap(); - let path = dirs::app_profiles_dir().join(file); + let path = dirs::app_profiles_dir()?.join(file); fs::write(path, data.as_bytes()).context("failed to save the file") } @@ -373,7 +375,7 @@ impl PrfItem { let itype = self.itype.as_ref()?.as_str(); let file = self.file.clone()?; let uid = self.uid.clone().unwrap_or("".into()); - let path = dirs::app_profiles_dir().join(file); + let path = dirs::app_profiles_dir().ok()?.join(file); if !path.exists() { return None; @@ -382,11 +384,11 @@ impl PrfItem { match itype { "script" => Some(ChainItem { uid, - data: ChainType::Script(fs::read_to_string(path).unwrap_or("".into())), + data: ChainType::Script(fs::read_to_string(path).ok()?), }), "merge" => Some(ChainItem { uid, - data: ChainType::Merge(config::read_merge_mapping(path)), + data: ChainType::Merge(help::read_merge_mapping(&path).ok()?), }), _ => None, } diff --git a/src-tauri/src/data/profiles.rs b/src-tauri/src/config/profiles.rs similarity index 69% rename from src-tauri/src/data/profiles.rs rename to src-tauri/src/config/profiles.rs index 6272cd8..f6a6399 100644 --- a/src-tauri/src/data/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -1,30 +1,24 @@ -use super::prfitem::PrfItem; -use super::ChainItem; -use crate::utils::{config, dirs, help}; +use super::{prfitem::PrfItem, ChainItem}; +use crate::utils::{dirs, help}; use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_yaml::Mapping; -use std::collections::HashMap; use std::{fs, io::Write}; -/// -/// ## Profiles Config -/// /// Define the `profiles.yaml` schema -/// #[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct Profiles { +pub struct IProfiles { /// same as PrfConfig.current - current: Option, + pub current: Option, /// same as PrfConfig.chain - chain: Option>, + pub chain: Option>, /// record valid fields for clash - valid: Option>, + pub valid: Option>, /// profile list - items: Option>, + pub items: Option>, } macro_rules! patch { @@ -35,72 +29,74 @@ macro_rules! patch { }; } -impl Profiles { +impl IProfiles { pub fn new() -> Self { - Profiles::read_file() - } - - /// read the config from the file - pub fn read_file() -> Self { - let mut profiles = config::read_yaml::(dirs::profiles_path()); - - if profiles.items.is_none() { - profiles.items = Some(vec![]); - } - - // compatiable with the old old old version - profiles.items.as_mut().map(|items| { - for mut item in items.iter_mut() { - if item.uid.is_none() { - item.uid = Some(help::get_uid("d")); + match dirs::profiles_path().and_then(|path| help::read_yaml::(&path)) { + Ok(mut profiles) => { + if profiles.items.is_none() { + profiles.items = Some(vec![]); } + // compatible with the old old old version + profiles.items.as_mut().map(|items| { + for mut item in items.iter_mut() { + if item.uid.is_none() { + item.uid = Some(help::get_uid("d")); + } + } + }); + profiles } - }); - - profiles + Err(err) => { + log::error!(target: "app", "{err}"); + Self::template() + } + } + } + + pub fn template() -> Self { + Self { + valid: Some(vec!["dns".into()]), + items: Some(vec![]), + ..Self::default() + } } - /// save the config to the file pub fn save_file(&self) -> Result<()> { - config::save_yaml( - dirs::profiles_path(), + help::save_yaml( + &dirs::profiles_path()?, self, - Some("# Profiles Config for Clash Verge\n\n"), + Some("# Profiles Config for Clash Verge"), ) } - /// get the current uid - pub fn get_current(&self) -> Option { - self.current.clone() - } - - /// only change the main to the target id - pub fn put_current(&mut self, uid: String) -> Result<()> { + /// 只修改current,valid和chain + pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> { if self.items.is_none() { self.items = Some(vec![]); } - let items = self.items.as_ref().unwrap(); - let some_uid = Some(uid.clone()); + if let Some(current) = patch.current { + let items = self.items.as_ref().unwrap(); + let some_uid = Some(current); - if items.iter().find(|&each| each.uid == some_uid).is_some() { - self.current = some_uid; - return self.save_file(); + if items.iter().any(|e| e.uid == some_uid) { + self.current = some_uid; + } } - bail!("invalid uid \"{uid}\""); + if let Some(chain) = patch.chain { + self.chain = Some(chain); + } + + if let Some(valid) = patch.valid { + self.valid = Some(valid); + } + + Ok(()) } - /// just change the `chain` - pub fn put_chain(&mut self, chain: Option>) -> Result<()> { - self.chain = chain; - self.save_file() - } - - /// just change the `field` - pub fn put_valid(&mut self, valid: Option>) -> Result<()> { - self.valid = valid; - self.save_file() + pub fn get_current(&self) -> Option { + self.current.clone() } /// get items ref @@ -110,8 +106,7 @@ impl Profiles { /// find the item by the uid pub fn get_item(&self, uid: &String) -> Result<&PrfItem> { - if self.items.is_some() { - let items = self.items.as_ref().unwrap(); + if let Some(items) = self.items.as_ref() { let some_uid = Some(uid.clone()); for each in items.iter() { @@ -140,7 +135,7 @@ impl Profiles { } let file = item.file.clone().unwrap(); - let path = dirs::app_profiles_dir().join(&file); + let path = dirs::app_profiles_dir()?.join(&file); fs::File::create(path) .context(format!("failed to create file \"{}\"", file))? @@ -209,7 +204,7 @@ impl Profiles { // the file must exists each.file = Some(file.clone()); - let path = dirs::app_profiles_dir().join(&file); + let path = dirs::app_profiles_dir()?.join(&file); fs::File::create(path) .context(format!("failed to create file \"{}\"", file))? @@ -244,10 +239,12 @@ impl Profiles { if let Some(index) = index { items.remove(index).file.map(|file| { - let path = dirs::app_profiles_dir().join(file); - if path.exists() { - let _ = fs::remove_file(path); - } + let _ = dirs::app_profiles_dir().map(|path| { + let path = path.join(file); + if path.exists() { + let _ = fs::remove_file(path); + } + }); }); } @@ -272,22 +269,18 @@ impl Profiles { return Ok(config); } - let current = self.current.clone().unwrap(); + let current = self.current.as_ref().unwrap(); for item in self.items.as_ref().unwrap().iter() { - if item.uid == Some(current.clone()) { - let file_path = match item.file.clone() { - Some(file) => dirs::app_profiles_dir().join(file), + if item.uid.as_ref() == Some(current) { + let file_path = match item.file.as_ref() { + Some(file) => dirs::app_profiles_dir()?.join(file), None => bail!("failed to get the file field"), }; - if !file_path.exists() { - bail!("failed to read the file \"{}\"", file_path.display()); - } - - return Ok(config::read_merge_mapping(file_path.clone())); + return Ok(help::read_merge_mapping(&file_path)?); } } - bail!("failed to find current profile \"uid:{current}\""); + bail!("failed to find the current profile \"uid:{current}\""); } /// generate the data for activate clash config @@ -317,13 +310,3 @@ pub struct PrfActivate { pub chain: Vec, pub valid: Vec, } - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct RuntimeResult { - pub config: Option, - pub config_yaml: Option, - // 记录在配置中(包括merge和script生成的)出现过的keys - // 这些keys不一定都生效 - pub exists_keys: Vec, - pub chain_logs: HashMap>, -} diff --git a/src-tauri/src/config/runtime.rs b/src-tauri/src/config/runtime.rs new file mode 100644 index 0000000..3b67b06 --- /dev/null +++ b/src-tauri/src/config/runtime.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; +use serde_yaml::Mapping; +use std::collections::HashMap; + +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct IRuntime { + pub config: Option, + // 记录在配置中(包括merge和script生成的)出现过的keys + // 这些keys不一定都生效 + pub exists_keys: Vec, + pub chain_logs: HashMap>, +} + +impl IRuntime { + pub fn new() -> Self { + Self::default() + } +} diff --git a/src-tauri/src/data/verge.rs b/src-tauri/src/config/verge.rs similarity index 63% rename from src-tauri/src/data/verge.rs rename to src-tauri/src/config/verge.rs index 97c2016..60143ee 100644 --- a/src-tauri/src/data/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -1,12 +1,11 @@ -use crate::utils::{config, dirs}; +use crate::utils::{dirs, help}; use anyhow::Result; use serde::{Deserialize, Serialize}; /// ### `verge.yaml` schema #[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct Verge { - /// app listening port - /// for app singleton +pub struct IVerge { + /// app listening port for app singleton pub app_singleton_port: Option, // i18n @@ -48,7 +47,7 @@ pub struct Verge { pub proxy_guard_duration: Option, /// theme setting - pub theme_setting: Option, + pub theme_setting: Option, /// web ui list pub web_ui_list: Option>, @@ -69,7 +68,7 @@ pub struct Verge { } #[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct VergeTheme { +pub struct IVergeTheme { pub primary_color: Option, pub secondary_color: Option, pub primary_text: Option, @@ -84,23 +83,42 @@ pub struct VergeTheme { pub css_injection: Option, } -impl Verge { +impl IVerge { pub fn new() -> Self { - config::read_yaml::(dirs::verge_path()) + match dirs::verge_path().and_then(|path| help::read_yaml::(&path)) { + Ok(config) => config, + Err(err) => { + log::error!(target: "app", "{err}"); + Self::template() + } + } } - /// Save Verge App Config + pub fn template() -> Self { + Self { + clash_core: Some("clash".into()), + language: Some("en".into()), + theme_mode: Some("system".into()), + theme_blur: Some(false), + traffic_graph: Some(true), + enable_auto_launch: Some(false), + enable_silent_start: Some(false), + enable_system_proxy: Some(false), + enable_proxy_guard: Some(false), + proxy_guard_duration: Some(30), + auto_close_connection: Some(true), + ..Self::default() + } + } + + /// Save IVerge App Config pub fn save_file(&self) -> Result<()> { - config::save_yaml( - dirs::verge_path(), - self, - Some("# The Config for Clash Verge App\n\n"), - ) + help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Verge Config")) } /// patch verge config /// only save to file - pub fn patch_config(&mut self, patch: Verge) -> Result<()> { + pub fn patch_config(&mut self, patch: IVerge) { macro_rules! patch { ($key: tt) => { if patch.$key.is_some() { @@ -130,7 +148,18 @@ impl Verge { patch!(auto_close_connection); patch!(default_latency_test); + } - self.save_file() + /// 在初始化前尝试拿到单例端口的值 + pub fn get_singleton_port() -> u16 { + #[cfg(not(feature = "verge-dev"))] + const SERVER_PORT: u16 = 33331; + #[cfg(feature = "verge-dev")] + const SERVER_PORT: u16 = 11233; + + match dirs::verge_path().and_then(|path| help::read_yaml::(&path)) { + Ok(config) => config.app_singleton_port.unwrap_or(SERVER_PORT), + Err(_) => SERVER_PORT, // 这里就不log错误了 + } } } diff --git a/src-tauri/src/core/clash_api.rs b/src-tauri/src/core/clash_api.rs new file mode 100644 index 0000000..cb00db8 --- /dev/null +++ b/src-tauri/src/core/clash_api.rs @@ -0,0 +1,64 @@ +use crate::config::Config; +use anyhow::{bail, Result}; +use reqwest::header::HeaderMap; +use serde_yaml::Mapping; +use std::collections::HashMap; + +/// PUT /configs +/// path 是绝对路径 +pub async fn put_configs(path: &str) -> Result<()> { + let (url, headers) = clash_client_info()?; + let url = format!("{url}/configs"); + + let mut data = HashMap::new(); + data.insert("path", path); + + let client = reqwest::ClientBuilder::new().no_proxy().build()?; + let builder = client.put(&url).headers(headers).json(&data); + let response = builder.send().await?; + + match response.status().as_u16() { + 204 => Ok(()), + status @ _ => { + bail!("failed to put configs with status \"{status}\"") + } + } +} + +/// PATCH /configs +pub async fn patch_configs(config: &Mapping) -> Result<()> { + let (url, headers) = clash_client_info()?; + let url = format!("{url}/configs"); + + let client = reqwest::ClientBuilder::new().no_proxy().build()?; + let builder = client.patch(&url).headers(headers.clone()).json(config); + builder.send().await?; + Ok(()) +} + +/// 根据clash info获取clash服务地址和请求头 +fn clash_client_info() -> Result<(String, HeaderMap)> { + let info = { Config::clash().data().get_info()? }; + + if info.server.is_none() { + let status = &info.status; + if info.port.is_none() { + bail!("failed to parse config.yaml file with status {status}"); + } else { + bail!("failed to parse the server with status {status}"); + } + } + + let server = info.server.unwrap(); + let server = format!("http://{server}"); + + let mut headers = HeaderMap::new(); + headers.insert("Content-Type", "application/json".parse()?); + + if let Some(secret) = info.secret.as_ref() { + let secret = format!("Bearer {}", secret.clone()).parse()?; + headers.insert("Authorization", secret); + } + + Ok((server, headers)) +} diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs new file mode 100644 index 0000000..b45ddf3 --- /dev/null +++ b/src-tauri/src/core/core.rs @@ -0,0 +1,246 @@ +use super::{clash_api, logger::Logger}; +use crate::log_err; +use crate::{config::*, utils::dirs}; +use anyhow::{bail, Context, Result}; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::{fs, io::Write, sync::Arc, time::Duration}; +use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; +use tauri::api::process::{Command, CommandChild, CommandEvent}; +use tokio::time::sleep; + +#[derive(Debug)] +pub struct CoreManager { + sidecar: Arc>>, + + #[allow(unused)] + use_service_mode: Arc>, +} + +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)), + }) + } + + 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(); + system.process(Pid::from_u32(pid)).map(|proc| { + if proc.name().contains("clash") { + proc.kill(); + } + }); + }); + + tauri::async_runtime::spawn(async { + // 启动clash + log_err!(Self::global().run_core().await); + }); + + Ok(()) + } + + /// 检查配置是否正确 + pub fn check_config(&self) -> Result<()> { + let config_path = Config::generate_file(ConfigType::Check)?; + let config_path = dirs::path_to_str(&config_path)?; + + let clash_core = { Config::verge().latest().clash_core.clone() }; + let clash_core = clash_core.unwrap_or("clash".into()); + + let output = Command::new_sidecar(clash_core)? + .args(["-t", "-f", config_path]) + .output()?; + + if !output.status.success() { + Logger::global().set_log(output.stdout.clone()); + bail!("{}", output.stdout); // 过滤掉终端颜色值 + } + + Ok(()) + } + + /// 启动核心 + pub async fn run_core(&self) -> Result<()> { + let config_path = Config::generate_file(ConfigType::Run)?; + + #[cfg(target_os = "windows")] + { + use super::win_service; + + // 服务模式 + let enable = { + let enable = Config::verge().data().enable_service_mode.clone(); + enable.unwrap_or(false) + }; + + *self.use_service_mode.lock() = enable; + + if enable { + // 服务模式启动失败就直接运行sidecar + match { + win_service::check_service().await?; + win_service::run_core_by_service().await + } { + Ok(_) => return Ok(()), + Err(err) => { + // 修改这个值,免得stop出错 + *self.use_service_mode.lock() = false; + + log::error!(target: "app", "{err}"); + } + } + } + } + + let mut sidecar = self.sidecar.lock(); + + if let Some(child) = sidecar.take() { + let _ = child.kill(); + } + + let app_dir = dirs::app_home_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("clash".into()); + + let config_path = dirs::path_to_str(&config_path)?; + + // fix #212 + let args = match clash_core.as_str() { + "clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path], + _ => 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(()) + }); + + *sidecar = Some(cmd_child); + + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => { + let can_short = line.starts_with("time=") && line.len() > 33; + let stdout = if can_short { &line[33..] } else { &line }; + log::info!(target: "app" ,"[clash]: {}", stdout); + Logger::global().set_log(line); + } + CommandEvent::Stderr(err) => { + 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"); + break; + } + _ => {} + } + } + }); + + Ok(()) + } + + /// 停止核心运行 + pub fn stop_core(&self) -> Result<()> { + #[cfg(target_os = "windows")] + if *self.use_service_mode.lock() { + tauri::async_runtime::block_on(async move { + log_err!(super::win_service::stop_core_by_service().await); + }); + return Ok(()); + } + + let mut sidecar = self.sidecar.lock(); + if let Some(child) = sidecar.take() { + let _ = child.kill(); + } + Ok(()) + } + + /// 切换核心 + 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" { + bail!("invalid clash core name \"{clash_core}\""); + } + + Config::verge().draft().clash_core = Some(clash_core); + + // 清掉旧日志 + Logger::global().clear_log(); + + match self.run_core().await { + Ok(_) => { + Config::verge().apply(); + Config::runtime().apply(); + log_err!(Config::verge().latest().save_file()); + Ok(()) + } + Err(err) => { + Config::verge().discard(); + Config::runtime().discard(); + Err(err) + } + } + } + + /// 更新proxies那些 + /// 如果涉及端口和外部控制则需要重启 + pub async fn update_config(&self) -> Result<()> { + // 更新配置 + Config::generate()?; + + // 检查配置是否正常 + self.check_config()?; + + // 更新运行时配置 + let path = Config::generate_file(ConfigType::Run)?; + let path = dirs::path_to_str(&path)?; + + // 发送请求 发送5次 + for i in 0..5 { + match clash_api::put_configs(path).await { + Ok(_) => break, + Err(err) => { + if i < 4 { + log::error!(target: "app", "{err}"); + } else { + bail!(err); + } + } + } + sleep(Duration::from_millis(250)).await; + } + + Ok(()) + } +} diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index dd04a48..5b46cea 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -1,65 +1,77 @@ use super::tray::Tray; -use crate::log_if_err; +use crate::log_err; use anyhow::{bail, Result}; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::sync::Arc; use tauri::{AppHandle, Manager, Window}; #[derive(Debug, Default, Clone)] pub struct Handle { - pub app_handle: Option, + pub app_handle: Arc>>, } impl Handle { - pub fn set_inner(&mut self, app_handle: AppHandle) { - self.app_handle = Some(app_handle); + pub fn global() -> &'static Handle { + static HANDLE: OnceCell = OnceCell::new(); + + HANDLE.get_or_init(|| Handle { + app_handle: Arc::new(Mutex::new(None)), + }) + } + + pub fn init(&self, app_handle: AppHandle) { + *self.app_handle.lock() = Some(app_handle); } pub fn get_window(&self) -> Option { self.app_handle + .lock() .as_ref() .map_or(None, |a| a.get_window("main")) } - pub fn refresh_clash(&self) { - if let Some(window) = self.get_window() { - log_if_err!(window.emit("verge://refresh-clash-config", "yes")); + pub fn refresh_clash() { + if let Some(window) = Self::global().get_window() { + log_err!(window.emit("verge://refresh-clash-config", "yes")); } } - pub fn refresh_verge(&self) { - if let Some(window) = self.get_window() { - log_if_err!(window.emit("verge://refresh-verge-config", "yes")); + pub fn refresh_verge() { + if let Some(window) = Self::global().get_window() { + log_err!(window.emit("verge://refresh-verge-config", "yes")); } } #[allow(unused)] - pub fn refresh_profiles(&self) { - if let Some(window) = self.get_window() { - log_if_err!(window.emit("verge://refresh-profiles-config", "yes")); + pub fn refresh_profiles() { + if let Some(window) = Self::global().get_window() { + log_err!(window.emit("verge://refresh-profiles-config", "yes")); } } - pub fn notice_message(&self, status: String, msg: String) { - if let Some(window) = self.get_window() { - log_if_err!(window.emit("verge://notice-message", (status, msg))); + pub fn notice_message, M: Into>(status: S, msg: M) { + if let Some(window) = Self::global().get_window() { + log_err!(window.emit("verge://notice-message", (status.into(), msg.into()))); } } - pub fn update_systray(&self) -> Result<()> { - if self.app_handle.is_none() { - bail!("update_systray unhandle error"); + pub fn update_systray() -> Result<()> { + let app_handle = Self::global().app_handle.lock(); + if app_handle.is_none() { + bail!("update_systray unhandled error"); } - let app_handle = self.app_handle.as_ref().unwrap(); - Tray::update_systray(app_handle)?; + Tray::update_systray(app_handle.as_ref().unwrap())?; Ok(()) } /// update the system tray state - pub fn update_systray_part(&self) -> Result<()> { - if self.app_handle.is_none() { - bail!("update_systray unhandle error"); + pub fn update_systray_part() -> Result<()> { + let app_handle = Self::global().app_handle.lock(); + if app_handle.is_none() { + bail!("update_systray unhandled error"); } - let app_handle = self.app_handle.as_ref().unwrap(); - Tray::update_part(app_handle)?; + Tray::update_part(app_handle.as_ref().unwrap())?; Ok(()) } } diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 4ce4847..38565bc 100644 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -1,52 +1,58 @@ -use crate::{data::*, feat, log_if_err}; +use crate::{config::Config, feat, log_err}; use anyhow::{bail, Result}; -use std::collections::HashMap; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::{collections::HashMap, sync::Arc}; use tauri::{AppHandle, GlobalShortcutManager}; pub struct Hotkey { - current: Vec, // 保存当前的热键设置 - manager: Option, + current: Arc>>, // 保存当前的热键设置 + + app_handle: Arc>>, } impl Hotkey { - pub fn new() -> Hotkey { - Hotkey { - current: Vec::new(), - manager: None, - } + pub fn global() -> &'static Hotkey { + static HOTKEY: OnceCell = OnceCell::new(); + + HOTKEY.get_or_init(|| Hotkey { + current: Arc::new(Mutex::new(Vec::new())), + app_handle: Arc::new(Mutex::new(None)), + }) } - pub fn init(&mut self, app_handle: AppHandle) -> Result<()> { - self.manager = Some(app_handle); - let data = Data::global(); - let verge = data.verge.lock(); + pub fn init(&self, app_handle: AppHandle) -> Result<()> { + *self.app_handle.lock() = Some(app_handle); - if let Some(hotkeys) = verge.hotkeys.as_ref() { + let verge = Config::verge(); + + if let Some(hotkeys) = verge.latest().hotkeys.as_ref() { for hotkey in hotkeys.iter() { let mut iter = hotkey.split(','); let func = iter.next(); let key = iter.next(); if func.is_some() && key.is_some() { - log_if_err!(self.register(key.unwrap(), func.unwrap())); + log_err!(self.register(key.unwrap(), func.unwrap())); } else { log::error!(target: "app", "invalid hotkey \"{}\":\"{}\"", key.unwrap_or("None"), func.unwrap_or("None")); } } - self.current = hotkeys.clone(); + *self.current.lock() = hotkeys.clone(); } Ok(()) } fn get_manager(&self) -> Result { - if self.manager.is_none() { + let app_handle = self.app_handle.lock(); + if app_handle.is_none() { bail!("failed to get hotkey manager"); } - Ok(self.manager.as_ref().unwrap().global_shortcut_manager()) + Ok(app_handle.as_ref().unwrap().global_shortcut_manager()) } - fn register(&mut self, hotkey: &str, func: &str) -> Result<()> { + fn register(&self, hotkey: &str, func: &str) -> Result<()> { let mut manager = self.get_manager()?; if manager.is_registered(hotkey)? { @@ -54,10 +60,10 @@ impl Hotkey { } let f = match func.trim() { - "clash_mode_rule" => || feat::change_clash_mode("rule"), - "clash_mode_global" => || feat::change_clash_mode("global"), - "clash_mode_direct" => || feat::change_clash_mode("direct"), - "clash_mode_script" => || feat::change_clash_mode("script"), + "clash_mode_rule" => || feat::change_clash_mode("rule".into()), + "clash_mode_global" => || feat::change_clash_mode("global".into()), + "clash_mode_direct" => || feat::change_clash_mode("direct".into()), + "clash_mode_script" => || feat::change_clash_mode("script".into()), "toggle_system_proxy" => || feat::toggle_system_proxy(), "enable_system_proxy" => || feat::enable_system_proxy(), "disable_system_proxy" => || feat::disable_system_proxy(), @@ -73,14 +79,14 @@ impl Hotkey { Ok(()) } - fn unregister(&mut self, hotkey: &str) -> Result<()> { + fn unregister(&self, hotkey: &str) -> Result<()> { self.get_manager()?.unregister(&hotkey)?; log::info!(target: "app", "unregister hotkey {hotkey}"); Ok(()) } - pub fn update(&mut self, new_hotkeys: Vec) -> Result<()> { - let current = self.current.to_owned(); + pub fn update(&self, new_hotkeys: Vec) -> Result<()> { + let mut current = self.current.lock(); let old_map = Self::get_map_from_vec(¤t); let new_map = Self::get_map_from_vec(&new_hotkeys); @@ -91,10 +97,10 @@ impl Hotkey { }); add.iter().for_each(|(key, func)| { - log_if_err!(self.register(key, func)); + log_err!(self.register(key, func)); }); - self.current = new_hotkeys; + *current = new_hotkeys; Ok(()) } diff --git a/src-tauri/src/core/logger.rs b/src-tauri/src/core/logger.rs new file mode 100644 index 0000000..b426415 --- /dev/null +++ b/src-tauri/src/core/logger.rs @@ -0,0 +1,36 @@ +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use std::{collections::VecDeque, sync::Arc}; + +const LOGS_QUEUE_LEN: usize = 100; + +pub struct Logger { + log_data: Arc>>, +} + +impl Logger { + pub fn global() -> &'static Logger { + static LOGGER: OnceCell = OnceCell::new(); + + LOGGER.get_or_init(|| Logger { + log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))), + }) + } + + pub fn get_log(&self) -> VecDeque { + self.log_data.lock().clone() + } + + pub fn set_log(&self, text: String) { + let mut logs = self.log_data.lock(); + if logs.len() > LOGS_QUEUE_LEN { + logs.pop_front(); + } + logs.push_back(text); + } + + pub fn clear_log(&self) { + let mut logs = self.log_data.lock(); + logs.clear(); + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index c91c609..18655ab 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,349 +1,11 @@ -use self::handle::Handle; -use self::hotkey::Hotkey; -use self::sysopt::Sysopt; -use self::timer::Timer; -use crate::config::enhance_config; -use crate::data::*; -use crate::log_if_err; -use anyhow::{bail, Result}; -use once_cell::sync::OnceCell; -use parking_lot::Mutex; -use serde_yaml::{Mapping, Value}; -use std::sync::Arc; - -mod handle; -mod hotkey; -mod service; -mod sysopt; -mod timer; +pub mod clash_api; +mod core; +pub mod handle; +pub mod hotkey; +pub mod logger; +pub mod sysopt; +pub mod timer; pub mod tray; +pub mod win_service; -pub use self::service::*; - -#[derive(Clone)] -pub struct Core { - pub service: Arc>, - pub sysopt: Arc>, - pub timer: Arc>, - pub hotkey: Arc>, - pub runtime: Arc>, - pub handle: Arc>, -} - -impl Core { - pub fn global() -> &'static Core { - static CORE: OnceCell = OnceCell::new(); - - CORE.get_or_init(|| Core { - service: Arc::new(Mutex::new(Service::new())), - sysopt: Arc::new(Mutex::new(Sysopt::new())), - timer: Arc::new(Mutex::new(Timer::new())), - hotkey: Arc::new(Mutex::new(Hotkey::new())), - runtime: Arc::new(Mutex::new(RuntimeResult::default())), - handle: Arc::new(Mutex::new(Handle::default())), - }) - } - - /// initialize the core state - pub fn init(&self, app_handle: tauri::AppHandle) { - // kill old clash process - Service::kill_old_clash(); - - let mut handle = self.handle.lock(); - handle.set_inner(app_handle.clone()); - drop(handle); - - let mut service = self.service.lock(); - log_if_err!(service.start()); - drop(service); - - log_if_err!(self.activate()); - - let mut sysopt = self.sysopt.lock(); - log_if_err!(sysopt.init_launch()); - log_if_err!(sysopt.init_sysproxy()); - drop(sysopt); - - let handle = self.handle.lock(); - log_if_err!(handle.update_systray_part()); - drop(handle); - - let mut hotkey = self.hotkey.lock(); - log_if_err!(hotkey.init(app_handle)); - drop(hotkey); - - // timer initialize - let mut timer = self.timer.lock(); - log_if_err!(timer.restore()); - } - - /// restart the clash sidecar - pub fn restart_clash(&self) -> Result<()> { - let mut service = self.service.lock(); - service.restart()?; - drop(service); - self.activate() - } - - /// change the clash core - pub fn change_core(&self, clash_core: Option) -> Result<()> { - let clash_core = clash_core.unwrap_or("clash".into()); - - if &clash_core != "clash" && &clash_core != "clash-meta" { - bail!("invalid clash core name \"{clash_core}\""); - } - - let global = Data::global(); - let mut verge = global.verge.lock(); - verge.patch_config(Verge { - clash_core: Some(clash_core.clone()), - ..Verge::default() - })?; - drop(verge); - - let mut service = self.service.lock(); - service.clear_logs(); - service.restart()?; - drop(service); - - self.activate() - } - - /// Patch Clash - /// handle the clash config changed - pub fn patch_clash(&self, patch: Mapping) -> Result<()> { - let patch_cloned = patch.clone(); - let clash_mode = patch.get("mode"); - let mixed_port = patch.get("mixed-port"); - let external = patch.get("external-controller"); - let secret = patch.get("secret"); - - let valid_port = { - let global = Data::global(); - let mut clash = global.clash.lock(); - clash.patch_config(patch_cloned)?; - clash.info.port.is_some() - }; - - // todo: port check - if (mixed_port.is_some() && valid_port) || external.is_some() || secret.is_some() { - let mut service = self.service.lock(); - service.restart()?; - drop(service); - - self.activate()?; - - let mut sysopt = self.sysopt.lock(); - sysopt.init_sysproxy()?; - } - - if clash_mode.is_some() { - let handle = self.handle.lock(); - handle.update_systray_part()?; - } - - Ok(()) - } - - /// Patch Verge - pub fn patch_verge(&self, patch: Verge) -> Result<()> { - // save the patch - let global = Data::global(); - let mut verge = global.verge.lock(); - verge.patch_config(patch.clone())?; - drop(verge); - - let tun_mode = patch.enable_tun_mode; - let auto_launch = patch.enable_auto_launch; - let system_proxy = patch.enable_system_proxy; - let proxy_bypass = patch.system_proxy_bypass; - let proxy_guard = patch.enable_proxy_guard; - let language = patch.language; - - #[cfg(target_os = "windows")] - { - let service_mode = patch.enable_service_mode; - - // 重启服务 - if service_mode.is_some() { - let mut service = self.service.lock(); - service.restart()?; - drop(service); - } - - if tun_mode.is_some() && *tun_mode.as_ref().unwrap_or(&false) { - let wintun_dll = crate::utils::dirs::app_home_dir().join("wintun.dll"); - if !wintun_dll.exists() { - bail!("failed to enable TUN for missing `wintun.dll`"); - } - } - - if service_mode.is_some() || tun_mode.is_some() { - self.activate()?; - } - } - - #[cfg(not(target_os = "windows"))] - if tun_mode.is_some() { - self.activate()?; - } - - let mut sysopt = self.sysopt.lock(); - - if auto_launch.is_some() { - sysopt.update_launch()?; - } - if system_proxy.is_some() || proxy_bypass.is_some() { - sysopt.update_sysproxy()?; - sysopt.guard_proxy(); - } - if proxy_guard.unwrap_or(false) { - sysopt.guard_proxy(); - } - - // 更新tray - if language.is_some() { - let handle = self.handle.lock(); - handle.update_systray()?; - } else if system_proxy.is_some() || tun_mode.is_some() { - let handle = self.handle.lock(); - handle.update_systray_part()?; - } - - if patch.hotkeys.is_some() { - let mut hotkey = self.hotkey.lock(); - hotkey.update(patch.hotkeys.unwrap())?; - } - - Ok(()) - } - - // update rule/global/direct/script mode - pub fn update_mode(&self, mode: &str) -> Result<()> { - // save config to file - let info = { - let global = Data::global(); - let mut clash = global.clash.lock(); - clash.config.insert(Value::from("mode"), Value::from(mode)); - clash.save_config()?; - clash.info.clone() - }; - - let mut mapping = Mapping::new(); - mapping.insert(Value::from("mode"), Value::from(mode)); - - let handle = self.handle.clone(); - - tauri::async_runtime::spawn(async move { - log_if_err!(Service::patch_config(info, mapping.to_owned()).await); - - // update tray - let handle = handle.lock(); - handle.refresh_clash(); - log_if_err!(handle.update_systray_part()); - }); - - Ok(()) - } - - /// activate the profile - /// auto activate enhanced profile - /// 触发clash配置更新 - pub fn activate(&self) -> Result<()> { - let global = Data::global(); - - let verge = global.verge.lock(); - let clash = global.clash.lock(); - let profiles = global.profiles.lock(); - - let tun_mode = verge.enable_tun_mode.clone().unwrap_or(false); - let profile_activate = profiles.gen_activate()?; - - let clash_config = clash.config.clone(); - let clash_info = clash.info.clone(); - - drop(clash); - drop(verge); - drop(profiles); - - let (config, exists_keys, logs) = enhance_config( - clash_config, - profile_activate.current, - profile_activate.chain, - profile_activate.valid, - tun_mode, - ); - - let mut runtime = self.runtime.lock(); - *runtime = RuntimeResult { - config: Some(config.clone()), - config_yaml: Some(serde_yaml::to_string(&config).unwrap_or("".into())), - exists_keys, - chain_logs: logs, - }; - drop(runtime); - - let mut service = self.service.lock(); - service.check_start()?; - drop(service); - - let handle = self.handle.clone(); - tauri::async_runtime::spawn(async move { - match Service::set_config(clash_info, config).await { - Ok(_) => { - let handle = handle.lock(); - handle.refresh_clash(); - handle.notice_message("set_config::ok".into(), "ok".into()); - } - Err(err) => { - let handle = handle.lock(); - handle.notice_message("set_config::error".into(), format!("{err}")); - log::error!(target: "app", "{err}") - } - } - }); - - Ok(()) - } - - /// Static function - /// update profile item - pub async fn update_profile_item(&self, uid: String, option: Option) -> Result<()> { - let global = Data::global(); - - let (url, opt) = { - let profiles = global.profiles.lock(); - let item = profiles.get_item(&uid)?; - - if let Some(typ) = item.itype.as_ref() { - // maybe only valid for `local` profile - if *typ != "remote" { - // reactivate the config - if Some(uid) == profiles.get_current() { - drop(profiles); - self.activate()?; - } - return Ok(()); - } - } - if item.url.is_none() { - bail!("failed to get the profile item url"); - } - (item.url.clone().unwrap(), item.option.clone()) - }; - - let merged_opt = PrfOption::merge(opt, option); - let item = PrfItem::from_url(&url, None, None, merged_opt).await?; - - let mut profiles = global.profiles.lock(); - profiles.update_item(uid.clone(), item)?; - - // reactivate the profile - if Some(uid) == profiles.get_current() { - drop(profiles); - self.activate()?; - } - - Ok(()) - } -} +pub use self::core::*; diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs deleted file mode 100644 index 21fb7ed..0000000 --- a/src-tauri/src/core/service.rs +++ /dev/null @@ -1,537 +0,0 @@ -use crate::data::{ClashInfo, Data}; -use crate::log_if_err; -use crate::utils::{config, dirs}; -use anyhow::{bail, Result}; -use parking_lot::RwLock; -use reqwest::header::HeaderMap; -use serde_yaml::Mapping; -use std::fs; -use std::io::Write; -use std::sync::Arc; -use std::{ - collections::{HashMap, VecDeque}, - time::Duration, -}; -use tauri::api::process::{Command, CommandChild, CommandEvent}; -use tokio::time::sleep; - -const LOGS_QUEUE_LEN: usize = 100; - -#[derive(Debug)] -pub struct Service { - sidecar: Option, - - logs: Arc>>, - - #[allow(unused)] - use_service_mode: bool, -} - -impl Service { - pub fn new() -> Service { - let queue = VecDeque::with_capacity(LOGS_QUEUE_LEN + 10); - - Service { - sidecar: None, - logs: Arc::new(RwLock::new(queue)), - use_service_mode: false, - } - } - - pub fn start(&mut self) -> Result<()> { - #[cfg(not(target_os = "windows"))] - self.start_clash_by_sidecar()?; - - #[cfg(target_os = "windows")] - { - let enable = { - let data = Data::global(); - let verge = data.verge.lock(); - verge.enable_service_mode.clone().unwrap_or(false) - }; - - self.use_service_mode = enable; - - if !enable { - return self.start_clash_by_sidecar(); - } - - tauri::async_runtime::spawn(async move { - match Self::check_service().await { - Ok(status) => { - // 未启动clash - if status.code != 0 { - log_if_err!(Self::start_clash_by_service().await); - } - } - Err(err) => log::error!(target: "app", "{err}"), - } - }); - } - - Ok(()) - } - - pub fn stop(&mut self) -> Result<()> { - #[cfg(not(target_os = "windows"))] - self.stop_clash_by_sidecar()?; - - #[cfg(target_os = "windows")] - { - let _ = self.stop_clash_by_sidecar(); - - if self.use_service_mode { - tauri::async_runtime::block_on(async move { - log_if_err!(Self::stop_clash_by_service().await); - }); - } - } - - Ok(()) - } - - pub fn restart(&mut self) -> Result<()> { - self.stop()?; - self.start() - } - - pub fn get_logs(&self) -> VecDeque { - self.logs.read().clone() - } - - #[allow(unused)] - pub fn set_logs(&self, text: String) { - let mut logs = self.logs.write(); - if logs.len() > LOGS_QUEUE_LEN { - (*logs).pop_front(); - } - (*logs).push_back(text); - } - - pub fn clear_logs(&self) { - let mut logs = self.logs.write(); - (*logs).clear(); - } - - /// start the clash sidecar - fn start_clash_by_sidecar(&mut self) -> Result<()> { - if self.sidecar.is_some() { - let sidecar = self.sidecar.take().unwrap(); - let _ = sidecar.kill(); - } - - let clash_core: String = { - let global = Data::global(); - let verge = global.verge.lock(); - verge.clash_core.clone().unwrap_or("clash".into()) - }; - - let app_dir = dirs::app_home_dir(); - let app_dir = app_dir.as_os_str().to_str().unwrap(); - - // fix #212 - let args = match clash_core.as_str() { - "clash-meta" => vec!["-m", "-d", app_dir], - _ => vec!["-d", app_dir], - }; - - let cmd = Command::new_sidecar(clash_core)?; - - let (mut rx, cmd_child) = cmd.args(args).spawn()?; - - // 将pid写入文件中 - let pid = cmd_child.pid(); - log_if_err!(|| -> Result<()> { - let path = dirs::clash_pid_path(); - fs::File::create(path)?.write(format!("{pid}").as_bytes())?; - Ok(()) - }()); - - self.sidecar = Some(cmd_child); - - // clash log - let logs = self.logs.clone(); - tauri::async_runtime::spawn(async move { - let write_log = |text: String| { - let mut logs = logs.write(); - if logs.len() >= LOGS_QUEUE_LEN { - (*logs).pop_front(); - } - (*logs).push_back(text); - }; - - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Stdout(line) => { - let can_short = line.starts_with("time=") && line.len() > 33; - let stdout = if can_short { &line[33..] } else { &line }; - log::info!(target: "app" ,"[clash]: {}", stdout); - write_log(line); - } - CommandEvent::Stderr(err) => { - log::error!(target: "app" ,"[clash error]: {}", err); - write_log(err); - } - CommandEvent::Error(err) => log::error!(target: "app" ,"{err}"), - CommandEvent::Terminated(_) => break, - _ => {} - } - } - }); - - Ok(()) - } - - /// stop the clash sidecar - fn stop_clash_by_sidecar(&mut self) -> Result<()> { - if let Some(sidecar) = self.sidecar.take() { - sidecar.kill()?; - } - Ok(()) - } - - pub fn check_start(&mut self) -> Result<()> { - #[cfg(target_os = "windows")] - { - let global = Data::global(); - let verge = global.verge.lock(); - let service_mode = verge.enable_service_mode.unwrap_or(false); - - if !service_mode && self.sidecar.is_none() { - self.start()?; - } - } - - #[cfg(not(target_os = "windows"))] - if self.sidecar.is_none() { - self.start()?; - } - - Ok(()) - } - - /// update clash config - /// using PUT methods - pub async fn set_config(info: ClashInfo, config: Mapping) -> Result<()> { - let temp_path = dirs::profiles_temp_path(); - config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?; - - let (server, headers) = Self::clash_client_info(info)?; - - let mut data = HashMap::new(); - data.insert("path", temp_path.as_os_str().to_str().unwrap()); - - macro_rules! report_err { - ($i: expr, $e: expr) => { - match $i { - 4 => bail!($e), - _ => log::error!(target: "app", $e), - } - }; - } - - // retry 5 times - for i in 0..5 { - let headers = headers.clone(); - match reqwest::ClientBuilder::new().no_proxy().build() { - Ok(client) => { - let builder = client.put(&server).headers(headers).json(&data); - match builder.send().await { - Ok(resp) => match resp.status().as_u16() { - 204 => break, - // 配置有问题不重试 - 400 => bail!("failed to update clash config with status 400"), - status @ _ => { - report_err!(i, "failed to activate clash with status \"{status}\"") - } - }, - Err(err) => report_err!(i, "{err}"), - } - } - Err(err) => report_err!(i, "{err}"), - } - sleep(Duration::from_millis(500)).await; - } - - Ok(()) - } - - /// patch clash config - pub async fn patch_config(info: ClashInfo, config: Mapping) -> Result<()> { - let (server, headers) = Self::clash_client_info(info)?; - - let client = reqwest::ClientBuilder::new().no_proxy().build()?; - let builder = client.patch(&server).headers(headers.clone()).json(&config); - builder.send().await?; - Ok(()) - } - - /// get clash client url and headers from clash info - fn clash_client_info(info: ClashInfo) -> Result<(String, HeaderMap)> { - if info.server.is_none() { - let status = &info.status; - if info.port.is_none() { - bail!("failed to parse config.yaml file with status {status}"); - } else { - bail!("failed to parse the server with status {status}"); - } - } - - let server = info.server.unwrap(); - let server = format!("http://{server}/configs"); - - let mut headers = HeaderMap::new(); - headers.insert("Content-Type", "application/json".parse().unwrap()); - - if let Some(secret) = info.secret.as_ref() { - let secret = format!("Bearer {}", secret.clone()).parse().unwrap(); - headers.insert("Authorization", secret); - } - - Ok((server, headers)) - } - - /// kill old clash process - pub fn kill_old_clash() { - use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt}; - - if let Ok(pid) = fs::read(dirs::clash_pid_path()) { - if let Ok(pid) = String::from_utf8_lossy(&pid).parse() { - let mut system = System::new(); - system.refresh_all(); - - let proc = system.process(Pid::from_u32(pid)); - if let Some(proc) = proc { - proc.kill(); - } - } - } - } -} - -impl Drop for Service { - fn drop(&mut self) { - log_if_err!(self.stop()); - } -} - -/// ### Service Mode -/// -#[cfg(target_os = "windows")] -pub mod win_service { - use super::*; - use anyhow::Context; - use deelevate::{PrivilegeLevel, Token}; - use runas::Command as RunasCommand; - use serde::{Deserialize, Serialize}; - use std::os::windows::process::CommandExt; - use std::{env::current_exe, process::Command as StdCommand}; - - const SERVICE_NAME: &str = "clash_verge_service"; - - const SERVICE_URL: &str = "http://127.0.0.1:33211"; - - #[derive(Debug, Deserialize, Serialize, Clone)] - pub struct ResponseBody { - pub bin_path: String, - pub config_dir: String, - pub log_file: String, - } - - #[derive(Debug, Deserialize, Serialize, Clone)] - pub struct JsonResponse { - pub code: u64, - pub msg: String, - pub data: Option, - } - - impl Service { - /// Install the Clash Verge Service - /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 - pub async fn install_service() -> Result<()> { - let binary_path = dirs::service_path(); - let install_path = binary_path.with_file_name("install-service.exe"); - - if !install_path.exists() { - bail!("installer exe not found"); - } - - let token = Token::with_current_process()?; - let level = token.privilege_level()?; - - let status = match level { - PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).status()?, - _ => StdCommand::new(install_path) - .creation_flags(0x08000000) - .status()?, - }; - - if !status.success() { - bail!( - "failed to install service with status {}", - status.code().unwrap() - ); - } - - Ok(()) - } - - /// Uninstall the Clash Verge Service - /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 - pub async fn uninstall_service() -> Result<()> { - let binary_path = dirs::service_path(); - let uninstall_path = binary_path.with_file_name("uninstall-service.exe"); - - if !uninstall_path.exists() { - bail!("uninstaller exe not found"); - } - - let token = Token::with_current_process()?; - let level = token.privilege_level()?; - - let status = match level { - PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).status()?, - _ => StdCommand::new(uninstall_path) - .creation_flags(0x08000000) - .status()?, - }; - - if !status.success() { - bail!( - "failed to uninstall service with status {}", - status.code().unwrap() - ); - } - - Ok(()) - } - - /// [deprecated] - /// start service - /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 - pub async fn start_service() -> Result<()> { - let token = Token::with_current_process()?; - let level = token.privilege_level()?; - - let args = ["start", SERVICE_NAME]; - - let status = match level { - PrivilegeLevel::NotPrivileged => RunasCommand::new("sc").args(&args).status()?, - _ => StdCommand::new("sc").args(&args).status()?, - }; - - match status.success() { - true => Ok(()), - false => bail!( - "failed to start service with status {}", - status.code().unwrap() - ), - } - } - - /// stop service - pub async fn stop_service() -> Result<()> { - let url = format!("{SERVICE_URL}/stop_service"); - let res = reqwest::ClientBuilder::new() - .no_proxy() - .build()? - .post(url) - .send() - .await? - .json::() - .await - .context("failed to connect to the Clash Verge Service")?; - - if res.code != 0 { - bail!(res.msg); - } - - Ok(()) - } - - /// check the windows service status - pub async fn check_service() -> Result { - let url = format!("{SERVICE_URL}/get_clash"); - let response = reqwest::ClientBuilder::new() - .no_proxy() - .build()? - .get(url) - .send() - .await? - .json::() - .await - .context("failed to connect to the Clash Verge Service")?; - - Ok(response) - } - - /// start the clash by service - pub(super) async fn start_clash_by_service() -> Result<()> { - let status = Self::check_service().await?; - - if status.code == 0 { - Self::stop_clash_by_service().await?; - sleep(Duration::from_secs(1)).await; - } - - let clash_core = { - let global = Data::global(); - let verge = global.verge.lock(); - verge.clash_core.clone().unwrap_or("clash".into()) - }; - - let clash_bin = format!("{clash_core}.exe"); - let bin_path = current_exe().unwrap().with_file_name(clash_bin); - let bin_path = bin_path.as_os_str().to_str().unwrap(); - - let config_dir = dirs::app_home_dir(); - let config_dir = config_dir.as_os_str().to_str().unwrap(); - - let log_path = dirs::service_log_file(); - let log_path = log_path.as_os_str().to_str().unwrap(); - - let mut map = HashMap::new(); - map.insert("bin_path", bin_path); - map.insert("config_dir", config_dir); - map.insert("log_file", log_path); - - let url = format!("{SERVICE_URL}/start_clash"); - let res = reqwest::ClientBuilder::new() - .no_proxy() - .build()? - .post(url) - .json(&map) - .send() - .await? - .json::() - .await - .context("failed to connect to the Clash Verge Service")?; - - if res.code != 0 { - bail!(res.msg); - } - - Ok(()) - } - - /// stop the clash by service - pub(super) async fn stop_clash_by_service() -> Result<()> { - let url = format!("{SERVICE_URL}/stop_clash"); - let res = reqwest::ClientBuilder::new() - .no_proxy() - .build()? - .post(url) - .send() - .await? - .json::() - .await - .context("failed to connect to the Clash Verge Service")?; - - if res.code != 0 { - bail!(res.msg); - } - - Ok(()) - } - } -} diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 53bf939..152ce58 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -1,23 +1,25 @@ -use crate::{data::*, log_if_err}; +use crate::{config::Config, log_err}; use anyhow::{anyhow, bail, Result}; use auto_launch::{AutoLaunch, AutoLaunchBuilder}; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; use std::sync::Arc; use sysproxy::Sysproxy; -use tauri::{async_runtime::Mutex, utils::platform::current_exe}; +use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe}; pub struct Sysopt { /// current system proxy setting - cur_sysproxy: Option, + cur_sysproxy: Arc>>, /// record the original system proxy /// recover it when exit - old_sysproxy: Option, + old_sysproxy: Arc>>, /// helps to auto launch the app - auto_launch: Option, + auto_launch: Arc>>, /// record whether the guard async is running or not - guard_state: Arc>, + guard_state: Arc>, } #[cfg(target_os = "windows")] @@ -28,44 +30,49 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1"; static DEFAULT_BYPASS: &str = "127.0.0.1,localhost,"; impl Sysopt { - pub fn new() -> Sysopt { - Sysopt { - cur_sysproxy: None, - old_sysproxy: None, - auto_launch: None, - guard_state: Arc::new(Mutex::new(false)), - } + pub fn global() -> &'static Sysopt { + static SYSOPT: OnceCell = OnceCell::new(); + + SYSOPT.get_or_init(|| Sysopt { + cur_sysproxy: Arc::new(Mutex::new(None)), + old_sysproxy: Arc::new(Mutex::new(None)), + auto_launch: Arc::new(Mutex::new(None)), + guard_state: Arc::new(TokioMutex::new(false)), + }) } /// init the sysproxy - pub fn init_sysproxy(&mut self) -> Result<()> { - let data = Data::global(); - let clash = data.clash.lock(); - let port = clash.info.port.clone(); + pub fn init_sysproxy(&self) -> Result<()> { + let port = { Config::clash().latest().get_info()?.port }; if port.is_none() { bail!("clash port is none"); } - let verge = data.verge.lock(); - - let enable = verge.enable_system_proxy.clone().unwrap_or(false); - let bypass = verge.system_proxy_bypass.clone(); - let bypass = bypass.unwrap_or(DEFAULT_BYPASS.into()); - let port = port.unwrap().parse::()?; - let host = String::from("127.0.0.1"); - self.cur_sysproxy = Some(Sysproxy { + let (enable, bypass) = { + let verge = Config::verge(); + let verge = verge.latest(); + ( + verge.enable_system_proxy.clone().unwrap_or(false), + verge.system_proxy_bypass.clone(), + ) + }; + + let current = Sysproxy { enable, - host, + host: String::from("127.0.0.1"), port, - bypass, - }); + bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()), + }; if enable { - self.old_sysproxy = Sysproxy::get_system_proxy().map_or(None, |p| Some(p)); - self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?; + let old = Sysproxy::get_system_proxy().map_or(None, |p| Some(p)); + current.set_system_proxy()?; + + *self.old_sysproxy.lock() = old; + *self.cur_sysproxy.lock() = Some(current); } // run the system proxy guard @@ -74,37 +81,46 @@ impl Sysopt { } /// update the system proxy - pub fn update_sysproxy(&mut self) -> Result<()> { - if self.cur_sysproxy.is_none() || self.old_sysproxy.is_none() { + pub fn update_sysproxy(&self) -> Result<()> { + let mut cur_sysproxy = self.cur_sysproxy.lock(); + let old_sysproxy = self.old_sysproxy.lock(); + + if cur_sysproxy.is_none() || old_sysproxy.is_none() { + drop(cur_sysproxy); + drop(old_sysproxy); return self.init_sysproxy(); } - let data = Data::global(); - let verge = data.verge.lock(); - - let enable = verge.enable_system_proxy.clone().unwrap_or(false); - let bypass = verge.system_proxy_bypass.clone(); - let bypass = bypass.unwrap_or(DEFAULT_BYPASS.into()); - - let mut sysproxy = self.cur_sysproxy.take().unwrap(); + let (enable, bypass) = { + let verge = Config::verge(); + let verge = verge.latest(); + ( + verge.enable_system_proxy.clone().unwrap_or(false), + verge.system_proxy_bypass.clone(), + ) + }; + let mut sysproxy = cur_sysproxy.take().unwrap(); sysproxy.enable = enable; - sysproxy.bypass = bypass; + sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into()); - self.cur_sysproxy = Some(sysproxy); - self.cur_sysproxy.as_ref().unwrap().set_system_proxy()?; + sysproxy.set_system_proxy()?; + *cur_sysproxy = Some(sysproxy); Ok(()) } /// reset the sysproxy - pub fn reset_sysproxy(&mut self) -> Result<()> { - let cur = self.cur_sysproxy.take(); + pub fn reset_sysproxy(&self) -> Result<()> { + let mut cur_sysproxy = self.cur_sysproxy.lock(); + let mut old_sysproxy = self.old_sysproxy.lock(); - if let Some(mut old) = self.old_sysproxy.take() { + let cur_sysproxy = cur_sysproxy.take(); + + if let Some(mut old) = old_sysproxy.take() { // 如果原代理和当前代理 端口一致,就disable关闭,否则就恢复原代理设置 // 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了 - let port_same = cur.map_or(true, |cur| old.port == cur.port); + let port_same = cur_sysproxy.map_or(true, |cur| old.port == cur.port); if old.enable && port_same { old.enable = false; @@ -114,7 +130,7 @@ impl Sysopt { } old.set_system_proxy()?; - } else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur { + } else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy { // 没有原代理,就按现在的代理设置disable即可 log::info!(target: "app", "reset proxy by disabling the current proxy"); cur.enable = false; @@ -127,10 +143,9 @@ impl Sysopt { } /// init the auto launch - pub fn init_launch(&mut self) -> Result<()> { - let data = Data::global(); - let verge = data.verge.lock(); - let enable = verge.enable_auto_launch.clone().unwrap_or(false); + pub fn init_launch(&self) -> Result<()> { + let enable = { Config::verge().latest().enable_auto_launch.clone() }; + let enable = enable.unwrap_or(false); let app_exe = current_exe()?; let app_exe = dunce::canonicalize(app_exe)?; @@ -167,51 +182,39 @@ impl Sysopt { .set_app_path(&app_path) .build()?; - self.auto_launch = Some(auto); - // 避免在开发时将自启动关了 #[cfg(feature = "verge-dev")] if !enable { return Ok(()); } - let auto = self.auto_launch.as_ref().unwrap(); - // macos每次启动都更新登录项,避免重复设置登录项 #[cfg(target_os = "macos")] - { - let _ = auto.disable(); - if enable { - auto.enable()?; - } - } + let _ = auto.disable(); - #[cfg(not(target_os = "macos"))] - { - match enable { - true => auto.enable()?, - false => auto.disable()?, - }; + if enable { + auto.enable()?; } + *self.auto_launch.lock() = Some(auto); Ok(()) } /// update the startup - pub fn update_launch(&mut self) -> Result<()> { - if self.auto_launch.is_none() { + pub fn update_launch(&self) -> Result<()> { + let auto_launch = self.auto_launch.lock(); + + if auto_launch.is_none() { + drop(auto_launch); return self.init_launch(); } - - let data = Data::global(); - let verge = data.verge.lock(); - let enable = verge.enable_auto_launch.clone().unwrap_or(false); - - let auto_launch = self.auto_launch.as_ref().unwrap(); + let enable = { Config::verge().latest().enable_auto_launch.clone() }; + let enable = enable.unwrap_or(false); + let auto_launch = auto_launch.as_ref().unwrap(); match enable { true => auto_launch.enable()?, - false => crate::log_if_err!(auto_launch.disable()), // 忽略关闭的错误 + false => log_err!(auto_launch.disable()), // 忽略关闭的错误 }; Ok(()) @@ -239,14 +242,16 @@ impl Sysopt { loop { sleep(Duration::from_secs(wait_secs)).await; - let global = Data::global(); - let verge = global.verge.lock(); - - let enable = verge.enable_system_proxy.clone().unwrap_or(false); - let guard = verge.enable_proxy_guard.clone().unwrap_or(false); - let guard_duration = verge.proxy_guard_duration.clone().unwrap_or(10); - let bypass = verge.system_proxy_bypass.clone(); - drop(verge); + let (enable, guard, guard_duration, bypass) = { + let verge = Config::verge(); + let verge = verge.latest(); + ( + verge.enable_system_proxy.clone().unwrap_or(false), + verge.enable_proxy_guard.clone().unwrap_or(false), + verge.proxy_guard_duration.clone().unwrap_or(10), + verge.system_proxy_bypass.clone(), + ) + }; // stop loop if !enable || !guard { @@ -256,30 +261,30 @@ impl Sysopt { // update duration wait_secs = guard_duration; - let clash = global.clash.lock(); - let port = clash.info.port.clone(); - let port = port.unwrap_or("".into()).parse::(); - drop(clash); - log::debug!(target: "app", "try to guard the system proxy"); - match port { - Ok(port) => { - let sysproxy = Sysproxy { - enable: true, - host: "127.0.0.1".into(), - port, - bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()), - }; + if let Ok(info) = { Config::clash().latest().get_info() } { + match info.port.unwrap_or("".into()).parse::() { + Ok(port) => { + let sysproxy = Sysproxy { + enable: true, + host: "127.0.0.1".into(), + port, + bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()), + }; - log_if_err!(sysproxy.set_system_proxy()); + log_err!(sysproxy.set_system_proxy()); + } + Err(_) => { + log::error!(target: "app", "failed to parse clash port in guard proxy") + } } - Err(_) => log::error!(target: "app", "failed to parse clash port"), } } let mut state = guard_state.lock().await; *state = false; + drop(state); }); } } diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index 4d9ccba..1b40f0f 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -1,50 +1,92 @@ -use super::Core; -use crate::utils::help::get_now; -use crate::{data::Data, log_if_err}; +use crate::config::Config; +use crate::feat; use anyhow::{Context, Result}; use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder}; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; use std::collections::HashMap; +use std::sync::Arc; type TaskID = u64; pub struct Timer { /// cron manager - delay_timer: DelayTimer, + delay_timer: Arc>, /// save the current state - timer_map: HashMap, + timer_map: Arc>>, /// increment id - timer_count: TaskID, + timer_count: Arc>, } impl Timer { - pub fn new() -> Self { - Timer { - delay_timer: DelayTimerBuilder::default().build(), - timer_map: HashMap::new(), - timer_count: 1, - } + pub fn global() -> &'static Timer { + static TIMER: OnceCell = OnceCell::new(); + + TIMER.get_or_init(|| Timer { + delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())), + timer_map: Arc::new(Mutex::new(HashMap::new())), + timer_count: Arc::new(Mutex::new(1)), + }) + } + + /// restore timer + pub fn init(&self) -> Result<()> { + self.refresh()?; + + let cur_timestamp = chrono::Local::now().timestamp(); + + let timer_map = self.timer_map.lock(); + let delay_timer = self.delay_timer.lock(); + + Config::profiles().latest().get_items().map(|items| { + items + .iter() + .filter_map(|item| { + // mins to seconds + let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60; + let updated = item.updated? as i64; + + if interval > 0 && cur_timestamp - updated >= interval { + Some(item) + } else { + None + } + }) + .for_each(|item| { + if let Some(uid) = item.uid.as_ref() { + if let Some((task_id, _)) = timer_map.get(uid) { + crate::log_err!(delay_timer.advance_task(*task_id)); + } + } + }) + }); + + Ok(()) } /// Correctly update all cron tasks - pub fn refresh(&mut self) -> Result<()> { + pub fn refresh(&self) -> Result<()> { let diff_map = self.gen_diff(); + let mut timer_map = self.timer_map.lock(); + let mut delay_timer = self.delay_timer.lock(); + for (uid, diff) in diff_map.into_iter() { match diff { DiffFlag::Del(tid) => { - let _ = self.timer_map.remove(&uid); - log_if_err!(self.delay_timer.remove_task(tid)); + let _ = timer_map.remove(&uid); + crate::log_err!(delay_timer.remove_task(tid)); } DiffFlag::Add(tid, val) => { - let _ = self.timer_map.insert(uid.clone(), (tid, val)); - log_if_err!(self.add_task(uid, tid, val)); + let _ = timer_map.insert(uid.clone(), (tid, val)); + crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val)); } DiffFlag::Mod(tid, val) => { - let _ = self.timer_map.insert(uid.clone(), (tid, val)); - log_if_err!(self.delay_timer.remove_task(tid)); - log_if_err!(self.add_task(uid, tid, val)); + let _ = timer_map.insert(uid.clone(), (tid, val)); + crate::log_err!(delay_timer.remove_task(tid)); + crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val)); } } } @@ -52,45 +94,11 @@ impl Timer { Ok(()) } - /// restore timer - pub fn restore(&mut self) -> Result<()> { - self.refresh()?; - - let cur_timestamp = get_now(); // seconds - - let global = Data::global(); - let profiles = global.profiles.lock(); - - profiles - .get_items() - .unwrap_or(&vec![]) - .iter() - .filter(|item| item.uid.is_some() && item.updated.is_some() && item.option.is_some()) - .filter(|item| { - // mins to seconds - let interval = - item.option.as_ref().unwrap().update_interval.unwrap_or(0) as usize * 60; - let updated = item.updated.unwrap(); - return interval > 0 && cur_timestamp - updated >= interval; - }) - .for_each(|item| { - let uid = item.uid.as_ref().unwrap(); - if let Some((task_id, _)) = self.timer_map.get(uid) { - log_if_err!(self.delay_timer.advance_task(*task_id)); - } - }); - - Ok(()) - } - /// generate a uid -> update_interval map fn gen_map(&self) -> HashMap { - let global = Data::global(); - let profiles = global.profiles.lock(); - let mut new_map = HashMap::new(); - if let Some(items) = profiles.get_items() { + if let Some(items) = Config::profiles().latest().get_items() { for item in items.iter() { if item.option.is_some() { let option = item.option.as_ref().unwrap(); @@ -107,11 +115,13 @@ impl Timer { } /// generate the diff map for refresh - fn gen_diff(&mut self) -> HashMap { + fn gen_diff(&self) -> HashMap { let mut diff_map = HashMap::new(); + let timer_map = self.timer_map.lock(); + let new_map = self.gen_map(); - let cur_map = &self.timer_map; + let cur_map = &timer_map; cur_map.iter().for_each(|(uid, (tid, val))| { let new_val = new_map.get(uid).unwrap_or(&0); @@ -123,34 +133,36 @@ impl Timer { } }); - let mut count = self.timer_count; + let mut count = self.timer_count.lock(); new_map.iter().for_each(|(uid, val)| { if cur_map.get(uid).is_none() { - diff_map.insert(uid.clone(), DiffFlag::Add(count, *val)); + diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val)); - count += 1; + *count += 1; } }); - self.timer_count = count; - diff_map } /// add a cron task - fn add_task(&self, uid: String, tid: TaskID, minutes: u64) -> Result<()> { - let core = Core::global(); - + fn add_task( + &self, + delay_timer: &mut DelayTimer, + uid: String, + tid: TaskID, + minutes: u64, + ) -> Result<()> { let task = TaskBuilder::default() .set_task_id(tid) .set_maximum_parallel_runnable_num(1) .set_frequency_repeated_by_minutes(minutes) // .set_frequency_repeated_by_seconds(minutes) // for test - .spawn_async_routine(move || Self::async_task(core.to_owned(), uid.to_owned())) + .spawn_async_routine(move || Self::async_task(uid.to_owned())) .context("failed to create timer task")?; - self.delay_timer + delay_timer .add_task(task) .context("failed to add timer task")?; @@ -158,9 +170,9 @@ impl Timer { } /// the task runner - async fn async_task(core: Core, uid: String) { + async fn async_task(uid: String) { log::info!(target: "app", "running timer task `{uid}`"); - log_if_err!(core.update_profile_item(uid, None).await); + crate::log_err!(feat::update_profile(uid, None).await); } } diff --git a/src-tauri/src/core/tray.rs b/src-tauri/src/core/tray.rs index a65c457..8ec8d09 100644 --- a/src-tauri/src/core/tray.rs +++ b/src-tauri/src/core/tray.rs @@ -1,5 +1,5 @@ -use crate::{data::Data, feat, utils::resolve}; -use anyhow::{Ok, Result}; +use crate::{config::Config, feat, utils::resolve}; +use anyhow::Result; use tauri::{ api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu, @@ -9,11 +9,7 @@ pub struct Tray {} impl Tray { pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu { - let data = Data::global(); - let zh = { - let verge = data.verge.lock(); - verge.language == Some("zh".into()) - }; + let zh = { Config::verge().latest().language == Some("zh".into()) }; let version = app_handle.package_info().version.to_string(); @@ -75,13 +71,15 @@ impl Tray { } pub fn update_part(app_handle: &AppHandle) -> Result<()> { - let global = Data::global(); - let clash = global.clash.lock(); - let mode = clash - .config - .get(&serde_yaml::Value::from("mode")) - .map(|val| val.as_str().unwrap_or("rule")) - .unwrap_or("rule"); + let mode = { + Config::clash() + .latest() + .0 + .get("mode") + .map(|val| val.as_str().unwrap_or("rule")) + .unwrap_or("rule") + .to_owned() + }; let tray = app_handle.tray_handle(); @@ -90,7 +88,8 @@ impl Tray { let _ = tray.get_item("direct_mode").set_selected(mode == "direct"); let _ = tray.get_item("script_mode").set_selected(mode == "script"); - let verge = global.verge.lock(); + let verge = Config::verge(); + let verge = verge.latest(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); @@ -105,7 +104,7 @@ impl Tray { SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => { let mode = &mode[0..mode.len() - 5]; - feat::change_clash_mode(mode); + feat::change_clash_mode(mode.into()); } "open_window" => resolve::create_window(app_handle), diff --git a/src-tauri/src/core/win_service.rs b/src-tauri/src/core/win_service.rs new file mode 100644 index 0000000..b4ef1a1 --- /dev/null +++ b/src-tauri/src/core/win_service.rs @@ -0,0 +1,173 @@ +#![cfg(target_os = "windows")] + +use crate::config::Config; +use crate::utils::dirs; +use anyhow::{bail, Context, Result}; +use deelevate::{PrivilegeLevel, Token}; +use runas::Command as RunasCommand; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::os::windows::process::CommandExt; +use std::time::Duration; +use std::{env::current_exe, process::Command as StdCommand}; +use tokio::time::sleep; + +const SERVICE_URL: &str = "http://127.0.0.1:33211"; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ResponseBody { + pub core_type: Option, + pub bin_path: String, + pub config_dir: String, + pub log_file: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct JsonResponse { + pub code: u64, + pub msg: String, + pub data: Option, +} + +/// Install the Clash Verge Service +/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 +pub async fn install_service() -> Result<()> { + let binary_path = dirs::service_path(); + let install_path = binary_path.with_file_name("install-service.exe"); + + if !install_path.exists() { + bail!("installer exe not found"); + } + + let token = Token::with_current_process()?; + let level = token.privilege_level()?; + + let status = match level { + PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).status()?, + _ => StdCommand::new(install_path) + .creation_flags(0x08000000) + .status()?, + }; + + if !status.success() { + bail!( + "failed to install service with status {}", + status.code().unwrap() + ); + } + + Ok(()) +} + +/// Uninstall the Clash Verge Service +/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 +pub async fn uninstall_service() -> Result<()> { + let binary_path = dirs::service_path(); + let uninstall_path = binary_path.with_file_name("uninstall-service.exe"); + + if !uninstall_path.exists() { + bail!("uninstaller exe not found"); + } + + let token = Token::with_current_process()?; + let level = token.privilege_level()?; + + let status = match level { + PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).status()?, + _ => StdCommand::new(uninstall_path) + .creation_flags(0x08000000) + .status()?, + }; + + if !status.success() { + bail!( + "failed to uninstall service with status {}", + status.code().unwrap() + ); + } + + Ok(()) +} + +/// check the windows service status +pub async fn check_service() -> Result { + let url = format!("{SERVICE_URL}/get_clash"); + let response = reqwest::ClientBuilder::new() + .no_proxy() + .build()? + .get(url) + .send() + .await? + .json::() + .await + .context("failed to connect to the Clash Verge Service")?; + + Ok(response) +} + +/// start the clash by service +pub(super) async fn run_core_by_service() -> Result<()> { + let status = check_service().await?; + + if status.code == 0 { + stop_core_by_service().await?; + sleep(Duration::from_secs(1)).await; + } + + let clash_core = { Config::verge().latest().clash_core.clone() }; + let clash_core = clash_core.unwrap_or("clash".into()); + + let clash_bin = format!("{clash_core}.exe"); + let bin_path = current_exe()?.with_file_name(clash_bin); + let bin_path = dirs::path_to_str(&bin_path)?; + + let config_dir = dirs::app_home_dir(); + let config_dir = dirs::path_to_str(&config_dir)?; + + let log_path = dirs::service_log_file(); + let log_path = dirs::path_to_str(&log_path)?; + + let mut map = HashMap::new(); + map.insert("core_type", clash_core.as_str()); + map.insert("bin_path", bin_path); + map.insert("config_dir", config_dir); + map.insert("log_file", log_path); + + let url = format!("{SERVICE_URL}/start_clash"); + let res = reqwest::ClientBuilder::new() + .no_proxy() + .build()? + .post(url) + .json(&map) + .send() + .await? + .json::() + .await + .context("failed to connect to the Clash Verge Service")?; + + if res.code != 0 { + bail!(res.msg); + } + + Ok(()) +} + +/// stop the clash by service +pub(super) async fn stop_core_by_service() -> Result<()> { + let url = format!("{SERVICE_URL}/stop_clash"); + let res = reqwest::ClientBuilder::new() + .no_proxy() + .build()? + .post(url) + .send() + .await? + .json::() + .await + .context("failed to connect to the Clash Verge Service")?; + + if res.code != 0 { + bail!(res.msg); + } + + Ok(()) +} diff --git a/src-tauri/src/data/mod.rs b/src-tauri/src/data/mod.rs deleted file mode 100644 index eb6a406..0000000 --- a/src-tauri/src/data/mod.rs +++ /dev/null @@ -1,32 +0,0 @@ -mod clash; -mod prfitem; -mod profiles; -mod verge; - -pub use self::clash::*; -pub use self::prfitem::*; -pub use self::profiles::*; -pub use self::verge::*; - -use once_cell::sync::OnceCell; -use parking_lot::Mutex; -use std::sync::Arc; - -#[derive(Debug, Clone)] -pub struct Data { - pub clash: Arc>, - pub verge: Arc>, - pub profiles: Arc>, -} - -impl Data { - pub fn global() -> &'static Data { - static DATA: OnceCell = OnceCell::new(); - - DATA.get_or_init(|| Data { - clash: Arc::new(Mutex::new(Clash::new())), - verge: Arc::new(Mutex::new(Verge::new())), - profiles: Arc::new(Mutex::new(Profiles::new())), - }) - } -} diff --git a/src-tauri/src/config/field.rs b/src-tauri/src/enhance/field.rs similarity index 100% rename from src-tauri/src/config/field.rs rename to src-tauri/src/enhance/field.rs diff --git a/src-tauri/src/config/merge.rs b/src-tauri/src/enhance/merge.rs similarity index 100% rename from src-tauri/src/config/merge.rs rename to src-tauri/src/enhance/merge.rs diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs new file mode 100644 index 0000000..0b00035 --- /dev/null +++ b/src-tauri/src/enhance/mod.rs @@ -0,0 +1,68 @@ +mod field; +mod merge; +mod script; +mod tun; + +pub(self) use self::field::*; +use self::merge::*; +use self::script::*; +use self::tun::*; +use crate::config::{ChainItem, ChainType}; +use serde_yaml::Mapping; +use std::collections::HashMap; +use std::collections::HashSet; + +type ResultLog = Vec<(String, String)>; + +pub fn enhance_config( + clash_config: Mapping, + profile_config: Mapping, + chain: Vec, + valid: Vec, + tun_mode: bool, +) -> (Mapping, Vec, HashMap) { + let mut config = profile_config; + let mut result_map = HashMap::new(); + let mut exists_keys = use_keys(&config); + + let valid = use_valid_fields(valid); + + chain.into_iter().for_each(|item| match item.data { + ChainType::Merge(merge) => { + exists_keys.extend(use_keys(&merge)); + config = use_merge(merge, config.to_owned()); + config = use_filter(config.to_owned(), &valid); + } + ChainType::Script(script) => { + let mut logs = vec![]; + + match use_script(script, config.to_owned()) { + Ok((res_config, res_logs)) => { + exists_keys.extend(use_keys(&res_config)); + config = use_filter(res_config, &valid); + logs.extend(res_logs); + } + Err(err) => logs.push(("exception".into(), err.to_string())), + } + + result_map.insert(item.uid, logs); + } + }); + + config = use_filter(config, &valid); + + for (key, value) in clash_config.into_iter() { + config.insert(key, value); + } + + let clash_fields = use_clash_fields(); + config = use_filter(config, &clash_fields); + config = use_tun(config, tun_mode); + config = use_sort(config); + + let mut exists_set = HashSet::new(); + exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s))); + exists_keys = exists_set.into_iter().collect(); + + (config, exists_keys, result_map) +} diff --git a/src-tauri/src/config/script.rs b/src-tauri/src/enhance/script.rs similarity index 100% rename from src-tauri/src/config/script.rs rename to src-tauri/src/enhance/script.rs diff --git a/src-tauri/src/config/tun.rs b/src-tauri/src/enhance/tun.rs similarity index 100% rename from src-tauri/src/config/tun.rs rename to src-tauri/src/enhance/tun.rs diff --git a/src-tauri/src/feat.rs b/src-tauri/src/feat.rs index 79fade1..7c73631 100644 --- a/src-tauri/src/feat.rs +++ b/src-tauri/src/feat.rs @@ -1,102 +1,307 @@ +//! +//! feat mod 里的函数主要用于 +//! - hotkey 快捷键 +//! - timer 定时器 +//! - cmds 页面调用 +//! +use crate::config::*; use crate::core::*; -use crate::data::*; -use crate::log_if_err; +use crate::log_err; +use anyhow::{bail, Result}; +use serde_yaml::{Mapping, Value}; // 重启clash pub fn restart_clash_core() { - let core = Core::global(); - let mut service = core.service.lock(); - log_if_err!(service.restart()); - drop(service); - log_if_err!(core.activate()); + tauri::async_runtime::spawn(async { + match CoreManager::global().run_core().await { + Ok(_) => { + handle::Handle::refresh_clash(); + handle::Handle::notice_message("set_config::ok", "ok"); + } + Err(err) => { + handle::Handle::notice_message("set_config::error", format!("{err}")); + log::error!(target:"app", "{err}"); + } + } + }); } -// 切换模式 -pub fn change_clash_mode(mode: &str) { - let core = Core::global(); - log_if_err!(core.update_mode(mode)); +// 切换模式 rule/global/direct/script mode +pub fn change_clash_mode(mode: String) { + let mut mapping = Mapping::new(); + mapping.insert(Value::from("mode"), mode.clone().into()); + + tauri::async_runtime::spawn(async move { + match clash_api::patch_configs(&mapping).await { + Ok(_) => { + // 更新配置 + Config::clash().data().patch_config(mapping); + + if let Ok(_) = Config::clash().data().save_config() { + handle::Handle::refresh_clash(); + log_err!(handle::Handle::update_systray_part()); + } + } + Err(err) => { + log::error!(target: "app", "{err}"); + } + } + }); } // 切换系统代理 pub fn toggle_system_proxy() { - let core = Core::global(); - let data = Data::global(); + let enable = Config::verge().draft().enable_system_proxy.clone(); + let enable = enable.unwrap_or(false); - let verge = data.verge.lock(); - let enable = !verge.enable_system_proxy.clone().unwrap_or(false); - drop(verge); - - log_if_err!(core.patch_verge(Verge { - enable_system_proxy: Some(enable), - ..Verge::default() - })); - - let handle = core.handle.lock(); - let _ = handle.refresh_verge(); + tauri::async_runtime::spawn(async move { + match patch_verge(IVerge { + enable_system_proxy: Some(!enable), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); } // 打开系统代理 pub fn enable_system_proxy() { - let core = Core::global(); - log_if_err!(core.patch_verge(Verge { - enable_system_proxy: Some(true), - ..Verge::default() - })); - - let handle = core.handle.lock(); - let _ = handle.refresh_verge(); + tauri::async_runtime::spawn(async { + match patch_verge(IVerge { + enable_system_proxy: Some(true), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); } // 关闭系统代理 pub fn disable_system_proxy() { - let core = Core::global(); - log_if_err!(core.patch_verge(Verge { - enable_system_proxy: Some(false), - ..Verge::default() - })); - - let handle = core.handle.lock(); - let _ = handle.refresh_verge(); + tauri::async_runtime::spawn(async { + match patch_verge(IVerge { + enable_system_proxy: Some(false), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); } // 切换tun模式 pub fn toggle_tun_mode() { - let core = Core::global(); - let data = Data::global(); + let enable = Config::verge().data().enable_tun_mode.clone(); + let enable = enable.unwrap_or(false); - let verge = data.verge.lock(); - let enable = !verge.enable_tun_mode.clone().unwrap_or(false); - drop(verge); - - log_if_err!(core.patch_verge(Verge { - enable_tun_mode: Some(enable), - ..Verge::default() - })); - - let handle = core.handle.lock(); - let _ = handle.refresh_verge(); + tauri::async_runtime::spawn(async move { + match patch_verge(IVerge { + enable_tun_mode: Some(!enable), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); } // 打开tun模式 pub fn enable_tun_mode() { - let core = Core::global(); - log_if_err!(core.patch_verge(Verge { - enable_tun_mode: Some(true), - ..Verge::default() - })); - - let handle = core.handle.lock(); - let _ = handle.refresh_verge(); + tauri::async_runtime::spawn(async { + match patch_verge(IVerge { + enable_tun_mode: Some(true), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); } // 关闭tun模式 pub fn disable_tun_mode() { - let core = Core::global(); - log_if_err!(core.patch_verge(Verge { - enable_tun_mode: Some(false), - ..Verge::default() - })); - - let handle = core.handle.lock(); - let _ = handle.refresh_verge(); + tauri::async_runtime::spawn(async { + match patch_verge(IVerge { + enable_tun_mode: Some(false), + ..IVerge::default() + }) + .await + { + Ok(_) => handle::Handle::refresh_verge(), + Err(err) => log::error!(target: "app", "{err}"), + } + }); +} + +/// 修改clash的配置 +pub async fn patch_clash(patch: Mapping) -> Result<()> { + Config::clash().draft().patch_config(patch.clone()); + + match { + let mixed_port = patch.get("mixed-port"); + if mixed_port.is_some() { + let changed = mixed_port != Config::clash().data().0.get("mixed-port"); + // 检查端口占用 + if changed { + if let Some(port) = mixed_port.clone().unwrap().as_u64() { + if !port_scanner::local_port_available(port as u16) { + Config::clash().discard(); + bail!("the port not available"); + } + } + } + }; + + // 激活配置 + if mixed_port.is_some() + || patch.get("secret").is_some() + || patch.get("external-controller").is_some() + { + CoreManager::global().run_core().await?; + } + + // 更新系统代理 + if mixed_port.is_some() { + log_err!(sysopt::Sysopt::global().init_sysproxy()); + } + + if patch.get("mode").is_some() { + log_err!(handle::Handle::update_systray_part()); + } + + >::Ok(()) + } { + Ok(()) => { + Config::clash().apply(); + Config::clash().data().save_config()?; + Ok(()) + } + Err(err) => { + Config::clash().discard(); + Err(err) + } + } +} + +/// 修改verge的配置 +/// 一般都是一个个的修改 +pub async fn patch_verge(patch: IVerge) -> Result<()> { + Config::verge().draft().patch_config(patch.clone()); + + let tun_mode = patch.enable_tun_mode; + let auto_launch = patch.enable_auto_launch; + let system_proxy = patch.enable_system_proxy; + let proxy_bypass = patch.system_proxy_bypass; + let language = patch.language; + + match { + #[cfg(target_os = "windows")] + { + todo!() + } + + if tun_mode.is_some() { + update_core_config().await?; + } + + if auto_launch.is_some() { + sysopt::Sysopt::global().update_launch()?; + } + if system_proxy.is_some() || proxy_bypass.is_some() { + sysopt::Sysopt::global().update_sysproxy()?; + sysopt::Sysopt::global().guard_proxy(); + } + + if let Some(true) = patch.enable_proxy_guard { + sysopt::Sysopt::global().guard_proxy(); + } + + if let Some(hotkeys) = patch.hotkeys { + hotkey::Hotkey::global().update(hotkeys)?; + } + + if language.is_some() { + handle::Handle::update_systray()?; + } else if system_proxy.or(tun_mode).is_some() { + handle::Handle::update_systray_part()?; + } + + >::Ok(()) + } { + Ok(()) => { + Config::verge().apply(); + Config::verge().data().save_file()?; + Ok(()) + } + Err(err) => { + Config::verge().discard(); + Err(err) + } + } +} + +/// 更新某个profile +/// 如果更新当前配置就激活配置 +pub async fn update_profile(uid: String, option: Option) -> Result<()> { + let url_opt = { + let profiles = Config::profiles(); + let profiles = profiles.latest(); + let item = profiles.get_item(&uid)?; + let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote"); + + if !is_remote { + None // 直接更新 + } else if item.url.is_none() { + bail!("failed to get the profile item url"); + } else { + Some((item.url.clone().unwrap(), item.option.clone())) + } + }; + + let should_update = match url_opt { + Some((url, opt)) => { + let merged_opt = PrfOption::merge(opt, option); + let item = PrfItem::from_url(&url, None, None, merged_opt).await?; + + let profiles = Config::profiles(); + let mut profiles = profiles.latest(); + profiles.update_item(uid.clone(), item)?; + + Some(uid) == profiles.get_current() + } + None => true, + }; + + if should_update { + update_core_config().await?; + } + + Ok(()) +} + +/// 更新配置 +async fn update_core_config() -> Result<()> { + match CoreManager::global().update_config().await { + Ok(_) => { + handle::Handle::refresh_clash(); + handle::Handle::notice_message("set_config::ok", "ok"); + Ok(()) + } + Err(err) => { + handle::Handle::notice_message("set_config::error", format!("{err}")); + Err(err) + } + } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 066939b..abe0e7d 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,12 +6,12 @@ mod cmds; mod config; mod core; -mod data; +mod enhance; mod feat; mod utils; use crate::utils::{init, resolve, server}; -use tauri::{api, Manager, SystemTray}; +use tauri::{api, SystemTray}; fn main() -> std::io::Result<()> { // 单例检测 @@ -20,18 +20,12 @@ fn main() -> std::io::Result<()> { return Ok(()); } - #[cfg(target_os = "windows")] - unsafe { - use crate::utils::dirs; - dirs::init_portable_flag(); - } - - crate::log_if_err!(init::init_config()); + crate::log_err!(init::init_config()); #[allow(unused_mut)] let mut builder = tauri::Builder::default() - .setup(|app| Ok(resolve::resolve_setup(app))) .system_tray(SystemTray::new()) + .setup(|app| Ok(resolve::resolve_setup(app))) .on_system_tray_event(core::tray::Tray::on_system_tray_event) .invoke_handler(tauri::generate_handler![ // common @@ -39,7 +33,7 @@ fn main() -> std::io::Result<()> { cmds::open_app_dir, cmds::open_logs_dir, cmds::open_web_url, - cmds::kill_sidecar, + // cmds::kill_sidecar, cmds::restart_sidecar, // clash cmds::get_clash_info, @@ -53,24 +47,20 @@ fn main() -> std::io::Result<()> { // verge cmds::get_verge_config, cmds::patch_verge_config, - cmds::update_hotkeys, + // cmds::update_hotkeys, // profile + cmds::get_profiles, + cmds::enhance_profiles, + cmds::patch_profiles_config, cmds::view_profile, cmds::patch_profile, cmds::create_profile, cmds::import_profile, cmds::update_profile, cmds::delete_profile, - cmds::select_profile, - cmds::get_profiles, - cmds::enhance_profiles, - cmds::change_profile_chain, - cmds::change_profile_valid, cmds::read_profile_file, cmds::save_profile_file, // service mode - cmds::service::start_service, - cmds::service::stop_service, cmds::service::check_service, cmds::service::install_service, cmds::service::uninstall_service, @@ -96,22 +86,10 @@ fn main() -> std::io::Result<()> { ); } - #[allow(unused_mut)] - let mut app = builder + let app = builder .build(tauri::generate_context!()) .expect("error while running tauri application"); - #[cfg(target_os = "macos")] - app.set_activation_policy(tauri::ActivationPolicy::Accessory); - - let app_handle = app.app_handle(); - ctrlc::set_handler(move || { - resolve::resolve_reset(); - app_handle.exit(0); - }) - .expect("error while exiting."); - - #[allow(unused)] app.run(|app_handle, e| match e { tauri::RunEvent::ExitRequested { api, .. } => { api.prevent_exit(); @@ -123,6 +101,8 @@ fn main() -> std::io::Result<()> { } #[cfg(target_os = "macos")] tauri::RunEvent::WindowEvent { label, event, .. } => { + use tauri::Manager; + if label == "main" { match event { tauri::WindowEvent::CloseRequested { api, .. } => { diff --git a/src-tauri/src/utils/config.rs b/src-tauri/src/utils/config.rs deleted file mode 100644 index 17fe14b..0000000 --- a/src-tauri/src/utils/config.rs +++ /dev/null @@ -1,59 +0,0 @@ -use anyhow::{Context, Result}; -use serde::{de::DeserializeOwned, Serialize}; -use serde_yaml::{Mapping, Value}; -use std::{fs, path::PathBuf}; - -/// read data from yaml as struct T -pub fn read_yaml(path: PathBuf) -> T { - if !path.exists() { - log::error!(target: "app", "file not found \"{}\"", path.display()); - return T::default(); - } - - let yaml_str = fs::read_to_string(&path).unwrap_or("".into()); - - match serde_yaml::from_str::(&yaml_str) { - Ok(val) => val, - Err(_) => { - log::error!(target: "app", "failed to read yaml file \"{}\"", path.display()); - T::default() - } - } -} - -/// read mapping from yaml fix #165 -pub fn read_merge_mapping(path: PathBuf) -> Mapping { - let map = Mapping::new(); - - if !path.exists() { - log::error!(target: "app", "file not found \"{}\"", path.display()); - return map; - } - - let yaml_str = fs::read_to_string(&path).unwrap_or("".into()); - - match serde_yaml::from_str::(&yaml_str) { - Ok(mut val) => { - crate::log_if_err!(val.apply_merge()); - val.as_mapping().unwrap_or(&map).to_owned() - } - Err(_) => { - log::error!(target: "app", "failed to read yaml file \"{}\"", path.display()); - map - } - } -} - -/// save the data to the file -/// can set `prefix` string to add some comments -pub fn save_yaml(path: PathBuf, data: &T, prefix: Option<&str>) -> Result<()> { - let data_str = serde_yaml::to_string(data)?; - - let yaml_str = match prefix { - Some(prefix) => format!("{prefix}{data_str}"), - None => data_str, - }; - - let path_str = path.as_os_str().to_string_lossy().to_string(); - fs::write(path, yaml_str.as_bytes()).context(format!("failed to save file \"{path_str}\"")) -} diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index b810180..553408a 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -1,4 +1,4 @@ -use std::env::temp_dir; +use anyhow::Result; use std::path::PathBuf; use tauri::{ api::path::{home_dir, resource_dir}, @@ -13,7 +13,6 @@ static APP_DIR: &str = "clash-verge-dev"; static CLASH_CONFIG: &str = "config.yaml"; static VERGE_CONFIG: &str = "verge.yaml"; static PROFILE_YAML: &str = "profiles.yaml"; -static PROFILE_TEMP: &str = "clash-verge-runtime.yaml"; static mut RESOURCE_DIR: Option = None; @@ -21,49 +20,58 @@ static mut RESOURCE_DIR: Option = None; #[allow(unused)] static mut PORTABLE_FLAG: bool = false; -pub static mut APP_VERSION: &str = "v1.1.1"; +pub static mut APP_VERSION: &str = "v1.1.2"; /// initialize portable flag -#[allow(unused)] -pub unsafe fn init_portable_flag() { - #[cfg(target_os = "windows")] - { - use tauri::utils::platform::current_exe; +#[cfg(target_os = "windows")] +pub unsafe fn init_portable_flag() -> Result<()> { + use tauri::utils::platform::current_exe; - let exe = current_exe().unwrap(); - let dir = exe.parent().unwrap(); + let exe = current_exe()?; + + if let Some(dir) = exe.parent() { let dir = PathBuf::from(dir).join(".config/PORTABLE"); if dir.exists() { PORTABLE_FLAG = true; } } + + Ok(()) } /// get the verge app home dir -pub fn app_home_dir() -> PathBuf { +pub fn app_home_dir() -> Result { #[cfg(target_os = "windows")] unsafe { use tauri::utils::platform::current_exe; if !PORTABLE_FLAG { - home_dir().unwrap().join(".config").join(APP_DIR) + Ok(home_dir() + .ok_or(anyhow::anyhow!("failed to get app home dir"))? + .join(".config") + .join(APP_DIR)) } else { - let app_exe = current_exe().unwrap(); - let app_exe = dunce::canonicalize(app_exe).unwrap(); - let app_dir = app_exe.parent().unwrap(); - PathBuf::from(app_dir).join(".config").join(APP_DIR) + let app_exe = current_exe()?; + let app_exe = dunce::canonicalize(app_exe)?; + let app_dir = app_exe + .parent() + .ok_or(anyhow::anyhow!("failed to get the portable app dir"))?; + Ok(PathBuf::from(app_dir).join(".config").join(APP_DIR)) } } #[cfg(not(target_os = "windows"))] - home_dir().unwrap().join(".config").join(APP_DIR) + Ok(home_dir() + .ok_or(anyhow::anyhow!("failed to get the app home dir"))? + .join(".config") + .join(APP_DIR)) } /// get the resources dir -pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf { +pub fn app_resources_dir(package_info: &PackageInfo) -> Result { let res_dir = resource_dir(package_info, &Env::default()) - .unwrap() + .ok_or(anyhow::anyhow!("failed to get the resource dir"))? .join("resources"); unsafe { @@ -74,51 +82,48 @@ pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf { APP_VERSION = Box::leak(Box::new(ver_str)); } - res_dir + Ok(res_dir) } /// profiles dir -pub fn app_profiles_dir() -> PathBuf { - app_home_dir().join("profiles") +pub fn app_profiles_dir() -> Result { + Ok(app_home_dir()?.join("profiles")) } /// logs dir -pub fn app_logs_dir() -> PathBuf { - app_home_dir().join("logs") +pub fn app_logs_dir() -> Result { + Ok(app_home_dir()?.join("logs")) } -pub fn clash_path() -> PathBuf { - app_home_dir().join(CLASH_CONFIG) +pub fn clash_path() -> Result { + Ok(app_home_dir()?.join(CLASH_CONFIG)) } -pub fn verge_path() -> PathBuf { - app_home_dir().join(VERGE_CONFIG) +pub fn verge_path() -> Result { + Ok(app_home_dir()?.join(VERGE_CONFIG)) } -pub fn profiles_path() -> PathBuf { - app_home_dir().join(PROFILE_YAML) +pub fn profiles_path() -> Result { + Ok(app_home_dir()?.join(PROFILE_YAML)) } -pub fn profiles_temp_path() -> PathBuf { - #[cfg(not(feature = "debug-yml"))] - return temp_dir().join(PROFILE_TEMP); - - #[cfg(feature = "debug-yml")] - return app_home_dir().join(PROFILE_TEMP); +pub fn app_res_dir() -> Result { + unsafe { + Ok(RESOURCE_DIR + .clone() + .ok_or(anyhow::anyhow!("failed to get the resource dir"))?) + } } -pub fn clash_pid_path() -> PathBuf { - unsafe { RESOURCE_DIR.clone().unwrap().join("clash.pid") } +pub fn clash_pid_path() -> Result { + unsafe { Ok(RESOURCE_DIR.clone().unwrap().join("clash.pid")) } } -#[cfg(windows)] -static SERVICE_PATH: &str = "clash-verge-service.exe"; - #[cfg(windows)] pub fn service_path() -> PathBuf { unsafe { let res_dir = RESOURCE_DIR.clone().unwrap(); - res_dir.join(SERVICE_PATH) + res_dir.join("clash-verge-service.exe") } } @@ -128,7 +133,7 @@ pub fn service_log_file() -> PathBuf { let log_dir = app_logs_dir().join("service"); - let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string(); + 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); @@ -136,3 +141,11 @@ pub fn service_log_file() -> PathBuf { log_file } + +pub fn path_to_str(path: &PathBuf) -> Result<&str> { + let path_str = path + .as_os_str() + .to_str() + .ok_or(anyhow::anyhow!("failed to get path from {:?}", path))?; + Ok(path_str) +} diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index b8ae558..2d4a32f 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -1,15 +1,51 @@ -use anyhow::Result; +use anyhow::{anyhow, bail, Context, Result}; use nanoid::nanoid; -use std::path::PathBuf; -use std::process::Command; -use std::str::FromStr; -use std::time::{SystemTime, UNIX_EPOCH}; +use serde::{de::DeserializeOwned, Serialize}; +use serde_yaml::{Mapping, Value}; +use std::{fs, path::PathBuf, process::Command, str::FromStr}; -pub fn get_now() -> usize { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() as _ +/// read data from yaml as struct T +pub fn read_yaml(path: &PathBuf) -> Result { + if !path.exists() { + bail!("file not found \"{}\"", path.display()); + } + + let yaml_str = fs::read_to_string(&path) + .context(format!("failed to read the file \"{}\"", path.display()))?; + + serde_yaml::from_str::(&yaml_str).context(format!( + "failed to read the file with yaml format \"{}\"", + path.display() + )) +} + +/// read mapping from yaml fix #165 +pub fn read_merge_mapping(path: &PathBuf) -> Result { + let mut val: Value = read_yaml(path)?; + val.apply_merge() + .context(format!("failed to apply merge \"{}\"", path.display()))?; + + Ok(val + .as_mapping() + .ok_or(anyhow!( + "failed to transform to yaml mapping \"{}\"", + path.display() + ))? + .to_owned()) +} + +/// save the data to the file +/// can set `prefix` string to add some comments +pub fn save_yaml(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> { + let data_str = serde_yaml::to_string(data)?; + + let yaml_str = match prefix { + Some(prefix) => format!("{prefix}\n\n{data_str}"), + None => data_str, + }; + + let path_str = path.as_os_str().to_string_lossy().to_string(); + fs::write(path, yaml_str.as_bytes()).context(format!("failed to save file \"{path_str}\"")) } const ALPHABET: [char; 62] = [ @@ -70,27 +106,40 @@ pub fn open_file(path: PathBuf) -> Result<()> { } #[macro_export] -macro_rules! log_if_err { - ($result: expr) => { - if let Err(err) = $result { - log::error!(target: "app", "{err}"); - } - }; +macro_rules! error { + ($result: expr) => { + log::error!(target: "app", "{}", $result); + }; +} + +#[macro_export] +macro_rules! log_err { + ($result: expr) => { + if let Err(err) = $result { + log::error!(target: "app", "{err}"); + } + }; + + ($result: expr, $err_str: expr) => { + if let Err(_) = $result { + log::error!(target: "app", "{}", $err_str); + } + }; } /// wrap the anyhow error /// transform the error to String #[macro_export] macro_rules! wrap_err { - ($stat: expr) => { - match $stat { - Ok(a) => Ok(a), - Err(err) => { - log::error!(target: "app", "{}", err.to_string()); - Err(format!("{}", err.to_string())) - } - } - }; + ($stat: expr) => { + match $stat { + Ok(a) => Ok(a), + Err(err) => { + log::error!(target: "app", "{}", err.to_string()); + Err(format!("{}", err.to_string())) + } + } + }; } /// return the string literal error diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index ce595e9..3aeeb6c 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -1,4 +1,4 @@ -use crate::utils::{dirs, tmpl}; +use crate::utils::dirs; use anyhow::Result; use chrono::Local; use log::LevelFilter; @@ -7,27 +7,33 @@ use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Config, Logger, Root}; use log4rs::encode::pattern::PatternEncoder; use std::fs; -use std::io::Write; use tauri::PackageInfo; /// initialize this instance's log file fn init_log() -> Result<()> { - let log_dir = dirs::app_logs_dir(); + let log_dir = dirs::app_logs_dir()?; if !log_dir.exists() { let _ = fs::create_dir_all(&log_dir); } - let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string(); + 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 time_format = "{d(%Y-%m-%d %H:%M:%S)} - {m}{n}"; - let stdout = ConsoleAppender::builder() - .encoder(Box::new(PatternEncoder::new(time_format))) - .build(); - let tofile = FileAppender::builder() - .encoder(Box::new(PatternEncoder::new(time_format))) - .build(log_file)?; + #[cfg(feature = "verge-dev")] + let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {M} {m}{n}"; + #[cfg(not(feature = "verge-dev"))] + let time_format = "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}"; + + let encode = Box::new(PatternEncoder::new(time_format)); + + let stdout = ConsoleAppender::builder().encoder(encode.clone()).build(); + let tofile = FileAppender::builder().encoder(encode).build(log_file)?; + + #[cfg(feature = "verge-dev")] + let level = LevelFilter::Debug; + #[cfg(not(feature = "verge-dev"))] + let level = LevelFilter::Info; let config = Config::builder() .appender(Appender::builder().build("stdout", Box::new(stdout))) @@ -36,9 +42,9 @@ fn init_log() -> Result<()> { Logger::builder() .appenders(["file", "stdout"]) .additive(false) - .build("app", LevelFilter::Info), + .build("app", level), ) - .build(Root::builder().appender("stdout").build(LevelFilter::Info))?; + .build(Root::builder().appender("stdout").build(level))?; log4rs::init_config(config)?; @@ -47,44 +53,32 @@ fn init_log() -> Result<()> { /// Initialize all the files from resources pub fn init_config() -> Result<()> { + #[cfg(target_os = "windows")] + unsafe { + let _ = dirs::init_portable_flag(); + } + let _ = init_log(); - let app_dir = dirs::app_home_dir(); - let profiles_dir = dirs::app_profiles_dir(); + let _ = dirs::app_home_dir().map(|app_dir| { + if !app_dir.exists() { + let _ = fs::create_dir_all(&app_dir); + } + }); - if !app_dir.exists() { - let _ = fs::create_dir_all(&app_dir); - } - if !profiles_dir.exists() { - let _ = fs::create_dir_all(&profiles_dir); - } + let _ = dirs::app_profiles_dir().map(|profiles_dir| { + if !profiles_dir.exists() { + let _ = fs::create_dir_all(&profiles_dir); + } + }); - // target path - let clash_path = app_dir.join("config.yaml"); - let verge_path = app_dir.join("verge.yaml"); - let profile_path = app_dir.join("profiles.yaml"); - - if !clash_path.exists() { - fs::File::create(clash_path)?.write(tmpl::CLASH_CONFIG)?; - } - if !verge_path.exists() { - fs::File::create(verge_path)?.write(tmpl::VERGE_CONFIG)?; - } - if !profile_path.exists() { - fs::File::create(profile_path)?.write(tmpl::PROFILES_CONFIG)?; - } Ok(()) } /// initialize app -pub fn init_resources(package_info: &PackageInfo) { - // create app dir - let app_dir = dirs::app_home_dir(); - let res_dir = dirs::app_resources_dir(package_info); - - if !app_dir.exists() { - let _ = fs::create_dir_all(&app_dir); - } +pub fn init_resources(package_info: &PackageInfo) -> Result<()> { + let app_dir = dirs::app_home_dir()?; + let res_dir = dirs::app_resources_dir(package_info)?; // copy the resource file for file in ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"].iter() { @@ -94,4 +88,6 @@ pub fn init_resources(package_info: &PackageInfo) { let _ = fs::copy(src_path, target_path); } } + + Ok(()) } diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index e6c37a8..aeb0a60 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -1,8 +1,7 @@ -pub mod config; pub mod dirs; pub mod help; pub mod init; pub mod resolve; pub mod server; pub mod tmpl; -mod winhelp; +// mod winhelp; diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 16ff18f..737042c 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -1,48 +1,43 @@ -use crate::{ - core::{tray, Core}, - data::Data, - utils::init, - utils::server, -}; +use crate::config::Config; +use crate::log_err; +use crate::{core::*, utils::init, utils::server}; use tauri::{App, AppHandle, Manager}; /// handle something when start app -pub fn resolve_setup(app: &App) { - let _ = app - .tray_handle() - .set_menu(tray::Tray::tray_menu(&app.app_handle())); +pub fn resolve_setup(app: &mut App) { + #[cfg(target_os = "macos")] + app.set_activation_policy(tauri::ActivationPolicy::Accessory); - init::init_resources(app.package_info()); + handle::Handle::global().init(app.app_handle()); - let silent_start = { - let global = Data::global(); - let verge = global.verge.lock(); - let singleton = verge.app_singleton_port.clone(); + log_err!(init::init_resources(app.package_info())); - // setup a simple http server for singleton - server::embed_server(&app.app_handle(), singleton); + // 启动核心 + log_err!(Config::init_config()); + log_err!(CoreManager::global().init()); - verge.enable_silent_start.clone().unwrap_or(false) - }; + // setup a simple http server for singleton + server::embed_server(app.app_handle()); - // core should be initialized after init_app fix #122 - let core = Core::global(); - core.init(app.app_handle()); + log_err!(tray::Tray::update_systray(&app.app_handle())); - if !silent_start { + let silent_start = { Config::verge().data().enable_silent_start.clone() }; + if !silent_start.unwrap_or(false) { create_window(&app.app_handle()); } + + log_err!(sysopt::Sysopt::global().init_launch()); + log_err!(sysopt::Sysopt::global().init_sysproxy()); + + log_err!(handle::Handle::update_systray_part()); + log_err!(hotkey::Hotkey::global().init(app.app_handle())); + log_err!(timer::Timer::global().init()); } /// reset system proxy pub fn resolve_reset() { - let core = Core::global(); - let mut sysopt = core.sysopt.lock(); - crate::log_if_err!(sysopt.reset_sysproxy()); - drop(sysopt); - - let mut service = core.service.lock(); - crate::log_if_err!(service.stop()); + log_err!(sysopt::Sysopt::global().reset_sysproxy()); + log_err!(CoreManager::global().stop_core()); } /// create main window @@ -66,31 +61,31 @@ pub fn create_window(app_handle: &AppHandle) { #[cfg(target_os = "windows")] { - use crate::utils::winhelp; use std::time::Duration; use tokio::time::sleep; use window_shadows::set_shadow; - use window_vibrancy::apply_blur; match builder .decorations(false) .transparent(true) .inner_size(800.0, 636.0) + .visible(false) .build() { Ok(_) => { let app_handle = app_handle.clone(); + if let Some(window) = app_handle.get_window("main") { + let _ = set_shadow(&window, true); + } + tauri::async_runtime::spawn(async move { sleep(Duration::from_secs(1)).await; if let Some(window) = app_handle.get_window("main") { let _ = window.show(); - let _ = set_shadow(&window, true); - - if !winhelp::is_win11() { - let _ = apply_blur(&window, None); - } + let _ = window.unminimize(); + let _ = window.set_focus(); } }); } @@ -99,10 +94,10 @@ pub fn create_window(app_handle: &AppHandle) { } #[cfg(target_os = "macos")] - crate::log_if_err!(builder.decorations(true).inner_size(800.0, 642.0).build()); + crate::log_err!(builder.decorations(true).inner_size(800.0, 642.0).build()); #[cfg(target_os = "linux")] - crate::log_if_err!(builder + crate::log_err!(builder .decorations(false) .transparent(true) .inner_size(800.0, 636.0) diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index c0162c6..f4e9836 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -1,26 +1,27 @@ extern crate warp; use super::resolve; -use crate::data::Verge; +use crate::config::IVerge; +use anyhow::{bail, Result}; use port_scanner::local_port_available; use tauri::AppHandle; use warp::Filter; -#[cfg(not(feature = "verge-dev"))] -const SERVER_PORT: u16 = 33331; -#[cfg(feature = "verge-dev")] -const SERVER_PORT: u16 = 11233; - /// check whether there is already exists -pub fn check_singleton() -> Result<(), ()> { - let verge = Verge::new(); - let port = verge.app_singleton_port.unwrap_or(SERVER_PORT); +pub fn check_singleton() -> Result<()> { + let port = IVerge::get_singleton_port(); if !local_port_available(port) { tauri::async_runtime::block_on(async { - let url = format!("http://127.0.0.1:{}/commands/visible", port); - reqwest::get(url).await.unwrap(); - Err(()) + let url = format!("http://127.0.0.1:{port}/commands/visible"); + let resp = reqwest::get(url).await?.text().await?; + + if &resp == "ok" { + bail!("app exists"); + } + + log::error!("failed to setup singleton listen server"); + Ok(()) }) } else { Ok(()) @@ -29,14 +30,13 @@ pub fn check_singleton() -> Result<(), ()> { /// The embed server only be used to implement singleton process /// maybe it can be used as pac server later -pub fn embed_server(app_handle: &AppHandle, port: Option) { - let app_handle = app_handle.clone(); - let port = port.unwrap_or(SERVER_PORT); +pub fn embed_server(app_handle: AppHandle) { + let port = IVerge::get_singleton_port(); tauri::async_runtime::spawn(async move { let commands = warp::path!("commands" / "visible").map(move || { resolve::create_window(&app_handle); - return format!("ok"); + format!("ok") }); warp::serve(commands).bind(([127, 0, 0, 1], port)).await; diff --git a/src-tauri/src/utils/tmpl.rs b/src-tauri/src/utils/tmpl.rs index 32fdafe..ec17c33 100644 --- a/src-tauri/src/utils/tmpl.rs +++ b/src-tauri/src/utils/tmpl.rs @@ -1,37 +1,5 @@ ///! Some config file template -/// template for clash core `config.yaml` -pub const CLASH_CONFIG: &[u8] = br#"# Default Config For Clash Core - -mixed-port: 7890 -log-level: info -allow-lan: false -external-controller: 127.0.0.1:9090 -mode: rule -secret: "" -"#; - -/// template for `profiles.yaml` -pub const PROFILES_CONFIG: &[u8] = b"# Profiles Config for Clash Verge - -current: ~ -items: ~ -"; - -/// template for `verge.yaml` -pub const VERGE_CONFIG: &[u8] = b"# Defaulf Config For Clash Verge - -clash_core: clash-meta -language: en -theme_mode: light -theme_blur: false -traffic_graph: true -enable_self_startup: false -enable_system_proxy: false -enable_proxy_guard: false -proxy_guard_duration: 10 -"; - /// template for new a profile item pub const ITEM_LOCAL: &str = "# Profile Template for clash verge @@ -61,9 +29,7 @@ append-proxy-groups: /// enhanced profile pub const ITEM_SCRIPT: &str = "// Define the `main` function -// The argument to this function is the clash config -// or the result of the previous handler -// so you should return the config after processing + function main(params) { return params; } diff --git a/src/components/profile/enhanced.tsx b/src/components/profile/enhanced.tsx index 6c9a5fd..3c38913 100644 --- a/src/components/profile/enhanced.tsx +++ b/src/components/profile/enhanced.tsx @@ -7,7 +7,7 @@ import { getProfiles, deleteProfile, enhanceProfiles, - changeProfileChain, + patchProfilesConfig, getRuntimeLogs, } from "@/services/cmds"; import ProfileMore from "./profile-more"; @@ -43,7 +43,7 @@ const EnhancedMode = (props: Props) => { if (chain.includes(uid)) return; const newChain = [...chain, uid]; - await changeProfileChain(newChain); + await patchProfilesConfig({ chain: newChain }); mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); mutateLogs(); }); @@ -52,7 +52,7 @@ const EnhancedMode = (props: Props) => { if (!chain.includes(uid)) return; const newChain = chain.filter((i) => i !== uid); - await changeProfileChain(newChain); + await patchProfilesConfig({ chain: newChain }); mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); mutateLogs(); }); @@ -72,7 +72,7 @@ const EnhancedMode = (props: Props) => { if (!chain.includes(uid)) return; const newChain = [uid].concat(chain.filter((i) => i !== uid)); - await changeProfileChain(newChain); + await patchProfilesConfig({ chain: newChain }); mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); mutateLogs(); }); @@ -81,7 +81,7 @@ const EnhancedMode = (props: Props) => { if (!chain.includes(uid)) return; const newChain = chain.filter((i) => i !== uid).concat([uid]); - await changeProfileChain(newChain); + await patchProfilesConfig({ chain: newChain }); mutateProfiles((conf = {}) => ({ ...conf, chain: newChain }), true); mutateLogs(); }); diff --git a/src/components/setting/mods/clash-field-viewer.tsx b/src/components/setting/mods/clash-field-viewer.tsx index c365a9b..dbfa51d 100644 --- a/src/components/setting/mods/clash-field-viewer.tsx +++ b/src/components/setting/mods/clash-field-viewer.tsx @@ -15,9 +15,9 @@ import { } from "@mui/material"; import { InfoRounded } from "@mui/icons-material"; import { - changeProfileValid, getProfiles, getRuntimeExists, + patchProfilesConfig, } from "@/services/cmds"; import { ModalHandler } from "@/hooks/use-modal-handler"; import { @@ -91,7 +91,7 @@ const ClashFieldViewer = ({ handler }: Props) => { if (curSet.size === oldSet.size && curSet.size === joinSet.size) return; try { - await changeProfileValid([...curSet]); + await patchProfilesConfig({ valid: [...curSet] }); mutateProfile(); // Notice.success("Refresh clash config", 1000); } catch (err: any) { diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 241a497..a56f275 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -7,7 +7,7 @@ import { useTranslation } from "react-i18next"; import { getProfiles, patchProfile, - selectProfile, + patchProfilesConfig, importProfile, } from "@/services/cmds"; import { getProxies, updateProxy } from "@/services/api"; @@ -113,7 +113,7 @@ const ProfilePage = () => { if (!newProfiles.current && remoteItem) { const current = remoteItem.uid; - selectProfile(current); + patchProfilesConfig({ current }); mutate("getProfiles", { ...newProfiles, current }, true); mutate("getRuntimeLogs"); } @@ -125,13 +125,13 @@ const ProfilePage = () => { } }; - const onSelect = useLockFn(async (uid: string, force: boolean) => { - if (!force && uid === profiles.current) return; + const onSelect = useLockFn(async (current: string, force: boolean) => { + if (!force && current === profiles.current) return; try { - await selectProfile(uid); - setCurrentProfile(uid); - mutate("getProfiles", { ...profiles, current: uid }, true); + await patchProfilesConfig({ current }); + setCurrentProfile(current); + mutate("getProfiles", { ...profiles, current: current }, true); mutate("getRuntimeLogs"); // if (force) Notice.success("Refresh clash config", 1000); } catch (err: any) { diff --git a/src/services/cmds.ts b/src/services/cmds.ts index d99c60a..43faf4b 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -32,6 +32,10 @@ export async function enhanceProfiles() { return invoke("enhance_profiles"); } +export async function patchProfilesConfig(profiles: CmdType.ProfilesConfig) { + return invoke("patch_profiles_config"); +} + export async function createProfile( item: Partial, fileData?: string | null @@ -76,18 +80,6 @@ export async function patchProfile( return invoke("patch_profile", { index, profile }); } -export async function selectProfile(index: string) { - return invoke("select_profile", { index }); -} - -export async function changeProfileChain(chain?: string[]) { - return invoke("change_profile_chain", { chain }); -} - -export async function changeProfileValid(valid?: string[]) { - return invoke("change_profile_valid", { valid }); -} - export async function getClashInfo() { return invoke("get_clash_info"); } @@ -136,10 +128,6 @@ export async function restartSidecar() { return invoke("restart_sidecar"); } -export async function killSidecar() { - return invoke("kill_sidecar"); -} - export async function openAppDir() { return invoke("open_app_dir").catch((err) => Notice.error(err?.message || err.toString(), 1500) @@ -158,14 +146,6 @@ export async function openWebUrl(url: string) { /// service mode -export async function startService() { - return invoke("start_service"); -} - -export async function stopService() { - return invoke("stop_service"); -} - export async function checkService() { try { const result = await invoke("check_service");