mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 15:24:46 -05:00
First pass at running subprocesses (#1156)
This commit is contained in:
parent
9b702da9e5
commit
48bf4062e4
14 changed files with 629 additions and 17 deletions
5
BUILD.gn
5
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",
|
||||
|
|
|
@ -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
|
||||
|
|
136
js/process.ts
Normal file
136
js/process.ts
Normal file
|
@ -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 };
|
||||
}
|
||||
}
|
178
js/process_test.ts
Normal file
178
js/process_test.ts
Normal file
|
@ -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();
|
||||
});
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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,
|
||||
|
|
37
src/msg.fbs
37
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;
|
||||
|
|
141
src/ops.rs
141
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)
|
||||
}
|
||||
|
|
|
@ -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(());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
Loading…
Reference in a new issue