From 091cb81fc3ca2b8ec96df7a534934388a6b882cd Mon Sep 17 00:00:00 2001 From: pablo <194239734+jimpablo@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:55:36 +0800 Subject: [PATCH] fix(process): report the Swift compiler outcome with bounded diagnostics (#1999) * fix(process): report the Swift compiler outcome with bounded diagnostics * fix(process): keep a signaled compiler's null exit code --- src/process/DarwinProcessRunTokenReader.ts | 64 +++++++++-- .../darwinProcessRunTokenReader.test.ts | 101 +++++++++++++++++- 2 files changed, 153 insertions(+), 12 deletions(-) diff --git a/src/process/DarwinProcessRunTokenReader.ts b/src/process/DarwinProcessRunTokenReader.ts index cc837b0281..69304b0636 100644 --- a/src/process/DarwinProcessRunTokenReader.ts +++ b/src/process/DarwinProcessRunTokenReader.ts @@ -6,7 +6,11 @@ import { fileURLToPath } from "node:url"; import { z } from "zod"; import { safeParseJson } from "../domain/safeJson.js"; -import { execFileOutput } from "./ExecFileOutput.js"; +import { + execFileOutput, + execFileOutputFailure, + type ExecFileOutputFailure, +} from "./ExecFileOutput.js"; import type { ProcessIdentityObservation, ProcessRunTokenObservation, @@ -72,6 +76,40 @@ const sourceFiles = [ */ export const SWIFTC_INTERRUPT = "SIGINT"; +/** Deadline for the one-shot helper compilation. */ +const COMPILER_TIMEOUT_MS = 60_000; + +/** Bound on compiler diagnostics embedded in the inspection failure text. */ +const STDERR_EXCERPT_LIMIT = 512; + +/** Name the observed compiler outcome without implying an exit code it lacks. */ +const compilerOutcome = ( + code: string | number | null, + failure: ExecFileOutputFailure | undefined, + timeoutMs: number, +): string => { + if (typeof code === "number") return `exit code ${String(code)}`; + if (code === "ETIMEDOUT") return `timed out after ${String(timeoutMs)} ms`; + if (code === null) + return typeof failure?.signal === "string" + ? `stopped by signal ${failure.signal}` + : "unknown"; + return code; +}; + +const compilerDiagnostics = ( + failure: ExecFileOutputFailure | undefined, + cause: unknown, +): string => { + if (failure === undefined) + return cause instanceof Error ? cause.message : String(cause); + const diagnostics = failure.stderr.trim(); + if (diagnostics === "") return "no compiler diagnostics were captured"; + return diagnostics.length > STDERR_EXCERPT_LIMIT || failure.outputTruncated + ? `${diagnostics.slice(0, STDERR_EXCERPT_LIMIT)}… [diagnostics truncated]` + : diagnostics; +}; + /** The native Darwin process-ownership reader could not be prepared. */ export class DarwinProcessOwnershipInspectionError extends Error { constructor(message: string, options?: ErrorOptions) { @@ -126,8 +164,13 @@ const waitForCompilation = async ( /** Compile and retain the narrow Darwin sysctl reader for this REA process. */ export const createDarwinProcessRunTokenReader = ( - options: { readonly xcrun?: string } = {}, + options: { + readonly xcrun?: string; + /** Deadline for the helper compilation; the default keeps verification fast. */ + readonly compileTimeoutMs?: number; + } = {}, ) => { + const compilerTimeoutMs = options.compileTimeoutMs ?? COMPILER_TIMEOUT_MS; let root: string | undefined; let executable: string | undefined; let compilation: ProcessReaderCompilation | undefined; @@ -171,7 +214,7 @@ export const createDarwinProcessRunTokenReader = ( options.xcrun ?? "/usr/bin/xcrun", ["swiftc", ...sourceFiles, "-o", output], { - timeout: 60_000, + timeout: compilerTimeoutMs, maxBuffer: 1024 * 1024, signal, stopSignal: SWIFTC_INTERRUPT, @@ -180,14 +223,19 @@ export const createDarwinProcessRunTokenReader = ( ); } catch (cause: unknown) { if (isExpectedAbort(cause, signal)) throw cause; + const failure = execFileOutputFailure(cause); + // A parsed failure carries its own null code (a signaled compiler); + // only an unparsed cause falls back to the raw error property. const code = - cause instanceof Error && "code" in cause - ? String(cause.code) - : "unknown"; + failure !== undefined + ? failure.code + : cause instanceof Error && "code" in cause + ? String(cause.code) + : "unknown"; throw new DarwinProcessOwnershipInspectionError( code === "ENOENT" - ? `macOS process ownership inspection requires the Apple Swift compiler via xcrun (compiler result: ${code})` - : `macOS process ownership helper compilation failed via xcrun (compiler result: ${code}): ${cause instanceof Error ? cause.message : String(cause)}`, + ? `macOS process ownership inspection requires the Apple Swift compiler via xcrun (compiler result: ${String(code)})` + : `macOS process ownership helper compilation failed via xcrun (compiler result: ${compilerOutcome(code, failure, compilerTimeoutMs)}): ${compilerDiagnostics(failure, cause)}`, { cause }, ); } diff --git a/tests/boundary/process/darwinProcessRunTokenReader.test.ts b/tests/boundary/process/darwinProcessRunTokenReader.test.ts index 6c0025d78c..c1bdb67b27 100644 --- a/tests/boundary/process/darwinProcessRunTokenReader.test.ts +++ b/tests/boundary/process/darwinProcessRunTokenReader.test.ts @@ -22,7 +22,10 @@ import { createSystemProcessOwnershipHost, systemProcessOwnershipHost, } from "../../../src/process/ProcessOwnershipObservation.js"; -import { createDarwinProcessRunTokenReader } from "../../../src/process/DarwinProcessRunTokenReader.js"; +import { + createDarwinProcessRunTokenReader, + DarwinProcessOwnershipInspectionError, +} from "../../../src/process/DarwinProcessRunTokenReader.js"; import { execFileOutput } from "../../../src/process/ExecFileOutput.js"; import { normalizeCaptureFailure } from "../../../src/process/capture/ProcessCaptureError.js"; import { PlaywrightElectronActiveProvider } from "../../../src/browser/PlaywrightElectronActiveProvider.js"; @@ -168,8 +171,19 @@ it("reports an actionable missing Swift compiler without installing it", async ( } }); +const preparationFailure = async ( + reader: ReturnType, +): Promise => { + const cause = await reader.prepare().then( + () => undefined, + (failure: unknown) => failure, + ); + expect(cause).toBeInstanceOf(DarwinProcessOwnershipInspectionError); + return cause as DarwinProcessOwnershipInspectionError; +}; + it.skipIf(process.platform === "win32")( - "preserves compiler diagnostics when Swift preparation fails", + "reports the compiler exit code with its diagnostics when preparation fails", async () => { const directory = await createTestTempDirectory( "rea-process-token-compiler-failure-test-", @@ -182,8 +196,87 @@ it.skipIf(process.platform === "win32")( await chmod(executable, 0o755); const reader = createDarwinProcessRunTokenReader({ xcrun: executable }); try { - await expect(reader.prepare()).rejects.toThrow( - /compilation failed.*module cache is not writable/su, + await expect(preparationFailure(reader)).resolves.toMatchObject({ + message: + "macOS process ownership helper compilation failed via xcrun (compiler result: exit code 1): module cache is not writable", + }); + } finally { + await reader.close(); + } + }, +); + +it.skipIf(process.platform === "win32")( + "reports compiler diagnostics captured before the compilation deadline", + async () => { + const directory = await createTestTempDirectory( + "rea-process-token-compiler-timeout-test-", + ); + const executable = join(directory, "hanging-xcrun"); + await writeFile( + executable, + '#!/usr/bin/env node\nprocess.stderr.write("error: PCH was compiled with module cache path that differs\\n", () => { setInterval(() => {}, 1000); });\n', + ); + await chmod(executable, 0o755); + const reader = createDarwinProcessRunTokenReader({ + xcrun: executable, + // Headroom for the child interpreter's cold start under suite load. + compileTimeoutMs: 3_000, + }); + try { + await expect(preparationFailure(reader)).resolves.toMatchObject({ + message: + "macOS process ownership helper compilation failed via xcrun (compiler result: timed out after 3000 ms): error: PCH was compiled with module cache path that differs", + }); + } finally { + await reader.close(); + } + }, +); + +it.skipIf(process.platform === "win32")( + "reports the stop signal when the compiler dies without an exit code", + async () => { + const directory = await createTestTempDirectory( + "rea-process-token-compiler-signal-test-", + ); + const executable = join(directory, "segv-xcrun"); + await writeFile( + executable, + '#!/usr/bin/env node\nprocess.stderr.write("fatal: segmentation fault during type checking\\n");\nprocess.kill(process.pid, "SIGSEGV");\n', + ); + await chmod(executable, 0o755); + const reader = createDarwinProcessRunTokenReader({ xcrun: executable }); + try { + await expect(preparationFailure(reader)).resolves.toMatchObject({ + message: + "macOS process ownership helper compilation failed via xcrun (compiler result: stopped by signal SIGSEGV): fatal: segmentation fault during type checking", + }); + } finally { + await reader.close(); + } + }, +); + +it.skipIf(process.platform === "win32")( + "bounds embedded compiler diagnostics and marks the truncation", + async () => { + const directory = await createTestTempDirectory( + "rea-process-token-compiler-verbose-test-", + ); + const executable = join(directory, "verbose-xcrun"); + const diagnostic = "x".repeat(600); + await writeFile( + executable, + `#!/usr/bin/env node\nprocess.stderr.write(${JSON.stringify(`${diagnostic}\\n`)}); process.exitCode = 1;\n`, + ); + await chmod(executable, 0o755); + const reader = createDarwinProcessRunTokenReader({ xcrun: executable }); + try { + const failure = await preparationFailure(reader); + expect(failure.message).toContain("compiler result: exit code 1"); + expect(failure.message).toContain( + `: ${"x".repeat(512)}… [diagnostics truncated]`, ); } finally { await reader.close();