diff options
author | Leo Kettmeir <crowlkats@toaxl.com> | 2022-04-21 00:20:33 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-04-21 00:20:33 +0200 |
commit | 8a7539cab36699465ec6e37455c54fa86f3c0cbe (patch) | |
tree | c3df15f3b673d1ec1a9c4ffada1a9274e3aca942 /runtime/ops/spawn.rs | |
parent | 8b258070542a81d217226fe832b26d81cf20113d (diff) |
feat(runtime): two-tier subprocess API (#11618)
Diffstat (limited to 'runtime/ops/spawn.rs')
-rw-r--r-- | runtime/ops/spawn.rs | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/runtime/ops/spawn.rs b/runtime/ops/spawn.rs new file mode 100644 index 000000000..196a7eed6 --- /dev/null +++ b/runtime/ops/spawn.rs @@ -0,0 +1,263 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use super::io::ChildStderrResource; +use super::io::ChildStdinResource; +use super::io::ChildStdoutResource; +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 enum Stdio { + Inherit, + Piped, + Null, +} + +fn subprocess_stdio_map(s: &Stdio) -> Result<std::process::Stdio, AnyError> { + match s { + Stdio::Inherit => Ok(std::process::Stdio::inherit()), + Stdio::Piped => Ok(std::process::Stdio::piped()), + Stdio::Null => Ok(std::process::Stdio::null()), + } +} + +#[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<i32>, +} + +impl From<std::process::ExitStatus> for ChildStatus { + fn from(status: ExitStatus) -> Self { + let code = status.code(); + #[cfg(unix)] + let signal = status.signal(); + #[cfg(not(unix))] + let signal = None; + + if let Some(signal) = signal { + ChildStatus { + success: false, + code: 128 + signal, + signal: Some(signal), + } + } else { + let code = code.expect("Should have either an exit code or a signal."); + + ChildStatus { + success: code == 0, + code, + signal: None, + } + } + } +} + +#[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)] + unsafe { + command.pre_exec(|| { + libc::setgroups(0, std::ptr::null()); + Ok(()) + }); + } + + command.stdin(subprocess_stdio_map(&args.stdio.stdin)?); + command.stdout(subprocess_stdio_map(&args.stdio.stdout)?); + command.stderr(subprocess_stdio_map(&args.stdio.stderr)?); + + 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)?; + Ok( + Rc::try_unwrap(resource) + .ok() + .unwrap() + .0 + .wait() + .await? + .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.into(), + stdout: if stdout { + Some(output.stdout.into()) + } else { + None + }, + stderr: if stderr { + Some(output.stderr.into()) + } else { + None + }, + }) +} |