fix(cli): a finished capture exits even when a site's DNS never answers (#5983)

* fix(cli): a finished capture exits even when a site's DNS never answers

Capture resolved every page request with the system lookup and gave up after
10 s, but a system lookup cannot be cancelled: the abandoned one kept the
process alive until the OS resolver timed out, so a capture that had printed
its result could run past the app's 5-minute cap and lose its folder.
Resolve through c-ares instead, as hostAnswers does, and cancel it at the
same 10 s deadline.

* test(cli): a capture lookup that answers must exit right after too

Dropping the timer cleanup kept every successful lookup alive 10 s and no test noticed.

* fix(cli): capture falls back to the system lookup and exits once its result is out

c-ares answers first. When it has no answer, the system lookup (hosts file, OS-only DNS)
runs in a child process killed at the 10 s deadline, cached per host, because Node waits
for an in-process system lookup even in process.exit. Capture then asks the CLI to exit
100 ms after printing its result, as render does.

* fix(cli): capture reads the hosts file first and kills its lookup child on exit

The hosts file answers before DNS, as the system lookup does (overrides and 0.0.0.0
blocks keep working). A host whose system answer is cached skips c-ares, a caller that
joins a running lookup keeps its own deadline, and a pending lookup child is killed when
the CLI exits.

* fix(cli): capture's forced exit waits until a slow reader has the whole result

The root exit now waits for stdout and stderr to hand over what is already
written before it calls process.exit, so a large JSON result on a slow pipe
arrives complete. A hostname listed several times in the hosts file now has
every address checked, not only the first.

* fix(cli): keep process exit in the CLI root and let test fixtures exit

The output wait is now a helper that only waits; cli.ts still owns the
process.exit. The process-ownership check treats test fixtures like tests,
since each one runs as its own process.

* chore(cli): run the process-ownership check before every commit

A process.exit outside a process root used to pass the local hooks and fail
only in CI's Preflight.
This commit is contained in:
Miguel Ángel
2026-10-21 02:46:17 +00:00
committed by GitHub
parent bbb691001d
commit 1289734770
19 changed files with 553 additions and 127 deletions
+3 -1
View File
@@ -39,7 +39,9 @@ function listTypeScriptFiles(directory, roots) {
return readdirSync(directory).flatMap((name) => {
const path = join(directory, name);
if (statSync(path).isDirectory()) return listTypeScriptFiles(path, roots);
if (!path.endsWith(".ts") || path.endsWith(".test.ts") || roots.includes(path)) return [];
if (!path.endsWith(".ts") || /\.(test|fixture)\.ts$/.test(path) || roots.includes(path)) {
return [];
}
return [path];
});
}
+18 -1
View File
@@ -1,6 +1,12 @@
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, it } from "node:test";
import { listDirectProcessTermination } from "./check-cli-process-ownership.mjs";
import {
checkCliProcessOwnership,
listDirectProcessTermination,
} from "./check-cli-process-ownership.mjs";
describe("CLI process ownership checker", () => {
it("reports direct process exit and exitCode writes", () => {
@@ -25,4 +31,15 @@ describe("CLI process ownership checker", () => {
[],
);
});
it("lets a test fixture, spawned as its own process, exit", (t) => {
const root = mkdtempSync(join(tmpdir(), "hf-ownership-"));
t.after(() => rmSync(root, { recursive: true, force: true }));
mkdirSync(join(root, "packages/cli/src/utils"), { recursive: true });
writeFileSync(join(root, "packages/cli/src/utils/a.fixture.ts"), "process.exit(0);\n");
writeFileSync(join(root, "packages/cli/src/utils/a.ts"), "process.exit(0);\n");
assert.deepEqual(checkCliProcessOwnership(root), [
"packages/cli/src/utils/a.ts:1:1 uses process.exit",
]);
});
});