import { ChildProcess, exec, execFile, spawn } from 'child_process' import { logPath, mihomoCoreDir, mihomoCorePath, mihomoTestDir, mihomoWorkConfigPath, mihomoWorkDir } from '../utils/dirs' import { generateProfile } from './factory' import { getAppConfig, getControledMihomoConfig, patchAppConfig, patchControledMihomoConfig } from '../config' import { dialog, ipcMain, safeStorage } from 'electron' import { startMihomoTraffic, startMihomoConnections, startMihomoLogs, startMihomoMemory, stopMihomoConnections, stopMihomoTraffic, stopMihomoLogs, stopMihomoMemory } from './mihomoApi' import chokidar from 'chokidar' import { writeFile } from 'fs/promises' import { promisify } from 'util' import { mainWindow } from '..' import path from 'path' chokidar.watch(path.join(mihomoCoreDir(), 'meta-update')).on('unlinkDir', async () => { try { await stopCore(true) await startCore() } catch (e) { dialog.showErrorBox('内核启动出错', `${e}`) } }) let child: ChildProcess let retry = 10 export async function startCore(): Promise[]> { const { core = 'mihomo', autoSetDNS = true } = await getAppConfig() const { tun } = await getControledMihomoConfig() const corePath = mihomoCorePath(core) await autoGrantCorePermition(corePath) await generateProfile() await checkProfile() await stopCore() if (tun?.enable && autoSetDNS) { try { await setPublicDNS() } catch (error) { await writeFile(logPath(), `[Manager]: set dns failed, ${error}`, { flag: 'a' }) } } child = spawn(corePath, ['-d', mihomoWorkDir()]) child.on('close', async (code, signal) => { await writeFile(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, { flag: 'a' }) if (retry) { await writeFile(logPath(), `[Manager]: Try Restart Core\n`, { flag: 'a' }) retry-- await restartCore() } else { await stopCore() } }) child.stdout?.on('data', async (data) => { await writeFile(logPath(), data, { flag: 'a' }) }) return new Promise((resolve, reject) => { child.stdout?.on('data', async (data) => { if (data.toString().includes('configure tun interface: operation not permitted')) { patchControledMihomoConfig({ tun: { enable: false } }) mainWindow?.webContents.send('controledMihomoConfigUpdated') ipcMain.emit('updateTrayMenu') reject('虚拟网卡启动失败, 请尝试手动授予内核权限') } if (data.toString().includes('External controller listen error')) { if (retry) { retry-- try { resolve(await startCore()) } catch (e) { reject(e) } } else { reject('内核连接失败, 请尝试修改外部控制端口或重启电脑') } } if (data.toString().includes('RESTful API listening at')) { resolve([ new Promise((resolve) => { child.stdout?.on('data', async (data) => { if (data.toString().includes('Start initial Compatible provider default')) { mainWindow?.webContents.send('coreRestart') 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 writeFile(logPath(), `[Manager]: recover dns failed, ${error}`, { flag: 'a' }) } if (child) { child.removeAllListeners() child.kill('SIGINT') } stopMihomoTraffic() stopMihomoConnections() stopMihomoLogs() stopMihomoMemory() } export async function restartCore(): Promise { try { await startCore() } catch (e) { dialog.showErrorBox('内核启动出错', `${e}`) } } async function checkProfile(): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) const execFilePromise = promisify(execFile) try { await execFilePromise(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()]) } catch (error) { if (error instanceof Error && 'stdout' in error) { const { stdout } = error as { stdout: string } const errorLines = stdout .split('\n') .filter((line) => line.includes('level=error')) .map((line) => line.split('level=error')[1]) throw new Error(`Profile Check Failed:\n${errorLines.join('\n')}`) } else { throw error } } } export async function autoGrantCorePermition(corePath: string): Promise { if (process.platform === 'win32') return const { encryptedPassword } = await getAppConfig() const execPromise = promisify(exec) if (encryptedPassword && isEncryptionAvailable()) { const password = safeStorage.decryptString(Buffer.from(encryptedPassword)) if (process.platform === 'linux') { try { await execPromise(`echo "${password}" | sudo -S chown root:root "${corePath}"`) await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`) } catch (error) { patchAppConfig({ encryptedPassword: undefined }) throw error } } if (process.platform === 'darwin') { try { await execPromise(`echo "${password}" | sudo -S chown root:admin "${corePath}"`) await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`) } catch (error) { patchAppConfig({ encryptedPassword: undefined }) throw error } } } } export async function manualGrantCorePermition(password?: string): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) const execPromise = promisify(exec) 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 execPromise(`echo "${password}" | sudo -S chown root:root "${corePath}"`) await execPromise(`echo "${password}" | sudo -S chmod +sx "${corePath}"`) } } export function isEncryptionAvailable(): boolean { return safeStorage.isEncryptionAvailable() } async function getDefaultService(password?: string): Promise { const execPromise = promisify(exec) let sudo = '' if (password) sudo = `echo "${password}" | sudo -S ` const { stdout: deviceOut } = await execPromise(`${sudo}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') const { stdout: order } = await execPromise(`${sudo}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(password?: string): Promise { const execPromise = promisify(exec) let sudo = '' if (password) sudo = `echo "${password}" | sudo -S ` const service = await getDefaultService(password) const { stdout: dns } = await execPromise(`${sudo}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, password?: string): Promise { const service = await getDefaultService(password) let sudo = '' if (password) sudo = `echo "${password}" | sudo -S ` const execPromise = promisify(exec) await execPromise(`${sudo}networksetup -setdnsservers "${service}" ${dns}`) } async function setPublicDNS(): Promise { if (process.platform !== 'darwin') return const { originDNS, encryptedPassword } = await getAppConfig() if (!originDNS) { let password: string | undefined if (encryptedPassword && isEncryptionAvailable()) { password = safeStorage.decryptString(Buffer.from(encryptedPassword)) } await getOriginDNS(password) await setDNS('223.5.5.5', password) } } async function recoverDNS(): Promise { if (process.platform !== 'darwin') return const { originDNS, encryptedPassword } = await getAppConfig() if (originDNS) { let password: string | undefined if (encryptedPassword && isEncryptionAvailable()) { password = safeStorage.decryptString(Buffer.from(encryptedPassword)) } await setDNS(originDNS, password) await patchAppConfig({ originDNS: undefined }) } }