diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 812bccf..7bed2ff 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -14,6 +14,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.6", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -59,6 +70,115 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dabe5a181f83789739c194cbe5a897dde195078fac08568d09221fd6137a7ba8" +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3ca4f8ff117c37c278a2f7415ce9be55560b846b5bc4412aaa5d29c1c3dae2" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi", +] + +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-net" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5373304df79b9b4395068fb080369ec7178608827306ce4d081cba51cac551df" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83137067e3a2a6a06d67168e49e68a0957d215410473a740cea95a2425c0b7c6" +dependencies = [ + "async-io", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "libc", + "once_cell", + "signal-hook", + "winapi", +] + +[[package]] +name = "async-task" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" + +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atk" version = "0.15.1" @@ -83,6 +203,12 @@ dependencies = [ "system-deps 6.0.2", ] +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + [[package]] name = "attohttpc" version = "0.18.0" @@ -162,6 +288,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + [[package]] name = "brotli" version = "3.3.3" @@ -220,6 +360,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + [[package]] name = "cairo-rs" version = "0.15.10" @@ -336,6 +482,7 @@ dependencies = [ "anyhow", "auto-launch", "chrono", + "delay_timer", "dirs", "dunce", "log", @@ -398,6 +545,25 @@ dependencies = [ "memchr", ] +[[package]] +name = "concat-idents" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6f90860248d75014b7b103db8fee4f291c07bfb41306cdf77a0a5ab7a10d2f" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -463,6 +629,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cron_clock" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8699d8ed16e3db689f8ae04d8dc3c6666a4ba7e724e5a157884b7cc385d16b" +dependencies = [ + "chrono", + "nom", + "once_cell", +] + [[package]] name = "crossbeam-channel" version = "0.5.4" @@ -606,6 +783,16 @@ dependencies = [ "syn", ] +[[package]] +name = "dashmap" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77a43b28d0668df09411cb0bc9a8c2adc40f9a048afe863e05fd43251e8e39c" +dependencies = [ + "cfg-if", + "num_cpus", +] + [[package]] name = "deflate" version = "0.7.20" @@ -625,6 +812,31 @@ dependencies = [ "adler32", ] +[[package]] +name = "delay_timer" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68db8078186fbb91582bc7ba4f7b90e52cc4618a585748dd6a9fb6f4a25ea3c4" +dependencies = [ + "anyhow", + "async-trait", + "autocfg", + "concat-idents", + "cron_clock", + "dashmap", + "event-listener", + "futures", + "log", + "lru", + "once_cell", + "rs-snowflake", + "rustc_version 0.2.3", + "smol", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "derivative" version = "2.2.0" @@ -757,6 +969,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + [[package]] name = "fastrand" version = "1.7.0" @@ -1289,6 +1507,9 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] [[package]] name = "headers" @@ -1728,6 +1949,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea2d928b485416e8908cff2d97d621db22b27f7b3b6729e438bcf42c671ba91" +dependencies = [ + "hashbrown", +] + [[package]] name = "mac" version = "0.1.1" @@ -1803,6 +2033,12 @@ dependencies = [ "unicase", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "minisign-verify" version = "0.2.0" @@ -1961,6 +2197,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -2404,6 +2650,19 @@ dependencies = [ "miniz_oxide 0.5.1", ] +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi", +] + [[package]] name = "port_scanner" version = "0.1.5" @@ -2706,6 +2965,21 @@ dependencies = [ "windows 0.33.0", ] +[[package]] +name = "rs-snowflake" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148be9baabb59959aeaf4b27597bce9f071511a6a74a4a20a2c5681722a21e" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.3.3" @@ -2816,13 +3090,22 @@ dependencies = [ "thin-slice", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser 0.7.0", +] + [[package]] name = "semver" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" dependencies = [ - "semver-parser", + "semver-parser 0.10.2", ] [[package]] @@ -2831,6 +3114,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "semver-parser" version = "0.10.2" @@ -3025,6 +3314,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "signal-hook" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" +dependencies = [ + "libc", + "signal-hook-registry", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -3052,6 +3351,24 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "smol" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cf3b5351f3e783c1d79ab5fc604eeed8b8ae9abd36b166e8b87a089efd85e4" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", + "once_cell", +] + [[package]] name = "socket2" version = "0.4.4" @@ -4090,6 +4407,15 @@ dependencies = [ "windows-bindgen", ] +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "which" version = "4.2.5" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6467b00..0afe654 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ nanoid = "0.4.0" chrono = "0.4.19" serde_json = "1.0" serde_yaml = "0.8" +delay_timer = "0.11.1" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.0.0-rc.6", features = ["process-all", "shell-all", "system-tray", "updater", "window-all"] } window-shadows = { git = "https://github.com/tauri-apps/window-shadows" } @@ -39,8 +40,8 @@ port_scanner = "0.1.5" winreg = { version = "0.10", features = ["transactions"] } [features] -default = [ "custom-protocol" ] -custom-protocol = [ "tauri/custom-protocol" ] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] verge-dev = [] debug-yml = [] diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index bafc118..d2d146f 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -1,25 +1,26 @@ use crate::{ - core::{ClashInfo, PrfItem, PrfOption, Profiles, VergeConfig}, - states::{ClashState, ProfilesState, VergeState}, + core::{ClashInfo, Core, PrfItem, PrfOption, Profiles, VergeConfig}, utils::{dirs, sysopt::SysProxyConfig}, }; -use crate::{ret_err, wrap_err}; +use crate::{log_if_err, ret_err, wrap_err}; use anyhow::Result; use serde_yaml::Mapping; use std::process::Command; use tauri::{api, Manager, State}; +type CmdResult = Result; + /// get all profiles from `profiles.yaml` #[tauri::command] -pub fn get_profiles<'a>(profiles_state: State<'_, ProfilesState>) -> Result { - let profiles = profiles_state.0.lock().unwrap(); +pub fn get_profiles(core: State<'_, Core>) -> CmdResult { + let profiles = core.profiles.lock().unwrap(); Ok(profiles.clone()) } /// synchronize data irregularly #[tauri::command] -pub fn sync_profiles(profiles_state: State<'_, ProfilesState>) -> Result<(), String> { - let mut profiles = profiles_state.0.lock().unwrap(); +pub fn sync_profiles(core: State<'_, Core>) -> CmdResult { + let mut profiles = core.profiles.lock().unwrap(); wrap_err!(profiles.sync_file()) } @@ -29,11 +30,11 @@ pub fn sync_profiles(profiles_state: State<'_, ProfilesState>) -> Result<(), Str pub async fn import_profile( url: String, option: Option, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { + core: State<'_, Core>, +) -> CmdResult { let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; - let mut profiles = profiles_state.0.lock().unwrap(); + let mut profiles = core.profiles.lock().unwrap(); wrap_err!(profiles.append_item(item)) } @@ -44,11 +45,11 @@ pub async fn import_profile( pub async fn create_profile( item: PrfItem, // partial file_data: Option, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { + core: State<'_, Core>, +) -> CmdResult { let item = wrap_err!(PrfItem::from(item, file_data).await)?; - let mut profiles = profiles_state.0.lock().unwrap(); + let mut profiles = core.profiles.lock().unwrap(); wrap_err!(profiles.append_item(item)) } @@ -57,12 +58,11 @@ pub async fn create_profile( pub async fn update_profile( index: String, option: Option, - clash_state: State<'_, ClashState>, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { + core: State<'_, Core>, +) -> CmdResult { let (url, opt) = { // must release the lock here - let profiles = profiles_state.0.lock().unwrap(); + let profiles = core.profiles.lock().unwrap(); let item = wrap_err!(profiles.get_item(&index))?; // check the profile type @@ -82,13 +82,12 @@ pub async fn update_profile( let fetch_opt = PrfOption::merge(opt, option); let item = wrap_err!(PrfItem::from_url(&url, None, None, fetch_opt).await)?; - let mut profiles = profiles_state.0.lock().unwrap(); + let mut profiles = core.profiles.lock().unwrap(); wrap_err!(profiles.update_item(index.clone(), item))?; // reactivate the profile if Some(index) == profiles.get_current() { - let clash = clash_state.0.lock().unwrap(); - wrap_err!(clash.activate_enhanced(&profiles, false, false))?; + log_if_err!(core.activate_enhanced(false, false)); } Ok(()) @@ -96,79 +95,55 @@ pub async fn update_profile( /// change the current profile #[tauri::command] -pub fn select_profile( - index: String, - clash_state: State<'_, ClashState>, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { - let mut profiles = profiles_state.0.lock().unwrap(); - wrap_err!(profiles.put_current(index))?; +pub fn select_profile(index: String, core: State<'_, Core>) -> CmdResult { + { + let mut profiles = core.profiles.lock().unwrap(); + wrap_err!(profiles.put_current(index))?; + } - let clash = clash_state.0.lock().unwrap(); - wrap_err!(clash.activate_enhanced(&profiles, false, false)) + log_if_err!(core.activate_enhanced(false, false)); + + Ok(()) } /// change the profile chain #[tauri::command] -pub fn change_profile_chain( - chain: Option>, - app_handle: tauri::AppHandle, - clash_state: State<'_, ClashState>, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { - let mut clash = clash_state.0.lock().unwrap(); - let mut profiles = profiles_state.0.lock().unwrap(); +pub fn change_profile_chain(chain: Option>, core: State<'_, Core>) -> CmdResult { + { + let mut profiles = core.profiles.lock().unwrap(); + profiles.put_chain(chain); + } - profiles.put_chain(chain); - clash.set_window(app_handle.get_window("main")); + log_if_err!(core.activate_enhanced(false, false)); - wrap_err!(clash.activate_enhanced(&profiles, false, false)) + Ok(()) } /// change the profile valid fields #[tauri::command] -pub fn change_profile_valid( - valid: Option>, - app_handle: tauri::AppHandle, - clash_state: State<'_, ClashState>, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { - let mut clash = clash_state.0.lock().unwrap(); - let mut profiles = profiles_state.0.lock().unwrap(); - +pub fn change_profile_valid(valid: Option>, core: State) -> CmdResult { + let mut profiles = core.profiles.lock().unwrap(); profiles.put_valid(valid); - clash.set_window(app_handle.get_window("main")); - wrap_err!(clash.activate_enhanced(&profiles, false, false)) + log_if_err!(core.activate_enhanced(false, false)); + + Ok(()) } /// manually exec enhanced profile #[tauri::command] -pub fn enhance_profiles( - app_handle: tauri::AppHandle, - clash_state: State<'_, ClashState>, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { - let mut clash = clash_state.0.lock().unwrap(); - let profiles = profiles_state.0.lock().unwrap(); - - clash.set_window(app_handle.get_window("main")); - - wrap_err!(clash.activate_enhanced(&profiles, false, false)) +pub fn enhance_profiles(core: State<'_, Core>) -> CmdResult { + log_if_err!(core.activate_enhanced(false, false)); + Ok(()) } /// delete profile item #[tauri::command] -pub fn delete_profile( - index: String, - clash_state: State<'_, ClashState>, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { - let mut profiles = profiles_state.0.lock().unwrap(); +pub fn delete_profile(index: String, core: State<'_, Core>) -> CmdResult { + let mut profiles = core.profiles.lock().unwrap(); if wrap_err!(profiles.delete_item(index))? { - let clash = clash_state.0.lock().unwrap(); - wrap_err!(clash.activate_enhanced(&profiles, false, false))?; + log_if_err!(core.activate_enhanced(false, false)); } Ok(()) @@ -176,19 +151,16 @@ pub fn delete_profile( /// patch the profile config #[tauri::command] -pub fn patch_profile( - index: String, - profile: PrfItem, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { - let mut profiles = profiles_state.0.lock().unwrap(); +pub fn patch_profile(index: String, profile: PrfItem, core: State<'_, Core>) -> CmdResult { + let mut profiles = core.profiles.lock().unwrap(); + wrap_err!(profiles.patch_item(index, profile)) } /// run vscode command to edit the profile #[tauri::command] -pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> Result<(), String> { - let profiles = profiles_state.0.lock().unwrap(); +pub fn view_profile(index: String, core: State<'_, Core>) -> CmdResult { + let mut profiles = core.profiles.lock().unwrap(); let item = wrap_err!(profiles.get_item(&index))?; let file = item.file.clone(); @@ -231,11 +203,9 @@ pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> /// read the profile item file data #[tauri::command] -pub fn read_profile_file( - index: String, - profiles_state: State<'_, ProfilesState>, -) -> Result { - let profiles = profiles_state.0.lock().unwrap(); +pub fn read_profile_file(index: String, core: State<'_, Core>) -> CmdResult { + let mut profiles = core.profiles.lock().unwrap(); + let item = wrap_err!(profiles.get_item(&index))?; let data = wrap_err!(item.read_file())?; @@ -247,34 +217,36 @@ pub fn read_profile_file( pub fn save_profile_file( index: String, file_data: Option, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { + core: State<'_, Core>, +) -> CmdResult { if file_data.is_none() { return Ok(()); } - let profiles = profiles_state.0.lock().unwrap(); + let mut profiles = core.profiles.lock().unwrap(); let item = wrap_err!(profiles.get_item(&index))?; wrap_err!(item.save_file(file_data.unwrap())) } /// restart the sidecar #[tauri::command] -pub fn restart_sidecar( - clash_state: State<'_, ClashState>, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { - let mut clash = clash_state.0.lock().unwrap(); - let mut profiles = profiles_state.0.lock().unwrap(); +pub fn restart_sidecar(core: State<'_, Core>) -> CmdResult { + let mut service = core.service.lock().unwrap(); - wrap_err!(clash.restart_sidecar(&mut profiles)) + wrap_err!(service.restart())?; + + // 更新配置 + + log_if_err!(core.activate_enhanced(false, false)); + + Ok(()) } /// 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(clash_state: State<'_, ClashState>) -> Result { - let clash = clash_state.0.lock().unwrap(); +pub fn get_clash_info(core: State<'_, Core>) -> CmdResult { + let clash = core.clash.lock().unwrap(); Ok(clash.info.clone()) } @@ -282,16 +254,8 @@ pub fn get_clash_info(clash_state: State<'_, ClashState>) -> Result, - verge_state: State<'_, VergeState>, - profiles_state: State<'_, ProfilesState>, -) -> Result<(), String> { - let mut clash = clash_state.0.lock().unwrap(); - let mut verge = verge_state.0.lock().unwrap(); - let mut profiles = profiles_state.0.lock().unwrap(); - wrap_err!(clash.patch_config(payload, &mut verge, &mut profiles)) +pub fn patch_clash_config(payload: Mapping, core: State<'_, Core>) -> CmdResult { + wrap_err!(core.patch_clash(payload)) } /// get the system proxy @@ -303,15 +267,15 @@ pub fn get_sys_proxy() -> Result { /// get the current proxy config /// which may not the same as system proxy #[tauri::command] -pub fn get_cur_proxy(verge_state: State<'_, VergeState>) -> Result, String> { - let verge = verge_state.0.lock().unwrap(); +pub fn get_cur_proxy(core: State<'_, Core>) -> CmdResult> { + let verge = core.verge.lock().unwrap(); Ok(verge.cur_sysproxy.clone()) } /// get the verge config #[tauri::command] -pub fn get_verge_config(verge_state: State<'_, VergeState>) -> Result { - let verge = verge_state.0.lock().unwrap(); +pub fn get_verge_config(core: State<'_, Core>) -> CmdResult { + let verge = core.verge.lock().unwrap(); let mut config = verge.config.clone(); if config.system_proxy_bypass.is_none() && verge.cur_sysproxy.is_some() { @@ -327,14 +291,12 @@ pub fn get_verge_config(verge_state: State<'_, VergeState>) -> Result, - verge_state: State<'_, VergeState>, - profiles_state: State<'_, ProfilesState>, + core: State<'_, Core>, ) -> Result<(), String> { let tun_mode = payload.enable_tun_mode.clone(); let system_proxy = payload.enable_system_proxy.clone(); - let mut verge = verge_state.0.lock().unwrap(); + let mut verge = core.verge.lock().unwrap(); wrap_err!(verge.patch_config(payload))?; // change tun mode @@ -348,10 +310,9 @@ pub fn patch_verge_config( } } - let clash = clash_state.0.lock().unwrap(); - let profiles = profiles_state.0.lock().unwrap(); + let profiles = core.profiles.lock().unwrap(); - wrap_err!(clash.activate_enhanced(&profiles, false, false))?; + log_if_err!(core.activate_enhanced(false, false)); } // change system tray diff --git a/src-tauri/src/core/clash copy.rs b/src-tauri/src/core/clash copy.rs new file mode 100644 index 0000000..463ab14 --- /dev/null +++ b/src-tauri/src/core/clash copy.rs @@ -0,0 +1,520 @@ +use super::{PrfEnhancedResult, Profiles, Verge, VergeConfig}; +use crate::log_if_err; +use crate::utils::{config, dirs, help}; +use anyhow::{bail, Result}; +use reqwest::header::HeaderMap; +use serde::{Deserialize, Serialize}; +use serde_yaml::{Mapping, Value}; +use std::{collections::HashMap, time::Duration}; +use tauri::api::process::{Command, CommandChild, CommandEvent}; +use tauri::Window; +use tokio::time::sleep; + +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct ClashInfo { + /// clash sidecar status + pub status: String, + + /// clash core port + pub port: Option, + + /// same as `external-controller` + pub server: Option, + + /// clash secret + pub secret: Option, +} + +pub struct Clash { + /// maintain the clash config + pub config: Mapping, + + /// some info + pub info: ClashInfo, + + /// clash sidecar + pub sidecar: Option, + + /// save the main window + pub window: Option, +} + +impl Clash { + pub fn new() -> Clash { + let config = Clash::read_config(); + let info = Clash::get_info(&config); + + Clash { + config, + info, + sidecar: None, + window: None, + } + } + + /// get clash config + fn read_config() -> Mapping { + config::read_yaml::(dirs::clash_path()) + } + + /// save the clash config + fn save_config(&self) -> Result<()> { + config::save_yaml( + dirs::clash_path(), + &self.config, + Some("# Default Config For Clash Core\n\n"), + ) + } + + /// parse the clash's config.yaml + /// get some information + fn get_info(clash_config: &Mapping) -> ClashInfo { + let key_port_1 = Value::from("port"); + let key_port_2 = Value::from("mixed-port"); + let key_server = Value::from("external-controller"); + let key_secret = Value::from("secret"); + + let port = match clash_config.get(&key_port_1) { + Some(value) => match value { + Value::String(val_str) => Some(val_str.clone()), + Value::Number(val_num) => Some(val_num.to_string()), + _ => None, + }, + _ => None, + }; + let port = match port { + Some(_) => port, + None => match clash_config.get(&key_port_2) { + Some(value) => match value { + Value::String(val_str) => Some(val_str.clone()), + Value::Number(val_num) => Some(val_num.to_string()), + _ => None, + }, + _ => None, + }, + }; + + let server = match clash_config.get(&key_server) { + Some(value) => match value { + Value::String(val_str) => { + // `external-controller` could be + // "127.0.0.1:9090" or ":9090" + // Todo: maybe it could support single port + let server = val_str.clone(); + let server = match server.starts_with(":") { + true => format!("127.0.0.1{server}"), + false => server, + }; + + Some(server) + } + _ => None, + }, + _ => None, + }; + let secret = match clash_config.get(&key_secret) { + Some(value) => match value { + Value::String(val_str) => Some(val_str.clone()), + Value::Bool(val_bool) => Some(val_bool.to_string()), + Value::Number(val_num) => Some(val_num.to_string()), + _ => None, + }, + _ => None, + }; + + ClashInfo { + status: "init".into(), + port, + server, + secret, + } + } + + /// save the main window + pub fn set_window(&mut self, win: Option) { + self.window = win; + } + + /// run clash sidecar + pub fn run_sidecar(&mut self, profiles: &Profiles, delay: bool) -> Result<()> { + let app_dir = dirs::app_home_dir(); + let app_dir = app_dir.as_os_str().to_str().unwrap(); + + let cmd = Command::new_sidecar("clash")?; + let (mut rx, cmd_child) = cmd.args(["-d", app_dir]).spawn()?; + + self.sidecar = Some(cmd_child); + + // clash log + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => log::info!("[clash]: {}", line), + CommandEvent::Stderr(err) => log::error!("[clash]: {}", err), + _ => {} + } + } + }); + + // activate profile + log_if_err!(self.activate(&profiles)); + log_if_err!(self.activate_enhanced(&profiles, delay, true)); + + Ok(()) + } + + /// drop clash sidecar + pub fn drop_sidecar(&mut self) -> Result<()> { + if let Some(sidecar) = self.sidecar.take() { + sidecar.kill()?; + } + Ok(()) + } + + /// restart clash sidecar + /// should reactivate profile after restart + pub fn restart_sidecar(&mut self, profiles: &mut Profiles) -> Result<()> { + self.update_config(); + self.drop_sidecar()?; + self.run_sidecar(profiles, false) + } + + /// update the clash info + pub fn update_config(&mut self) { + self.config = Clash::read_config(); + self.info = Clash::get_info(&self.config); + } + + /// patch update the clash config + pub fn patch_config( + &mut self, + patch: Mapping, + verge: &mut Verge, + profiles: &mut Profiles, + ) -> Result<()> { + let mix_port_key = Value::from("mixed-port"); + let mut port = None; + + for (key, value) in patch.into_iter() { + let value = value.clone(); + + // check whether the mix_port is changed + if key == mix_port_key { + if value.is_number() { + port = value.as_i64().as_ref().map(|n| n.to_string()); + } else { + port = value.as_str().as_ref().map(|s| s.to_string()); + } + } + + self.config.insert(key.clone(), value); + } + + self.save_config()?; + + if let Some(port) = port { + self.restart_sidecar(profiles)?; + verge.init_sysproxy(Some(port)); + } + + Ok(()) + } + + /// revise the `tun` and `dns` config + fn _tun_mode(mut config: Mapping, enable: bool) -> Mapping { + macro_rules! revise { + ($map: expr, $key: expr, $val: expr) => { + let ret_key = Value::String($key.into()); + $map.insert(ret_key, Value::from($val)); + }; + } + + // if key not exists then append value + macro_rules! append { + ($map: expr, $key: expr, $val: expr) => { + let ret_key = Value::String($key.into()); + if !$map.contains_key(&ret_key) { + $map.insert(ret_key, Value::from($val)); + } + }; + } + + // tun config + let tun_val = config.get(&Value::from("tun")); + let mut new_tun = Mapping::new(); + + if tun_val.is_some() && tun_val.as_ref().unwrap().is_mapping() { + new_tun = tun_val.as_ref().unwrap().as_mapping().unwrap().clone(); + } + + revise!(new_tun, "enable", enable); + + if enable { + append!(new_tun, "stack", "gvisor"); + append!(new_tun, "dns-hijack", vec!["198.18.0.2:53"]); + append!(new_tun, "auto-route", true); + append!(new_tun, "auto-detect-interface", true); + } + + revise!(config, "tun", new_tun); + + // dns config + let dns_val = config.get(&Value::from("dns")); + let mut new_dns = Mapping::new(); + + if dns_val.is_some() && dns_val.as_ref().unwrap().is_mapping() { + new_dns = dns_val.as_ref().unwrap().as_mapping().unwrap().clone(); + } + + // 借鉴cfw的默认配置 + revise!(new_dns, "enable", enable); + + if enable { + append!(new_dns, "enhanced-mode", "fake-ip"); + append!( + new_dns, + "nameserver", + vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"] + ); + append!(new_dns, "fallback", vec![] as Vec<&str>); + + #[cfg(target_os = "windows")] + append!( + new_dns, + "fake-ip-filter", + vec![ + "dns.msftncsi.com", + "www.msftncsi.com", + "www.msftconnecttest.com" + ] + ); + } + + revise!(config, "dns", new_dns); + config + } + + /// activate the profile + /// generate a new profile to the temp_dir + /// then put the path to the clash core + fn _activate(info: ClashInfo, config: Mapping, window: Option) -> Result<()> { + let verge_config = VergeConfig::new(); + let tun_enable = verge_config.enable_tun_mode.unwrap_or(false); + + let config = Clash::_tun_mode(config, tun_enable); + + let temp_path = dirs::profiles_temp_path(); + config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?; + + tauri::async_runtime::spawn(async move { + if info.server.is_none() { + return; + } + + 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); + } + + let mut data = HashMap::new(); + data.insert("path", temp_path.as_os_str().to_str().unwrap()); + + // retry 5 times + for _ in 0..5 { + match reqwest::ClientBuilder::new().no_proxy().build() { + Ok(client) => { + let builder = client.put(&server).headers(headers.clone()).json(&data); + + match builder.send().await { + Ok(resp) => { + if resp.status() != 204 { + log::error!("failed to activate clash for status \"{}\"", resp.status()); + } + + // emit the window to update something + if let Some(window) = window { + window.emit("verge://refresh-clash-config", "yes").unwrap(); + } + + // do not retry + break; + } + Err(err) => log::error!("failed to activate for `{err}`"), + } + } + Err(err) => log::error!("failed to activate for `{err}`"), + } + sleep(Duration::from_millis(500)).await; + } + }); + + Ok(()) + } + + /// enhanced profiles mode + /// - (sync) refresh config if enhance chain is null + /// - (async) enhanced config + pub fn activate_enhanced(&self, profiles: &Profiles, delay: bool, skip: bool) -> Result<()> { + if self.window.is_none() { + bail!("failed to get the main window"); + } + + let event_name = help::get_uid("e"); + let event_name = format!("enhanced-cb-{event_name}"); + + // generate the payload + let payload = profiles.gen_enhanced(event_name.clone())?; + + let info = self.info.clone(); + + // do not run enhanced + if payload.chain.len() == 0 { + if skip { + return Ok(()); + } + + let mut config = self.config.clone(); + let filter_data = Clash::strict_filter(payload.current); + + for (key, value) in filter_data.into_iter() { + config.insert(key, value); + } + + return Clash::_activate(info, config, self.window.clone()); + } + + let window = self.window.clone().unwrap(); + let window_move = self.window.clone(); + + window.once(&event_name, move |event| { + if let Some(result) = event.payload() { + let result: PrfEnhancedResult = serde_json::from_str(result).unwrap(); + + if let Some(data) = result.data { + let mut config = Clash::read_config(); + let filter_data = Clash::loose_filter(data); // loose filter + + for (key, value) in filter_data.into_iter() { + config.insert(key, value); + } + + log_if_err!(Clash::_activate(info, config, window_move)); + log::info!("profile enhanced status {}", result.status); + } + + result.error.map(|err| log::error!("{err}")); + } + }); + + tauri::async_runtime::spawn(async move { + // wait the window setup during resolve app + if delay { + sleep(Duration::from_secs(2)).await; + } + window.emit("script-handler", payload).unwrap(); + }); + + Ok(()) + } + + /// activate the profile + /// auto activate enhanced profile + pub fn activate(&self, profiles: &Profiles) -> Result<()> { + let data = profiles.gen_activate()?; + let data = Clash::strict_filter(data); + + let info = self.info.clone(); + let mut config = self.config.clone(); + + for (key, value) in data.into_iter() { + config.insert(key, value); + } + + Clash::_activate(info, config, self.window.clone()) + } + + /// only 5 default fields available (clash config fields) + /// convert to lowercase + fn strict_filter(config: Mapping) -> Mapping { + // Only the following fields are allowed: + // proxies/proxy-providers/proxy-groups/rule-providers/rules + let valid_keys = vec![ + "proxies", + "proxy-providers", + "proxy-groups", + "rules", + "rule-providers", + ]; + + let mut new_config = Mapping::new(); + + for (key, value) in config.into_iter() { + key.as_str().map(|key_str| { + // change to lowercase + let mut key_str = String::from(key_str); + key_str.make_ascii_lowercase(); + + // filter + if valid_keys.contains(&&*key_str) { + new_config.insert(Value::String(key_str), value); + } + }); + } + + new_config + } + + /// more clash config fields available + /// convert to lowercase + fn loose_filter(config: Mapping) -> Mapping { + // all of these can not be revised by script or merge + // http/https/socks port should be under control + let not_allow = vec![ + "port", + "socks-port", + "mixed-port", + "allow-lan", + "mode", + "external-controller", + "secret", + "log-level", + ]; + + let mut new_config = Mapping::new(); + + for (key, value) in config.into_iter() { + key.as_str().map(|key_str| { + // change to lowercase + let mut key_str = String::from(key_str); + key_str.make_ascii_lowercase(); + + // filter + if !not_allow.contains(&&*key_str) { + new_config.insert(Value::String(key_str), value); + } + }); + } + + new_config + } +} + +impl Default for Clash { + fn default() -> Self { + Clash::new() + } +} + +impl Drop for Clash { + fn drop(&mut self) { + if let Err(err) = self.drop_sidecar() { + log::error!("{err}"); + } + } +} diff --git a/src-tauri/src/core/clash.rs b/src-tauri/src/core/clash.rs index 463ab14..2e5d402 100644 --- a/src-tauri/src/core/clash.rs +++ b/src-tauri/src/core/clash.rs @@ -1,14 +1,7 @@ -use super::{PrfEnhancedResult, Profiles, Verge, VergeConfig}; -use crate::log_if_err; -use crate::utils::{config, dirs, help}; -use anyhow::{bail, Result}; -use reqwest::header::HeaderMap; +use crate::utils::{config, dirs}; +use anyhow::Result; use serde::{Deserialize, Serialize}; use serde_yaml::{Mapping, Value}; -use std::{collections::HashMap, time::Duration}; -use tauri::api::process::{Command, CommandChild, CommandEvent}; -use tauri::Window; -use tokio::time::sleep; #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct ClashInfo { @@ -25,56 +18,16 @@ pub struct ClashInfo { pub secret: Option, } -pub struct Clash { - /// maintain the clash config - pub config: Mapping, - - /// some info - pub info: ClashInfo, - - /// clash sidecar - pub sidecar: Option, - - /// save the main window - pub window: Option, -} - -impl Clash { - pub fn new() -> Clash { - let config = Clash::read_config(); - let info = Clash::get_info(&config); - - Clash { - config, - info, - sidecar: None, - window: None, - } - } - - /// get clash config - fn read_config() -> Mapping { - config::read_yaml::(dirs::clash_path()) - } - - /// save the clash config - fn save_config(&self) -> Result<()> { - config::save_yaml( - dirs::clash_path(), - &self.config, - Some("# Default Config For Clash Core\n\n"), - ) - } - +impl ClashInfo { /// parse the clash's config.yaml /// get some information - fn get_info(clash_config: &Mapping) -> ClashInfo { + pub fn from(config: &Mapping) -> ClashInfo { let key_port_1 = Value::from("port"); let key_port_2 = Value::from("mixed-port"); let key_server = Value::from("external-controller"); let key_secret = Value::from("secret"); - let port = match clash_config.get(&key_port_1) { + let port = match config.get(&key_port_1) { Some(value) => match value { Value::String(val_str) => Some(val_str.clone()), Value::Number(val_num) => Some(val_num.to_string()), @@ -84,7 +37,7 @@ impl Clash { }; let port = match port { Some(_) => port, - None => match clash_config.get(&key_port_2) { + None => match config.get(&key_port_2) { Some(value) => match value { Value::String(val_str) => Some(val_str.clone()), Value::Number(val_num) => Some(val_num.to_string()), @@ -94,7 +47,7 @@ impl Clash { }, }; - let server = match clash_config.get(&key_server) { + let server = match config.get(&key_server) { Some(value) => match value { Value::String(val_str) => { // `external-controller` could be @@ -112,7 +65,8 @@ impl Clash { }, _ => None, }; - let secret = match clash_config.get(&key_secret) { + + let secret = match config.get(&key_secret) { Some(value) => match value { Value::String(val_str) => Some(val_str.clone()), Value::Bool(val_bool) => Some(val_bool.to_string()), @@ -129,99 +83,78 @@ impl Clash { secret, } } +} - /// save the main window - pub fn set_window(&mut self, win: Option) { - self.window = win; +pub struct Clash { + /// maintain the clash config + pub config: Mapping, + + /// some info + pub info: ClashInfo, +} + +impl Clash { + pub fn new() -> Clash { + let config = Clash::read_config(); + let info = ClashInfo::from(&config); + + Clash { config, info } } - /// run clash sidecar - pub fn run_sidecar(&mut self, profiles: &Profiles, delay: bool) -> Result<()> { - let app_dir = dirs::app_home_dir(); - let app_dir = app_dir.as_os_str().to_str().unwrap(); - - let cmd = Command::new_sidecar("clash")?; - let (mut rx, cmd_child) = cmd.args(["-d", app_dir]).spawn()?; - - self.sidecar = Some(cmd_child); - - // clash log - tauri::async_runtime::spawn(async move { - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Stdout(line) => log::info!("[clash]: {}", line), - CommandEvent::Stderr(err) => log::error!("[clash]: {}", err), - _ => {} - } - } - }); - - // activate profile - log_if_err!(self.activate(&profiles)); - log_if_err!(self.activate_enhanced(&profiles, delay, true)); - - Ok(()) + /// get clash config + pub fn read_config() -> Mapping { + config::read_yaml::(dirs::clash_path()) } - /// drop clash sidecar - pub fn drop_sidecar(&mut self) -> Result<()> { - if let Some(sidecar) = self.sidecar.take() { - sidecar.kill()?; - } - Ok(()) - } - - /// restart clash sidecar - /// should reactivate profile after restart - pub fn restart_sidecar(&mut self, profiles: &mut Profiles) -> Result<()> { - self.update_config(); - self.drop_sidecar()?; - self.run_sidecar(profiles, false) + /// 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"), + ) } + /// todo: delete /// update the clash info pub fn update_config(&mut self) { self.config = Clash::read_config(); - self.info = Clash::get_info(&self.config); + self.info = ClashInfo::from(&self.config); } /// patch update the clash config - pub fn patch_config( - &mut self, - patch: Mapping, - verge: &mut Verge, - profiles: &mut Profiles, - ) -> Result<()> { - let mix_port_key = Value::from("mixed-port"); - let mut port = None; + /// 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 mut change_port = false; + let mut change_info = false; for (key, value) in patch.into_iter() { - let value = value.clone(); - - // check whether the mix_port is changed - if key == mix_port_key { - if value.is_number() { - port = value.as_i64().as_ref().map(|n| n.to_string()); - } else { - port = value.as_str().as_ref().map(|s| s.to_string()); - } + if key == port_key { + change_port = true; } - self.config.insert(key.clone(), value); + if key == port_key || key == server_key || key == secret_key { + change_info = true; + } + + self.config.insert(key, value); + } + + if change_info { + self.info = ClashInfo::from(&self.config); } self.save_config()?; - if let Some(port) = port { - self.restart_sidecar(profiles)?; - verge.init_sysproxy(Some(port)); - } - - Ok(()) + Ok(change_port) } /// revise the `tun` and `dns` config - fn _tun_mode(mut config: Mapping, enable: bool) -> Mapping { + pub fn _tun_mode(mut config: Mapping, enable: bool) -> Mapping { macro_rules! revise { ($map: expr, $key: expr, $val: expr) => { let ret_key = Value::String($key.into()); @@ -294,154 +227,9 @@ impl Clash { config } - /// activate the profile - /// generate a new profile to the temp_dir - /// then put the path to the clash core - fn _activate(info: ClashInfo, config: Mapping, window: Option) -> Result<()> { - let verge_config = VergeConfig::new(); - let tun_enable = verge_config.enable_tun_mode.unwrap_or(false); - - let config = Clash::_tun_mode(config, tun_enable); - - let temp_path = dirs::profiles_temp_path(); - config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?; - - tauri::async_runtime::spawn(async move { - if info.server.is_none() { - return; - } - - 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); - } - - let mut data = HashMap::new(); - data.insert("path", temp_path.as_os_str().to_str().unwrap()); - - // retry 5 times - for _ in 0..5 { - match reqwest::ClientBuilder::new().no_proxy().build() { - Ok(client) => { - let builder = client.put(&server).headers(headers.clone()).json(&data); - - match builder.send().await { - Ok(resp) => { - if resp.status() != 204 { - log::error!("failed to activate clash for status \"{}\"", resp.status()); - } - - // emit the window to update something - if let Some(window) = window { - window.emit("verge://refresh-clash-config", "yes").unwrap(); - } - - // do not retry - break; - } - Err(err) => log::error!("failed to activate for `{err}`"), - } - } - Err(err) => log::error!("failed to activate for `{err}`"), - } - sleep(Duration::from_millis(500)).await; - } - }); - - Ok(()) - } - - /// enhanced profiles mode - /// - (sync) refresh config if enhance chain is null - /// - (async) enhanced config - pub fn activate_enhanced(&self, profiles: &Profiles, delay: bool, skip: bool) -> Result<()> { - if self.window.is_none() { - bail!("failed to get the main window"); - } - - let event_name = help::get_uid("e"); - let event_name = format!("enhanced-cb-{event_name}"); - - // generate the payload - let payload = profiles.gen_enhanced(event_name.clone())?; - - let info = self.info.clone(); - - // do not run enhanced - if payload.chain.len() == 0 { - if skip { - return Ok(()); - } - - let mut config = self.config.clone(); - let filter_data = Clash::strict_filter(payload.current); - - for (key, value) in filter_data.into_iter() { - config.insert(key, value); - } - - return Clash::_activate(info, config, self.window.clone()); - } - - let window = self.window.clone().unwrap(); - let window_move = self.window.clone(); - - window.once(&event_name, move |event| { - if let Some(result) = event.payload() { - let result: PrfEnhancedResult = serde_json::from_str(result).unwrap(); - - if let Some(data) = result.data { - let mut config = Clash::read_config(); - let filter_data = Clash::loose_filter(data); // loose filter - - for (key, value) in filter_data.into_iter() { - config.insert(key, value); - } - - log_if_err!(Clash::_activate(info, config, window_move)); - log::info!("profile enhanced status {}", result.status); - } - - result.error.map(|err| log::error!("{err}")); - } - }); - - tauri::async_runtime::spawn(async move { - // wait the window setup during resolve app - if delay { - sleep(Duration::from_secs(2)).await; - } - window.emit("script-handler", payload).unwrap(); - }); - - Ok(()) - } - - /// activate the profile - /// auto activate enhanced profile - pub fn activate(&self, profiles: &Profiles) -> Result<()> { - let data = profiles.gen_activate()?; - let data = Clash::strict_filter(data); - - let info = self.info.clone(); - let mut config = self.config.clone(); - - for (key, value) in data.into_iter() { - config.insert(key, value); - } - - Clash::_activate(info, config, self.window.clone()) - } - /// only 5 default fields available (clash config fields) /// convert to lowercase - fn strict_filter(config: Mapping) -> Mapping { + pub fn strict_filter(config: Mapping) -> Mapping { // Only the following fields are allowed: // proxies/proxy-providers/proxy-groups/rule-providers/rules let valid_keys = vec![ @@ -472,7 +260,7 @@ impl Clash { /// more clash config fields available /// convert to lowercase - fn loose_filter(config: Mapping) -> Mapping { + pub fn loose_filter(config: Mapping) -> Mapping { // all of these can not be revised by script or merge // http/https/socks port should be under control let not_allow = vec![ @@ -510,11 +298,3 @@ impl Default for Clash { Clash::new() } } - -impl Drop for Clash { - fn drop(&mut self) { - if let Err(err) = self.drop_sidecar() { - log::error!("{err}"); - } - } -} diff --git a/src-tauri/src/core/enhance.rs b/src-tauri/src/core/enhance.rs new file mode 100644 index 0000000..a1c79e8 --- /dev/null +++ b/src-tauri/src/core/enhance.rs @@ -0,0 +1,66 @@ +use super::prfitem::PrfItem; +use crate::utils::{config, dirs}; +use serde::{Deserialize, Serialize}; +use serde_yaml::Mapping; +use std::fs; + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct PrfEnhanced { + pub current: Mapping, + + pub chain: Vec, + + pub valid: Vec, + + pub callback: String, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct PrfEnhancedResult { + pub data: Option, + + pub status: String, + + pub error: Option, +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize)] +pub struct PrfData { + item: PrfItem, + + #[serde(skip_serializing_if = "Option::is_none")] + merge: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + script: Option, +} + +impl PrfData { + pub fn from_item(item: &PrfItem) -> Option { + match item.itype.as_ref() { + Some(itype) => { + let file = item.file.clone()?; + let path = dirs::app_profiles_dir().join(file); + + if !path.exists() { + return None; + } + + match itype.as_str() { + "script" => Some(PrfData { + item: item.clone(), + script: Some(fs::read_to_string(path).unwrap_or("".into())), + merge: None, + }), + "merge" => Some(PrfData { + item: item.clone(), + merge: Some(config::read_yaml::(path)), + script: None, + }), + _ => None, + } + } + None => None, + } + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 1cf8f1e..caed860 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,7 +1,227 @@ +use self::notice::Notice; +use self::service::Service; +use crate::core::enhance::PrfEnhancedResult; +use crate::log_if_err; +use crate::utils::{config, dirs, help}; +use anyhow::{bail, Result}; +use serde_yaml::Mapping; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use tauri::Window; +use tokio::time::sleep; + mod clash; +mod enhance; +mod notice; +mod prfitem; mod profiles; +mod service; +mod timer; mod verge; pub use self::clash::*; +pub use self::prfitem::*; pub use self::profiles::*; pub use self::verge::*; + +#[derive(Clone)] +pub struct Core { + pub clash: Arc>, + + pub verge: Arc>, + + pub profiles: Arc>, + + pub service: Arc>, + + pub window: Arc>>, +} + +impl Core { + pub fn new() -> Core { + let clash = Clash::new(); + let verge = Verge::new(); + let profiles = Profiles::new(); + let service = Service::new(); + + Core { + clash: Arc::new(Mutex::new(clash)), + verge: Arc::new(Mutex::new(verge)), + profiles: Arc::new(Mutex::new(profiles)), + service: Arc::new(Mutex::new(service)), + window: Arc::new(Mutex::new(None)), + } + } + + pub fn init(&self) { + log_if_err!(self.restart_clash()); + + let clash = self.clash.lock().unwrap(); + let mut verge = self.verge.lock().unwrap(); + verge.init_sysproxy(clash.info.port.clone()); + + log_if_err!(verge.init_launch()); + + // system tray + // verge.config.enable_system_proxy.map(|enable| { + // log_if_err!(app + // .tray_handle() + // .get_item("system_proxy") + // .set_selected(enable)); + // }); + } + + /// save the window instance + pub fn set_win(&self, win: Option) { + let mut window = self.window.lock().unwrap(); + *window = win; + } + + /// restart the clash sidecar + pub fn restart_clash(&self) -> Result<()> { + { + let mut service = self.service.lock().unwrap(); + service.restart()?; + } + + self.activate()?; + self.activate_enhanced(false, true) + } + + /// handle the clash config changed + pub fn patch_clash(&self, patch: Mapping) -> Result<()> { + let (changed, port) = { + let mut clash = self.clash.lock().unwrap(); + (clash.patch_config(patch)?, clash.info.port.clone()) + }; + + // todo: port check + + if changed { + let mut service = self.service.lock().unwrap(); + service.restart()?; + + self.activate()?; + self.activate_enhanced(false, true)?; + + let mut verge = self.verge.lock().unwrap(); + verge.init_sysproxy(port); + } + + Ok(()) + } + + /// activate the profile + /// auto activate enhanced profile + pub fn activate(&self) -> Result<()> { + let data = { + let profiles = self.profiles.lock().unwrap(); + let data = profiles.gen_activate()?; + Clash::strict_filter(data) + }; + + let (mut config, info) = { + let clash = self.clash.lock().unwrap(); + let config = clash.config.clone(); + let info = clash.info.clone(); + (config, info) + }; + + for (key, value) in data.into_iter() { + config.insert(key, value); + } + + let config = { + let verge = self.verge.lock().unwrap(); + let tun_mode = verge.config.enable_tun_mode.unwrap_or(false); + Clash::_tun_mode(config, tun_mode) + }; + + let notice = { + let window = self.window.lock().unwrap(); + Notice::from(window.clone()) + }; + + let service = self.service.lock().unwrap(); + service.set_config(info, config, notice) + } + + /// enhanced profiles mode + pub fn activate_enhanced(&self, delay: bool, skip: bool) -> Result<()> { + let window = self.window.lock().unwrap(); + if window.is_none() { + bail!("failed to get the main window"); + } + + let event_name = help::get_uid("e"); + let event_name = format!("enhanced-cb-{event_name}"); + + // generate the payload + let payload = { + let profiles = self.profiles.lock().unwrap(); + profiles.gen_enhanced(event_name.clone())? + }; + + // do not run enhanced + if payload.chain.len() == 0 { + if skip { + return Ok(()); + } + + return self.activate(); + } + + let tun_mode = { + let verge = self.verge.lock().unwrap(); + verge.config.enable_tun_mode.unwrap_or(false) + }; + + let info = { + let clash = self.clash.lock().unwrap(); + clash.info.clone() + }; + + let notice = Notice::from(window.clone()); + let service = self.service.clone(); + + let window = window.clone().unwrap(); + window.once(&event_name, move |event| { + let result = event.payload(); + + if result.is_none() { + log::warn!("event payload result is none"); + return; + } + + let result = result.unwrap(); + let result: PrfEnhancedResult = serde_json::from_str(result).unwrap(); + + if let Some(data) = result.data { + let mut config = Clash::read_config(); + let filter_data = Clash::loose_filter(data); // loose filter + + for (key, value) in filter_data.into_iter() { + config.insert(key, value); + } + + let config = Clash::_tun_mode(config, tun_mode); + + let service = service.lock().unwrap(); + log_if_err!(service.set_config(info, config, notice)); + + log::info!("profile enhanced status {}", result.status); + } + + result.error.map(|err| log::error!("{err}")); + }); + + // wait the window setup during resolve app + // if delay { + // sleep(Duration::from_secs(2)).await; + // } + + window.emit("script-handler", payload).unwrap(); + + Ok(()) + } +} diff --git a/src-tauri/src/core/notice.rs b/src-tauri/src/core/notice.rs new file mode 100644 index 0000000..8e942da --- /dev/null +++ b/src-tauri/src/core/notice.rs @@ -0,0 +1,35 @@ +use crate::log_if_err; +use tauri::Window; + +#[derive(Debug, Default, Clone)] +pub struct Notice { + win: Option, +} + +impl Notice { + pub fn from(win: Option) -> Notice { + Notice { win } + } + + pub fn set_win(&mut self, win: Option) { + self.win = win; + } + + pub fn refresh_clash(&self) { + if let Some(window) = self.win.as_ref() { + log_if_err!(window.emit("verge://refresh-clash-config", "yes")); + } + } + + pub fn refresh_verge(&self) { + if let Some(window) = self.win.as_ref() { + log_if_err!(window.emit("verge://refresh-verge-config", "yes")); + } + } + + pub fn refresh_profiles(&self) { + if let Some(window) = self.win.as_ref() { + log_if_err!(window.emit("verge://refresh-profiles-config", "yes")); + } + } +} diff --git a/src-tauri/src/core/prfitem.rs b/src-tauri/src/core/prfitem.rs new file mode 100644 index 0000000..3d99906 --- /dev/null +++ b/src-tauri/src/core/prfitem.rs @@ -0,0 +1,320 @@ +use crate::utils::{dirs, help, tmpl}; +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +use std::fs; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PrfItem { + pub uid: Option, + + /// profile item type + /// enum value: remote | local | script | merge + #[serde(rename = "type")] + pub itype: Option, + + /// profile name + pub name: Option, + + /// profile description + #[serde(skip_serializing_if = "Option::is_none")] + pub desc: Option, + + /// profile file + pub file: Option, + + /// source url + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + + /// selected infomation + #[serde(skip_serializing_if = "Option::is_none")] + pub selected: Option>, + + /// subscription user info + #[serde(skip_serializing_if = "Option::is_none")] + pub extra: Option, + + /// updated time + pub updated: Option, + + /// some options of the item + #[serde(skip_serializing_if = "Option::is_none")] + pub option: Option, + + /// the file data + #[serde(skip)] + pub file_data: Option, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct PrfSelected { + pub name: Option, + pub now: Option, +} + +#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)] +pub struct PrfExtra { + pub upload: usize, + pub download: usize, + pub total: usize, + pub expire: usize, +} + +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct PrfOption { + /// for `remote` profile's http request + /// see issue #13 + #[serde(skip_serializing_if = "Option::is_none")] + pub user_agent: Option, + + /// for `remote` profile + #[serde(skip_serializing_if = "Option::is_none")] + pub with_proxy: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub update_interval: Option, +} + +impl PrfOption { + pub fn merge(one: Option, other: Option) -> Option { + if one.is_none() { + return other; + } + + if one.is_some() && other.is_some() { + let mut one = one.unwrap(); + let other = other.unwrap(); + + if let Some(val) = other.user_agent { + one.user_agent = Some(val); + } + + if let Some(val) = other.with_proxy { + one.with_proxy = Some(val); + } + + if let Some(val) = other.update_interval { + one.update_interval = Some(val); + } + + return Some(one); + } + + return one; + } + + pub fn diff_update_interval(one: Option<&Self>, other: Option<&Self>) -> bool { + if one.is_some() && other.is_some() { + let one = one.unwrap(); + let other = other.unwrap(); + + return one.update_interval == other.update_interval; + } + + return false; + } +} + +impl Default for PrfItem { + fn default() -> Self { + PrfItem { + uid: None, + itype: None, + name: None, + desc: None, + file: None, + url: None, + selected: None, + extra: None, + updated: None, + option: None, + file_data: None, + } + } +} + +impl PrfItem { + /// From partial item + /// must contain `itype` + pub async fn from(item: PrfItem, file_data: Option) -> Result { + if item.itype.is_none() { + bail!("type should not be null"); + } + + match item.itype.unwrap().as_str() { + "remote" => { + if item.url.is_none() { + bail!("url should not be null"); + } + let url = item.url.as_ref().unwrap().as_str(); + let name = item.name; + let desc = item.desc; + PrfItem::from_url(url, name, desc, item.option).await + } + "local" => { + let name = item.name.unwrap_or("Local File".into()); + let desc = item.desc.unwrap_or("".into()); + PrfItem::from_local(name, desc, file_data) + } + "merge" => { + let name = item.name.unwrap_or("Merge".into()); + let desc = item.desc.unwrap_or("".into()); + PrfItem::from_merge(name, desc) + } + "script" => { + let name = item.name.unwrap_or("Script".into()); + let desc = item.desc.unwrap_or("".into()); + PrfItem::from_script(name, desc) + } + typ @ _ => bail!("invalid type \"{typ}\""), + } + } + + /// ## Local type + /// create a new item from name/desc + pub fn from_local(name: String, desc: String, file_data: Option) -> Result { + let uid = help::get_uid("l"); + let file = format!("{uid}.yaml"); + + Ok(PrfItem { + uid: Some(uid), + itype: Some("local".into()), + name: Some(name), + desc: Some(desc), + file: Some(file), + url: None, + selected: None, + extra: None, + option: None, + updated: Some(help::get_now()), + file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())), + }) + } + + /// ## Remote type + /// create a new item from url + pub async fn from_url( + url: &str, + name: Option, + desc: Option, + option: Option, + ) -> Result { + let with_proxy = match option.as_ref() { + Some(opt) => opt.with_proxy.unwrap_or(false), + None => false, + }; + let user_agent = match option.as_ref() { + Some(opt) => opt.user_agent.clone(), + None => None, + }; + + let mut builder = reqwest::ClientBuilder::new(); + + if !with_proxy { + builder = builder.no_proxy(); + } + + builder = builder.user_agent(user_agent.unwrap_or("clash-verge/v0.1.0".into())); + + let resp = builder.build()?.get(url).send().await?; + let header = resp.headers(); + + // parse the Subscription Userinfo + let extra = match header.get("Subscription-Userinfo") { + Some(value) => { + let sub_info = value.to_str().unwrap_or(""); + + Some(PrfExtra { + upload: help::parse_str(sub_info, "upload=").unwrap_or(0), + download: help::parse_str(sub_info, "download=").unwrap_or(0), + total: help::parse_str(sub_info, "total=").unwrap_or(0), + expire: help::parse_str(sub_info, "expire=").unwrap_or(0), + }) + } + None => None, + }; + + let uid = help::get_uid("r"); + let file = format!("{uid}.yaml"); + let name = name.unwrap_or(uid.clone()); + let data = resp.text_with_charset("utf-8").await?; + + Ok(PrfItem { + uid: Some(uid), + itype: Some("remote".into()), + name: Some(name), + desc, + file: Some(file), + url: Some(url.into()), + selected: None, + extra, + option, + updated: Some(help::get_now()), + file_data: Some(data), + }) + } + + /// ## Merge type (enhance) + /// create the enhanced item by using `merge` rule + pub fn from_merge(name: String, desc: String) -> Result { + let uid = help::get_uid("m"); + let file = format!("{uid}.yaml"); + + Ok(PrfItem { + uid: Some(uid), + itype: Some("merge".into()), + name: Some(name), + desc: Some(desc), + file: Some(file), + url: None, + selected: None, + extra: None, + option: None, + updated: Some(help::get_now()), + file_data: Some(tmpl::ITEM_MERGE.into()), + }) + } + + /// ## Script type (enhance) + /// create the enhanced item by using javascript(browserjs) + pub fn from_script(name: String, desc: String) -> Result { + let uid = help::get_uid("s"); + let file = format!("{uid}.js"); // js ext + + Ok(PrfItem { + uid: Some(uid), + itype: Some("script".into()), + name: Some(name), + desc: Some(desc), + file: Some(file), + url: None, + selected: None, + extra: None, + option: None, + updated: Some(help::get_now()), + file_data: Some(tmpl::ITEM_SCRIPT.into()), + }) + } + + /// get the file data + pub fn read_file(&self) -> Result { + if self.file.is_none() { + bail!("could not find the file"); + } + + let file = self.file.clone().unwrap(); + let path = dirs::app_profiles_dir().join(file); + fs::read_to_string(path).context("failed to read the file") + } + + /// save the file data + pub fn save_file(&self, data: String) -> Result<()> { + if self.file.is_none() { + bail!("could not find the file"); + } + + let file = self.file.clone().unwrap(); + let path = dirs::app_profiles_dir().join(file); + fs::write(path, data.as_bytes()).context("failed to save the file") + } +} diff --git a/src-tauri/src/core/profiles.rs b/src-tauri/src/core/profiles.rs index 1955526..af37f9c 100644 --- a/src-tauri/src/core/profiles.rs +++ b/src-tauri/src/core/profiles.rs @@ -1,307 +1,11 @@ -use crate::utils::{config, dirs, help, tmpl}; +use super::enhance::{PrfData, PrfEnhanced}; +use super::prfitem::PrfItem; +use crate::utils::{config, dirs, help}; use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_yaml::Mapping; use std::{fs, io::Write}; -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct PrfItem { - pub uid: Option, - - /// profile item type - /// enum value: remote | local | script | merge - #[serde(rename = "type")] - pub itype: Option, - - /// profile name - pub name: Option, - - /// profile description - #[serde(skip_serializing_if = "Option::is_none")] - pub desc: Option, - - /// profile file - pub file: Option, - - /// source url - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, - - /// selected infomation - #[serde(skip_serializing_if = "Option::is_none")] - pub selected: Option>, - - /// subscription user info - #[serde(skip_serializing_if = "Option::is_none")] - pub extra: Option, - - /// updated time - pub updated: Option, - - /// some options of the item - #[serde(skip_serializing_if = "Option::is_none")] - pub option: Option, - - /// the file data - #[serde(skip)] - pub file_data: Option, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct PrfSelected { - pub name: Option, - pub now: Option, -} - -#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)] -pub struct PrfExtra { - pub upload: usize, - pub download: usize, - pub total: usize, - pub expire: usize, -} - -#[derive(Default, Debug, Clone, Deserialize, Serialize)] -pub struct PrfOption { - /// for `remote` profile's http request - /// see issue #13 - #[serde(skip_serializing_if = "Option::is_none")] - pub user_agent: Option, - - /// for `remote` profile - #[serde(skip_serializing_if = "Option::is_none")] - pub with_proxy: Option, -} - -impl PrfOption { - pub fn merge(one: Option, other: Option) -> Option { - if one.is_some() && other.is_some() { - let mut one = one.unwrap(); - let other = other.unwrap(); - - if let Some(val) = other.user_agent { - one.user_agent = Some(val); - } - - if let Some(val) = other.with_proxy { - one.with_proxy = Some(val); - } - - return Some(one); - } - - if one.is_none() { - return other; - } - - return one; - } -} - -impl Default for PrfItem { - fn default() -> Self { - PrfItem { - uid: None, - itype: None, - name: None, - desc: None, - file: None, - url: None, - selected: None, - extra: None, - updated: None, - option: None, - file_data: None, - } - } -} - -impl PrfItem { - /// From partial item - /// must contain `itype` - pub async fn from(item: PrfItem, file_data: Option) -> Result { - if item.itype.is_none() { - bail!("type should not be null"); - } - - match item.itype.unwrap().as_str() { - "remote" => { - if item.url.is_none() { - bail!("url should not be null"); - } - let url = item.url.as_ref().unwrap().as_str(); - let name = item.name; - let desc = item.desc; - PrfItem::from_url(url, name, desc, item.option).await - } - "local" => { - let name = item.name.unwrap_or("Local File".into()); - let desc = item.desc.unwrap_or("".into()); - PrfItem::from_local(name, desc, file_data) - } - "merge" => { - let name = item.name.unwrap_or("Merge".into()); - let desc = item.desc.unwrap_or("".into()); - PrfItem::from_merge(name, desc) - } - "script" => { - let name = item.name.unwrap_or("Script".into()); - let desc = item.desc.unwrap_or("".into()); - PrfItem::from_script(name, desc) - } - typ @ _ => bail!("invalid type \"{typ}\""), - } - } - - /// ## Local type - /// create a new item from name/desc - pub fn from_local(name: String, desc: String, file_data: Option) -> Result { - let uid = help::get_uid("l"); - let file = format!("{uid}.yaml"); - - Ok(PrfItem { - uid: Some(uid), - itype: Some("local".into()), - name: Some(name), - desc: Some(desc), - file: Some(file), - url: None, - selected: None, - extra: None, - option: None, - updated: Some(help::get_now()), - file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())), - }) - } - - /// ## Remote type - /// create a new item from url - pub async fn from_url( - url: &str, - name: Option, - desc: Option, - option: Option, - ) -> Result { - let with_proxy = match option.as_ref() { - Some(opt) => opt.with_proxy.unwrap_or(false), - None => false, - }; - let user_agent = match option.as_ref() { - Some(opt) => opt.user_agent.clone(), - None => None, - }; - - let mut builder = reqwest::ClientBuilder::new(); - - if !with_proxy { - builder = builder.no_proxy(); - } - - builder = builder.user_agent(user_agent.unwrap_or("clash-verge/v0.1.0".into())); - - let resp = builder.build()?.get(url).send().await?; - let header = resp.headers(); - - // parse the Subscription Userinfo - let extra = match header.get("Subscription-Userinfo") { - Some(value) => { - let sub_info = value.to_str().unwrap_or(""); - - Some(PrfExtra { - upload: help::parse_str(sub_info, "upload=").unwrap_or(0), - download: help::parse_str(sub_info, "download=").unwrap_or(0), - total: help::parse_str(sub_info, "total=").unwrap_or(0), - expire: help::parse_str(sub_info, "expire=").unwrap_or(0), - }) - } - None => None, - }; - - let uid = help::get_uid("r"); - let file = format!("{uid}.yaml"); - let name = name.unwrap_or(uid.clone()); - let data = resp.text_with_charset("utf-8").await?; - - Ok(PrfItem { - uid: Some(uid), - itype: Some("remote".into()), - name: Some(name), - desc, - file: Some(file), - url: Some(url.into()), - selected: None, - extra, - option, - updated: Some(help::get_now()), - file_data: Some(data), - }) - } - - /// ## Merge type (enhance) - /// create the enhanced item by using `merge` rule - pub fn from_merge(name: String, desc: String) -> Result { - let uid = help::get_uid("m"); - let file = format!("{uid}.yaml"); - - Ok(PrfItem { - uid: Some(uid), - itype: Some("merge".into()), - name: Some(name), - desc: Some(desc), - file: Some(file), - url: None, - selected: None, - extra: None, - option: None, - updated: Some(help::get_now()), - file_data: Some(tmpl::ITEM_MERGE.into()), - }) - } - - /// ## Script type (enhance) - /// create the enhanced item by using javascript(browserjs) - pub fn from_script(name: String, desc: String) -> Result { - let uid = help::get_uid("s"); - let file = format!("{uid}.js"); // js ext - - Ok(PrfItem { - uid: Some(uid), - itype: Some("script".into()), - name: Some(name), - desc: Some(desc), - file: Some(file), - url: None, - selected: None, - extra: None, - option: None, - updated: Some(help::get_now()), - file_data: Some(tmpl::ITEM_SCRIPT.into()), - }) - } - - /// get the file data - pub fn read_file(&self) -> Result { - if self.file.is_none() { - bail!("could not find the file"); - } - - let file = self.file.clone().unwrap(); - let path = dirs::app_profiles_dir().join(file); - fs::read_to_string(path).context("failed to read the file") - } - - /// save the file data - pub fn save_file(&self, data: String) -> Result<()> { - if self.file.is_none() { - bail!("could not find the file"); - } - - let file = self.file.clone().unwrap(); - let path = dirs::app_profiles_dir().join(file); - fs::write(path, data.as_bytes()).context("failed to save the file") - } -} - /// /// ## Profiles Config /// @@ -331,6 +35,10 @@ macro_rules! patch { } impl Profiles { + 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()); @@ -616,64 +324,3 @@ impl Profiles { }) } } - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct PrfEnhanced { - pub current: Mapping, - - pub chain: Vec, - - pub valid: Vec, - - pub callback: String, -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct PrfEnhancedResult { - pub data: Option, - - pub status: String, - - pub error: Option, -} - -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct PrfData { - item: PrfItem, - - #[serde(skip_serializing_if = "Option::is_none")] - merge: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - script: Option, -} - -impl PrfData { - pub fn from_item(item: &PrfItem) -> Option { - match item.itype.as_ref() { - Some(itype) => { - let file = item.file.clone()?; - let path = dirs::app_profiles_dir().join(file); - - if !path.exists() { - return None; - } - - match itype.as_str() { - "script" => Some(PrfData { - item: item.clone(), - script: Some(fs::read_to_string(path).unwrap_or("".into())), - merge: None, - }), - "merge" => Some(PrfData { - item: item.clone(), - merge: Some(config::read_yaml::(path)), - script: None, - }), - _ => None, - } - } - None => None, - } - } -} diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs new file mode 100644 index 0000000..b1eb42e --- /dev/null +++ b/src-tauri/src/core/service.rs @@ -0,0 +1,121 @@ +use super::{notice::Notice, ClashInfo}; +use crate::log_if_err; +use crate::utils::{config, dirs}; +use anyhow::{bail, Result}; +use reqwest::header::HeaderMap; +use serde_yaml::Mapping; +use std::{collections::HashMap, time::Duration}; +use tauri::api::process::{Command, CommandChild, CommandEvent}; +use tokio::time::sleep; + +#[derive(Debug)] +pub struct Service { + sidecar: Option, +} + +impl Service { + pub fn new() -> Service { + Service { sidecar: None } + } + + pub fn start(&mut self) -> Result<()> { + if self.sidecar.is_some() { + bail!("could not run clash sidecar twice"); + } + + let app_dir = dirs::app_home_dir(); + let app_dir = app_dir.as_os_str().to_str().unwrap(); + + let cmd = Command::new_sidecar("clash")?; + let (mut rx, cmd_child) = cmd.args(["-d", app_dir]).spawn()?; + + self.sidecar = Some(cmd_child); + + // clash log + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(line) => log::info!("[clash]: {}", line), + CommandEvent::Stderr(err) => log::error!("[clash]: {}", err), + _ => {} + } + } + }); + + Ok(()) + } + + pub fn stop(&mut self) -> Result<()> { + if let Some(sidecar) = self.sidecar.take() { + sidecar.kill()?; + } + Ok(()) + } + + pub fn restart(&mut self) -> Result<()> { + self.stop()?; + self.start() + } + + pub fn set_config(&self, info: ClashInfo, config: Mapping, notice: Notice) -> Result<()> { + if self.sidecar.is_none() { + bail!("did not start sidecar"); + } + + let temp_path = dirs::profiles_temp_path(); + config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?; + + if info.server.is_none() { + bail!("failed to parse the server"); + } + + 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); + } + + tauri::async_runtime::spawn(async move { + let mut data = HashMap::new(); + data.insert("path", temp_path.as_os_str().to_str().unwrap()); + + // retry 5 times + for _ in 0..5 { + match reqwest::ClientBuilder::new().no_proxy().build() { + Ok(client) => { + let builder = client.put(&server).headers(headers.clone()).json(&data); + + match builder.send().await { + Ok(resp) => { + if resp.status() != 204 { + log::error!("failed to activate clash with status \"{}\"", resp.status()); + } + + notice.refresh_clash(); + + // do not retry + break; + } + Err(err) => log::error!("failed to activate for `{err}`"), + } + } + Err(err) => log::error!("failed to activate for `{err}`"), + } + sleep(Duration::from_millis(500)).await; + } + }); + + Ok(()) + } +} + +impl Drop for Service { + fn drop(&mut self) { + log_if_err!(self.stop()); + } +} diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs new file mode 100644 index 0000000..2e94a6f --- /dev/null +++ b/src-tauri/src/core/timer.rs @@ -0,0 +1,20 @@ +use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, Task, TaskBuilder}; +use std::collections::HashMap; + +pub struct Timer { + delay_timer: DelayTimer, + + timer_map: HashMap, + + timer_count: u64, +} + +impl Timer { + pub fn new() -> Self { + Timer { + delay_timer: DelayTimerBuilder::default().build(), + timer_map: HashMap::new(), + timer_count: 1, + } + } +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 77e99fa..52790e7 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -5,7 +5,6 @@ mod cmds; mod core; -mod states; mod utils; use crate::{ @@ -39,9 +38,7 @@ fn main() -> std::io::Result<()> { #[allow(unused_mut)] let mut builder = tauri::Builder::default() - .manage(states::VergeState::default()) - .manage(states::ClashState::default()) - .manage(states::ProfilesState::default()) + .manage(core::Core::new()) .setup(|app| Ok(resolve::resolve_setup(app))) .system_tray(SystemTray::new().with_menu(tray_menu)) .on_system_tray_event(move |app_handle, event| match event { @@ -53,8 +50,8 @@ fn main() -> std::io::Result<()> { window.set_focus().unwrap(); } "system_proxy" => { - let verge_state = app_handle.state::(); - let mut verge = verge_state.0.lock().unwrap(); + let core = app_handle.state::(); + let mut verge = core.verge.lock().unwrap(); let old_value = verge.config.enable_system_proxy.clone().unwrap_or(false); let new_value = !old_value; @@ -68,8 +65,8 @@ fn main() -> std::io::Result<()> { } } "tun_mode" => { - let verge_state = app_handle.state::(); - let mut verge = verge_state.0.lock().unwrap(); + let core = app_handle.state::(); + let mut verge = core.verge.lock().unwrap(); let old_value = verge.config.enable_tun_mode.clone().unwrap_or(false); let new_value = !old_value; @@ -83,12 +80,8 @@ fn main() -> std::io::Result<()> { } } "restart_clash" => { - let clash_state = app_handle.state::(); - let profiles_state = app_handle.state::(); - let mut clash = clash_state.0.lock().unwrap(); - let mut profiles = profiles_state.0.lock().unwrap(); - - crate::log_if_err!(clash.restart_sidecar(&mut profiles)); + let core = app_handle.state::(); + crate::log_if_err!(core.restart_clash()); } "quit" => { resolve::resolve_reset(app_handle); diff --git a/src-tauri/src/states.rs b/src-tauri/src/states.rs index 1e10192..c22364f 100644 --- a/src-tauri/src/states.rs +++ b/src-tauri/src/states.rs @@ -1,11 +1,11 @@ -use crate::core::{Clash, Profiles, Verge}; -use std::sync::{Arc, Mutex}; +// use crate::core::{Clash, Profiles, Verge}; +// use std::sync::{Arc, Mutex}; -#[derive(Default)] -pub struct ProfilesState(pub Arc>); +// #[derive(Default)] +// pub struct ProfilesState(pub Arc>); -#[derive(Default)] -pub struct ClashState(pub Arc>); +// #[derive(Default)] +// pub struct ClashState(pub Arc>); -#[derive(Default)] -pub struct VergeState(pub Arc>); +// #[derive(Default)] +// pub struct VergeState(pub Arc>); diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 8d69342..5678357 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -1,5 +1,5 @@ use crate::log_if_err; -use crate::{core::Profiles, states, utils::init, utils::server}; +use crate::{core::Core, utils::init, utils::server}; use tauri::{App, AppHandle, Manager}; /// handle something when start app @@ -11,36 +11,21 @@ pub fn resolve_setup(app: &App) { init::init_app(app.package_info()); // init states - let clash_state = app.state::(); - let verge_state = app.state::(); - let profiles_state = app.state::(); + let core = app.state::(); - let mut clash = clash_state.0.lock().unwrap(); - let mut verge = verge_state.0.lock().unwrap(); - let mut profiles = profiles_state.0.lock().unwrap(); + core.set_win(app.get_window("main")); + core.init(); - *profiles = Profiles::read_file(); + // clash.set_window(app.get_window("main")); + // log_if_err!(clash.run_sidecar(&profiles, true)); - clash.set_window(app.get_window("main")); - log_if_err!(clash.run_sidecar(&profiles, true)); - - verge.init_sysproxy(clash.info.port.clone()); - log_if_err!(verge.init_launch()); - - verge.config.enable_system_proxy.map(|enable| { - log_if_err!(app - .tray_handle() - .get_item("system_proxy") - .set_selected(enable)); - }); - - resolve_window(app, verge.config.enable_silent_start.clone()); + resolve_window(app, None); } /// reset system proxy pub fn resolve_reset(app_handle: &AppHandle) { - let verge_state = app_handle.state::(); - let mut verge = verge_state.0.lock().unwrap(); + let core = app_handle.state::(); + let mut verge = core.verge.lock().unwrap(); verge.reset_sysproxy(); }