From 0f107abde6feb67b4253a121ca92991bb21a5de3 Mon Sep 17 00:00:00 2001 From: BTMuli Date: Wed, 3 Dec 2025 18:54:49 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20=E9=87=8D=E6=9E=84=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E6=9D=83=E9=99=90=E9=87=8D=E5=90=AF=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/Cargo.lock | 26 ++++----- src-tauri/Cargo.toml | 2 +- src-tauri/src/commands.rs | 120 ++++++++++++++++++++++++-------------- src-tauri/src/lib.rs | 36 ++++++++++-- src-tauri/src/plugins.rs | 10 ++-- 5 files changed, 126 insertions(+), 68 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c0609c22..80464c1f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2398,9 +2398,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ "value-bag", ] @@ -5011,7 +5011,7 @@ dependencies = [ [[package]] name = "tauri-plugin-deep-link" version = "2.4.5" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "dunce", "plist", @@ -5031,7 +5031,7 @@ dependencies = [ [[package]] name = "tauri-plugin-dialog" version = "2.4.2" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "log", "raw-window-handle", @@ -5048,7 +5048,7 @@ dependencies = [ [[package]] name = "tauri-plugin-fs" version = "2.4.4" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "anyhow", "dunce", @@ -5069,7 +5069,7 @@ dependencies = [ [[package]] name = "tauri-plugin-http" version = "2.5.4" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "bytes", "cookie_store", @@ -5092,7 +5092,7 @@ dependencies = [ [[package]] name = "tauri-plugin-log" version = "2.7.1" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "android_logger", "byte-unit", @@ -5113,7 +5113,7 @@ dependencies = [ [[package]] name = "tauri-plugin-opener" version = "2.5.2" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "dunce", "glob", @@ -5134,7 +5134,7 @@ dependencies = [ [[package]] name = "tauri-plugin-os" version = "2.3.2" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "gethostname", "log", @@ -5151,7 +5151,7 @@ dependencies = [ [[package]] name = "tauri-plugin-process" version = "2.3.1" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "tauri", "tauri-plugin", @@ -5160,7 +5160,7 @@ dependencies = [ [[package]] name = "tauri-plugin-shell" version = "2.3.3" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "encoding_rs", "log", @@ -5180,7 +5180,7 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" version = "2.3.6" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "serde", "serde_json", @@ -5194,7 +5194,7 @@ dependencies = [ [[package]] name = "tauri-plugin-sql" version = "2.3.1" -source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#1483c63101aef6ee7ac4701ec91f0a2e5c912ad4" +source = "git+ssh://git@github.com/tauri-apps/plugins-workspace.git?branch=v2#d9d51eb8ea2fd6ce3298ec0dc370cc04351a9b98" dependencies = [ "futures-core", "indexmap 2.12.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7f5c9060..54c7ebdd 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -21,7 +21,7 @@ tauri-build = { version = "2.5.3", features = [] } [dependencies] chrono = "0.4.42" -log = "0.4.28" +log = "0.4.29" prost = "0.14.1" prost-types = "0.14.1" serde = { version = "1.0.228", features = ["derive"] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2c1fbcaf..1ba6a799 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,5 +1,5 @@ //! 命令模块,负责处理命令 -//! @since Beta v0.7.8 +//! @since Beta v0.8.8 use tauri::{AppHandle, Emitter, Manager, WebviewWindowBuilder}; use tauri_utils::config::{WebviewUrl, WindowConfig}; @@ -128,57 +128,89 @@ pub fn is_in_admin() -> bool { } } +#[cfg(target_os = "windows")] +pub fn shell_runas_with_args(args: &str) -> Result<(), String> { + use std::ffi::OsStr; + use std::iter::once; + use std::os::windows::ffi::OsStrExt; + use std::ptr::null_mut; + + use windows_sys::Win32::Foundation::HWND; + use windows_sys::Win32::UI::Shell::ShellExecuteW; + use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL; + + fn to_wide(s: &OsStr) -> Vec { + s.encode_wide().chain(once(0)).collect() + } + + let exe_path = std::env::current_exe().map_err(|e| e.to_string())?; + let exe_wide = to_wide(exe_path.as_os_str()); + let args_wide = to_wide(OsStr::new(args)); + let cwd_wide = + exe_path.parent().map(|p| to_wide(p.as_os_str())).unwrap_or_else(|| to_wide(OsStr::new(""))); + + unsafe { + let result = ShellExecuteW( + 0 as HWND, + to_wide(OsStr::new("runas")).as_ptr(), + exe_wide.as_ptr(), + args_wide.as_ptr(), + if cwd_wide.len() > 1 { cwd_wide.as_ptr() } else { null_mut() }, + SW_SHOWNORMAL, + ); + if (result as usize) > 32 { + Ok(()) + } else { + Err("Failed to ShellExecuteW runas".into()) + } + } +} + +// 等待父进程退出(释放单例锁)后,再以管理员身份启动新实例 +#[cfg(target_os = "windows")] +pub fn run_watchdog(parent_pid: u32, args_to_pass: &str) -> Result<(), String> { + use std::time::Duration; + use windows_sys::Win32::Foundation::HANDLE; + use windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE; + use windows_sys::Win32::System::Threading::{OpenProcess, WaitForSingleObject, INFINITE}; + + // 打开父进程句柄用于等待 + let handle: HANDLE = unsafe { OpenProcess(SYNCHRONIZE, 0, parent_pid) }; + if handle == std::ptr::null_mut() { + // 如果拿不到句柄,可能父进程已退出,稍作等待后继续 + std::thread::sleep(Duration::from_millis(300)); + } else { + unsafe { + WaitForSingleObject(handle, INFINITE); + } + } + + // 父进程已退出 → 触发 UAC 提权启动新实例 + shell_runas_with_args(args_to_pass) +} + // 以管理员权限重启应用 #[tauri::command] -pub fn run_with_admin() -> Result<(), String> { +pub fn run_with_admin(app_handle: AppHandle) -> Result<(), String> { #[cfg(not(target_os = "windows"))] { return Err("This function is only supported on Windows.".into()); } + #[cfg(target_os = "windows")] { - use std::ffi::OsStr; - use std::iter::once; - use std::os::windows::ffi::OsStrExt; - use std::ptr::null_mut; - use windows_sys::Win32::Foundation::HWND; - use windows_sys::Win32::UI::Shell::ShellExecuteW; - use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOWNORMAL; + let parent_pid = std::process::id(); + let exe = std::env::current_exe().map_err(|e| e.to_string())?; + let mut cmd = std::process::Command::new(exe); + cmd + .arg("--watchdog") + .arg(format!("--ppid={}", parent_pid)) + // 看门狗不加载单例插件(通过参数决定 main 的初始化) + .spawn() + .map_err(|e| format!("spawn watchdog failed: {e}"))?; - fn to_wide(s: &OsStr) -> Vec { - s.encode_wide().chain(once(0)).collect() - } - - let exe_path = std::env::current_exe().map_err(|e| e.to_string())?; - if !exe_path.exists() { - return Err(format!("executable not found: {}", exe_path.display())); - } - - let elevated_arg = "--elevated"; - // /C start "" "" --elevated-action=post_install - let params = format!("/C start \"\" \"{}\" {}", exe_path.display(), elevated_arg); - - let cmd_w = to_wide(OsStr::new("cmd.exe")); - let verb_w = to_wide(OsStr::new("runas")); - let params_w = to_wide(OsStr::new(¶ms)); - let workdir_w = - exe_path.parent().map(|p| to_wide(p.as_os_str())).unwrap_or_else(|| to_wide(OsStr::new(""))); - - unsafe { - let result = ShellExecuteW( - 0 as HWND, - verb_w.as_ptr(), - cmd_w.as_ptr(), - params_w.as_ptr(), - if workdir_w.len() > 1 { workdir_w.as_ptr() } else { null_mut() }, - SW_SHOWNORMAL, - ); - - if (result as usize) > 32 { - Ok(()) - } else { - Err("Failed to restart as administrator via cmd.".into()) - } - } + // 立即退出:单例锁释放 + app_handle.exit(0); + Ok(()) } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e2038f9b..7a09c08c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,5 @@ //! 主模块,用于启动应用 -//! @since Beta v0.7.8 +//! @since Beta v0.8.8 mod client; mod commands; @@ -10,12 +10,12 @@ mod yae; use crate::client::create_mhy_client; use crate::commands::{ - create_window, execute_js, get_dir_size, init_app, is_in_admin, run_with_admin, + create_window, execute_js, get_dir_size, init_app, is_in_admin, run_watchdog, run_with_admin, }; use crate::plugins::{build_log_plugin, build_si_plugin}; #[cfg(target_os = "windows")] use crate::yae::call_yae_dll; -use tauri::{generate_context, generate_handler, Builder, Manager, Window, WindowEvent}; +use tauri::{generate_context, generate_handler, Manager, Window, WindowEvent}; // 窗口事件处理 fn window_event_handler(app: &Window, event: &WindowEvent) { @@ -40,9 +40,35 @@ fn window_event_handler(app: &Window, event: &WindowEvent) { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - Builder::default() + #[cfg(target_os = "windows")] + { + let args: Vec = std::env::args().collect(); + let is_watchdog = args.iter().any(|a| a == "--watchdog"); + // 看门狗模式:不初始化 Tauri,不加载单例,纯等待 + 提权启动 + if is_watchdog { + // 解析父进程 PID + let mut ppid: u32 = 0; + for a in &args { + if let Some(rest) = a.strip_prefix("--ppid=") { + if let Ok(v) = rest.parse::() { + ppid = v; + } + } + } + // 等父进程退出后再 runas 启动管理员实例,传入 --elevated 标志 + let _ = run_watchdog(ppid, "--elevated"); + // 看门狗退出 + return; + } + } + + // 正常应用实例:加载单例插件,防止多实例 + let mut builder = tauri::Builder::default(); + + // 只有在正常/管理员实例下才加载单例插件;看门狗不加载 + builder = builder.plugin(build_si_plugin()); + builder .on_window_event(move |app, event| window_event_handler(app, event)) - .plugin(build_si_plugin()) .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) diff --git a/src-tauri/src/plugins.rs b/src-tauri/src/plugins.rs index 9896a3ed..e1045050 100644 --- a/src-tauri/src/plugins.rs +++ b/src-tauri/src/plugins.rs @@ -12,13 +12,13 @@ use tauri_plugin_single_instance::init; pub fn build_si_plugin() -> TauriPlugin { init(move |app, argv, _cwd| { // 把 argv 转成 Vec - let args: Vec = argv.iter().map(|s| s.to_string()).collect(); + // let args: Vec = argv.iter().map(|s| s.to_string()).collect(); // 如果包含提升约定参数,发出专门事件并短路退出 - if args.iter().any(|a| a == "--elevated") { - // 提升实例通常只负责传参或执行一次性任务,退出避免与主实例冲突 - std::process::exit(0); - } + // if args.iter().any(|a| a == "--elevated") { + // 提升实例通常只负责传参或执行一次性任务,退出避免与主实例冲突 + // std::process::exit(0); + // } // 非提升启动:按原逻辑广播 deep link if let Err(e) = app.emit("active_deep_link", argv) {