fix: improve gateway PID recovery and port detection (#660)

- Refactor port detection into reusable getListeningPids/killListeningPids
- Add ss command fallback when lsof is unavailable
- Extract readPidFile for cleaner PID file reading
- Remove unnecessary PowerShell candidate from Windows shell detection
- Add PID validity check in gateway_state.json fallback (Number.isFinite + > 0)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-05-12 20:53:21 +08:00
committed by GitHub
parent c987448f01
commit 8b57c4a278
2 changed files with 48 additions and 35 deletions
+45 -32
View File
@@ -64,7 +64,6 @@ function getWindowsShell() {
const systemRoot = process.env.SystemRoot || 'C:\\Windows'
const candidates = [
process.env.ComSpec,
join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
join(systemRoot, 'System32', 'cmd.exe'),
].filter(Boolean)
@@ -141,11 +140,12 @@ function getPort() {
function getListeningPids(port) {
if (!port || isNaN(port)) return []
const uniquePids = (pids) => [...new Set(pids.filter(pid => Number.isFinite(pid)))]
try {
if (process.platform === 'win32') {
const out = execSync('netstat -aon -p tcp', { encoding: 'utf-8' })
return [...new Set(out.split('\n')
return uniquePids(out.split('\n')
.map(line => line.trim())
.filter(line => line.includes('LISTENING'))
.map(line => line.split(/\s+/))
@@ -154,15 +154,38 @@ function getListeningPids(port) {
const listenPort = parseInt(address.split(':').pop(), 10)
return listenPort === port
})
.map(parts => parseInt(parts[parts.length - 1], 10))
.filter(pid => Number.isFinite(pid)))]
.map(parts => parseInt(parts[parts.length - 1], 10)))
}
const out = execSync(`lsof -tiTCP:${port} -sTCP:LISTEN`, { encoding: 'utf-8' }).trim()
return [...new Set(out.split('\n').map(pid => parseInt(pid, 10)).filter(pid => Number.isFinite(pid)))]
} catch {
return []
}
try {
const out = execSync(`lsof -tiTCP:${port} -sTCP:LISTEN`, { encoding: 'utf-8' }).trim()
return uniquePids(out.split('\n').map(pid => parseInt(pid, 10)))
} catch {}
try {
const out = execSync(`ss -ltnp 'sport = :${port}'`, { encoding: 'utf-8' })
return uniquePids(out.split('\n')
.map(line => line.match(/pid=(\d+)/)?.[1])
.map(pid => parseInt(pid || '', 10)))
} catch {}
return []
}
function killListeningPids(port, pids = getListeningPids(port)) {
if (pids.length === 0) return
console.log(` ⚠ Port ${port} is in use by PID(s): ${pids.join(' ')}, killing...`)
try {
if (process.platform === 'win32') {
execSync(`taskkill /F /PID ${pids.join(' /PID ')}`, { encoding: 'utf-8' })
} else {
execSync(`kill -9 ${pids.join(' ')}`, { encoding: 'utf-8' })
}
} catch {}
}
function recoverPidFromPort() {
@@ -177,18 +200,25 @@ function recoverPidFromPort() {
return null
}
function getPid() {
const recovered = recoverPidFromPort()
if (recovered) return recovered
function readPidFile() {
try {
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim())
if (pid && isRunning(pid)) return pid
return Number.isFinite(pid) ? pid : null
} catch {}
return null
}
function getPid() {
const pid = readPidFile()
if (pid) {
if (isRunning(pid)) return pid
removePid()
}
return recoverPidFromPort()
}
function isRunning(pid) {
try {
process.kill(pid, 0)
@@ -216,29 +246,12 @@ function startDaemon(port) {
removePid()
// Check if port is already in use
try {
const isWin = process.platform === 'win32'
let pids = ''
if (isWin) {
const out = execSync(`netstat -aon | findstr :${port}`, { encoding: 'utf-8' }).trim()
const lines = out.split('\n').filter(l => l.includes('LISTENING'))
pids = [...new Set(lines.map(l => l.trim().split(/\s+/).pop()).filter(Boolean))].join(' ')
} else {
pids = execSync(`lsof -ti:${port}`, { encoding: 'utf-8' }).trim()
}
if (pids) {
console.log(` ⚠ Port ${port} is in use by PID(s): ${pids}, killing...`)
if (isWin) {
execSync(`taskkill /F /PID ${pids.split(' ').join(' /PID ')}`, { encoding: 'utf-8' })
} else {
execSync(`kill -9 ${pids}`, { encoding: 'utf-8' })
}
const occupied = getListeningPids(port)
if (occupied.length) {
killListeningPids(port, occupied)
// Brief wait for port to be released
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 500)
}
} catch {
// Port is free
}
mkdirSync(PID_DIR, { recursive: true })
@@ -221,7 +221,7 @@ export class GatewayManager {
const data = JSON.parse(content)
const pid = typeof data.pid === 'number' ? data.pid : parseInt(data.pid, 10) || null
const state = data?.gateway_state
return pid && (state === 'running' || state === 'starting') ? pid : null
return pid && Number.isFinite(pid) && pid > 0 && (state === 'running' || state === 'starting') ? pid : null
} catch {
return null
}