diff options
author | Divy Srivastava <dj.srivastava23@gmail.com> | 2023-12-13 15:44:16 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-12-13 11:14:16 +0100 |
commit | 5a91a065b882215dde209baf626247e54c21a392 (patch) | |
tree | 192cb8b3b0a4037453b5fd5b2a60e4d52d4543a8 /runtime/ops/process.rs | |
parent | bbf8f69cb979be0f36c38ae52b1588e648b3252e (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/ops/process.rs')
-rw-r--r-- | runtime/ops/process.rs | 116 |
1 files changed, 102 insertions, 14 deletions
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 '{}'", |