diff --git a/scripts/verify-native-pascal-calls.mjs b/scripts/verify-native-pascal-calls.mjs index 62cc31daf..79def6c87 100644 --- a/scripts/verify-native-pascal-calls.mjs +++ b/scripts/verify-native-pascal-calls.mjs @@ -4,7 +4,6 @@ import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { isAbsolute, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { promisify } from "node:util"; import { artifactCli, @@ -12,7 +11,44 @@ import { withArtifactMcp, } from "./lib/artifact-e2e.mjs"; -const execute = promisify(execFile); +const execute = (command, arguments_, { timeout }) => + new Promise((resolve, reject) => { + let timedOut = false; + const child = execFile( + command, + arguments_, + { detached: true, encoding: "utf8" }, + (error, stdout, stderr) => { + clearTimeout(deadline); + if (timedOut) { + const failure = new Error(`Command timed out after ${timeout} ms`); + Object.assign(failure, { + code: "ETIMEDOUT", + killed: true, + signal: "SIGKILL", + stdout, + stderr, + }); + reject(failure); + } else if (error !== null) { + reject(error); + } else { + resolve({ stdout, stderr }); + } + }, + ); + const deadline = setTimeout(() => { + timedOut = true; + if (child.pid === undefined) return; + try { + // This script only runs on macOS. detached:true gives the owned + // compiler invocation its own process group, inherited by FPC tools. + process.kill(-child.pid, "SIGKILL"); + } catch (cause) { + if (cause.code !== "ESRCH") child.kill("SIGKILL"); + } + }, timeout); + }); const compiler = process.env.REA_VERIFY_FPC_COMMAND; if (process.platform !== "darwin" || !isAbsolute(compiler ?? "")) throw new Error( @@ -79,12 +115,11 @@ const stable = (result) => })); try { const version = ( - await execute(compiler, ["-iV"], { timeout: 30_000, killSignal: "SIGKILL" }) + await execute(compiler, ["-iV"], { timeout: 30_000 }) ).stdout.trim(); const architecture = ( await execute(compiler, ["-iTP"], { timeout: 30_000, - killSignal: "SIGKILL", }) ).stdout.trim(); await execute( @@ -98,7 +133,7 @@ try { `-FU${directory}`, source, ], - { timeout: 120_000, killSignal: "SIGKILL" }, + { timeout: 120_000 }, ); const binary = join(directory, "calls"); const cli = await artifactCli( diff --git a/src/domain/native/nativeCallObservation.ts b/src/domain/native/nativeCallObservation.ts index fe5c15fd9..6ab2d13ab 100644 --- a/src/domain/native/nativeCallObservation.ts +++ b/src/domain/native/nativeCallObservation.ts @@ -122,7 +122,10 @@ export const nativeCodeLocationSchema = z.strictObject({ column: z.number().int().positive().nullable(), }) .nullable() - .optional(), + .optional() + .describe( + "Compiler or debugger source attribution only; REA does not read or verify the named source-file bytes.", + ), }); /** One observed entry into a requested function or method. */ diff --git a/src/native/NativeCallObservation.ts b/src/native/NativeCallObservation.ts index bb3960b56..26d9fc474 100644 --- a/src/native/NativeCallObservation.ts +++ b/src/native/NativeCallObservation.ts @@ -31,6 +31,7 @@ export const NATIVE_CALL_OBSERVATION_LIMITATIONS = [ "Registers are the raw integer argument registers at entry. For Objective-C methods the first two hold the receiver and the selector.", "Receiver classes come from LLDB's Objective-C runtime reader without running target code; they are null for class methods and for receivers LLDB cannot classify.", "Calls that never reach a symbol's entry, such as inlined code or objc_direct methods called directly, are not observed.", + "Source locations come from LLDB line-table metadata; REA does not read or verify the named source-file contents.", "The target is launched directly under LLDB, not through LaunchServices; LLDB disables ASLR for it, so load addresses can equal file addresses.", "The observation window is checked about once a second, and recording pauses the process at every entry, which slows it.", ] as const;