summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLeo Kettmeir <crowlkats@toaxl.com>2022-04-21 00:20:33 +0200
committerGitHub <noreply@github.com>2022-04-21 00:20:33 +0200
commit8a7539cab36699465ec6e37455c54fa86f3c0cbe (patch)
treec3df15f3b673d1ec1a9c4ffada1a9274e3aca942
parent8b258070542a81d217226fe832b26d81cf20113d (diff)
feat(runtime): two-tier subprocess API (#11618)
-rw-r--r--.github/workflows/ci.yml6
-rw-r--r--cli/diagnostics.rs6
-rw-r--r--cli/dts/lib.deno.unstable.d.ts139
-rw-r--r--cli/tests/unit/command_test.ts687
-rw-r--r--cli/tests/unit/process_test.ts5
-rw-r--r--ext/web/lib.deno_web.d.ts1
-rw-r--r--runtime/js/40_spawn.js206
-rw-r--r--runtime/js/90_deno_ns.js4
-rw-r--r--runtime/ops/io.rs8
-rw-r--r--runtime/ops/mod.rs1
-rw-r--r--runtime/ops/spawn.rs263
-rw-r--r--runtime/web_worker.rs1
-rw-r--r--runtime/worker.rs1
13 files changed, 1323 insertions, 5 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1394be10a..3612b3a3e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -236,7 +236,7 @@ jobs:
~/.cargo/registry/index
~/.cargo/registry/cache
~/.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
- name: Cache build output (main)
@@ -252,7 +252,7 @@ jobs:
!./target/*/*.zip
!./target/*/*.tar.gz
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.
- name: Cache build output (PR)
@@ -268,7 +268,7 @@ jobs:
!./target/*/*.tar.gz
key: never_saved
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'.
- name: Skip save cache (PR)
diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs
index 8181c5fa0..24d7ab0e7 100644
--- a/cli/diagnostics.rs
+++ b/cli/diagnostics.rs
@@ -66,6 +66,12 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
"umask",
"utime",
"utimeSync",
+ "spawnChild",
+ "Child",
+ "spawn",
+ "spawnSync",
+ "ChildStatus",
+ "SpawnOutput",
];
static MSG_MISSING_PROPERTY_DENO: Lazy<Regex> = Lazy::new(|| {
diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts
index 0ad070469..6d5ad3af3 100644
--- a/cli/dts/lib.deno.unstable.d.ts
+++ b/cli/dts/lib.deno.unstable.d.ts
@@ -1361,6 +1361,145 @@ declare namespace Deno {
export function upgradeHttp(
request: Request,
): 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 process’s 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(
diff --git a/cli/tests/unit/command_test.ts b/cli/tests/unit/command_test.ts
new file mode 100644
index 000000000..f7213f034
--- /dev/null
+++ b/cli/tests/unit/command_test.ts
@@ -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);
+ }
+ },
+);
diff --git a/cli/tests/unit/process_test.ts b/cli/tests/unit/process_test.ts
index e4e2cc3c5..5acb92226 100644
--- a/cli/tests/unit/process_test.ts
+++ b/cli/tests/unit/process_test.ts
@@ -557,8 +557,9 @@ Deno.test(
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
- // so we check if PATH isn't present as that is a common env var across OS's and isn't set for processes.
+ // 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));
diff --git a/ext/web/lib.deno_web.d.ts b/ext/web/lib.deno_web.d.ts
index 8c845ced1..13ad113fe 100644
--- a/ext/web/lib.deno_web.d.ts
+++ b/ext/web/lib.deno_web.d.ts
@@ -630,6 +630,7 @@ interface WritableStreamErrorCallback {
interface WritableStream<W = any> {
readonly locked: boolean;
abort(reason?: any): Promise<void>;
+ close(): Promise<void>;
getWriter(): WritableStreamDefaultWriter<W>;
}
diff --git a/runtime/js/40_spawn.js b/runtime/js/40_spawn.js
new file mode 100644
index 000000000..c55ce657d
--- /dev/null
+++ b/runtime/js/40_spawn.js
@@ -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);
diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js
index ddaecd7c9..61e894f8a 100644
--- a/runtime/js/90_deno_ns.js
+++ b/runtime/js/90_deno_ns.js
@@ -151,5 +151,9 @@
funlockSync: __bootstrap.fs.funlockSync,
refTimer: __bootstrap.timers.refTimer,
unrefTimer: __bootstrap.timers.unrefTimer,
+ Child: __bootstrap.spawn.Child,
+ spawnChild: __bootstrap.spawn.spawnChild,
+ spawn: __bootstrap.spawn.spawn,
+ spawnSync: __bootstrap.spawn.spawnSync,
};
})(this);
diff --git a/runtime/ops/io.rs b/runtime/ops/io.rs
index b8449af86..34cd541d5 100644
--- a/runtime/ops/io.rs
+++ b/runtime/ops/io.rs
@@ -134,6 +134,10 @@ where
stream.shutdown().await?;
Ok(())
}
+
+ pub fn into_inner(self) -> S {
+ self.stream.into_inner()
+ }
}
#[derive(Debug)]
@@ -178,6 +182,10 @@ where
.await?;
Ok((nread, buf))
}
+
+ pub fn into_inner(self) -> S {
+ self.stream.into_inner()
+ }
}
pub type ChildStdinResource = WriteOnlyResource<process::ChildStdin>;
diff --git a/runtime/ops/mod.rs b/runtime/ops/mod.rs
index 750dfe0f2..526c36d63 100644
--- a/runtime/ops/mod.rs
+++ b/runtime/ops/mod.rs
@@ -9,6 +9,7 @@ pub mod permissions;
pub mod process;
pub mod runtime;
pub mod signal;
+pub mod spawn;
pub mod tty;
mod utils;
pub mod web_worker;
diff --git a/runtime/ops/spawn.rs b/runtime/ops/spawn.rs
new file mode 100644
index 000000000..196a7eed6
--- /dev/null
+++ b/runtime/ops/spawn.rs
@@ -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
+ },
+ })
+}
diff --git a/runtime/web_worker.rs b/runtime/web_worker.rs
index f4c040aa4..ac103adda 100644
--- a/runtime/web_worker.rs
+++ b/runtime/web_worker.rs
@@ -427,6 +427,7 @@ impl WebWorker {
.enabled(options.use_deno_namespace),
ops::permissions::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::tty::init().enabled(options.use_deno_namespace),
deno_http::init().enabled(options.use_deno_namespace),
diff --git a/runtime/worker.rs b/runtime/worker.rs
index fa147a7e6..370475703 100644
--- a/runtime/worker.rs
+++ b/runtime/worker.rs
@@ -132,6 +132,7 @@ impl MainWorker {
options.create_web_worker_cb.clone(),
options.web_worker_preload_module_cb.clone(),
),
+ ops::spawn::init(),
ops::fs_events::init(),
ops::fs::init(),
ops::io::init(),