From d571d77cae9d8ea7ffc1ec7262855fb4905ed859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Tue, 27 Oct 2026 16:08:52 +0000 Subject: [PATCH] fix(cli): a capture DNS helper that dies silently says how it ended (#6570) When the system lookup helper exited without printing anything, the failure carried only its command line. It now names the exit code or the signal. --- packages/cli/src/capture/publicNetwork.test.ts | 17 +++++++++++++++++ packages/cli/src/capture/publicNetwork.ts | 11 +++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/capture/publicNetwork.test.ts b/packages/cli/src/capture/publicNetwork.test.ts index 720f66b8b4..c456e42e93 100644 --- a/packages/cli/src/capture/publicNetwork.test.ts +++ b/packages/cli/src/capture/publicNetwork.test.ts @@ -91,6 +91,23 @@ describe("public address policy", () => { "Capture DNS failed: ENOTFOUND", ); }); + it.each([ + [{ code: 3221225794 }, "Capture DNS failed: the lookup helper exited with code 3221225794"], + [ + { code: null, signal: "SIGKILL" }, + "Capture DNS failed: the lookup helper was ended by SIGKILL", + ], + ])( + "names how the system lookup helper ended when it says nothing (%j)", + async (ended, message) => { + mocks.resolve4.mockRejectedValue(new Error("queryA ENOTFOUND")); + mocks.execFile.mockImplementation((_file, _args, _options, callback) => { + callback(Object.assign(new Error("Command failed"), ended), "", ""); + return liveChild(); + }); + await expect(resolvePublicAddress("helper-died.example.org")).rejects.toThrow(message); + }, + ); it("checks a system answer like a DNS one, and asks the system once per host", async () => { mocks.resolve4.mockRejectedValue(new Error("queryA ENOTFOUND")); systemLookupPrints('["127.0.0.1"]'); diff --git a/packages/cli/src/capture/publicNetwork.ts b/packages/cli/src/capture/publicNetwork.ts index cd4a2f5d9f..2641db103b 100644 --- a/packages/cli/src/capture/publicNetwork.ts +++ b/packages/cli/src/capture/publicNetwork.ts @@ -1,4 +1,4 @@ -import { execFile, type ChildProcess } from "node:child_process"; +import { execFile, type ChildProcess, type ExecFileException } from "node:child_process"; import { promises as dns } from "node:dns"; import { readFile } from "node:fs/promises"; import { isIP, type Socket } from "node:net"; @@ -138,6 +138,13 @@ const SYSTEM_LOOKUP = `require("node:dns").lookup(process.argv[1], { all: true, const liveLookups = new Set(); +function helperEnded(error: ExecFileException | null): string { + if (!error) return "the lookup helper printed no answer"; + return error.signal + ? `the lookup helper was ended by ${error.signal}` + : `the lookup helper exited with code ${error.code}`; +} + export function systemLookupInKillableChild(host: string, timeoutMs: number): Promise { if (!process.listeners("exit").includes(killLiveLookups)) process.on("exit", killLiveLookups); const answers = new Promise((resolve, reject) => { @@ -152,7 +159,7 @@ export function systemLookupInKillableChild(host: string, timeoutMs: number): Pr }, (error, stdout, stderr) => { if (error?.killed) reject(new Error("Capture DNS timed out")); - else if (!stdout) reject(new Error(`Capture DNS failed: ${stderr || error?.message}`)); + else if (!stdout) reject(new Error(`Capture DNS failed: ${stderr || helperEnded(error)}`)); else { try { resolve(JSON.parse(stdout) as string[]);