Files
wonfen ed0b801573 fix(service): unify installation and safe sidecar handoff
Delegate service and core installation to Service IPC so replacement uses
one elevation. Share fallback occupancy checks and retain the execution
guard until the Sidecar exits.

Track missing approved cores and start refusals as unavailable, and show
when no core is running. Build matching service tools from the local IPC
dependency and pass both core executables through the Dev installer.
2026-09-22 14:55:24 +08:00

800 lines
24 KiB
JavaScript

import { execFileSync, execSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
import zlib from 'node:zlib'
import AdmZip from 'adm-zip'
import { glob } from 'glob'
import { HttpsProxyAgent } from 'https-proxy-agent'
import { extract } from 'tar'
import { resolveServiceRelease } from './service-release.mjs'
import { log_debug, log_error, log_info, log_success } from './utils.mjs'
/** Prepares platform resources, caching versions and unchanged files unless `--force` is used. */
const cwd = process.cwd()
const TEMP_DIR = path.join(cwd, 'node_modules/.verge')
const FORCE = process.argv.includes('--force') || process.argv.includes('-f')
const VERSION_CACHE_FILE = path.join(TEMP_DIR, '.version_cache.json')
const HASH_CACHE_FILE = path.join(TEMP_DIR, '.hash_cache.json')
const PLATFORM_MAP = {
'x86_64-pc-windows-msvc': 'win32',
'i686-pc-windows-msvc': 'win32',
'aarch64-pc-windows-msvc': 'win32',
'x86_64-apple-darwin': 'darwin',
'aarch64-apple-darwin': 'darwin',
'x86_64-unknown-linux-gnu': 'linux',
'i686-unknown-linux-gnu': 'linux',
'aarch64-unknown-linux-gnu': 'linux',
'armv7-unknown-linux-gnueabihf': 'linux',
'riscv64gc-unknown-linux-gnu': 'linux',
'loongarch64-unknown-linux-gnu': 'linux',
}
const ARCH_MAP = {
'x86_64-pc-windows-msvc': 'x64',
'i686-pc-windows-msvc': 'ia32',
'aarch64-pc-windows-msvc': 'arm64',
'x86_64-apple-darwin': 'x64',
'aarch64-apple-darwin': 'arm64',
'x86_64-unknown-linux-gnu': 'x64',
'i686-unknown-linux-gnu': 'ia32',
'aarch64-unknown-linux-gnu': 'arm64',
'armv7-unknown-linux-gnueabihf': 'arm',
'riscv64gc-unknown-linux-gnu': 'riscv64',
'loongarch64-unknown-linux-gnu': 'loong64',
}
const arg1 = process.argv.slice(2)[0]
const arg2 = process.argv.slice(2)[1]
const target = arg1 === '--force' || arg1 === '-f' ? arg2 : arg1
if (process.env.CI && !target) {
throw new Error('prebuild requires an explicit target triple in CI')
}
const { platform, arch } = target
? { platform: PLATFORM_MAP[target], arch: ARCH_MAP[target] }
: process
const SIDECAR_HOST = target
? target
: execSync('rustc -vV')
.toString()
.match(/(?<=host: ).+(?=\s*)/g)[0]
const RESOURCES_DIR = path.join(cwd, 'src-tauri', 'resources')
const SIDECAR_DIR = path.join(cwd, 'src-tauri', 'sidecar')
// Linux service binaries are bundled as externalBin sidecars (see tauri.linux.conf.json)
const SERVICE_DIR = platform === 'linux' ? SIDECAR_DIR : RESOURCES_DIR
// Version cache
async function loadVersionCache() {
try {
if (fs.existsSync(VERSION_CACHE_FILE)) {
const data = await fsp.readFile(VERSION_CACHE_FILE, 'utf-8')
return JSON.parse(data)
}
} catch (err) {
log_debug('Failed to load version cache:', err.message)
}
return {}
}
async function saveVersionCache(cache) {
try {
await fsp.mkdir(TEMP_DIR, { recursive: true })
await fsp.writeFile(VERSION_CACHE_FILE, JSON.stringify(cache, null, 2))
log_debug('Version cache saved')
} catch (err) {
log_debug('Failed to save version cache:', err.message)
}
}
async function getCachedVersion(key) {
const cache = await loadVersionCache()
const cached = cache[key]
if (cached && Date.now() - cached.timestamp < 3600000) {
log_info(`Using cached version for ${key}: ${cached.version}`)
return cached.version
}
return null
}
async function setCachedVersion(key, version) {
const cache = await loadVersionCache()
cache[key] = { version, timestamp: Date.now() }
await saveVersionCache(cache)
}
// File hash cache
async function calculateFileHash(filePath) {
try {
const fileBuffer = await fsp.readFile(filePath)
const hashSum = createHash('sha256')
hashSum.update(fileBuffer)
return hashSum.digest('hex')
} catch (ignoreErr) {
return null
}
}
async function loadHashCache() {
try {
if (fs.existsSync(HASH_CACHE_FILE)) {
const data = await fsp.readFile(HASH_CACHE_FILE, 'utf-8')
return JSON.parse(data)
}
} catch (err) {
log_debug('Failed to load hash cache:', err.message)
}
return {}
}
async function saveHashCache(cache) {
try {
await fsp.mkdir(TEMP_DIR, { recursive: true })
await fsp.writeFile(HASH_CACHE_FILE, JSON.stringify(cache, null, 2))
log_debug('Hash cache saved')
} catch (err) {
log_debug('Failed to save hash cache:', err.message)
}
}
async function hasFileChanged(filePath, targetPath) {
if (FORCE) return true
if (!fs.existsSync(targetPath)) return true
const hashCache = await loadHashCache()
const sourceHash = await calculateFileHash(filePath)
const targetHash = await calculateFileHash(targetPath)
if (!sourceHash || !targetHash) return true
const cacheKey = targetPath
const cachedHash = hashCache[cacheKey]
if (cachedHash === sourceHash && sourceHash === targetHash) {
return false
}
return true
}
async function updateHashCache(targetPath) {
const hashCache = await loadHashCache()
const hash = await calculateFileHash(targetPath)
if (hash) {
hashCache[targetPath] = hash
await saveHashCache(hashCache)
}
}
// Mihomo release maps
const META_ALPHA_VERSION_URL =
'https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha/version.txt'
const META_ALPHA_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download/Prerelease-Alpha`
let META_ALPHA_VERSION
const META_VERSION_URL =
'https://github.com/MetaCubeX/mihomo/releases/latest/download/version.txt'
const META_URL_PREFIX = `https://github.com/MetaCubeX/mihomo/releases/download`
let META_VERSION
const META_ALPHA_MAP = {
'win32-x64': 'mihomo-windows-amd64-v2',
'win32-ia32': 'mihomo-windows-386',
'win32-arm64': 'mihomo-windows-arm64',
'darwin-x64': 'mihomo-darwin-amd64-v1-go122',
'darwin-arm64': 'mihomo-darwin-arm64-go122',
'linux-x64': 'mihomo-linux-amd64-v2',
'linux-ia32': 'mihomo-linux-386',
'linux-arm64': 'mihomo-linux-arm64',
'linux-arm': 'mihomo-linux-armv7',
'linux-riscv64': 'mihomo-linux-riscv64',
'linux-loong64': 'mihomo-linux-loong64',
}
const META_MAP = {
'win32-x64': 'mihomo-windows-amd64-v2',
'win32-ia32': 'mihomo-windows-386',
'win32-arm64': 'mihomo-windows-arm64',
'darwin-x64': 'mihomo-darwin-amd64-v2-go122',
'darwin-arm64': 'mihomo-darwin-arm64-go122',
'linux-x64': 'mihomo-linux-amd64-v2',
'linux-ia32': 'mihomo-linux-386',
'linux-arm64': 'mihomo-linux-arm64',
'linux-arm': 'mihomo-linux-armv7',
'linux-riscv64': 'mihomo-linux-riscv64',
'linux-loong64': 'mihomo-linux-loong64',
}
// Release discovery
async function getLatestAlphaVersion() {
if (!FORCE) {
const cached = await getCachedVersion('META_ALPHA_VERSION')
if (cached) {
META_ALPHA_VERSION = cached
return
}
}
const options = {}
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy)
try {
const response = await fetch(META_ALPHA_VERSION_URL, {
...options,
method: 'GET',
})
if (!response.ok)
throw new Error(
`Failed to fetch ${META_ALPHA_VERSION_URL}: ${response.status}`,
)
META_ALPHA_VERSION = (await response.text()).trim()
log_info(`Latest alpha version: ${META_ALPHA_VERSION}`)
await setCachedVersion('META_ALPHA_VERSION', META_ALPHA_VERSION)
} catch (err) {
log_error('Error fetching latest alpha version:', err.message)
process.exit(1)
}
}
async function getLatestReleaseVersion() {
if (!FORCE) {
const cached = await getCachedVersion('META_VERSION')
if (cached) {
META_VERSION = cached
return
}
}
const options = {}
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy)
try {
const response = await fetch(META_VERSION_URL, {
...options,
method: 'GET',
})
if (!response.ok)
throw new Error(`Failed to fetch ${META_VERSION_URL}: ${response.status}`)
META_VERSION = (await response.text()).trim()
log_info(`Latest release version: ${META_VERSION}`)
await setCachedVersion('META_VERSION', META_VERSION)
} catch (err) {
log_error('Error fetching latest release version:', err.message)
process.exit(1)
}
}
if (!META_MAP[`${platform}-${arch}`]) {
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`)
}
if (!META_ALPHA_MAP[`${platform}-${arch}`]) {
throw new Error(`clash meta alpha unsupported platform "${platform}-${arch}"`)
}
function clashMetaAlpha() {
const name = META_ALPHA_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz'
return {
name: 'verge-mihomo-alpha',
targetFile: `verge-mihomo-alpha-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,
exeFile: `${name}${isWin ? '.exe' : ''}`,
zipFile: `${name}-${META_ALPHA_VERSION}.${urlExt}`,
downloadURL: `${META_ALPHA_URL_PREFIX}/${name}-${META_ALPHA_VERSION}.${urlExt}`,
}
}
function clashMeta() {
const name = META_MAP[`${platform}-${arch}`]
const isWin = platform === 'win32'
const urlExt = isWin ? 'zip' : 'gz'
return {
name: 'verge-mihomo',
targetFile: `verge-mihomo-${SIDECAR_HOST}${isWin ? '.exe' : ''}`,
exeFile: `${name}${isWin ? '.exe' : ''}`,
zipFile: `${name}-${META_VERSION}.${urlExt}`,
downloadURL: `${META_URL_PREFIX}/${META_VERSION}/${name}-${META_VERSION}.${urlExt}`,
}
}
async function downloadFile(url, outPath) {
const options = {}
const httpProxy =
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.HTTPS_PROXY ||
process.env.https_proxy
if (httpProxy) options.agent = new HttpsProxyAgent(httpProxy)
const response = await fetch(url, {
...options,
method: 'GET',
headers: { 'Content-Type': 'application/octet-stream' },
})
if (!response.ok) {
const body = await response.text().catch(() => '')
await fsp.mkdir(path.dirname(outPath), { recursive: true })
await fsp.writeFile(outPath, body)
throw new Error(`Failed to download ${url}: status ${response.status}`)
}
const buf = Buffer.from(await response.arrayBuffer())
await fsp.mkdir(path.dirname(outPath), { recursive: true })
if (url.endsWith('.gz') || url.endsWith('.tgz')) {
if (!(buf[0] === 0x1f && buf[1] === 0x8b)) {
await fsp.writeFile(outPath, buf)
throw new Error(
`Downloaded file for ${url} is not a valid gzip (magic mismatch).`,
)
}
} else if (url.endsWith('.zip')) {
if (!(buf[0] === 0x50 && buf[1] === 0x4b)) {
await fsp.writeFile(outPath, buf)
throw new Error(
`Downloaded file for ${url} is not a valid zip (magic mismatch).`,
)
}
}
await fsp.writeFile(outPath, buf)
log_success(`download finished: ${url}`)
}
async function resolveSidecar(binInfo) {
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo
const sidecarPath = path.join(SIDECAR_DIR, targetFile)
await fsp.mkdir(SIDECAR_DIR, { recursive: true })
if (!FORCE && fs.existsSync(sidecarPath)) {
log_success(`"${name}" already exists, skipping download`)
return
}
const tempDir = path.join(TEMP_DIR, name)
const tempZip = path.join(tempDir, zipFile)
const tempExe = path.join(tempDir, exeFile)
await fsp.mkdir(tempDir, { recursive: true })
try {
if (!fs.existsSync(tempZip)) {
await downloadFile(downloadURL, tempZip)
}
if (zipFile.endsWith('.zip')) {
const zip = new AdmZip(tempZip)
zip.getEntries().forEach((entry) => {
log_debug(`"${name}" entry: ${entry.entryName}`)
})
zip.extractAllTo(tempDir, true)
if (fs.existsSync(tempExe)) {
await fsp.rename(tempExe, sidecarPath)
} else {
const files = await fsp.readdir(tempDir)
const candidate = files.find(
(f) =>
f === path.basename(exeFile) ||
f.endsWith('.exe') ||
!f.includes('.'),
)
if (!candidate)
throw new Error(`Expected binary not found in ${tempDir}`)
await fsp.rename(path.join(tempDir, candidate), sidecarPath)
}
if (platform !== 'win32') await fsp.chmod(sidecarPath, 0o755)
log_success(`unzip finished: "${name}"`)
} else if (zipFile.endsWith('.tgz')) {
await extract({ cwd: tempDir, file: tempZip })
const files = await fsp.readdir(tempDir)
log_debug(`"${name}" extracted files:`, files)
let extracted = files.find(
(f) =>
f === path.basename(exeFile) ||
f.startsWith('虚空终端-') ||
!f.includes('.'),
)
if (!extracted) extracted = files[0]
if (!extracted) throw new Error(`Expected file not found in ${tempDir}`)
await fsp.rename(path.join(tempDir, extracted), sidecarPath)
await fsp.chmod(sidecarPath, 0o755)
log_success(`tgz processed: "${name}"`)
} else {
const readStream = fs.createReadStream(tempZip)
const writeStream = fs.createWriteStream(sidecarPath)
await new Promise((resolve, reject) => {
readStream
.pipe(zlib.createGunzip())
.on('error', (e) => {
log_error(`gunzip error for ${name}:`, e.message)
reject(e)
})
.pipe(writeStream)
.on('finish', () => {
if (platform !== 'win32')
execFileSync('chmod', ['755', sidecarPath])
resolve()
})
.on('error', (e) => {
log_error(`write stream error for ${name}:`, e.message)
reject(e)
})
})
log_success(`gz binary processed: "${name}"`)
}
} catch (err) {
await fsp.rm(sidecarPath, { recursive: true, force: true })
throw err
} finally {
await fsp.rm(tempDir, { recursive: true, force: true })
}
}
async function resolveResource(binInfo) {
const { file, downloadURL, localPath, dir } = binInfo
const baseDir = dir ?? RESOURCES_DIR
const targetPath = path.join(baseDir, file)
if (!FORCE && fs.existsSync(targetPath) && !downloadURL && !localPath) {
log_success(`"${file}" already exists, skipping`)
return
}
if (downloadURL) {
if (!FORCE && fs.existsSync(targetPath)) {
log_success(`"${file}" already exists, skipping download`)
return
}
await fsp.mkdir(baseDir, { recursive: true })
await downloadFile(downloadURL, targetPath)
await updateHashCache(targetPath)
}
if (localPath) {
if (!(await hasFileChanged(localPath, targetPath))) {
return
}
await fsp.mkdir(baseDir, { recursive: true })
await fsp.copyFile(localPath, targetPath)
await updateHashCache(targetPath)
log_success(`Copied file: ${file}`)
}
log_success(`${file} finished`)
}
// Windows NSIS plugin
const resolvePlugin = async () => {
const url =
'https://nsis.sourceforge.io/mediawiki/images/e/ef/NSIS_Simple_Service_Plugin_Unicode_1.30.zip'
const tempDir = path.join(TEMP_DIR, 'SimpleSC')
const tempZip = path.join(
tempDir,
'NSIS_Simple_Service_Plugin_Unicode_1.30.zip',
)
const tempDll = path.join(tempDir, 'SimpleSC.dll')
const pluginDir = path.join(process.env.APPDATA || '', 'Local/NSIS')
const pluginPath = path.join(pluginDir, 'SimpleSC.dll')
await fsp.mkdir(pluginDir, { recursive: true })
await fsp.mkdir(tempDir, { recursive: true })
if (!FORCE && fs.existsSync(pluginPath)) return
try {
if (!fs.existsSync(tempZip)) {
await downloadFile(url, tempZip)
}
const zip = new AdmZip(tempZip)
zip
.getEntries()
.forEach((entry) => void log_debug(`"SimpleSC" entry`, entry.entryName))
zip.extractAllTo(tempDir, true)
if (fs.existsSync(tempDll)) {
await fsp.cp(tempDll, pluginPath, { recursive: true, force: true })
log_success(`unzip finished: "SimpleSC"`)
} else {
const files = await fsp.readdir(tempDir)
const dll = files.find((f) => f.toLowerCase().endsWith('.dll'))
if (dll) {
await fsp.cp(path.join(tempDir, dll), pluginPath, {
recursive: true,
force: true,
})
log_success(`unzip finished: "SimpleSC" (found ${dll})`)
} else {
throw new Error('SimpleSC.dll not found in zip')
}
}
} finally {
await fsp.rm(tempDir, { recursive: true, force: true })
}
}
// Service executable permissions
const resolveServicePermission = async () => {
const serviceExecutables = [
'clash-verge-service*',
'clash-verge-service-install*',
'clash-verge-service-uninstall*',
]
const hashCache = await loadHashCache()
let hasChanges = false
for (const f of serviceExecutables) {
const files = glob.sync(path.join(SERVICE_DIR, f))
for (const filePath of files) {
if (fs.existsSync(filePath)) {
const currentHash = await calculateFileHash(filePath)
const cacheKey = `${filePath}_chmod`
if (!FORCE && hashCache[cacheKey] === currentHash) {
continue
}
try {
execSync(`chmod 755 ${filePath}`)
log_success(`chmod finished: "${filePath}"`)
} catch (e) {
log_error(`chmod failed for ${filePath}:`, e.message)
}
hashCache[cacheKey] = currentHash
hasChanges = true
}
}
}
if (hasChanges) {
await saveHashCache(hashCache)
}
}
// Other resources
const SERVICE_BINARIES = [
'clash-verge-service',
'clash-verge-service-install',
'clash-verge-service-uninstall',
]
function serviceFileInfo(name) {
const ext = platform === 'win32' ? '.exe' : ''
const suffix = platform === 'linux' ? `-${SIDECAR_HOST}` : ''
return {
sourceFile: `${name}${ext}`,
targetFile: `${name}${suffix}${ext}`,
}
}
async function findExtractedFile(dir, fileName) {
const entries = await fsp.readdir(dir, { withFileTypes: true })
for (const entry of entries) {
const entryPath = path.join(dir, entry.name)
if (entry.isFile() && entry.name === fileName) return entryPath
if (entry.isDirectory()) {
const found = await findExtractedFile(entryPath, fileName)
if (found) return found
}
}
return null
}
async function resolveServiceBundle() {
const files = SERVICE_BINARIES.map((name) => {
const info = serviceFileInfo(name)
return {
...info,
targetPath: path.join(SERVICE_DIR, info.targetFile),
}
})
const cargoManifest = await fsp.readFile(
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,
platform,
)
const tempDir = path.join(TEMP_DIR, 'clash-verge-service-ipc')
const tempArchive = path.join(tempDir, archiveFile)
await fsp.mkdir(tempDir, { recursive: true })
await fsp.mkdir(SERVICE_DIR, { recursive: true })
try {
await downloadFile(downloadURL, tempArchive)
if (platform === 'win32') {
const zip = new AdmZip(tempArchive)
zip
.getEntries()
.forEach(
(entry) =>
void log_debug('"clash-verge-service-ipc" entry:', entry.entryName),
)
zip.extractAllTo(tempDir, true)
} else {
await extract({ cwd: tempDir, file: tempArchive })
}
for (const { sourceFile, targetFile, targetPath } of files) {
const extractedFile = await findExtractedFile(tempDir, sourceFile)
if (!extractedFile) {
throw new Error(`Expected binary ${sourceFile} not found in archive`)
}
await fsp.copyFile(extractedFile, targetPath)
if (platform !== 'win32') await fsp.chmod(targetPath, 0o755)
await updateHashCache(targetPath)
log_success(`Extracted service file: ${targetFile}`)
}
log_success(`service bundle finished: ${archiveFile}`)
} finally {
await fsp.rm(tempDir, { recursive: true, force: true })
}
}
/// The NSIS installer publishes the bundled cores into the service's approved directory and must
/// attest exactly the bytes it unpacked. The digests are computed here, where the sidecars land,
/// and reach installer.nsi through the installerHooks include (see tauri.windows.conf.json).
const CORE_HASHES_NSH = path.join(
cwd,
'src-tauri',
'packages',
'windows',
'core-hashes.nsh',
)
async function resolveCoreHashes() {
const lines = []
for (const [define, name] of [
['MIHOMO_SHA256', 'verge-mihomo'],
['MIHOMO_ALPHA_SHA256', 'verge-mihomo-alpha'],
]) {
const sidecar = path.join(SIDECAR_DIR, `${name}-${SIDECAR_HOST}.exe`)
const digest = createHash('sha256')
.update(await fsp.readFile(sidecar))
.digest('hex')
lines.push(`!define ${define} "${digest}"`)
}
await fsp.writeFile(CORE_HASHES_NSH, `${lines.join('\n')}\n`)
log_success(`Generated ${CORE_HASHES_NSH}`)
}
const resolveMmdb = () =>
resolveResource({
file: 'Country.mmdb',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb`,
})
const resolveASNMmdb = () =>
resolveResource({
file: 'ASN.mmdb',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/GeoLite2-ASN.mmdb`,
})
const resolveGeosite = () =>
resolveResource({
file: 'geosite.dat',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`,
})
const resolveGeoIP = () =>
resolveResource({
file: 'geoip.dat',
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`,
})
const resolveEnableLoopback = () =>
resolveResource({
file: 'enableLoopback.exe',
downloadURL: `https://github.com/Kuingsmile/uwp-tool/releases/download/latest/enableLoopback.exe`,
})
const resolveSetDnsScript = () =>
resolveResource({
file: 'set_dns.sh',
localPath: path.join(cwd, 'scripts/set_dns.sh'),
})
const resolveUnSetDnsScript = () =>
resolveResource({
file: 'unset_dns.sh',
localPath: path.join(cwd, 'scripts/unset_dns.sh'),
})
const tasks = [
{
name: 'verge-mihomo-alpha',
func: () =>
getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())),
retry: 5,
},
{
name: 'verge-mihomo',
func: () =>
getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())),
retry: 5,
},
// After both sidecar tasks: it hashes what they downloaded.
{ name: 'core_hashes', func: resolveCoreHashes, retry: 1, winOnly: true },
{ name: 'plugin', func: resolvePlugin, retry: 5, winOnly: true },
{ name: 'service', func: resolveServiceBundle, retry: 5 },
{ name: 'mmdb', func: resolveMmdb, retry: 5 },
{ name: 'asn_mmdb', func: resolveASNMmdb, retry: 5 },
{ name: 'geosite', func: resolveGeosite, retry: 5 },
{ name: 'geoip', func: resolveGeoIP, retry: 5 },
{
name: 'enableLoopback',
func: resolveEnableLoopback,
retry: 5,
winOnly: true,
},
{
name: 'service_chmod',
func: resolveServicePermission,
retry: 5,
unixOnly: platform === 'linux' || platform === 'darwin',
},
{
name: 'set_dns_script',
func: resolveSetDnsScript,
retry: 5,
macosOnly: true,
},
{
name: 'unset_dns_script',
func: resolveUnSetDnsScript,
retry: 5,
macosOnly: true,
},
]
async function runTask() {
const task = tasks.shift()
if (!task) return
if (task.unixOnly && platform === 'win32') return runTask()
if (task.winOnly && platform !== 'win32') return runTask()
if (task.macosOnly && platform !== 'darwin') return runTask()
if (task.linuxOnly && platform !== 'linux') return runTask()
for (let i = 0; i < task.retry; i++) {
try {
await task.func()
break
} catch (err) {
log_error(`task::${task.name} try ${i} ==`, err.message)
if (i === task.retry - 1) throw err
}
}
return runTask()
}
runTask()