mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-15 13:50:47 +08:00
* feat: add Linux theme change notifications and update dark-light dependency * fix: remove unused tauri Emitter import * update changelog * fix: clippy * fix: pnpm format
188 lines
5.4 KiB
JavaScript
188 lines
5.4 KiB
JavaScript
import { spawn } from 'node:child_process'
|
|
import { createHash } from 'node:crypto'
|
|
import { access, mkdir, readFile, writeFile } 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 serviceRepository = resolve(
|
|
repositoryRoot,
|
|
'..',
|
|
'clash-verge-service-ipc',
|
|
)
|
|
const serviceManifest = join(serviceRepository, 'Cargo.toml')
|
|
const developmentReceipt = join(
|
|
repositoryRoot,
|
|
'target',
|
|
'development-service.json',
|
|
)
|
|
|
|
function executable(name, platform) {
|
|
return join(
|
|
serviceRepository,
|
|
'target',
|
|
'debug',
|
|
platform === 'win32' ? `${name}.exe` : name,
|
|
)
|
|
}
|
|
|
|
function run(command, args, options = {}) {
|
|
return new Promise((resolvePromise, reject) => {
|
|
const child = spawn(command, args, {
|
|
env: options.env ?? process.env,
|
|
stdio: options.stdio ?? 'inherit',
|
|
windowsHide: true,
|
|
})
|
|
child.once('error', reject)
|
|
child.once('exit', (code, signal) => {
|
|
if (code === 0) {
|
|
resolvePromise()
|
|
return
|
|
}
|
|
const error = new Error(
|
|
`${command} failed with ${signal ? `signal ${signal}` : `exit code ${code}`}`,
|
|
)
|
|
error.exitCode = code
|
|
reject(error)
|
|
})
|
|
})
|
|
}
|
|
|
|
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,
|
|
} = {}) {
|
|
await access(serviceManifest)
|
|
await run('cargo', [
|
|
'build',
|
|
'--manifest-path',
|
|
serviceManifest,
|
|
'--features',
|
|
'standalone,client,development-channel',
|
|
'--bins',
|
|
])
|
|
|
|
const service = executable('clash-verge-service', platform)
|
|
await access(service)
|
|
return dirname(service)
|
|
}
|
|
|
|
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 installer = join(
|
|
serviceDirectory,
|
|
platform === 'win32'
|
|
? 'clash-verge-service-install.exe'
|
|
: 'clash-verge-service-install',
|
|
)
|
|
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.
|
|
}
|
|
}
|
|
|
|
try {
|
|
await elevateInstaller(installer, platform)
|
|
} catch (error) {
|
|
if (error.exitCode !== 75) throw error
|
|
}
|
|
await run(driver, ['ready'])
|
|
await writeInstalledFingerprint(fingerprint)
|
|
return serviceDirectory
|
|
}
|