1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-25 16:49:18 -05:00

feat(runtime): two-tier subprocess API (#11618)

This commit is contained in:
Leo Kettmeir 2022-04-21 00:20:33 +02:00 committed by GitHub
parent 8b25807054
commit 8a7539cab3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1323 additions and 5 deletions

View file

@ -236,7 +236,7 @@ jobs:
~/.cargo/registry/index ~/.cargo/registry/index
~/.cargo/registry/cache ~/.cargo/registry/cache
~/.cargo/git/db ~/.cargo/git/db
key: 8-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }} key: 9-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }}
# In main branch, always creates fresh cache # In main branch, always creates fresh cache
- name: Cache build output (main) - name: Cache build output (main)
@ -252,7 +252,7 @@ jobs:
!./target/*/*.zip !./target/*/*.zip
!./target/*/*.tar.gz !./target/*/*.tar.gz
key: | key: |
8-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }} 9-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }}
# Restore cache from the latest 'main' branch build. # Restore cache from the latest 'main' branch build.
- name: Cache build output (PR) - name: Cache build output (PR)
@ -268,7 +268,7 @@ jobs:
!./target/*/*.tar.gz !./target/*/*.tar.gz
key: never_saved key: never_saved
restore-keys: | restore-keys: |
8-cargo-target-${{ matrix.os }}-${{ matrix.profile }}- 9-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-
# Don't save cache after building PRs or branches other than 'main'. # Don't save cache after building PRs or branches other than 'main'.
- name: Skip save cache (PR) - name: Skip save cache (PR)

View file

@ -66,6 +66,12 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
"umask", "umask",
"utime", "utime",
"utimeSync", "utimeSync",
"spawnChild",
"Child",
"spawn",
"spawnSync",
"ChildStatus",
"SpawnOutput",
]; ];
static MSG_MISSING_PROPERTY_DENO: Lazy<Regex> = Lazy::new(|| { static MSG_MISSING_PROPERTY_DENO: Lazy<Regex> = Lazy::new(|| {

View file

@ -1361,6 +1361,145 @@ declare namespace Deno {
export function upgradeHttp( export function upgradeHttp(
request: Request, request: Request,
): Promise<[Deno.Conn, Uint8Array]>; ): Promise<[Deno.Conn, Uint8Array]>;
export interface SpawnOptions {
/** Arguments to pass to the process. */
args?: string[];
/**
* The working directory of the process.
* If not specified, the cwd of the parent process is used.
*/
cwd?: string | URL;
/**
* Clear environmental variables from parent process.
* Doesn't guarantee that only `opt.env` variables are present,
* as the OS may set environmental variables for processes.
*/
clearEnv?: boolean;
/** Environmental variables to pass to the subprocess. */
env?: Record<string, string>;
/**
* Sets the child processs user ID. This translates to a setuid call
* in the child process. Failure in the setuid call will cause the spawn to fail.
*/
uid?: number;
/** Similar to `uid`, but sets the group ID of the child process. */
gid?: number;
/** Defaults to "null". */
stdin?: "piped" | "inherit" | "null";
/** Defaults to "piped". */
stdout?: "piped" | "inherit" | "null";
/** Defaults to "piped". */
stderr?: "piped" | "inherit" | "null";
}
/**
* Spawns a child process.
*
* If stdin is set to "piped", the stdin WritableStream needs to be closed manually.
*
* ```ts
* const child = Deno.spawnChild(Deno.execPath(), {
* args: [
* "eval",
* "console.log('Hello World')",
* ],
* stdin: "piped",
* });
*
* // open a file and pipe the subprocess output to it.
* child.stdout.pipeTo(Deno.openSync("output").writable);
*
* // manually close stdin
* child.stdin.close();
* const status = await child.status;
* ```
*/
export function spawnChild<T extends SpawnOptions = SpawnOptions>(
command: string | URL,
options?: T,
): Child<T>;
export class Child<T extends SpawnOptions> {
readonly stdin: T["stdin"] extends "piped" ? WritableStream<Uint8Array>
: null;
readonly stdout: T["stdout"] extends "inherit" | "null" ? null
: ReadableStream<Uint8Array>;
readonly stderr: T["stderr"] extends "inherit" | "null" ? null
: ReadableStream<Uint8Array>;
readonly pid: number;
/** Get the status of the child. */
readonly status: Promise<ChildStatus>;
/** Waits for the child to exit completely, returning all its output and status. */
output(): Promise<SpawnOutput<T>>;
/** Kills the process with given Signal. */
kill(signo: Signal): void;
}
/**
* Executes a subprocess, waiting for it to finish and
* collecting all of its output.
* The stdio options are ignored.
*
* ```ts
* const { status, stdout, stderr } = await Deno.spawn(Deno.execPath(), {
* args: [
* "eval",
* "console.log('hello'); console.error('world')",
* ],
* });
* console.assert(status.code === 0);
* console.assert("hello\n" === new TextDecoder().decode(stdout));
* console.assert("world\n" === new TextDecoder().decode(stderr));
* ```
*/
export function spawn<T extends SpawnOptions = SpawnOptions>(
command: string | URL,
options?: T,
): Promise<SpawnOutput<T>>;
/**
* Synchronously executes a subprocess, waiting for it to finish and
* collecting all of its output.
* The stdio options are ignored.
*
* * ```ts
* const { status, stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
* args: [
* "eval",
* "console.log('hello'); console.error('world')",
* ],
* });
* console.assert(status.code === 0);
* console.assert("hello\n" === new TextDecoder().decode(stdout));
* console.assert("world\n" === new TextDecoder().decode(stderr));
* ```
*/
export function spawnSync<T extends SpawnOptions = SpawnOptions>(
command: string | URL,
options?: T,
): SpawnOutput<T>;
export type ChildStatus =
| {
success: true;
code: 0;
signal: null;
}
| {
success: false;
code: number;
signal: number | null;
};
export interface SpawnOutput<T extends SpawnOptions> {
status: ChildStatus;
stdout: T["stdout"] extends "inherit" | "null" ? null : Uint8Array;
stderr: T["stderr"] extends "inherit" | "null" ? null : Uint8Array;
}
} }
declare function fetch( declare function fetch(

View file

@ -0,0 +1,687 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import {
assert,
assertEquals,
assertRejects,
assertStringIncludes,
assertThrows,
} from "./test_util.ts";
Deno.test(
{ permissions: { write: true, run: true, read: true } },
async function spawnWithCwdIsAsync() {
const enc = new TextEncoder();
const cwd = await Deno.makeTempDir({ prefix: "deno_command_test" });
const exitCodeFile = "deno_was_here";
const programFile = "poll_exit.ts";
const program = `
async function tryExit() {
try {
const code = parseInt(await Deno.readTextFile("${exitCodeFile}"));
Deno.exit(code);
} catch {
// Retry if we got here before deno wrote the file.
setTimeout(tryExit, 0.01);
}
}
tryExit();
`;
Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program));
const child = Deno.spawnChild(Deno.execPath(), {
cwd,
args: ["run", "--allow-read", programFile],
stdout: "inherit",
stderr: "inherit",
});
// Write the expected exit code *after* starting deno.
// This is how we verify that `Child` is actually asynchronous.
const code = 84;
Deno.writeFileSync(`${cwd}/${exitCodeFile}`, enc.encode(`${code}`));
const status = await child.status;
await Deno.remove(cwd, { recursive: true });
assertEquals(status.success, false);
assertEquals(status.code, code);
assertEquals(status.signal, null);
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnStdinPiped() {
const child = Deno.spawnChild(Deno.execPath(), {
args: [
"eval",
"if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
],
stdin: "piped",
stdout: "null",
stderr: "null",
});
assert(child.stdin !== null);
assert(child.stdout === null);
assert(child.stderr === null);
const msg = new TextEncoder().encode("hello");
const writer = child.stdin.getWriter();
await writer.write(msg);
writer.releaseLock();
await child.stdin.close();
const status = await child.status;
assertEquals(status.success, true);
assertEquals(status.code, 0);
assertEquals(status.signal, null);
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnStdoutPiped() {
const child = Deno.spawnChild(Deno.execPath(), {
args: [
"eval",
"await Deno.stdout.write(new TextEncoder().encode('hello'))",
],
stderr: "null",
});
assert(child.stdin === null);
assert(child.stdout !== null);
assert(child.stderr === null);
const readable = child.stdout.pipeThrough(new TextDecoderStream());
const reader = readable.getReader();
const res = await reader.read();
assert(!res.done);
assertEquals(res.value, "hello");
const resEnd = await reader.read();
assert(resEnd.done);
assertEquals(resEnd.value, undefined);
reader.releaseLock();
const status = await child.status;
assertEquals(status.success, true);
assertEquals(status.code, 0);
assertEquals(status.signal, null);
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnStderrPiped() {
const child = Deno.spawnChild(Deno.execPath(), {
args: [
"eval",
"await Deno.stderr.write(new TextEncoder().encode('hello'))",
],
stderr: "piped",
stdout: "null",
});
assert(child.stdin === null);
assert(child.stdout === null);
assert(child.stderr !== null);
const readable = child.stderr.pipeThrough(new TextDecoderStream());
const reader = readable.getReader();
const res = await reader.read();
assert(!res.done);
assertEquals(res.value, "hello");
const resEnd = await reader.read();
assert(resEnd.done);
assertEquals(resEnd.value, undefined);
reader.releaseLock();
const status = await child.status;
assertEquals(status.success, true);
assertEquals(status.code, 0);
assertEquals(status.signal, null);
},
);
Deno.test(
{ permissions: { run: true, write: true, read: true } },
async function spawnRedirectStdoutStderr() {
const tempDir = await Deno.makeTempDir();
const fileName = tempDir + "/redirected_stdio.txt";
const file = await Deno.open(fileName, {
create: true,
write: true,
});
const child = Deno.spawnChild(Deno.execPath(), {
args: [
"eval",
"Deno.stderr.write(new TextEncoder().encode('error\\n')); Deno.stdout.write(new TextEncoder().encode('output\\n'));",
],
});
await child.stdout.pipeTo(file.writable, {
preventClose: true,
});
await child.stderr.pipeTo(file.writable);
await child.status;
const fileContents = await Deno.readFile(fileName);
const decoder = new TextDecoder();
const text = decoder.decode(fileContents);
assertStringIncludes(text, "error");
assertStringIncludes(text, "output");
},
);
Deno.test(
{ permissions: { run: true, write: true, read: true } },
async function spawnRedirectStdin() {
const tempDir = await Deno.makeTempDir();
const fileName = tempDir + "/redirected_stdio.txt";
const encoder = new TextEncoder();
await Deno.writeFile(fileName, encoder.encode("hello"));
const file = await Deno.open(fileName);
const child = Deno.spawnChild(Deno.execPath(), {
args: [
"eval",
"if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
],
stdin: "piped",
stdout: "null",
stderr: "null",
});
await file.readable.pipeTo(child.stdin, {
preventClose: true,
});
await child.stdin.close();
const status = await child.status;
assertEquals(status.code, 0);
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnKillSuccess() {
const child = Deno.spawnChild(Deno.execPath(), {
args: ["eval", "setTimeout(() => {}, 10000)"],
stdout: "null",
stderr: "null",
});
child.kill("SIGKILL");
const status = await child.status;
assertEquals(status.success, false);
if (Deno.build.os === "windows") {
assertEquals(status.code, 1);
assertEquals(status.signal, null);
} else {
assertEquals(status.code, 137);
assertEquals(status.signal, 9);
}
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnKillFailed() {
const child = Deno.spawnChild(Deno.execPath(), {
args: ["eval", "setTimeout(() => {}, 5000)"],
stdout: "null",
stderr: "null",
});
assertThrows(() => {
// @ts-expect-error testing runtime error of bad signal
child.kill("foobar");
}, TypeError);
await child.status;
},
);
Deno.test(
{ permissions: { read: true, run: false } },
async function spawnPermissions() {
await assertRejects(async () => {
await Deno.spawn(Deno.execPath(), {
args: ["eval", "console.log('hello world')"],
});
}, Deno.errors.PermissionDenied);
},
);
Deno.test(
{ permissions: { read: true, run: false } },
function spawnSyncPermissions() {
assertThrows(() => {
Deno.spawnSync(Deno.execPath(), {
args: ["eval", "console.log('hello world')"],
});
}, Deno.errors.PermissionDenied);
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnSuccess() {
const { status } = await Deno.spawn(Deno.execPath(), {
args: ["eval", "console.log('hello world')"],
});
assertEquals(status.success, true);
assertEquals(status.code, 0);
assertEquals(status.signal, null);
},
);
Deno.test(
{ permissions: { run: true, read: true } },
function spawnSyncSuccess() {
const { status } = Deno.spawnSync(Deno.execPath(), {
args: ["eval", "console.log('hello world')"],
});
assertEquals(status.success, true);
assertEquals(status.code, 0);
assertEquals(status.signal, null);
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnUrl() {
const { status, stdout } = await Deno.spawn(
new URL(`file:///${Deno.execPath()}`),
{
args: ["eval", "console.log('hello world')"],
},
);
assertEquals(new TextDecoder().decode(stdout), "hello world\n");
assertEquals(status.success, true);
assertEquals(status.code, 0);
assertEquals(status.signal, null);
},
);
Deno.test(
{ permissions: { run: true, read: true } },
function spawnSyncUrl() {
const { status, stdout } = Deno.spawnSync(
new URL(`file:///${Deno.execPath()}`),
{
args: ["eval", "console.log('hello world')"],
},
);
assertEquals(new TextDecoder().decode(stdout), "hello world\n");
assertEquals(status.success, true);
assertEquals(status.code, 0);
assertEquals(status.signal, null);
},
);
Deno.test({ permissions: { run: true } }, async function spawnNotFound() {
await assertRejects(
() => Deno.spawn("this file hopefully doesn't exist"),
Deno.errors.NotFound,
);
});
Deno.test({ permissions: { run: true } }, function spawnSyncNotFound() {
assertThrows(
() => Deno.spawnSync("this file hopefully doesn't exist"),
Deno.errors.NotFound,
);
});
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnFailedWithCode() {
const { status } = await Deno.spawn(Deno.execPath(), {
args: ["eval", "Deno.exit(41 + 1)"],
});
assertEquals(status.success, false);
assertEquals(status.code, 42);
assertEquals(status.signal, null);
},
);
Deno.test(
{ permissions: { run: true, read: true } },
function spawnSyncFailedWithCode() {
const { status } = Deno.spawnSync(Deno.execPath(), {
args: ["eval", "Deno.exit(41 + 1)"],
});
assertEquals(status.success, false);
assertEquals(status.code, 42);
assertEquals(status.signal, null);
},
);
Deno.test(
{
permissions: { run: true, read: true },
},
async function spawnFailedWithSignal() {
const { status } = await Deno.spawn(Deno.execPath(), {
args: ["eval", "--unstable", "Deno.kill(Deno.pid, 'SIGKILL')"],
});
assertEquals(status.success, false);
if (Deno.build.os === "windows") {
assertEquals(status.code, 1);
assertEquals(status.signal, null);
} else {
assertEquals(status.code, 128 + 9);
assertEquals(status.signal, 9);
}
},
);
Deno.test(
{
permissions: { run: true, read: true },
},
function spawnSyncFailedWithSignal() {
const { status } = Deno.spawnSync(Deno.execPath(), {
args: ["eval", "--unstable", "Deno.kill(Deno.pid, 'SIGKILL')"],
});
assertEquals(status.success, false);
if (Deno.build.os === "windows") {
assertEquals(status.code, 1);
assertEquals(status.signal, null);
} else {
assertEquals(status.code, 128 + 9);
assertEquals(status.signal, 9);
}
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnOutput() {
const { stdout } = await Deno.spawn(Deno.execPath(), {
args: [
"eval",
"await Deno.stdout.write(new TextEncoder().encode('hello'))",
],
});
const s = new TextDecoder().decode(stdout);
assertEquals(s, "hello");
},
);
Deno.test(
{ permissions: { run: true, read: true } },
function spawnSyncOutput() {
const { stdout } = Deno.spawnSync(Deno.execPath(), {
args: [
"eval",
"await Deno.stdout.write(new TextEncoder().encode('hello'))",
],
});
const s = new TextDecoder().decode(stdout);
assertEquals(s, "hello");
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnStderrOutput() {
const { stderr } = await Deno.spawn(Deno.execPath(), {
args: [
"eval",
"await Deno.stderr.write(new TextEncoder().encode('error'))",
],
});
const s = new TextDecoder().decode(stderr);
assertEquals(s, "error");
},
);
Deno.test(
{ permissions: { run: true, read: true } },
function spawnSyncStderrOutput() {
const { stderr } = Deno.spawnSync(Deno.execPath(), {
args: [
"eval",
"await Deno.stderr.write(new TextEncoder().encode('error'))",
],
});
const s = new TextDecoder().decode(stderr);
assertEquals(s, "error");
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnOverrideStdio() {
const { stdout, stderr } = await Deno.spawn(Deno.execPath(), {
args: [
"eval",
"console.log('hello'); console.error('world')",
],
stdin: "piped",
stdout: "null",
stderr: "null",
});
// @ts-ignore: for testing
assertEquals(new TextDecoder().decode(stdout), "hello\n");
// @ts-ignore: for testing
assertEquals(new TextDecoder().decode(stderr), "world\n");
},
);
Deno.test(
{ permissions: { run: true, read: true } },
function spawnSyncOverrideStdio() {
const { stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
args: [
"eval",
"console.log('hello'); console.error('world')",
],
stdin: "piped",
stdout: "null",
stderr: "null",
});
// @ts-ignore: for testing
assertEquals(new TextDecoder().decode(stdout), "hello\n");
// @ts-ignore: for testing
assertEquals(new TextDecoder().decode(stderr), "world\n");
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function spawnEnv() {
const { stdout } = await Deno.spawn(Deno.execPath(), {
args: [
"eval",
"Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
],
env: {
FOO: "0123",
BAR: "4567",
},
});
const s = new TextDecoder().decode(stdout);
assertEquals(s, "01234567");
},
);
Deno.test(
{ permissions: { run: true, read: true } },
function spawnEnv() {
const { stdout } = Deno.spawnSync(Deno.execPath(), {
args: [
"eval",
"Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
],
env: {
FOO: "0123",
BAR: "4567",
},
});
const s = new TextDecoder().decode(stdout);
assertEquals(s, "01234567");
},
);
Deno.test(
{ permissions: { run: true, read: true, env: true } },
async function spawnClearEnv() {
const { stdout } = await Deno.spawn(Deno.execPath(), {
args: [
"eval",
"-p",
"JSON.stringify(Deno.env.toObject())",
],
clearEnv: true,
env: {
FOO: "23147",
},
});
const obj = JSON.parse(new TextDecoder().decode(stdout));
// can't check for object equality because the OS may set additional env
// vars for processes, so we check if PATH isn't present as that is a common
// env var across OS's and isn't set for processes.
assertEquals(obj.FOO, "23147");
assert(!("PATH" in obj));
},
);
Deno.test(
{ permissions: { run: true, read: true, env: true } },
function spawnSyncClearEnv() {
const { stdout } = Deno.spawnSync(Deno.execPath(), {
args: [
"eval",
"-p",
"JSON.stringify(Deno.env.toObject())",
],
clearEnv: true,
env: {
FOO: "23147",
},
});
const obj = JSON.parse(new TextDecoder().decode(stdout));
// can't check for object equality because the OS may set additional env
// vars for processes, so we check if PATH isn't present as that is a common
// env var across OS's and isn't set for processes.
assertEquals(obj.FOO, "23147");
assert(!("PATH" in obj));
},
);
Deno.test(
{
permissions: { run: true, read: true },
ignore: Deno.build.os === "windows",
},
async function spawnUid() {
const { stdout } = await Deno.spawn("id", {
args: ["-u"],
});
const currentUid = new TextDecoder().decode(stdout);
if (currentUid !== "0") {
await assertRejects(async () => {
await Deno.spawn("echo", {
args: ["fhqwhgads"],
uid: 0,
});
}, Deno.errors.PermissionDenied);
}
},
);
Deno.test(
{
permissions: { run: true, read: true },
ignore: Deno.build.os === "windows",
},
function spawnSyncUid() {
const { stdout } = Deno.spawnSync("id", {
args: ["-u"],
});
const currentUid = new TextDecoder().decode(stdout);
if (currentUid !== "0") {
assertThrows(() => {
Deno.spawnSync("echo", {
args: ["fhqwhgads"],
uid: 0,
});
}, Deno.errors.PermissionDenied);
}
},
);
Deno.test(
{
permissions: { run: true, read: true },
ignore: Deno.build.os === "windows",
},
async function spawnGid() {
const { stdout } = await Deno.spawn("id", {
args: ["-g"],
});
const currentGid = new TextDecoder().decode(stdout);
if (currentGid !== "0") {
await assertRejects(async () => {
await Deno.spawn("echo", {
args: ["fhqwhgads"],
gid: 0,
});
}, Deno.errors.PermissionDenied);
}
},
);
Deno.test(
{
permissions: { run: true, read: true },
ignore: Deno.build.os === "windows",
},
function spawnSyncGid() {
const { stdout } = Deno.spawnSync("id", {
args: ["-g"],
});
const currentGid = new TextDecoder().decode(stdout);
if (currentGid !== "0") {
assertThrows(() => {
Deno.spawnSync("echo", {
args: ["fhqwhgads"],
gid: 0,
});
}, Deno.errors.PermissionDenied);
}
},
);

View file

@ -557,8 +557,9 @@ Deno.test(
const obj = JSON.parse(new TextDecoder().decode(await p.output())); const obj = JSON.parse(new TextDecoder().decode(await p.output()));
// can't check for object equality because the OS may set additional env vars for processes // can't check for object equality because the OS may set additional env
// so we check if PATH isn't present as that is a common env var across OS's and isn't set for processes. // vars for processes, so we check if PATH isn't present as that is a common
// env var across OS's and isn't set for processes.
assertEquals(obj.FOO, "23147"); assertEquals(obj.FOO, "23147");
assert(!("PATH" in obj)); assert(!("PATH" in obj));

View file

@ -630,6 +630,7 @@ interface WritableStreamErrorCallback {
interface WritableStream<W = any> { interface WritableStream<W = any> {
readonly locked: boolean; readonly locked: boolean;
abort(reason?: any): Promise<void>; abort(reason?: any): Promise<void>;
close(): Promise<void>;
getWriter(): WritableStreamDefaultWriter<W>; getWriter(): WritableStreamDefaultWriter<W>;
} }

206
runtime/js/40_spawn.js Normal file
View file

@ -0,0 +1,206 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
"use strict";
((window) => {
const core = window.Deno.core;
const { pathFromURL } = window.__bootstrap.util;
const { illegalConstructorKey } = window.__bootstrap.webUtil;
const {
ArrayPrototypeMap,
ObjectEntries,
String,
TypeError,
Uint8Array,
PromiseAll,
} = window.__bootstrap.primordials;
const { readableStreamForRid, writableStreamForRid } =
window.__bootstrap.streamUtils;
function spawnChild(command, {
args = [],
cwd = undefined,
clearEnv = false,
env = {},
uid = undefined,
gid = undefined,
stdin = "null",
stdout = "piped",
stderr = "piped",
} = {}) {
const child = core.opSync("op_spawn_child", {
cmd: pathFromURL(command),
args: ArrayPrototypeMap(args, String),
cwd: pathFromURL(cwd),
clearEnv,
env: ObjectEntries(env),
uid,
gid,
stdin,
stdout,
stderr,
});
return new Child(illegalConstructorKey, child);
}
async function collectOutput(readableStream) {
if (!(readableStream instanceof ReadableStream)) {
return null;
}
const bufs = [];
let size = 0;
for await (const chunk of readableStream) {
bufs.push(chunk);
size += chunk.byteLength;
}
const buffer = new Uint8Array(size);
let offset = 0;
for (const chunk of bufs) {
buffer.set(chunk, offset);
offset += chunk.byteLength;
}
return buffer;
}
class Child {
#rid;
#pid;
get pid() {
return this.#pid;
}
#stdinRid;
#stdin = null;
get stdin() {
return this.#stdin;
}
#stdoutRid;
#stdout = null;
get stdout() {
return this.#stdout;
}
#stderrRid;
#stderr = null;
get stderr() {
return this.#stderr;
}
constructor(key = null, {
rid,
pid,
stdinRid,
stdoutRid,
stderrRid,
} = null) {
if (key !== illegalConstructorKey) {
throw new TypeError("Illegal constructor.");
}
this.#rid = rid;
this.#pid = pid;
if (stdinRid !== null) {
this.#stdinRid = stdinRid;
this.#stdin = writableStreamForRid(stdinRid);
}
if (stdoutRid !== null) {
this.#stdoutRid = stdoutRid;
this.#stdout = readableStreamForRid(stdoutRid);
}
if (stderrRid !== null) {
this.#stderrRid = stderrRid;
this.#stderr = readableStreamForRid(stderrRid);
}
this.#status = core.opAsync("op_spawn_wait", this.#rid).then((res) => {
this.#rid = null;
return res;
});
}
#status;
get status() {
return this.#status;
}
async output() {
if (this.#rid === null) {
throw new TypeError("Child process has already terminated.");
}
if (this.#stdout?.locked) {
throw new TypeError(
"Can't collect output because stdout is locked",
);
}
if (this.#stderr?.locked) {
throw new TypeError(
"Can't collect output because stderr is locked",
);
}
const [status, stdout, stderr] = await PromiseAll([
this.#status,
collectOutput(this.#stdout),
collectOutput(this.#stderr),
]);
return {
status,
stdout,
stderr,
};
}
kill(signo) {
if (this.#rid === null) {
throw new TypeError("Child process has already terminated.");
}
core.opSync("op_kill", this.#pid, signo);
}
}
function spawn(command, options) { // TODO(@crowlKats): more options (like input)?
return spawnChild(command, {
...options,
stdin: "null",
stdout: "piped",
stderr: "piped",
}).output();
}
function spawnSync(command, {
args = [],
cwd = undefined,
clearEnv = false,
env = {},
uid = undefined,
gid = undefined,
} = {}) { // TODO(@crowlKats): more options (like input)?
return core.opSync("op_spawn_sync", {
cmd: pathFromURL(command),
args: ArrayPrototypeMap(args, String),
cwd: pathFromURL(cwd),
clearEnv,
env: ObjectEntries(env),
uid,
gid,
stdin: "null",
stdout: "piped",
stderr: "piped",
});
}
window.__bootstrap.spawn = {
Child,
spawnChild,
spawn,
spawnSync,
};
})(this);

View file

@ -151,5 +151,9 @@
funlockSync: __bootstrap.fs.funlockSync, funlockSync: __bootstrap.fs.funlockSync,
refTimer: __bootstrap.timers.refTimer, refTimer: __bootstrap.timers.refTimer,
unrefTimer: __bootstrap.timers.unrefTimer, unrefTimer: __bootstrap.timers.unrefTimer,
Child: __bootstrap.spawn.Child,
spawnChild: __bootstrap.spawn.spawnChild,
spawn: __bootstrap.spawn.spawn,
spawnSync: __bootstrap.spawn.spawnSync,
}; };
})(this); })(this);

View file

@ -134,6 +134,10 @@ where
stream.shutdown().await?; stream.shutdown().await?;
Ok(()) Ok(())
} }
pub fn into_inner(self) -> S {
self.stream.into_inner()
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -178,6 +182,10 @@ where
.await?; .await?;
Ok((nread, buf)) Ok((nread, buf))
} }
pub fn into_inner(self) -> S {
self.stream.into_inner()
}
} }
pub type ChildStdinResource = WriteOnlyResource<process::ChildStdin>; pub type ChildStdinResource = WriteOnlyResource<process::ChildStdin>;

View file

@ -9,6 +9,7 @@ pub mod permissions;
pub mod process; pub mod process;
pub mod runtime; pub mod runtime;
pub mod signal; pub mod signal;
pub mod spawn;
pub mod tty; pub mod tty;
mod utils; mod utils;
pub mod web_worker; pub mod web_worker;

263
runtime/ops/spawn.rs Normal file
View file

@ -0,0 +1,263 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use super::io::ChildStderrResource;
use super::io::ChildStdinResource;
use super::io::ChildStdoutResource;
use crate::permissions::Permissions;
use deno_core::error::AnyError;
use deno_core::op;
use deno_core::Extension;
use deno_core::OpState;
use deno_core::Resource;
use deno_core::ResourceId;
use deno_core::ZeroCopyBuf;
use serde::Deserialize;
use serde::Serialize;
use std::borrow::Cow;
use std::cell::RefCell;
use std::process::ExitStatus;
use std::rc::Rc;
#[cfg(unix)]
use std::os::unix::prelude::ExitStatusExt;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
pub fn init() -> Extension {
Extension::builder()
.ops(vec![
op_spawn_child::decl(),
op_spawn_wait::decl(),
op_spawn_sync::decl(),
])
.build()
}
struct ChildResource(tokio::process::Child);
impl Resource for ChildResource {
fn name(&self) -> Cow<str> {
"child".into()
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum Stdio {
Inherit,
Piped,
Null,
}
fn subprocess_stdio_map(s: &Stdio) -> Result<std::process::Stdio, AnyError> {
match s {
Stdio::Inherit => Ok(std::process::Stdio::inherit()),
Stdio::Piped => Ok(std::process::Stdio::piped()),
Stdio::Null => Ok(std::process::Stdio::null()),
}
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SpawnArgs {
cmd: String,
args: Vec<String>,
cwd: Option<String>,
clear_env: bool,
env: Vec<(String, String)>,
#[cfg(unix)]
gid: Option<u32>,
#[cfg(unix)]
uid: Option<u32>,
#[serde(flatten)]
stdio: ChildStdio,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChildStdio {
stdin: Stdio,
stdout: Stdio,
stderr: Stdio,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChildStatus {
success: bool,
code: i32,
signal: Option<i32>,
}
impl From<std::process::ExitStatus> for ChildStatus {
fn from(status: ExitStatus) -> Self {
let code = status.code();
#[cfg(unix)]
let signal = status.signal();
#[cfg(not(unix))]
let signal = None;
if let Some(signal) = signal {
ChildStatus {
success: false,
code: 128 + signal,
signal: Some(signal),
}
} else {
let code = code.expect("Should have either an exit code or a signal.");
ChildStatus {
success: code == 0,
code,
signal: None,
}
}
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpawnOutput {
status: ChildStatus,
stdout: Option<ZeroCopyBuf>,
stderr: Option<ZeroCopyBuf>,
}
fn create_command(
state: &mut OpState,
args: SpawnArgs,
) -> Result<std::process::Command, AnyError> {
super::check_unstable(state, "Deno.spawn");
state.borrow_mut::<Permissions>().run.check(&args.cmd)?;
let mut command = std::process::Command::new(args.cmd);
command.args(args.args);
if let Some(cwd) = args.cwd {
command.current_dir(cwd);
}
if args.clear_env {
command.env_clear();
}
command.envs(args.env);
#[cfg(unix)]
if let Some(gid) = args.gid {
super::check_unstable(state, "Deno.spawn.gid");
command.gid(gid);
}
#[cfg(unix)]
if let Some(uid) = args.uid {
super::check_unstable(state, "Deno.spawn.uid");
command.uid(uid);
}
#[cfg(unix)]
unsafe {
command.pre_exec(|| {
libc::setgroups(0, std::ptr::null());
Ok(())
});
}
command.stdin(subprocess_stdio_map(&args.stdio.stdin)?);
command.stdout(subprocess_stdio_map(&args.stdio.stdout)?);
command.stderr(subprocess_stdio_map(&args.stdio.stderr)?);
Ok(command)
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Child {
rid: ResourceId,
pid: u32,
stdin_rid: Option<ResourceId>,
stdout_rid: Option<ResourceId>,
stderr_rid: Option<ResourceId>,
}
#[op]
fn op_spawn_child(
state: &mut OpState,
args: SpawnArgs,
) -> Result<Child, AnyError> {
let mut command = tokio::process::Command::from(create_command(state, args)?);
// TODO(@crowlkats): allow detaching processes.
// currently deno will orphan a process when exiting with an error or Deno.exit()
// We want to kill child when it's closed
command.kill_on_drop(true);
let mut child = command.spawn()?;
let pid = child.id().expect("Process ID should be set.");
let stdin_rid = child
.stdin
.take()
.map(|stdin| state.resource_table.add(ChildStdinResource::from(stdin)));
let stdout_rid = child
.stdout
.take()
.map(|stdout| state.resource_table.add(ChildStdoutResource::from(stdout)));
let stderr_rid = child
.stderr
.take()
.map(|stderr| state.resource_table.add(ChildStderrResource::from(stderr)));
let child_rid = state.resource_table.add(ChildResource(child));
Ok(Child {
rid: child_rid,
pid,
stdin_rid,
stdout_rid,
stderr_rid,
})
}
#[op]
async fn op_spawn_wait(
state: Rc<RefCell<OpState>>,
rid: ResourceId,
) -> Result<ChildStatus, AnyError> {
let resource = state
.borrow_mut()
.resource_table
.take::<ChildResource>(rid)?;
Ok(
Rc::try_unwrap(resource)
.ok()
.unwrap()
.0
.wait()
.await?
.into(),
)
}
#[op]
fn op_spawn_sync(
state: &mut OpState,
args: SpawnArgs,
) -> Result<SpawnOutput, AnyError> {
let stdout = matches!(args.stdio.stdout, Stdio::Piped);
let stderr = matches!(args.stdio.stderr, Stdio::Piped);
let output = create_command(state, args)?.output()?;
Ok(SpawnOutput {
status: output.status.into(),
stdout: if stdout {
Some(output.stdout.into())
} else {
None
},
stderr: if stderr {
Some(output.stderr.into())
} else {
None
},
})
}

View file

@ -427,6 +427,7 @@ impl WebWorker {
.enabled(options.use_deno_namespace), .enabled(options.use_deno_namespace),
ops::permissions::init().enabled(options.use_deno_namespace), ops::permissions::init().enabled(options.use_deno_namespace),
ops::process::init().enabled(options.use_deno_namespace), ops::process::init().enabled(options.use_deno_namespace),
ops::spawn::init().enabled(options.use_deno_namespace),
ops::signal::init().enabled(options.use_deno_namespace), ops::signal::init().enabled(options.use_deno_namespace),
ops::tty::init().enabled(options.use_deno_namespace), ops::tty::init().enabled(options.use_deno_namespace),
deno_http::init().enabled(options.use_deno_namespace), deno_http::init().enabled(options.use_deno_namespace),

View file

@ -132,6 +132,7 @@ impl MainWorker {
options.create_web_worker_cb.clone(), options.create_web_worker_cb.clone(),
options.web_worker_preload_module_cb.clone(), options.web_worker_preload_module_cb.clone(),
), ),
ops::spawn::init(),
ops::fs_events::init(), ops::fs_events::init(),
ops::fs::init(), ops::fs::init(),
ops::io::init(), ops::io::init(),