fix(ext/node): support stdin child_process IPC & fd stdout/stderr (#24106)

Add supports for "ipc" and fd options in child_process spawn API.

Internal changes: Adds a hidden rid and "ipc_for_internal_use" option to
Deno.Command. Used by `node:child_process`

Example:
```js
const out = fs.openSync("./logfile.txt", 'a')
const proc = spawn(process.execPath, ["./main.mjs", "child"], {
  stdio: ["ipc", out, "inherit"]
});
```

Ref #16753
This commit is contained in:
Divy Srivastava 2024-06-07 22:51:32 +05:30 committed by Nathan Whitaker
parent 3eb7a2a6d3
commit 8994b51771
No known key found for this signature in database
5 changed files with 62 additions and 17 deletions

View File

@ -362,17 +362,25 @@ export class ChildProcess extends EventEmitter {
}
}
const supportedNodeStdioTypes: NodeStdio[] = ["pipe", "ignore", "inherit"];
const supportedNodeStdioTypes: NodeStdio[] = [
"pipe",
"ignore",
"inherit",
"ipc",
];
function toDenoStdio(
pipe: NodeStdio | number | Stream | null | undefined,
): DenoStdio {
if (pipe instanceof Stream) {
return "inherit";
}
if (typeof pipe === "number") {
/* Assume it's a rid returned by fs APIs */
return pipe;
}
if (
!supportedNodeStdioTypes.includes(pipe as NodeStdio) ||
typeof pipe === "number"
!supportedNodeStdioTypes.includes(pipe as NodeStdio)
) {
notImplemented(`toDenoStdio pipe=${typeof pipe} (${pipe})`);
}
@ -385,6 +393,8 @@ function toDenoStdio(
return "null";
case "inherit":
return "inherit";
case "ipc":
return "ipc_for_internal_use";
default:
notImplemented(`toDenoStdio pipe=${typeof pipe} (${pipe})`);
}
@ -1083,8 +1093,7 @@ function toDenoArgs(args: string[]): string[] {
if (useRunArgs) {
// -A is not ideal, but needed to propagate permissions.
// --unstable is needed for Node compat.
denoArgs.unshift("run", "-A", "--unstable");
denoArgs.unshift("run", "-A");
}
return denoArgs;

View File

@ -37,11 +37,12 @@ use std::os::unix::process::CommandExt;
pub const UNSTABLE_FEATURE_NAME: &str = "process";
#[derive(Copy, Clone, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(rename_all = "snake_case")]
pub enum Stdio {
Inherit,
Piped,
Null,
IpcForInternalUse,
}
impl Stdio {
@ -50,6 +51,7 @@ impl Stdio {
Stdio::Inherit => std::process::Stdio::inherit(),
Stdio::Piped => std::process::Stdio::piped(),
Stdio::Null => std::process::Stdio::null(),
_ => unreachable!(),
}
}
}
@ -72,6 +74,9 @@ impl<'de> Deserialize<'de> for StdioOrRid {
"inherit" => Ok(StdioOrRid::Stdio(Stdio::Inherit)),
"piped" => Ok(StdioOrRid::Stdio(Stdio::Piped)),
"null" => Ok(StdioOrRid::Stdio(Stdio::Null)),
"ipc_for_internal_use" => {
Ok(StdioOrRid::Stdio(Stdio::IpcForInternalUse))
}
val => Err(serde::de::Error::unknown_variant(
val,
&["inherit", "piped", "null"],
@ -102,6 +107,10 @@ impl StdioOrRid {
}
}
}
pub fn is_ipc(&self) -> bool {
matches!(self, StdioOrRid::Stdio(Stdio::IpcForInternalUse))
}
}
deno_core::extension!(
@ -150,9 +159,9 @@ pub struct SpawnArgs {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChildStdio {
stdin: Stdio,
stdout: Stdio,
stderr: Stdio,
stdin: StdioOrRid,
stdout: StdioOrRid,
stderr: StdioOrRid,
}
#[derive(Serialize)]
@ -210,7 +219,7 @@ type CreateCommand = (std::process::Command, Option<ResourceId>);
fn create_command(
state: &mut OpState,
args: SpawnArgs,
mut args: SpawnArgs,
api_name: &str,
) -> Result<CreateCommand, AnyError> {
state
@ -249,14 +258,19 @@ fn create_command(
command.uid(uid);
}
command.stdin(args.stdio.stdin.as_stdio());
if args.stdio.stdin.is_ipc() {
args.ipc = Some(0);
} else {
command.stdin(args.stdio.stdin.as_stdio(state)?);
}
command.stdout(match args.stdio.stdout {
Stdio::Inherit => StdioOrRid::Rid(1).as_stdio(state)?,
value => value.as_stdio(),
StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(1).as_stdio(state)?,
value => value.as_stdio(state)?,
});
command.stderr(match args.stdio.stderr {
Stdio::Inherit => StdioOrRid::Rid(2).as_stdio(state)?,
value => value.as_stdio(),
StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(2).as_stdio(state)?,
value => value.as_stdio(state)?,
});
#[cfg(unix)]
@ -608,8 +622,8 @@ fn op_spawn_sync(
state: &mut OpState,
#[serde] args: SpawnArgs,
) -> Result<SpawnOutput, AnyError> {
let stdout = matches!(args.stdio.stdout, Stdio::Piped);
let stderr = matches!(args.stdio.stderr, Stdio::Piped);
let stdout = matches!(args.stdio.stdout, StdioOrRid::Stdio(Stdio::Piped));
let stderr = matches!(args.stdio.stderr, StdioOrRid::Stdio(Stdio::Piped));
let (mut command, _) =
create_command(state, args, "Deno.Command().outputSync()")?;
let output = command.output().with_context(|| {

View File

@ -0,0 +1,5 @@
{
"args": "run -A main.mjs",
"output": "main.out",
"exitCode": 0
}

View File

@ -0,0 +1,16 @@
import { spawn } from "node:child_process";
import process from "node:process";
if (process.argv[2] === "child") {
process.send("hahah");
} else {
const proc = spawn(process.execPath, ["./main.mjs", "child"], {
stdio: ["ipc", "inherit", "inherit"],
});
proc.on("message", function (msg) {
console.log(`msg: ${msg}`);
proc.kill();
Deno.exit(0);
});
}

View File

@ -0,0 +1 @@
msg: hahah