mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/mihomo-party-org/clash-party.git
synced 2026-09-20 08:03:39 +08:00
perf: speed up app startup and shutdown
- Cache admin privilege check; bypass cmd.exe wrapper via execFile - Reuse the synchronous fltmc result from setupPlatformSpecifics to prime the admin privilege cache, avoiding a second elevated-state probe - Parallelize startup init: appConfig, i18n, admin check now run concurrently - Skip high-privilege core check when current process is admin - Coalesce mihomo process scan to a single tasklist + batched powershell - Parallelize the four mihomo websocket streams - Light-path cleanup of Windows named pipes; reserve PowerShell scan for retry - Defer Sub-Store frontend/backend startup until after the main window is created, freeing the critical startup path - Run post-core background tasks (profile updater, webdav, tun follow-up) in parallel - Tighten shutdown timeouts and skip recoverDNS on non-macOS
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
## 性能优化 (Performance)
|
||||
|
||||
- 重构简化连接页
|
||||
- 优化软件启动和退出速度
|
||||
|
||||
# 1.9.5
|
||||
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import { exec } from 'child_process'
|
||||
import { execFile } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { managerLogger } from '../utils/logger'
|
||||
|
||||
const execPromise = promisify(exec)
|
||||
const execFilePromise = promisify(execFile)
|
||||
|
||||
// admin 状态在 Node 进程生命周期内不会变化,永久缓存
|
||||
let adminPrivilegePromise: Promise<boolean> | null = null
|
||||
|
||||
// 允许 lifecycle.ts 同步检测的结果直接填入缓存,避免重复跑一次 fltmc
|
||||
export function primeAdminPrivilegesCache(value: boolean): void {
|
||||
if (adminPrivilegePromise) return
|
||||
adminPrivilegePromise = Promise.resolve(value)
|
||||
managerLogger.info(`Admin privileges primed from sync check: ${value}`)
|
||||
}
|
||||
|
||||
export async function checkAdminPrivileges(): Promise<boolean> {
|
||||
if (process.platform !== 'win32') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (adminPrivilegePromise) return adminPrivilegePromise
|
||||
|
||||
adminPrivilegePromise = (async () => {
|
||||
try {
|
||||
await execPromise('chcp 65001 >nul 2>&1 && fltmc', { encoding: 'utf8' })
|
||||
await execFilePromise('fltmc', [], { windowsHide: true, timeout: 1500 })
|
||||
managerLogger.info('Admin privileges confirmed via fltmc')
|
||||
return true
|
||||
} catch (fltmcError: unknown) {
|
||||
@@ -18,7 +31,7 @@ export async function checkAdminPrivileges(): Promise<boolean> {
|
||||
managerLogger.debug(`fltmc failed with code ${errorCode}, trying net session as fallback`)
|
||||
|
||||
try {
|
||||
await execPromise('chcp 65001 >nul 2>&1 && net session', { encoding: 'utf8' })
|
||||
await execFilePromise('net', ['session'], { windowsHide: true, timeout: 1500 })
|
||||
managerLogger.info('Admin privileges confirmed via net session')
|
||||
return true
|
||||
} catch (netSessionError: unknown) {
|
||||
@@ -29,4 +42,7 @@ export async function checkAdminPrivileges(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
return adminPrivilegePromise
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ function setupCoreListeners(
|
||||
if (process.platform === 'win32') {
|
||||
managerLogger.info('Attempting Windows pipe cleanup and retry...')
|
||||
try {
|
||||
await cleanupWindowsNamedPipes()
|
||||
await cleanupWindowsNamedPipes(true)
|
||||
await new Promise((r) => setTimeout(r, 2000))
|
||||
} catch (cleanupError) {
|
||||
managerLogger.error('Pipe cleanup failed:', cleanupError)
|
||||
@@ -350,10 +350,12 @@ function setupCoreListeners(
|
||||
|
||||
await waitForCoreReady()
|
||||
await getAxios(true)
|
||||
await startMihomoTraffic()
|
||||
await startMihomoConnections()
|
||||
await startMihomoLogs()
|
||||
await startMihomoMemory()
|
||||
await Promise.all([
|
||||
startMihomoTraffic(),
|
||||
startMihomoConnections(),
|
||||
startMihomoLogs(),
|
||||
startMihomoMemory()
|
||||
])
|
||||
retry = 10
|
||||
}
|
||||
})
|
||||
@@ -380,13 +382,13 @@ export async function startCore(detached = false, skipStop = false): Promise<Pro
|
||||
|
||||
// 停止核心
|
||||
export async function stopCore(force = false): Promise<void> {
|
||||
if (!force && process.platform === 'darwin') {
|
||||
try {
|
||||
if (!force) {
|
||||
await recoverDNS()
|
||||
}
|
||||
} catch (error) {
|
||||
managerLogger.error('recover dns failed', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (child) {
|
||||
child.removeAllListeners()
|
||||
|
||||
@@ -133,41 +133,64 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
for (const executable of mihomoExecutables) {
|
||||
let stdout = ''
|
||||
try {
|
||||
const { stdout } = await execPromise(
|
||||
`chcp 65001 >nul 2>&1 && tasklist /FI "IMAGENAME eq ${executable}" /FO CSV`,
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
const lines = stdout.split('\n').filter((line) => line.includes(executable))
|
||||
const result = await execFilePromise('tasklist', ['/FO', 'CSV', '/NH'], {
|
||||
windowsHide: true,
|
||||
timeout: 3000,
|
||||
maxBuffer: 4 * 1024 * 1024
|
||||
})
|
||||
stdout = result.stdout
|
||||
} catch (error) {
|
||||
managerLogger.error('Failed to list processes via tasklist', error)
|
||||
return false
|
||||
}
|
||||
|
||||
if (lines.length > 0) {
|
||||
managerLogger.info(`Found ${lines.length} ${executable} processes running`)
|
||||
const candidatePids: { pid: string; image: string }[] = []
|
||||
for (const line of stdout.split('\n')) {
|
||||
const match = line.match(/^"([^"]+)","(\d+)"/)
|
||||
if (!match) continue
|
||||
const image = match[1].toLowerCase()
|
||||
if (mihomoExecutables.includes(image)) {
|
||||
candidatePids.push({ pid: match[2], image })
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(',')
|
||||
if (parts.length >= 2) {
|
||||
const pid = parts[1].replace(/"/g, '').trim()
|
||||
if (candidatePids.length === 0) {
|
||||
managerLogger.info('No mihomo processes found running')
|
||||
return false
|
||||
}
|
||||
|
||||
managerLogger.info(`Found ${candidatePids.length} mihomo processes running`)
|
||||
|
||||
const pidArgs = candidatePids.map(({ pid }) => pid).join(',')
|
||||
try {
|
||||
const { stdout: processInfo } = await execPromise(
|
||||
`powershell -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Process -Id ${pid} | Select-Object Name,Id,Path,CommandLine | ConvertTo-Json"`,
|
||||
{ encoding: 'utf8' }
|
||||
const { stdout: processInfo } = await execFilePromise(
|
||||
'powershell',
|
||||
[
|
||||
'-NoProfile',
|
||||
'-Command',
|
||||
`Get-Process -Id ${pidArgs} -ErrorAction SilentlyContinue | Select-Object Name,Id,Path | ConvertTo-Json -Compress`
|
||||
],
|
||||
{ windowsHide: true, timeout: 4000, maxBuffer: 4 * 1024 * 1024 }
|
||||
)
|
||||
const processJson = JSON.parse(processInfo)
|
||||
managerLogger.info(`Process ${pid} info: ${processInfo.substring(0, 200)}`)
|
||||
|
||||
if (processJson.Name.includes('mihomo') && processJson.Path === null) {
|
||||
if (!processInfo.trim()) return false
|
||||
|
||||
const parsed = JSON.parse(processInfo)
|
||||
const list = Array.isArray(parsed) ? parsed : [parsed]
|
||||
for (const proc of list) {
|
||||
if (
|
||||
proc &&
|
||||
typeof proc.Name === 'string' &&
|
||||
proc.Name.toLowerCase().includes('mihomo') &&
|
||||
proc.Path === null
|
||||
) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
managerLogger.info(`Cannot get info for process ${pid}, might be high privilege`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
managerLogger.error(`Failed to check ${executable} processes`, error)
|
||||
}
|
||||
managerLogger.info('PowerShell process inspection failed', error)
|
||||
}
|
||||
} else {
|
||||
let foundProcesses = false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { exec } from 'child_process'
|
||||
import { exec, execFile } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { rm } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
@@ -6,6 +6,7 @@ import { managerLogger } from '../utils/logger'
|
||||
import { getAxios } from './mihomoApi'
|
||||
|
||||
const execPromise = promisify(exec)
|
||||
const execFilePromise = promisify(execFile)
|
||||
|
||||
// 常量
|
||||
const CORE_READY_MAX_RETRIES = 30
|
||||
@@ -19,7 +20,38 @@ export async function cleanupSocketFile(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupWindowsNamedPipes(): Promise<void> {
|
||||
// thorough=true 走 PowerShell 慢路径,仅在外部控制器监听冲突时使用
|
||||
export async function cleanupWindowsNamedPipes(thorough = false): Promise<void> {
|
||||
if (!thorough) {
|
||||
try {
|
||||
const { stdout } = await execFilePromise(
|
||||
'tasklist',
|
||||
['/FI', 'IMAGENAME eq mihomo*', '/FO', 'CSV', '/NH'],
|
||||
{ windowsHide: true, timeout: 1500, maxBuffer: 1 * 1024 * 1024 }
|
||||
)
|
||||
|
||||
const pids: number[] = []
|
||||
for (const line of stdout.split('\n')) {
|
||||
const match = line.match(/^"([^"]+)","(\d+)"/)
|
||||
if (!match) continue
|
||||
const pid = parseInt(match[2], 10)
|
||||
if (!isNaN(pid) && pid !== process.pid) pids.push(pid)
|
||||
}
|
||||
|
||||
if (pids.length === 0) return
|
||||
|
||||
for (const pid of pids) {
|
||||
await terminateProcess(pid)
|
||||
}
|
||||
|
||||
// 给进程留出退出窗口,避免 pipe 占用导致后续启动失败
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
} catch (error) {
|
||||
managerLogger.warn('Lightweight pipe cleanup failed:', error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
const { stdout } = await execPromise(
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
initCoreWatcher
|
||||
} from './core/manager'
|
||||
import { createTray } from './resolve/tray'
|
||||
import { init, initBasic, safeShowErrorBox } from './utils/init'
|
||||
import { init, initBasic, safeShowErrorBox, startSubStoreServices } from './utils/init'
|
||||
import { initShortcut } from './resolve/shortcut'
|
||||
import { initProfileUpdater } from './core/profileUpdater'
|
||||
import { startMonitor } from './resolve/trafficMonitor'
|
||||
@@ -48,7 +48,7 @@ function getWindowsPowerShellMajorVersion(): number | null {
|
||||
try {
|
||||
const stdout = execFileSync('reg', ['query', key, '/v', 'PowerShellVersion'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 1000
|
||||
timeout: 800
|
||||
})
|
||||
const version = stdout.match(/PowerShellVersion\s+REG_\w+\s+([^\s]+)/)?.[1]
|
||||
const major = version ? parseInt(version.split('.')[0], 10) : NaN
|
||||
@@ -61,6 +61,7 @@ function getWindowsPowerShellMajorVersion(): number | null {
|
||||
return null
|
||||
}
|
||||
|
||||
// PowerShell 版本过低必须在 app 启动前提示并退出,因此保持同步执行
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
const major = getWindowsPowerShellMajorVersion()
|
||||
@@ -103,50 +104,6 @@ initApp().catch((e) => {
|
||||
|
||||
setupPlatformSpecifics()
|
||||
|
||||
async function checkHighPrivilegeCoreEarly(): Promise<void> {
|
||||
if (process.platform !== 'win32') return
|
||||
|
||||
try {
|
||||
await initBasic()
|
||||
const isCurrentAppAdmin = await checkAdminPrivileges()
|
||||
if (isCurrentAppAdmin) return
|
||||
|
||||
const hasHighPrivilegeCore = await checkHighPrivilegeCore()
|
||||
if (!hasHighPrivilegeCore) return
|
||||
|
||||
try {
|
||||
const appConfig = await getAppConfig()
|
||||
const language = appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US')
|
||||
await initI18n({ lng: language })
|
||||
} catch {
|
||||
await initI18n({ lng: 'zh-CN' })
|
||||
}
|
||||
|
||||
const choice = dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
title: i18next.t('core.highPrivilege.title'),
|
||||
message: i18next.t('core.highPrivilege.message'),
|
||||
buttons: [i18next.t('common.confirm'), i18next.t('common.cancel')],
|
||||
defaultId: 0,
|
||||
cancelId: 1
|
||||
})
|
||||
|
||||
if (choice === 0) {
|
||||
try {
|
||||
await restartAsAdmin(false)
|
||||
app.exit(0)
|
||||
} catch (error) {
|
||||
safeShowErrorBox('common.error.adminRequired', `${error}`)
|
||||
app.exit(1)
|
||||
}
|
||||
} else {
|
||||
app.exit(0)
|
||||
}
|
||||
} catch (e) {
|
||||
mainLogger.error('Failed to check high privilege core', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function initHardwareAcceleration(): Promise<void> {
|
||||
try {
|
||||
await initBasic()
|
||||
@@ -177,23 +134,69 @@ app.on('open-url', async (_event, url) => {
|
||||
|
||||
const initPromise = (async () => {
|
||||
await initBasic()
|
||||
await checkHighPrivilegeCoreEarly()
|
||||
await initAdminStatus()
|
||||
|
||||
const adminPromise: Promise<boolean> =
|
||||
process.platform === 'win32' ? checkAdminPrivileges().catch(() => false) : Promise.resolve(true)
|
||||
|
||||
const appConfigPromise = (async () => {
|
||||
try {
|
||||
const appConfig = await getAppConfig()
|
||||
if (!appConfig.language) {
|
||||
const cfg = await getAppConfig()
|
||||
if (!cfg.language) {
|
||||
const systemLanguage = getSystemLanguage()
|
||||
await patchAppConfig({ language: systemLanguage })
|
||||
appConfig.language = systemLanguage
|
||||
cfg.language = systemLanguage
|
||||
}
|
||||
await initI18n({ lng: appConfig.language })
|
||||
return appConfig
|
||||
await initI18n({ lng: cfg.language })
|
||||
return cfg
|
||||
} catch (e) {
|
||||
safeShowErrorBox('common.error.initFailed', `${e}`)
|
||||
app.quit()
|
||||
throw e
|
||||
}
|
||||
})()
|
||||
|
||||
await adminPromise
|
||||
await initAdminStatus()
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const isAdmin = await adminPromise
|
||||
if (!isAdmin) {
|
||||
try {
|
||||
const hasHighPrivilegeCore = await checkHighPrivilegeCore()
|
||||
if (hasHighPrivilegeCore) {
|
||||
try {
|
||||
await appConfigPromise
|
||||
} catch {
|
||||
await initI18n({ lng: 'zh-CN' })
|
||||
}
|
||||
const choice = dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
title: i18next.t('core.highPrivilege.title'),
|
||||
message: i18next.t('core.highPrivilege.message'),
|
||||
buttons: [i18next.t('common.confirm'), i18next.t('common.cancel')],
|
||||
defaultId: 0,
|
||||
cancelId: 1
|
||||
})
|
||||
|
||||
if (choice === 0) {
|
||||
try {
|
||||
await restartAsAdmin(false)
|
||||
app.exit(0)
|
||||
} catch (error) {
|
||||
safeShowErrorBox('common.error.adminRequired', `${error}`)
|
||||
app.exit(1)
|
||||
}
|
||||
} else {
|
||||
app.exit(0)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
mainLogger.error('Failed to check high privilege core', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appConfigPromise
|
||||
})()
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
@@ -219,9 +222,15 @@ app.whenReady().then(async () => {
|
||||
const startPromises = await startCore()
|
||||
if (startPromises.length > 0) {
|
||||
startPromises[0].then(async () => {
|
||||
await initProfileUpdater()
|
||||
await initWebdavBackupScheduler()
|
||||
await checkAdminRestartForTun()
|
||||
await Promise.allSettled([
|
||||
initProfileUpdater().catch((e) => mainLogger.warn('Failed to init profile updater', e)),
|
||||
initWebdavBackupScheduler().catch((e) =>
|
||||
mainLogger.warn('Failed to init webdav backup scheduler', e)
|
||||
),
|
||||
checkAdminRestartForTun().catch((e) =>
|
||||
mainLogger.warn('Failed admin-restart-for-tun follow-up', e)
|
||||
)
|
||||
])
|
||||
})
|
||||
}
|
||||
coreStarted = true
|
||||
@@ -240,6 +249,10 @@ app.whenReady().then(async () => {
|
||||
|
||||
await createWindowPromise
|
||||
|
||||
void startSubStoreServices().catch((e) =>
|
||||
mainLogger.warn('Failed to start sub-store services', e)
|
||||
)
|
||||
|
||||
const { showFloatingWindow: showFloating = false, disableTray = false } = appConfig
|
||||
const uiTasks: Promise<void>[] = [initShortcut()]
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { spawn, exec, execSync } from 'child_process'
|
||||
import { spawn, exec, execFileSync } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { stat } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import { app, powerMonitor } from 'electron'
|
||||
import { stopCore, cleanupCoreWatcher } from './core/manager'
|
||||
import { primeAdminPrivilegesCache } from './core/admin'
|
||||
import { triggerSysProxy, disableSysProxySync } from './sys/sysproxy'
|
||||
import { exePath } from './utils/dirs'
|
||||
|
||||
@@ -55,15 +56,19 @@ export function setupPlatformSpecifics(): void {
|
||||
app.commandLine.appendSwitch('in-process-gpu')
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' && isWindowsElevatedSync()) {
|
||||
if (process.platform === 'win32') {
|
||||
const elevated = isWindowsElevatedSync()
|
||||
if (elevated) {
|
||||
primeAdminPrivilegesCache(true)
|
||||
app.commandLine.appendSwitch('disable-gpu-sandbox')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isWindowsElevatedSync(): boolean {
|
||||
if (process.platform !== 'win32') return false
|
||||
try {
|
||||
execSync('fltmc', { stdio: 'ignore', windowsHide: true })
|
||||
execFileSync('fltmc', [], { stdio: 'ignore', windowsHide: true, timeout: 800 })
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
@@ -107,7 +112,7 @@ export function setupAppLifecycle(): void {
|
||||
}),
|
||||
stopCore()
|
||||
]).then(() => {}),
|
||||
3000
|
||||
1200
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,11 @@ async function killOldMihomoProcesses(): Promise<void> {
|
||||
try {
|
||||
const execFilePromise = promisify(execFile)
|
||||
const coreNames = new Set(['mihomo.exe', 'mihomo-alpha.exe', 'mihomo-smart.exe'])
|
||||
const { stdout } = await execFilePromise('tasklist', ['/FO', 'CSV', '/NH'])
|
||||
const { stdout } = await execFilePromise('tasklist', ['/FO', 'CSV', '/NH'], {
|
||||
windowsHide: true,
|
||||
timeout: 3000,
|
||||
maxBuffer: 4 * 1024 * 1024
|
||||
})
|
||||
|
||||
const pids = stdout
|
||||
.split('\n')
|
||||
@@ -445,13 +449,7 @@ export async function ensureRuntimeFiles(): Promise<void> {
|
||||
export async function init(): Promise<void> {
|
||||
const { sysProxy } = await getAppConfig()
|
||||
|
||||
const initTasks: Promise<void>[] = [
|
||||
(async (): Promise<void> => {
|
||||
await ensureRuntimeFiles()
|
||||
await Promise.all([startSubStoreFrontendServer(), startSubStoreBackendServer()])
|
||||
})(),
|
||||
startSSIDCheck()
|
||||
]
|
||||
const initTasks: Promise<void>[] = [ensureRuntimeFiles(), startSSIDCheck()]
|
||||
|
||||
initTasks.push(
|
||||
(async (): Promise<void> => {
|
||||
@@ -469,3 +467,8 @@ export async function init(): Promise<void> {
|
||||
await Promise.all(initTasks)
|
||||
initDeeplink()
|
||||
}
|
||||
|
||||
export async function startSubStoreServices(): Promise<void> {
|
||||
await ensureRuntimeFiles()
|
||||
await Promise.all([startSubStoreFrontendServer(), startSubStoreBackendServer()])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user