diff options
Diffstat (limited to 'js')
-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 |
6 files changed, 332 insertions, 5 deletions
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"; +} |