test(pty): scope the group-reap test's ps to its own PTY (#34669)

* test(pty): scope the group-reap test's ps to its own PTY

The helper listed every host process with full command lines, which
overflows execFileSync's 1MB buffer on busy machines (ENOBUFS). Discover
the tagged job on the test PTY's terminal and recheck survivors by their
captured pids, so the output stays small by construction and a job that
lost the hung-up terminal is still caught.

Refs #34410

* test(pty): clean up a job that discovery never captured

Refs #34410
This commit is contained in:
Neil
2026-10-27 19:07:07 -07:00
committed by GitHub
parent 61337dfb33
commit 4fe3d7624b
@@ -2,15 +2,26 @@ import { execFileSync } from 'node:child_process'
import * as pty from 'node-pty'
import { expect, it } from 'vitest'
import { forceKillPosixPtyProcessGroups } from './posix-pty-process-groups'
import { readPtsName } from './node-pty-pts-name'
type TaggedProcess = { pid: number; pgid: number }
const itOnPosix = process.platform === 'win32' ? it.skip : it
function findTaggedProcesses(token: string): TaggedProcess[] {
const output = execFileSync('ps', ['-axo', 'pid=,pgid=,state=,command='], {
encoding: 'utf8'
})
// Select processes by terminal or pid: a whole-host listing with command lines overflows the buffer on busy machines.
function findTaggedProcesses(selection: string[], token: string): TaggedProcess[] {
let output: string
try {
output = execFileSync('ps', [...selection, '-o', 'pid=,pgid=,state=,command='], {
encoding: 'utf8'
})
} catch (error) {
// ps exits 1 when nothing matches the selection.
if (error instanceof Error && 'status' in error && error.status === 1) {
return []
}
throw error
}
const matches: TaggedProcess[] = []
for (const line of output.split(/\r?\n/)) {
if (!line.includes(token)) {
@@ -24,6 +35,14 @@ function findTaggedProcesses(token: string): TaggedProcess[] {
return matches
}
// Survivors lose the hung-up terminal, so recheck them by pid; the token rejects reused pids.
function findSurvivors(tagged: TaggedProcess[], token: string): TaggedProcess[] {
if (tagged.length === 0) {
return []
}
return findTaggedProcesses(['-p', tagged.map((process) => process.pid).join(',')], token)
}
async function waitFor(
predicate: () => boolean,
timeoutMs = 5_000,
@@ -38,8 +57,10 @@ async function waitFor(
}
}
function cleanupTaggedProcesses(token: string): void {
for (const pgid of new Set(findTaggedProcesses(token).map((process) => process.pgid))) {
// Runs before the leader is killed, so a job discovery never captured is still on the terminal.
function cleanupTaggedProcesses(tty: string, tagged: TaggedProcess[], token: string): void {
const remaining = [...findTaggedProcesses(['-t', tty], token), ...findSurvivors(tagged, token)]
for (const pgid of new Set(remaining.map((process) => process.pgid))) {
try {
process.kill(-pgid, 'SIGKILL')
} catch {
@@ -64,12 +85,17 @@ itOnPosix(
env: process.env as Record<string, string>
})
const exited = new Promise<void>((resolve) => proc.onExit(() => resolve()))
const tty = readPtsName(proc)?.replace(/^\/dev\//, '')
if (!tty) {
throw new Error('node-pty did not expose the PTY slave name')
}
let tagged: TaggedProcess[] = []
try {
proc.write(`/bin/sh -lc 'trap "" HUP TERM; while :; do sleep 60; done' ${token}\n`)
await waitFor(() => findTaggedProcesses(token).length > 0)
await waitFor(() => findTaggedProcesses(['-t', tty], token).length > 0)
const tagged = findTaggedProcesses(token)
tagged = findTaggedProcesses(['-t', tty], token)
const rootProcessGroup = readProcessGroup(proc.pid)
expect(tagged.some((process) => process.pgid !== rootProcessGroup)).toBe(true)
@@ -80,13 +106,16 @@ itOnPosix(
setTimeout(() => reject(new Error('PTY leader did not exit')), 5_000)
)
])
await waitFor(() => findTaggedProcesses(token).length === 0)
await waitFor(() => findSurvivors(tagged, token).length === 0)
} finally {
cleanupTaggedProcesses(token)
try {
proc.kill('SIGKILL')
} catch {
// node-pty may already have reaped and disposed the leader.
cleanupTaggedProcesses(tty, tagged, token)
} finally {
try {
proc.kill('SIGKILL')
} catch {
// node-pty may already have reaped and disposed the leader.
}
}
}
},