import { ChildProcess, exec, execFile, spawn } from 'child_process' import { dataDir, coreLogPath, mihomoCoreDir, mihomoCorePath, mihomoProfileWorkDir, mihomoTestDir, mihomoWorkConfigPath, mihomoWorkDir } from '../utils/dirs' import { generateProfile } from './factory' import { getAppConfig, getControledMihomoConfig, getProfileConfig, patchAppConfig, patchControledMihomoConfig, manageSmartOverride } from '../config' import { app, ipcMain, net } from 'electron' import { startMihomoTraffic, startMihomoConnections, startMihomoLogs, startMihomoMemory, stopMihomoConnections, stopMihomoTraffic, stopMihomoLogs, stopMihomoMemory, patchMihomoConfig } from './mihomoApi' import chokidar from 'chokidar' import { readFile, rm, writeFile } from 'fs/promises' import { promisify } from 'util' import { mainWindow } from '..' import path from 'path' import os from 'os' import { createWriteStream, existsSync } from 'fs' import { uploadRuntimeConfig } from '../resolve/gistApi' import { startMonitor } from '../resolve/trafficMonitor' import { safeShowErrorBox } from '../utils/init' import i18next from '../../shared/i18n' import { managerLogger } from '../utils/logger' chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => { try { await stopCore(true) await startCore() } catch (e) { safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) } }) export const mihomoIpcPath = process.platform === 'win32' ? '\\\\.\\pipe\\MihomoParty\\mihomo' : '/tmp/mihomo-party.sock' const ctlParam = process.platform === 'win32' ? '-ext-ctl-pipe' : '-ext-ctl-unix' let setPublicDNSTimer: NodeJS.Timeout | null = null let recoverDNSTimer: NodeJS.Timeout | null = null let child: ChildProcess let retry = 10 export async function startCore(detached = false): Promise[]> { const { core = 'mihomo', autoSetDNS = true, diffWorkDir = false, mihomoCpuPriority = 'PRIORITY_NORMAL', disableLoopbackDetector = false, disableEmbedCA = false, disableSystemCA = false, 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')) } } const { current } = await getProfileConfig() const { tun } = await getControledMihomoConfig() const corePath = mihomoCorePath(core) // 管理 Smart 内核覆写配置 await manageSmartOverride() await generateProfile() await checkProfile() await stopCore() if (tun?.enable && autoSetDNS) { try { await setPublicDNS() } catch (error) { await managerLogger.error('set dns failed', error) } } // 内核日志输出到独立的 core-日期.log 文件 const stdout = createWriteStream(coreLogPath(), { flags: 'a' }) const stderr = createWriteStream(coreLogPath(), { flags: 'a' }) const env = { DISABLE_LOOPBACK_DETECTOR: String(disableLoopbackDetector), DISABLE_EMBED_CA: String(disableEmbedCA), DISABLE_SYSTEM_CA: String(disableSystemCA), SKIP_SAFE_PATH_CHECK: String(skipSafePathCheck) } child = spawn( corePath, ['-d', diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), ctlParam, mihomoIpcPath], { detached: detached, stdio: detached ? 'ignore' : undefined, env: env } ) if (process.platform === 'win32' && child.pid) { os.setPriority(child.pid, os.constants.priority[mihomoCpuPriority]) } if (detached) { child.unref() return new Promise((resolve) => { resolve([new Promise(() => {})]) }) } child.on('close', async (code, signal) => { await managerLogger.info(`Core closed, code: ${code}, signal: ${signal}`) if (retry) { await managerLogger.info('Try Restart Core') retry-- await restartCore() } else { await stopCore() } }) child.stdout?.pipe(stdout) child.stderr?.pipe(stderr) return new Promise((resolve, reject) => { child.stdout?.on('data', async (data) => { const str = data.toString() if (str.includes('configure tun interface: operation not permitted')) { patchControledMihomoConfig({ tun: { enable: false } }) mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') reject(i18next.t('tun.error.tunPermissionDenied')) } if ((process.platform !== 'win32' && str.includes('External controller unix listen error')) || (process.platform === 'win32' && str.includes('External controller pipe listen error')) ) { reject(i18next.t('mihomo.error.externalControllerListenError')) } if ( (process.platform !== 'win32' && str.includes('RESTful API unix listening at')) || (process.platform === 'win32' && str.includes('RESTful API pipe listening at')) ) { resolve([ new Promise((resolve) => { child.stdout?.on('data', async (data) => { if (data.toString().toLowerCase().includes('start initial compatible provider default')) { try { mainWindow?.webContents.send('groupsUpdated') mainWindow?.webContents.send('rulesUpdated') await uploadRuntimeConfig() } catch { // ignore } await patchMihomoConfig({ 'log-level': logLevel }) resolve() } }) }) ]) await startMihomoTraffic() await startMihomoConnections() await startMihomoLogs() await startMihomoMemory() retry = 10 } }) }) } export async function stopCore(force = false): Promise { try { if (!force) { await recoverDNS() } } catch (error) { await managerLogger.error('recover dns failed', error) } if (child) { child.removeAllListeners() child.kill('SIGINT') } stopMihomoTraffic() stopMihomoConnections() stopMihomoLogs() stopMihomoMemory() } export async function restartCore(): Promise { try { await startCore() } catch (e) { // 记录错误到日志而不是显示阻塞对话框 await managerLogger.error('restart core failed', e) // 重新抛出错误,让调用者处理 throw e } } export async function keepCoreAlive(): Promise { try { await startCore(true) if (child && child.pid) { await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString()) } } catch (e) { safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`) } } export async function quitWithoutCore(): Promise { await keepCoreAlive() await startMonitor(true) app.exit() } async function checkProfile(): Promise { const { core = 'mihomo', diffWorkDir = false, skipSafePathCheck = false } = await getAppConfig() const { current } = await getProfileConfig() const corePath = mihomoCorePath(core) const execFilePromise = promisify(execFile) const env = { SKIP_SAFE_PATH_CHECK: String(skipSafePathCheck) } try { await execFilePromise(corePath, [ '-t', '-f', diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work'), '-d', mihomoTestDir() ], { env }) } catch (error) { await managerLogger.error('Profile check failed', error) if (error instanceof Error && 'stdout' in error) { const { stdout, stderr } = error as { stdout: string; stderr?: string } await managerLogger.info('Profile check stdout', stdout) await managerLogger.info('Profile check stderr', stderr) const errorLines = stdout .split('\n') .filter((line) => line.includes('level=error') || line.includes('error')) .map((line) => { if (line.includes('level=error')) { return line.split('level=error')[1]?.trim() || line } return line.trim() }) .filter(line => line.length > 0) if (errorLines.length === 0) { const allLines = stdout.split('\n').filter(line => line.trim().length > 0) throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${allLines.join('\n')}`) } else { throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}:\n${errorLines.join('\n')}`) } } else { throw new Error(`${i18next.t('mihomo.error.profileCheckFailed')}: ${error}`) } } } export async function checkTunPermissions(): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) try { if (process.platform === 'win32') { const execPromise = promisify(exec) try { await execPromise('net session') return true } catch { return false } } if (process.platform === 'darwin' || process.platform === 'linux') { const { stat } = await import('fs/promises') const stats = await stat(corePath) return (stats.mode & 0o4000) !== 0 && stats.uid === 0 } } catch { return false } return false } export async function grantTunPermissions(): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) const execPromise = promisify(exec) const execFilePromise = promisify(execFile) if (process.platform === 'darwin') { const shell = `chown root:admin ${corePath.replace(' ', '\\\\ ')}\nchmod +sx ${corePath.replace(' ', '\\\\ ')}` const command = `do shell script "${shell}" with administrator privileges` await execPromise(`osascript -e '${command}'`) } if (process.platform === 'linux') { await execFilePromise('pkexec', [ 'bash', '-c', `chown root:root "${corePath}" && chmod +sx "${corePath}"` ]) } if (process.platform === 'win32') { throw new Error('Windows platform requires running as administrator') } } export async function checkAdminPrivileges(): Promise { if (process.platform !== 'win32') { return true } try { const execPromise = promisify(exec) await execPromise('net session') return true } catch { return false } } export async function restartAsAdmin(): Promise { if (process.platform !== 'win32') { throw new Error('This function is only available on Windows') } const exePath = process.execPath const args = process.argv.slice(1) const restartArgs = [...args, '--admin-restart-for-tun'] try { // 处理路径和参数的引号 const escapedExePath = exePath.replace(/'/g, "''") const argsString = restartArgs.map(arg => arg.replace(/'/g, "''")).join("', '") let command: string if (restartArgs.length > 0) { command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -ArgumentList '${argsString}' -Verb RunAs"` } else { command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"` } await managerLogger.info('Restarting as administrator with command', command) // 执行PowerShell命令 exec(command, { windowsHide: true }, async (error, _stdout, stderr) => { if (error) { await managerLogger.error('PowerShell execution error', error) await managerLogger.error('stderr', stderr) } else { await managerLogger.info('PowerShell command executed successfully') } }) await new Promise(resolve => setTimeout(resolve, 1500)) const { app } = await import('electron') app.quit() } catch (error) { await managerLogger.error('Failed to restart as administrator', error) throw new Error(`Failed to restart as administrator: ${error}`) } } export async function checkMihomoCorePermissions(): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) try { if (process.platform === 'win32') { // Windows权限检查 return await checkAdminPrivileges() } if (process.platform === 'darwin' || process.platform === 'linux') { const { stat } = await import('fs/promises') const stats = await stat(corePath) return (stats.mode & 0o4000) !== 0 && stats.uid === 0 } } catch { return false } return false } // 检测高权限内核 export async function checkHighPrivilegeCore(): Promise { try { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) await managerLogger.info(`Checking high privilege core: ${corePath}`) if (process.platform === 'win32') { const { existsSync } = await import('fs') if (!existsSync(corePath)) { await managerLogger.info('Core file does not exist') return false } const hasHighPrivilegeProcess = await checkHighPrivilegeMihomoProcess() if (hasHighPrivilegeProcess) { await managerLogger.info('Found high privilege mihomo process running') return true } const isAdmin = await checkAdminPrivileges() await managerLogger.info(`Current process admin privileges: ${isAdmin}`) return isAdmin } if (process.platform === 'darwin' || process.platform === 'linux') { const { stat, existsSync } = await import('fs') const { promisify } = await import('util') const statAsync = promisify(stat) if (!existsSync(corePath)) { await managerLogger.info('Core file does not exist') return false } const stats = await statAsync(corePath) const hasSetuid = (stats.mode & 0o4000) !== 0 const isOwnedByRoot = stats.uid === 0 await managerLogger.info(`Core file stats - setuid: ${hasSetuid}, owned by root: ${isOwnedByRoot}, mode: ${stats.mode.toString(8)}`) return hasSetuid && isOwnedByRoot } } catch (error) { await managerLogger.error('Failed to check high privilege core', error) return false } return false } async function checkHighPrivilegeMihomoProcess(): Promise { try { if (process.platform === 'win32') { const execPromise = promisify(exec) try { const { stdout } = await execPromise('tasklist /FI "IMAGENAME eq mihomo.exe" /FO CSV') const lines = stdout.split('\n').filter(line => line.includes('mihomo.exe')) if (lines.length > 0) { await managerLogger.info(`Found ${lines.length} mihomo processes running`) for (const line of lines) { const parts = line.split(',') if (parts.length >= 2) { const pid = parts[1].replace(/"/g, '').trim() try { const { stdout: processInfo } = await execPromise(`wmic process where "ProcessId=${pid}" get Name,ProcessId,ExecutablePath,CommandLine /format:csv`) await managerLogger.info(`Process ${pid} info: ${processInfo.substring(0, 200)}`) if (processInfo.includes('mihomo')) { return true } } catch (error) { await managerLogger.info(`Cannot get info for process ${pid}, might be high privilege`) } } } } } catch (error) { await managerLogger.error('Failed to check mihomo processes', error) } } if (process.platform === 'darwin' || process.platform === 'linux') { const execPromise = promisify(exec) try { const { stdout } = await execPromise('ps aux | grep mihomo | grep -v grep') const lines = stdout.split('\n').filter(line => line.trim() && line.includes('mihomo')) if (lines.length > 0) { await managerLogger.info(`Found ${lines.length} mihomo processes running`) for (const line of lines) { const parts = line.trim().split(/\s+/) if (parts.length >= 1) { const user = parts[0] await managerLogger.info(`Mihomo process running as user: ${user}`) if (user === 'root') { return true } } } } } catch (error) { await managerLogger.error('Failed to check mihomo processes on Unix', error) } } } catch (error) { await managerLogger.error('Failed to check high privilege mihomo process', error) } return false } // TUN模式获取权限 export async function requestTunPermissions(): Promise { if (process.platform === 'win32') { await restartAsAdmin() } else { const hasPermissions = await checkMihomoCorePermissions() if (!hasPermissions) { await grantTunPermissions() } } } export async function checkAdminRestartForTun(): Promise { if (process.argv.includes('--admin-restart-for-tun')) { await managerLogger.info('Detected admin restart for TUN mode, auto-enabling TUN...') try { if (process.platform === 'win32') { const hasAdminPrivileges = await checkAdminPrivileges() if (hasAdminPrivileges) { await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } }) await restartCore() await managerLogger.info('TUN mode auto-enabled after admin restart') const { mainWindow } = await import('../index') mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') } else { await managerLogger.warn('Admin restart detected but no admin privileges found') } } } catch (error) { await managerLogger.error('Failed to auto-enable TUN after admin restart', error) } } else { // 检查TUN配置与权限的匹配 await validateTunPermissionsOnStartup() } } export async function validateTunPermissionsOnStartup(): Promise { try { const { tun } = await getControledMihomoConfig() if (!tun?.enable) { return } const hasPermissions = await checkMihomoCorePermissions() if (!hasPermissions) { await managerLogger.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') await managerLogger.info('TUN auto-disabled due to insufficient permissions') } else { await managerLogger.info('TUN permissions validated successfully') } } catch (error) { await managerLogger.error('Failed to validate TUN permissions on startup', error) } } export async function manualGrantCorePermition(): Promise { return grantTunPermissions() } export async function getDefaultDevice(): Promise { const execPromise = promisify(exec) const { stdout: deviceOut } = await execPromise(`route -n get default`) let device = deviceOut.split('\n').find((s) => s.includes('interface:')) device = device?.trim().split(' ').slice(1).join(' ') if (!device) throw new Error('Get device failed') return device } async function getDefaultService(): Promise { const execPromise = promisify(exec) const device = await getDefaultDevice() const { stdout: order } = await execPromise(`networksetup -listnetworkserviceorder`) const block = order.split('\n\n').find((s) => s.includes(`Device: ${device}`)) if (!block) throw new Error('Get networkservice failed') for (const line of block.split('\n')) { if (line.match(/^\(\d+\).*/)) { return line.trim().split(' ').slice(1).join(' ') } } throw new Error('Get service failed') } async function getOriginDNS(): Promise { const execPromise = promisify(exec) const service = await getDefaultService() const { stdout: dns } = await execPromise(`networksetup -getdnsservers "${service}"`) if (dns.startsWith("There aren't any DNS Servers set on")) { await patchAppConfig({ originDNS: 'Empty' }) } else { await patchAppConfig({ originDNS: dns.trim().replace(/\n/g, ' ') }) } } async function setDNS(dns: string): Promise { const service = await getDefaultService() const execPromise = promisify(exec) await execPromise(`networksetup -setdnsservers "${service}" ${dns}`) } async function setPublicDNS(): Promise { if (process.platform !== 'darwin') return if (net.isOnline()) { const { originDNS } = await getAppConfig() if (!originDNS) { await getOriginDNS() await setDNS('223.5.5.5') } } else { if (setPublicDNSTimer) clearTimeout(setPublicDNSTimer) setPublicDNSTimer = setTimeout(() => setPublicDNS(), 5000) } } async function recoverDNS(): Promise { if (process.platform !== 'darwin') return if (net.isOnline()) { const { originDNS } = await getAppConfig() if (originDNS) { await setDNS(originDNS) await patchAppConfig({ originDNS: undefined }) } } else { if (recoverDNSTimer) clearTimeout(recoverDNSTimer) recoverDNSTimer = setTimeout(() => recoverDNS(), 5000) } }