Skip to content

Commit

Permalink
fix(ext/node): support stdin child_process IPC & fd stdout/stderr (#2…
Browse files Browse the repository at this point in the history
…4106)

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
  • Loading branch information
littledivy committed Jun 7, 2024
1 parent ed20102 commit 3735a1a
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 17 deletions.
19 changes: 14 additions & 5 deletions ext/node/polyfills/internal/child_process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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})`);
}
Expand All @@ -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})`);
}
Expand Down Expand Up @@ -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;
Expand Down
38 changes: 26 additions & 12 deletions runtime/ops/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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!(),
}
}
}
Expand All @@ -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"],
Expand Down Expand Up @@ -102,6 107,10 @@ impl StdioOrRid {
}
}
}

pub fn is_ipc(&self) -> bool {
matches!(self, StdioOrRid::Stdio(Stdio::IpcForInternalUse))
}
}

deno_core::extension!(
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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(|| {
Expand Down
5 changes: 5 additions & 0 deletions tests/specs/node/stdio_ipc/__test__.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 1,5 @@
{
"args": "run -A main.mjs",
"output": "main.out",
"exitCode": 0
}
16 changes: 16 additions & 0 deletions tests/specs/node/stdio_ipc/main.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
}
1 change: 1 addition & 0 deletions tests/specs/node/stdio_ipc/main.out
Original file line number Diff line number Diff line change
@@ -0,0 1 @@
msg: hahah

0 comments on commit 3735a1a

Please sign in to comment.