mirror of
https://github.com/morluto/rea
synced 2026-10-28 15:07:26 -04:00
122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
|
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
|
|
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
|
import { mkdir, mkdtemp, rm, symlink } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { SWIFTC_INTERRUPT } from "../process/DarwinProcessRunTokenReader.js";
|
|
import { execFileOutput } from "../process/ExecFileOutput.js";
|
|
import { safeParseJson } from "../domain/safeJson.js";
|
|
import { DEFAULT_NATIVE_UI_DEADLINE_MS } from "../domain/native/nativeDeadlines.js";
|
|
import type { NativeUiHelper } from "./NativeUiObservation.js";
|
|
import { NATIVE_UI_HELPER_MAX_BUFFER } from "./NativeUiOutputBudget.js";
|
|
|
|
export interface NativeUiHelperRuntime {
|
|
readonly invoke: NativeUiHelper;
|
|
readonly cleanupLease: AnalysisCleanupLease;
|
|
readonly signal: AbortSignal;
|
|
/** Omitted by test doubles; production runtimes always set the selected deadline. */
|
|
readonly deadlineMs?: number;
|
|
close(): Promise<void>;
|
|
resources(): readonly string[];
|
|
}
|
|
|
|
/** Lazily compile one owned helper per observation/scenario and remove its compiler cache. */
|
|
export const createNativeUiHelperRuntime = (
|
|
environment: Readonly<NodeJS.ProcessEnv>,
|
|
deadlineMs = DEFAULT_NATIVE_UI_DEADLINE_MS,
|
|
): NativeUiHelperRuntime => {
|
|
const selectedEnvironment = snapshotEnvironment(environment);
|
|
const deadlineSignal = AbortSignal.timeout(deadlineMs);
|
|
const operations = new ArtifactResourceScope();
|
|
// Both helper stages share the selected deadline. A shorter fixed exec
|
|
// timeout would fail the subprocess before that deadline and hide it.
|
|
let root: string | undefined;
|
|
let preparation: Promise<string> | undefined;
|
|
const cleanupLease = new AnalysisCleanupLease(async () => {
|
|
await operations.close();
|
|
if (root !== undefined) {
|
|
await rm(root, { recursive: true, force: true });
|
|
root = undefined;
|
|
preparation = undefined;
|
|
}
|
|
});
|
|
const prepare = async (signal: AbortSignal): Promise<string> => {
|
|
root = await mkdtemp(join(tmpdir(), "rea-native-ui-"));
|
|
const output = join(root, "observer");
|
|
const main = join(root, "main.swift");
|
|
await symlink(
|
|
fileURLToPath(
|
|
new URL("../../bridge/native/ReaNativeUI.swift", import.meta.url),
|
|
),
|
|
main,
|
|
);
|
|
// Keep swiftc's intermediate objects inside the removable root.
|
|
const compilerTemporary = join(root, "tmp");
|
|
await mkdir(compilerTemporary);
|
|
await execFileOutput(
|
|
"/usr/bin/xcrun",
|
|
[
|
|
"swiftc",
|
|
"-module-cache-path",
|
|
join(root, "modules"),
|
|
fileURLToPath(
|
|
new URL(
|
|
"../../bridge/native/NativeUIChildren.swift",
|
|
import.meta.url,
|
|
),
|
|
),
|
|
main,
|
|
"-o",
|
|
output,
|
|
],
|
|
{
|
|
timeout: deadlineMs,
|
|
maxBuffer: 1024 * 1024,
|
|
stopSignal: SWIFTC_INTERRUPT,
|
|
env: { ...selectedEnvironment, TMPDIR: compilerTemporary },
|
|
signal,
|
|
},
|
|
);
|
|
return output;
|
|
};
|
|
const invoke: NativeUiHelper = (parameters, callerSignal) =>
|
|
operations.run(async () => {
|
|
const signal =
|
|
callerSignal === undefined
|
|
? deadlineSignal
|
|
: AbortSignal.any([callerSignal, deadlineSignal]);
|
|
preparation ??= prepare(signal);
|
|
const executable = await preparation;
|
|
const output = await execFileOutput(
|
|
executable,
|
|
[JSON.stringify(parameters)],
|
|
{
|
|
timeout: deadlineMs,
|
|
maxBuffer: NATIVE_UI_HELPER_MAX_BUFFER,
|
|
stopSignal: "SIGTERM",
|
|
env: selectedEnvironment,
|
|
signal,
|
|
},
|
|
);
|
|
const parsed = safeParseJson(output.stdout);
|
|
if (!parsed.ok)
|
|
throw new Error(
|
|
`Native helper returned invalid JSON: ${parsed.error}`,
|
|
{
|
|
cause: parsed.cause,
|
|
},
|
|
);
|
|
return parsed.value;
|
|
});
|
|
return {
|
|
invoke,
|
|
cleanupLease,
|
|
signal: deadlineSignal,
|
|
deadlineMs,
|
|
close: () => cleanupLease.release(),
|
|
resources: () => (root === undefined ? [] : [root]),
|
|
};
|
|
};
|