forked from denoland/deno
-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First pass at running subprocesses (denoland#1156)
- Loading branch information
1 parent
9b702da
commit 48bf406
Showing
14 changed files
with
629 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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 }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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(); | ||
}); |
Oops, something went wrong.