summaryrefslogtreecommitdiff
path: root/runtime/ops/spawn.rs
diff options
context:
space:
mode:
authorLeo Kettmeir <crowlkats@toaxl.com>2022-04-21 00:20:33 +0200
committerGitHub <noreply@github.com>2022-04-21 00:20:33 +0200
commit8a7539cab36699465ec6e37455c54fa86f3c0cbe (patch)
treec3df15f3b673d1ec1a9c4ffada1a9274e3aca942 /runtime/ops/spawn.rs
parent8b258070542a81d217226fe832b26d81cf20113d (diff)
feat(runtime): two-tier subprocess API (#11618)
Diffstat (limited to 'runtime/ops/spawn.rs')
-rw-r--r--runtime/ops/spawn.rs263
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
+ },
+ })
+}