// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. use super::io::ChildStderrResource; use super::io::ChildStdinResource; use super::io::ChildStdoutResource; use super::io::StdFileResource; use crate::permissions::Permissions; use deno_core::error::AnyError; use deno_core::op; use deno_core::serde_json; use deno_core::AsyncMutFuture; use deno_core::AsyncRefCell; use deno_core::Extension; use deno_core::OpState; use deno_core::RcRef; use deno_core::Resource; use deno_core::ResourceId; use serde::Deserialize; use serde::Serialize; use std::borrow::Cow; use std::cell::RefCell; use std::rc::Rc; use tokio::process::Command; #[cfg(unix)] use std::os::unix::process::ExitStatusExt; pub fn init() -> Extension { Extension::builder() .ops(vec![op_run::decl(), op_run_status::decl(), op_kill::decl()]) .build() } #[derive(Copy, Clone, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Stdio { Inherit, Piped, Null, } impl Stdio { pub fn as_stdio(&self) -> std::process::Stdio { match &self { Stdio::Inherit => std::process::Stdio::inherit(), Stdio::Piped => std::process::Stdio::piped(), Stdio::Null => std::process::Stdio::null(), } } } #[derive(Copy, Clone, PartialEq)] pub enum StdioOrRid { Stdio(Stdio), Rid(ResourceId), } impl<'de> Deserialize<'de> for StdioOrRid { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { use serde_json::Value; let value = Value::deserialize(deserializer)?; match value { Value::String(val) => match val.as_str() { "inherit" => Ok(StdioOrRid::Stdio(Stdio::Inherit)), "piped" => Ok(StdioOrRid::Stdio(Stdio::Piped)), "null" => Ok(StdioOrRid::Stdio(Stdio::Null)), val => Err(serde::de::Error::unknown_variant( val, &["inherit", "piped", "null"], )), }, Value::Number(val) => match val.as_u64() { Some(val) if val <= ResourceId::MAX as u64 => { Ok(StdioOrRid::Rid(val as ResourceId)) } _ => Err(serde::de::Error::custom("Expected a positive integer")), }, _ => Err(serde::de::Error::custom( r#"Expected a resource id, "inherit", "piped", or "null""#, )), } } } impl StdioOrRid { pub fn as_stdio( &self, state: &mut OpState, ) -> Result { match &self { StdioOrRid::Stdio(val) => Ok(val.as_stdio()), StdioOrRid::Rid(rid) => StdFileResource::as_stdio(state, *rid), } } } #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct RunArgs { cmd: Vec, cwd: Option, clear_env: bool, env: Vec<(String, String)>, #[cfg(unix)] gid: Option, #[cfg(unix)] uid: Option, stdin: StdioOrRid, stdout: StdioOrRid, stderr: StdioOrRid, } struct ChildResource { child: AsyncRefCell, } impl Resource for ChildResource { fn name(&self) -> Cow { "child".into() } } impl ChildResource { fn borrow_mut(self: Rc) -> AsyncMutFuture { RcRef::map(self, |r| &r.child).borrow_mut() } } #[derive(Serialize)] #[serde(rename_all = "camelCase")] // TODO(@AaronO): maybe find a more descriptive name or a convention for return structs struct RunInfo { rid: ResourceId, pid: Option, stdin_rid: Option, stdout_rid: Option, stderr_rid: Option, } #[op] fn op_run(state: &mut OpState, run_args: RunArgs) -> Result { let args = run_args.cmd; state.borrow_mut::().run.check(&args[0])?; let env = run_args.env; let cwd = run_args.cwd; let mut c = Command::new(args.get(0).unwrap()); (1..args.len()).for_each(|i| { let arg = args.get(i).unwrap(); c.arg(arg); }); cwd.map(|d| c.current_dir(d)); if run_args.clear_env { super::check_unstable(state, "Deno.run.clearEnv"); c.env_clear(); } for (key, value) in &env { c.env(key, value); } #[cfg(unix)] if let Some(gid) = run_args.gid { super::check_unstable(state, "Deno.run.gid"); c.gid(gid); } #[cfg(unix)] if let Some(uid) = run_args.uid { super::check_unstable(state, "Deno.run.uid"); c.uid(uid); } #[cfg(unix)] unsafe { c.pre_exec(|| { libc::setgroups(0, std::ptr::null()); Ok(()) }); } // TODO: make this work with other resources, eg. sockets c.stdin(run_args.stdin.as_stdio(state)?); c.stdout( match run_args.stdout { StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(1), value => value, } .as_stdio(state)?, ); c.stderr( match run_args.stderr { StdioOrRid::Stdio(Stdio::Inherit) => StdioOrRid::Rid(2), value => value, } .as_stdio(state)?, ); // We want to kill child when it's closed c.kill_on_drop(true); // Spawn the command. let mut child = c.spawn()?; let pid = child.id(); let stdin_rid = match child.stdin.take() { Some(child_stdin) => { let rid = state .resource_table .add(ChildStdinResource::from(child_stdin)); Some(rid) } None => None, }; let stdout_rid = match child.stdout.take() { Some(child_stdout) => { let rid = state .resource_table .add(ChildStdoutResource::from(child_stdout)); Some(rid) } None => None, }; let stderr_rid = match child.stderr.take() { Some(child_stderr) => { let rid = state .resource_table .add(ChildStderrResource::from(child_stderr)); Some(rid) } None => None, }; let child_resource = ChildResource { child: AsyncRefCell::new(child), }; let child_rid = state.resource_table.add(child_resource); Ok(RunInfo { rid: child_rid, pid, stdin_rid, stdout_rid, stderr_rid, }) } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct ProcessStatus { got_signal: bool, exit_code: i32, exit_signal: i32, } #[op] async fn op_run_status( state: Rc>, rid: ResourceId, ) -> Result { let resource = state .borrow_mut() .resource_table .get::(rid)?; let mut child = resource.borrow_mut().await; let run_status = child.wait().await?; let code = run_status.code(); #[cfg(unix)] let signal = run_status.signal(); #[cfg(not(unix))] let signal = None; code .or(signal) .expect("Should have either an exit code or a signal."); let got_signal = signal.is_some(); Ok(ProcessStatus { got_signal, exit_code: code.unwrap_or(-1), exit_signal: signal.unwrap_or(-1), }) } #[cfg(unix)] pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> { let signo = super::signal::signal_str_to_int(signal)?; use nix::sys::signal::{kill as unix_kill, Signal}; use nix::unistd::Pid; let sig = Signal::try_from(signo)?; unix_kill(Pid::from_raw(pid), Option::Some(sig)).map_err(AnyError::from) } #[cfg(not(unix))] pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> { use deno_core::error::type_error; use std::io::Error; use std::io::ErrorKind::NotFound; use winapi::shared::minwindef::FALSE; use winapi::shared::minwindef::TRUE; use winapi::shared::winerror::ERROR_INVALID_PARAMETER; use winapi::um::errhandlingapi::GetLastError; use winapi::um::handleapi::CloseHandle; use winapi::um::processthreadsapi::OpenProcess; use winapi::um::processthreadsapi::TerminateProcess; use winapi::um::wincon::GenerateConsoleCtrlEvent; use winapi::um::wincon::{CTRL_BREAK_EVENT, CTRL_CLOSE_EVENT, CTRL_C_EVENT}; use winapi::um::winnt::PROCESS_TERMINATE; if pid < 0 { return Err(type_error(format!( "Invalid process id (pid) {} for signal {}.", pid, signal ))); } if matches!(signal, "SIGINT" | "SIGBREAK" | "SIGHUP") { let is_generated = unsafe { GenerateConsoleCtrlEvent( match signal { "SIGINT" => CTRL_C_EVENT, "SIGBREAK" => CTRL_BREAK_EVENT, // Need tokio::windows::signal::CtrlClose or equivalent // in signal.rs to get this working "SIGHUP" => CTRL_CLOSE_EVENT, _ => unreachable!(), }, pid as u32, ) }; match is_generated { FALSE => { Err(Error::from_raw_os_error(unsafe { GetLastError() } as i32).into()) } TRUE => Ok(()), _ => unreachable!(), } } else if matches!(signal, "SIGKILL" | "SIGTERM") { // PID 0 = System Idle Process and can't be opened. if pid == 0 { return Err(type_error(format!("Cannot use {} on PID 0", signal))); } let handle = unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as u32) }; if handle.is_null() { let err = match unsafe { GetLastError() } { ERROR_INVALID_PARAMETER => Error::from(NotFound), // Invalid `pid`. errno => Error::from_raw_os_error(errno as i32), }; Err(err.into()) } else { let is_terminated = unsafe { TerminateProcess(handle, 1) }; unsafe { CloseHandle(handle) }; match is_terminated { FALSE => Err(Error::last_os_error().into()), TRUE => Ok(()), _ => unreachable!(), } } } else { Err(type_error(format!( "Signal {} is unsupported on Windows.", signal ))) } } #[op] fn op_kill( state: &mut OpState, pid: i32, signal: String, ) -> Result<(), AnyError> { state.borrow_mut::().run.check_all()?; kill(pid, &signal)?; Ok(()) }