|
| 1 | +// fallow-ignore-file unused-file |
| 2 | +// SCRATCH (w3-1824): traces every real signal a vitest fork sends to a foreign |
| 3 | +// pid, plus every `kill`/`pkill` subprocess it spawns. Appends NDJSON lines to |
| 4 | +// $W3_1824_KILL_TRACE. Never committed. |
| 5 | +import childProcess from 'node:child_process'; |
| 6 | +import fs from 'node:fs'; |
| 7 | +import module from 'node:module'; |
| 8 | +import util from 'node:util'; |
| 9 | +import { beforeAll, expect } from 'vitest'; |
| 10 | + |
| 11 | +const out = process.env.W3_1824_KILL_TRACE; |
| 12 | + |
| 13 | +function currentTest(): { file?: string; name?: string } { |
| 14 | + try { |
| 15 | + const state = expect.getState(); |
| 16 | + return { file: state.testPath, name: state.currentTestName }; |
| 17 | + } catch { |
| 18 | + return {}; |
| 19 | + } |
| 20 | +} |
| 21 | + |
| 22 | +function readPgid(): number | undefined { |
| 23 | + try { |
| 24 | + // /proc/self/stat: pid (comm) state ppid pgrp ... |
| 25 | + const stat = fs.readFileSync('/proc/self/stat', 'utf8'); |
| 26 | + const afterComm = stat.slice(stat.lastIndexOf(')') + 2).split(' '); |
| 27 | + return Number(afterComm[2]); |
| 28 | + } catch { |
| 29 | + return undefined; |
| 30 | + } |
| 31 | +} |
| 32 | + |
| 33 | +function record(entry: Record<string, unknown>): void { |
| 34 | + if (!out) return; |
| 35 | + const line = JSON.stringify({ |
| 36 | + ts: new Date().toISOString(), |
| 37 | + workerPid: process.pid, |
| 38 | + workerPpid: process.ppid, |
| 39 | + workerPgid: readPgid(), |
| 40 | + ...currentTest(), |
| 41 | + ...entry, |
| 42 | + }); |
| 43 | + fs.appendFileSync(out, `${line}\n`); |
| 44 | +} |
| 45 | + |
| 46 | +const realKill = process.kill.bind(process); |
| 47 | +process.kill = ((pid: number, signal?: string | number) => { |
| 48 | + // signal 0 is a liveness probe; ignore self-signals. |
| 49 | + if (signal !== 0 && pid !== process.pid) { |
| 50 | + record({ |
| 51 | + kind: 'process.kill', |
| 52 | + pid, |
| 53 | + signal: signal ?? 'SIGTERM', |
| 54 | + stack: new Error().stack?.split('\n').slice(2, 9).join(' | '), |
| 55 | + }); |
| 56 | + } |
| 57 | + return realKill(pid, signal as never); |
| 58 | +}) as typeof process.kill; |
| 59 | + |
| 60 | +const KILL_BINARIES = new Set(['kill', 'pkill', 'killall']); |
| 61 | + |
| 62 | +function traceSpawn(cmd: unknown, args: unknown): void { |
| 63 | + const command = String(cmd); |
| 64 | + if (!KILL_BINARIES.has(command.split('/').pop() ?? '')) return; |
| 65 | + record({ |
| 66 | + kind: 'spawn', |
| 67 | + command, |
| 68 | + args: Array.isArray(args) ? args : [], |
| 69 | + stack: new Error().stack?.split('\n').slice(3, 10).join(' | '), |
| 70 | + }); |
| 71 | +} |
| 72 | + |
| 73 | +for (const name of ['spawn', 'spawnSync', 'execFile', 'execFileSync'] as const) { |
| 74 | + const real = childProcess[name] as (...a: unknown[]) => unknown; |
| 75 | + const wrapped = (...a: unknown[]) => { |
| 76 | + traceSpawn(a[0], a[1]); |
| 77 | + return real(...a); |
| 78 | + }; |
| 79 | + // util.promisify(execFile) relies on the custom promisify symbol; keep it. |
| 80 | + const custom = (real as unknown as Record<symbol, unknown>)[util.promisify.custom]; |
| 81 | + if (custom) Object.defineProperty(wrapped, util.promisify.custom, { value: custom }); |
| 82 | + (childProcess as unknown as Record<string, unknown>)[name] = wrapped; |
| 83 | +} |
| 84 | + |
| 85 | +// Named ESM imports of the builtin (`import { spawn } from 'node:child_process'`) |
| 86 | +// only see the patched functions after the CJS exports are re-synced. |
| 87 | +module.syncBuiltinESMExports(); |
| 88 | +record({ kind: 'worker-start' }); |
| 89 | +beforeAll(() => { |
| 90 | + record({ kind: 'file-start' }); |
| 91 | +}); |
0 commit comments