// Copyright 2018-2021 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::Permissions; 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; 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() .ops(vec![ op_spawn_child::decl(), op_spawn_wait::decl(), op_spawn_sync::decl(), ]) .build() } struct ChildResource(tokio::process::Child); impl Resource for ChildResource { fn name(&self) -> Cow<str> { "child".into() } } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct SpawnArgs { cmd: String, args: Vec<String>, cwd: Option<String>, clear_env: bool, env: Vec<(String, String)>, #[cfg(unix)] gid: Option<u32>, #[cfg(unix)] uid: Option<u32>, #[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<String>, } impl TryFrom<ExitStatus> for ChildStatus { type Error = AnyError; fn try_from(status: ExitStatus) -> Result<Self, Self::Error> { let code = status.code(); #[cfg(unix)] let signal = status.signal(); #[cfg(not(unix))] let signal: Option<i32> = 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<ZeroCopyBuf>, stderr: Option<ZeroCopyBuf>, } fn create_command( state: &mut OpState, args: SpawnArgs, ) -> Result<std::process::Command, AnyError> { super::check_unstable(state, "Deno.spawn"); state.borrow_mut::<Permissions>().run.check(&args.cmd)?; let mut command = std::process::Command::new(args.cmd); 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.spawn.gid"); command.gid(gid); } #[cfg(unix)] if let Some(uid) = args.uid { super::check_unstable(state, "Deno.spawn.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<ResourceId>, stdout_rid: Option<ResourceId>, stderr_rid: Option<ResourceId>, } #[op] fn op_spawn_child( state: &mut OpState, args: SpawnArgs, ) -> Result<Child, AnyError> { let mut command = tokio::process::Command::from(create_command(state, args)?); // 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] async fn op_spawn_wait( state: Rc<RefCell<OpState>>, rid: ResourceId, ) -> Result<ChildStatus, AnyError> { let resource = state .borrow_mut() .resource_table .take::<ChildResource>(rid)?; Rc::try_unwrap(resource) .ok() .unwrap() .0 .wait() .await? .try_into() } #[op] fn op_spawn_sync( state: &mut OpState, args: SpawnArgs, ) -> Result<SpawnOutput, AnyError> { let stdout = matches!(args.stdio.stdout, Stdio::Piped); let stderr = matches!(args.stdio.stderr, Stdio::Piped); let output = create_command(state, args)?.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 }, }) }