refactor: improve core permission management

This commit is contained in:
ezequielnick 2025-08-10 20:42:46 +08:00
parent a8f8cd0fd3
commit 1e3f31d1a7
2 changed files with 249 additions and 1 deletions

View File

@ -203,12 +203,22 @@ export async function stopCore(force = false): Promise<void> {
child.removeAllListeners() child.removeAllListeners()
child.kill('SIGINT') child.kill('SIGINT')
} }
await cleanupCoreFiles()
stopMihomoTraffic() stopMihomoTraffic()
stopMihomoConnections() stopMihomoConnections()
stopMihomoLogs() stopMihomoLogs()
stopMihomoMemory() stopMihomoMemory()
} }
async function cleanupCoreFiles(): Promise<void> {
const pidFile = path.join(dataDir(), 'core.pid')
const stateFile = path.join(dataDir(), 'core.state')
await rm(pidFile).catch(() => {})
await rm(stateFile).catch(() => {})
}
export async function restartCore(): Promise<void> { export async function restartCore(): Promise<void> {
try { try {
await startCore() await startCore()
@ -226,13 +236,25 @@ export async function keepCoreAlive(): Promise<void> {
try { try {
await startCore(true) await startCore(true)
if (child && child.pid) { if (child && child.pid) {
await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString()) await writeCoreStateFile(child.pid)
} }
} catch (e) { } catch (e) {
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`)
} }
} }
async function writeCoreStateFile(pid: number): Promise<void> {
const isAdmin = await checkAdminPrivileges()
const stateData = {
pid,
isAdmin,
startTime: Date.now()
}
await writeFile(path.join(dataDir(), 'core.pid'), pid.toString())
await writeFile(path.join(dataDir(), 'core.state'), JSON.stringify(stateData))
}
export async function quitWithoutCore(): Promise<void> { export async function quitWithoutCore(): Promise<void> {
await keepCoreAlive() await keepCoreAlive()
await startMonitor(true) await startMonitor(true)
@ -460,7 +482,228 @@ export async function checkAdminRestartForTun(): Promise<void> {
} }
} else { } else {
// 检查TUN配置与权限的匹配 // 检查TUN配置与权限的匹配
await checkAndAlignPermissions()
}
}
export async function checkAndAlignPermissions(): Promise<void> {
try {
const runningCoreInfo = await getRunningCoreInfo()
const currentUserIsAdmin = await checkAdminPrivileges()
if (runningCoreInfo) {
const coreIsAdmin = runningCoreInfo.isAdmin
if (currentUserIsAdmin !== coreIsAdmin) {
await handlePermissionMismatch(currentUserIsAdmin, coreIsAdmin)
return
}
}
await validateTunPermissionsOnStartup() await validateTunPermissionsOnStartup()
} catch (error) {
console.error('Failed to check and align permissions:', error)
}
}
async function getRunningCoreInfo(): Promise<{ pid: number; isAdmin: boolean } | null> {
const stateFile = path.join(dataDir(), 'core.state')
const pidFile = path.join(dataDir(), 'core.pid')
if (existsSync(stateFile)) {
try {
const stateData = JSON.parse(await readFile(stateFile, 'utf-8'))
const { pid, isAdmin } = stateData
if (isProcessRunning(pid)) {
return { pid, isAdmin }
} else {
await rm(stateFile).catch(() => {})
await rm(pidFile).catch(() => {})
return null
}
} catch {
// ignore
}
}
if (existsSync(pidFile)) {
try {
const pidStr = await readFile(pidFile, 'utf-8')
const pid = parseInt(pidStr.trim())
if (!isProcessRunning(pid)) {
await rm(pidFile)
return null
}
const isAdmin = await isProcessRunningAsAdmin(pid)
return { pid, isAdmin }
} catch {
return null
}
}
return null
}
function isProcessRunning(pid: number): boolean {
try {
process.kill(pid, 0)
return true
} catch {
return false
}
}
async function isProcessRunningAsAdmin(_pid: number): Promise<boolean> {
if (process.platform !== 'win32') {
return true
}
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
} }
} }

View File

@ -51,6 +51,11 @@ export const patchMihomoConfig = async (patch: Partial<IMihomoConfig>): Promise<
return await instance.patch('/configs', patch) return await instance.patch('/configs', patch)
} }
export const mihomoRestart = async (): Promise<void> => {
const instance = await getAxios()
return await instance.post('/restart', {})
}
export const mihomoCloseConnection = async (id: string): Promise<void> => { export const mihomoCloseConnection = async (id: string): Promise<void> => {
const instance = await getAxios() const instance = await getAxios()
return await instance.delete(`/connections/${encodeURIComponent(id)}`) return await instance.delete(`/connections/${encodeURIComponent(id)}`)