// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use super::check_unstable; use crate::permissions::PermissionsContainer; use deno_core::anyhow::Context; use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::op2; use deno_core::serde_json; use deno_core::AsyncMutFuture; use deno_core::AsyncRefCell; use deno_core::OpState; use deno_core::RcRef; use deno_core::Resource; use deno_core::ResourceId; use deno_core::ToJsBuffer; use deno_io::fs::FileResource; use deno_io::ChildStderrResource; use deno_io::ChildStdinResource; use deno_io::ChildStdoutResource; use serde::Deserialize; use serde::Serialize; use std::borrow::Cow; use std::cell::RefCell; use std::process::ExitStatus; use std::rc::Rc; use tokio::process::Command; #[cfg(windows)] use std::os::windows::process::CommandExt; #[cfg(unix)] use std::os::unix::prelude::ExitStatusExt; #[cfg(unix)] use std::os::unix::process::CommandExt; pub const UNSTABLE_FEATURE_NAME: &str = "process"; #[derive(Copy, Clone, Eq, 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, Eq, 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) => { FileResource::with_file(state, *rid, |file| Ok(file.as_stdio()?)) } } } } deno_core::extension!( deno_process, ops = [ op_spawn_child, op_spawn_wait, op_spawn_sync, op_spawn_kill, deprecated::op_run, deprecated::op_run_status, deprecated::op_kill, ], ); /// Second member stores the pid separately from the RefCell. It's needed for /// `op_spawn_kill`, where the RefCell is borrowed mutably by `op_spawn_wait`. struct ChildResource(RefCell, u32); 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, ipc: Option, #[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, } type CreateCommand = (std::process::Command, Option); fn 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); } 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(), }); #[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_rid = Some( state .resource_table .add(deno_node::IpcJsonStreamResource::new(fd1 as _)?), ); /* The other end passed to child process via DENO_CHANNEL_FD */ command.env("DENO_CHANNEL_FD", format!("{}", ipc)); return Ok((command, pipe_rid)); } Ok((command, None)) } #[cfg(windows)] // Safety: We setup a windows named pipe and pass one end to the child process. unsafe { use windows_sys::Win32::Foundation::CloseHandle; use windows_sys::Win32::Foundation::DuplicateHandle; use windows_sys::Win32::Foundation::DUPLICATE_SAME_ACCESS; use windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED; use windows_sys::Win32::Foundation::ERROR_PIPE_CONNECTED; use windows_sys::Win32::Foundation::GENERIC_READ; use windows_sys::Win32::Foundation::GENERIC_WRITE; use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; use windows_sys::Win32::Security::SECURITY_ATTRIBUTES; use windows_sys::Win32::Storage::FileSystem::CreateFileW; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_FIRST_PIPE_INSTANCE; use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OVERLAPPED; use windows_sys::Win32::Storage::FileSystem::OPEN_EXISTING; use windows_sys::Win32::Storage::FileSystem::PIPE_ACCESS_DUPLEX; use windows_sys::Win32::System::Pipes::ConnectNamedPipe; use windows_sys::Win32::System::Pipes::CreateNamedPipeW; use windows_sys::Win32::System::Pipes::PIPE_READMODE_BYTE; use windows_sys::Win32::System::Pipes::PIPE_TYPE_BYTE; use windows_sys::Win32::System::Threading::GetCurrentProcess; use std::io; use std::os::windows::ffi::OsStrExt; use std::path::Path; use std::ptr; if let Some(ipc) = args.ipc { if ipc < 0 { return Ok((command, None)); } let (path, hd1) = loop { let name = format!("\\\\.\\pipe\\{}", uuid::Uuid::new_v4()); let mut path = Path::new(&name) .as_os_str() .encode_wide() .collect::>(); path.push(0); let hd1 = CreateNamedPipeW( path.as_ptr(), PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE | FILE_FLAG_OVERLAPPED, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, 1, 65536, 65536, 0, std::ptr::null_mut(), ); if hd1 == INVALID_HANDLE_VALUE { let err = io::Error::last_os_error(); /* If the pipe name is already in use, try again. */ if err.raw_os_error() == Some(ERROR_ACCESS_DENIED as i32) { continue; } return Err(err.into()); } break (path, hd1); }; /* Create child pipe handle. */ let s = SECURITY_ATTRIBUTES { nLength: std::mem::size_of::() as u32, lpSecurityDescriptor: ptr::null_mut(), bInheritHandle: 1, }; let mut hd2 = CreateFileW( path.as_ptr(), GENERIC_READ | GENERIC_WRITE, 0, &s, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0, ); if hd2 == INVALID_HANDLE_VALUE { return Err(io::Error::last_os_error().into()); } // Will not block because we have create the pair. if ConnectNamedPipe(hd1, ptr::null_mut()) == 0 { let err = std::io::Error::last_os_error(); if err.raw_os_error() != Some(ERROR_PIPE_CONNECTED as i32) { CloseHandle(hd2); return Err(err.into()); } } // Duplicating the handle to allow the child process to use it. if DuplicateHandle( GetCurrentProcess(), hd2, GetCurrentProcess(), &mut hd2, 0, 1, DUPLICATE_SAME_ACCESS, ) == 0 { return Err(std::io::Error::last_os_error().into()); } /* One end returned to parent process (this) */ let pipe_fd = Some( state .resource_table .add(deno_node::IpcJsonStreamResource::new(hd1 as i64)?), ); /* The other end passed to child process via DENO_CHANNEL_FD */ command.env("DENO_CHANNEL_FD", format!("{}", hd2 as i64)); return Ok((command, pipe_fd)); } } #[cfg(not(unix))] return Ok((command, None)); } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct Child { rid: ResourceId, pid: u32, stdin_rid: Option, stdout_rid: Option, stderr_rid: Option, pipe_fd: Option, } fn spawn_child( state: &mut OpState, command: std::process::Command, pipe_fd: Option, ) -> 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 = match command.spawn() { Ok(child) => child, Err(err) => { let command = command.as_std(); let command_name = command.get_program().to_string_lossy(); if let Some(cwd) = command.get_current_dir() { // launching a sub process always depends on the real // file system so using these methods directly is ok #[allow(clippy::disallowed_methods)] if !cwd.exists() { return Err( std::io::Error::new( std::io::ErrorKind::NotFound, format!( "Failed to spawn '{}': No such cwd '{}'", command_name, cwd.to_string_lossy() ), ) .into(), ); } #[allow(clippy::disallowed_methods)] if !cwd.is_dir() { return Err( std::io::Error::new( std::io::ErrorKind::NotFound, format!( "Failed to spawn '{}': cwd is not a directory '{}'", command_name, cwd.to_string_lossy() ), ) .into(), ); } } return Err(AnyError::from(err).context(format!( "Failed to spawn '{}'", command.get_program().to_string_lossy() ))); } }; 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(RefCell::new(child), pid)); Ok(Child { rid: child_rid, pid, stdin_rid, stdout_rid, stderr_rid, pipe_fd, }) } #[op2] #[serde] fn op_spawn_child( state: &mut OpState, #[serde] args: SpawnArgs, #[string] api_name: String, ) -> Result { let (command, pipe_rid) = create_command(state, args, &api_name)?; spawn_child(state, command, pipe_rid) } #[op2(async)] #[allow(clippy::await_holding_refcell_ref)] #[serde] async fn op_spawn_wait( state: Rc>, #[smi] rid: ResourceId, ) -> Result { let resource = state .borrow_mut() .resource_table .get::(rid)?; let result = resource.0.try_borrow_mut()?.wait().await?.try_into(); if let Ok(resource) = state.borrow_mut().resource_table.take_any(rid) { resource.close(); } result } #[op2] #[serde] fn op_spawn_sync( state: &mut OpState, #[serde] args: SpawnArgs, ) -> Result { 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 output = command.output().with_context(|| { format!( "Failed to spawn '{}'", command.get_program().to_string_lossy() ) })?; 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 }, }) } #[op2(fast)] fn op_spawn_kill( state: &mut OpState, #[smi] rid: ResourceId, #[string] signal: String, ) -> Result<(), AnyError> { if let Ok(child_resource) = state.resource_table.get::(rid) { deprecated::kill(child_resource.1 as i32, &signal)?; return Ok(()); } Err(type_error("Child process has already terminated.")) } mod deprecated { use super::*; #[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 pub struct RunInfo { rid: ResourceId, pid: Option, stdin_rid: Option, stdout_rid: Option, stderr_rid: Option, } #[op2] #[serde] pub fn op_run( state: &mut OpState, #[serde] run_args: RunArgs, ) -> Result { let args = run_args.cmd; state .borrow_mut::() .check_run(&args[0], "Deno.run()")?; let env = run_args.env; let cwd = run_args.cwd; let mut c = Command::new(args.first().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, UNSTABLE_FEATURE_NAME, "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, UNSTABLE_FEATURE_NAME, "Deno.run.gid"); c.gid(gid); } #[cfg(unix)] if let Some(uid) = run_args.uid { super::check_unstable(state, UNSTABLE_FEATURE_NAME, "Deno.run.uid"); c.uid(uid); } #[cfg(unix)] // TODO(bartlomieju): #[allow(clippy::undocumented_unsafe_blocks)] 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")] pub struct ProcessStatus { got_signal: bool, exit_code: i32, exit_signal: i32, } #[op2(async)] #[serde] pub async fn op_run_status( state: Rc>, #[smi] 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 = Default::default(); 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::super::signal::signal_str_to_int(signal)?; use nix::sys::signal::kill as unix_kill; use nix::sys::signal::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 std::io::Error; use std::io::ErrorKind::NotFound; use winapi::shared::minwindef::DWORD; 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::winnt::PROCESS_TERMINATE; if !matches!(signal, "SIGKILL" | "SIGTERM") { Err(type_error(format!("Invalid signal: {signal}"))) } else if pid <= 0 { Err(type_error("Invalid pid")) } else { let handle = // SAFETY: winapi call unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as DWORD) }; if handle.is_null() { // SAFETY: winapi call 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 { // SAFETY: winapi calls unsafe { let is_terminated = TerminateProcess(handle, 1); CloseHandle(handle); match is_terminated { FALSE => Err(Error::last_os_error().into()), TRUE => Ok(()), _ => unreachable!(), } } } } } #[op2(fast)] pub fn op_kill( state: &mut OpState, #[smi] pid: i32, #[string] signal: String, #[string] api_name: String, ) -> Result<(), AnyError> { state .borrow_mut::() .check_run_all(&api_name)?; kill(pid, &signal)?; Ok(()) } }