mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user