summaryrefslogtreecommitdiff
path: root/runtime
diff options
context:
space:
mode:
authorDivy Srivastava <dj.srivastava23@gmail.com>2023-12-13 15:44:16 +0530
committerGitHub <noreply@github.com>2023-12-13 11:14:16 +0100
commit5a91a065b882215dde209baf626247e54c21a392 (patch)
tree192cb8b3b0a4037453b5fd5b2a60e4d52d4543a8 /runtime
parentbbf8f69cb979be0f36c38ae52b1588e648b3252e (diff)
fix: implement child_process IPC (#21490)
This PR implements the Node child_process IPC functionality in Deno on Unix systems. For `fd > 2` a duplex unix pipe is set up between the parent and child processes. Currently implements data passing via the channel in the JSON serialization format.
Diffstat (limited to 'runtime')
-rw-r--r--runtime/js/40_process.js10
-rw-r--r--runtime/js/99_main.js3
-rw-r--r--runtime/ops/process.rs116
-rw-r--r--runtime/worker_bootstrap.rs5
4 files changed, 119 insertions, 15 deletions
diff --git a/runtime/js/40_process.js b/runtime/js/40_process.js
index b8e05ce5a..e628aeb4a 100644
--- a/runtime/js/40_process.js
+++ b/runtime/js/40_process.js
@@ -159,6 +159,7 @@ function spawnChildInner(opFn, command, apiName, {
stderr = "piped",
signal = undefined,
windowsRawArguments = false,
+ ipc = -1,
} = {}) {
const child = opFn({
cmd: pathFromURL(command),
@@ -172,6 +173,7 @@ function spawnChildInner(opFn, command, apiName, {
stdout,
stderr,
windowsRawArguments,
+ ipc,
}, apiName);
return new ChildProcess(illegalConstructorKey, {
...child,
@@ -203,6 +205,12 @@ class ChildProcess {
#waitPromise;
#waitComplete = false;
+ #pipeFd;
+ // internal, used by ext/node
+ get _pipeFd() {
+ return this.#pipeFd;
+ }
+
#pid;
get pid() {
return this.#pid;
@@ -239,6 +247,7 @@ class ChildProcess {
stdinRid,
stdoutRid,
stderrRid,
+ pipeFd, // internal
} = null) {
if (key !== illegalConstructorKey) {
throw new TypeError("Illegal constructor.");
@@ -246,6 +255,7 @@ class ChildProcess {
this.#rid = rid;
this.#pid = pid;
+ this.#pipeFd = pipeFd;
if (stdinRid !== null) {
this.#stdin = writableStreamForRid(stdinRid);
diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js
index 0469b38bf..5b4b164a2 100644
--- a/runtime/js/99_main.js
+++ b/runtime/js/99_main.js
@@ -440,6 +440,7 @@ function bootstrapMainRuntime(runtimeOptions) {
3: inspectFlag,
5: hasNodeModulesDir,
6: maybeBinaryNpmCommandName,
+ 7: nodeIpcFd,
} = runtimeOptions;
performance.setTimeOrigin(DateNow());
@@ -545,7 +546,7 @@ function bootstrapMainRuntime(runtimeOptions) {
ObjectDefineProperty(globalThis, "Deno", util.readOnly(finalDenoNs));
if (nodeBootstrap) {
- nodeBootstrap(hasNodeModulesDir, maybeBinaryNpmCommandName);
+ nodeBootstrap(hasNodeModulesDir, maybeBinaryNpmCommandName, nodeIpcFd);
}
}
diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs
index 1fdd4bf4d..6f89e5529 100644
--- a/runtime/ops/process.rs
+++ b/runtime/ops/process.rs
@@ -141,6 +141,8 @@ pub struct SpawnArgs {
uid: Option<u32>,
#[cfg(windows)]
windows_raw_arguments: bool,
+ #[cfg(unix)]
+ ipc: Option<i32>,
#[serde(flatten)]
stdio: ChildStdio,
@@ -205,11 +207,18 @@ pub struct SpawnOutput {
stderr: Option<ToJsBuffer>,
}
+type CreateCommand = (
+ std::process::Command,
+ // TODO(@littledivy): Ideally this would return Option<ResourceId> but we are dealing with file descriptors
+ // all the way until setupChannel which makes it easier to share code between parent and child fork.
+ Option<i32>,
+);
+
fn create_command(
state: &mut OpState,
args: SpawnArgs,
api_name: &str,
-) -> Result<std::process::Command, AnyError> {
+) -> Result<CreateCommand, AnyError> {
state
.borrow_mut::<PermissionsContainer>()
.check_run(&args.cmd, api_name)?;
@@ -245,15 +254,6 @@ fn create_command(
if let Some(uid) = args.uid {
command.uid(uid);
}
- #[cfg(unix)]
- // TODO(bartlomieju):
- #[allow(clippy::undocumented_unsafe_blocks)]
- unsafe {
- command.pre_exec(|| {
- libc::setgroups(0, std::ptr::null());
- Ok(())
- });
- }
command.stdin(args.stdio.stdin.as_stdio());
command.stdout(match args.stdio.stdout {
@@ -265,7 +265,91 @@ fn create_command(
value => value.as_stdio(),
});
- Ok(command)
+ #[cfg(unix)]
+ // TODO(bartlomieju):
+ #[allow(clippy::undocumented_unsafe_blocks)]
+ unsafe {
+ if let Some(ipc) = args.ipc {
+ if ipc < 0 {
+ return Ok((command, None));
+ }
+ // SockFlag is broken on macOS
+ // https://github.com/nix-rust/nix/issues/861
+ let mut fds = [-1, -1];
+ #[cfg(not(target_os = "macos"))]
+ let flags = libc::SOCK_CLOEXEC | libc::SOCK_NONBLOCK;
+
+ #[cfg(target_os = "macos")]
+ let flags = 0;
+
+ let ret = libc::socketpair(
+ libc::AF_UNIX,
+ libc::SOCK_STREAM | flags,
+ 0,
+ fds.as_mut_ptr(),
+ );
+ if ret != 0 {
+ return Err(std::io::Error::last_os_error().into());
+ }
+
+ if cfg!(target_os = "macos") {
+ let fcntl =
+ |fd: i32, flag: libc::c_int| -> Result<(), std::io::Error> {
+ let flags = libc::fcntl(fd, libc::F_GETFL, 0);
+
+ if flags == -1 {
+ return Err(fail(fds));
+ }
+ let ret = libc::fcntl(fd, libc::F_SETFL, flags | flag);
+ if ret == -1 {
+ return Err(fail(fds));
+ }
+ Ok(())
+ };
+
+ fn fail(fds: [i32; 2]) -> std::io::Error {
+ unsafe {
+ libc::close(fds[0]);
+ libc::close(fds[1]);
+ }
+ std::io::Error::last_os_error()
+ }
+
+ // SOCK_NONBLOCK is not supported on macOS.
+ (fcntl)(fds[0], libc::O_NONBLOCK)?;
+ (fcntl)(fds[1], libc::O_NONBLOCK)?;
+
+ // SOCK_CLOEXEC is not supported on macOS.
+ (fcntl)(fds[0], libc::FD_CLOEXEC)?;
+ (fcntl)(fds[1], libc::FD_CLOEXEC)?;
+ }
+
+ let fd1 = fds[0];
+ let fd2 = fds[1];
+
+ command.pre_exec(move || {
+ if ipc >= 0 {
+ let _fd = libc::dup2(fd2, ipc);
+ libc::close(fd2);
+ }
+ libc::setgroups(0, std::ptr::null());
+ Ok(())
+ });
+
+ /* One end returned to parent process (this) */
+ let pipe_fd = Some(fd1);
+
+ /* The other end passed to child process via DENO_CHANNEL_FD */
+ command.env("DENO_CHANNEL_FD", format!("{}", ipc));
+
+ return Ok((command, pipe_fd));
+ }
+
+ Ok((command, None))
+ }
+
+ #[cfg(not(unix))]
+ return Ok((command, None));
}
#[derive(Serialize)]
@@ -276,11 +360,13 @@ struct Child {
stdin_rid: Option<ResourceId>,
stdout_rid: Option<ResourceId>,
stderr_rid: Option<ResourceId>,
+ pipe_fd: Option<i32>,
}
fn spawn_child(
state: &mut OpState,
command: std::process::Command,
+ pipe_fd: Option<i32>,
) -> Result<Child, AnyError> {
let mut command = tokio::process::Command::from(command);
// TODO(@crowlkats): allow detaching processes.
@@ -362,6 +448,7 @@ fn spawn_child(
stdin_rid,
stdout_rid,
stderr_rid,
+ pipe_fd,
})
}
@@ -372,8 +459,8 @@ fn op_spawn_child(
#[serde] args: SpawnArgs,
#[string] api_name: String,
) -> Result<Child, AnyError> {
- let command = create_command(state, args, &api_name)?;
- spawn_child(state, command)
+ let (command, pipe_fd) = create_command(state, args, &api_name)?;
+ spawn_child(state, command, pipe_fd)
}
#[op2(async)]
@@ -402,7 +489,8 @@ fn op_spawn_sync(
) -> Result<SpawnOutput, AnyError> {
let stdout = matches!(args.stdio.stdout, Stdio::Piped);
let stderr = matches!(args.stdio.stderr, Stdio::Piped);
- let mut command = create_command(state, args, "Deno.Command().outputSync()")?;
+ let (mut command, _) =
+ create_command(state, args, "Deno.Command().outputSync()")?;
let output = command.output().with_context(|| {
format!(
"Failed to spawn '{}'",
diff --git a/runtime/worker_bootstrap.rs b/runtime/worker_bootstrap.rs
index 828bb3766..8674190f3 100644
--- a/runtime/worker_bootstrap.rs
+++ b/runtime/worker_bootstrap.rs
@@ -59,6 +59,7 @@ pub struct BootstrapOptions {
pub inspect: bool,
pub has_node_modules_dir: bool,
pub maybe_binary_npm_command_name: Option<String>,
+ pub node_ipc_fd: Option<i32>,
}
impl Default for BootstrapOptions {
@@ -86,6 +87,7 @@ impl Default for BootstrapOptions {
args: Default::default(),
has_node_modules_dir: Default::default(),
maybe_binary_npm_command_name: None,
+ node_ipc_fd: None,
}
}
}
@@ -115,6 +117,8 @@ struct BootstrapV8<'a>(
bool,
// maybe_binary_npm_command_name
Option<&'a str>,
+ // node_ipc_fd
+ i32,
);
impl BootstrapOptions {
@@ -134,6 +138,7 @@ impl BootstrapOptions {
self.enable_testing_features,
self.has_node_modules_dir,
self.maybe_binary_npm_command_name.as_deref(),
+ self.node_ipc_fd.unwrap_or(-1),
);
bootstrap.serialize(ser).unwrap()