release: playwriter@0.0.61
Simplify Unix port killing by replacing shell pipelines with direct lsof -t and process.kill().
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.0.61
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- **Simplified Unix port killing**: Replaced shell pipeline approach (lsof/grep/awk/xargs) with direct `lsof -t` for PID discovery and `process.kill()` for termination. This eliminates spawn overhead and makes the code more maintainable while improving reliability.
|
||||||
|
|
||||||
## 0.0.60
|
## 0.0.60
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "playwriter",
|
"name": "playwriter",
|
||||||
"description": "",
|
"description": "",
|
||||||
"version": "0.0.60",
|
"version": "0.0.61",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
+45
-72
@@ -4,12 +4,13 @@
|
|||||||
* This mirrors the approach used by the `kill-port-process` npm package:
|
* This mirrors the approach used by the `kill-port-process` npm package:
|
||||||
* https://github.com/hilleer/kill-port-process
|
* https://github.com/hilleer/kill-port-process
|
||||||
*
|
*
|
||||||
* Important fix (ported from https://github.com/hilleer/kill-port-process/pull/199):
|
* Notes:
|
||||||
* do NOT pipe `xargs.stdout` into `process.stdin` (stdin is not writable and can
|
* - Windows: discover listeners via PowerShell/netstat and kill via taskkill.
|
||||||
* throw `dest.end is not a function`). We simply don't pipe `xargs` stdout.
|
* - Unix: discover listeners via lsof (preferred) or fuser (fallback) and kill via
|
||||||
|
* process.kill(). This intentionally avoids shell pipelines (grep/awk/xargs).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execFile, spawn } from 'node:child_process'
|
import { execFile } from 'node:child_process'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
import { promisify } from 'node:util'
|
import { promisify } from 'node:util'
|
||||||
|
|
||||||
@@ -22,16 +23,10 @@ function isValidPort(port: number): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parsePids(output: string): number[] {
|
function parsePids(output: string): number[] {
|
||||||
const pids = output
|
const matches = output.match(/\d+/g) || []
|
||||||
.split(/\r?\n/g)
|
const pids = matches
|
||||||
.map((line) => {
|
.map((text) => {
|
||||||
return line.trim()
|
return Number(text)
|
||||||
})
|
|
||||||
.filter((line) => {
|
|
||||||
return Boolean(line)
|
|
||||||
})
|
|
||||||
.map((line) => {
|
|
||||||
return Number(line)
|
|
||||||
})
|
})
|
||||||
.filter((pid) => {
|
.filter((pid) => {
|
||||||
return Number.isInteger(pid) && pid > 0
|
return Number.isInteger(pid) && pid > 0
|
||||||
@@ -115,6 +110,15 @@ async function getPidsForPortWindows(port: number): Promise<number[]> {
|
|||||||
|
|
||||||
async function getPidsForPortUnix(port: number): Promise<number[]> {
|
async function getPidsForPortUnix(port: number): Promise<number[]> {
|
||||||
try {
|
try {
|
||||||
|
// Prefer lsof's built-in filtering and pid-only output.
|
||||||
|
const { stdout } = await execFileAsync('lsof', ['-n', '-P', '-i', `TCP:${port}`, '-sTCP:LISTEN', '-t'])
|
||||||
|
const pids = parsePids(stdout)
|
||||||
|
return pids
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Compatibility fallback: some environments may have lsof but not support
|
||||||
|
// the exact flags above. Parse the tabular output.
|
||||||
const { stdout } = await execFileAsync('lsof', ['-i', `tcp:${port}`])
|
const { stdout } = await execFileAsync('lsof', ['-i', `tcp:${port}`])
|
||||||
const pids = parseUnixLsofPids(stdout)
|
const pids = parseUnixLsofPids(stdout)
|
||||||
if (pids.length > 0) {
|
if (pids.length > 0) {
|
||||||
@@ -125,7 +129,9 @@ async function getPidsForPortUnix(port: number): Promise<number[]> {
|
|||||||
// Fallback for Linux environments that may not have `lsof`.
|
// Fallback for Linux environments that may not have `lsof`.
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync('fuser', [`${port}/tcp`])
|
const { stdout } = await execFileAsync('fuser', [`${port}/tcp`])
|
||||||
return parsePids(stdout)
|
return parsePids(stdout).filter((pid) => {
|
||||||
|
return pid !== port
|
||||||
|
})
|
||||||
} catch {
|
} catch {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -160,62 +166,6 @@ async function terminatePidWindows(pid: number): Promise<void> {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unixKillPortUsingPipes({ port, signal }: { port: number; signal: KillPortSignal }): Promise<void> {
|
|
||||||
const killCommand = signal === 'SIGTERM' ? '-15' : '-9'
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
|
||||||
const lsof = spawn('lsof', ['-i', `tcp:${port}`], { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
||||||
const grep = spawn('grep', ['LISTEN'], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
||||||
const awk = spawn('awk', ['{print $2}'], { stdio: ['pipe', 'pipe', 'pipe'] })
|
|
||||||
const xargs = spawn('xargs', ['kill', killCommand], { stdio: ['pipe', 'ignore', 'pipe'] })
|
|
||||||
|
|
||||||
if (!lsof.stdout || !grep.stdin || !grep.stdout || !awk.stdin || !awk.stdout || !xargs.stdin) {
|
|
||||||
reject(new Error('Failed to create stdio pipes for kill-port process chain'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lsof.stdout.pipe(grep.stdin)
|
|
||||||
grep.stdout.pipe(awk.stdin)
|
|
||||||
awk.stdout.pipe(xargs.stdin)
|
|
||||||
|
|
||||||
// IMPORTANT: do not pipe xargs stdout anywhere.
|
|
||||||
|
|
||||||
const stderrChunks: string[] = []
|
|
||||||
const collectStderr = (name: string, stream: NodeJS.ReadableStream | null) => {
|
|
||||||
stream?.on('data', (data: unknown) => {
|
|
||||||
const text = data instanceof Buffer ? data.toString() : String(data)
|
|
||||||
stderrChunks.push(`${name} - ${text}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
collectStderr('lsof', lsof.stderr)
|
|
||||||
collectStderr('grep', grep.stderr)
|
|
||||||
collectStderr('awk', awk.stderr)
|
|
||||||
collectStderr('xargs', xargs.stderr)
|
|
||||||
|
|
||||||
const onError = (name: string) => {
|
|
||||||
return (error: unknown) => {
|
|
||||||
reject(new Error(`kill-port process error in ${name}`, { cause: toError(error) }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lsof.on('error', onError('lsof'))
|
|
||||||
grep.on('error', onError('grep'))
|
|
||||||
awk.on('error', onError('awk'))
|
|
||||||
xargs.on('error', onError('xargs'))
|
|
||||||
|
|
||||||
xargs.on('close', (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const extra = stderrChunks.length > 0 ? `\n${stderrChunks.join('\n')}` : ''
|
|
||||||
reject(new Error(`Failed to kill process on port ${port} (exit code ${code}).${extra}`))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Kill any listening process bound to the provided TCP port.
|
* Kill any listening process bound to the provided TCP port.
|
||||||
*/
|
*/
|
||||||
@@ -244,5 +194,28 @@ export async function killPortProcess({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await unixKillPortUsingPipes({ port, signal })
|
const pids = await getListeningPidsForPort({ port })
|
||||||
|
const currentPid = process.pid
|
||||||
|
const targetPids = pids.filter((pid) => {
|
||||||
|
return pid !== currentPid
|
||||||
|
})
|
||||||
|
|
||||||
|
if (targetPids.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
targetPids.map(async (pid) => {
|
||||||
|
try {
|
||||||
|
process.kill(pid, signal)
|
||||||
|
} catch (error) {
|
||||||
|
const err = toError(error)
|
||||||
|
// If the process already exited between discovery and kill, ignore.
|
||||||
|
if ((err as NodeJS.ErrnoException).code === 'ESRCH') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to kill pid ${pid} on port ${port}`, { cause: err })
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user