test: make sure process killing logic works without ps on Linux (#22366)

The bare-bones `debian` distribution we use for testing doesn't have
`ps`. This patch switches to reading `/proc` file system directly on
Linux instead of relying on `ps`.

Performance measurements for the 20000 active processes on Debian Docker
container, tested on my M1 Max Mac:
- the `ps -eo pid,ppid,pgid` call + parsing takes 293ms
- the manual synchronous `/proc` traversal + parsing takes 326ms

So this is ~10% perf penalty.

Drive-by: rename `pgid` into `pgrp` so that it stands out from `pid`
(process ID) and `ppid` (parent process ID).
This commit is contained in:
Andrey Lushnikov 2023-04-12 20:16:42 +00:00 committed by GitHub
parent 336d2114c8
commit 1962b5be3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -18,6 +18,7 @@ import type { Fixtures } from '@playwright/test';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
import { execSync, spawn } from 'child_process'; import { execSync, spawn } from 'child_process';
import net from 'net'; import net from 'net';
import fs from 'fs';
import { stripAnsi } from './utils'; import { stripAnsi } from './utils';
type TestChildParams = { type TestChildParams = {
@ -32,28 +33,59 @@ import childProcess from 'child_process';
type ProcessData = { type ProcessData = {
pid: number, // process ID pid: number, // process ID
pgid: number, // process groupd ID pgrp: number, // process groupd ID
children: Set<ProcessData>, // direct children of the process children: Set<ProcessData>, // direct children of the process
}; };
function buildProcessTreePosix(pid: number): ProcessData { function readAllProcessesLinux(): { pid: number, ppid: number, pgrp: number }[] {
const result: {pid: number, ppid: number, pgrp: number}[] = [];
for (const dir of fs.readdirSync('/proc')) {
const pid = +dir;
if (isNaN(pid))
continue;
try {
const statFile = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
// Format of /proc/*/stat is described https://man7.org/linux/man-pages/man5/proc.5.html
const match = statFile.match(/^(?<pid>\d+)\s+\((?<comm>.*)\)\s+(?<state>R|S|D|Z|T|t|W|X|x|K|W|P)\s+(?<ppid>\d+)\s+(?<pgrp>\d+)/);
if (match) {
result.push({
pid: +match.groups.pid,
ppid: +match.groups.ppid,
pgrp: +match.groups.pgrp,
});
}
} catch (e) {
// We don't have access to some /proc/<pid>/stat file.
}
}
return result;
}
function readAllProcessesMacOS(): { pid: number, ppid: number, pgrp: number }[] {
const result: {pid: number, ppid: number, pgrp: number}[] = [];
const processTree = childProcess.spawnSync('ps', ['-eo', 'pid,pgid,ppid']); const processTree = childProcess.spawnSync('ps', ['-eo', 'pid,pgid,ppid']);
const lines = processTree.stdout.toString().trim().split('\n'); const lines = processTree.stdout.toString().trim().split('\n');
const pidToProcess = new Map<number, ProcessData>();
const edges: { pid: number, ppid: number }[] = [];
for (const line of lines) { for (const line of lines) {
const [pid, pgid, ppid] = line.trim().split(/\s+/).map(token => +token); const [pid, pgrp, ppid] = line.trim().split(/\s+/).map(token => +token);
// On linux, the very first line of `ps` is the header with "PID PGID PPID". // On linux, the very first line of `ps` is the header with "PID PGID PPID".
if (isNaN(pid) || isNaN(pgid) || isNaN(ppid)) if (isNaN(pid) || isNaN(pgrp) || isNaN(ppid))
continue; continue;
pidToProcess.set(pid, { pid, pgid, children: new Set() }); result.push({ pid, ppid, pgrp });
edges.push({ pid, ppid });
} }
for (const { pid, ppid } of edges) { return result;
}
function buildProcessTreePosix(pid: number): ProcessData {
// Certain Linux distributions might not have `ps` installed.
const allProcesses = process.platform === 'darwin' ? readAllProcessesMacOS() : readAllProcessesLinux();
const pidToProcess = new Map<number, ProcessData>();
for (const { pid, pgrp } of allProcesses)
pidToProcess.set(pid, { pid, pgrp, children: new Set() });
for (const { pid, ppid } of allProcesses) {
const parent = pidToProcess.get(ppid); const parent = pidToProcess.get(ppid);
const child = pidToProcess.get(pid); const child = pidToProcess.get(pid);
// On POSIX, certain processes might not have parent (e.g. PID=1 and occasionally PID=2). // On POSIX, certain processes might not have parent (e.g. PID=1 and occasionally PID=2)
// or we might not have access to it proc info.
if (parent && child) if (parent && child)
parent.children.add(child); parent.children.add(child);
} }
@ -151,13 +183,13 @@ export class TestChildProcess {
const rootProcess = buildProcessTreePosix(this.process.pid); const rootProcess = buildProcessTreePosix(this.process.pid);
const descendantProcessGroups = (function flatten(processData: ProcessData, result: Set<number> = new Set()) { const descendantProcessGroups = (function flatten(processData: ProcessData, result: Set<number> = new Set()) {
// Process can nullify its own process group with `setpgid`. Use its PID instead. // Process can nullify its own process group with `setpgid`. Use its PID instead.
result.add(processData.pgid || processData.pid); result.add(processData.pgrp || processData.pid);
processData.children.forEach(child => flatten(child, result)); processData.children.forEach(child => flatten(child, result));
return result; return result;
})(rootProcess); })(rootProcess);
for (const pgid of descendantProcessGroups) { for (const pgrp of descendantProcessGroups) {
try { try {
process.kill(-pgid, 'SIGKILL'); process.kill(-pgrp, 'SIGKILL');
} catch (e) { } catch (e) {
// the process might have already stopped // the process might have already stopped
} }