refactor: single instance and handle deep links

This commit is contained in:
oomeow 2025-11-08 20:24:18 +08:00
parent c8aa72186e
commit d6f9bb4256
No known key found for this signature in database
GPG Key ID: 1E1E69B3EC8F6EA7
6 changed files with 39 additions and 158 deletions

17
src-tauri/Cargo.lock generated
View File

@ -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"

View File

@ -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]

View File

@ -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<AppHandle> = 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::Wry>) -> tauri::Builder<tauri::Wry> {
#[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<dyn std::error::Error>> {
#[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();

View File

@ -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 = {

View File

@ -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);

View File

@ -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<Mutex<Option<oneshot::Sender<()>>>> = 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::string::String> = 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::<std::string::String>(
"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::<QueryParam>())
.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::<std::string::String>(
"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 {