fix: prevent killing unintended processes due to PID reuse in lightweight mode

This commit is contained in:
moon
2026-07-13 04:04:09 +08:00
committed by zjdndjf
parent 84b19e3304
commit 196fdcbf86
3 changed files with 71 additions and 18 deletions

View File

@@ -55,7 +55,8 @@ import {
cleanupSocketFile,
cleanupWindowsNamedPipes,
validateWindowsPipeAccess,
waitForCoreReady
waitForCoreReady,
verifyProcessOwner
} from './process'
import { setPublicDNS, recoverDNS } from './dns'
@@ -81,6 +82,7 @@ const execFilePromise = promisify(execFile)
const ctlParam = process.platform === 'win32' ? '-ext-ctl-pipe' : '-ext-ctl-unix'
const coreHookTimeout = 30000
const automaticRestartDelay = 750
const coreProcessNames = ['mihomo', 'mihomo-alpha', 'mihomo-smart'] as const
// 核心进程状态
interface CoreProcessWatchdog {
@@ -326,24 +328,27 @@ async function stopPidFileCore(): Promise<void> {
const pid = parseInt(pidString.trim())
if (!isNaN(pid)) {
try {
process.kill(pid, 0)
process.kill(pid, 'SIGINT')
const deadline = Date.now() + 500
let stillRunning = true
while (stillRunning && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50))
try {
process.kill(pid, 0)
} catch {
stillRunning = false
if (await verifyProcessOwner(pid, coreProcessNames)) {
process.kill(pid, 'SIGINT')
const deadline = Date.now() + 500
let stillRunning = true
while (stillRunning && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50))
try {
process.kill(pid, 0)
} catch {
stillRunning = false
}
}
}
if (stillRunning) {
try {
process.kill(pid, 'SIGKILL')
} catch {
// ignore
if (stillRunning) {
try {
process.kill(pid, 'SIGKILL')
} catch {
// ignore
}
}
} else {
managerLogger.info(`PID ${pid} is not a known mihomo process, skipping kill`)
}
} catch {
// ignore

View File

@@ -169,3 +169,45 @@ export async function waitForCoreReady(): Promise<void> {
}
}
}
function normalizeProcessName(name: string): string {
return name
.trim()
.replace(/\.exe$/i, '')
.toLowerCase()
}
export async function verifyProcessOwner(
pid: number,
expectedNames: readonly string[]
): Promise<boolean> {
try {
process.kill(pid, 0)
} catch {
return false
}
try {
let processName = ''
if (process.platform === 'win32') {
const { stdout } = await execFilePromise(
'tasklist',
['/FI', `PID eq ${pid}`, '/FO', 'CSV', '/NH'],
{ windowsHide: true, timeout: 1000 }
)
const match = stdout.match(/^"([^"]+)","(\d+)"/m)
if (!match || parseInt(match[2], 10) !== pid) return false
processName = match[1]
} else {
const { stdout } = await execFilePromise('ps', ['-p', `${pid}`, '-o', 'comm='], {
timeout: 1000
})
processName = stdout.trim().split(/\r?\n/, 1)[0] || ''
}
const normalizedName = normalizeProcessName(processName)
return expectedNames.some((name) => normalizeProcessName(name) === normalizedName)
} catch {
return false
}
}

View File

@@ -4,6 +4,7 @@ import { existsSync } from 'fs'
import { readFile, rm, writeFile } from 'fs/promises'
import { dataDir, resourcesFilesDir } from '../utils/dirs'
import { getAppConfig } from '../config'
import { verifyProcessOwner } from '../core/process'
let child: ChildProcess
@@ -12,7 +13,12 @@ export async function startMonitor(detached = false): Promise<void> {
if (existsSync(path.join(dataDir(), 'monitor.pid'))) {
const pid = parseInt(await readFile(path.join(dataDir(), 'monitor.pid'), 'utf-8'))
try {
process.kill(pid, 'SIGINT')
if (!isNaN(pid)) {
const isOwner = await verifyProcessOwner(pid, ['TrafficMonitor'])
if (isOwner) {
process.kill(pid, 'SIGINT')
}
}
} catch {
// ignore
} finally {