// SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2026 Bryan Joshua Pedini /** * Run-level leak detection (runner process). Throwing here makes `playwright test` exit non-zero * (verified with Playwright 1.63), so any leak fails the build: * - containers / networks / volumes labelled dpsapi.e2e=; * - live processes whose environment carries DPSAPI_E2E_PROC=; * - leftover per-test workspaces. * Leaked resources are cleaned up before failing. */ import fs from 'node:fs'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { LABEL_RUN, PROC_MARKER_ENV } from './env'; function docker(args: string[]): { code: number | null; out: string; err: string } { const r = spawnSync('docker', args, { encoding: 'utf8', timeout: 120_000 }); return { code: r.status, out: r.stdout ?? '', err: r.stderr ?? '' }; } function lines(s: string): string[] { return s.split('\n').map((l) => l.trim()).filter(Boolean); } /** PIDs of live processes whose environ contains PROC_MARKER_ENV=. */ export function markedProcesses(runId: string): Array<{ pid: number; cmdline: string }> { const marker = Buffer.from(`${PROC_MARKER_ENV}=${runId}\0`); const out: Array<{ pid: number; cmdline: string }> = []; for (const d of fs.readdirSync('/proc')) { if (!/^\d+$/.test(d) || Number(d) === process.pid) continue; try { const env = fs.readFileSync(`/proc/${d}/environ`); if (env.includes(marker)) { const stat = fs.readFileSync(`/proc/${d}/stat`, 'utf8'); if (/^\d+ \(.*\) Z /.test(stat)) continue; // zombie: already exited, waiting to be reaped out.push({ pid: Number(d), cmdline: fs.readFileSync(`/proc/${d}/cmdline`, 'utf8').split('\0').join(' ').trim() }); } } catch { /* gone or not ours */ } } return out; } export default async function globalTeardown(): Promise { const runId = process.env.DPSAPI_E2E_RUN_ID; const runDir = process.env.DPSAPI_E2E_RUN_DIR; if (!runId || !runDir) return; const leaks: string[] = []; // Containers const ps = docker(['ps', '-a', '--filter', `label=${LABEL_RUN}=${runId}`, '--format', '{{.Names}}']); if (ps.code !== 0) leaks.push(`cannot list containers: ${ps.err.trim()}`); const containers = lines(ps.out); if (containers.length) { leaks.push(`containers left behind: ${containers.join(', ')}`); docker(['rm', '-f', '-v', ...containers]); } // Networks and volumes (phase-2 suites may create them) for (const kind of ['network', 'volume'] as const) { const r = docker([kind, 'ls', '-q', '--filter', `label=${LABEL_RUN}=${runId}`]); const ids = lines(r.out); if (ids.length) { leaks.push(`${kind}s left behind: ${ids.join(', ')}`); docker([kind, 'rm', ...ids]); } } // Processes const procs = markedProcesses(runId); if (procs.length) { leaks.push(`processes left running: ${procs.map((p) => `${p.pid} (${p.cmdline.slice(0, 120)})`).join('; ')}`); for (const p of procs) { try { process.kill(p.pid, 'SIGKILL'); } catch { /* gone */ } } } // Workspaces const ws = path.join(runDir, 'ws'); const left = fs.existsSync(ws) ? fs.readdirSync(ws) : []; if (left.length) leaks.push(`temporary workspaces left behind: ${left.join(', ')}`); try { fs.rmSync(runDir, { recursive: true, force: true }); } catch (e) { // root-owned leftovers (image tests): remove through a throwaway container const name = `dpsapi-e2e-${runId}-final-rm`; docker(['run', '--rm', '--name', name, '--label', `${LABEL_RUN}=${runId}`, '--network', 'none', '-v', `${runDir}:/victim`, process.env.DPSAPI_E2E_ALPINE || 'alpine:3.22', 'sh', '-c', 'find /victim -mindepth 1 -delete']); fs.rmSync(runDir, { recursive: true, force: true }); if (fs.existsSync(runDir)) leaks.push(`cannot remove run directory ${runDir}: ${(e as Error).message}`); } if (leaks.length) { throw new Error(`[dpsapi-e2e] resource leaks detected for run ${runId} (cleaned up, failing the run):\n- ${leaks.join('\n- ')}`); } console.log(`[dpsapi-e2e] run ${runId}: no leaked containers, processes or workspaces`); }