diff --git a/src/bindings/utils/getForkExecArgv.ts b/src/bindings/utils/getForkExecArgv.ts new file mode 100644 index 00000000..4fff9de0 --- /dev/null +++ b/src/bindings/utils/getForkExecArgv.ts @@ -0,0 +1,26 @@ +export function getForkExecArgv(execArgv: readonly string[] = process.execArgv): string[] { + const result: string[] = []; + + for (let i = 0; i < execArgv.length; i++) { + const arg = execArgv[i]!; + + // Explicit execArgv also bypasses fork's removal of the parent eval arguments. + if (arg === "--input-type" || arg === "-e" || arg === "--eval" || arg === "-pe") { + i++; + continue; + } + + if (arg === "-p" || arg === "--print") { + if (execArgv[i + 1] != null && !execArgv[i + 1]!.startsWith("-")) + i++; + continue; + } + + if (arg.startsWith("--input-type=") || arg.startsWith("--eval=") || arg.startsWith("--print=")) + continue; + + result.push(arg); + } + + return result; +} diff --git a/src/bindings/utils/testBindingBinary.ts b/src/bindings/utils/testBindingBinary.ts index c26f4636..d4194f1e 100644 --- a/src/bindings/utils/testBindingBinary.ts +++ b/src/bindings/utils/testBindingBinary.ts @@ -8,6 +8,7 @@ import {BuildGpu} from "../types.js"; import {LlamaLogLevelToAddonLogLevel} from "../Llama.js"; import {defaultBindingTestLogLevel, newGithubIssueUrl} from "../../config.js"; import {getPlatform} from "./getPlatform.js"; +import {getForkExecArgv} from "./getForkExecArgv.js"; import type {BindingModule} from "../AddonTypes.js"; const require = createRequire(import.meta.url); @@ -127,6 +128,7 @@ export async function testBindingBinary( let pipeSet = false; const subProcess = forkFunction.fork(__filename, [], { + execArgv: getForkExecArgv(), detached: false, silent: true, stdio: pipeOutputOnNode diff --git a/test/standalone/utils/getForkExecArgv.test.ts b/test/standalone/utils/getForkExecArgv.test.ts new file mode 100644 index 00000000..39851550 --- /dev/null +++ b/test/standalone/utils/getForkExecArgv.test.ts @@ -0,0 +1,61 @@ +import {fork} from "node:child_process"; +import {mkdtemp, rm, writeFile} from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import {describe, expect, test} from "vitest"; +import {getForkExecArgv} from "../../../src/bindings/utils/getForkExecArgv.js"; + + +describe("getForkExecArgv", () => { + test("preserves file-compatible arguments without changing the input", () => { + const args = ["--trace-warnings", "--conditions", "custom", "--max-old-space-size=256"]; + expect(getForkExecArgv(args)).to.eql(args); + expect(args).to.eql(["--trace-warnings", "--conditions", "custom", "--max-old-space-size=256"]); + }); + + test.each([ + {args: ["--trace-warnings", "--input-type=module", "-e", "throw new Error('parent eval ran')"], expected: ["--trace-warnings"]}, + {args: ["--trace-warnings", "--input-type", "module", "--eval", "throw new Error('parent eval ran')"], expected: ["--trace-warnings"]}, + {args: ["--trace-warnings", "--input-type=commonjs", "--eval=throw new Error('parent eval ran')"], expected: ["--trace-warnings"]}, + {args: ["--trace-warnings", "--input-type", "commonjs", "-p", "'parent print ran'"], expected: ["--trace-warnings"]}, + {args: ["--trace-warnings", "--input-type=commonjs", "-pe", "'parent print ran'"], expected: ["--trace-warnings"]}, + {args: ["--trace-warnings", "--input-type=commonjs", "--print='parent print ran'"], expected: ["--trace-warnings"]}, + {args: ["-p", "--conditions", "custom", "-e", "'parent eval ran'"], expected: ["--conditions", "custom"]}, + {args: ["--print", "--trace-warnings", "--eval", "'parent eval ran'"], expected: ["--trace-warnings"]} + ])("starts a file child with input arguments $args", async ({args, expected}) => { + const directory = await mkdtemp(path.join(os.tmpdir(), "node-llama-cpp-fork-")); + const childPath = path.join(directory, "child.mjs"); + await writeFile(childPath, "process.send({execArgv: process.execArgv}); process.disconnect();\n"); + + try { + const childArgs = getForkExecArgv(args); + const result = await new Promise<{execArgv: string[]}>((resolve, reject) => { + const child = fork(childPath, [], { + execArgv: childArgs, + stdio: ["ignore", "ignore", "pipe", "ipc"] + }); + let stderr = ""; + let message: {execArgv: string[]} | undefined; + const timeout = setTimeout(() => { + child.kill(); + reject(new Error("File child did not finish")); + }, 5000); + + child.stderr?.on("data", (data) => stderr += data); + child.on("message", (value: {execArgv: string[]}) => message = value); + child.on("error", reject); + child.on("exit", (code) => { + clearTimeout(timeout); + if (code !== 0 || message == null) + reject(new Error(`File child failed: ${code}: ${stderr}`)); + else + resolve(message); + }); + }); + + expect(result.execArgv).to.eql(expected); + } finally { + await rm(directory, {recursive: true, force: true}); + } + }); +});