refactor: improve core permission management2

This commit is contained in:
ezequielnick 2025-08-11 18:52:11 +08:00
parent 1e3f31d1a7
commit 0dad7a6d8b
2 changed files with 222 additions and 190 deletions

View File

@ -72,23 +72,14 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
skipSafePathCheck = false skipSafePathCheck = false
} = await getAppConfig() } = await getAppConfig()
const { 'log-level': logLevel } = await getControledMihomoConfig() 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')) await handleExistingCore()
try {
process.kill(pid, 'SIGINT')
} catch {
// ignore
} finally {
await rm(path.join(dataDir(), 'core.pid'))
}
}
const { current } = await getProfileConfig() const { current } = await getProfileConfig()
const { tun } = await getControledMihomoConfig() const { tun } = await getControledMihomoConfig()
const corePath = mihomoCorePath(core) const corePath = mihomoCorePath(core)
// 管理 Smart 内核覆写配置
await manageSmartOverride() await manageSmartOverride()
await generateProfile() await generateProfile()
await checkProfile() await checkProfile()
await stopCore() await stopCore()
@ -154,6 +145,14 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
if ((process.platform !== 'win32' && str.includes('External controller unix listen error')) || if ((process.platform !== 'win32' && str.includes('External controller unix listen error')) ||
(process.platform === 'win32' && str.includes('External controller pipe 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')) reject(i18next.t('mihomo.error.externalControllerListenError'))
} }
@ -223,11 +222,9 @@ export async function restartCore(): Promise<void> {
try { try {
await startCore() await startCore()
} catch (e) { } catch (e) {
// 记录错误到日志而不是显示阻塞对话框
await writeFile(logPath(), `[Manager]: restart core failed, ${e}\n`, { await writeFile(logPath(), `[Manager]: restart core failed, ${e}\n`, {
flag: 'a' flag: 'a'
}) })
// 重新抛出错误,让调用者处理
throw e throw e
} }
} }
@ -282,13 +279,8 @@ async function checkProfile(): Promise<void> {
mihomoTestDir() mihomoTestDir()
], { env }) ], { env })
} catch (error) { } catch (error) {
console.error('Profile check failed:', error)
if (error instanceof Error && 'stdout' in error) { if (error instanceof Error && 'stdout' in error) {
const { stdout, stderr } = error as { stdout: string; stderr?: string } const { stdout } = error as { stdout: string; stderr?: string }
console.log('Profile check stdout:', stdout)
console.log('Profile check stderr:', stderr)
const errorLines = stdout const errorLines = stdout
.split('\n') .split('\n')
.filter((line) => line.includes('level=error') || line.includes('error')) .filter((line) => line.includes('level=error') || line.includes('error'))
@ -385,10 +377,10 @@ export async function restartAsAdmin(): Promise<void> {
const exePath = process.execPath const exePath = process.execPath
const args = process.argv.slice(1) 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 { try {
// 处理路径和参数的引号
const escapedExePath = exePath.replace(/'/g, "''") const escapedExePath = exePath.replace(/'/g, "''")
const argsString = restartArgs.map(arg => arg.replace(/'/g, "''")).join("', '") const argsString = restartArgs.map(arg => arg.replace(/'/g, "''")).join("', '")
@ -399,15 +391,10 @@ export async function restartAsAdmin(): Promise<void> {
command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"` 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) => { exec(command, { windowsHide: true }, (error, _stdout, stderr) => {
if (error) { if (error) {
console.error('PowerShell execution error:', error) console.error('PowerShell execution error:', error)
console.error('stderr:', stderr) console.error('stderr:', stderr)
} else {
console.log('PowerShell command executed successfully')
} }
}) })
@ -429,7 +416,6 @@ export async function checkMihomoCorePermissions(): Promise<boolean> {
try { try {
if (process.platform === 'win32') { if (process.platform === 'win32') {
// Windows权限检查
return await checkAdminPrivileges() return await checkAdminPrivileges()
} }
@ -445,7 +431,6 @@ export async function checkMihomoCorePermissions(): Promise<boolean> {
return false return false
} }
// TUN模式获取权限
export async function requestTunPermissions(): Promise<void> { export async function requestTunPermissions(): Promise<void> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
await restartAsAdmin() await restartAsAdmin()
@ -459,8 +444,6 @@ export async function requestTunPermissions(): Promise<void> {
export async function checkAdminRestartForTun(): Promise<void> { export async function checkAdminRestartForTun(): Promise<void> {
if (process.argv.includes('--admin-restart-for-tun')) { if (process.argv.includes('--admin-restart-for-tun')) {
console.log('Detected admin restart for TUN mode, auto-enabling TUN...')
try { try {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const hasAdminPrivileges = await checkAdminPrivileges() const hasAdminPrivileges = await checkAdminPrivileges()
@ -468,20 +451,17 @@ export async function checkAdminRestartForTun(): Promise<void> {
await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } }) await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } })
await restartCore() await restartCore()
console.log('TUN mode auto-enabled after admin restart')
const { mainWindow } = await import('../index') const { mainWindow } = await import('../index')
mainWindow?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
} else {
console.warn('Admin restart detected but no admin privileges found')
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to auto-enable TUN after admin restart:', error) console.error('Failed to auto-enable TUN after admin restart:', error)
} }
} else if (process.argv.includes('--admin-restart-for-permission')) {
await checkAndAlignPermissions()
} else { } else {
// 检查TUN配置与权限的匹配
await checkAndAlignPermissions() await checkAndAlignPermissions()
} }
} }
@ -491,18 +471,59 @@ export async function checkAndAlignPermissions(): Promise<void> {
const runningCoreInfo = await getRunningCoreInfo() const runningCoreInfo = await getRunningCoreInfo()
const currentUserIsAdmin = await checkAdminPrivileges() const currentUserIsAdmin = await checkAdminPrivileges()
if (runningCoreInfo) { if (runningCoreInfo && !currentUserIsAdmin && runningCoreInfo.isAdmin) {
const coreIsAdmin = 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) { if (result.response === 0) {
await handlePermissionMismatch(currentUserIsAdmin, coreIsAdmin) await restartAsAdmin()
return } else {
const { app } = await import('electron')
app.quit()
} }
throw new Error('Application restart required for permission alignment')
} }
await validateTunPermissionsOnStartup() await validateTunPermissionsOnStartup()
} catch (error) { } 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<void> {
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(() => {}) await rm(pidFile).catch(() => {})
return null return null
} }
} catch { } catch (error) {
// ignore 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<boolean> {
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 { function isProcessRunning(pid: number): boolean {
@ -556,156 +650,36 @@ function isProcessRunning(pid: number): boolean {
} }
} }
async function isProcessRunningAsAdmin(_pid: number): Promise<boolean> { async function isProcessRunningAsAdmin(pid: number): Promise<boolean> {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
return true 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() return await checkAdminPrivileges()
} }
async function handlePermissionMismatch(guiIsAdmin: boolean, coreIsAdmin: boolean): Promise<void> {
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<void> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<void> { export async function validateTunPermissionsOnStartup(): Promise<void> {
try { try {
@ -718,23 +692,55 @@ export async function validateTunPermissionsOnStartup(): Promise<void> {
const hasPermissions = await checkMihomoCorePermissions() const hasPermissions = await checkMihomoCorePermissions()
if (!hasPermissions) { if (!hasPermissions) {
console.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...')
await patchControledMihomoConfig({ tun: { enable: false } }) await patchControledMihomoConfig({ tun: { enable: false } })
const { mainWindow } = await import('../index') const { mainWindow } = await import('../index')
mainWindow?.webContents.send('controledMihomoConfigUpdated') mainWindow?.webContents.send('controledMihomoConfigUpdated')
ipcMain.emit('updateTrayMenu') ipcMain.emit('updateTrayMenu')
console.log('TUN auto-disabled due to insufficient permissions')
} else {
console.log('TUN permissions validated successfully')
} }
} catch (error) { } catch (error) {
console.error('Failed to validate TUN permissions on startup:', error) console.error('Failed to validate TUN permissions on startup:', error)
} }
} }
async function checkForElevatedCore(): Promise<void> {
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<void> { export async function manualGrantCorePermition(): Promise<void> {
return grantTunPermissions() return grantTunPermissions()
} }

View File

@ -169,16 +169,24 @@ app.whenReady().then(async () => {
showSafeErrorBox('common.error.initFailed', `${e}`) showSafeErrorBox('common.error.initFailed', `${e}`)
app.quit() app.quit()
} }
// 权限检查
try { try {
const [startPromise] = await startCore() console.log('Starting permission check before GUI creation...')
startPromise.then(async () => { await checkAdminRestartForTun()
await initProfileUpdater() console.log('Permission check passed, continuing with app startup...')
// 上次是否为了开启 TUN 而重启
await checkAdminRestartForTun()
})
} catch (e) { } 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 { try {
await startMonitor() await startMonitor()
} catch { } catch {
@ -194,6 +202,24 @@ app.whenReady().then(async () => {
const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig() const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig()
registerIpcMainHandlers() registerIpcMainHandlers()
await createWindow() 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) { if (showFloating) {
try { try {
await showFloatingWindow() await showFloatingWindow()