feat: enhance core checks with TUN startup detection and unified popup design

This commit is contained in:
ezequielnick 2025-08-12 20:58:01 +08:00
parent f005a4f4cd
commit db605f24fc
9 changed files with 123 additions and 50 deletions

View File

@ -350,14 +350,59 @@ export async function checkAdminPrivileges(): Promise<boolean> {
} }
} }
export async function restartAsAdmin(): Promise<void> { // TUN 权限确认框
export async function showTunPermissionDialog(): Promise<boolean> {
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<void> {
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<void> {
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
throw new Error('This function is only available on Windows') throw new Error('This function is only available on Windows')
} }
const exePath = process.execPath const exePath = process.execPath
const args = process.argv.slice(1) const args = process.argv.slice(1)
const restartArgs = [...args, '--admin-restart-for-tun'] const restartArgs = forTun ? [...args, '--admin-restart-for-tun'] : args
try { try {
// 处理路径和参数的引号 // 处理路径和参数的引号
@ -393,8 +438,6 @@ export async function restartAsAdmin(): Promise<void> {
} }
} }
export async function checkMihomoCorePermissions(): Promise<boolean> { export async function checkMihomoCorePermissions(): Promise<boolean> {
const { core = 'mihomo' } = await getAppConfig() const { core = 'mihomo' } = await getAppConfig()
const corePath = mihomoCorePath(core) const corePath = mihomoCorePath(core)
@ -474,32 +517,36 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
if (process.platform === 'win32') { if (process.platform === 'win32') {
const execPromise = promisify(exec) const execPromise = promisify(exec)
try { const mihomoExecutables = ['mihomo.exe', 'mihomo-alpha.exe', 'mihomo-smart.exe']
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) { for (const executable of mihomoExecutables) {
await managerLogger.info(`Found ${lines.length} mihomo processes running`) 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) { if (lines.length > 0) {
const parts = line.split(',') await managerLogger.info(`Found ${lines.length} ${executable} processes running`)
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')) { for (const line of lines) {
return true 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<boolean> {
const execPromise = promisify(exec) const execPromise = promisify(exec)
try { try {
const { stdout } = await execPromise('ps aux | grep mihomo | grep -v grep') const mihomoExecutables = ['mihomo', 'mihomo-alpha', 'mihomo-smart']
const lines = stdout.split('\n').filter(line => line.trim() && line.includes('mihomo')) let foundProcesses = false
if (lines.length > 0) { for (const executable of mihomoExecutables) {
await managerLogger.info(`Found ${lines.length} mihomo processes running`) 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) { if (lines.length > 0) {
const parts = line.trim().split(/\s+/) foundProcesses = true
if (parts.length >= 1) { await managerLogger.info(`Found ${lines.length} ${executable} processes running`)
const user = parts[0]
await managerLogger.info(`Mihomo process running as user: ${user}`)
if (user === 'root') { for (const line of lines) {
return true 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) { } catch (error) {
await managerLogger.error('Failed to check mihomo processes on Unix', error) await managerLogger.error('Failed to check mihomo processes on Unix', error)
} }
@ -572,7 +632,7 @@ export async function checkAdminRestartForTun(): Promise<void> {
await managerLogger.error('Failed to auto-enable TUN after admin restart', error) await managerLogger.error('Failed to auto-enable TUN after admin restart', error)
} }
} else { } else {
// 检查TUN配置与权限的匹配 // 检查TUN配置与权限的匹配,但不自动开启 TUN
await validateTunPermissionsOnStartup() await validateTunPermissionsOnStartup()
} }
} }

View File

@ -155,7 +155,8 @@ async function checkHighPrivilegeCoreEarly(): Promise<void> {
if (choice === 0) { if (choice === 0) {
try { try {
await restartAsAdmin() // 非TUN重启
await restartAsAdmin(false)
process.exit(0) process.exit(0)
} catch (error) { } catch (error) {
showSafeErrorBox('common.error.adminRequired', `${error}`) showSafeErrorBox('common.error.adminRequired', `${error}`)

View File

@ -66,7 +66,9 @@ import {
restartAsAdmin, restartAsAdmin,
checkMihomoCorePermissions, checkMihomoCorePermissions,
requestTunPermissions, requestTunPermissions,
checkHighPrivilegeCore checkHighPrivilegeCore,
showTunPermissionDialog,
showErrorDialog
} from '../core/manager' } from '../core/manager'
import { triggerSysProxy } from '../sys/sysproxy' import { triggerSysProxy } from '../sys/sysproxy'
import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater' import { checkUpdate, downloadAndInstallUpdate } from '../resolve/autoUpdater'
@ -202,6 +204,8 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('checkMihomoCorePermissions', () => ipcErrorWrapper(checkMihomoCorePermissions)()) ipcMain.handle('checkMihomoCorePermissions', () => ipcErrorWrapper(checkMihomoCorePermissions)())
ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)()) ipcMain.handle('requestTunPermissions', () => ipcErrorWrapper(requestTunPermissions)())
ipcMain.handle('checkHighPrivilegeCore', () => ipcErrorWrapper(checkHighPrivilegeCore)()) 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('checkTunPermissions', () => ipcErrorWrapper(checkTunPermissions)())
ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)()) ipcMain.handle('grantTunPermissions', () => ipcErrorWrapper(grantTunPermissions)())

View File

@ -44,7 +44,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
if (!hasPermissions) { if (!hasPermissions) {
if (window.electron.process.platform === 'win32') { if (window.electron.process.platform === 'win32') {
const confirmed = confirm(t('tun.permissions.required')) const confirmed = await window.electron.ipcRenderer.invoke('showTunPermissionDialog')
if (confirmed) { if (confirmed) {
try { try {
const notification = new Notification(t('tun.permissions.restarting')) const notification = new Notification(t('tun.permissions.restarting'))
@ -53,7 +53,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
return return
} catch (error) { } catch (error) {
console.error('Failed to restart as admin:', 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 return
} }
} else { } else {
@ -65,7 +65,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
await window.electron.ipcRenderer.invoke('requestTunPermissions') await window.electron.ipcRenderer.invoke('requestTunPermissions')
} catch (error) { } catch (error) {
console.warn('Permission grant failed:', 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 return
} }
} }

View File

@ -37,7 +37,7 @@
"common.error.adminRequired": "Please run with administrator privileges for first launch", "common.error.adminRequired": "Please run with administrator privileges for first launch",
"common.error.initFailed": "Application initialization failed", "common.error.initFailed": "Application initialization failed",
"core.highPrivilege.title": "High Privilege Core Detected", "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.versionReady": "v{{version}} Version Ready",
"common.updater.goToDownload": "Go to Download", "common.updater.goToDownload": "Go to Download",
"common.updater.update": "Update", "common.updater.update": "Update",
@ -319,11 +319,9 @@
"tun.excludeAddress.placeholder": "Example: 172.20.0.0/16", "tun.excludeAddress.placeholder": "Example: 172.20.0.0/16",
"tun.notifications.coreAuthSuccess": "Core Authorization Successful", "tun.notifications.coreAuthSuccess": "Core Authorization Successful",
"tun.notifications.firewallResetSuccess": "Firewall Reset Successful", "tun.notifications.firewallResetSuccess": "Firewall Reset Successful",
"tun.error.tunPermissionDenied": "TUN interface start failed, please try to manually grant core permissions", "tun.permissions.title": "Administrator Privileges Required",
"tun.permissions.required": "TUN mode requires administrator privileges. Restart the application now to get permissions?", "tun.permissions.message": "TUN mode requires administrator privileges. Restart the application now to get permissions?",
"tun.permissions.failed": "Permission authorization failed", "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...", "tun.permissions.restarting": "Restarting application with administrator privileges, please click 'Yes' in the UAC dialog...",
"dns.title": "DNS Settings", "dns.title": "DNS Settings",
"dns.enable": "Enable DNS", "dns.enable": "Enable DNS",

View File

@ -36,6 +36,8 @@
"common.error.shortcutRegistrationFailedWithError": "ثبت میانبر با خطا مواجه شد: {{error}}", "common.error.shortcutRegistrationFailedWithError": "ثبت میانبر با خطا مواجه شد: {{error}}",
"common.error.adminRequired": "لطفا برای اولین اجرا با دسترسی مدیر برنامه را اجرا کنید", "common.error.adminRequired": "لطفا برای اولین اجرا با دسترسی مدیر برنامه را اجرا کنید",
"common.error.initFailed": "راه‌اندازی برنامه با خطا مواجه شد", "common.error.initFailed": "راه‌اندازی برنامه با خطا مواجه شد",
"core.highPrivilege.title": "هسته با سطح دسترسی بالا شناسایی شد",
"core.highPrivilege.message": "هسته‌ای با سطح دسترسی بالا شناسایی شد. برنامه باید در حالت مدیر سیستم برای تطابق سطح دسترسی‌ها دوباره راه‌اندازی شود. آیا می‌خواهید اکنون راه‌اندازی مجدد شود؟",
"common.updater.versionReady": "نسخه v{{version}} آماده است", "common.updater.versionReady": "نسخه v{{version}} آماده است",
"common.updater.goToDownload": "دانلود", "common.updater.goToDownload": "دانلود",
"common.updater.update": "به‌روزرسانی", "common.updater.update": "به‌روزرسانی",

View File

@ -36,7 +36,9 @@
"common.error.shortcutRegistrationFailedWithError": "Не удалось зарегистрировать сочетание клавиш: {{error}}", "common.error.shortcutRegistrationFailedWithError": "Не удалось зарегистрировать сочетание клавиш: {{error}}",
"common.error.adminRequired": "Для первого запуска требуются права администратора", "common.error.adminRequired": "Для первого запуска требуются права администратора",
"common.error.initFailed": "Не удалось инициализировать приложение", "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.goToDownload": "Перейти к загрузке",
"common.updater.update": "Обновить", "common.updater.update": "Обновить",
"settings.general": "Общие настройки", "settings.general": "Общие настройки",

View File

@ -37,7 +37,7 @@
"common.error.adminRequired": "首次启动请以管理员权限运行", "common.error.adminRequired": "首次启动请以管理员权限运行",
"common.error.initFailed": "应用初始化失败", "common.error.initFailed": "应用初始化失败",
"core.highPrivilege.title": "检测到高权限内核", "core.highPrivilege.title": "检测到高权限内核",
"core.highPrivilege.message": "检测到当前内核具有高权限(管理员权限或 setuid 位),为了安全起见,建议以管理员权限重启应用。是否现在重启?", "core.highPrivilege.message": "检测到运行中的高权限内核,需以管理员模式重启应用以匹配权限,确定重启?",
"common.updater.versionReady": "v{{version}} 版本就绪", "common.updater.versionReady": "v{{version}} 版本就绪",
"common.updater.goToDownload": "前往下载", "common.updater.goToDownload": "前往下载",
"common.updater.update": "更新", "common.updater.update": "更新",
@ -319,11 +319,9 @@
"tun.excludeAddress.placeholder": "例: 172.20.0.0/16", "tun.excludeAddress.placeholder": "例: 172.20.0.0/16",
"tun.notifications.coreAuthSuccess": "内核授权成功", "tun.notifications.coreAuthSuccess": "内核授权成功",
"tun.notifications.firewallResetSuccess": "防火墙重设成功", "tun.notifications.firewallResetSuccess": "防火墙重设成功",
"tun.error.tunPermissionDenied": "虚拟网卡启动失败,请尝试手动授予内核权限", "tun.permissions.title": "需要管理员权限",
"tun.permissions.required": "启用TUN模式需要管理员权限是否现在重启应用获取权限", "tun.permissions.message": "启用TUN模式需要管理员权限是否现在重启应用获取权限",
"tun.permissions.failed": "权限授权失败", "tun.permissions.failed": "权限授权失败",
"tun.permissions.windowsRestart": "Windows下需要以管理员身份重新启动应用才能使用TUN模式",
"tun.permissions.requesting": "正在请求管理员权限请在UAC对话框中点击'是'...",
"tun.permissions.restarting": "正在以管理员权限重启应用请在UAC对话框中点击'是'...", "tun.permissions.restarting": "正在以管理员权限重启应用请在UAC对话框中点击'是'...",
"dns.title": "DNS 设置", "dns.title": "DNS 设置",
"dns.enable": "启用 DNS", "dns.enable": "启用 DNS",

View File

@ -253,6 +253,14 @@ export async function restartAsAdmin(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('restartAsAdmin')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('restartAsAdmin'))
} }
export async function showTunPermissionDialog(): Promise<boolean> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showTunPermissionDialog'))
}
export async function showErrorDialog(title: string, message: string): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('showErrorDialog', title, message))
}
export async function getFilePath(ext: string[]): Promise<string[] | undefined> { export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext)) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFilePath', ext))
} }