diff --git a/Changelog.md b/Changelog.md
index 87fca5b5f..7079a8bff 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -8,10 +8,14 @@
- 修复 DNS 覆写:空内容报错、初始值覆盖已有设置、已填写字段残留原配置、空字段在高级编辑器中缺失
- 修复 Merge 覆写 DNS 策略、过滤条件和 hosts 时,旧条目仍被保留的问题
- 修复首次以管理员身份启动后,系统服务因数据目录所有权不符而拒绝启动内核的问题
+- 修复重新安装服务后内核缺失、无法继续使用 Sidecar 的问题
+- 修复内核未运行时,首页仍显示服务模式的问题
+- 修复服务重装重复请求管理员授权、卸载后误报失败的问题
**🖥️ Windows**
- 修复 Windows 服务因路径或目录权限问题无法启动时,缺少重新安装服务的提示
+- 修复 Windows 服务回退检查失败后状态未更新、无法重新尝试 Sidecar 的问题
**🐧 Linux**
@@ -23,7 +27,7 @@
🚀 优化改进
- 优化 DNS 覆写开关:按订阅独立记忆,自动关闭仅影响当前订阅,并提示作用范围
-- 优化系统服务版本检查:版本与当前应用要求不一致时,提示卸载后重新安装
+- 优化系统服务兼容性检查:升级后服务版本或协议不兼容时,提示重新安装
- 优化预构建资源,补充 ASN 数据库文件
- 扩展配置或脚本写入由应用设置接管的字段时,弹出提示并在扩展日志中记录该值已被丢弃
diff --git a/scripts/dev-service.mjs b/scripts/dev-service.mjs
index b5c093509..4f91840a8 100644
--- a/scripts/dev-service.mjs
+++ b/scripts/dev-service.mjs
@@ -1,27 +1,23 @@
-import { spawn } from 'node:child_process'
-import { createHash } from 'node:crypto'
-import { access, mkdir, readFile, writeFile } from 'node:fs/promises'
+import { execFileSync, spawn } from 'node:child_process'
+import { access } from 'node:fs/promises'
import { dirname, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
export const developmentServiceDirectoryEnvironment =
'CLASH_VERGE_DEV_SERVICE_DIR'
-const developmentServiceInstallerEnvironment =
- 'CLASH_VERGE_DEV_SERVICE_INSTALLER'
-
-const scriptDirectory = dirname(fileURLToPath(import.meta.url))
-const repositoryRoot = resolve(scriptDirectory, '..')
+const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const serviceRepository = resolve(
repositoryRoot,
'..',
'clash-verge-service-ipc',
)
const serviceManifest = join(serviceRepository, 'Cargo.toml')
-const developmentReceipt = join(
- repositoryRoot,
- 'target',
- 'development-service.json',
-)
+export const developmentServiceWatchPaths = [
+ 'src',
+ 'resources',
+ 'Cargo.toml',
+ 'Cargo.lock',
+].map((name) => join(serviceRepository, name))
function executable(name, platform) {
return join(
@@ -54,78 +50,6 @@ function run(command, args, options = {}) {
})
}
-async function hashFiles(paths) {
- const hash = createHash('sha256')
- for (const path of paths) {
- hash.update(await readFile(path))
- }
- return hash.digest('hex')
-}
-
-async function readInstalledFingerprint() {
- try {
- const parsed = JSON.parse(await readFile(developmentReceipt, 'utf8'))
- return typeof parsed.fingerprint === 'string' ? parsed.fingerprint : null
- } catch {
- return null
- }
-}
-
-async function writeInstalledFingerprint(fingerprint) {
- await mkdir(dirname(developmentReceipt), { recursive: true })
- await writeFile(
- developmentReceipt,
- `${JSON.stringify({ fingerprint }, null, 2)}\n`,
- 'utf8',
- )
-}
-
-function windowsElevationInvocation(installer, environment = process.env) {
- const script = [
- "$ErrorActionPreference = 'Stop'",
- `$installer = [Environment]::GetEnvironmentVariable('${developmentServiceInstallerEnvironment}', 'Process')`,
- "if ([string]::IsNullOrWhiteSpace($installer)) { throw 'Development service installer path is missing' }",
- '$child = Start-Process -FilePath $installer -Verb RunAs -Wait -PassThru -WindowStyle Hidden',
- 'exit $child.ExitCode',
- ].join('; ')
- return {
- command: 'powershell.exe',
- args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script],
- env: {
- ...environment,
- [developmentServiceInstallerEnvironment]: installer,
- },
- }
-}
-
-async function elevateInstaller(installer, platform) {
- if (platform === 'win32') {
- const invocation = windowsElevationInvocation(installer)
- return run(invocation.command, invocation.args, { env: invocation.env })
- }
-
- if (platform === 'darwin') {
- const script =
- 'on run argv\nset toolPath to item 1 of argv\nset groupId to item 2 of argv\ndo shell script "CLASH_VERGE_SERVICE_GID=" & quoted form of groupId & " " & quoted form of toolPath with administrator privileges\nend run'
- return run('osascript', ['-e', script, installer, String(process.getgid())])
- }
-
- if (typeof process.getuid === 'function' && process.getuid() === 0) {
- return run(installer, [], {
- env: {
- ...process.env,
- CLASH_VERGE_SERVICE_GID: String(process.getgid()),
- },
- })
- }
- try {
- await run('pkexec', [installer])
- } catch (error) {
- if (error.code !== 'ENOENT' && error.exitCode !== 127) throw error
- await run('sudo', [installer])
- }
-}
-
export async function prepareDevelopmentService({
platform = process.platform,
} = {}) {
@@ -148,40 +72,28 @@ export async function ensureDevelopmentService({
platform = process.platform,
} = {}) {
const serviceDirectory = await prepareDevelopmentService({ platform })
- const service = join(
- serviceDirectory,
- platform === 'win32' ? 'clash-verge-service.exe' : 'clash-verge-service',
- )
+ const extension = platform === 'win32' ? '.exe' : ''
const installer = join(
serviceDirectory,
- platform === 'win32'
- ? 'clash-verge-service-install.exe'
- : 'clash-verge-service-install',
+ `clash-verge-service-install${extension}`,
)
- const driver = join(
- serviceDirectory,
- platform === 'win32'
- ? 'service-integration-driver.exe'
- : 'service-integration-driver',
- )
- const fingerprint = await hashFiles([service, installer])
- const installedFingerprint = await readInstalledFingerprint()
-
- if (installedFingerprint === fingerprint) {
- try {
- await run(driver, ['probe'], { stdio: 'ignore' })
- return serviceDirectory
- } catch {
- // The expected binary is installed but unavailable; the repair tool owns recovery.
- }
+ const host = execFileSync('rustc', ['-vV'], { encoding: 'utf8' }).match(
+ /^host: (.+)$/m,
+ )?.[1]
+ if (!host) throw new Error('rustc did not report its host target')
+ const args = ['--prepare-install', '--ensure']
+ for (const name of ['verge-mihomo', 'verge-mihomo-alpha']) {
+ args.push(
+ '--core',
+ `${name}${extension}`,
+ join(
+ repositoryRoot,
+ 'src-tauri',
+ 'sidecar',
+ `${name}-${host}${extension}`,
+ ),
+ )
}
-
- try {
- await elevateInstaller(installer, platform)
- } catch (error) {
- if (error.exitCode !== 75) throw error
- }
- await run(driver, ['ready'])
- await writeInstalledFingerprint(fingerprint)
+ await run(installer, args)
return serviceDirectory
}
diff --git a/scripts/dev.mjs b/scripts/dev.mjs
index 53350b109..7235a73d6 100644
--- a/scripts/dev.mjs
+++ b/scripts/dev.mjs
@@ -10,6 +10,7 @@ import {
} from './dev-control.mjs'
import {
developmentServiceDirectoryEnvironment,
+ developmentServiceWatchPaths,
ensureDevelopmentService,
prepareDevelopmentService,
} from './dev-service.mjs'
@@ -31,6 +32,9 @@ export function buildTauriInvocation(
env.RUSTFLAGS = '--cfg tokio_unstable'
args.push('tokio-trace')
}
+ for (const path of developmentServiceWatchPaths) {
+ args.push('--additional-watch-folders', path)
+ }
return {
command: 'pnpm',
args,
diff --git a/scripts/prebuild.mjs b/scripts/prebuild.mjs
index 264cb31af..22dd96032 100644
--- a/scripts/prebuild.mjs
+++ b/scripts/prebuild.mjs
@@ -586,6 +586,41 @@ async function resolveServiceBundle() {
path.join(cwd, 'src-tauri', 'Cargo.toml'),
'utf8',
)
+ const serviceDependency = cargoManifest
+ .split(/\r?\n/)
+ .find((line) => line.trimStart().startsWith('clash_verge_service_ipc ='))
+ const sourcePath = serviceDependency?.match(/\bpath\s*=\s*"([^"]+)"/)?.[1]
+ if (sourcePath) {
+ const manifest = path.resolve(cwd, 'src-tauri', sourcePath, 'Cargo.toml')
+ const targetDirectory = path.join(cwd, 'target', 'bundled-service')
+ execFileSync(
+ 'cargo',
+ [
+ 'build',
+ '--manifest-path',
+ manifest,
+ '--target-dir',
+ targetDirectory,
+ '--target',
+ SIDECAR_HOST,
+ '--release',
+ '--features',
+ 'standalone,client',
+ '--bins',
+ ],
+ { stdio: 'inherit' },
+ )
+ await fsp.mkdir(SERVICE_DIR, { recursive: true })
+ for (const { sourceFile, targetPath } of files) {
+ await fsp.copyFile(
+ path.join(targetDirectory, SIDECAR_HOST, 'release', sourceFile),
+ targetPath,
+ )
+ if (platform !== 'win32') await fsp.chmod(targetPath, 0o755)
+ await updateHashCache(targetPath)
+ }
+ return
+ }
const { archiveFile, downloadURL } = resolveServiceRelease(
cargoManifest,
SIDECAR_HOST,
diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs
index a9716f604..554f1be0b 100644
--- a/src-tauri/src/core/manager/lifecycle.rs
+++ b/src-tauri/src/core/manager/lifecycle.rs
@@ -185,16 +185,17 @@ where
apply_proxy().await
}
-#[cfg(target_os = "windows")]
+#[cfg(any(target_os = "windows", test))]
async fn run_service_start_with_sidecar_fallback(
start_service: Start,
confirm_idle: Check,
start_sidecar: Fallback,
+ record_unavailable: impl FnOnce(std::string::String),
) -> Result<()>
where
Start: FnOnce() -> StartFuture,
StartFuture: std::future::Future