diff --git a/Cargo.lock b/Cargo.lock index a4981eb5c..27ffaed0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1265,8 +1265,8 @@ dependencies = [ [[package]] name = "clash_verge_service_ipc" -version = "2.7.0" -source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.0#19461f50181ce53bb9b6c4445b07b5eb4cded7ec" +version = "2.7.1" +source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.1#807480e9afc7b9deb8ff3040b54be34326986dfd" dependencies = [ "anyhow", "kode-bridge", diff --git a/Changelog.md b/Changelog.md index 2f1bfc188..695b0d1f3 100644 --- a/Changelog.md +++ b/Changelog.md @@ -27,6 +27,7 @@ **🖥️ Windows** +- 修复 Windows 便携版或 Scoop 安装后服务内核无法启动的问题 - 修复 Windows 系统代理设置偶发失败的问题 - 修复 Windows 关机被应用阻止的问题 - 修复 Windows 服务模式下 TUN 使用 system/mixed 堆栈时无法接管流量的问题,防火墙放行规则改为自动添加 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d11aac03d..fc3c9975a 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -99,7 +99,7 @@ tracing = { workspace = true } tauri-plugin-devtools = { version = "2.1.0", optional = true } tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" } async-trait = "0.1.92" -clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.0", version = "2.7.0", features = [ +clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.1", version = "2.7.1", features = [ "client", ] } arc-swap = "1.9.2" diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 9b3b79662..7529cc6e2 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -564,8 +564,39 @@ fn uninstall_service() -> Result<()> { #[cfg(target_os = "windows")] fn install_service() -> Result<()> { - use std::process::Output; logging!(info, Type::Service, "install service"); + let cores = crate::config::IVerge::VALID_CLASH_CORES + .iter() + .map(|core| service_core_path(core, ".exe")) + .collect::>>()?; + install_windows_service_with_cores(&cores, run_windows_service_installer) +} + +#[cfg(target_os = "windows")] +fn install_windows_service_with_cores( + cores: &[PathBuf], + mut run_installer: impl FnMut(&[std::ffi::OsString]) -> Result<()>, +) -> Result<()> { + let cores: Vec<_> = cores.iter().filter(|core| core.is_file()).collect(); + if cores.is_empty() { + bail!("no core executable is available; restore a bundled core before installing the service"); + } + let mut arguments = vec!["--install-service".into()]; + for core in cores { + let digest = sha256_hex(core)?; + arguments.extend([ + "--install-core".into(), + core.as_os_str().to_owned(), + "--sha256".into(), + digest.into(), + ]); + } + run_installer(&arguments).context("failed to install the service and its cores; choose Repair to retry") +} + +#[cfg(target_os = "windows")] +fn run_windows_service_installer(arguments: &[std::ffi::OsString]) -> Result<()> { + use std::process::Output; use deelevate::{PrivilegeLevel, Token}; use runas::Command as RunasCommand; @@ -583,7 +614,7 @@ fn install_service() -> Result<()> { let level = token.privilege_level()?; let output = match level { PrivilegeLevel::NotPrivileged => { - let status = RunasCommand::new(&install_path).show(false).status()?; + let status = RunasCommand::new(&install_path).args(arguments).show(false).status()?; Output { status, stdout: Vec::new(), @@ -592,7 +623,10 @@ fn install_service() -> Result<()> { } _ => { // StdCommand returns Output directly - StdCommand::new(&install_path).creation_flags(0x08000000).output()? + StdCommand::new(&install_path) + .args(arguments) + .creation_flags(0x08000000) + .output()? } }; @@ -2167,6 +2201,82 @@ mod tests { } } + #[cfg(target_os = "windows")] + #[test] + fn windows_service_install_combines_service_and_attested_cores() -> anyhow::Result<()> { + use std::ffi::OsString; + let root = TestDirectory::new("scoop install")?; + let cores: Vec<_> = crate::config::IVerge::VALID_CLASH_CORES + .iter() + .map(|name| root.path().join(format!("{name}.exe"))) + .collect(); + for core in &cores { + std::fs::write(core, b"abc")?; + } + let mut calls = Vec::new(); + super::install_windows_service_with_cores(&cores, |arguments| { + calls.push(arguments.to_vec()); + Ok(()) + })?; + assert_eq!(calls.len(), 1, "service and cores must share one elevation"); + let digest = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"; + let mut expected: Vec = vec!["--install-service".into()]; + expected.extend(cores.iter().flat_map(|core| { + [ + "--install-core".into(), + core.as_os_str().to_owned(), + "--sha256".into(), + digest.into(), + ] + })); + assert_eq!(calls[0], expected); + Ok(()) + } + + #[cfg(target_os = "windows")] + #[test] + fn windows_service_install_skips_unavailable_cores() -> anyhow::Result<()> { + let root = TestDirectory::new("missing-core")?; + let core = root.path().join("verge-mihomo.exe"); + let missing = root.path().join("verge-mihomo-alpha.exe"); + std::fs::write(&core, b"abc")?; + let cores = [core.clone(), missing.clone(), root.path().to_path_buf()]; + let mut calls = Vec::new(); + super::install_windows_service_with_cores(&cores, |arguments| { + calls.push(arguments.to_vec()); + Ok(()) + })?; + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].len(), 5); + assert_eq!(calls[0][0], "--install-service"); + assert_eq!(calls[0][2], core.as_os_str()); + + let error = super::install_windows_service_with_cores(&[missing, root.path().to_path_buf()], |_| { + panic!("no available core must fail before invoking the installer") + }) + .expect_err("installation requires at least one available core"); + assert!(error.to_string().contains("no core executable")); + Ok(()) + } + + #[cfg(target_os = "windows")] + #[test] + fn windows_service_install_propagates_installer_failure() -> anyhow::Result<()> { + let root = TestDirectory::new("install-failure")?; + let core = root.path().join("verge-mihomo.exe"); + std::fs::write(&core, b"abc")?; + let mut calls = 0; + let result = super::install_windows_service_with_cores(std::slice::from_ref(&core), |_| { + calls += 1; + bail!("installer failed with exit code 1") + }); + let error = result.expect_err("installation failure must not be reported as success"); + assert!(format!("{error:#}").contains("exit code 1")); + assert!(error.to_string().contains("Repair")); + assert_eq!(calls, 1); + Ok(()) + } + fn staging_directory(home: &Path) -> PathBuf { home.join("Applications/.clash-verge-rev-dev/service-core") }