mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-02-10 11:40:28 +08:00
refactor: split manager.ts into permissions, process, dns modules
This commit is contained in:
parent
b42287d104
commit
6a9edd8665
@ -25,3 +25,15 @@ fi
|
||||
if hash update-desktop-database 2>/dev/null; then
|
||||
update-desktop-database /usr/share/applications || true
|
||||
fi
|
||||
|
||||
# Update icon cache for GNOME/GTK environments
|
||||
if hash gtk-update-icon-cache 2>/dev/null; then
|
||||
for dir in /usr/share/icons/hicolor /usr/share/icons/gnome; do
|
||||
[ -d "$dir" ] && gtk-update-icon-cache -f -t "$dir" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
# Refresh GNOME Shell icon cache
|
||||
if hash update-icon-caches 2>/dev/null; then
|
||||
update-icon-caches /usr/share/icons/* 2>/dev/null || true
|
||||
fi
|
||||
|
||||
@ -15,6 +15,13 @@ case "$1" in
|
||||
if hash update-desktop-database 2>/dev/null; then
|
||||
update-desktop-database /usr/share/applications || true
|
||||
fi
|
||||
|
||||
# Update icon cache
|
||||
if hash gtk-update-icon-cache 2>/dev/null; then
|
||||
for dir in /usr/share/icons/hicolor /usr/share/icons/gnome; do
|
||||
[ -d "$dir" ] && gtk-update-icon-cache -f -t "$dir" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
# others
|
||||
|
||||
@ -59,7 +59,11 @@ linux:
|
||||
desktop:
|
||||
entry:
|
||||
Name: Clash Party
|
||||
GenericName: Proxy Client
|
||||
Comment: A GUI client based on Mihomo
|
||||
MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
|
||||
Keywords: proxy;clash;mihomo;vpn;
|
||||
StartupWMClass: clash-party
|
||||
target:
|
||||
- deb
|
||||
- rpm
|
||||
|
||||
76
src/main/core/dns.ts
Normal file
76
src/main/core/dns.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { net } from 'electron'
|
||||
import { getAppConfig, patchAppConfig } from '../config'
|
||||
|
||||
const execPromise = promisify(exec)
|
||||
|
||||
let setPublicDNSTimer: NodeJS.Timeout | null = null
|
||||
let recoverDNSTimer: NodeJS.Timeout | null = null
|
||||
|
||||
export async function getDefaultDevice(): Promise<string> {
|
||||
const { stdout: deviceOut } = await execPromise(`route -n get default`)
|
||||
let device = deviceOut.split('\n').find((s) => s.includes('interface:'))
|
||||
device = device?.trim().split(' ').slice(1).join(' ')
|
||||
if (!device) throw new Error('Get device failed')
|
||||
return device
|
||||
}
|
||||
|
||||
async function getDefaultService(): Promise<string> {
|
||||
const device = await getDefaultDevice()
|
||||
const { stdout: order } = await execPromise(`networksetup -listnetworkserviceorder`)
|
||||
const block = order.split('\n\n').find((s) => s.includes(`Device: ${device}`))
|
||||
if (!block) throw new Error('Get networkservice failed')
|
||||
for (const line of block.split('\n')) {
|
||||
if (line.match(/^\(\d+\).*/)) {
|
||||
return line.trim().split(' ').slice(1).join(' ')
|
||||
}
|
||||
}
|
||||
throw new Error('Get service failed')
|
||||
}
|
||||
|
||||
async function getOriginDNS(): Promise<void> {
|
||||
const service = await getDefaultService()
|
||||
const { stdout: dns } = await execPromise(`networksetup -getdnsservers "${service}"`)
|
||||
if (dns.startsWith("There aren't any DNS Servers set on")) {
|
||||
await patchAppConfig({ originDNS: 'Empty' })
|
||||
} else {
|
||||
await patchAppConfig({ originDNS: dns.trim().replace(/\n/g, ' ') })
|
||||
}
|
||||
}
|
||||
|
||||
async function setDNS(dns: string): Promise<void> {
|
||||
const service = await getDefaultService()
|
||||
// networksetup 需要 root 权限,通过 osascript 请求管理员权限执行
|
||||
const shell = `networksetup -setdnsservers "${service}" ${dns}`
|
||||
const command = `do shell script "${shell}" with administrator privileges`
|
||||
await execPromise(`osascript -e '${command}'`)
|
||||
}
|
||||
|
||||
export async function setPublicDNS(): Promise<void> {
|
||||
if (process.platform !== 'darwin') return
|
||||
if (net.isOnline()) {
|
||||
const { originDNS } = await getAppConfig()
|
||||
if (!originDNS) {
|
||||
await getOriginDNS()
|
||||
await setDNS('223.5.5.5')
|
||||
}
|
||||
} else {
|
||||
if (setPublicDNSTimer) clearTimeout(setPublicDNSTimer)
|
||||
setPublicDNSTimer = setTimeout(() => setPublicDNS(), 5000)
|
||||
}
|
||||
}
|
||||
|
||||
export async function recoverDNS(): Promise<void> {
|
||||
if (process.platform !== 'darwin') return
|
||||
if (net.isOnline()) {
|
||||
const { originDNS } = await getAppConfig()
|
||||
if (originDNS) {
|
||||
await setDNS(originDNS)
|
||||
await patchAppConfig({ originDNS: undefined })
|
||||
}
|
||||
} else {
|
||||
if (recoverDNSTimer) clearTimeout(recoverDNSTimer)
|
||||
recoverDNSTimer = setTimeout(() => recoverDNS(), 5000)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
408
src/main/core/permissions.ts
Normal file
408
src/main/core/permissions.ts
Normal file
@ -0,0 +1,408 @@
|
||||
import { exec, execFile } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { stat } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import { app, dialog, ipcMain } from 'electron'
|
||||
import { getAppConfig, patchControledMihomoConfig } from '../config'
|
||||
import { mihomoCorePath, mihomoCoreDir } from '../utils/dirs'
|
||||
import { managerLogger } from '../utils/logger'
|
||||
import i18next from '../../shared/i18n'
|
||||
import path from 'path'
|
||||
|
||||
const execPromise = promisify(exec)
|
||||
const execFilePromise = promisify(execFile)
|
||||
|
||||
// 内核名称白名单
|
||||
const ALLOWED_CORES = ['mihomo', 'mihomo-alpha', 'mihomo-smart'] as const
|
||||
type AllowedCore = (typeof ALLOWED_CORES)[number]
|
||||
|
||||
export function isValidCoreName(core: string): core is AllowedCore {
|
||||
return ALLOWED_CORES.includes(core as AllowedCore)
|
||||
}
|
||||
|
||||
export function validateCorePath(corePath: string): void {
|
||||
if (corePath.includes('..')) {
|
||||
throw new Error('Invalid core path: directory traversal detected')
|
||||
}
|
||||
|
||||
const dangerousChars = /[;&|`$(){}[\]<>'"\\]/
|
||||
if (dangerousChars.test(path.basename(corePath))) {
|
||||
throw new Error('Invalid core path: contains dangerous characters')
|
||||
}
|
||||
|
||||
const normalizedPath = path.normalize(path.resolve(corePath))
|
||||
const expectedDir = path.normalize(path.resolve(mihomoCoreDir()))
|
||||
|
||||
if (!normalizedPath.startsWith(expectedDir + path.sep) && normalizedPath !== expectedDir) {
|
||||
throw new Error('Invalid core path: not in expected directory')
|
||||
}
|
||||
}
|
||||
|
||||
function shellEscape(arg: string): string {
|
||||
return "'" + arg.replace(/'/g, "'\\''") + "'"
|
||||
}
|
||||
|
||||
// 会话管理员状态缓存
|
||||
let sessionAdminStatus: boolean | null = null
|
||||
|
||||
export async function initAdminStatus(): Promise<void> {
|
||||
if (process.platform === 'win32' && sessionAdminStatus === null) {
|
||||
sessionAdminStatus = await checkAdminPrivileges().catch(() => false)
|
||||
}
|
||||
}
|
||||
|
||||
export function getSessionAdminStatus(): boolean {
|
||||
if (process.platform !== 'win32') {
|
||||
return true
|
||||
}
|
||||
return sessionAdminStatus ?? false
|
||||
}
|
||||
|
||||
export async function checkAdminPrivileges(): Promise<boolean> {
|
||||
if (process.platform !== 'win32') {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
await execPromise('chcp 65001 >nul 2>&1 && fltmc', { encoding: 'utf8' })
|
||||
managerLogger.info('Admin privileges confirmed via fltmc')
|
||||
return true
|
||||
} catch (fltmcError: unknown) {
|
||||
const errorCode = (fltmcError as { code?: number })?.code || 0
|
||||
managerLogger.debug(`fltmc failed with code ${errorCode}, trying net session as fallback`)
|
||||
|
||||
try {
|
||||
await execPromise('chcp 65001 >nul 2>&1 && net session', { encoding: 'utf8' })
|
||||
managerLogger.info('Admin privileges confirmed via net session')
|
||||
return true
|
||||
} catch (netSessionError: unknown) {
|
||||
const netErrorCode = (netSessionError as { code?: number })?.code || 0
|
||||
managerLogger.debug(
|
||||
`Both fltmc and net session failed, no admin privileges. Error codes: fltmc=${errorCode}, net=${netErrorCode}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkMihomoCorePermissions(): Promise<boolean> {
|
||||
const { core = 'mihomo' } = await getAppConfig()
|
||||
const corePath = mihomoCorePath(core)
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
return await checkAdminPrivileges()
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||
const stats = await stat(corePath)
|
||||
return (stats.mode & 0o4000) !== 0 && stats.uid === 0
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export async function checkHighPrivilegeCore(): Promise<boolean> {
|
||||
try {
|
||||
const { core = 'mihomo' } = await getAppConfig()
|
||||
const corePath = mihomoCorePath(core)
|
||||
|
||||
managerLogger.info(`Checking high privilege core: ${corePath}`)
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
if (!existsSync(corePath)) {
|
||||
managerLogger.info('Core file does not exist')
|
||||
return false
|
||||
}
|
||||
|
||||
const hasHighPrivilegeProcess = await checkHighPrivilegeMihomoProcess()
|
||||
if (hasHighPrivilegeProcess) {
|
||||
managerLogger.info('Found high privilege mihomo process running')
|
||||
return true
|
||||
}
|
||||
|
||||
const isAdmin = await checkAdminPrivileges()
|
||||
managerLogger.info(`Current process admin privileges: ${isAdmin}`)
|
||||
return isAdmin
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||
managerLogger.info('Non-Windows platform, skipping high privilege core check')
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
managerLogger.error('Failed to check high privilege core', error)
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
|
||||
const mihomoExecutables =
|
||||
process.platform === 'win32'
|
||||
? ['mihomo.exe', 'mihomo-alpha.exe', 'mihomo-smart.exe']
|
||||
: ['mihomo', 'mihomo-alpha', 'mihomo-smart']
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
for (const executable of mihomoExecutables) {
|
||||
try {
|
||||
const { stdout } = await execPromise(
|
||||
`chcp 65001 >nul 2>&1 && tasklist /FI "IMAGENAME eq ${executable}" /FO CSV`,
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
const lines = stdout.split('\n').filter((line) => line.includes(executable))
|
||||
|
||||
if (lines.length > 0) {
|
||||
managerLogger.info(`Found ${lines.length} ${executable} processes running`)
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.split(',')
|
||||
if (parts.length >= 2) {
|
||||
const pid = parts[1].replace(/"/g, '').trim()
|
||||
try {
|
||||
const { stdout: processInfo } = await execPromise(
|
||||
`powershell -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Process -Id ${pid} | Select-Object Name,Id,Path,CommandLine | ConvertTo-Json"`,
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
const processJson = JSON.parse(processInfo)
|
||||
managerLogger.info(`Process ${pid} info: ${processInfo.substring(0, 200)}`)
|
||||
|
||||
if (processJson.Name.includes('mihomo') && processJson.Path === null) {
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
managerLogger.info(`Cannot get info for process ${pid}, might be high privilege`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
managerLogger.error(`Failed to check ${executable} processes`, error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let foundProcesses = false
|
||||
|
||||
for (const executable of mihomoExecutables) {
|
||||
try {
|
||||
const { stdout } = await execPromise(`ps aux | grep ${executable} | grep -v grep`)
|
||||
const lines = stdout.split('\n').filter((line) => line.trim() && line.includes(executable))
|
||||
|
||||
if (lines.length > 0) {
|
||||
foundProcesses = true
|
||||
managerLogger.info(`Found ${lines.length} ${executable} processes running`)
|
||||
|
||||
for (const line of lines) {
|
||||
const parts = line.trim().split(/\s+/)
|
||||
if (parts.length >= 1) {
|
||||
const user = parts[0]
|
||||
managerLogger.info(`${executable} process running as user: ${user}`)
|
||||
|
||||
if (user === 'root') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundProcesses) {
|
||||
managerLogger.info('No mihomo processes found running')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
managerLogger.error('Failed to check high privilege mihomo process', error)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export async function grantTunPermissions(): Promise<void> {
|
||||
const { core = 'mihomo' } = await getAppConfig()
|
||||
|
||||
if (!isValidCoreName(core)) {
|
||||
throw new Error(`Invalid core name: ${core}. Allowed values: ${ALLOWED_CORES.join(', ')}`)
|
||||
}
|
||||
|
||||
const corePath = mihomoCorePath(core)
|
||||
validateCorePath(corePath)
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
const escapedPath = shellEscape(corePath)
|
||||
const script = `do shell script "chown root:admin ${escapedPath} && chmod +sx ${escapedPath}" with administrator privileges`
|
||||
await execFilePromise('osascript', ['-e', script])
|
||||
}
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
await execFilePromise('pkexec', ['chown', 'root:root', corePath])
|
||||
await execFilePromise('pkexec', ['chmod', '+sx', corePath])
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
throw new Error('Windows platform requires running as administrator')
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartAsAdmin(forTun: boolean = true): Promise<void> {
|
||||
if (process.platform !== 'win32') {
|
||||
throw new Error('This function is only available on Windows')
|
||||
}
|
||||
|
||||
const exePath = process.execPath
|
||||
const args = process.argv.slice(1)
|
||||
const restartArgs = forTun ? [...args, '--admin-restart-for-tun'] : args
|
||||
|
||||
try {
|
||||
const escapedExePath = exePath.replace(/'/g, "''")
|
||||
const argsString = restartArgs.map((arg) => arg.replace(/'/g, "''")).join("', '")
|
||||
|
||||
const command =
|
||||
restartArgs.length > 0
|
||||
? `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -ArgumentList '${argsString}' -Verb RunAs"`
|
||||
: `powershell -NoProfile -Command "Start-Process -FilePath '${escapedExePath}' -Verb RunAs"`
|
||||
|
||||
managerLogger.info('Restarting as administrator with command', command)
|
||||
|
||||
exec(command, { windowsHide: true }, (error, _stdout, stderr) => {
|
||||
if (error) {
|
||||
managerLogger.error('PowerShell execution error', error)
|
||||
managerLogger.error('stderr', stderr)
|
||||
} else {
|
||||
managerLogger.info('PowerShell command executed successfully')
|
||||
}
|
||||
})
|
||||
|
||||
app.quit()
|
||||
} catch (error) {
|
||||
managerLogger.error('Failed to restart as administrator', error)
|
||||
throw new Error(`Failed to restart as administrator: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestTunPermissions(): Promise<void> {
|
||||
if (process.platform === 'win32') {
|
||||
await restartAsAdmin()
|
||||
} else {
|
||||
const hasPermissions = await checkMihomoCorePermissions()
|
||||
if (!hasPermissions) {
|
||||
await grantTunPermissions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function showTunPermissionDialog(): Promise<boolean> {
|
||||
managerLogger.info('Preparing TUN permission dialog...')
|
||||
|
||||
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') || '取消'
|
||||
|
||||
const choice = dialog.showMessageBoxSync({
|
||||
type: 'warning',
|
||||
title,
|
||||
message,
|
||||
buttons: [confirmText, cancelText],
|
||||
defaultId: 0,
|
||||
cancelId: 1
|
||||
})
|
||||
|
||||
managerLogger.info(`TUN permission dialog choice: ${choice}`)
|
||||
return choice === 0
|
||||
}
|
||||
|
||||
export async function showErrorDialog(title: string, message: string): Promise<void> {
|
||||
const okText = i18next.t('common.confirm') || '确认'
|
||||
|
||||
dialog.showMessageBoxSync({
|
||||
type: 'error',
|
||||
title,
|
||||
message,
|
||||
buttons: [okText],
|
||||
defaultId: 0
|
||||
})
|
||||
}
|
||||
|
||||
export async function validateTunPermissionsOnStartup(_restartCore: () => Promise<void>): Promise<void> {
|
||||
const { getControledMihomoConfig } = await import('../config')
|
||||
const { tun } = await getControledMihomoConfig()
|
||||
|
||||
if (!tun?.enable) {
|
||||
return
|
||||
}
|
||||
|
||||
const hasPermissions = await checkMihomoCorePermissions()
|
||||
|
||||
if (!hasPermissions) {
|
||||
managerLogger.warn('TUN is enabled but insufficient permissions detected, prompting user...')
|
||||
const confirmed = await showTunPermissionDialog()
|
||||
if (confirmed) {
|
||||
await restartAsAdmin()
|
||||
return
|
||||
}
|
||||
|
||||
managerLogger.warn('User declined admin restart, auto-disabling TUN...')
|
||||
await patchControledMihomoConfig({ tun: { enable: false } })
|
||||
|
||||
const { mainWindow } = await import('../index')
|
||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
|
||||
managerLogger.info('TUN auto-disabled due to insufficient permissions')
|
||||
} else {
|
||||
managerLogger.info('TUN permissions validated successfully')
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAdminRestartForTun(restartCore: () => Promise<void>): Promise<void> {
|
||||
if (process.argv.includes('--admin-restart-for-tun')) {
|
||||
managerLogger.info('Detected admin restart for TUN mode, auto-enabling TUN...')
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
const hasAdminPrivileges = await checkAdminPrivileges()
|
||||
if (hasAdminPrivileges) {
|
||||
await patchControledMihomoConfig({ tun: { enable: true }, dns: { enable: true } })
|
||||
|
||||
const { checkAutoRun, enableAutoRun } = await import('../sys/autoRun')
|
||||
const autoRunEnabled = await checkAutoRun()
|
||||
if (autoRunEnabled) {
|
||||
await enableAutoRun()
|
||||
}
|
||||
|
||||
await restartCore()
|
||||
|
||||
managerLogger.info('TUN mode auto-enabled after admin restart')
|
||||
|
||||
const { mainWindow } = await import('../index')
|
||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
} else {
|
||||
managerLogger.warn('Admin restart detected but no admin privileges found')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
managerLogger.error('Failed to auto-enable TUN after admin restart', error)
|
||||
}
|
||||
} else {
|
||||
await validateTunPermissionsOnStartup(restartCore)
|
||||
}
|
||||
}
|
||||
|
||||
export function checkTunPermissions(): Promise<boolean> {
|
||||
return checkMihomoCorePermissions()
|
||||
}
|
||||
|
||||
export function manualGrantCorePermition(): Promise<void> {
|
||||
return grantTunPermissions()
|
||||
}
|
||||
139
src/main/core/process.ts
Normal file
139
src/main/core/process.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { rm } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import { managerLogger } from '../utils/logger'
|
||||
import { getAxios } from './mihomoApi'
|
||||
|
||||
const execPromise = promisify(exec)
|
||||
|
||||
// 常量
|
||||
const CORE_READY_MAX_RETRIES = 30
|
||||
const CORE_READY_RETRY_INTERVAL_MS = 500
|
||||
|
||||
export async function cleanupSocketFile(): Promise<void> {
|
||||
if (process.platform === 'win32') {
|
||||
await cleanupWindowsNamedPipes()
|
||||
} else {
|
||||
await cleanupUnixSockets()
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupWindowsNamedPipes(): Promise<void> {
|
||||
try {
|
||||
try {
|
||||
const { stdout } = await execPromise(
|
||||
`powershell -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Process | Where-Object {$_.ProcessName -like '*mihomo*'} | Select-Object Id,ProcessName | ConvertTo-Json"`,
|
||||
{ encoding: 'utf8' }
|
||||
)
|
||||
|
||||
if (stdout.trim()) {
|
||||
managerLogger.info(`Found potential pipe-blocking processes: ${stdout}`)
|
||||
|
||||
try {
|
||||
const processes = JSON.parse(stdout)
|
||||
const processArray = Array.isArray(processes) ? processes : [processes]
|
||||
|
||||
for (const proc of processArray) {
|
||||
const pid = proc.Id
|
||||
if (pid && pid !== process.pid) {
|
||||
await terminateProcess(pid)
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
managerLogger.warn('Failed to parse process list JSON:', parseError)
|
||||
await fallbackTextParsing(stdout)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
managerLogger.warn('Failed to check mihomo processes:', error)
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
} catch (error) {
|
||||
managerLogger.error('Windows named pipe cleanup failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function terminateProcess(pid: number): Promise<void> {
|
||||
try {
|
||||
process.kill(pid, 0)
|
||||
process.kill(pid, 'SIGTERM')
|
||||
managerLogger.info(`Terminated process ${pid} to free pipe`)
|
||||
} catch (error: unknown) {
|
||||
if ((error as { code?: string })?.code !== 'ESRCH') {
|
||||
managerLogger.warn(`Failed to terminate process ${pid}:`, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fallbackTextParsing(stdout: string): Promise<void> {
|
||||
const lines = stdout.split('\n').filter((line) => line.includes('mihomo'))
|
||||
for (const line of lines) {
|
||||
const match = line.match(/(\d+)/)
|
||||
if (match) {
|
||||
const pid = parseInt(match[1])
|
||||
if (pid !== process.pid) {
|
||||
await terminateProcess(pid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupUnixSockets(): Promise<void> {
|
||||
try {
|
||||
const socketPaths = [
|
||||
'/tmp/mihomo-party.sock',
|
||||
'/tmp/mihomo-party-admin.sock',
|
||||
`/tmp/mihomo-party-${process.getuid?.() || 'user'}.sock`
|
||||
]
|
||||
|
||||
for (const socketPath of socketPaths) {
|
||||
try {
|
||||
if (existsSync(socketPath)) {
|
||||
await rm(socketPath)
|
||||
managerLogger.info(`Cleaned up socket file: ${socketPath}`)
|
||||
}
|
||||
} catch (error) {
|
||||
managerLogger.warn(`Failed to cleanup socket file ${socketPath}:`, error)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
managerLogger.error('Unix socket cleanup failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateWindowsPipeAccess(pipePath: string): Promise<void> {
|
||||
try {
|
||||
managerLogger.info(`Validating pipe access for: ${pipePath}`)
|
||||
managerLogger.info(`Pipe validation completed for: ${pipePath}`)
|
||||
} catch (error) {
|
||||
managerLogger.error('Windows pipe validation failed:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForCoreReady(): Promise<void> {
|
||||
for (let i = 0; i < CORE_READY_MAX_RETRIES; i++) {
|
||||
try {
|
||||
const axios = await getAxios(true)
|
||||
await axios.get('/')
|
||||
managerLogger.info(
|
||||
`Core ready after ${i + 1} attempts (${(i + 1) * CORE_READY_RETRY_INTERVAL_MS}ms)`
|
||||
)
|
||||
return
|
||||
} catch {
|
||||
if (i === 0) {
|
||||
managerLogger.info('Waiting for core to be ready...')
|
||||
}
|
||||
|
||||
if (i === CORE_READY_MAX_RETRIES - 1) {
|
||||
managerLogger.warn(
|
||||
`Core not ready after ${CORE_READY_MAX_RETRIES} attempts, proceeding anyway`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, CORE_READY_RETRY_INTERVAL_MS))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user