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
This commit is contained in:
pablo
2026-10-12 08:55:36 +08:00
committed by GitHub
parent ed6af7eb8c
commit 091cb81fc3
2 changed files with 153 additions and 12 deletions
+56 -8
View File
@@ -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 },
);
}
@@ -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<typeof createDarwinProcessRunTokenReader>,
): Promise<DarwinProcessOwnershipInspectionError> => {
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();