diff options
-rw-r--r-- | BUILD.gn | 5 | ||||
-rw-r--r-- | js/deno.ts | 1 | ||||
-rw-r--r-- | js/process.ts | 136 | ||||
-rw-r--r-- | js/process_test.ts | 178 | ||||
-rw-r--r-- | js/test_util.ts | 15 | ||||
-rw-r--r-- | js/unit_tests.ts | 1 | ||||
-rw-r--r-- | js/util.ts | 6 | ||||
-rw-r--r-- | src/flags.rs | 9 | ||||
-rw-r--r-- | src/isolate.rs | 5 | ||||
-rw-r--r-- | src/msg.fbs | 37 | ||||
-rw-r--r-- | src/ops.rs | 141 | ||||
-rw-r--r-- | src/permissions.rs | 14 | ||||
-rw-r--r-- | src/resources.rs | 88 | ||||
-rwxr-xr-x | tools/unit_tests.py | 10 |
14 files changed, 629 insertions, 17 deletions
@@ -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. |