Repository: denoland/deno
Pull request: fix(ext/node): support stdin child_process IPC & fd stdout/stderr

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 

Changed files:
filename: ext/node/polyfills/internal/child_process.ts
patch: @@ -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;

filename: runtime/ops/process.rs
patch: @@ -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(|| {

filename: tests/specs/node/stdio_ipc/__test__.jsonc
patch: @@ -0,0 +1,5 @@
+{
+  "args": "run -A main.mjs",
+  "output": "main.out",
+  "exitCode": 0
+}

filename: tests/specs/node/stdio_ipc/main.mjs
patch: @@ -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);
+  });
+}

filename: tests/specs/node/stdio_ipc/main.out
patch: @@ -0,0 +1 @@
+msg: hahah