mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 21:20:29 +08:00
Compare commits
3 Commits
0dad7a6d8b
...
bb58b60c21
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb58b60c21 | ||
|
|
cb3eedfcb8 | ||
|
|
d030a8722d |
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
### 新功能 (Feat)
|
### 新功能 (Feat)
|
||||||
- 如果当前没有以管理员模式运行,TUN 开关保持关闭
|
- 如果当前没有以管理员模式运行,TUN 开关保持关闭
|
||||||
|
- 区分 app 日志与 core 日志输出为不同文件
|
||||||
|
|
||||||
### 修复 (Fix)
|
### 修复 (Fix)
|
||||||
- 修复某些系统下的悬浮窗开启崩溃的问题
|
- 修复某些系统下的悬浮窗开启崩溃的问题
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { getAppConfig } from './app'
|
import { getAppConfig } from './app'
|
||||||
import { addOverrideItem, removeOverrideItem, getOverrideItem } from './override'
|
import { addOverrideItem, removeOverrideItem, getOverrideItem } from './override'
|
||||||
|
import { overrideLogger } from '../utils/logger'
|
||||||
|
|
||||||
const SMART_OVERRIDE_ID = 'smart-core-override'
|
const SMART_OVERRIDE_ID = 'smart-core-override'
|
||||||
|
|
||||||
@ -237,7 +238,7 @@ export async function createSmartOverride(): Promise<void> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create Smart override:', error)
|
await overrideLogger.error('Failed to create Smart override', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -252,7 +253,7 @@ export async function removeSmartOverride(): Promise<void> {
|
|||||||
await removeOverrideItem(SMART_OVERRIDE_ID)
|
await removeOverrideItem(SMART_OVERRIDE_ID)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to remove Smart override:', error)
|
await overrideLogger.error('Failed to remove Smart override', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { ChildProcess, exec, execFile, spawn } from 'child_process'
|
import { ChildProcess, exec, execFile, spawn } from 'child_process'
|
||||||
import {
|
import {
|
||||||
dataDir,
|
dataDir,
|
||||||
logPath,
|
coreLogPath,
|
||||||
mihomoCoreDir,
|
mihomoCoreDir,
|
||||||
mihomoCorePath,
|
mihomoCorePath,
|
||||||
mihomoProfileWorkDir,
|
mihomoProfileWorkDir,
|
||||||
@ -41,6 +41,7 @@ import { uploadRuntimeConfig } from '../resolve/gistApi'
|
|||||||
import { startMonitor } from '../resolve/trafficMonitor'
|
import { startMonitor } from '../resolve/trafficMonitor'
|
||||||
import { safeShowErrorBox } from '../utils/init'
|
import { safeShowErrorBox } from '../utils/init'
|
||||||
import i18next from '../../shared/i18n'
|
import i18next from '../../shared/i18n'
|
||||||
|
import { managerLogger } from '../utils/logger'
|
||||||
|
|
||||||
chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => {
|
chokidar.watch(path.join(mihomoCoreDir(), 'meta-update'), {}).on('unlinkDir', async () => {
|
||||||
try {
|
try {
|
||||||
@ -72,14 +73,23 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
skipSafePathCheck = false
|
skipSafePathCheck = false
|
||||||
} = await getAppConfig()
|
} = await getAppConfig()
|
||||||
const { 'log-level': logLevel } = await getControledMihomoConfig()
|
const { 'log-level': logLevel } = await getControledMihomoConfig()
|
||||||
|
if (existsSync(path.join(dataDir(), 'core.pid'))) {
|
||||||
await handleExistingCore()
|
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 { current } = await getProfileConfig()
|
||||||
const { tun } = await getControledMihomoConfig()
|
const { tun } = await getControledMihomoConfig()
|
||||||
const corePath = mihomoCorePath(core)
|
const corePath = mihomoCorePath(core)
|
||||||
|
|
||||||
|
// 管理 Smart 内核覆写配置
|
||||||
await manageSmartOverride()
|
await manageSmartOverride()
|
||||||
|
|
||||||
await generateProfile()
|
await generateProfile()
|
||||||
await checkProfile()
|
await checkProfile()
|
||||||
await stopCore()
|
await stopCore()
|
||||||
@ -87,13 +97,12 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
try {
|
try {
|
||||||
await setPublicDNS()
|
await setPublicDNS()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await writeFile(logPath(), `[Manager]: set dns failed, ${error}`, {
|
await managerLogger.error('set dns failed', error)
|
||||||
flag: 'a'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const stdout = createWriteStream(logPath(), { flags: 'a' })
|
// 内核日志输出到独立的 core-日期.log 文件
|
||||||
const stderr = createWriteStream(logPath(), { flags: 'a' })
|
const stdout = createWriteStream(coreLogPath(), { flags: 'a' })
|
||||||
|
const stderr = createWriteStream(coreLogPath(), { flags: 'a' })
|
||||||
const env = {
|
const env = {
|
||||||
DISABLE_LOOPBACK_DETECTOR: String(disableLoopbackDetector),
|
DISABLE_LOOPBACK_DETECTOR: String(disableLoopbackDetector),
|
||||||
DISABLE_EMBED_CA: String(disableEmbedCA),
|
DISABLE_EMBED_CA: String(disableEmbedCA),
|
||||||
@ -119,11 +128,9 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
child.on('close', async (code, signal) => {
|
child.on('close', async (code, signal) => {
|
||||||
await writeFile(logPath(), `[Manager]: Core closed, code: ${code}, signal: ${signal}\n`, {
|
await managerLogger.info(`Core closed, code: ${code}, signal: ${signal}`)
|
||||||
flag: 'a'
|
|
||||||
})
|
|
||||||
if (retry) {
|
if (retry) {
|
||||||
await writeFile(logPath(), `[Manager]: Try Restart Core\n`, { flag: 'a' })
|
await managerLogger.info('Try Restart Core')
|
||||||
retry--
|
retry--
|
||||||
await restartCore()
|
await restartCore()
|
||||||
} else {
|
} else {
|
||||||
@ -145,14 +152,6 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
|||||||
if ((process.platform !== 'win32' && str.includes('External controller unix listen error')) ||
|
if ((process.platform !== 'win32' && str.includes('External controller unix listen error')) ||
|
||||||
(process.platform === 'win32' && str.includes('External controller pipe listen error'))
|
(process.platform === 'win32' && str.includes('External controller pipe listen error'))
|
||||||
) {
|
) {
|
||||||
if (str.includes('permission denied') || str.includes('access denied') ||
|
|
||||||
str.includes('operation not permitted') || str.includes('Access is denied')) {
|
|
||||||
checkForElevatedCore().catch((error) => {
|
|
||||||
if (error instanceof Error && error.message !== 'Application restart required for permission alignment') {
|
|
||||||
console.error('Failed to check for elevated core:', error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
reject(i18next.t('mihomo.error.externalControllerListenError'))
|
reject(i18next.t('mihomo.error.externalControllerListenError'))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,38 +192,26 @@ export async function stopCore(force = false): Promise<void> {
|
|||||||
await recoverDNS()
|
await recoverDNS()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await writeFile(logPath(), `[Manager]: recover dns failed, ${error}`, {
|
await managerLogger.error('recover dns failed', error)
|
||||||
flag: 'a'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
child.removeAllListeners()
|
child.removeAllListeners()
|
||||||
child.kill('SIGINT')
|
child.kill('SIGINT')
|
||||||
}
|
}
|
||||||
|
|
||||||
await cleanupCoreFiles()
|
|
||||||
stopMihomoTraffic()
|
stopMihomoTraffic()
|
||||||
stopMihomoConnections()
|
stopMihomoConnections()
|
||||||
stopMihomoLogs()
|
stopMihomoLogs()
|
||||||
stopMihomoMemory()
|
stopMihomoMemory()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cleanupCoreFiles(): Promise<void> {
|
|
||||||
const pidFile = path.join(dataDir(), 'core.pid')
|
|
||||||
const stateFile = path.join(dataDir(), 'core.state')
|
|
||||||
|
|
||||||
await rm(pidFile).catch(() => {})
|
|
||||||
await rm(stateFile).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function restartCore(): Promise<void> {
|
export async function restartCore(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await startCore()
|
await startCore()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await writeFile(logPath(), `[Manager]: restart core failed, ${e}\n`, {
|
// 记录错误到日志而不是显示阻塞对话框
|
||||||
flag: 'a'
|
await managerLogger.error('restart core failed', e)
|
||||||
})
|
// 重新抛出错误,让调用者处理
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,25 +220,13 @@ export async function keepCoreAlive(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
await startCore(true)
|
await startCore(true)
|
||||||
if (child && child.pid) {
|
if (child && child.pid) {
|
||||||
await writeCoreStateFile(child.pid)
|
await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString())
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`)
|
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeCoreStateFile(pid: number): Promise<void> {
|
|
||||||
const isAdmin = await checkAdminPrivileges()
|
|
||||||
const stateData = {
|
|
||||||
pid,
|
|
||||||
isAdmin,
|
|
||||||
startTime: Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
await writeFile(path.join(dataDir(), 'core.pid'), pid.toString())
|
|
||||||
await writeFile(path.join(dataDir(), 'core.state'), JSON.stringify(stateData))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function quitWithoutCore(): Promise<void> {
|
export async function quitWithoutCore(): Promise<void> {
|
||||||
await keepCoreAlive()
|
await keepCoreAlive()
|
||||||
await startMonitor(true)
|
await startMonitor(true)
|
||||||
@ -279,8 +254,13 @@ async function checkProfile(): Promise<void> {
|
|||||||
mihomoTestDir()
|
mihomoTestDir()
|
||||||
], { env })
|
], { env })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
await managerLogger.error('Profile check failed', error)
|
||||||
|
|
||||||
if (error instanceof Error && 'stdout' in error) {
|
if (error instanceof Error && 'stdout' in error) {
|
||||||
const { stdout } = error as { stdout: string; stderr?: string }
|
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
|
const errorLines = stdout
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter((line) => line.includes('level=error') || line.includes('error'))
|
.filter((line) => line.includes('level=error') || line.includes('error'))
|
||||||
@ -370,17 +350,17 @@ export async function checkAdminPrivileges(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function restartAsAdmin(): Promise<void> {
|
export async function restartAsAdmin(addTunFlag = 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 cleanArgs = args.filter(arg => !arg.startsWith('--admin-restart'))
|
const restartArgs = addTunFlag ? [...args, '--admin-restart-for-tun'] : args.filter(arg => arg !== '--admin-restart-for-tun')
|
||||||
const restartArgs = [...cleanArgs, '--admin-restart-for-permission']
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 处理路径和参数的引号
|
||||||
const escapedExePath = exePath.replace(/'/g, "''")
|
const escapedExePath = exePath.replace(/'/g, "''")
|
||||||
const argsString = restartArgs.map(arg => arg.replace(/'/g, "''")).join("', '")
|
const argsString = restartArgs.map(arg => arg.replace(/'/g, "''")).join("', '")
|
||||||
|
|
||||||
@ -391,10 +371,18 @@ export async function restartAsAdmin(): Promise<void> {
|
|||||||
command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
|
command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
exec(command, { windowsHide: true }, (error, _stdout, stderr) => {
|
const logMessage = addTunFlag
|
||||||
|
? 'Restarting as administrator for TUN mode with command'
|
||||||
|
: 'Restarting as normal user from high privilege detection with command'
|
||||||
|
await managerLogger.info(logMessage, command)
|
||||||
|
|
||||||
|
// 执行PowerShell命令
|
||||||
|
exec(command, { windowsHide: true }, async (error, _stdout, stderr) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('PowerShell execution error:', error)
|
await managerLogger.error('PowerShell execution error', error)
|
||||||
console.error('stderr:', stderr)
|
await managerLogger.error('stderr', stderr)
|
||||||
|
} else {
|
||||||
|
await managerLogger.info('PowerShell command executed successfully')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -403,7 +391,7 @@ export async function restartAsAdmin(): Promise<void> {
|
|||||||
const { app } = await import('electron')
|
const { app } = await import('electron')
|
||||||
app.quit()
|
app.quit()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to restart as administrator:', error)
|
await managerLogger.error('Failed to restart as administrator', error)
|
||||||
throw new Error(`Failed to restart as administrator: ${error}`)
|
throw new Error(`Failed to restart as administrator: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -416,6 +404,7 @@ export async function checkMihomoCorePermissions(): Promise<boolean> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
|
// Windows权限检查
|
||||||
return await checkAdminPrivileges()
|
return await checkAdminPrivileges()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,6 +420,7 @@ export async function checkMihomoCorePermissions(): Promise<boolean> {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TUN模式获取权限
|
||||||
export async function requestTunPermissions(): Promise<void> {
|
export async function requestTunPermissions(): Promise<void> {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
await restartAsAdmin()
|
await restartAsAdmin()
|
||||||
@ -444,6 +434,8 @@ export async function requestTunPermissions(): Promise<void> {
|
|||||||
|
|
||||||
export async function checkAdminRestartForTun(): Promise<void> {
|
export async function checkAdminRestartForTun(): Promise<void> {
|
||||||
if (process.argv.includes('--admin-restart-for-tun')) {
|
if (process.argv.includes('--admin-restart-for-tun')) {
|
||||||
|
await managerLogger.info('Detected admin restart for TUN mode, auto-enabling TUN...')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const hasAdminPrivileges = await checkAdminPrivileges()
|
const hasAdminPrivileges = await checkAdminPrivileges()
|
||||||
@ -451,234 +443,67 @@ export async function checkAdminRestartForTun(): Promise<void> {
|
|||||||
await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } })
|
await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } })
|
||||||
await restartCore()
|
await restartCore()
|
||||||
|
|
||||||
|
await managerLogger.info('TUN mode auto-enabled after admin restart')
|
||||||
|
|
||||||
const { mainWindow } = await import('../index')
|
const { mainWindow } = await import('../index')
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
|
} else {
|
||||||
|
await managerLogger.warn('Admin restart detected but no admin privileges found')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to auto-enable TUN after admin restart:', error)
|
await managerLogger.error('Failed to auto-enable TUN after admin restart', error)
|
||||||
}
|
}
|
||||||
} else if (process.argv.includes('--admin-restart-for-permission')) {
|
|
||||||
await checkAndAlignPermissions()
|
|
||||||
} else {
|
} else {
|
||||||
await checkAndAlignPermissions()
|
await checkHighPrivilegeCore()
|
||||||
}
|
// 检查TUN配置与权限的匹配
|
||||||
}
|
|
||||||
|
|
||||||
export async function checkAndAlignPermissions(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const runningCoreInfo = await getRunningCoreInfo()
|
|
||||||
const currentUserIsAdmin = await checkAdminPrivileges()
|
|
||||||
|
|
||||||
if (runningCoreInfo && !currentUserIsAdmin && runningCoreInfo.isAdmin) {
|
|
||||||
const { dialog } = await import('electron')
|
|
||||||
const result = await dialog.showMessageBox({
|
|
||||||
type: 'warning',
|
|
||||||
title: '权限不匹配',
|
|
||||||
message: '检测到系统中有高权限内核正在运行',
|
|
||||||
detail: '需要以管理员模式重新启动本应用程序。点击确定将自动重启为管理员模式。',
|
|
||||||
buttons: ['确定', '取消'],
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.response === 0) {
|
|
||||||
await restartAsAdmin()
|
|
||||||
} else {
|
|
||||||
const { app } = await import('electron')
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Application restart required for permission alignment')
|
|
||||||
}
|
|
||||||
|
|
||||||
await validateTunPermissionsOnStartup()
|
await validateTunPermissionsOnStartup()
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message === 'Application restart required for permission alignment') {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
console.error('Failed to check permissions:', error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleExistingCore(): Promise<void> {
|
export async function checkHighPrivilegeCore(): Promise<void> {
|
||||||
const pidFile = path.join(dataDir(), 'core.pid')
|
try {
|
||||||
|
// 在 Win 平台检查
|
||||||
if (!existsSync(pidFile)) {
|
if (process.platform !== 'win32') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const hasAdminPrivileges = await checkAdminPrivileges()
|
||||||
const pidStr = await readFile(pidFile, 'utf-8')
|
if (hasAdminPrivileges) {
|
||||||
const pid = parseInt(pidStr.trim())
|
await managerLogger.info('Detected high privilege core startup, showing dialog...')
|
||||||
|
|
||||||
if (isProcessRunning(pid)) {
|
const { dialog, app } = await import('electron')
|
||||||
try {
|
const { t } = await import('i18next')
|
||||||
process.kill(pid, 'SIGINT')
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to kill existing process:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await cleanupCoreFiles()
|
// 对话框
|
||||||
} catch (error) {
|
const result = await dialog.showMessageBox({
|
||||||
throw error
|
type: 'warning',
|
||||||
}
|
title: t('tun.permissions.highPrivilegeDetected.title'),
|
||||||
}
|
message: t('tun.permissions.highPrivilegeDetected.message'),
|
||||||
|
detail: t('tun.permissions.highPrivilegeDetected.detail'),
|
||||||
|
buttons: [
|
||||||
|
t('tun.permissions.highPrivilegeDetected.restart'),
|
||||||
|
t('tun.permissions.highPrivilegeDetected.exit')
|
||||||
|
],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
noLink: true
|
||||||
|
})
|
||||||
|
|
||||||
async function getRunningCoreInfo(): Promise<{ pid: number; isAdmin: boolean } | null> {
|
if (result.response === 0) {
|
||||||
const stateFile = path.join(dataDir(), 'core.state')
|
await managerLogger.info('User confirmed restart as normal user')
|
||||||
const pidFile = path.join(dataDir(), 'core.pid')
|
await restartAsAdmin(false)
|
||||||
|
|
||||||
if (existsSync(stateFile)) {
|
|
||||||
try {
|
|
||||||
const stateData = JSON.parse(await readFile(stateFile, 'utf-8'))
|
|
||||||
const { pid, isAdmin } = stateData
|
|
||||||
|
|
||||||
if (isProcessRunning(pid)) {
|
|
||||||
return { pid, isAdmin }
|
|
||||||
} else {
|
} else {
|
||||||
await rm(stateFile).catch(() => {})
|
await managerLogger.info('User cancelled, exiting application')
|
||||||
await rm(pidFile).catch(() => {})
|
app.quit()
|
||||||
return null
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to read state file:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existsSync(pidFile)) {
|
|
||||||
try {
|
|
||||||
const pidStr = await readFile(pidFile, 'utf-8')
|
|
||||||
const pid = parseInt(pidStr.trim())
|
|
||||||
|
|
||||||
if (!isProcessRunning(pid)) {
|
|
||||||
await rm(pidFile)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin = await isProcessRunningAsAdmin(pid)
|
|
||||||
return { pid, isAdmin }
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await findMihomoCoreProcess()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function findMihomoCoreProcess(): Promise<{ pid: number; isAdmin: boolean } | null> {
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const execPromise = promisify(exec)
|
|
||||||
const { stdout } = await execPromise('tasklist /fi "imagename eq mihomo*" /fo csv')
|
|
||||||
|
|
||||||
const lines = stdout.split('\n').filter(line => line.trim() && !line.includes('INFO:'))
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.includes('mihomo')) {
|
|
||||||
const parts = line.split(',').map(part => part.replace(/"/g, '').trim())
|
|
||||||
if (parts.length >= 2) {
|
|
||||||
const processName = parts[0]
|
|
||||||
const pidStr = parts[1]
|
|
||||||
const pid = parseInt(pidStr)
|
|
||||||
|
|
||||||
if (!isNaN(pid) && processName.toLowerCase().includes('mihomo')) {
|
|
||||||
const isAdmin = await checkProcessElevation(pid)
|
|
||||||
return { pid, isAdmin }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to search for mihomo processes:', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkProcessElevation(pid: number): Promise<boolean> {
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const execPromise = promisify(exec)
|
|
||||||
const currentIsAdmin = await checkAdminPrivileges()
|
|
||||||
|
|
||||||
if (currentIsAdmin) {
|
|
||||||
try {
|
|
||||||
const { stdout } = await execPromise(`wmic process where "ProcessId=${pid}" get ExecutablePath,ProcessId /format:list`)
|
|
||||||
return stdout.includes('ExecutablePath=')
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const { stdout } = await execPromise(`tasklist /fi "PID eq ${pid}" /fo csv /v`)
|
|
||||||
const lines = stdout.split('\n')
|
|
||||||
|
|
||||||
if (lines.length > 1) {
|
|
||||||
const processLine = lines[1]
|
|
||||||
if (processLine.includes('NT AUTHORITY') || processLine.includes('SYSTEM')) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
} catch {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to check process elevation:', error)
|
await managerLogger.error('Failed to check high privilege core', error)
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProcessRunning(pid: number): boolean {
|
|
||||||
try {
|
|
||||||
process.kill(pid, 0)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function isProcessRunningAsAdmin(pid: number): Promise<boolean> {
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stateFile = path.join(dataDir(), 'core.state')
|
|
||||||
if (existsSync(stateFile)) {
|
|
||||||
const stateData = JSON.parse(await readFile(stateFile, 'utf-8'))
|
|
||||||
if (stateData.pid === pid && typeof stateData.isAdmin === 'boolean') {
|
|
||||||
return stateData.isAdmin
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
|
|
||||||
return await checkAdminPrivileges()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function validateTunPermissionsOnStartup(): Promise<void> {
|
export async function validateTunPermissionsOnStartup(): Promise<void> {
|
||||||
@ -692,52 +517,20 @@ export async function validateTunPermissionsOnStartup(): Promise<void> {
|
|||||||
const hasPermissions = await checkMihomoCorePermissions()
|
const hasPermissions = await checkMihomoCorePermissions()
|
||||||
|
|
||||||
if (!hasPermissions) {
|
if (!hasPermissions) {
|
||||||
|
await managerLogger.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...')
|
||||||
|
|
||||||
await patchControledMihomoConfig({ tun: { enable: false } })
|
await patchControledMihomoConfig({ tun: { enable: false } })
|
||||||
|
|
||||||
const { mainWindow } = await import('../index')
|
const { mainWindow } = await import('../index')
|
||||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to validate TUN permissions on startup:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkForElevatedCore(): Promise<void> {
|
await managerLogger.info('TUN auto-disabled due to insufficient permissions')
|
||||||
try {
|
|
||||||
const runningCoreInfo = await getRunningCoreInfo()
|
|
||||||
if (!runningCoreInfo) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentUserIsAdmin = await checkAdminPrivileges()
|
|
||||||
|
|
||||||
if (!currentUserIsAdmin && runningCoreInfo.isAdmin) {
|
|
||||||
const { dialog } = await import('electron')
|
|
||||||
const result = await dialog.showMessageBox({
|
|
||||||
type: 'warning',
|
|
||||||
title: '权限不匹配',
|
|
||||||
message: '检测到系统中有高权限内核正在运行',
|
|
||||||
detail: '需要以管理员模式重新启动本应用程序。点击确定将自动重启为管理员模式。',
|
|
||||||
buttons: ['确定', '取消'],
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.response === 0) {
|
|
||||||
await restartAsAdmin()
|
|
||||||
} else {
|
} else {
|
||||||
const { app } = await import('electron')
|
await managerLogger.info('TUN permissions validated successfully')
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Application restart required for permission alignment')
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message === 'Application restart required for permission alignment') {
|
await managerLogger.error('Failed to validate TUN permissions on startup', error)
|
||||||
throw error
|
|
||||||
}
|
|
||||||
console.error('Failed to check for elevated core:', error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,11 +51,6 @@ export const patchMihomoConfig = async (patch: Partial<IMihomoConfig>): Promise<
|
|||||||
return await instance.patch('/configs', patch)
|
return await instance.patch('/configs', patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mihomoRestart = async (): Promise<void> => {
|
|
||||||
const instance = await getAxios()
|
|
||||||
return await instance.post('/restart', {})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mihomoCloseConnection = async (id: string): Promise<void> => {
|
export const mihomoCloseConnection = async (id: string): Promise<void> => {
|
||||||
const instance = await getAxios()
|
const instance = await getAxios()
|
||||||
return await instance.delete(`/connections/${encodeURIComponent(id)}`)
|
return await instance.delete(`/connections/${encodeURIComponent(id)}`)
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { startMonitor } from './resolve/trafficMonitor'
|
|||||||
import { showFloatingWindow } from './resolve/floatingWindow'
|
import { showFloatingWindow } from './resolve/floatingWindow'
|
||||||
import { initI18n } from '../shared/i18n'
|
import { initI18n } from '../shared/i18n'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
|
import { logger } from './utils/logger'
|
||||||
|
|
||||||
// 错误处理
|
// 错误处理
|
||||||
function showSafeErrorBox(titleKey: string, message: string): void {
|
function showSafeErrorBox(titleKey: string, message: string): void {
|
||||||
@ -169,24 +170,16 @@ app.whenReady().then(async () => {
|
|||||||
showSafeErrorBox('common.error.initFailed', `${e}`)
|
showSafeErrorBox('common.error.initFailed', `${e}`)
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
// 权限检查
|
|
||||||
try {
|
try {
|
||||||
console.log('Starting permission check before GUI creation...')
|
const [startPromise] = await startCore()
|
||||||
|
startPromise.then(async () => {
|
||||||
|
await initProfileUpdater()
|
||||||
|
// 上次是否为了开启 TUN 而重启
|
||||||
await checkAdminRestartForTun()
|
await checkAdminRestartForTun()
|
||||||
console.log('Permission check passed, continuing with app startup...')
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Permission check failed:', e)
|
showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`)
|
||||||
|
|
||||||
if (e instanceof Error && e.message === 'Application restart required for permission alignment') {
|
|
||||||
console.log('Application is restarting for permission alignment, exiting...')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showSafeErrorBox('common.error.initFailed', `${e}`)
|
|
||||||
app.quit()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await startMonitor()
|
await startMonitor()
|
||||||
} catch {
|
} catch {
|
||||||
@ -202,29 +195,11 @@ app.whenReady().then(async () => {
|
|||||||
const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig()
|
const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig()
|
||||||
registerIpcMainHandlers()
|
registerIpcMainHandlers()
|
||||||
await createWindow()
|
await createWindow()
|
||||||
|
|
||||||
// 异步启动内核
|
|
||||||
const startCoreAsync = async () => {
|
|
||||||
try {
|
|
||||||
const [startPromise] = await startCore()
|
|
||||||
startPromise.then(async () => {
|
|
||||||
await initProfileUpdater()
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Core start failed:', e)
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`)
|
|
||||||
}, 3000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startCoreAsync()
|
|
||||||
if (showFloating) {
|
if (showFloating) {
|
||||||
try {
|
try {
|
||||||
await showFloatingWindow()
|
await showFloatingWindow()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create floating window on startup:', error)
|
await logger.error('Failed to create floating window on startup', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!disableTray) {
|
if (!disableTray) {
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
subStoreDir,
|
subStoreDir,
|
||||||
themesDir
|
themesDir
|
||||||
} from '../utils/dirs'
|
} from '../utils/dirs'
|
||||||
|
import { systemLogger } from '../utils/logger'
|
||||||
|
|
||||||
export async function webdavBackup(): Promise<boolean> {
|
export async function webdavBackup(): Promise<boolean> {
|
||||||
const { createClient } = await import('webdav/dist/node/index.js')
|
const { createClient } = await import('webdav/dist/node/index.js')
|
||||||
@ -75,7 +76,7 @@ export async function webdavBackup(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to clean up old backup files:', error)
|
await systemLogger.error('Failed to clean up old backup files', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,54 +5,23 @@ import { join } from 'path'
|
|||||||
import { getAppConfig, patchAppConfig } from '../config'
|
import { getAppConfig, patchAppConfig } from '../config'
|
||||||
import { applyTheme } from './theme'
|
import { applyTheme } from './theme'
|
||||||
import { buildContextMenu, showTrayIcon } from './tray'
|
import { buildContextMenu, showTrayIcon } from './tray'
|
||||||
import { writeFile } from 'fs/promises'
|
import { floatingWindowLogger } from '../utils/logger'
|
||||||
import { logDir } from '../utils/dirs'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
export let floatingWindow: BrowserWindow | null = null
|
export let floatingWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
// 悬浮窗日志记录
|
function logError(message: string, error?: any): void {
|
||||||
async function logFloatingWindow(message: string, error?: any): Promise<void> {
|
floatingWindowLogger.log(`FloatingWindow Error: ${message}`, error).catch(() => {})
|
||||||
try {
|
|
||||||
const timestamp = new Date().toISOString()
|
|
||||||
const logMessage = error
|
|
||||||
? `[${timestamp}] [FloatingWindow] ${message}: ${error}\n`
|
|
||||||
: `[${timestamp}] [FloatingWindow] ${message}\n`
|
|
||||||
|
|
||||||
const logPath = path.join(logDir(), 'floating-window.log')
|
|
||||||
await writeFile(logPath, logMessage, { flag: 'a' })
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.error(`[FloatingWindow] ${message}:`, error)
|
|
||||||
} else {
|
|
||||||
console.log(`[FloatingWindow] ${message}`)
|
|
||||||
}
|
|
||||||
} catch (logError) {
|
|
||||||
|
|
||||||
console.error('[FloatingWindow] Failed to write log:', logError)
|
|
||||||
console.log(`[FloatingWindow] Original message: ${message}`, error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createFloatingWindow(): Promise<void> {
|
async function createFloatingWindow(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await logFloatingWindow('Starting to create floating window...')
|
const floatingWindowState = windowStateKeeper({ file: 'floating-window-state.json' })
|
||||||
const floatingWindowState = windowStateKeeper({
|
|
||||||
file: 'floating-window-state.json'
|
|
||||||
})
|
|
||||||
await logFloatingWindow('Window state keeper initialized')
|
|
||||||
const { customTheme = 'default.css', floatingWindowCompatMode = true } = await getAppConfig()
|
const { customTheme = 'default.css', floatingWindowCompatMode = true } = await getAppConfig()
|
||||||
await logFloatingWindow(`App config loaded, theme: ${customTheme}, compatMode: ${floatingWindowCompatMode}`)
|
|
||||||
|
|
||||||
const safeMode = process.env.FLOATING_SAFE_MODE === 'true'
|
const safeMode = process.env.FLOATING_SAFE_MODE === 'true'
|
||||||
const forceWin10Mode = process.env.FLOATING_WIN10_MODE === 'true'
|
const useCompatMode = floatingWindowCompatMode ||
|
||||||
const useCompatMode = floatingWindowCompatMode || forceWin10Mode || safeMode
|
process.env.FLOATING_COMPAT_MODE === 'true' ||
|
||||||
|
safeMode
|
||||||
await logFloatingWindow(`Safe mode: ${safeMode}`)
|
|
||||||
await logFloatingWindow(`Force Win10 mode: ${forceWin10Mode}`)
|
|
||||||
await logFloatingWindow(`Compat mode from config: ${floatingWindowCompatMode}`)
|
|
||||||
await logFloatingWindow(`Platform: ${process.platform}, System version: ${process.getSystemVersion()}`)
|
|
||||||
await logFloatingWindow(`Using compatibility mode: ${useCompatMode}`)
|
|
||||||
|
|
||||||
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
const windowOptions: Electron.BrowserWindowConstructorOptions = {
|
||||||
width: 120,
|
width: 120,
|
||||||
@ -60,16 +29,16 @@ async function createFloatingWindow(): Promise<void> {
|
|||||||
x: floatingWindowState.x,
|
x: floatingWindowState.x,
|
||||||
y: floatingWindowState.y,
|
y: floatingWindowState.y,
|
||||||
show: false,
|
show: false,
|
||||||
frame: safeMode ? true : false,
|
frame: safeMode,
|
||||||
alwaysOnTop: !safeMode,
|
alwaysOnTop: !safeMode,
|
||||||
resizable: safeMode,
|
resizable: safeMode,
|
||||||
transparent: !safeMode && !useCompatMode, // 兼容模式下禁用透明
|
transparent: !safeMode && !useCompatMode,
|
||||||
skipTaskbar: !safeMode,
|
skipTaskbar: !safeMode,
|
||||||
minimizable: safeMode,
|
minimizable: safeMode,
|
||||||
maximizable: safeMode,
|
maximizable: safeMode,
|
||||||
fullscreenable: false,
|
fullscreenable: false,
|
||||||
closable: safeMode,
|
closable: safeMode,
|
||||||
backgroundColor: safeMode ? '#ffffff' : (useCompatMode ? '#f0f0f0' : '#00000000'), // 兼容模式使用浅灰色
|
backgroundColor: safeMode ? '#ffffff' : (useCompatMode ? '#f0f0f0' : '#00000000'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
spellcheck: false,
|
spellcheck: false,
|
||||||
@ -79,91 +48,46 @@ async function createFloatingWindow(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// windows 添加兼容性处理
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
windowOptions.hasShadow = !safeMode
|
windowOptions.hasShadow = !safeMode
|
||||||
windowOptions.webPreferences!.offscreen = false
|
windowOptions.webPreferences!.offscreen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
await logFloatingWindow(`Creating BrowserWindow with options: ${JSON.stringify(windowOptions, null, 2)}`)
|
|
||||||
|
|
||||||
try {
|
|
||||||
floatingWindow = new BrowserWindow(windowOptions)
|
floatingWindow = new BrowserWindow(windowOptions)
|
||||||
await logFloatingWindow('BrowserWindow created successfully')
|
|
||||||
} catch (error) {
|
|
||||||
await logFloatingWindow('Failed to create BrowserWindow', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await logFloatingWindow('Attaching window state management...')
|
|
||||||
floatingWindowState.manage(floatingWindow)
|
floatingWindowState.manage(floatingWindow)
|
||||||
await logFloatingWindow('Window state management attached')
|
|
||||||
} catch (error) {
|
|
||||||
await logFloatingWindow('Failed to attach window state management', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
await logFloatingWindow('Setting up event listeners...')
|
// 事件监听器
|
||||||
|
floatingWindow.webContents.on('render-process-gone', (_, details) => {
|
||||||
try {
|
logError('Render process gone', details.reason)
|
||||||
await logFloatingWindow('Adding render-process-gone listener...')
|
|
||||||
floatingWindow.webContents.on('render-process-gone', async (_, details) => {
|
|
||||||
await logFloatingWindow('Render process gone', details.reason)
|
|
||||||
floatingWindow = null
|
floatingWindow = null
|
||||||
})
|
})
|
||||||
await logFloatingWindow('Render-process-gone listener added')
|
|
||||||
} catch (error) {
|
|
||||||
await logFloatingWindow('Failed to add render-process-gone listener', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
await logFloatingWindow('Adding ready-to-show listener...')
|
floatingWindow.on('ready-to-show', () => {
|
||||||
floatingWindow.on('ready-to-show', async () => {
|
|
||||||
try {
|
|
||||||
await logFloatingWindow('Window ready to show, applying theme...')
|
|
||||||
applyTheme(customTheme)
|
applyTheme(customTheme)
|
||||||
await logFloatingWindow('Theme applied, showing window...')
|
|
||||||
floatingWindow?.show()
|
floatingWindow?.show()
|
||||||
await logFloatingWindow('Window shown, setting always on top...')
|
|
||||||
floatingWindow?.setAlwaysOnTop(true, 'screen-saver')
|
floatingWindow?.setAlwaysOnTop(true, 'screen-saver')
|
||||||
await logFloatingWindow('Floating window setup completed successfully')
|
|
||||||
} catch (error) {
|
|
||||||
await logFloatingWindow('Error in ready-to-show', error)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
await logFloatingWindow('Ready-to-show listener added')
|
|
||||||
|
|
||||||
await logFloatingWindow('Adding moved listener...')
|
|
||||||
floatingWindow.on('moved', () => {
|
floatingWindow.on('moved', () => {
|
||||||
if (floatingWindow) floatingWindowState.saveState(floatingWindow)
|
floatingWindow && floatingWindowState.saveState(floatingWindow)
|
||||||
})
|
})
|
||||||
await logFloatingWindow('Moved listener added')
|
|
||||||
await logFloatingWindow('Adding IPC listener...')
|
// IPC 监听器
|
||||||
ipcMain.on('updateFloatingWindow', () => {
|
ipcMain.on('updateFloatingWindow', () => {
|
||||||
if (floatingWindow) {
|
if (floatingWindow) {
|
||||||
floatingWindow?.webContents.send('controledMihomoConfigUpdated')
|
floatingWindow.webContents.send('controledMihomoConfigUpdated')
|
||||||
floatingWindow?.webContents.send('appConfigUpdated')
|
floatingWindow.webContents.send('appConfigUpdated')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await logFloatingWindow('IPC listener added')
|
|
||||||
|
|
||||||
await logFloatingWindow('Loading page...')
|
// 加载页面
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
const url = is.dev && process.env['ELECTRON_RENDERER_URL']
|
||||||
const devUrl = `${process.env['ELECTRON_RENDERER_URL']}/floating.html`
|
? `${process.env['ELECTRON_RENDERER_URL']}/floating.html`
|
||||||
await logFloatingWindow(`Loading dev URL: ${devUrl}`)
|
: join(__dirname, '../renderer/floating.html')
|
||||||
await floatingWindow.loadURL(devUrl)
|
|
||||||
} else {
|
is.dev ? await floatingWindow.loadURL(url) : await floatingWindow.loadFile(url)
|
||||||
const filePath = join(__dirname, '../renderer/floating.html')
|
|
||||||
await logFloatingWindow(`Loading file: ${filePath}`)
|
|
||||||
await floatingWindow.loadFile(filePath)
|
|
||||||
}
|
|
||||||
await logFloatingWindow('Page loaded successfully')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await logFloatingWindow('Failed to create floating window', error)
|
logError('Failed to create floating window', error)
|
||||||
if (error instanceof Error) {
|
|
||||||
await logFloatingWindow(`Error stack: ${error.stack}`)
|
|
||||||
}
|
|
||||||
floatingWindow = null
|
floatingWindow = null
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@ -172,26 +96,20 @@ async function createFloatingWindow(): Promise<void> {
|
|||||||
export async function showFloatingWindow(): Promise<void> {
|
export async function showFloatingWindow(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (floatingWindow && !floatingWindow.isDestroyed()) {
|
if (floatingWindow && !floatingWindow.isDestroyed()) {
|
||||||
await logFloatingWindow('Showing existing floating window')
|
|
||||||
floatingWindow.show()
|
floatingWindow.show()
|
||||||
} else {
|
} else {
|
||||||
await logFloatingWindow('Creating new floating window')
|
|
||||||
await createFloatingWindow()
|
await createFloatingWindow()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await logFloatingWindow('Failed to show floating window', error)
|
logError('Failed to show floating window', error)
|
||||||
|
|
||||||
// 如果已经是兼容模式还是崩溃,说明问题很严重,自动禁用悬浮窗
|
// 如果已经是兼容模式还是崩溃,自动禁用悬浮窗
|
||||||
const { floatingWindowCompatMode = true } = await getAppConfig()
|
const { floatingWindowCompatMode = true } = await getAppConfig()
|
||||||
if (floatingWindowCompatMode) {
|
if (floatingWindowCompatMode) {
|
||||||
await logFloatingWindow('Compatibility mode was already enabled, disabling floating window completely')
|
|
||||||
await patchAppConfig({ showFloatingWindow: false })
|
await patchAppConfig({ showFloatingWindow: false })
|
||||||
} else {
|
} else {
|
||||||
await logFloatingWindow('Enabling compatibility mode and retrying')
|
|
||||||
await patchAppConfig({ floatingWindowCompatMode: true })
|
await patchAppConfig({ floatingWindowCompatMode: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
await logFloatingWindow('Disabled floating window in config due to error')
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { nativeImage } from 'electron'
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
|
import { systemLogger } from '../utils/logger'
|
||||||
|
|
||||||
export let pacPort: number
|
export let pacPort: number
|
||||||
export let subStorePort: number
|
export let subStorePort: number
|
||||||
@ -168,7 +169,6 @@ export async function downloadSubStore(): Promise<void> {
|
|||||||
)
|
)
|
||||||
await writeFile(tempBackendPath, Buffer.from(backendRes.data))
|
await writeFile(tempBackendPath, Buffer.from(backendRes.data))
|
||||||
// 下载前端文件
|
// 下载前端文件
|
||||||
const tempFrontendDir = path.join(tempDir, 'dist')
|
|
||||||
const frontendRes = await axios.get(
|
const frontendRes = await axios.get(
|
||||||
'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip',
|
'https://github.com/sub-store-org/Sub-Store-Front-End/releases/latest/download/dist.zip',
|
||||||
{
|
{
|
||||||
@ -192,7 +192,7 @@ export async function downloadSubStore(): Promise<void> {
|
|||||||
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
|
await cp(path.join(tempDir, 'dist'), frontendDir, { recursive: true })
|
||||||
await rm(tempDir, { recursive: true })
|
await rm(tempDir, { recursive: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('substore.downloadFailed:', error)
|
await systemLogger.error('substore.downloadFailed', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,14 +22,15 @@ import { triggerSysProxy } from '../sys/sysproxy'
|
|||||||
import { quitWithoutCore, restartCore, checkMihomoCorePermissions, requestTunPermissions, restartAsAdmin } from '../core/manager'
|
import { quitWithoutCore, restartCore, checkMihomoCorePermissions, requestTunPermissions, restartAsAdmin } from '../core/manager'
|
||||||
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
|
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
|
import { trayLogger } from '../utils/logger'
|
||||||
|
|
||||||
export let tray: Tray | null = null
|
export let tray: Tray | null = null
|
||||||
|
|
||||||
export const buildContextMenu = async (): Promise<Menu> => {
|
export const buildContextMenu = async (): Promise<Menu> => {
|
||||||
// 添加调试日志
|
// 添加调试日志
|
||||||
console.log('Current translation for tray.showWindow:', t('tray.showWindow'))
|
await trayLogger.debug('Current translation for tray.showWindow', t('tray.showWindow'))
|
||||||
console.log('Current translation for tray.hideFloatingWindow:', t('tray.hideFloatingWindow'))
|
await trayLogger.debug('Current translation for tray.hideFloatingWindow', t('tray.hideFloatingWindow'))
|
||||||
console.log('Current translation for tray.showFloatingWindow:', t('tray.showFloatingWindow'))
|
await trayLogger.debug('Current translation for tray.showFloatingWindow', t('tray.showFloatingWindow'))
|
||||||
|
|
||||||
const { mode, tun } = await getControledMihomoConfig()
|
const { mode, tun } = await getControledMihomoConfig()
|
||||||
const {
|
const {
|
||||||
@ -187,7 +188,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
try {
|
try {
|
||||||
await restartAsAdmin()
|
await restartAsAdmin()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to restart as admin from tray:', error)
|
await trayLogger.error('Failed to restart as admin from tray', error)
|
||||||
item.checked = false
|
item.checked = false
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
return
|
return
|
||||||
@ -196,7 +197,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
try {
|
try {
|
||||||
await requestTunPermissions()
|
await requestTunPermissions()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to grant TUN permissions from tray:', error)
|
await trayLogger.error('Failed to grant TUN permissions from tray', error)
|
||||||
item.checked = false
|
item.checked = false
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
return
|
return
|
||||||
@ -204,7 +205,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Permission check failed in tray:', error)
|
await trayLogger.warn('Permission check failed in tray', error)
|
||||||
}
|
}
|
||||||
|
|
||||||
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
|
await patchControledMihomoConfig({ tun: { enable }, dns: { enable: true } })
|
||||||
@ -418,13 +419,13 @@ export async function closeTrayIcon(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function showDockIcon(): Promise<void> {
|
export async function showDockIcon(): Promise<void> {
|
||||||
if (process.platform === 'darwin' && !app.dock.isVisible()) {
|
if (process.platform === 'darwin' && app.dock && !app.dock.isVisible()) {
|
||||||
await app.dock.show()
|
await app.dock.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hideDockIcon(): Promise<void> {
|
export async function hideDockIcon(): Promise<void> {
|
||||||
if (process.platform === 'darwin' && app.dock.isVisible()) {
|
if (process.platform === 'darwin' && app.dock && app.dock.isVisible()) {
|
||||||
app.dock.hide()
|
app.dock.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { resourcesFilesDir } from '../utils/dirs'
|
|||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import { proxyLogger } from '../utils/logger'
|
||||||
|
|
||||||
let defaultBypass: string[]
|
let defaultBypass: string[]
|
||||||
let triggerSysProxyTimer: NodeJS.Timeout | null = null
|
let triggerSysProxyTimer: NodeJS.Timeout | null = null
|
||||||
@ -175,7 +176,7 @@ async function requestSocketRecreation(): Promise<void> {
|
|||||||
// Wait a bit for socket recreation
|
// Wait a bit for socket recreation
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('Failed to send signal to helper:', error)
|
await proxyLogger.error('Failed to send signal to helper', error)
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,16 +198,16 @@ async function helperRequest(requestFn: () => Promise<unknown>, maxRetries = 1):
|
|||||||
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
(error as Error).message?.includes('connect ECONNREFUSED') ||
|
||||||
(error as Error).message?.includes('ENOENT'))) {
|
(error as Error).message?.includes('ENOENT'))) {
|
||||||
|
|
||||||
console.log(`Helper request failed (attempt ${attempt + 1}), checking socket file...`)
|
await proxyLogger.info(`Helper request failed (attempt ${attempt + 1}), checking socket file...`)
|
||||||
|
|
||||||
if (!isSocketFileExists()) {
|
if (!isSocketFileExists()) {
|
||||||
console.log('Socket file missing, requesting recreation...')
|
await proxyLogger.info('Socket file missing, requesting recreation...')
|
||||||
try {
|
try {
|
||||||
await requestSocketRecreation()
|
await requestSocketRecreation()
|
||||||
console.log('Socket recreation requested, retrying...')
|
await proxyLogger.info('Socket recreation requested, retrying...')
|
||||||
continue
|
continue
|
||||||
} catch (signalError) {
|
} catch (signalError) {
|
||||||
console.log('Failed to request socket recreation:', signalError)
|
await proxyLogger.warn('Failed to request socket recreation', signalError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -134,12 +134,27 @@ export function logDir(): string {
|
|||||||
|
|
||||||
export function logPath(): string {
|
export function logPath(): string {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const name = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const name = `mihomo-party-${year}-${month}-${day}`
|
||||||
return path.join(logDir(), `${name}.log`)
|
return path.join(logDir(), `${name}.log`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function substoreLogPath(): string {
|
export function substoreLogPath(): string {
|
||||||
const date = new Date()
|
const date = new Date()
|
||||||
const name = `sub-store-${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const name = `sub-store-${year}-${month}-${day}`
|
||||||
|
return path.join(logDir(), `${name}.log`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function coreLogPath(): string {
|
||||||
|
const date = new Date()
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const name = `core-${year}-${month}-${day}`
|
||||||
return path.join(logDir(), `${name}.log`)
|
return path.join(logDir(), `${name}.log`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import {
|
|||||||
import { app, dialog } from 'electron'
|
import { app, dialog } from 'electron'
|
||||||
import { startSSIDCheck } from '../sys/ssid'
|
import { startSSIDCheck } from '../sys/ssid'
|
||||||
import i18next from '../../shared/i18n'
|
import i18next from '../../shared/i18n'
|
||||||
|
import { initLogger } from './logger'
|
||||||
|
|
||||||
// 安全错误处理
|
// 安全错误处理
|
||||||
export function safeShowErrorBox(titleKey: string, message: string): void {
|
export function safeShowErrorBox(titleKey: string, message: string): void {
|
||||||
@ -115,7 +116,7 @@ async function initDirs(): Promise<void> {
|
|||||||
await mkdir(dir, { recursive: true })
|
await mkdir(dir, { recursive: true })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to create directory ${dir}:`, error)
|
await initLogger.error(`Failed to create directory ${dir}`, error)
|
||||||
throw new Error(`Failed to create directory ${dir}: ${error}`)
|
throw new Error(`Failed to create directory ${dir}: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,7 +137,7 @@ async function initConfig(): Promise<void> {
|
|||||||
await writeFile(config.path, yaml.stringify(config.content))
|
await writeFile(config.path, yaml.stringify(config.content))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to create ${config.name} at ${config.path}:`, error)
|
await initLogger.error(`Failed to create ${config.name} at ${config.path}`, error)
|
||||||
throw new Error(`Failed to create ${config.name}: ${error}`)
|
throw new Error(`Failed to create ${config.name}: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,7 +164,7 @@ async function initFiles(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to copy ${file}:`, error)
|
await initLogger.error(`Failed to copy ${file}`, error)
|
||||||
if (['country.mmdb', 'geoip.dat', 'geosite.dat'].includes(file)) {
|
if (['country.mmdb', 'geoip.dat', 'geosite.dat'].includes(file)) {
|
||||||
throw new Error(`Failed to copy critical file ${file}: ${error}`)
|
throw new Error(`Failed to copy critical file ${file}: ${error}`)
|
||||||
}
|
}
|
||||||
|
|||||||
108
src/main/utils/logger.ts
Normal file
108
src/main/utils/logger.ts
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { writeFile } from 'fs/promises'
|
||||||
|
import { logPath } from './dirs'
|
||||||
|
|
||||||
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
||||||
|
|
||||||
|
class Logger {
|
||||||
|
private moduleName: string
|
||||||
|
|
||||||
|
constructor(moduleName: string) {
|
||||||
|
this.moduleName = moduleName
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatTimestamp(): string {
|
||||||
|
return new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatLogMessage(level: LogLevel, message: string, error?: any): string {
|
||||||
|
const timestamp = this.formatTimestamp()
|
||||||
|
const errorStr = error ? `: ${error}` : ''
|
||||||
|
return `[${timestamp}] [${level.toUpperCase()}] [${this.moduleName}] ${message}${errorStr}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
private async writeToFile(level: LogLevel, message: string, error?: any): Promise<void> {
|
||||||
|
try {
|
||||||
|
const appLogPath = logPath()
|
||||||
|
const logMessage = this.formatLogMessage(level, message, error)
|
||||||
|
await writeFile(appLogPath, logMessage, { flag: 'a' })
|
||||||
|
} catch (logError) {
|
||||||
|
// 如果写入日志文件失败,仍然输出到控制台
|
||||||
|
console.error(`[Logger] Failed to write to log file:`, logError)
|
||||||
|
console.error(`[Logger] Original message: [${level.toUpperCase()}] [${this.moduleName}] ${message}`, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private logToConsole(level: LogLevel, message: string, error?: any): void {
|
||||||
|
const prefix = `[${this.moduleName}] ${message}`
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'debug':
|
||||||
|
console.debug(prefix, error || '')
|
||||||
|
break
|
||||||
|
case 'info':
|
||||||
|
console.log(prefix, error || '')
|
||||||
|
break
|
||||||
|
case 'warn':
|
||||||
|
console.warn(prefix, error || '')
|
||||||
|
break
|
||||||
|
case 'error':
|
||||||
|
console.error(prefix, error || '')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async debug(message: string, error?: any): Promise<void> {
|
||||||
|
await this.writeToFile('debug', message, error)
|
||||||
|
this.logToConsole('debug', message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async info(message: string, error?: any): Promise<void> {
|
||||||
|
await this.writeToFile('info', message, error)
|
||||||
|
this.logToConsole('info', message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async warn(message: string, error?: any): Promise<void> {
|
||||||
|
await this.writeToFile('warn', message, error)
|
||||||
|
this.logToConsole('warn', message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async error(message: string, error?: any): Promise<void> {
|
||||||
|
await this.writeToFile('error', message, error)
|
||||||
|
this.logToConsole('error', message, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容原有的 logFloatingWindow 函数签名
|
||||||
|
async log(message: string, error?: any): Promise<void> {
|
||||||
|
if (error) {
|
||||||
|
await this.error(message, error)
|
||||||
|
} else {
|
||||||
|
await this.info(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建不同模块的日志实例
|
||||||
|
export const createLogger = (moduleName: string): Logger => {
|
||||||
|
return new Logger(moduleName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的应用日志实例 - 所有模块共享同一个日志文件
|
||||||
|
export const appLogger = createLogger('app')
|
||||||
|
|
||||||
|
// 为了保持向后兼容性,创建各模块的日志实例(都指向同一个应用日志)
|
||||||
|
export const floatingWindowLogger = createLogger('floating-window')
|
||||||
|
export const coreLogger = createLogger('mihomo-core')
|
||||||
|
export const apiLogger = createLogger('mihomo-api')
|
||||||
|
export const configLogger = createLogger('config')
|
||||||
|
export const systemLogger = createLogger('system')
|
||||||
|
export const trafficLogger = createLogger('traffic-monitor')
|
||||||
|
export const trayLogger = createLogger('tray')
|
||||||
|
export const initLogger = createLogger('init')
|
||||||
|
export const ipcLogger = createLogger('ipc')
|
||||||
|
export const proxyLogger = createLogger('sysproxy')
|
||||||
|
export const managerLogger = createLogger('manager')
|
||||||
|
export const factoryLogger = createLogger('factory')
|
||||||
|
export const overrideLogger = createLogger('override')
|
||||||
|
|
||||||
|
// 默认日志实例
|
||||||
|
export const logger = appLogger
|
||||||
@ -22,6 +22,10 @@ export const defaultConfig: IAppConfig = {
|
|||||||
controlDns: true,
|
controlDns: true,
|
||||||
controlSniff: true,
|
controlSniff: true,
|
||||||
floatingWindowCompatMode: true,
|
floatingWindowCompatMode: true,
|
||||||
|
disableLoopbackDetector: false,
|
||||||
|
disableEmbedCA: false,
|
||||||
|
disableSystemCA: false,
|
||||||
|
skipSafePathCheck: false,
|
||||||
nameserverPolicy: {},
|
nameserverPolicy: {},
|
||||||
siderOrder: [
|
siderOrder: [
|
||||||
'sysproxy',
|
'sysproxy',
|
||||||
|
|||||||
@ -323,6 +323,11 @@
|
|||||||
"tun.permissions.windowsRestart": "On Windows, you need to restart the application as administrator to use TUN mode",
|
"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.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...",
|
||||||
|
"tun.permissions.highPrivilegeDetected.title": "High Privilege Core Detected",
|
||||||
|
"tun.permissions.highPrivilegeDetected.message": "Application is running with administrator privileges",
|
||||||
|
"tun.permissions.highPrivilegeDetected.detail": "About to restart the application in administrator mode. Restart now?",
|
||||||
|
"tun.permissions.highPrivilegeDetected.restart": "Restart",
|
||||||
|
"tun.permissions.highPrivilegeDetected.exit": "Exit Application",
|
||||||
"dns.title": "DNS Settings",
|
"dns.title": "DNS Settings",
|
||||||
"dns.enable": "Enable DNS",
|
"dns.enable": "Enable DNS",
|
||||||
"dns.enhancedMode.title": "Domain Mapping Mode",
|
"dns.enhancedMode.title": "Domain Mapping Mode",
|
||||||
|
|||||||
@ -307,6 +307,16 @@
|
|||||||
"tun.notifications.coreAuthSuccess": "مجوزدهی به هسته با موفقیت انجام شد",
|
"tun.notifications.coreAuthSuccess": "مجوزدهی به هسته با موفقیت انجام شد",
|
||||||
"tun.notifications.firewallResetSuccess": "بازنشانی دیوار آتش با موفقیت انجام شد",
|
"tun.notifications.firewallResetSuccess": "بازنشانی دیوار آتش با موفقیت انجام شد",
|
||||||
"tun.error.tunPermissionDenied": "راهاندازی رابط TUN با شکست مواجه شد، لطفا به صورت دستی به هسته مجوز دهید",
|
"tun.error.tunPermissionDenied": "راهاندازی رابط TUN با شکست مواجه شد، لطفا به صورت دستی به هسته مجوز دهید",
|
||||||
|
"tun.permissions.required": "حالت TUN نیاز به مجوزهای مدیر دارد. آیا اکنون برنامه را مجدداً راهاندازی کنیم تا مجوزها را دریافت کنیم؟",
|
||||||
|
"tun.permissions.failed": "مجوزدهی با شکست مواجه شد",
|
||||||
|
"tun.permissions.windowsRestart": "در ویندوز، برای استفاده از حالت TUN باید برنامه را به عنوان مدیر مجدداً راهاندازی کنید",
|
||||||
|
"tun.permissions.requesting": "درخواست مجوزهای مدیر، لطفا در دیالوگ UAC روی 'بله' کلیک کنید...",
|
||||||
|
"tun.permissions.restarting": "راهاندازی مجدد برنامه با مجوزهای مدیر، لطفا در دیالوگ UAC روی 'بله' کلیک کنید...",
|
||||||
|
"tun.permissions.highPrivilegeDetected.title": "هسته با مجوزهای بالا شناسایی شد",
|
||||||
|
"tun.permissions.highPrivilegeDetected.message": "برنامه با مجوزهای مدیر در حال اجرا است",
|
||||||
|
"tun.permissions.highPrivilegeDetected.detail": "برنامه در حالت مدیر سیستم دوباره راهاندازی خواهد شد. آیا میخواهید اکنون راهاندازی مجدد شود؟",
|
||||||
|
"tun.permissions.highPrivilegeDetected.restart": "راهاندازی مجدد",
|
||||||
|
"tun.permissions.highPrivilegeDetected.exit": "خروج از برنامه",
|
||||||
"dns.title": "تنظیمات DNS",
|
"dns.title": "تنظیمات DNS",
|
||||||
"dns.enable": "فعالسازی DNS",
|
"dns.enable": "فعالسازی DNS",
|
||||||
"dns.enhancedMode.title": "حالت نگاشت دامنه",
|
"dns.enhancedMode.title": "حالت نگاشت دامنه",
|
||||||
|
|||||||
@ -307,6 +307,16 @@
|
|||||||
"tun.notifications.coreAuthSuccess": "Авторизация ядра успешна",
|
"tun.notifications.coreAuthSuccess": "Авторизация ядра успешна",
|
||||||
"tun.notifications.firewallResetSuccess": "Брандмауэр успешно сброшен",
|
"tun.notifications.firewallResetSuccess": "Брандмауэр успешно сброшен",
|
||||||
"tun.error.tunPermissionDenied": "Ошибка запуска TUN, попробуйте вручную предоставить разрешения ядру",
|
"tun.error.tunPermissionDenied": "Ошибка запуска TUN, попробуйте вручную предоставить разрешения ядру",
|
||||||
|
"tun.permissions.required": "Режим TUN требует прав администратора. Перезапустить приложение сейчас для получения разрешений?",
|
||||||
|
"tun.permissions.failed": "Ошибка авторизации разрешений",
|
||||||
|
"tun.permissions.windowsRestart": "В Windows необходимо перезапустить приложение от имени администратора для использования режима TUN",
|
||||||
|
"tun.permissions.requesting": "Запрос прав администратора, пожалуйста, нажмите 'Да' в диалоге UAC...",
|
||||||
|
"tun.permissions.restarting": "Перезапуск приложения с правами администратора, пожалуйста, нажмите 'Да' в диалоге UAC...",
|
||||||
|
"tun.permissions.highPrivilegeDetected.title": "Обнаружено ядро с высокими привилегиями",
|
||||||
|
"tun.permissions.highPrivilegeDetected.message": "Приложение работает с правами администратора",
|
||||||
|
"tun.permissions.highPrivilegeDetected.detail": "Сейчас приложение будет перезапущено в режиме администратора. Перезапустить сейчас?",
|
||||||
|
"tun.permissions.highPrivilegeDetected.restart": "Перезапустить",
|
||||||
|
"tun.permissions.highPrivilegeDetected.exit": "Выйти из приложения",
|
||||||
"dns.title": "Настройки DNS",
|
"dns.title": "Настройки DNS",
|
||||||
"dns.enable": "Включить DNS",
|
"dns.enable": "Включить DNS",
|
||||||
"dns.enhancedMode.title": "Режим маппинга доменов",
|
"dns.enhancedMode.title": "Режим маппинга доменов",
|
||||||
|
|||||||
@ -323,6 +323,11 @@
|
|||||||
"tun.permissions.windowsRestart": "Windows下需要以管理员身份重新启动应用才能使用TUN模式",
|
"tun.permissions.windowsRestart": "Windows下需要以管理员身份重新启动应用才能使用TUN模式",
|
||||||
"tun.permissions.requesting": "正在请求管理员权限,请在UAC对话框中点击'是'...",
|
"tun.permissions.requesting": "正在请求管理员权限,请在UAC对话框中点击'是'...",
|
||||||
"tun.permissions.restarting": "正在以管理员权限重启应用,请在UAC对话框中点击'是'...",
|
"tun.permissions.restarting": "正在以管理员权限重启应用,请在UAC对话框中点击'是'...",
|
||||||
|
"tun.permissions.highPrivilegeDetected.title": "检测到高权限内核",
|
||||||
|
"tun.permissions.highPrivilegeDetected.message": "应用正在以管理员权限运行",
|
||||||
|
"tun.permissions.highPrivilegeDetected.detail": "即将以管理员模式重启应用,现在重新启动吗?",
|
||||||
|
"tun.permissions.highPrivilegeDetected.restart": "重新启动",
|
||||||
|
"tun.permissions.highPrivilegeDetected.exit": "退出应用",
|
||||||
"dns.title": "DNS 设置",
|
"dns.title": "DNS 设置",
|
||||||
"dns.enable": "启用 DNS",
|
"dns.enable": "启用 DNS",
|
||||||
"dns.enhancedMode.title": "域名映射模式",
|
"dns.enhancedMode.title": "域名映射模式",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user