From 1962b5be3c46427db2e320812140469efbcae318 Mon Sep 17 00:00:00 2001 From: Andrey Lushnikov Date: Wed, 12 Apr 2023 20:16:42 +0000 Subject: [PATCH] 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). --- tests/config/commonFixtures.ts | 60 ++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/tests/config/commonFixtures.ts b/tests/config/commonFixtures.ts index a904506842..951b7763ab 100644 --- a/tests/config/commonFixtures.ts +++ b/tests/config/commonFixtures.ts @@ -18,6 +18,7 @@ import type { Fixtures } from '@playwright/test'; import type { ChildProcess } from 'child_process'; import { execSync, spawn } from 'child_process'; import net from 'net'; +import fs from 'fs'; import { stripAnsi } from './utils'; type TestChildParams = { @@ -32,28 +33,59 @@ import childProcess from 'child_process'; type ProcessData = { pid: number, // process ID - pgid: number, // process groupd ID + pgrp: number, // process groupd ID children: Set, // 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(/^(?\d+)\s+\((?.*)\)\s+(?R|S|D|Z|T|t|W|X|x|K|W|P)\s+(?\d+)\s+(?\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//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 lines = processTree.stdout.toString().trim().split('\n'); - - const pidToProcess = new Map(); - const edges: { pid: number, ppid: number }[] = []; 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". - if (isNaN(pid) || isNaN(pgid) || isNaN(ppid)) + if (isNaN(pid) || isNaN(pgrp) || isNaN(ppid)) continue; - pidToProcess.set(pid, { pid, pgid, children: new Set() }); - edges.push({ pid, ppid }); + result.push({ pid, ppid, pgrp }); } - 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(); + 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 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) parent.children.add(child); } @@ -151,13 +183,13 @@ export class TestChildProcess { const rootProcess = buildProcessTreePosix(this.process.pid); const descendantProcessGroups = (function flatten(processData: ProcessData, result: Set = new Set()) { // 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)); return result; })(rootProcess); - for (const pgid of descendantProcessGroups) { + for (const pgrp of descendantProcessGroups) { try { - process.kill(-pgid, 'SIGKILL'); + process.kill(-pgrp, 'SIGKILL'); } catch (e) { // the process might have already stopped }