From d6f9bb42569167e3de9104192238e04b3d3f237b Mon Sep 17 00:00:00 2001 From: oomeow Date: Sat, 8 Nov 2025 20:24:18 +0800 Subject: [PATCH] refactor: single instance and handle deep links --- src-tauri/Cargo.lock | 17 ++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 36 ++++++------- src-tauri/src/utils/init.rs | 51 ------------------ src-tauri/src/utils/resolve/mod.rs | 5 -- src-tauri/src/utils/server.rs | 87 ++---------------------------- 6 files changed, 39 insertions(+), 158 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b7bf8c381..0793b48a5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1171,6 +1171,7 @@ dependencies = [ "tauri-plugin-notification", "tauri-plugin-process", "tauri-plugin-shell", + "tauri-plugin-single-instance", "tauri-plugin-updater", "tauri-plugin-window-state", "tokio", @@ -7775,6 +7776,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd707f8c86b4e3004e2c141fa24351f1909ba40ce1b8437e30d5ed5277dd3710" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-plugin-deep-link", + "thiserror 2.0.17", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + [[package]] name = "tauri-plugin-updater" version = "2.9.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1c252fdf3..242c6252c 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -119,6 +119,7 @@ signal-hook = "0.3.18" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-autostart = "2.5.1" tauri-plugin-global-shortcut = "2.3.1" +tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } tauri-plugin-updater = "2.9.0" [features] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 01573ef46..82b1e4875 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -20,7 +20,7 @@ use crate::utils::window_manager::WindowManager; use crate::{ core::{EventDrivenProxyManager, handle, hotkey}, process::AsyncHandler, - utils::{resolve, server}, + utils::resolve, }; use anyhow::Result; use config::Config; @@ -37,21 +37,24 @@ i18n!("locales", fallback = "zh"); pub static APP_HANDLE: OnceCell = OnceCell::new(); /// Application initialization helper functions mod app_init { - use super::*; + use crate::{module::lightweight, utils::window_manager::WindowManager}; - /// Initialize singleton monitoring for other instances - pub fn init_singleton_check() -> Result<()> { - tauri::async_runtime::block_on(async move { - logging!(info, Type::Setup, "开始检查单例实例..."); - server::check_singleton().await?; - Ok(()) - }) - } + use super::*; /// Setup plugins for the Tauri builder pub fn setup_plugins(builder: tauri::Builder) -> tauri::Builder { #[allow(unused_mut)] let mut builder = builder + .plugin(tauri_plugin_single_instance::init(|_app, _args, _cwd| { + AsyncHandler::block_on(async move { + logging!(info, Type::Window, "检测到从单例模式恢复应用窗口"); + if !lightweight::exit_lightweight_mode().await { + WindowManager::show_main_window().await; + } else { + logging!(error, Type::Window, "轻量模式退出失败,无法恢复应用窗口"); + }; + }); + })) .plugin(tauri_plugin_notification::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_clipboard_manager::init()) @@ -88,12 +91,6 @@ mod app_init { /// Setup deep link handling pub fn setup_deep_links(app: &tauri::App) -> Result<(), Box> { - #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] - { - logging!(info, Type::Setup, "注册深层链接..."); - app.deep_link().register_all()?; - } - app.deep_link().on_open_url(|event| { let urls = event.urls(); AsyncHandler::spawn(move || async move { @@ -229,9 +226,10 @@ mod app_init { } pub fn run() { - if app_init::init_singleton_check().is_err() { - return; - } + // if app_init::init_singleton_check().is_err() { + // println!("app exists"); + // return; + // } let _ = utils::dirs::init_portable_flag(); diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index cb8d3e6c8..a2f7de0a6 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -462,57 +462,6 @@ pub async fn init_resources() -> Result<()> { Ok(()) } -/// initialize url scheme -#[cfg(target_os = "windows")] -pub fn init_scheme() -> Result<()> { - use tauri::utils::platform::current_exe; - use winreg::{RegKey, enums::*}; - - let app_exe = current_exe()?; - let app_exe = dunce::canonicalize(app_exe)?; - let app_exe = app_exe.to_string_lossy().into_owned(); - - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - let (clash, _) = hkcu.create_subkey("Software\\Classes\\Clash")?; - clash.set_value("", &"Clash Verge")?; - clash.set_value("URL Protocol", &"Clash Verge URL Scheme Protocol")?; - let (default_icon, _) = hkcu.create_subkey("Software\\Classes\\Clash\\DefaultIcon")?; - default_icon.set_value("", &app_exe)?; - let (command, _) = hkcu.create_subkey("Software\\Classes\\Clash\\Shell\\Open\\Command")?; - command.set_value("", &format!("{app_exe} \"%1\""))?; - - Ok(()) -} -#[cfg(target_os = "linux")] -pub fn init_scheme() -> Result<()> { - const DESKTOP_FILE: &str = "clash-verge.desktop"; - - for scheme in DEEP_LINK_SCHEMES { - let handler = format!("x-scheme-handler/{scheme}"); - let output = std::process::Command::new("xdg-mime") - .arg("default") - .arg(DESKTOP_FILE) - .arg(&handler) - .output()?; - if !output.status.success() { - return Err(anyhow::anyhow!( - "failed to set {handler}, {}", - String::from_utf8_lossy(&output.stderr) - )); - } - } - - crate::utils::linux::ensure_mimeapps_entries(DESKTOP_FILE, DEEP_LINK_SCHEMES)?; - Ok(()) -} -#[cfg(target_os = "macos")] -pub const fn init_scheme() -> Result<()> { - Ok(()) -} - -#[cfg(target_os = "linux")] -const DEEP_LINK_SCHEMES: &[&str] = &["clash", "clash-verge"]; - pub async fn startup_script() -> Result<()> { let app_handle = handle::Handle::app_handle(); let script_path = { diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 5a3556161..83a1d7045 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -27,7 +27,6 @@ pub fn resolve_setup_handle() { pub fn resolve_setup_sync() { AsyncHandler::spawn(|| async { - AsyncHandler::spawn_blocking(init_scheme); AsyncHandler::spawn_blocking(init_embed_server); AsyncHandler::spawn_blocking(init_signal); }); @@ -89,10 +88,6 @@ pub fn init_handle() { handle::Handle::global().init(); } -pub(super) fn init_scheme() { - logging_error!(Type::Setup, init::init_scheme()); -} - #[cfg(not(feature = "tauri-dev"))] pub(super) async fn resolve_setup_logger() { logging_error!(Type::Setup, init::init_logger().await); diff --git a/src-tauri/src/utils/server.rs b/src-tauri/src/utils/server.rs index 9e8e12296..dda3c4874 100644 --- a/src-tauri/src/utils/server.rs +++ b/src-tauri/src/utils/server.rs @@ -1,70 +1,18 @@ -use super::resolve; use crate::{ config::{Config, DEFAULT_PAC, IVerge}, - logging, logging_error, - module::lightweight, + logging, process::AsyncHandler, - utils::{logging::Type, window_manager::WindowManager}, + utils::logging::Type, }; -use anyhow::{Result, bail}; use once_cell::sync::OnceCell; use parking_lot::Mutex; -use port_scanner::local_port_available; -use reqwest::ClientBuilder; -use smartstring::alias::String; -use std::time::Duration; use tokio::sync::oneshot; use warp::Filter; -#[derive(serde::Deserialize, Debug)] -struct QueryParam { - param: String, -} - // 关闭 embedded server 的信号发送端 static SHUTDOWN_SENDER: OnceCell>>> = OnceCell::new(); -/// check whether there is already exists -pub async fn check_singleton() -> Result<()> { - let port = IVerge::get_singleton_port(); - if !local_port_available(port) { - let client = ClientBuilder::new() - .timeout(Duration::from_millis(500)) - .build()?; - // 需要确保 Send - #[allow(clippy::needless_collect)] - let argvs: Vec = std::env::args().collect(); - if argvs.len() > 1 { - #[cfg(not(target_os = "macos"))] - { - let param = argvs[1].as_str(); - if param.starts_with("clash:") { - client - .get(format!( - "http://127.0.0.1:{port}/commands/scheme?param={param}" - )) - .send() - .await?; - } - } - } else { - client - .get(format!("http://127.0.0.1:{port}/commands/visible")) - .send() - .await?; - } - logging!( - error, - Type::Window, - "failed to setup singleton listen server" - ); - bail!("app exists"); - } - Ok(()) -} - -/// The embed server only be used to implement singleton process -/// maybe it can be used as pac server later +/// The embed server only be used as pac server pub fn embed_server() { let (shutdown_tx, shutdown_rx) = oneshot::channel(); #[allow(clippy::expect_used)] @@ -74,19 +22,6 @@ pub fn embed_server() { let port = IVerge::get_singleton_port(); AsyncHandler::spawn(move || async move { - let visible = warp::path!("commands" / "visible").and_then(|| async { - logging!(info, Type::Window, "检测到从单例模式恢复应用窗口"); - if !lightweight::exit_lightweight_mode().await { - WindowManager::show_main_window().await; - } else { - logging!(error, Type::Window, "轻量模式退出失败,无法恢复应用窗口"); - }; - Ok::<_, warp::Rejection>(warp::reply::with_status::( - "ok".to_string(), - warp::http::StatusCode::OK, - )) - }); - let verge_config = Config::verge().await; let clash_config = Config::clash().await; @@ -109,21 +44,7 @@ pub fn embed_server() { .unwrap_or_default() }); - // Use map instead of and_then to avoid Send issues - let scheme = warp::path!("commands" / "scheme") - .and(warp::query::()) - .and_then(|query: QueryParam| async move { - AsyncHandler::spawn(|| async move { - logging_error!(Type::Setup, resolve::resolve_scheme(&query.param).await); - }); - Ok::<_, warp::Rejection>(warp::reply::with_status::( - "ok".to_string(), - warp::http::StatusCode::OK, - )) - }); - - let commands = visible.or(scheme).or(pac); - warp::serve(commands) + warp::serve(pac) .bind(([127, 0, 0, 1], port)) .await .graceful(async {