-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
First version of run command support
- Loading branch information
1 parent
b157946
commit 8578ab4
Showing
14 changed files
with
629 additions
and
13 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 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 }; | ||
} | ||
} |
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 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(); | ||
}); |
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
Oops, something went wrong.