diff options
Diffstat (limited to 'runtime/ops/process.rs')
-rw-r--r-- | runtime/ops/process.rs | 672 |
1 files changed, 473 insertions, 199 deletions
diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs index ca37c08b7..ad14ef2e4 100644 --- a/runtime/ops/process.rs +++ b/runtime/ops/process.rs @@ -1,13 +1,10 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +use super::check_unstable; +use super::signal; use crate::permissions::PermissionsContainer; use deno_core::error::AnyError; use deno_core::op; -use deno_io::ChildStderrResource; -use deno_io::ChildStdinResource; -use deno_io::ChildStdoutResource; -use deno_io::StdFileResource; - use deno_core::serde_json; use deno_core::AsyncMutFuture; use deno_core::AsyncRefCell; @@ -16,21 +13,26 @@ use deno_core::OpState; use deno_core::RcRef; use deno_core::Resource; use deno_core::ResourceId; +use deno_core::ZeroCopyBuf; +use deno_io::ChildStderrResource; +use deno_io::ChildStdinResource; +use deno_io::ChildStdoutResource; +use deno_io::StdFileResource; 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(unix)] -use std::os::unix::process::ExitStatusExt; +#[cfg(windows)] +use std::os::windows::process::CommandExt; -pub fn init() -> Extension { - Extension::builder("deno_process") - .ops(vec![op_run::decl(), op_run_status::decl(), op_kill::decl()]) - .build() -} +#[cfg(unix)] +use std::os::unix::prelude::ExitStatusExt; +#[cfg(unix)] +use std::os::unix::process::CommandExt; #[derive(Copy, Clone, Eq, PartialEq, Deserialize)] #[serde(rename_all = "camelCase")] @@ -98,10 +100,32 @@ impl StdioOrRid { } } +pub fn init_ops() -> Extension { + Extension::builder("deno_process") + .ops(vec![ + op_spawn_child::decl(), + op_spawn_wait::decl(), + op_spawn_sync::decl(), + deprecated::op_run::decl(), + deprecated::op_run_status::decl(), + deprecated::op_kill::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 RunArgs { - cmd: Vec<String>, +pub struct SpawnArgs { + cmd: String, + args: Vec<String>, cwd: Option<String>, clear_env: bool, env: Vec<(String, String)>, @@ -109,142 +133,176 @@ pub struct RunArgs { gid: Option<u32>, #[cfg(unix)] uid: Option<u32>, - stdin: StdioOrRid, - stdout: StdioOrRid, - stderr: StdioOrRid, + #[cfg(windows)] + windows_raw_arguments: bool, + + #[serde(flatten)] + stdio: ChildStdio, } -struct ChildResource { - child: AsyncRefCell<tokio::process::Child>, +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ChildStdio { + stdin: Stdio, + stdout: Stdio, + stderr: Stdio, } -impl Resource for ChildResource { - fn name(&self) -> Cow<str> { - "child".into() - } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ChildStatus { + success: bool, + code: i32, + signal: Option<String>, } -impl ChildResource { - fn borrow_mut(self: Rc<Self>) -> AsyncMutFuture<tokio::process::Child> { - RcRef::map(self, |r| &r.child).borrow_mut() +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")] -// TODO(@AaronO): maybe find a more descriptive name or a convention for return structs -struct RunInfo { - rid: ResourceId, - pid: Option<u32>, - stdin_rid: Option<ResourceId>, - stdout_rid: Option<ResourceId>, - stderr_rid: Option<ResourceId>, +pub struct SpawnOutput { + status: ChildStatus, + stdout: Option<ZeroCopyBuf>, + stderr: Option<ZeroCopyBuf>, } -#[op] -fn op_run(state: &mut OpState, run_args: RunArgs) -> Result<RunInfo, AnyError> { - let args = run_args.cmd; +fn create_command( + state: &mut OpState, + args: SpawnArgs, + api_name: &str, +) -> Result<std::process::Command, AnyError> { state .borrow_mut::<PermissionsContainer>() - .check_run(&args[0], "Deno.run()")?; - 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)); + .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); + } - if run_args.clear_env { - super::check_unstable(state, "Deno.run.clearEnv"); - c.env_clear(); + #[cfg(not(windows))] + command.args(args.args); + + if let Some(cwd) = args.cwd { + command.current_dir(cwd); } - for (key, value) in &env { - c.env(key, value); + + if args.clear_env { + command.env_clear(); } + command.envs(args.env); #[cfg(unix)] - if let Some(gid) = run_args.gid { - super::check_unstable(state, "Deno.run.gid"); - c.gid(gid); + if let Some(gid) = args.gid { + command.gid(gid); } #[cfg(unix)] - if let Some(uid) = run_args.uid { - super::check_unstable(state, "Deno.run.uid"); - c.uid(uid); + if let Some(uid) = args.uid { + command.uid(uid); } #[cfg(unix)] // TODO(bartlomieju): #[allow(clippy::undocumented_unsafe_blocks)] unsafe { - c.pre_exec(|| { + command.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)?, - ); + 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>, +} + +fn spawn_child( + state: &mut OpState, + command: std::process::Command, +) -> Result<Child, AnyError> { + 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 - 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, - }; + 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 child_resource = ChildResource { - child: AsyncRefCell::new(child), - }; - let child_rid = state.resource_table.add(child_resource); + let stdout_rid = child + .stdout + .take() + .map(|stdout| state.resource_table.add(ChildStdoutResource::from(stdout))); - Ok(RunInfo { + 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, @@ -253,109 +311,325 @@ fn op_run(state: &mut OpState, run_args: RunArgs) -> Result<RunInfo, AnyError> { }) } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct ProcessStatus { - got_signal: bool, - exit_code: i32, - exit_signal: i32, +#[op] +fn op_spawn_child( + state: &mut OpState, + args: SpawnArgs, + api_name: String, +) -> Result<Child, AnyError> { + let command = create_command(state, args, &api_name)?; + spawn_child(state, command) } #[op] -async fn op_run_status( +async fn op_spawn_wait( state: Rc<RefCell<OpState>>, rid: ResourceId, -) -> Result<ProcessStatus, AnyError> { +) -> Result<ChildStatus, AnyError> { let resource = state .borrow_mut() .resource_table - .get::<ChildResource>(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(); + .take::<ChildResource>(rid)?; + Rc::try_unwrap(resource) + .ok() + .unwrap() + .0 + .wait() + .await? + .try_into() +} - Ok(ProcessStatus { - got_signal, - exit_code: code.unwrap_or(-1), - exit_signal: signal.unwrap_or(-1), +#[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, "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 + }, }) } -#[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; - 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) -} +mod deprecated { + use super::*; + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct RunArgs { + cmd: Vec<String>, + cwd: Option<String>, + clear_env: bool, + env: Vec<(String, String)>, + #[cfg(unix)] + gid: Option<u32>, + #[cfg(unix)] + uid: Option<u32>, + stdin: StdioOrRid, + stdout: StdioOrRid, + stderr: StdioOrRid, + } -#[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::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 { - // SAFETY: winapi call - let handle = unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as DWORD) }; + struct ChildResource { + child: AsyncRefCell<tokio::process::Child>, + } - 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()) + impl Resource for ChildResource { + fn name(&self) -> Cow<str> { + "child".into() + } + } + + impl ChildResource { + fn borrow_mut(self: Rc<Self>) -> AsyncMutFuture<tokio::process::Child> { + 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<u32>, + stdin_rid: Option<ResourceId>, + stdout_rid: Option<ResourceId>, + stderr_rid: Option<ResourceId>, + } + + #[op] + fn op_run( + state: &mut OpState, + run_args: RunArgs, + ) -> Result<RunInfo, AnyError> { + let args = run_args.cmd; + state + .borrow_mut::<PermissionsContainer>() + .check_run(&args[0], "Deno.run()")?; + 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)] + // 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")] + struct ProcessStatus { + got_signal: bool, + exit_code: i32, + exit_signal: i32, + } + + #[op] + async fn op_run_status( + state: Rc<RefCell<OpState>>, + rid: ResourceId, + ) -> Result<ProcessStatus, AnyError> { + let resource = state + .borrow_mut() + .resource_table + .get::<ChildResource>(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; + 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 deno_core::error::type_error; + 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 { - // 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!(), + // SAFETY: winapi call + let handle = + 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!(), + } } } } } -} -#[op] -fn op_kill( - state: &mut OpState, - pid: i32, - signal: String, - api_name: String, -) -> Result<(), AnyError> { - state - .borrow_mut::<PermissionsContainer>() - .check_run_all(&api_name)?; - kill(pid, &signal)?; - Ok(()) + #[op] + fn op_kill( + state: &mut OpState, + pid: i32, + signal: String, + api_name: String, + ) -> Result<(), AnyError> { + state + .borrow_mut::<PermissionsContainer>() + .check_run_all(&api_name)?; + kill(pid, &signal)?; + Ok(()) + } } |