fix(service): stage approved cores during Windows service installation

This commit is contained in:
wonfen
2026-09-15 16:09:54 +08:00
parent a8e8fe66dc
commit f58e0a0af5
4 changed files with 117 additions and 6 deletions
Generated
+2 -2
View File
@@ -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",
+1
View File
@@ -27,6 +27,7 @@
**🖥️ Windows**
- 修复 Windows 便携版或 Scoop 安装后服务内核无法启动的问题
- 修复 Windows 系统代理设置偶发失败的问题
- 修复 Windows 关机被应用阻止的问题
- 修复 Windows 服务模式下 TUN 使用 system/mixed 堆栈时无法接管流量的问题,防火墙放行规则改为自动添加
+1 -1
View File
@@ -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"
+113 -3
View File
@@ -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::<Result<Vec<_>>>()?;
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<OsString> = 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")
}