Files
hyperframes/packages/cli/src/utils/dnsTestServer.ts
T
Miguel Ángel 1289734770 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.
2026-10-21 02:46:17 +00:00

38 lines
1.3 KiB
TypeScript

import { createSocket } from "node:dgram";
export interface TestDnsServer {
/** `host:port`, for a Resolver's setServers. */
address: string;
close(): void;
}
/** A DNS server: "silent" never replies, "answer" answers A with `ipv4`, "no-aaaa" answers A and drops AAAA. */
export async function startTestDnsServer(
mode: "silent" | "answer" | "no-aaaa",
ipv4 = "127.0.0.1",
): Promise<TestDnsServer> {
const socket = createSocket("udp4");
socket.on("message", (msg, peer) => {
if (mode === "silent") return;
let end = 12;
while (msg[end] !== 0) end += (msg[end] ?? 0) + 1;
end += 5;
const isA = msg.readUInt16BE(end - 4) === 1;
if (mode === "no-aaaa" && !isA) return;
const header = Buffer.alloc(12);
msg.copy(header, 0, 0, 2);
header.writeUInt16BE(0x8180, 2);
header.writeUInt16BE(1, 4);
header.writeUInt16BE(isA ? 1 : 0, 6);
const answer = Buffer.from([0xc0, 0x0c, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4]);
const record = Buffer.concat([answer, Buffer.from(ipv4.split(".").map(Number))]);
socket.send(
Buffer.concat([header, msg.subarray(12, end), ...(isA ? [record] : [])]),
peer.port,
peer.address,
);
});
await new Promise<void>((resolve) => socket.bind(0, "127.0.0.1", resolve));
return { address: `127.0.0.1:${socket.address().port}`, close: () => socket.close() };
}