import { triggerAutoProxy, triggerManualProxy } from '@mihomo-party/sysproxy' import { getAppConfig, getControledMihomoConfig } from '../config' import { pacPort, startPacServer, stopPacServer } from '../resolve/server' import { promisify } from 'util' import { execFile } from 'child_process' import path from 'path' import { resourcesFilesDir } from '../utils/dirs' import { net } from 'electron' import axios from 'axios' import fs from 'fs' let defaultBypass: string[] let triggerSysProxyTimer: NodeJS.Timeout | null = null const helperSocketPath = '/tmp/mihomo-party-helper.sock' if (process.platform === 'linux') defaultBypass = ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1'] if (process.platform === 'darwin') defaultBypass = [ '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', 'localhost', '*.local', '*.crashlytics.com', '' ] if (process.platform === 'win32') defaultBypass = [ 'localhost', '127.*', '192.168.*', '10.*', '172.16.*', '172.17.*', '172.18.*', '172.19.*', '172.20.*', '172.21.*', '172.22.*', '172.23.*', '172.24.*', '172.25.*', '172.26.*', '172.27.*', '172.28.*', '172.29.*', '172.30.*', '172.31.*', '' ] export async function triggerSysProxy(enable: boolean): Promise { if (net.isOnline()) { if (enable) { await disableSysProxy() await enableSysProxy() } else { await disableSysProxy() } } else { if (triggerSysProxyTimer) clearTimeout(triggerSysProxyTimer) triggerSysProxyTimer = setTimeout(() => triggerSysProxy(enable), 5000) } } async function enableSysProxy(): Promise { await startPacServer() const { sysProxy } = await getAppConfig() const { mode, host, bypass = defaultBypass } = sysProxy const { 'mixed-port': port = 7890 } = await getControledMihomoConfig() const execFilePromise = promisify(execFile) switch (mode || 'manual') { case 'auto': { if (process.platform === 'win32') { try { await execFilePromise(path.join(resourcesFilesDir(), 'sysproxy.exe'), [ 'pac', `http://${host || '127.0.0.1'}:${pacPort}/pac` ]) } catch { triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`) } } else if (process.platform === 'darwin') { await helperRequest(() => axios.post( 'http://localhost/pac', { url: `http://${host || '127.0.0.1'}:${pacPort}/pac` }, { socketPath: helperSocketPath } ) ) } else { triggerAutoProxy(true, `http://${host || '127.0.0.1'}:${pacPort}/pac`) } break } case 'manual': { if (process.platform === 'win32') { try { await execFilePromise(path.join(resourcesFilesDir(), 'sysproxy.exe'), [ 'global', `${host || '127.0.0.1'}:${port}`, bypass.join(';') ]) } catch { triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(',')) } } else if (process.platform === 'darwin') { await helperRequest(() => axios.post( 'http://localhost/global', { host: host || '127.0.0.1', port: port.toString(), bypass: bypass.join(',') }, { socketPath: helperSocketPath } ) ) } else { triggerManualProxy(true, host || '127.0.0.1', port, bypass.join(',')) } break } } } async function disableSysProxy(): Promise { await stopPacServer() const execFilePromise = promisify(execFile) if (process.platform === 'win32') { try { await execFilePromise(path.join(resourcesFilesDir(), 'sysproxy.exe'), ['set', '1']) } catch { triggerAutoProxy(false, '') triggerManualProxy(false, '', 0, '') } } else if (process.platform === 'darwin') { await helperRequest(() => axios.get('http://localhost/off', { socketPath: helperSocketPath }) ) } else { triggerAutoProxy(false, '') triggerManualProxy(false, '', 0, '') } } // Helper function to check if socket file exists function isSocketFileExists(): boolean { try { return fs.existsSync(helperSocketPath) } catch { return false } } // Helper function to send signal to recreate socket async function requestSocketRecreation(): Promise { try { // Send SIGUSR1 signal to helper process to recreate socket const { exec } = require('child_process') const { promisify } = require('util') const execPromise = promisify(exec) // Use osascript with administrator privileges (same pattern as grantTunPermissions) const shell = `pkill -USR1 -f party.mihomo.helper` const command = `do shell script "${shell}" with administrator privileges` await execPromise(`osascript -e '${command}'`) // Wait a bit for socket recreation await new Promise(resolve => setTimeout(resolve, 1000)) } catch (error) { console.log('Failed to send signal to helper:', error) throw error } } // Wrapper function for helper requests with auto-retry on socket issues async function helperRequest(requestFn: () => Promise, maxRetries = 1): Promise { let lastError: Error | null = null for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await requestFn() } catch (error) { lastError = error as Error // Check if it's a connection error and socket file doesn't exist if (attempt < maxRetries && ((error as NodeJS.ErrnoException).code === 'ECONNREFUSED' || (error as NodeJS.ErrnoException).code === 'ENOENT' || (error as Error).message?.includes('connect ECONNREFUSED') || (error as Error).message?.includes('ENOENT'))) { console.log(`Helper request failed (attempt ${attempt + 1}), checking socket file...`) if (!isSocketFileExists()) { console.log('Socket file missing, requesting recreation...') try { await requestSocketRecreation() console.log('Socket recreation requested, retrying...') continue } catch (signalError) { console.log('Failed to request socket recreation:', signalError) } } } // If not a connection error or we've exhausted retries, throw the error if (attempt === maxRetries) { throw lastError } } } throw lastError }