From 0dad7a6d8bbd18e20d2fe1e5fcd9b9f114ccf4f2 Mon Sep 17 00:00:00 2001 From: ezequielnick <107352853+ezequielnick@users.noreply.github.com> Date: Mon, 11 Aug 2025 18:52:11 +0800 Subject: [PATCH] refactor: improve core permission management2 --- src/main/core/manager.ts | 372 ++++++++++++++++++++------------------- src/main/index.ts | 40 ++++- 2 files changed, 222 insertions(+), 190 deletions(-) diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index ec8de6a..cbf759b 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -72,23 +72,14 @@ export async function startCore(detached = false): Promise[]> { skipSafePathCheck = false } = await getAppConfig() const { 'log-level': logLevel } = await getControledMihomoConfig() - if (existsSync(path.join(dataDir(), 'core.pid'))) { - const pid = parseInt(await readFile(path.join(dataDir(), 'core.pid'), 'utf-8')) - try { - process.kill(pid, 'SIGINT') - } catch { - // ignore - } finally { - await rm(path.join(dataDir(), 'core.pid')) - } - } + + await handleExistingCore() + const { current } = await getProfileConfig() const { tun } = await getControledMihomoConfig() const corePath = mihomoCorePath(core) - // 管理 Smart 内核覆写配置 await manageSmartOverride() - await generateProfile() await checkProfile() await stopCore() @@ -154,6 +145,14 @@ export async function startCore(detached = false): Promise[]> { if ((process.platform !== 'win32' && str.includes('External controller unix listen error')) || (process.platform === 'win32' && str.includes('External controller pipe listen error')) ) { + if (str.includes('permission denied') || str.includes('access denied') || + str.includes('operation not permitted') || str.includes('Access is denied')) { + checkForElevatedCore().catch((error) => { + if (error instanceof Error && error.message !== 'Application restart required for permission alignment') { + console.error('Failed to check for elevated core:', error) + } + }) + } reject(i18next.t('mihomo.error.externalControllerListenError')) } @@ -223,11 +222,9 @@ export async function restartCore(): Promise { try { await startCore() } catch (e) { - // 记录错误到日志而不是显示阻塞对话框 await writeFile(logPath(), `[Manager]: restart core failed, ${e}\n`, { flag: 'a' }) - // 重新抛出错误,让调用者处理 throw e } } @@ -282,13 +279,8 @@ async function checkProfile(): Promise { mihomoTestDir() ], { env }) } catch (error) { - console.error('Profile check failed:', error) - if (error instanceof Error && 'stdout' in error) { - const { stdout, stderr } = error as { stdout: string; stderr?: string } - console.log('Profile check stdout:', stdout) - console.log('Profile check stderr:', stderr) - + const { stdout } = error as { stdout: string; stderr?: string } const errorLines = stdout .split('\n') .filter((line) => line.includes('level=error') || line.includes('error')) @@ -385,10 +377,10 @@ export async function restartAsAdmin(): Promise { const exePath = process.execPath const args = process.argv.slice(1) - const restartArgs = [...args, '--admin-restart-for-tun'] + const cleanArgs = args.filter(arg => !arg.startsWith('--admin-restart')) + const restartArgs = [...cleanArgs, '--admin-restart-for-permission'] try { - // 处理路径和参数的引号 const escapedExePath = exePath.replace(/'/g, "''") const argsString = restartArgs.map(arg => arg.replace(/'/g, "''")).join("', '") @@ -399,15 +391,10 @@ export async function restartAsAdmin(): Promise { command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"` } - console.log('Restarting as administrator with command:', command) - - // 执行PowerShell命令 exec(command, { windowsHide: true }, (error, _stdout, stderr) => { if (error) { console.error('PowerShell execution error:', error) console.error('stderr:', stderr) - } else { - console.log('PowerShell command executed successfully') } }) @@ -429,7 +416,6 @@ export async function checkMihomoCorePermissions(): Promise { try { if (process.platform === 'win32') { - // Windows权限检查 return await checkAdminPrivileges() } @@ -445,7 +431,6 @@ export async function checkMihomoCorePermissions(): Promise { return false } -// TUN模式获取权限 export async function requestTunPermissions(): Promise { if (process.platform === 'win32') { await restartAsAdmin() @@ -459,8 +444,6 @@ export async function requestTunPermissions(): Promise { export async function checkAdminRestartForTun(): Promise { if (process.argv.includes('--admin-restart-for-tun')) { - console.log('Detected admin restart for TUN mode, auto-enabling TUN...') - try { if (process.platform === 'win32') { const hasAdminPrivileges = await checkAdminPrivileges() @@ -468,20 +451,17 @@ export async function checkAdminRestartForTun(): Promise { await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } }) await restartCore() - console.log('TUN mode auto-enabled after admin restart') - const { mainWindow } = await import('../index') mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') - } else { - console.warn('Admin restart detected but no admin privileges found') } } } catch (error) { console.error('Failed to auto-enable TUN after admin restart:', error) } + } else if (process.argv.includes('--admin-restart-for-permission')) { + await checkAndAlignPermissions() } else { - // 检查TUN配置与权限的匹配 await checkAndAlignPermissions() } } @@ -491,18 +471,59 @@ export async function checkAndAlignPermissions(): Promise { const runningCoreInfo = await getRunningCoreInfo() const currentUserIsAdmin = await checkAdminPrivileges() - if (runningCoreInfo) { - const coreIsAdmin = runningCoreInfo.isAdmin + if (runningCoreInfo && !currentUserIsAdmin && runningCoreInfo.isAdmin) { + const { dialog } = await import('electron') + const result = await dialog.showMessageBox({ + type: 'warning', + title: '权限不匹配', + message: '检测到系统中有高权限内核正在运行', + detail: '需要以管理员模式重新启动本应用程序。点击确定将自动重启为管理员模式。', + buttons: ['确定', '取消'], + defaultId: 0, + cancelId: 1 + }) - if (currentUserIsAdmin !== coreIsAdmin) { - await handlePermissionMismatch(currentUserIsAdmin, coreIsAdmin) - return + if (result.response === 0) { + await restartAsAdmin() + } else { + const { app } = await import('electron') + app.quit() } + + throw new Error('Application restart required for permission alignment') } await validateTunPermissionsOnStartup() } catch (error) { - console.error('Failed to check and align permissions:', error) + if (error instanceof Error && error.message === 'Application restart required for permission alignment') { + throw error + } + console.error('Failed to check permissions:', error) + } +} + +async function handleExistingCore(): Promise { + const pidFile = path.join(dataDir(), 'core.pid') + + if (!existsSync(pidFile)) { + return + } + + try { + const pidStr = await readFile(pidFile, 'utf-8') + const pid = parseInt(pidStr.trim()) + + if (isProcessRunning(pid)) { + try { + process.kill(pid, 'SIGINT') + } catch (error) { + console.warn('Failed to kill existing process:', error) + } + } + + await cleanupCoreFiles() + } catch (error) { + throw error } } @@ -522,8 +543,8 @@ async function getRunningCoreInfo(): Promise<{ pid: number; isAdmin: boolean } | await rm(pidFile).catch(() => {}) return null } - } catch { - // ignore + } catch (error) { + console.warn('Failed to read state file:', error) } } @@ -544,7 +565,80 @@ async function getRunningCoreInfo(): Promise<{ pid: number; isAdmin: boolean } | } } - return null + return await findMihomoCoreProcess() +} + +async function findMihomoCoreProcess(): Promise<{ pid: number; isAdmin: boolean } | null> { + if (process.platform !== 'win32') { + return null + } + + try { + const execPromise = promisify(exec) + const { stdout } = await execPromise('tasklist /fi "imagename eq mihomo*" /fo csv') + + const lines = stdout.split('\n').filter(line => line.trim() && !line.includes('INFO:')) + + for (const line of lines) { + if (line.includes('mihomo')) { + const parts = line.split(',').map(part => part.replace(/"/g, '').trim()) + if (parts.length >= 2) { + const processName = parts[0] + const pidStr = parts[1] + const pid = parseInt(pidStr) + + if (!isNaN(pid) && processName.toLowerCase().includes('mihomo')) { + const isAdmin = await checkProcessElevation(pid) + return { pid, isAdmin } + } + } + } + } + + return null + } catch (error) { + console.warn('Failed to search for mihomo processes:', error) + return null + } +} + +async function checkProcessElevation(pid: number): Promise { + if (process.platform !== 'win32') { + return false + } + + try { + const execPromise = promisify(exec) + const currentIsAdmin = await checkAdminPrivileges() + + if (currentIsAdmin) { + try { + const { stdout } = await execPromise(`wmic process where "ProcessId=${pid}" get ExecutablePath,ProcessId /format:list`) + return stdout.includes('ExecutablePath=') + } catch { + return false + } + } else { + try { + const { stdout } = await execPromise(`tasklist /fi "PID eq ${pid}" /fo csv /v`) + const lines = stdout.split('\n') + + if (lines.length > 1) { + const processLine = lines[1] + if (processLine.includes('NT AUTHORITY') || processLine.includes('SYSTEM')) { + return true + } + return true + } + return false + } catch { + return true + } + } + } catch (error) { + console.warn('Failed to check process elevation:', error) + return true + } } function isProcessRunning(pid: number): boolean { @@ -556,156 +650,36 @@ function isProcessRunning(pid: number): boolean { } } -async function isProcessRunningAsAdmin(_pid: number): Promise { +async function isProcessRunningAsAdmin(pid: number): Promise { if (process.platform !== 'win32') { return true } + try { + const stateFile = path.join(dataDir(), 'core.state') + if (existsSync(stateFile)) { + const stateData = JSON.parse(await readFile(stateFile, 'utf-8')) + if (stateData.pid === pid && typeof stateData.isAdmin === 'boolean') { + return stateData.isAdmin + } + } + } catch { + } + return await checkAdminPrivileges() } -async function handlePermissionMismatch(guiIsAdmin: boolean, coreIsAdmin: boolean): Promise { - if (guiIsAdmin && !coreIsAdmin) { - console.log('GUI is admin but core is not, restarting core with elevated permissions') - await restartCore() - } else if (!guiIsAdmin && coreIsAdmin) { - console.log('GUI is not admin but core is elevated, attempting graceful handling') - await handleElevatedCoreScenario() - } -} -async function handleElevatedCoreScenario(): Promise { - const { tun } = await getControledMihomoConfig() - if (tun?.enable) { - console.log('TUN mode detected with elevated core, attempting graceful shutdown') - const success = await attemptGracefulCoreShutdown() - if (success) { - await patchControledMihomoConfig({ tun: { enable: false } }) - const { mainWindow } = await import('../index') - mainWindow?.webContents.send('controledMihomoConfigUpdated') - ipcMain.emit('updateTrayMenu') - } else { - console.warn('Cannot shutdown elevated core gracefully, user intervention required') - throw new Error('Elevated core process requires manual intervention') - } - } else { - console.log('Non-TUN mode, attempting graceful shutdown of elevated core') - const success = await attemptGracefulCoreShutdown() - if (!success) { - console.warn('Cannot shutdown elevated core gracefully') - throw new Error('Cannot manage elevated core process') - } - } -} -async function attemptGracefulCoreShutdown(): Promise { - try { - const success = await shutdownViaMihomoAPI() - if (success) { - console.log('Core shutdown gracefully via API') - return true - } - } catch (error) { - console.warn('API shutdown failed:', error) - } - try { - const success = await forceStopRunningCore() - if (success) { - console.log('Core stopped via process termination') - return true - } - } catch (error) { - console.warn('Force stop failed:', error) - } - return false -} -async function shutdownViaMihomoAPI(): Promise { - try { - const { mihomoRestart } = await import('./mihomoApi') - await mihomoRestart() - await waitForProcessExit(3000) - return true - } catch (error) { - console.warn('Mihomo API shutdown failed:', error) - return false - } -} -async function forceStopRunningCore(): Promise { - const pidFile = path.join(dataDir(), 'core.pid') - const stateFile = path.join(dataDir(), 'core.state') - if (!existsSync(pidFile)) { - return true - } - try { - const pidStr = await readFile(pidFile, 'utf-8') - const pid = parseInt(pidStr.trim()) - - if (!isProcessRunning(pid)) { - await rm(pidFile).catch(() => {}) - await rm(stateFile).catch(() => {}) - return true - } - - if (process.platform === 'win32') { - const currentIsAdmin = await checkAdminPrivileges() - if (!currentIsAdmin) { - console.warn('Cannot terminate elevated process without admin privileges') - return false - } - } - - process.kill(pid, 'SIGTERM') - - await new Promise(resolve => setTimeout(resolve, 1000)) - - if (isProcessRunning(pid)) { - process.kill(pid, 'SIGKILL') - await new Promise(resolve => setTimeout(resolve, 500)) - } - - await rm(pidFile).catch(() => {}) - await rm(stateFile).catch(() => {}) - - return !isProcessRunning(pid) - } catch (error) { - console.error('Failed to force stop running core:', error) - return false - } -} - -async function waitForProcessExit(timeoutMs: number): Promise { - const pidFile = path.join(dataDir(), 'core.pid') - - if (!existsSync(pidFile)) { - return true - } - - try { - const pidStr = await readFile(pidFile, 'utf-8') - const pid = parseInt(pidStr.trim()) - - const startTime = Date.now() - while (Date.now() - startTime < timeoutMs) { - if (!isProcessRunning(pid)) { - return true - } - await new Promise(resolve => setTimeout(resolve, 100)) - } - - return false - } catch { - return true - } -} export async function validateTunPermissionsOnStartup(): Promise { try { @@ -718,23 +692,55 @@ export async function validateTunPermissionsOnStartup(): Promise { const hasPermissions = await checkMihomoCorePermissions() if (!hasPermissions) { - console.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...') - await patchControledMihomoConfig({ tun: { enable: false } }) const { mainWindow } = await import('../index') mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') - - console.log('TUN auto-disabled due to insufficient permissions') - } else { - console.log('TUN permissions validated successfully') } } catch (error) { console.error('Failed to validate TUN permissions on startup:', error) } } +async function checkForElevatedCore(): Promise { + try { + const runningCoreInfo = await getRunningCoreInfo() + if (!runningCoreInfo) { + return + } + + const currentUserIsAdmin = await checkAdminPrivileges() + + if (!currentUserIsAdmin && runningCoreInfo.isAdmin) { + const { dialog } = await import('electron') + const result = await dialog.showMessageBox({ + type: 'warning', + title: '权限不匹配', + message: '检测到系统中有高权限内核正在运行', + detail: '需要以管理员模式重新启动本应用程序。点击确定将自动重启为管理员模式。', + buttons: ['确定', '取消'], + defaultId: 0, + cancelId: 1 + }) + + if (result.response === 0) { + await restartAsAdmin() + } else { + const { app } = await import('electron') + app.quit() + } + + throw new Error('Application restart required for permission alignment') + } + } catch (error) { + if (error instanceof Error && error.message === 'Application restart required for permission alignment') { + throw error + } + console.error('Failed to check for elevated core:', error) + } +} + export async function manualGrantCorePermition(): Promise { return grantTunPermissions() } diff --git a/src/main/index.ts b/src/main/index.ts index bacfd60..6f55b47 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -169,16 +169,24 @@ app.whenReady().then(async () => { showSafeErrorBox('common.error.initFailed', `${e}`) app.quit() } + // 权限检查 try { - const [startPromise] = await startCore() - startPromise.then(async () => { - await initProfileUpdater() - // 上次是否为了开启 TUN 而重启 - await checkAdminRestartForTun() - }) + console.log('Starting permission check before GUI creation...') + await checkAdminRestartForTun() + console.log('Permission check passed, continuing with app startup...') } catch (e) { - showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`) + console.error('Permission check failed:', e) + + if (e instanceof Error && e.message === 'Application restart required for permission alignment') { + console.log('Application is restarting for permission alignment, exiting...') + return + } + + showSafeErrorBox('common.error.initFailed', `${e}`) + app.quit() + return } + try { await startMonitor() } catch { @@ -194,6 +202,24 @@ app.whenReady().then(async () => { const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig() registerIpcMainHandlers() await createWindow() + + // 异步启动内核 + const startCoreAsync = async () => { + try { + const [startPromise] = await startCore() + startPromise.then(async () => { + await initProfileUpdater() + }) + } catch (e) { + console.error('Core start failed:', e) + + setTimeout(() => { + showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`) + }, 3000) + } + } + + startCoreAsync() if (showFloating) { try { await showFloatingWindow()