diff --git a/runtime/build.rs b/runtime/build.rs index efcdb8f71f..153a856286 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -207,7 +207,6 @@ mod startup_snapshot { "40_http.js", "40_process.js", "40_signals.js", - "40_spawn.js", "40_tty.js", "41_prompt.js", "90_deno_ns.js", diff --git a/runtime/js/40_process.js b/runtime/js/40_process.js index bb224ae9c9..9599b7b746 100644 --- a/runtime/js/40_process.js +++ b/runtime/js/40_process.js @@ -2,10 +2,6 @@ const core = globalThis.Deno.core; const ops = core.ops; -import { FsFile } from "internal:runtime/30_fs.js"; -import { readAll } from "internal:deno_io/12_io.js"; -import { pathFromURL } from "internal:runtime/06_util.js"; -import { assert } from "internal:deno_web/00_infra.js"; const primordials = globalThis.__bootstrap.primordials; const { ArrayPrototypeMap, @@ -14,7 +10,25 @@ const { ObjectEntries, SafeArrayIterator, String, + ObjectPrototypeIsPrototypeOf, + PromisePrototypeThen, + SafePromiseAll, + SymbolFor, + Symbol, } = primordials; +import { FsFile } from "internal:runtime/30_fs.js"; +import { readAll } from "internal:deno_io/12_io.js"; +import { pathFromURL } from "internal:runtime/06_util.js"; +import { assert } from "internal:deno_web/00_infra.js"; +import * as abortSignal from "internal:deno_web/03_abort_signal.js"; +import { + readableStreamCollectIntoUint8Array, + readableStreamForRidUnrefable, + readableStreamForRidUnrefableRef, + readableStreamForRidUnrefableUnref, + ReadableStreamPrototype, + writableStreamForRid, +} from "internal:deno_web/06_streams.js"; function opKill(pid, signo, apiName) { ops.op_kill(pid, signo, apiName); @@ -130,4 +144,301 @@ function run({ return new Process(res); } -export { kill, Process, run }; +const illegalConstructorKey = Symbol("illegalConstructorKey"); +const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId"); + +function spawnChildInner(opFn, command, apiName, { + args = [], + cwd = undefined, + clearEnv = false, + env = {}, + uid = undefined, + gid = undefined, + stdin = "null", + stdout = "piped", + stderr = "piped", + signal = undefined, + windowsRawArguments = false, +} = {}) { + const child = opFn({ + cmd: pathFromURL(command), + args: ArrayPrototypeMap(args, String), + cwd: pathFromURL(cwd), + clearEnv, + env: ObjectEntries(env), + uid, + gid, + stdin, + stdout, + stderr, + windowsRawArguments, + }, apiName); + return new ChildProcess(illegalConstructorKey, { + ...child, + signal, + }); +} + +function spawnChild(command, options = {}) { + return spawnChildInner( + ops.op_spawn_child, + command, + "Deno.Command().spawn()", + options, + ); +} + +function collectOutput(readableStream) { + if ( + !(ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, readableStream)) + ) { + return null; + } + + return readableStreamCollectIntoUint8Array(readableStream); +} + +class ChildProcess { + #rid; + #waitPromiseId; + #unrefed = false; + + #pid; + get pid() { + return this.#pid; + } + + #stdin = null; + get stdin() { + if (this.#stdin == null) { + throw new TypeError("stdin is not piped"); + } + return this.#stdin; + } + + #stdoutRid; + #stdout = null; + get stdout() { + if (this.#stdout == null) { + throw new TypeError("stdout is not piped"); + } + return this.#stdout; + } + + #stderrRid; + #stderr = null; + get stderr() { + if (this.#stderr == null) { + throw new TypeError("stderr is not piped"); + } + return this.#stderr; + } + + constructor(key = null, { + signal, + rid, + pid, + stdinRid, + stdoutRid, + stderrRid, + } = null) { + if (key !== illegalConstructorKey) { + throw new TypeError("Illegal constructor."); + } + + this.#rid = rid; + this.#pid = pid; + + if (stdinRid !== null) { + this.#stdin = writableStreamForRid(stdinRid); + } + + if (stdoutRid !== null) { + this.#stdoutRid = stdoutRid; + this.#stdout = readableStreamForRidUnrefable(stdoutRid); + } + + if (stderrRid !== null) { + this.#stderrRid = stderrRid; + this.#stderr = readableStreamForRidUnrefable(stderrRid); + } + + const onAbort = () => this.kill("SIGTERM"); + signal?.[abortSignal.add](onAbort); + + const waitPromise = core.opAsync("op_spawn_wait", this.#rid); + this.#waitPromiseId = waitPromise[promiseIdSymbol]; + this.#status = PromisePrototypeThen(waitPromise, (res) => { + this.#rid = null; + signal?.[abortSignal.remove](onAbort); + return res; + }); + } + + #status; + get status() { + return this.#status; + } + + async output() { + if (this.#stdout?.locked) { + throw new TypeError( + "Can't collect output because stdout is locked", + ); + } + if (this.#stderr?.locked) { + throw new TypeError( + "Can't collect output because stderr is locked", + ); + } + + const { 0: status, 1: stdout, 2: stderr } = await SafePromiseAll([ + this.#status, + collectOutput(this.#stdout), + collectOutput(this.#stderr), + ]); + + return { + success: status.success, + code: status.code, + signal: status.signal, + get stdout() { + if (stdout == null) { + throw new TypeError("stdout is not piped"); + } + return stdout; + }, + get stderr() { + if (stderr == null) { + throw new TypeError("stderr is not piped"); + } + return stderr; + }, + }; + } + + kill(signo = "SIGTERM") { + if (this.#rid === null) { + throw new TypeError("Child process has already terminated."); + } + ops.op_kill(this.#pid, signo, "Deno.Child.kill()"); + } + + ref() { + this.#unrefed = false; + core.refOp(this.#waitPromiseId); + if (this.#stdout) readableStreamForRidUnrefableRef(this.#stdout); + if (this.#stderr) readableStreamForRidUnrefableRef(this.#stderr); + } + + unref() { + this.#unrefed = true; + core.unrefOp(this.#waitPromiseId); + if (this.#stdout) readableStreamForRidUnrefableUnref(this.#stdout); + if (this.#stderr) readableStreamForRidUnrefableUnref(this.#stderr); + } +} + +function spawn(command, options) { + if (options?.stdin === "piped") { + throw new TypeError( + "Piped stdin is not supported for this function, use 'Deno.Command().spawn()' instead", + ); + } + return spawnChildInner( + ops.op_spawn_child, + command, + "Deno.Command().output()", + options, + ) + .output(); +} + +function spawnSync(command, { + args = [], + cwd = undefined, + clearEnv = false, + env = {}, + uid = undefined, + gid = undefined, + stdin = "null", + stdout = "piped", + stderr = "piped", + windowsRawArguments = false, +} = {}) { + if (stdin === "piped") { + throw new TypeError( + "Piped stdin is not supported for this function, use 'Deno.Command().spawn()' instead", + ); + } + const result = ops.op_spawn_sync({ + cmd: pathFromURL(command), + args: ArrayPrototypeMap(args, String), + cwd: pathFromURL(cwd), + clearEnv, + env: ObjectEntries(env), + uid, + gid, + stdin, + stdout, + stderr, + windowsRawArguments, + }); + return { + success: result.status.success, + code: result.status.code, + signal: result.status.signal, + get stdout() { + if (result.stdout == null) { + throw new TypeError("stdout is not piped"); + } + return result.stdout; + }, + get stderr() { + if (result.stderr == null) { + throw new TypeError("stderr is not piped"); + } + return result.stderr; + }, + }; +} + +class Command { + #command; + #options; + + constructor(command, options) { + this.#command = command; + this.#options = options; + } + + output() { + if (this.#options?.stdin === "piped") { + throw new TypeError( + "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead", + ); + } + return spawn(this.#command, this.#options); + } + + outputSync() { + if (this.#options?.stdin === "piped") { + throw new TypeError( + "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead", + ); + } + return spawnSync(this.#command, this.#options); + } + + spawn() { + const options = { + ...(this.#options ?? {}), + stdout: this.#options?.stdout ?? "inherit", + stderr: this.#options?.stderr ?? "inherit", + stdin: this.#options?.stdin ?? "inherit", + }; + return spawnChild(this.#command, options); + } +} + +export { ChildProcess, Command, kill, Process, run }; diff --git a/runtime/js/40_spawn.js b/runtime/js/40_spawn.js deleted file mode 100644 index 173f596cc8..0000000000 --- a/runtime/js/40_spawn.js +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -const core = globalThis.Deno.core; -const ops = core.ops; -const primordials = globalThis.__bootstrap.primordials; -import { pathFromURL } from "internal:runtime/06_util.js"; -import { add, remove } from "internal:deno_web/03_abort_signal.js"; -const { - ArrayPrototypeMap, - ObjectEntries, - ObjectPrototypeIsPrototypeOf, - String, - TypeError, - PromisePrototypeThen, - SafePromiseAll, - SymbolFor, - Symbol, -} = primordials; -import { - readableStreamCollectIntoUint8Array, - readableStreamForRidUnrefable, - readableStreamForRidUnrefableRef, - readableStreamForRidUnrefableUnref, - ReadableStreamPrototype, - writableStreamForRid, -} from "internal:deno_web/06_streams.js"; - -const illegalConstructorKey = Symbol("illegalConstructorKey"); - -const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId"); - -function spawnChildInner(opFn, command, apiName, { - args = [], - cwd = undefined, - clearEnv = false, - env = {}, - uid = undefined, - gid = undefined, - stdin = "null", - stdout = "piped", - stderr = "piped", - signal = undefined, - windowsRawArguments = false, -} = {}) { - const child = opFn({ - cmd: pathFromURL(command), - args: ArrayPrototypeMap(args, String), - cwd: pathFromURL(cwd), - clearEnv, - env: ObjectEntries(env), - uid, - gid, - stdin, - stdout, - stderr, - windowsRawArguments, - }, apiName); - return new ChildProcess(illegalConstructorKey, { - ...child, - signal, - }); -} - -function spawnChild(command, options = {}) { - return spawnChildInner( - ops.op_spawn_child, - command, - "Deno.Command().spawn()", - options, - ); -} - -function collectOutput(readableStream) { - if ( - !(ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, readableStream)) - ) { - return null; - } - - return readableStreamCollectIntoUint8Array(readableStream); -} - -class ChildProcess { - #rid; - #waitPromiseId; - #unrefed = false; - - #pid; - get pid() { - return this.#pid; - } - - #stdin = null; - get stdin() { - if (this.#stdin == null) { - throw new TypeError("stdin is not piped"); - } - return this.#stdin; - } - - #stdoutRid; - #stdout = null; - get stdout() { - if (this.#stdout == null) { - throw new TypeError("stdout is not piped"); - } - return this.#stdout; - } - - #stderrRid; - #stderr = null; - get stderr() { - if (this.#stderr == null) { - throw new TypeError("stderr is not piped"); - } - return this.#stderr; - } - - constructor(key = null, { - signal, - rid, - pid, - stdinRid, - stdoutRid, - stderrRid, - } = null) { - if (key !== illegalConstructorKey) { - throw new TypeError("Illegal constructor."); - } - - this.#rid = rid; - this.#pid = pid; - - if (stdinRid !== null) { - this.#stdin = writableStreamForRid(stdinRid); - } - - if (stdoutRid !== null) { - this.#stdoutRid = stdoutRid; - this.#stdout = readableStreamForRidUnrefable(stdoutRid); - } - - if (stderrRid !== null) { - this.#stderrRid = stderrRid; - this.#stderr = readableStreamForRidUnrefable(stderrRid); - } - - const onAbort = () => this.kill("SIGTERM"); - signal?.[add](onAbort); - - const waitPromise = core.opAsync("op_spawn_wait", this.#rid); - this.#waitPromiseId = waitPromise[promiseIdSymbol]; - this.#status = PromisePrototypeThen(waitPromise, (res) => { - this.#rid = null; - signal?.[remove](onAbort); - return res; - }); - } - - #status; - get status() { - return this.#status; - } - - async output() { - if (this.#stdout?.locked) { - throw new TypeError( - "Can't collect output because stdout is locked", - ); - } - if (this.#stderr?.locked) { - throw new TypeError( - "Can't collect output because stderr is locked", - ); - } - - const { 0: status, 1: stdout, 2: stderr } = await SafePromiseAll([ - this.#status, - collectOutput(this.#stdout), - collectOutput(this.#stderr), - ]); - - return { - success: status.success, - code: status.code, - signal: status.signal, - get stdout() { - if (stdout == null) { - throw new TypeError("stdout is not piped"); - } - return stdout; - }, - get stderr() { - if (stderr == null) { - throw new TypeError("stderr is not piped"); - } - return stderr; - }, - }; - } - - kill(signo = "SIGTERM") { - if (this.#rid === null) { - throw new TypeError("Child process has already terminated."); - } - ops.op_kill(this.#pid, signo, "Deno.Child.kill()"); - } - - ref() { - this.#unrefed = false; - core.refOp(this.#waitPromiseId); - if (this.#stdout) readableStreamForRidUnrefableRef(this.#stdout); - if (this.#stderr) readableStreamForRidUnrefableRef(this.#stderr); - } - - unref() { - this.#unrefed = true; - core.unrefOp(this.#waitPromiseId); - if (this.#stdout) readableStreamForRidUnrefableUnref(this.#stdout); - if (this.#stderr) readableStreamForRidUnrefableUnref(this.#stderr); - } -} - -function spawn(command, options) { - if (options?.stdin === "piped") { - throw new TypeError( - "Piped stdin is not supported for this function, use 'Deno.Command().spawn()' instead", - ); - } - return spawnChildInner( - ops.op_spawn_child, - command, - "Deno.Command().output()", - options, - ) - .output(); -} - -function spawnSync(command, { - args = [], - cwd = undefined, - clearEnv = false, - env = {}, - uid = undefined, - gid = undefined, - stdin = "null", - stdout = "piped", - stderr = "piped", - windowsRawArguments = false, -} = {}) { - if (stdin === "piped") { - throw new TypeError( - "Piped stdin is not supported for this function, use 'Deno.Command().spawn()' instead", - ); - } - const result = ops.op_spawn_sync({ - cmd: pathFromURL(command), - args: ArrayPrototypeMap(args, String), - cwd: pathFromURL(cwd), - clearEnv, - env: ObjectEntries(env), - uid, - gid, - stdin, - stdout, - stderr, - windowsRawArguments, - }); - return { - success: result.status.success, - code: result.status.code, - signal: result.status.signal, - get stdout() { - if (result.stdout == null) { - throw new TypeError("stdout is not piped"); - } - return result.stdout; - }, - get stderr() { - if (result.stderr == null) { - throw new TypeError("stderr is not piped"); - } - return result.stderr; - }, - }; -} - -class Command { - #command; - #options; - - constructor(command, options) { - this.#command = command; - this.#options = options; - } - - output() { - if (this.#options?.stdin === "piped") { - throw new TypeError( - "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead", - ); - } - return spawn(this.#command, this.#options); - } - - outputSync() { - if (this.#options?.stdin === "piped") { - throw new TypeError( - "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead", - ); - } - return spawnSync(this.#command, this.#options); - } - - spawn() { - const options = { - ...(this.#options ?? {}), - stdout: this.#options?.stdout ?? "inherit", - stderr: this.#options?.stderr ?? "inherit", - stdin: this.#options?.stdin ?? "inherit", - }; - return spawnChild(this.#command, options); - } -} - -export { ChildProcess, Command }; diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index 39a1def90d..f83695952c 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -22,7 +22,6 @@ import * as fsEvents from "internal:runtime/40_fs_events.js"; import * as process from "internal:runtime/40_process.js"; import * as signals from "internal:runtime/40_signals.js"; import * as tty from "internal:runtime/40_tty.js"; -import * as spawn from "internal:runtime/40_spawn.js"; // TODO(bartlomieju): this is funky we have two `http` imports import * as httpRuntime from "internal:runtime/40_http.js"; @@ -148,9 +147,9 @@ const denoNs = { consoleSize: tty.consoleSize, gid: os.gid, uid: os.uid, - Command: spawn.Command, + Command: process.Command, // TODO(bartlomieju): why is this exported? - ChildProcess: spawn.ChildProcess, + ChildProcess: process.ChildProcess, }; const denoNsUnstable = { diff --git a/runtime/ops/mod.rs b/runtime/ops/mod.rs index 0564474b1f..48c22ca920 100644 --- a/runtime/ops/mod.rs +++ b/runtime/ops/mod.rs @@ -8,7 +8,6 @@ pub mod permissions; pub mod process; pub mod runtime; pub mod signal; -pub mod spawn; pub mod tty; mod utils; pub mod web_worker; diff --git a/runtime/ops/process.rs b/runtime/ops/process.rs index ca37c08b74..ad14ef2e48 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,25 +100,20 @@ impl StdioOrRid { } } -#[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, +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 { - child: AsyncRefCell, -} +struct ChildResource(tokio::process::Child); impl Resource for ChildResource { fn name(&self) -> Cow { @@ -124,127 +121,188 @@ impl Resource for ChildResource { } } -impl ChildResource { - fn borrow_mut(self: Rc) -> AsyncMutFuture { - RcRef::map(self, |r| &r.child).borrow_mut() +#[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, + + #[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")] -// 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, +pub struct SpawnOutput { + status: ChildStatus, + stdout: Option, + stderr: Option, } -#[op] -fn op_run(state: &mut OpState, run_args: RunArgs) -> Result { - let args = run_args.cmd; +fn create_command( + state: &mut OpState, + args: SpawnArgs, + api_name: &str, +) -> Result { state .borrow_mut::() - .check_run(&args[0], "Deno.run()")?; - let env = run_args.env; - let cwd = run_args.cwd; + .check_run(&args.cmd, api_name)?; - 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)); + let mut command = std::process::Command::new(args.cmd); - if run_args.clear_env { - super::check_unstable(state, "Deno.run.clearEnv"); - c.env_clear(); + #[cfg(windows)] + if args.windows_raw_arguments { + for arg in args.args.iter() { + command.raw_arg(arg); + } + } else { + command.args(args.args); } - for (key, value) in &env { - c.env(key, value); + + #[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) = 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, + stdout_rid: Option, + stderr_rid: Option, +} + +fn spawn_child( + state: &mut OpState, + command: std::process::Command, +) -> 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 - c.kill_on_drop(true); + command.kill_on_drop(true); - // Spawn the command. - let mut child = c.spawn()?; - let pid = child.id(); + let mut child = command.spawn()?; + let pid = child.id().expect("Process ID should be set."); - 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 stdin_rid = child + .stdin + .take() + .map(|stdin| state.resource_table.add(ChildStdinResource::from(stdin))); - 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 stdout_rid = child + .stdout + .take() + .map(|stdout| state.resource_table.add(ChildStdoutResource::from(stdout))); - 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 stderr_rid = child + .stderr + .take() + .map(|stderr| state.resource_table.add(ChildStderrResource::from(stderr))); - let child_resource = ChildResource { - child: AsyncRefCell::new(child), - }; - let child_rid = state.resource_table.add(child_resource); + let child_rid = state.resource_table.add(ChildResource(child)); - Ok(RunInfo { + Ok(Child { rid: child_rid, pid, stdin_rid, @@ -253,109 +311,325 @@ fn op_run(state: &mut OpState, run_args: RunArgs) -> Result { }) } -#[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 { + 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>, rid: ResourceId, -) -> Result { +) -> 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(); + .take::(rid)?; + Rc::try_unwrap(resource) + .ok() + .unwrap() + .0 + .wait() + .await? + .try_into() +} - #[cfg(unix)] - let signal = run_status.signal(); - #[cfg(not(unix))] - let signal = None; +#[op] +fn op_spawn_sync( + state: &mut OpState, + args: SpawnArgs, +) -> Result { + 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()?; - 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), + 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::*; -#[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; + #[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, + } - 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, + } - 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 { + "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::() + .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>, + 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; + 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::() - .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::() + .check_run_all(&api_name)?; + kill(pid, &signal)?; + Ok(()) + } } diff --git a/runtime/ops/spawn.rs b/runtime/ops/spawn.rs deleted file mode 100644 index 884c462296..0000000000 --- a/runtime/ops/spawn.rs +++ /dev/null @@ -1,289 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use super::process::Stdio; -use super::process::StdioOrRid; -use crate::permissions::PermissionsContainer; -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 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; -#[cfg(windows)] -use std::os::windows::process::CommandExt; -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("deno_spawn") - .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 { - "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, - - #[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, -} - -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); - } - #[cfg(unix)] - // TODO(bartlomieju): - #[allow(clippy::undocumented_unsafe_blocks)] - unsafe { - command.pre_exec(|| { - libc::setgroups(0, std::ptr::null()); - Ok(()) - }); - } - - 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, - stdout_rid: Option, - stderr_rid: Option, -} - -fn spawn_child( - state: &mut OpState, - command: std::process::Command, -) -> 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 = 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] -fn op_spawn_child( - state: &mut OpState, - args: SpawnArgs, - api_name: String, -) -> Result { - let command = create_command(state, args, &api_name)?; - spawn_child(state, command) -} - -#[op] -async fn op_spawn_wait( - state: Rc>, - rid: ResourceId, -) -> Result { - let resource = state - .borrow_mut() - .resource_table - .take::(rid)?; - Rc::try_unwrap(resource) - .ok() - .unwrap() - .0 - .wait() - .await? - .try_into() -} - -#[op] -fn op_spawn_sync( - state: &mut OpState, - args: SpawnArgs, -) -> Result { - 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 - }, - }) -} diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs index fbf10905fd..ffafbc1d36 100644 --- a/runtime/web_worker.rs +++ b/runtime/web_worker.rs @@ -438,8 +438,7 @@ impl WebWorker { deno_node::init::(options.npm_resolver), ops::os::init_for_worker(), ops::permissions::init(), - ops::process::init(), - ops::spawn::init(), + ops::process::init_ops(), ops::signal::init(), ops::tty::init(), deno_http::init(), diff --git a/runtime/worker.rs b/runtime/worker.rs index 1f567837a8..8fa17a7f5c 100644 --- a/runtime/worker.rs +++ b/runtime/worker.rs @@ -254,7 +254,6 @@ impl MainWorker { options.web_worker_pre_execute_module_cb.clone(), options.format_js_error_fn.clone(), ), - ops::spawn::init(), ops::fs_events::init(), ops::fs::init::(), deno_io::init(options.stdio), @@ -269,7 +268,7 @@ impl MainWorker { deno_node::init::(options.npm_resolver), ops::os::init(exit_code.clone()), ops::permissions::init(), - ops::process::init(), + ops::process::init_ops(), ops::signal::init(), ops::tty::init(), deno_http::init(),