// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use super::io::ChildStderrResource; use super::io::ChildStdinResource; use super::io::ChildStdoutResource; use super::process::Stdio; use super::process::StdioOrRid; use crate::permissions::PermissionsContainer; use deno_core::error::AnyError; use deno_core::op; use deno_core::Extension; use deno_core::OpState; use deno_core::Resource; use deno_core::ResourceId; use deno_core::ZeroCopyBuf; use serde::Deserialize; use serde::Serialize; use std::borrow::Cow; use std::cell::RefCell; #[cfg(windows)] use std::os::windows::process::CommandExt; use std::process::ExitStatus; use std::rc::Rc; #[cfg(unix)] use std::os::unix::prelude::ExitStatusExt; #[cfg(unix)] use std::os::unix::process::CommandExt; pub fn init() -> Extension { Extension::builder("deno_spawn") .ops(vec![ op_spawn_child::decl(), op_node_unstable_spawn_child::decl(), op_spawn_wait::decl(), op_spawn_sync::decl(), op_node_unstable_spawn_sync::decl(), ]) .build() } struct ChildResource(tokio::process::Child); impl Resource for ChildResource { fn name(&self) -> Cow { "child".into() } } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct SpawnArgs { cmd: String, args: Vec, cwd: Option, clear_env: bool, env: Vec<(String, String)>, #[cfg(unix)] gid: Option, #[cfg(unix)] uid: Option, #[cfg(windows)] windows_raw_arguments: bool, #[serde(flatten)] stdio: ChildStdio, } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct ChildStdio { stdin: Stdio, stdout: Stdio, stderr: Stdio, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct ChildStatus { success: bool, code: i32, signal: Option, } impl TryFrom for ChildStatus { type Error = AnyError; fn try_from(status: ExitStatus) -> Result { let code = status.code(); #[cfg(unix)] let signal = status.signal(); #[cfg(not(unix))] let signal: Option = None; let status = if let Some(signal) = signal { ChildStatus { success: false, code: 128 + signal, #[cfg(unix)] signal: Some( crate::ops::signal::signal_int_to_str(signal)?.to_string(), ), #[cfg(not(unix))] signal: None, } } else { let code = code.expect("Should have either an exit code or a signal."); ChildStatus { success: code == 0, code, signal: None, } }; Ok(status) } } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SpawnOutput { status: ChildStatus, stdout: Option, stderr: Option, } fn node_unstable_create_command( state: &mut OpState, args: SpawnArgs, api_name: &str, ) -> Result { state .borrow_mut::() .check_run(&args.cmd, api_name)?; let mut command = std::process::Command::new(args.cmd); #[cfg(windows)] if args.windows_raw_arguments { for arg in args.args.iter() { command.raw_arg(arg); } } else { command.args(args.args); } #[cfg(not(windows))] command.args(args.args); if let Some(cwd) = args.cwd { command.current_dir(cwd); } if args.clear_env { command.env_clear(); } command.envs(args.env); #[cfg(unix)] if let Some(gid) = args.gid { command.gid(gid); } #[cfg(unix)] 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 { Stdio::Inherit => StdioOrRid::Rid(1).as_stdio(state)?, value => value.as_stdio(), }); command.stderr(match args.stdio.stderr { Stdio::Inherit => StdioOrRid::Rid(2).as_stdio(state)?, value => value.as_stdio(), }); Ok(command) } fn create_command( state: &mut OpState, args: SpawnArgs, api_name: &str, ) -> Result { super::check_unstable(state, "Deno.Command"); state .borrow_mut::() .check_run(&args.cmd, api_name)?; let mut command = std::process::Command::new(args.cmd); #[cfg(windows)] if args.windows_raw_arguments { for arg in args.args.iter() { command.raw_arg(arg); } } else { command.args(args.args); } #[cfg(not(windows))] command.args(args.args); if let Some(cwd) = args.cwd { command.current_dir(cwd); } if args.clear_env { command.env_clear(); } command.envs(args.env); #[cfg(unix)] if let Some(gid) = args.gid { super::check_unstable(state, "Deno.CommandOptions.gid"); command.gid(gid); } #[cfg(unix)] if let Some(uid) = args.uid { super::check_unstable(state, "Deno.CommandOptions.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 { Stdio::Inherit => StdioOrRid::Rid(1).as_stdio(state)?, value => value.as_stdio(), }); command.stderr(match args.stdio.stderr { Stdio::Inherit => StdioOrRid::Rid(2).as_stdio(state)?, value => value.as_stdio(), }); Ok(command) } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct Child { rid: ResourceId, pid: u32, stdin_rid: Option, stdout_rid: Option, stderr_rid: Option, } fn spawn_child( state: &mut OpState, command: std::process::Command, ) -> Result { let mut command = tokio::process::Command::from(command); // TODO(@crowlkats): allow detaching processes. // currently deno will orphan a process when exiting with an error or Deno.exit() // We want to kill child when it's closed command.kill_on_drop(true); let mut child = command.spawn()?; let pid = child.id().expect("Process ID should be set."); let stdin_rid = child .stdin .take() .map(|stdin| state.resource_table.add(ChildStdinResource::from(stdin))); let stdout_rid = child .stdout .take() .map(|stdout| state.resource_table.add(ChildStdoutResource::from(stdout))); let stderr_rid = child .stderr .take() .map(|stderr| state.resource_table.add(ChildStderrResource::from(stderr))); let child_rid = state.resource_table.add(ChildResource(child)); Ok(Child { rid: child_rid, pid, stdin_rid, stdout_rid, stderr_rid, }) } #[op] fn op_spawn_child( state: &mut OpState, args: SpawnArgs, api_name: String, ) -> Result { let command = create_command(state, args, &api_name)?; spawn_child(state, command) } #[op] fn op_node_unstable_spawn_child( state: &mut OpState, args: SpawnArgs, api_name: String, ) -> Result { let command = node_unstable_create_command(state, args, &api_name)?; spawn_child(state, command) } #[op] async fn op_spawn_wait( state: Rc>, rid: ResourceId, ) -> Result { let resource = state .borrow_mut() .resource_table .take::(rid)?; Rc::try_unwrap(resource) .ok() .unwrap() .0 .wait() .await? .try_into() } #[op] fn op_spawn_sync( state: &mut OpState, args: SpawnArgs, ) -> Result { let stdout = matches!(args.stdio.stdout, Stdio::Piped); let stderr = matches!(args.stdio.stderr, Stdio::Piped); let output = create_command(state, args, "Deno.Command().outputSync()")?.output()?; Ok(SpawnOutput { status: output.status.try_into()?, stdout: if stdout { Some(output.stdout.into()) } else { None }, stderr: if stderr { Some(output.stderr.into()) } else { None }, }) } #[op] fn op_node_unstable_spawn_sync( state: &mut OpState, args: SpawnArgs, ) -> Result { let stdout = matches!(args.stdio.stdout, Stdio::Piped); let stderr = matches!(args.stdio.stderr, Stdio::Piped); let output = node_unstable_create_command(state, args, "Deno.Command().outputSync()")? .output()?; Ok(SpawnOutput { status: output.status.try_into()?, stdout: if stdout { Some(output.stdout.into()) } else { None }, stderr: if stderr { Some(output.stderr.into()) } else { None }, }) }