mirror of
https://github.com/morluto/rea.git
synced 2026-11-02 21:06:04 +00:00
fix(javascript): emit no fork when a literal stdio array omits the IPC channel (#7992)
* fix(javascript): emit no fork when a literal stdio array omits the IPC channel
Node's fork() requires an explicit stdio array to carry the IPC channel:
if (!ArrayPrototypeIncludes(options.stdio, 'ipc'))
throw new ERR_CHILD_PROCESS_IPC_REQUIRED('options.stdio')
The throw is synchronous, before any child process exists, but the analysis
reported a resolved fork with stdioMode "array" — byte-identical to the valid
`[..., "ipc"]` form — which projects to a child-process node, a resolved spawns
relation and resolved connects-stdio.
Measured on Node 24.22.1:
fork({stdio: ["inherit","inherit","inherit"]}) -> THROWS ERR_CHILD_PROCESS_IPC_REQUIRED
fork({stdio: ["pipe","pipe","pipe"]}) -> THROWS ERR_CHILD_PROCESS_IPC_REQUIRED
fork({stdio: ["ipc"]}) -> SPAWNED connected=true
fork({stdio: ["inherit","inherit","inherit","ipc"]}) -> SPAWNED connected=true
fork({stdio: "pipe"}) -> SPAWNED connected=true
spawn({stdio: ["inherit","inherit","inherit"]})-> SPAWNED connected=false
A string stdio is normalised with "ipc" appended, so only the array form can omit
it. The same array is valid for spawn, which has no IPC requirement.
Drop a fork candidate only when its stdio array is fully literal and provably
lacks "ipc"; a spread or a non-literal element may still supply the channel and
stays a candidate.
* chore: retrigger title and readiness checks after the title was corrected
This commit is contained in:
1 parent
d39b3000ec
commit
2bfd84b2c8
2 files changed
+100
-1
No files matched your search
@@ -0,0 +1,62 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
|
||||
/**
|
||||
* Node's `fork()` requires an explicit `stdio` array to carry the IPC channel:
|
||||
* `if (!ArrayIncludes(options.stdio, 'ipc')) throw new ERR_CHILD_PROCESS_IPC_REQUIRED`.
|
||||
* The throw is synchronous, before any child process exists, so such a call is
|
||||
* not a spawn. A string `stdio` is normalised with `"ipc"` appended, so only the
|
||||
* array form can omit it, and the same array is valid for `spawn`.
|
||||
*/
|
||||
const spawnsFor = (call: string) =>
|
||||
analyzeJavaScriptSemantics(
|
||||
`import { fork } from "node:child_process"; ${call};`,
|
||||
).childProcessSpawns;
|
||||
|
||||
it.each([
|
||||
["three inherits", `["inherit", "inherit", "inherit"]`],
|
||||
["three pipes", `["pipe", "pipe", "pipe"]`],
|
||||
["ignore then pipes", `["ignore", "pipe", "pipe"]`],
|
||||
["one inherit", `["inherit"]`],
|
||||
["an empty array", `[]`],
|
||||
] as const)(
|
||||
"emits no fork for a literal stdio array with %s and no ipc",
|
||||
(_name, stdio) => {
|
||||
expect(spawnsFor(`fork("worker.js", { stdio: ${stdio} })`)).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["a fourth element", `["inherit", "inherit", "inherit", "ipc"]`],
|
||||
["only ipc", `["ipc"]`],
|
||||
["ipc and a pipe", `["pipe", "ipc"]`],
|
||||
] as const)("keeps the fork for a stdio array with %s", (_name, stdio) => {
|
||||
expect(spawnsFor(`fork("worker.js", { stdio: ${stdio} })`)).toEqual([
|
||||
expect.objectContaining({ method: "fork", stdioMode: "array" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a string stdio", `fork("worker.js", { stdio: "pipe" })`],
|
||||
["omitted stdio", `fork("worker.js", {})`],
|
||||
] as const)("keeps the fork for %s", (_name, call) => {
|
||||
expect(spawnsFor(call)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["a spread element", `[...base, "ipc"]`],
|
||||
["a non-literal element", `[channel]`],
|
||||
] as const)(
|
||||
"keeps the fork when %s may still supply the channel",
|
||||
(_name, stdio) => {
|
||||
expect(spawnsFor(`fork("worker.js", { stdio: ${stdio} })`)).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the same array for spawn, which has no IPC requirement", () => {
|
||||
const spawned = analyzeJavaScriptSemantics(
|
||||
`import { spawn } from "node:child_process"; spawn("tool", [], { stdio: ["inherit", "inherit", "inherit"] });`,
|
||||
);
|
||||
expect(spawned.childProcessSpawns).toHaveLength(1);
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type JavaScriptExpressionShapeResolver,
|
||||
} from "./javascriptSemanticExpressionShapes.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
semanticStaticPropertyName,
|
||||
staticJavaScriptTruthiness,
|
||||
staticObjectProperty,
|
||||
@@ -68,8 +69,10 @@ export const collectJavaScriptSemanticChildProcesses = (
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
callables: readonly JavaScriptSemanticCallable[],
|
||||
): ChildProcessAnalysis => {
|
||||
const candidates = collectSpawns(program, state, callables);
|
||||
const context = createSemanticExpressionShapeResolver(state);
|
||||
const candidates = collectSpawns(program, state, callables).filter(
|
||||
(candidate) => !forkRejectsStdioArray(candidate, context),
|
||||
);
|
||||
const childProcessSpawns = candidates.map((candidate) =>
|
||||
immutableSpawn(candidate, context),
|
||||
);
|
||||
@@ -444,6 +447,40 @@ const stdioMode = (
|
||||
return "dynamic";
|
||||
};
|
||||
|
||||
/**
|
||||
* Node's `fork` requires an explicit `stdio` array to carry the IPC channel. An
|
||||
* array that provably omits `"ipc"` throws ERR_CHILD_PROCESS_IPC_REQUIRED
|
||||
* synchronously, before any child process exists, so it is not a spawn at all.
|
||||
* A string `stdio` is normalised with `"ipc"` appended, so only the array form
|
||||
* can omit it; a spread or unreadable element may still supply the channel and
|
||||
* is therefore left alone.
|
||||
*/
|
||||
const forkRejectsStdioArray = (
|
||||
candidate: SpawnCandidate,
|
||||
context: ChildSpawnContext,
|
||||
): boolean => {
|
||||
if (candidate.method !== "fork") return false;
|
||||
const { options } = spawnArguments(candidate, context);
|
||||
if (options === undefined) return false;
|
||||
const expression = unwrapJavaScriptExpression(options).node;
|
||||
if (context.isNullish(expression)) return false;
|
||||
const lookup = staticObjectProperty(expression, "stdio", context.isNullish);
|
||||
if (lookup.status !== "explicit") return false;
|
||||
const value = unwrapJavaScriptExpression(lookup.value).node;
|
||||
if (!t.isArrayExpression(value)) return false;
|
||||
const literals = value.elements.map((element) =>
|
||||
element === null || t.isSpreadElement(element)
|
||||
? undefined
|
||||
: readExactJavaScriptLiteral(element),
|
||||
);
|
||||
if (literals.some((literal) => literal === undefined || !literal.found))
|
||||
return false;
|
||||
return !literals.some(
|
||||
(literal) =>
|
||||
literal !== undefined && literal.found && literal.value === "ipc",
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Without a string or array `stdio`, Node's `fork` pipes the standard streams
|
||||
* when `silent` is truthy and inherits them otherwise.
|
||||
|
||||
Reference in new issue
Block a user