1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 07:14:47 -05:00

refactor(runtime): merge "spawn" into "process" (#18022)

This commit merges "runtime/js/40_spawn.js" into
"runtime/js/40_process.js", and "runtime::ops::spawn" 
into "runtime::ops::process".

It makes little sense to have them separated given that we want to
factor out these APIs into a separate extension crate.
This commit is contained in:
Bartek Iwańczuk 2023-03-05 08:19:34 -04:00 committed by GitHub
parent d4807f458e
commit de0d148d93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 792 additions and 827 deletions

View file

@ -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",

View file

@ -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 };

View file

@ -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 };

View file

@ -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 = {

View file

@ -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;

View file

@ -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,9 +100,276 @@ 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 {
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>,
#[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<String>,
}
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")]
pub struct SpawnOutput {
status: ChildStatus,
stdout: Option<ZeroCopyBuf>,
stderr: Option<ZeroCopyBuf>,
}
fn create_command(
state: &mut OpState,
args: SpawnArgs,
api_name: &str,
) -> Result<std::process::Command, AnyError> {
state
.borrow_mut::<PermissionsContainer>()
.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<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
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<Child, AnyError> {
let command = create_command(state, args, &api_name)?;
spawn_child(state, command)
}
#[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)?;
Rc::try_unwrap(resource)
.ok()
.unwrap()
.0
.wait()
.await?
.try_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, "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
},
})
}
mod deprecated {
use super::*;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunArgs {
cmd: Vec<String>,
cwd: Option<String>,
clear_env: bool,
@ -112,37 +381,40 @@ pub struct RunArgs {
stdin: StdioOrRid,
stdout: StdioOrRid,
stderr: StdioOrRid,
}
}
struct ChildResource {
struct ChildResource {
child: AsyncRefCell<tokio::process::Child>,
}
}
impl Resource for ChildResource {
impl Resource for ChildResource {
fn name(&self) -> Cow<str> {
"child".into()
}
}
}
impl ChildResource {
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 {
#[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> {
#[op]
fn op_run(
state: &mut OpState,
run_args: RunArgs,
) -> Result<RunInfo, AnyError> {
let args = run_args.cmd;
state
.borrow_mut::<PermissionsContainer>()
@ -251,21 +523,21 @@ fn op_run(state: &mut OpState, run_args: RunArgs) -> Result<RunInfo, AnyError> {
stdout_rid,
stderr_rid,
})
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ProcessStatus {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ProcessStatus {
got_signal: bool,
exit_code: i32,
exit_signal: i32,
}
}
#[op]
async fn op_run_status(
#[op]
async fn op_run_status(
state: Rc<RefCell<OpState>>,
rid: ResourceId,
) -> Result<ProcessStatus, AnyError> {
) -> Result<ProcessStatus, AnyError> {
let resource = state
.borrow_mut()
.resource_table
@ -289,20 +561,20 @@ async fn op_run_status(
exit_code: code.unwrap_or(-1),
exit_signal: signal.unwrap_or(-1),
})
}
}
#[cfg(unix)]
pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
#[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> {
#[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;
@ -322,7 +594,8 @@ pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
Err(type_error("Invalid pid"))
} else {
// SAFETY: winapi call
let handle = unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as DWORD) };
let handle =
unsafe { OpenProcess(PROCESS_TERMINATE, FALSE, pid as DWORD) };
if handle.is_null() {
// SAFETY: winapi call
@ -344,18 +617,19 @@ pub fn kill(pid: i32, signal: &str) -> Result<(), AnyError> {
}
}
}
}
}
#[op]
fn op_kill(
#[op]
fn op_kill(
state: &mut OpState,
pid: i32,
signal: String,
api_name: String,
) -> Result<(), AnyError> {
) -> Result<(), AnyError> {
state
.borrow_mut::<PermissionsContainer>()
.check_run_all(&api_name)?;
kill(pid, &signal)?;
Ok(())
}
}

View file

@ -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<str> {
"child".into()
}
}
#[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>,
#[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<String>,
}
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")]
pub struct SpawnOutput {
status: ChildStatus,
stdout: Option<ZeroCopyBuf>,
stderr: Option<ZeroCopyBuf>,
}
fn create_command(
state: &mut OpState,
args: SpawnArgs,
api_name: &str,
) -> Result<std::process::Command, AnyError> {
state
.borrow_mut::<PermissionsContainer>()
.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<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
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<Child, AnyError> {
let command = create_command(state, args, &api_name)?;
spawn_child(state, command)
}
#[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)?;
Rc::try_unwrap(resource)
.ok()
.unwrap()
.0
.wait()
.await?
.try_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, "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
},
})
}

View file

@ -438,8 +438,7 @@ impl WebWorker {
deno_node::init::<PermissionsContainer>(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(),

View file

@ -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::<PermissionsContainer>(),
deno_io::init(options.stdio),
@ -269,7 +268,7 @@ impl MainWorker {
deno_node::init::<PermissionsContainer>(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(),