Skip to content

Commit

Permalink
First version of run command support
Browse files Browse the repository at this point in the history
  • Loading branch information
piscisaureus authored and ry committed Nov 14, 2018
1 parent b157946 commit 8578ab4
Show file tree
Hide file tree
Showing 14 changed files with 629 additions and 13 deletions.
5 changes: 3 additions & 2 deletions BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,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",
Expand All @@ -114,9 +114,10 @@ ts_sources = [
"js/read_link.ts",
"js/remove.ts",
"js/rename.ts",
"js/resources.ts",
"js/repl.ts",
"js/resources.ts",
"js/stat.ts",
"js/subprocess.ts",
"js/symlink.ts",
"js/text_encoding.ts",
"js/timers.ts",
Expand Down
1 change: 1 addition & 0 deletions js/deno.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, Subprocess, SubprocessStatus } from "./subprocess";
export const args: string[] = [];

// Provide the compiler API in an obfuscated way
Expand Down
136 changes: 136 additions & 0 deletions js/subprocess.ts
Original file line number Diff line number Diff line change
@@ -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 SubprocessStdio = "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?: SubprocessStdio;
stderr?: SubprocessStdio;
stdin?: SubprocessStdio;
}

export class Subprocess {
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<SubprocessStatus> {
return await runStatus(this.rid);
}

close(): void {
close(this.rid);
}
}

export interface SubprocessStatus {
success: boolean;
code?: number;
signal?: number; // TODO: Make this a string, e.g. 'SIGTERM'.
}

function stdioMap(s: SubprocessStdio): msg.SubprocessStdio {
switch (s) {
case "inherit":
return msg.SubprocessStdio.Inherit;
case "piped":
return msg.SubprocessStdio.Piped;
case "null":
return msg.SubprocessStdio.Null;
default:
return unreachable();
}
}

export function run(opt: RunOptions): Subprocess {
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 Subprocess(res);
}

async function runStatus(rid: number): Promise<SubprocessStatus> {
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 changes: 178 additions & 0 deletions js/subprocess_test.ts
Original file line number Diff line number Diff line change
@@ -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 child = run({
args: ["python", "-c", "print('hello world')"]
});
const status = await child.status();
console.log("status", status);
assertEqual(status.success, true);
assertEqual(status.code, 0);
assertEqual(status.signal, undefined);
child.close();
});

testPerm({ run: true }, async function runCommandFailedWithCode() {
let child = run({
args: ["python", "-c", "import sys;sys.exit(41 + 1)"]
});
let status = await child.status();
assertEqual(status.success, false);
assertEqual(status.code, 42);
assertEqual(status.signal, undefined);
child.close();
});

testPerm({ run: true }, async function runCommandFailedWithSignal() {
if (deno.platform.os === "win") {
return; // No signals on windows.
}
const child = run({
args: ["python", "-c", "import os;os.kill(os.getpid(), 9)"]
});
const status = await child.status();
assertEqual(status.success, false);
assertEqual(status.code, undefined);
assertEqual(status.signal, 9);
child.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 child = 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 child.status();
assertEqual(status.success, false);
assertEqual(status.code, code);
assertEqual(status.signal, undefined);
child.close();
});

testPerm({ run: true }, async function runStdinPiped() {
const child = run({
args: ["python", "-c", "import sys; assert 'hello' == sys.stdin.read();"],
stdin: "piped"
});
assert(!child.stdout);
assert(!child.stderr);

let msg = new TextEncoder().encode("hello");
let n = await child.stdin.write(msg);
assertEqual(n, msg.byteLength);

child.stdin.close();

const status = await child.status();
assertEqual(status.success, true);
assertEqual(status.code, 0);
assertEqual(status.signal, undefined);
child.close();
});

testPerm({ run: true }, async function runStdoutPiped() {
const child = run({
args: ["python", "-c", "import sys; sys.stdout.write('hello')"],
stdout: "piped"
});
assert(!child.stdin);
assert(!child.stderr);

const data = new Uint8Array(10);
let r = await child.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 child.stdout.read(data);
assertEqual(r.nread, 0);
assertEqual(r.eof, true);
child.stdout.close();

const status = await child.status();
assertEqual(status.success, true);
assertEqual(status.code, 0);
assertEqual(status.signal, undefined);
child.close();
});

testPerm({ run: true }, async function runStderrPiped() {
const child = run({
args: ["python", "-c", "import sys; sys.stderr.write('hello')"],
stderr: "piped"
});
assert(!child.stdin);
assert(!child.stdout);

const data = new Uint8Array(10);
let r = await child.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 child.stderr.read(data);
assertEqual(r.nread, 0);
assertEqual(r.eof, true);
child.stderr.close();

const status = await child.status();
assertEqual(status.success, true);
assertEqual(status.code, 0);
assertEqual(status.signal, undefined);
child.close();
});
15 changes: 10 additions & 5 deletions js/test_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,28 @@ 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");
}
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]))
};
}

Expand All @@ -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)));
}
}
}
}
Expand Down
Loading

0 comments on commit 8578ab4

Please sign in to comment.