summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--BUILD.gn5
-rw-r--r--js/deno.ts1
-rw-r--r--js/process.ts136
-rw-r--r--js/process_test.ts178
-rw-r--r--js/test_util.ts15
-rw-r--r--js/unit_tests.ts1
-rw-r--r--js/util.ts6
-rw-r--r--src/flags.rs9
-rw-r--r--src/isolate.rs5
-rw-r--r--src/msg.fbs37
-rw-r--r--src/ops.rs141
-rw-r--r--src/permissions.rs14
-rw-r--r--src/resources.rs88
-rwxr-xr-xtools/unit_tests.py10
14 files changed, 629 insertions, 17 deletions
diff --git a/BUILD.gn b/BUILD.gn
index 67d643c9c..f30aea6b5 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -91,13 +91,13 @@ ts_sources = [
"js/errors.ts",
"js/fetch.ts",
"js/file.ts",
- "js/headers.ts",
"js/file_info.ts",
"js/files.ts",
"js/flatbuffers.ts",
"js/form_data.ts",
"js/global_eval.ts",
"js/globals.ts",
+ "js/headers.ts",
"js/io.ts",
"js/libdeno.ts",
"js/main.ts",
@@ -109,14 +109,15 @@ ts_sources = [
"js/os.ts",
"js/platform.ts",
"js/plugins.d.ts",
+ "js/process.ts",
"js/promise_util.ts",
"js/read_dir.ts",
"js/read_file.ts",
"js/read_link.ts",
"js/remove.ts",
"js/rename.ts",
- "js/resources.ts",
"js/repl.ts",
+ "js/resources.ts",
"js/stat.ts",
"js/symlink.ts",
"js/text_encoding.ts",
diff --git a/js/deno.ts b/js/deno.ts
index afcde7033..532357dab 100644
--- a/js/deno.ts
+++ b/js/deno.ts
@@ -41,6 +41,7 @@ export { FileInfo } from "./file_info";
export { connect, dial, listen, Listener, Conn } from "./net";
export { metrics } from "./metrics";
export { resources } from "./resources";
+export { run, RunOptions, Process, ProcessStatus } from "./process";
export const args: string[] = [];
// Provide the compiler API in an obfuscated way
diff --git a/js/process.ts b/js/process.ts
new file mode 100644
index 000000000..0a1393ed0
--- /dev/null
+++ b/js/process.ts
@@ -0,0 +1,136 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import * as dispatch from "./dispatch";
+import * as flatbuffers from "./flatbuffers";
+import * as msg from "gen/msg_generated";
+import { assert, unreachable } from "./util";
+import { close, File } from "./files";
+import { ReadCloser, WriteCloser } from "./io";
+
+/** How to handle subsubprocess stdio.
+ *
+ * "inherit" The default if unspecified. The child inherits from the
+ * corresponding parent descriptor.
+ *
+ * "piped" A new pipe should be arranged to connect the parent and child
+ * subprocesses.
+ *
+ * "null" This stream will be ignored. This is the equivalent of attaching the
+ * stream to /dev/null.
+ */
+export type ProcessStdio = "inherit" | "piped" | "null";
+
+// TODO Maybe extend VSCode's 'CommandOptions'?
+// tslint:disable-next-line:max-line-length
+// See https://code.visualstudio.com/docs/editor/tasks-appendix#_schema-for-tasksjson
+export interface RunOptions {
+ args: string[];
+ cwd?: string;
+ stdout?: ProcessStdio;
+ stderr?: ProcessStdio;
+ stdin?: ProcessStdio;
+}
+
+export class Process {
+ readonly rid: number;
+ readonly pid: number;
+ readonly stdin?: WriteCloser;
+ readonly stdout?: ReadCloser;
+ readonly stderr?: ReadCloser;
+
+ // @internal
+ constructor(res: msg.RunRes) {
+ this.rid = res.rid();
+ this.pid = res.pid();
+
+ if (res.stdinRid() > 0) {
+ this.stdin = new File(res.stdinRid());
+ }
+
+ if (res.stdoutRid() > 0) {
+ this.stdout = new File(res.stdoutRid());
+ }
+
+ if (res.stderrRid() > 0) {
+ this.stderr = new File(res.stderrRid());
+ }
+ }
+
+ async status(): Promise<ProcessStatus> {
+ return await runStatus(this.rid);
+ }
+
+ close(): void {
+ close(this.rid);
+ }
+}
+
+export interface ProcessStatus {
+ success: boolean;
+ code?: number;
+ signal?: number; // TODO: Make this a string, e.g. 'SIGTERM'.
+}
+
+function stdioMap(s: ProcessStdio): msg.ProcessStdio {
+ switch (s) {
+ case "inherit":
+ return msg.ProcessStdio.Inherit;
+ case "piped":
+ return msg.ProcessStdio.Piped;
+ case "null":
+ return msg.ProcessStdio.Null;
+ default:
+ return unreachable();
+ }
+}
+
+export function run(opt: RunOptions): Process {
+ const builder = flatbuffers.createBuilder();
+ const argsOffset = msg.Run.createArgsVector(
+ builder,
+ opt.args.map(a => builder.createString(a))
+ );
+ const cwdOffset = opt.cwd == null ? -1 : builder.createString(opt.cwd);
+ msg.Run.startRun(builder);
+ msg.Run.addArgs(builder, argsOffset);
+ if (opt.cwd != null) {
+ msg.Run.addCwd(builder, cwdOffset);
+ }
+ if (opt.stdin) {
+ msg.Run.addStdin(builder, stdioMap(opt.stdin!));
+ }
+ if (opt.stdout) {
+ msg.Run.addStdout(builder, stdioMap(opt.stdout!));
+ }
+ if (opt.stderr) {
+ msg.Run.addStderr(builder, stdioMap(opt.stderr!));
+ }
+ const inner = msg.Run.endRun(builder);
+ const baseRes = dispatch.sendSync(builder, msg.Any.Run, inner);
+ assert(baseRes != null);
+ assert(msg.Any.RunRes === baseRes!.innerType());
+ const res = new msg.RunRes();
+ assert(baseRes!.inner(res) != null);
+
+ return new Process(res);
+}
+
+async function runStatus(rid: number): Promise<ProcessStatus> {
+ const builder = flatbuffers.createBuilder();
+ msg.RunStatus.startRunStatus(builder);
+ msg.RunStatus.addRid(builder, rid);
+ const inner = msg.RunStatus.endRunStatus(builder);
+
+ const baseRes = await dispatch.sendAsync(builder, msg.Any.RunStatus, inner);
+ assert(baseRes != null);
+ assert(msg.Any.RunStatusRes === baseRes!.innerType());
+ const res = new msg.RunStatusRes();
+ assert(baseRes!.inner(res) != null);
+
+ if (res.gotSignal()) {
+ const signal = res.exitSignal();
+ return { signal, success: false };
+ } else {
+ const code = res.exitCode();
+ return { code, success: code === 0 };
+ }
+}
diff --git a/js/process_test.ts b/js/process_test.ts
new file mode 100644
index 000000000..6cba1a1b7
--- /dev/null
+++ b/js/process_test.ts
@@ -0,0 +1,178 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import { test, testPerm, assert, assertEqual } from "./test_util.ts";
+import { run, DenoError, ErrorKind } from "deno";
+import * as deno from "deno";
+
+test(async function runPermissions() {
+ let caughtError = false;
+ try {
+ deno.run({ args: ["python", "-c", "print('hello world')"] });
+ } catch (e) {
+ caughtError = true;
+ assertEqual(e.kind, deno.ErrorKind.PermissionDenied);
+ assertEqual(e.name, "PermissionDenied");
+ }
+ assert(caughtError);
+});
+
+testPerm({ run: true }, async function runSuccess() {
+ const p = run({
+ args: ["python", "-c", "print('hello world')"]
+ });
+ const status = await p.status();
+ console.log("status", status);
+ assertEqual(status.success, true);
+ assertEqual(status.code, 0);
+ assertEqual(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runCommandFailedWithCode() {
+ let p = run({
+ args: ["python", "-c", "import sys;sys.exit(41 + 1)"]
+ });
+ let status = await p.status();
+ assertEqual(status.success, false);
+ assertEqual(status.code, 42);
+ assertEqual(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runCommandFailedWithSignal() {
+ if (deno.platform.os === "win") {
+ return; // No signals on windows.
+ }
+ const p = run({
+ args: ["python", "-c", "import os;os.kill(os.getpid(), 9)"]
+ });
+ const status = await p.status();
+ assertEqual(status.success, false);
+ assertEqual(status.code, undefined);
+ assertEqual(status.signal, 9);
+ p.close();
+});
+
+testPerm({ run: true }, async function runNotFound() {
+ let error;
+ try {
+ run({ args: ["this file hopefully doesn't exist"] });
+ } catch (e) {
+ error = e;
+ }
+ assert(error !== undefined);
+ assert(error instanceof DenoError);
+ assertEqual(error.kind, ErrorKind.NotFound);
+});
+
+testPerm({ write: true, run: true }, async function runWithCwdIsAsync() {
+ const enc = new TextEncoder();
+ const cwd = deno.makeTempDirSync({ prefix: "deno_command_test" });
+
+ const exitCodeFile = "deno_was_here";
+ const pyProgramFile = "poll_exit.py";
+ const pyProgram = `
+from sys import exit
+from time import sleep
+
+while True:
+ try:
+ with open("${exitCodeFile}", "r") as f:
+ line = f.readline()
+ code = int(line)
+ exit(code)
+ except IOError:
+ # Retry if we got here before deno wrote the file.
+ sleep(0.01)
+ pass
+`;
+
+ deno.writeFileSync(`${cwd}/${pyProgramFile}.py`, enc.encode(pyProgram));
+ const p = run({
+ cwd,
+ args: ["python", `${pyProgramFile}.py`]
+ });
+
+ // Write the expected exit code *after* starting python.
+ // This is how we verify that `run()` is actually asynchronous.
+ const code = 84;
+ deno.writeFileSync(`${cwd}/${exitCodeFile}`, enc.encode(`${code}`));
+
+ const status = await p.status();
+ assertEqual(status.success, false);
+ assertEqual(status.code, code);
+ assertEqual(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runStdinPiped() {
+ const p = run({
+ args: ["python", "-c", "import sys; assert 'hello' == sys.stdin.read();"],
+ stdin: "piped"
+ });
+ assert(!p.stdout);
+ assert(!p.stderr);
+
+ let msg = new TextEncoder().encode("hello");
+ let n = await p.stdin.write(msg);
+ assertEqual(n, msg.byteLength);
+
+ p.stdin.close();
+
+ const status = await p.status();
+ assertEqual(status.success, true);
+ assertEqual(status.code, 0);
+ assertEqual(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runStdoutPiped() {
+ const p = run({
+ args: ["python", "-c", "import sys; sys.stdout.write('hello')"],
+ stdout: "piped"
+ });
+ assert(!p.stdin);
+ assert(!p.stderr);
+
+ const data = new Uint8Array(10);
+ let r = await p.stdout.read(data);
+ assertEqual(r.nread, 5);
+ assertEqual(r.eof, false);
+ const s = new TextDecoder().decode(data.subarray(0, r.nread));
+ assertEqual(s, "hello");
+ r = await p.stdout.read(data);
+ assertEqual(r.nread, 0);
+ assertEqual(r.eof, true);
+ p.stdout.close();
+
+ const status = await p.status();
+ assertEqual(status.success, true);
+ assertEqual(status.code, 0);
+ assertEqual(status.signal, undefined);
+ p.close();
+});
+
+testPerm({ run: true }, async function runStderrPiped() {
+ const p = run({
+ args: ["python", "-c", "import sys; sys.stderr.write('hello')"],
+ stderr: "piped"
+ });
+ assert(!p.stdin);
+ assert(!p.stdout);
+
+ const data = new Uint8Array(10);
+ let r = await p.stderr.read(data);
+ assertEqual(r.nread, 5);
+ assertEqual(r.eof, false);
+ const s = new TextDecoder().decode(data.subarray(0, r.nread));
+ assertEqual(s, "hello");
+ r = await p.stderr.read(data);
+ assertEqual(r.nread, 0);
+ assertEqual(r.eof, true);
+ p.stderr.close();
+
+ const status = await p.status();
+ assertEqual(status.success, true);
+ assertEqual(status.code, 0);
+ assertEqual(status.signal, undefined);
+ p.close();
+});
diff --git a/js/test_util.ts b/js/test_util.ts
index 34a920d47..93fc67491 100644
--- a/js/test_util.ts
+++ b/js/test_util.ts
@@ -18,17 +18,19 @@ interface DenoPermissions {
write?: boolean;
net?: boolean;
env?: boolean;
+ run?: boolean;
}
function permToString(perms: DenoPermissions): string {
const w = perms.write ? 1 : 0;
const n = perms.net ? 1 : 0;
const e = perms.env ? 1 : 0;
- return `permW${w}N${n}E${e}`;
+ const r = perms.run ? 1 : 0;
+ return `permW${w}N${n}E${e}R${r}`;
}
function permFromString(s: string): DenoPermissions {
- const re = /^permW([01])N([01])E([01])$/;
+ const re = /^permW([01])N([01])E([01])R([01])$/;
const found = s.match(re);
if (!found) {
throw Error("Not a permission string");
@@ -36,7 +38,8 @@ function permFromString(s: string): DenoPermissions {
return {
write: Boolean(Number(found[1])),
net: Boolean(Number(found[2])),
- env: Boolean(Number(found[3]))
+ env: Boolean(Number(found[3])),
+ run: Boolean(Number(found[4]))
};
}
@@ -53,8 +56,10 @@ test(function permSerialization() {
for (const write of [true, false]) {
for (const net of [true, false]) {
for (const env of [true, false]) {
- const perms: DenoPermissions = { write, net, env };
- testing.assertEqual(perms, permFromString(permToString(perms)));
+ for (const run of [true, false]) {
+ const perms: DenoPermissions = { write, net, env, run };
+ testing.assertEqual(perms, permFromString(permToString(perms)));
+ }
}
}
}
diff --git a/js/unit_tests.ts b/js/unit_tests.ts
index 57bd554d5..2fcb5cd39 100644
--- a/js/unit_tests.ts
+++ b/js/unit_tests.ts
@@ -22,6 +22,7 @@ import "./mkdir_test.ts";
import "./net_test.ts";
import "./os_test.ts";
import "./platform_test.ts";
+import "./process_test.ts";
import "./read_dir_test.ts";
import "./read_file_test.ts";
import "./read_link_test.ts";
diff --git a/js/util.ts b/js/util.ts
index 9ba1d6346..c5197164e 100644
--- a/js/util.ts
+++ b/js/util.ts
@@ -131,3 +131,9 @@ const TypedArrayConstructor = Object.getPrototypeOf(Uint8Array);
export function isTypedArray(x: unknown): x is TypedArray {
return x instanceof TypedArrayConstructor;
}
+
+// Returns whether o is an object, not null, and not a function.
+// @internal
+export function isObject(o: unknown): o is object {
+ return o != null && typeof o === "object";
+}
diff --git a/src/flags.rs b/src/flags.rs
index dbbe51684..fcc0d0461 100644
--- a/src/flags.rs
+++ b/src/flags.rs
@@ -25,6 +25,7 @@ pub struct DenoFlags {
pub allow_write: bool,
pub allow_net: bool,
pub allow_env: bool,
+ pub allow_run: bool,
pub types: bool,
}
@@ -93,10 +94,9 @@ fn set_recognized_flags(
if matches.opt_present("allow-env") {
flags.allow_env = true;
}
- // TODO: uncomment once https://github.com/denoland/deno/pull/1156 lands on master
- // if matches.opt_present("allow-run") {
- // flags.allow_run = true;
- // }
+ if matches.opt_present("allow-run") {
+ flags.allow_run = true;
+ }
if matches.opt_present("types") {
flags.types = true;
}
@@ -126,6 +126,7 @@ pub fn set_flags(
opts.optflag("", "allow-write", "Allow file system write access.");
opts.optflag("", "allow-net", "Allow network access.");
opts.optflag("", "allow-env", "Allow environment access.");
+ opts.optflag("", "allow-run", "Allow running subprocesses.");
opts.optflag("", "recompile", "Force recompilation of TypeScript code.");
opts.optflag("h", "help", "Print this message.");
opts.optflag("D", "log-debug", "Log debug output.");
diff --git a/src/isolate.rs b/src/isolate.rs
index ccfb3453c..c02f4d6ce 100644
--- a/src/isolate.rs
+++ b/src/isolate.rs
@@ -89,6 +89,11 @@ impl IsolateState {
perm.check_net(filename)
}
+ pub fn check_run(&self) -> DenoResult<()> {
+ let mut perm = self.permissions.lock().unwrap();
+ perm.check_run()
+ }
+
fn metrics_op_dispatched(
&self,
bytes_sent_control: u64,
diff --git a/src/msg.fbs b/src/msg.fbs
index 2a9c013a3..d6bbc220e 100644
--- a/src/msg.fbs
+++ b/src/msg.fbs
@@ -53,6 +53,10 @@ union Any {
CwdRes,
Metrics,
MetricsRes,
+ Run,
+ RunRes,
+ RunStatus,
+ RunStatusRes
}
enum ErrorKind: byte {
@@ -78,8 +82,8 @@ enum ErrorKind: byte {
WriteZero,
Other,
UnexpectedEof,
-
BadResource,
+ CommandFailed,
// url errors
@@ -413,4 +417,35 @@ table MetricsRes {
bytes_received: uint64;
}
+enum ProcessStdio: byte { Inherit, Piped, Null }
+
+table Run {
+ args: [string];
+ cwd: string;
+ stdin: ProcessStdio;
+ stdout: ProcessStdio;
+ stderr: ProcessStdio;
+}
+
+table RunRes {
+ rid: uint32;
+ pid: uint32;
+ // The following stdio rids are only valid if "Piped" was specified for the
+ // corresponding stdio stream. The caller MUST issue a close op for all valid
+ // stdio streams.
+ stdin_rid: uint32;
+ stdout_rid: uint32;
+ stderr_rid: uint32;
+}
+
+table RunStatus {
+ rid: uint32;
+}
+
+table RunStatusRes {
+ got_signal: bool;
+ exit_code: int;
+ exit_signal: int;
+}
+
root_type Base;
diff --git a/src/ops.rs b/src/ops.rs
index 0e2c7e119..e08e67705 100644
--- a/src/ops.rs
+++ b/src/ops.rs
@@ -25,10 +25,13 @@ use resources::table_entries;
use std;
use std::fs;
use std::net::{Shutdown, SocketAddr};
-#[cfg(any(unix))]
+#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
+#[cfg(unix)]
+use std::os::unix::process::ExitStatusExt;
use std::path::Path;
use std::path::PathBuf;
+use std::process::Command;
use std::str::FromStr;
use std::sync::Arc;
use std::time::UNIX_EPOCH;
@@ -36,6 +39,7 @@ use std::time::{Duration, Instant};
use tokio;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
+use tokio_process::CommandExt;
use tokio_threadpool;
type OpResult = DenoResult<Buf>;
@@ -100,6 +104,8 @@ pub fn dispatch(
msg::Any::ReplReadline => op_repl_readline,
msg::Any::ReplStart => op_repl_start,
msg::Any::Resources => op_resources,
+ msg::Any::Run => op_run,
+ msg::Any::RunStatus => op_run_status,
msg::Any::SetEnv => op_set_env,
msg::Any::Shutdown => op_shutdown,
msg::Any::Start => op_start,
@@ -1352,3 +1358,136 @@ fn op_resources(
},
))
}
+
+fn subprocess_stdio_map(v: msg::ProcessStdio) -> std::process::Stdio {
+ match v {
+ msg::ProcessStdio::Inherit => std::process::Stdio::inherit(),
+ msg::ProcessStdio::Piped => std::process::Stdio::piped(),
+ msg::ProcessStdio::Null => std::process::Stdio::null(),
+ }
+}
+
+fn op_run(
+ state: &Arc<IsolateState>,
+ base: &msg::Base,
+ data: &'static mut [u8],
+) -> Box<Op> {
+ assert!(base.sync());
+ let cmd_id = base.cmd_id();
+
+ if let Err(e) = state.check_run() {
+ return odd_future(e);
+ }
+
+ assert_eq!(data.len(), 0);
+ let inner = base.inner_as_run().unwrap();
+ let args = inner.args().unwrap();
+ let cwd = inner.cwd();
+
+ let mut cmd = Command::new(args.get(0));
+ (1..args.len()).for_each(|i| {
+ let arg = args.get(i);
+ cmd.arg(arg);
+ });
+ cwd.map(|d| cmd.current_dir(d));
+
+ cmd.stdin(subprocess_stdio_map(inner.stdin()));
+ cmd.stdout(subprocess_stdio_map(inner.stdout()));
+ cmd.stderr(subprocess_stdio_map(inner.stderr()));
+
+ // Spawn the command.
+ let child = match cmd.spawn_async() {
+ Ok(v) => v,
+ Err(err) => {
+ return odd_future(err.into());
+ }
+ };
+
+ let pid = child.id();
+ let resources = resources::add_child(child);
+
+ let mut res_args = msg::RunResArgs {
+ rid: resources.child_rid,
+ pid,
+ ..Default::default()
+ };
+
+ if let Some(stdin_rid) = resources.stdin_rid {
+ res_args.stdin_rid = stdin_rid;
+ }
+ if let Some(stdout_rid) = resources.stdout_rid {
+ res_args.stdout_rid = stdout_rid;
+ }
+ if let Some(stderr_rid) = resources.stderr_rid {
+ res_args.stderr_rid = stderr_rid;
+ }
+
+ let builder = &mut FlatBufferBuilder::new();
+ let inner = msg::RunRes::create(builder, &res_args);
+ ok_future(serialize_response(
+ cmd_id,
+ builder,
+ msg::BaseArgs {
+ inner: Some(inner.as_union_value()),
+ inner_type: msg::Any::RunRes,
+ ..Default::default()
+ },
+ ))
+}
+
+fn op_run_status(
+ state: &Arc<IsolateState>,
+ base: &msg::Base,
+ data: &'static mut [u8],
+) -> Box<Op> {
+ assert_eq!(data.len(), 0);
+ let cmd_id = base.cmd_id();
+ let inner = base.inner_as_run_status().unwrap();
+ let rid = inner.rid();
+
+ if let Err(e) = state.check_run() {
+ return odd_future(e);
+ }
+
+ let future = match resources::child_status(rid) {
+ Err(e) => {
+ return odd_future(e);
+ }
+ Ok(f) => f,
+ };
+
+ let future = future.and_then(move |run_status| {
+ 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();
+
+ let builder = &mut FlatBufferBuilder::new();
+ let inner = msg::RunStatusRes::create(
+ builder,
+ &msg::RunStatusResArgs {
+ got_signal,
+ exit_code: code.unwrap_or(-1),
+ exit_signal: signal.unwrap_or(-1),
+ ..Default::default()
+ },
+ );
+ Ok(serialize_response(
+ cmd_id,
+ builder,
+ msg::BaseArgs {
+ inner: Some(inner.as_union_value()),
+ inner_type: msg::Any::RunStatusRes,
+ ..Default::default()
+ },
+ ))
+ });
+ Box::new(future)
+}
diff --git a/src/permissions.rs b/src/permissions.rs
index 263c936fe..7ca13d44a 100644
--- a/src/permissions.rs
+++ b/src/permissions.rs
@@ -12,6 +12,7 @@ pub struct DenoPermissions {
pub allow_write: bool,
pub allow_net: bool,
pub allow_env: bool,
+ pub allow_run: bool,
}
impl DenoPermissions {
@@ -20,9 +21,22 @@ impl DenoPermissions {
allow_write: flags.allow_write,
allow_env: flags.allow_env,
allow_net: flags.allow_net,
+ allow_run: flags.allow_run,
}
}
+ pub fn check_run(&mut self) -> DenoResult<()> {
+ if self.allow_run {
+ return Ok(());
+ };
+ // TODO get location (where access occurred)
+ let r = permission_prompt("Deno requests access to run a subprocess.");
+ if r.is_ok() {
+ self.allow_run = true;
+ }
+ r
+ }
+
pub fn check_write(&mut self, filename: &str) -> DenoResult<()> {
if self.allow_write {
return Ok(());
diff --git a/src/resources.rs b/src/resources.rs
index 90b7ce772..36e0d9486 100644
--- a/src/resources.rs
+++ b/src/resources.rs
@@ -20,12 +20,14 @@ use tokio_write;
use futures;
use futures::future::{Either, FutureResult};
+use futures::Future;
use futures::Poll;
use hyper;
use std;
use std::collections::HashMap;
use std::io::{Error, Read, Write};
use std::net::{Shutdown, SocketAddr};
+use std::process::ExitStatus;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;
use std::sync::Mutex;
@@ -33,6 +35,7 @@ use tokio;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use tokio_io;
+use tokio_process;
pub type ResourceId = u32; // Sometimes referred to RID.
@@ -63,6 +66,10 @@ enum Repr {
TcpStream(tokio::net::TcpStream),
HttpBody(HttpBody),
Repl(Repl),
+ Child(tokio_process::Child),
+ ChildStdin(tokio_process::ChildStdin),
+ ChildStdout(tokio_process::ChildStdout),
+ ChildStderr(tokio_process::ChildStderr),
}
pub fn table_entries() -> Vec<(u32, String)> {
@@ -94,6 +101,10 @@ fn inspect_repr(repr: &Repr) -> String {
Repr::TcpStream(_) => "tcpStream",
Repr::HttpBody(_) => "httpBody",
Repr::Repl(_) => "repl",
+ Repr::Child(_) => "child",
+ Repr::ChildStdin(_) => "childStdin",
+ Repr::ChildStdout(_) => "childStdout",
+ Repr::ChildStderr(_) => "childStderr",
};
String::from(h_repr)
@@ -160,6 +171,8 @@ impl AsyncRead for Resource {
Repr::Stdin(ref mut f) => f.poll_read(buf),
Repr::TcpStream(ref mut f) => f.poll_read(buf),
Repr::HttpBody(ref mut f) => f.poll_read(buf),
+ Repr::ChildStdout(ref mut f) => f.poll_read(buf),
+ Repr::ChildStderr(ref mut f) => f.poll_read(buf),
_ => panic!("Cannot read"),
},
}
@@ -187,6 +200,7 @@ impl AsyncWrite for Resource {
Repr::Stdout(ref mut f) => f.poll_write(buf),
Repr::Stderr(ref mut f) => f.poll_write(buf),
Repr::TcpStream(ref mut f) => f.poll_write(buf),
+ Repr::ChildStdin(ref mut f) => f.poll_write(buf),
_ => panic!("Cannot write"),
},
}
@@ -244,6 +258,80 @@ pub fn add_repl(repl: Repl) -> Resource {
Resource { rid }
}
+pub struct ChildResources {
+ pub child_rid: ResourceId,
+ pub stdin_rid: Option<ResourceId>,
+ pub stdout_rid: Option<ResourceId>,
+ pub stderr_rid: Option<ResourceId>,
+}
+
+pub fn add_child(mut c: tokio_process::Child) -> ChildResources {
+ let child_rid = new_rid();
+ let mut tg = RESOURCE_TABLE.lock().unwrap();
+
+ let mut resources = ChildResources {
+ child_rid,
+ stdin_rid: None,
+ stdout_rid: None,
+ stderr_rid: None,
+ };
+
+ if c.stdin().is_some() {
+ let stdin = c.stdin().take().unwrap();
+ let rid = new_rid();
+ let r = tg.insert(rid, Repr::ChildStdin(stdin));
+ assert!(r.is_none());
+ resources.stdin_rid = Some(rid);
+ }
+ if c.stdout().is_some() {
+ let stdout = c.stdout().take().unwrap();
+ let rid = new_rid();
+ let r = tg.insert(rid, Repr::ChildStdout(stdout));
+ assert!(r.is_none());
+ resources.stdout_rid = Some(rid);
+ }
+ if c.stderr().is_some() {
+ let stderr = c.stderr().take().unwrap();
+ let rid = new_rid();
+ let r = tg.insert(rid, Repr::ChildStderr(stderr));
+ assert!(r.is_none());
+ resources.stderr_rid = Some(rid);
+ }
+
+ let r = tg.insert(child_rid, Repr::Child(c));
+ assert!(r.is_none());
+
+ return resources;
+}
+
+pub struct ChildStatus {
+ rid: ResourceId,
+}
+
+// Invert the dumbness that tokio_process causes by making Child itself a future.
+impl Future for ChildStatus {
+ type Item = ExitStatus;
+ type Error = DenoError;
+
+ fn poll(&mut self) -> Poll<ExitStatus, DenoError> {
+ let mut table = RESOURCE_TABLE.lock().unwrap();
+ let maybe_repr = table.get_mut(&self.rid);
+ match maybe_repr {
+ Some(Repr::Child(ref mut child)) => child.poll().map_err(DenoError::from),
+ _ => Err(bad_resource()),
+ }
+ }
+}
+
+pub fn child_status(rid: ResourceId) -> DenoResult<ChildStatus> {
+ let mut table = RESOURCE_TABLE.lock().unwrap();
+ let maybe_repr = table.get_mut(&rid);
+ match maybe_repr {
+ Some(Repr::Child(ref mut _child)) => Ok(ChildStatus { rid }),
+ _ => Err(bad_resource()),
+ }
+}
+
pub fn readline(rid: ResourceId, prompt: &str) -> DenoResult<String> {
let mut table = RESOURCE_TABLE.lock().unwrap();
let maybe_repr = table.get_mut(&rid);
diff --git a/tools/unit_tests.py b/tools/unit_tests.py
index 9682aec89..47dfcf886 100755
--- a/tools/unit_tests.py
+++ b/tools/unit_tests.py
@@ -41,10 +41,12 @@ def run_unit_test(deno_exe, permStr, flags=[]):
# tests by the special string. permW0N0 means allow-write but not allow-net.
# See js/test_util.ts for more details.
def unit_tests(deno_exe):
- run_unit_test(deno_exe, "permW0N0E0")
- run_unit_test(deno_exe, "permW1N0E0", ["--allow-write"])
- run_unit_test(deno_exe, "permW0N1E0", ["--allow-net"])
- run_unit_test(deno_exe, "permW0N0E1", ["--allow-env"])
+ run_unit_test(deno_exe, "permW0N0E0R0")
+ run_unit_test(deno_exe, "permW1N0E0R0", ["--allow-write"])
+ run_unit_test(deno_exe, "permW0N1E0R0", ["--allow-net"])
+ run_unit_test(deno_exe, "permW0N0E1R0", ["--allow-env"])
+ run_unit_test(deno_exe, "permW0N0E0R1", ["--allow-run"])
+ run_unit_test(deno_exe, "permW1N0E0R1", ["--allow-run", "--allow-write"])
# TODO We might accidentally miss some. We should be smarter about which we
# run. Maybe we can use the "filtered out" number to check this.