diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 5baa7f0..ec8de6a 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -203,12 +203,22 @@ export async function stopCore(force = false): Promise { child.removeAllListeners() child.kill('SIGINT') } + + await cleanupCoreFiles() stopMihomoTraffic() stopMihomoConnections() stopMihomoLogs() stopMihomoMemory() } +async function cleanupCoreFiles(): Promise { + 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 { try { await startCore() @@ -226,13 +236,25 @@ export async function keepCoreAlive(): Promise { try { await startCore(true) if (child && child.pid) { - await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString()) + await writeCoreStateFile(child.pid) } } catch (e) { safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) } } +async function writeCoreStateFile(pid: number): Promise { + 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 { await keepCoreAlive() await startMonitor(true) @@ -460,7 +482,228 @@ export async function checkAdminRestartForTun(): Promise { } } else { // 检查TUN配置与权限的匹配 + await checkAndAlignPermissions() + } +} + +export async function checkAndAlignPermissions(): Promise { + 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() + } 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 { + if (process.platform !== 'win32') { + return true + } + + 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 } } diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 9227da5..44ebb65 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -51,6 +51,11 @@ export const patchMihomoConfig = async (patch: Partial): Promise< return await instance.patch('/configs', patch) } +export const mihomoRestart = async (): Promise => { + const instance = await getAxios() + return await instance.post('/restart', {}) +} + export const mihomoCloseConnection = async (id: string): Promise => { const instance = await getAxios() return await instance.delete(`/connections/${encodeURIComponent(id)}`)