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:
parent
336d2114c8
commit
1962b5be3c
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue