From db605f24fcc333bb83ec30f5ad4a1de0c54c4f27 Mon Sep 17 00:00:00 2001 From: ezequielnick <107352853+ezequielnick@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:58:01 +0800 Subject: [PATCH] feat: enhance core checks with TUN startup detection and unified popup design --- src/main/core/manager.ts | 128 +++++++++++++----- src/main/index.ts | 3 +- src/main/utils/ipc.ts | 6 +- .../src/components/sider/tun-switcher.tsx | 6 +- src/renderer/src/locales/en-US.json | 8 +- src/renderer/src/locales/fa-IR.json | 2 + src/renderer/src/locales/ru-RU.json | 4 +- src/renderer/src/locales/zh-CN.json | 8 +- src/renderer/src/utils/ipc.ts | 8 ++ 9 files changed, 123 insertions(+), 50 deletions(-) diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 43398dd..50439c8 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -350,14 +350,59 @@ export async function checkAdminPrivileges(): Promise { } } -export async function restartAsAdmin(): Promise { +// TUN 权限确认框 +export async function showTunPermissionDialog(): Promise { + const { dialog } = await import('electron') + const i18next = await import('i18next') + + await managerLogger.info('Preparing TUN permission dialog...') + await managerLogger.info(`i18next available: ${typeof i18next.t === 'function'}`) + + const title = i18next.t('tun.permissions.title') || '需要管理员权限' + const message = i18next.t('tun.permissions.message') || '启用TUN模式需要管理员权限,是否现在重启应用获取权限?' + const confirmText = i18next.t('common.confirm') || '确认' + const cancelText = i18next.t('common.cancel') || '取消' + + await managerLogger.info(`Dialog texts - Title: "${title}", Message: "${message}", Confirm: "${confirmText}", Cancel: "${cancelText}"`) + + const choice = dialog.showMessageBoxSync({ + type: 'warning', + title: title, + message: message, + buttons: [confirmText, cancelText], + defaultId: 0, + cancelId: 1 + }) + + await managerLogger.info(`TUN permission dialog choice: ${choice}`) + + return choice === 0 +} + +// 错误显示框 +export async function showErrorDialog(title: string, message: string): Promise { + const { dialog } = await import('electron') + const i18next = await import('i18next') + + const okText = i18next.t('common.confirm') || '确认' + + dialog.showMessageBoxSync({ + type: 'error', + title: title, + message: message, + buttons: [okText], + defaultId: 0 + }) +} + +export async function restartAsAdmin(forTun: boolean = true): 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'] + const restartArgs = forTun ? [...args, '--admin-restart-for-tun'] : args try { // 处理路径和参数的引号 @@ -393,8 +438,6 @@ export async function restartAsAdmin(): Promise { } } - - export async function checkMihomoCorePermissions(): Promise { const { core = 'mihomo' } = await getAppConfig() const corePath = mihomoCorePath(core) @@ -474,32 +517,36 @@ async function checkHighPrivilegeMihomoProcess(): Promise { 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')) + const mihomoExecutables = ['mihomo.exe', 'mihomo-alpha.exe', 'mihomo-smart.exe'] - if (lines.length > 0) { - await managerLogger.info(`Found ${lines.length} mihomo processes running`) + for (const executable of mihomoExecutables) { + try { + const { stdout } = await execPromise(`tasklist /FI "IMAGENAME eq ${executable}" /FO CSV`) + const lines = stdout.split('\n').filter(line => line.includes(executable)) - 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 (lines.length > 0) { + await managerLogger.info(`Found ${lines.length} ${executable} processes running`) - if (processInfo.includes('mihomo')) { - return true + 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.info(`Cannot get info for process ${pid}, might be high privilege`) } } } + } catch (error) { + await managerLogger.error(`Failed to check ${executable} processes`, error) } - } catch (error) { - await managerLogger.error('Failed to check mihomo processes', error) } } @@ -507,24 +554,37 @@ async function checkHighPrivilegeMihomoProcess(): Promise { 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')) + const mihomoExecutables = ['mihomo', 'mihomo-alpha', 'mihomo-smart'] + let foundProcesses = false - if (lines.length > 0) { - await managerLogger.info(`Found ${lines.length} mihomo processes running`) + for (const executable of mihomoExecutables) { + try { + const { stdout } = await execPromise(`ps aux | grep ${executable} | grep -v grep`) + const lines = stdout.split('\n').filter(line => line.trim() && line.includes(executable)) - 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 (lines.length > 0) { + foundProcesses = true + await managerLogger.info(`Found ${lines.length} ${executable} processes running`) - if (user === 'root') { - return true + for (const line of lines) { + const parts = line.trim().split(/\s+/) + if (parts.length >= 1) { + const user = parts[0] + await managerLogger.info(`${executable} process running as user: ${user}`) + + if (user === 'root') { + return true + } + } } } + } catch (error) { } } + + if (!foundProcesses) { + await managerLogger.info('No mihomo processes found running') + } } catch (error) { await managerLogger.error('Failed to check mihomo processes on Unix', error) } @@ -572,7 +632,7 @@ export async function checkAdminRestartForTun(): Promise { await managerLogger.error('Failed to auto-enable TUN after admin restart', error) } } else { - // 检查TUN配置与权限的匹配 + // 检查TUN配置与权限的匹配,但不自动开启 TUN await validateTunPermissionsOnStartup() } } diff --git a/src/main/index.ts b/src/main/index.ts index 080a55a..c0a5b06 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -155,7 +155,8 @@ async function checkHighPrivilegeCoreEarly(): Promise { if (choice === 0) { try { - await restartAsAdmin() + // 非TUN重启 + await restartAsAdmin(false) process.exit(0) } catch (error) { showSafeErrorBox('common.error.adminRequired', `${error}`) diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 2fdee17..2441498 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -66,7 +66,9 @@ import { restartAsAdmin, checkMihomoCorePermissions, requestTunPermissions, - checkHighPrivilegeCore + checkHighPrivilegeCore, + showTunPermissionDialog, + showErrorDialog } from '../core/manager' import { triggerSysProxy } from '../sys/sysproxy' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' @@ -202,6 +204,8 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('checkMihomoCorePermissions', () => ipcErrorWrapper(checkMihomoCorePermissions)()) ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)()) ipcMain.handle('checkHighPrivilegeCore', () => ipcErrorWrapper(checkHighPrivilegeCore)()) + ipcMain.handle('showTunPermissionDialog', () => ipcErrorWrapper(showTunPermissionDialog)()) + ipcMain.handle('showErrorDialog', (_, title: string, message: string) => ipcErrorWrapper(showErrorDialog)(title, message)) ipcMain.handle('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)()) ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)()) diff --git a/src/renderer/src/components/sider/tun-switcher.tsx b/src/renderer/src/components/sider/tun-switcher.tsx index 7312758..1259dc9 100644 --- a/src/renderer/src/components/sider/tun-switcher.tsx +++ b/src/renderer/src/components/sider/tun-switcher.tsx @@ -44,7 +44,7 @@ const TunSwitcher: React.FC = (props) => { if (!hasPermissions) { if (window.electron.process.platform === 'win32') { - const confirmed = confirm(t('tun.permissions.required')) + const confirmed = await window.electron.ipcRenderer.invoke('showTunPermissionDialog') if (confirmed) { try { const notification = new Notification(t('tun.permissions.restarting')) @@ -53,7 +53,7 @@ const TunSwitcher: React.FC = (props) => { return } catch (error) { console.error('Failed to restart as admin:', error) - alert(t('tun.permissions.failed') + ': ' + error) + await window.electron.ipcRenderer.invoke('showErrorDialog', t('tun.permissions.failed'), String(error)) return } } else { @@ -65,7 +65,7 @@ const TunSwitcher: React.FC = (props) => { await window.electron.ipcRenderer.invoke('requestTunPermissions') } catch (error) { console.warn('Permission grant failed:', error) - alert(t('tun.permissions.failed') + ': ' + error) + await window.electron.ipcRenderer.invoke('showErrorDialog', t('tun.permissions.failed'), String(error)) return } } diff --git a/src/renderer/src/locales/en-US.json b/src/renderer/src/locales/en-US.json index ed016a3..b4ad798 100644 --- a/src/renderer/src/locales/en-US.json +++ b/src/renderer/src/locales/en-US.json @@ -37,7 +37,7 @@ "common.error.adminRequired": "Please run with administrator privileges for first launch", "common.error.initFailed": "Application initialization failed", "core.highPrivilege.title": "High Privilege Core Detected", - "core.highPrivilege.message": "A high privilege core (administrator privileges or setuid bit) has been detected. For security reasons, it is recommended to restart the application with administrator privileges. Restart now?", + "core.highPrivilege.message": "A high-privilege core is detected. The application needs to restart in administrator mode to match permissions. Restart now?", "common.updater.versionReady": "v{{version}} Version Ready", "common.updater.goToDownload": "Go to Download", "common.updater.update": "Update", @@ -319,11 +319,9 @@ "tun.excludeAddress.placeholder": "Example: 172.20.0.0/16", "tun.notifications.coreAuthSuccess": "Core Authorization Successful", "tun.notifications.firewallResetSuccess": "Firewall Reset Successful", - "tun.error.tunPermissionDenied": "TUN interface start failed, please try to manually grant core permissions", - "tun.permissions.required": "TUN mode requires administrator privileges. Restart the application now to get permissions?", + "tun.permissions.title": "Administrator Privileges Required", + "tun.permissions.message": "TUN mode requires administrator privileges. Restart the application now to get permissions?", "tun.permissions.failed": "Permission authorization failed", - "tun.permissions.windowsRestart": "On Windows, you need to restart the application as administrator to use TUN mode", - "tun.permissions.requesting": "Requesting administrator privileges, please click 'Yes' in the UAC dialog...", "tun.permissions.restarting": "Restarting application with administrator privileges, please click 'Yes' in the UAC dialog...", "dns.title": "DNS Settings", "dns.enable": "Enable DNS", diff --git a/src/renderer/src/locales/fa-IR.json b/src/renderer/src/locales/fa-IR.json index 3ccee90..d1b942d 100644 --- a/src/renderer/src/locales/fa-IR.json +++ b/src/renderer/src/locales/fa-IR.json @@ -36,6 +36,8 @@ "common.error.shortcutRegistrationFailedWithError": "ثبت میانبر با خطا مواجه شد: {{error}}", "common.error.adminRequired": "لطفا برای اولین اجرا با دسترسی مدیر برنامه را اجرا کنید", "common.error.initFailed": "راه‌اندازی برنامه با خطا مواجه شد", + "core.highPrivilege.title": "هسته با سطح دسترسی بالا شناسایی شد", + "core.highPrivilege.message": "هسته‌ای با سطح دسترسی بالا شناسایی شد. برنامه باید در حالت مدیر سیستم برای تطابق سطح دسترسی‌ها دوباره راه‌اندازی شود. آیا می‌خواهید اکنون راه‌اندازی مجدد شود؟", "common.updater.versionReady": "نسخه v{{version}} آماده است", "common.updater.goToDownload": "دانلود", "common.updater.update": "به‌روزرسانی", diff --git a/src/renderer/src/locales/ru-RU.json b/src/renderer/src/locales/ru-RU.json index 84bb8bb..eb0c221 100644 --- a/src/renderer/src/locales/ru-RU.json +++ b/src/renderer/src/locales/ru-RU.json @@ -36,7 +36,9 @@ "common.error.shortcutRegistrationFailedWithError": "Не удалось зарегистрировать сочетание клавиш: {{error}}", "common.error.adminRequired": "Для первого запуска требуются права администратора", "common.error.initFailed": "Не удалось инициализировать приложение", - "common.updater.versionReady": "Версия v{{version}} готова", + "core.highPrivilege.title": "High Privilege Core Detected", + "core.highPrivilege.message": "Обнаружено ядро с повышенными привилегиями. Приложение необходимо перезапустить в режиме администратора для согласования прав. Перезапустить сейчас?", + "common.updater.versionReady": "Обнаружено ядро с повышенными привилегиями", "common.updater.goToDownload": "Перейти к загрузке", "common.updater.update": "Обновить", "settings.general": "Общие настройки", diff --git a/src/renderer/src/locales/zh-CN.json b/src/renderer/src/locales/zh-CN.json index 2a101d0..b014d5b 100644 --- a/src/renderer/src/locales/zh-CN.json +++ b/src/renderer/src/locales/zh-CN.json @@ -37,7 +37,7 @@ "common.error.adminRequired": "首次启动请以管理员权限运行", "common.error.initFailed": "应用初始化失败", "core.highPrivilege.title": "检测到高权限内核", - "core.highPrivilege.message": "检测到当前内核具有高权限(管理员权限或 setuid 位),为了安全起见,建议以管理员权限重启应用。是否现在重启?", + "core.highPrivilege.message": "检测到运行中的高权限内核,需以管理员模式重启应用以匹配权限,确定重启?", "common.updater.versionReady": "v{{version}} 版本就绪", "common.updater.goToDownload": "前往下载", "common.updater.update": "更新", @@ -319,11 +319,9 @@ "tun.excludeAddress.placeholder": "例: 172.20.0.0/16", "tun.notifications.coreAuthSuccess": "内核授权成功", "tun.notifications.firewallResetSuccess": "防火墙重设成功", - "tun.error.tunPermissionDenied": "虚拟网卡启动失败,请尝试手动授予内核权限", - "tun.permissions.required": "启用TUN模式需要管理员权限,是否现在重启应用获取权限?", + "tun.permissions.title": "需要管理员权限", + "tun.permissions.message": "启用TUN模式需要管理员权限,是否现在重启应用获取权限?", "tun.permissions.failed": "权限授权失败", - "tun.permissions.windowsRestart": "Windows下需要以管理员身份重新启动应用才能使用TUN模式", - "tun.permissions.requesting": "正在请求管理员权限,请在UAC对话框中点击'是'...", "tun.permissions.restarting": "正在以管理员权限重启应用,请在UAC对话框中点击'是'...", "dns.title": "DNS 设置", "dns.enable": "启用 DNS", diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 316ecae..fa081ab 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -253,6 +253,14 @@ export async function restartAsAdmin(): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('restartAsAdmin')) } +export async function showTunPermissionDialog(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showTunPermissionDialog')) +} + +export async function showErrorDialog(title: string, message: string): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showErrorDialog', title, message)) +} + export async function getFilePath(ext: string[]): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext)) }