mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
Compare commits
2 Commits
bb58b60c21
...
0dad7a6d8b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dad7a6d8b | ||
|
|
1e3f31d1a7 |
@ -72,23 +72,14 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
||||
skipSafePathCheck = false
|
||||
} = await getAppConfig()
|
||||
const { 'log-level': logLevel } = await getControledMihomoConfig()
|
||||
if (existsSync(path.join(dataDir(), 'core.pid'))) {
|
||||
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'))
|
||||
}
|
||||
}
|
||||
|
||||
await handleExistingCore()
|
||||
|
||||
const { current } = await getProfileConfig()
|
||||
const { tun } = await getControledMihomoConfig()
|
||||
const corePath = mihomoCorePath(core)
|
||||
|
||||
// 管理 Smart 内核覆写配置
|
||||
await manageSmartOverride()
|
||||
|
||||
await generateProfile()
|
||||
await checkProfile()
|
||||
await stopCore()
|
||||
@ -154,6 +145,14 @@ export async function startCore(detached = false): Promise<Promise<void>[]> {
|
||||
if ((process.platform !== 'win32' && str.includes('External controller unix 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'))
|
||||
}
|
||||
|
||||
@ -203,21 +202,29 @@ export async function stopCore(force = false): Promise<void> {
|
||||
child.removeAllListeners()
|
||||
child.kill('SIGINT')
|
||||
}
|
||||
|
||||
await cleanupCoreFiles()
|
||||
stopMihomoTraffic()
|
||||
stopMihomoConnections()
|
||||
stopMihomoLogs()
|
||||
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> {
|
||||
try {
|
||||
await startCore()
|
||||
} catch (e) {
|
||||
// 记录错误到日志而不是显示阻塞对话框
|
||||
await writeFile(logPath(), `[Manager]: restart core failed, ${e}\n`, {
|
||||
flag: 'a'
|
||||
})
|
||||
// 重新抛出错误,让调用者处理
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@ -226,13 +233,25 @@ export async function keepCoreAlive(): Promise<void> {
|
||||
try {
|
||||
await startCore(true)
|
||||
if (child && child.pid) {
|
||||
await writeFile(path.join(dataDir(), 'core.pid'), child.pid.toString())
|
||||
await writeCoreStateFile(child.pid)
|
||||
}
|
||||
} catch (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> {
|
||||
await keepCoreAlive()
|
||||
await startMonitor(true)
|
||||
@ -260,13 +279,8 @@ async function checkProfile(): Promise<void> {
|
||||
mihomoTestDir()
|
||||
], { env })
|
||||
} catch (error) {
|
||||
console.error('Profile check failed:', error)
|
||||
|
||||
if (error instanceof Error && 'stdout' in error) {
|
||||
const { stdout, stderr } = error as { stdout: string; stderr?: string }
|
||||
console.log('Profile check stdout:', stdout)
|
||||
console.log('Profile check stderr:', stderr)
|
||||
|
||||
const { stdout } = error as { stdout: string; stderr?: string }
|
||||
const errorLines = stdout
|
||||
.split('\n')
|
||||
.filter((line) => line.includes('level=error') || line.includes('error'))
|
||||
@ -363,10 +377,10 @@ export async function restartAsAdmin(): Promise<void> {
|
||||
|
||||
const exePath = process.execPath
|
||||
const args = process.argv.slice(1)
|
||||
const restartArgs = [...args, '--admin-restart-for-tun']
|
||||
const cleanArgs = args.filter(arg => !arg.startsWith('--admin-restart'))
|
||||
const restartArgs = [...cleanArgs, '--admin-restart-for-permission']
|
||||
|
||||
try {
|
||||
// 处理路径和参数的引号
|
||||
const escapedExePath = exePath.replace(/'/g, "''")
|
||||
const argsString = restartArgs.map(arg => arg.replace(/'/g, "''")).join("', '")
|
||||
|
||||
@ -377,15 +391,10 @@ export async function restartAsAdmin(): Promise<void> {
|
||||
command = `powershell -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
|
||||
}
|
||||
|
||||
console.log('Restarting as administrator with command:', command)
|
||||
|
||||
// 执行PowerShell命令
|
||||
exec(command, { windowsHide: true }, (error, _stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('PowerShell execution error:', error)
|
||||
console.error('stderr:', stderr)
|
||||
} else {
|
||||
console.log('PowerShell command executed successfully')
|
||||
}
|
||||
})
|
||||
|
||||
@ -407,7 +416,6 @@ export async function checkMihomoCorePermissions(): Promise<boolean> {
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows权限检查
|
||||
return await checkAdminPrivileges()
|
||||
}
|
||||
|
||||
@ -423,7 +431,6 @@ export async function checkMihomoCorePermissions(): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
// TUN模式获取权限
|
||||
export async function requestTunPermissions(): Promise<void> {
|
||||
if (process.platform === 'win32') {
|
||||
await restartAsAdmin()
|
||||
@ -437,8 +444,6 @@ export async function requestTunPermissions(): Promise<void> {
|
||||
|
||||
export async function checkAdminRestartForTun(): Promise<void> {
|
||||
if (process.argv.includes('--admin-restart-for-tun')) {
|
||||
console.log('Detected admin restart for TUN mode, auto-enabling TUN...')
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
const hasAdminPrivileges = await checkAdminPrivileges()
|
||||
@ -446,24 +451,236 @@ export async function checkAdminRestartForTun(): Promise<void> {
|
||||
await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } })
|
||||
await restartCore()
|
||||
|
||||
console.log('TUN mode auto-enabled after admin restart')
|
||||
|
||||
const { mainWindow } = await import('../index')
|
||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
} else {
|
||||
console.warn('Admin restart detected but no admin privileges found')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to auto-enable TUN after admin restart:', error)
|
||||
}
|
||||
} else if (process.argv.includes('--admin-restart-for-permission')) {
|
||||
await checkAndAlignPermissions()
|
||||
} else {
|
||||
// 检查TUN配置与权限的匹配
|
||||
await validateTunPermissionsOnStartup()
|
||||
await checkAndAlignPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
} 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> {
|
||||
const pidFile = path.join(dataDir(), 'core.pid')
|
||||
|
||||
if (!existsSync(pidFile)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const pidStr = await readFile(pidFile, 'utf-8')
|
||||
const pid = parseInt(pidStr.trim())
|
||||
|
||||
if (isProcessRunning(pid)) {
|
||||
try {
|
||||
process.kill(pid, 'SIGINT')
|
||||
} catch (error) {
|
||||
console.warn('Failed to kill existing process:', error)
|
||||
}
|
||||
}
|
||||
|
||||
await cleanupCoreFiles()
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function getRunningCoreInfo(): Promise<{ pid: number; isAdmin: boolean } | null> {
|
||||
const stateFile = path.join(dataDir(), 'core.state')
|
||||
const pidFile = path.join(dataDir(), 'core.pid')
|
||||
|
||||
if (existsSync(stateFile)) {
|
||||
try {
|
||||
const stateData = JSON.parse(await readFile(stateFile, 'utf-8'))
|
||||
const { pid, isAdmin } = stateData
|
||||
|
||||
if (isProcessRunning(pid)) {
|
||||
return { pid, isAdmin }
|
||||
} else {
|
||||
await rm(stateFile).catch(() => {})
|
||||
await rm(pidFile).catch(() => {})
|
||||
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) {
|
||||
console.warn('Failed to check process elevation:', 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> {
|
||||
try {
|
||||
const { tun } = await getControledMihomoConfig()
|
||||
@ -475,23 +692,55 @@ export async function validateTunPermissionsOnStartup(): Promise<void> {
|
||||
const hasPermissions = await checkMihomoCorePermissions()
|
||||
|
||||
if (!hasPermissions) {
|
||||
console.warn('TUN is enabled but insufficient permissions detected, auto-disabling TUN...')
|
||||
|
||||
await patchControledMihomoConfig({ tun: { enable: false } })
|
||||
|
||||
const { mainWindow } = await import('../index')
|
||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
|
||||
console.log('TUN auto-disabled due to insufficient permissions')
|
||||
} else {
|
||||
console.log('TUN permissions validated successfully')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to validate TUN permissions on startup:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkForElevatedCore(): Promise<void> {
|
||||
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 {
|
||||
const { app } = await import('electron')
|
||||
app.quit()
|
||||
}
|
||||
|
||||
throw new Error('Application restart required for permission alignment')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Application restart required for permission alignment') {
|
||||
throw error
|
||||
}
|
||||
console.error('Failed to check for elevated core:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function manualGrantCorePermition(): Promise<void> {
|
||||
return grantTunPermissions()
|
||||
}
|
||||
|
||||
@ -51,6 +51,11 @@ export const patchMihomoConfig = async (patch: Partial<IMihomoConfig>): Promise<
|
||||
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> => {
|
||||
const instance = await getAxios()
|
||||
return await instance.delete(`/connections/${encodeURIComponent(id)}`)
|
||||
|
||||
@ -169,16 +169,24 @@ app.whenReady().then(async () => {
|
||||
showSafeErrorBox('common.error.initFailed', `${e}`)
|
||||
app.quit()
|
||||
}
|
||||
// 权限检查
|
||||
try {
|
||||
const [startPromise] = await startCore()
|
||||
startPromise.then(async () => {
|
||||
await initProfileUpdater()
|
||||
// 上次是否为了开启 TUN 而重启
|
||||
await checkAdminRestartForTun()
|
||||
})
|
||||
console.log('Starting permission check before GUI creation...')
|
||||
await checkAdminRestartForTun()
|
||||
console.log('Permission check passed, continuing with app startup...')
|
||||
} catch (e) {
|
||||
showSafeErrorBox('mihomo.error.coreStartFailed', `${e}`)
|
||||
console.error('Permission check failed:', 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 {
|
||||
await startMonitor()
|
||||
} catch {
|
||||
@ -194,6 +202,24 @@ app.whenReady().then(async () => {
|
||||
const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig()
|
||||
registerIpcMainHandlers()
|
||||
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) {
|
||||
try {
|
||||
await showFloatingWindow()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user