Files
Charlie CheeverandClaude Opus 5.5 0e48adacce A nested api-test run finds its server's owner on Windows
`withAppServer` reuses a server an ancestor process started, and walked
the ancestors with `ps -o ppid=`, which has no Windows build: there the
walk stopped at the immediate parent. `exact api-test` runs `exact test`,
which runs `agent.mjs --test`, so that grandchild never saw the owner,
tried to start a second server on the owner's port, and the run exited 1.

The ancestors are now walked up one table of every process's parent: CIM's
`Win32_Process` (`ProcessId`, `ParentProcessId`) on Windows, as
`commandLine` already reads it there, and `ps -A -o pid=,ppid=`
elsewhere, one process instead of one per level. The walk stops at an
unknown pid, the root, or a pid already seen.
Win32_Process:
https://learn.microsoft.com/windows/win32/cimwin32prov/win32-process

scripts/verify.test.mjs passes on macOS (44), including the nested
api-test drive and a new test of the table and the walk over POSIX and
Windows output. Not run on Windows.

Closes issues/closed/20261113-windows-nested-api-test-misses-the.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-13 03:30:27 -08:00

39 lines
2.0 KiB
JavaScript

/** Other processes: whether one is alive, its command line, and this process's ancestors. */
import { spawnSync } from 'node:child_process';
export const alive = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } };
// A process's command line: `ps` has no Windows build, where CIM's Win32_Process reports it. Empty when neither answers.
export const commandLine = (pid) => (process.platform === 'win32'
? spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `(Get-CimInstance Win32_Process -Filter 'ProcessId=${Number(pid)}').CommandLine`], { encoding: 'utf8' })
: spawnSync('ps', ['-o', 'command=', '-p', String(pid)], { encoding: 'utf8' })).stdout?.trim() ?? '';
/** Every process's parent, by pid: `ps` has no Windows build, where CIM's Win32_Process reports them, as for
* `commandLine`. Empty when neither answers. */
function processParents() {
const r = process.platform === 'win32'
? spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', 'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId) $($_.ParentProcessId)" }'], { encoding: 'utf8' })
: spawnSync('ps', ['-A', '-o', 'pid=,ppid='], { encoding: 'utf8' });
return parentsOf(r.stdout ?? '');
}
/** `<pid> <parent pid>` lines as a map from pid to parent pid. */
export function parentsOf(text) {
const parents = new Map();
for (const line of text.split('\n')) {
const [pid, parent] = line.trim().split(/\s+/).map(Number);
if (Number.isInteger(pid) && Number.isInteger(parent)) parents.set(pid, parent);
}
return parents;
}
/** The ancestors' pids of the process whose parent is `pid`, nearest first, as strings: `pid`, its parent, and so
* on up to the first pid that `parents` does not know, the root (0 or 1), or a cycle. */
export function ancestors(pid = process.ppid, parents = processParents()) {
const out = [];
while (pid > 1 && out.length < 32 && !out.includes(String(pid))) {
out.push(String(pid));
pid = parents.get(pid) ?? 0;
}
return out;
}