summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/diagnostics.rs6
-rw-r--r--cli/dts/lib.deno.unstable.d.ts209
-rw-r--r--cli/tests/unit/command_test.ts844
-rw-r--r--runtime/js/40_spawn.js128
-rw-r--r--runtime/js/90_deno_ns.js1
5 files changed, 1186 insertions, 2 deletions
diff --git a/cli/diagnostics.rs b/cli/diagnostics.rs
index 69ff8ae25..05502dca4 100644
--- a/cli/diagnostics.rs
+++ b/cli/diagnostics.rs
@@ -33,8 +33,14 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
"Child",
"spawn",
"spawnSync",
+ "SpawnOptions",
"ChildStatus",
"SpawnOutput",
+ "command",
+ "Command",
+ "CommandOptions",
+ "CommandStatus",
+ "CommandOutput",
"serve",
"ServeInit",
"ServeTlsInit",
diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts
index f19356393..12a02d457 100644
--- a/cli/dts/lib.deno.unstable.d.ts
+++ b/cli/dts/lib.deno.unstable.d.ts
@@ -1396,6 +1396,8 @@ declare namespace Deno {
/** **UNSTABLE**: New API, yet to be vetted.
*
+ * @deprecated Use the Deno.Command API instead.
+ *
* Options which can be set when calling {@linkcode Deno.spawn},
* {@linkcode Deno.spawnSync}, and {@linkcode Deno.spawnChild}.
*
@@ -1455,6 +1457,8 @@ declare namespace Deno {
/** **UNSTABLE**: New API, yet to be vetted.
*
+ * @deprecated Use the Deno.Command API instead.
+ *
* Spawns a child process.
*
* If any stdio options are not set to `"piped"`, accessing the corresponding
@@ -1489,6 +1493,8 @@ declare namespace Deno {
/** **UNSTABLE**: New API, yet to be vetted.
*
+ * @deprecated Use the Deno.Command API instead.
+ *
* The interface for handling a child process returned from
* {@linkcode Deno.spawnChild}.
*
@@ -1519,6 +1525,8 @@ declare namespace Deno {
/** **UNSTABLE**: New API, yet to be vetted.
*
+ * @deprecated Use the Deno.Command API instead.
+ *
* Executes a subprocess, waiting for it to finish and collecting all of its
* output.
*
@@ -1531,7 +1539,7 @@ declare namespace Deno {
* const { code, stdout, stderr } = await Deno.spawn(Deno.execPath(), {
* args: [
* "eval",
- * "console.log('hello'); console.error('world')",
+ * "console.log('hello'); console.error('world')",
* ],
* });
* console.assert(code === 0);
@@ -1548,6 +1556,8 @@ declare namespace Deno {
/** **UNSTABLE**: New API, yet to be vetted.
*
+ * @deprecated Use the Deno.Command API instead.
+ *
* Synchronously executes a subprocess, waiting for it to finish and
* collecting all of its output.
*
@@ -1560,7 +1570,7 @@ declare namespace Deno {
* const { code, stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
* args: [
* "eval",
- * "console.log('hello'); console.error('world')",
+ * "console.log('hello'); console.error('world')",
* ],
* });
* console.assert(code === 0);
@@ -1577,6 +1587,8 @@ declare namespace Deno {
/** **UNSTABLE**: New API, yet to be vetted.
*
+ * @deprecated Use the Deno.Command API instead.
+ *
* @category Sub Process
*/
export interface ChildStatus {
@@ -1592,6 +1604,8 @@ declare namespace Deno {
/** **UNSTABLE**: New API, yet to be vetted.
*
+ * @deprecated Use the Deno.Command API instead.
+ *
* The interface returned from calling {@linkcode Deno.spawn} or
* {@linkcode Deno.spawnSync} which represents the result of spawning the
* child process.
@@ -1604,6 +1618,197 @@ declare namespace Deno {
/** The buffered output from the child processes `stderr`. */
readonly stderr: Uint8Array;
}
+
+ /** **UNSTABLE**: New API, yet to be vetted.
+ *
+ * Create a child process.
+ *
+ * If any stdio options are not set to `"piped"`, accessing the corresponding
+ * field on the `Command` or its `CommandOutput` will throw a `TypeError`.
+ *
+ * If `stdin` is set to `"piped"`, the `stdin` {@linkcode WritableStream}
+ * needs to be closed manually.
+ *
+ * ```ts
+ * const command = new Deno.Command(Deno.execPath(), {
+ * args: [
+ * "eval",
+ * "console.log('Hello World')",
+ * ],
+ * stdin: "piped",
+ * });
+ * command.spawn();
+ *
+ * // open a file and pipe the subprocess output to it.
+ * command.stdout.pipeTo(Deno.openSync("output").writable);
+ *
+ * // manually close stdin
+ * command.stdin.close();
+ * const status = await command.status;
+ * ```
+ *
+ * ```ts
+ * const command = new Deno.Command(Deno.execPath(), {
+ * args: [
+ * "eval",
+ * "console.log('hello'); console.error('world')",
+ * ],
+ * });
+ * const { code, stdout, stderr } = await command.output();
+ * console.assert(code === 0);
+ * console.assert("hello\n" === new TextDecoder().decode(stdout));
+ * console.assert("world\n" === new TextDecoder().decode(stderr));
+ * ```
+ *
+ * ```ts
+ * const command = new Deno.Command(Deno.execPath(), {
+ * args: [
+ * "eval",
+ * "console.log('hello'); console.error('world')",
+ * ],
+ * });
+ * const { code, stdout, stderr } = command.outputSync();
+ * console.assert(code === 0);
+ * console.assert("hello\n" === new TextDecoder().decode(stdout));
+ * console.assert("world\n" === new TextDecoder().decode(stderr));
+ * ```
+ *
+ * @category Sub Process
+ */
+ export class Command {
+ get stdin(): WritableStream<Uint8Array>;
+ get stdout(): ReadableStream<Uint8Array>;
+ get stderr(): ReadableStream<Uint8Array>;
+ readonly pid: number;
+ /** Get the status of the child process. */
+ readonly status: Promise<CommandStatus>;
+
+ constructor(command: string | URL, options?: CommandOptions);
+ /**
+ * Executes the {@linkcode Deno.Command}, waiting for it to finish and
+ * collecting all of its output.
+ * If `spawn()` was called, calling this function will collect the remaining
+ * output.
+ *
+ * Will throw an error if `stdin: "piped"` is set.
+ *
+ * If options `stdout` or `stderr` are not set to `"piped"`, accessing the
+ * corresponding field on {@linkcode Deno.CommandOutput} will throw a `TypeError`.
+ */
+ output(): Promise<CommandOutput>;
+ /**
+ * Synchronously executes the {@linkcode Deno.Command}, waiting for it to
+ * finish and collecting all of its output.
+ *
+ * Will throw an error if `stdin: "piped"` is set.
+ *
+ * If options `stdout` or `stderr` are not set to `"piped"`, accessing the
+ * corresponding field on {@linkcode Deno.CommandOutput} will throw a `TypeError`.
+ */
+ outputSync(): CommandOutput;
+ /**
+ * Spawns a streamable subprocess, allowing to use the other methods.
+ */
+ spawn(): void;
+
+ /** Kills the process with given {@linkcode Deno.Signal}. Defaults to
+ * `"SIGTERM"`. */
+ kill(signo?: Signal): void;
+
+ /** Ensure that the status of the child process prevents the Deno process
+ * from exiting. */
+ ref(): void;
+ /** Ensure that the status of the child process does not block the Deno
+ * process from exiting. */
+ unref(): void;
+ }
+
+ /** **UNSTABLE**: New API, yet to be vetted.
+ *
+ * Options which can be set when calling {@linkcode Deno.command}.
+ *
+ * @category Sub Process
+ */
+ export interface CommandOptions {
+ /** 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 `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 set uid call will cause the spawn to fail.
+ */
+ uid?: number;
+ /** Similar to `uid`, but sets the group ID of the child process. */
+ gid?: number;
+ /**
+ * An {@linkcode AbortSignal} that allows closing the process using the
+ * corresponding {@linkcode AbortController} by sending the process a
+ * SIGTERM signal.
+ *
+ * Ignored by {@linkcode Command.outputSync}.
+ */
+ signal?: AbortSignal;
+
+ /** How `stdin` of the spawned process should be handled.
+ *
+ * Defaults to `"null"`. */
+ stdin?: "piped" | "inherit" | "null";
+ /** How `stdout` of the spawned process should be handled.
+ *
+ * Defaults to `"piped"`. */
+ stdout?: "piped" | "inherit" | "null";
+ /** How `stderr` of the spawned process should be handled.
+ *
+ * Defaults to "piped". */
+ stderr?: "piped" | "inherit" | "null";
+
+ /** Skips quoting and escaping of the arguments on Windows. This option
+ * is ignored on non-windows platforms. Defaults to `false`. */
+ windowsRawArguments?: boolean;
+ }
+
+ /** **UNSTABLE**: New API, yet to be vetted.
+ *
+ * @category Sub Process
+ */
+ export interface CommandStatus {
+ /** If the child process exits with a 0 status code, `success` will be set
+ * to `true`, otherwise `false`. */
+ success: boolean;
+ /** The exit code of the child process. */
+ code: number;
+ /** The signal associated with the child process. */
+ signal: Signal | null;
+ }
+
+ /** **UNSTABLE**: New API, yet to be vetted.
+ *
+ * The interface returned from calling {@linkcode Command.output} or
+ * {@linkcode Command.outputSync} which represents the result of spawning the
+ * child process.
+ *
+ * @category Sub Process
+ */
+ export interface CommandOutput extends ChildStatus {
+ /** The buffered output from the child process' `stdout`. */
+ readonly stdout: Uint8Array;
+ /** The buffered output from the child process' `stderr`. */
+ readonly stderr: Uint8Array;
+ }
}
/** **UNSTABLE**: New API, yet to be vetted.
diff --git a/cli/tests/unit/command_test.ts b/cli/tests/unit/command_test.ts
new file mode 100644
index 000000000..e49cdc46c
--- /dev/null
+++ b/cli/tests/unit/command_test.ts
@@ -0,0 +1,844 @@
+// 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 commandWithCwdIsAsync() {
+ 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 = new Deno.Command(Deno.execPath(), {
+ cwd,
+ args: ["run", "--allow-read", programFile],
+ stdout: "inherit",
+ stderr: "inherit",
+ });
+ child.spawn();
+
+ // 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 commandStdinPiped() {
+ const child = new Deno.Command(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",
+ });
+ child.spawn();
+
+ assertThrows(() => child.stdout, TypeError, "stdout is not piped");
+ assertThrows(() => child.stderr, TypeError, "stderr is not piped");
+
+ 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 commandStdoutPiped() {
+ const child = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stdout.write(new TextEncoder().encode('hello'))",
+ ],
+ stderr: "null",
+ });
+ child.spawn();
+
+ assertThrows(() => child.stdin, TypeError, "stdin is not piped");
+ assertThrows(() => child.stderr, TypeError, "stderr is not piped");
+
+ 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 commandStderrPiped() {
+ const child = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stderr.write(new TextEncoder().encode('hello'))",
+ ],
+ stdout: "null",
+ });
+ child.spawn();
+
+ assertThrows(() => child.stdin, TypeError, "stdin is not piped");
+ assertThrows(() => child.stdout, TypeError, "stdout is not piped");
+
+ 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 commandRedirectStdoutStderr() {
+ const tempDir = await Deno.makeTempDir();
+ const fileName = tempDir + "/redirected_stdio.txt";
+ const file = await Deno.open(fileName, {
+ create: true,
+ write: true,
+ });
+
+ const child = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "Deno.stderr.write(new TextEncoder().encode('error\\n')); Deno.stdout.write(new TextEncoder().encode('output\\n'));",
+ ],
+ });
+ child.spawn();
+ 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 commandRedirectStdin() {
+ 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 = new Deno.Command(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",
+ });
+ child.spawn();
+ 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 commandKillSuccess() {
+ const child = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "setTimeout(() => {}, 10000)"],
+ stdout: "null",
+ stderr: "null",
+ });
+ child.spawn();
+
+ 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, "SIGKILL");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandKillFailed() {
+ const child = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "setTimeout(() => {}, 5000)"],
+ stdout: "null",
+ stderr: "null",
+ });
+ child.spawn();
+
+ assertThrows(() => {
+ // @ts-expect-error testing runtime error of bad signal
+ child.kill("foobar");
+ }, TypeError);
+
+ await child.status;
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandKillOptional() {
+ const child = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "setTimeout(() => {}, 10000)"],
+ stdout: "null",
+ stderr: "null",
+ });
+ child.spawn();
+
+ child.kill();
+ 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, 143);
+ assertEquals(status.signal, "SIGTERM");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandAbort() {
+ const ac = new AbortController();
+ const child = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "setTimeout(console.log, 1e8)",
+ ],
+ signal: ac.signal,
+ stdout: "null",
+ stderr: "null",
+ });
+ child.spawn();
+ queueMicrotask(() => ac.abort());
+ 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.success, false);
+ assertEquals(status.code, 143);
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: false } },
+ async function commandPermissions() {
+ await assertRejects(async () => {
+ await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).output();
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: false } },
+ function commandSyncPermissions() {
+ assertThrows(() => {
+ new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).outputSync();
+ }, Deno.errors.PermissionDenied);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandSuccess() {
+ const output = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).output();
+
+ assertEquals(output.success, true);
+ assertEquals(output.code, 0);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncSuccess() {
+ const output = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).outputSync();
+
+ assertEquals(output.success, true);
+ assertEquals(output.code, 0);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandUrl() {
+ const output = await new Deno.Command(
+ new URL(`file:///${Deno.execPath()}`),
+ {
+ args: ["eval", "console.log('hello world')"],
+ },
+ ).output();
+
+ assertEquals(new TextDecoder().decode(output.stdout), "hello world\n");
+
+ assertEquals(output.success, true);
+ assertEquals(output.code, 0);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncUrl() {
+ const output = new Deno.Command(
+ new URL(`file:///${Deno.execPath()}`),
+ {
+ args: ["eval", "console.log('hello world')"],
+ },
+ ).outputSync();
+
+ assertEquals(new TextDecoder().decode(output.stdout), "hello world\n");
+
+ assertEquals(output.success, true);
+ assertEquals(output.code, 0);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test({ permissions: { run: true } }, function commandNotFound() {
+ assertThrows(
+ () => new Deno.Command("this file hopefully doesn't exist").output(),
+ Deno.errors.NotFound,
+ );
+});
+
+Deno.test({ permissions: { run: true } }, function commandSyncNotFound() {
+ assertThrows(
+ () => new Deno.Command("this file hopefully doesn't exist").outputSync(),
+ Deno.errors.NotFound,
+ );
+});
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandFailedWithCode() {
+ const output = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "Deno.exit(41 + 1)"],
+ }).output();
+ assertEquals(output.success, false);
+ assertEquals(output.code, 42);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncFailedWithCode() {
+ const output = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "Deno.exit(41 + 1)"],
+ }).outputSync();
+ assertEquals(output.success, false);
+ assertEquals(output.code, 42);
+ assertEquals(output.signal, null);
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ },
+ async function commandFailedWithSignal() {
+ const output = await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "--unstable", "Deno.kill(Deno.pid, 'SIGKILL')"],
+ }).output();
+ assertEquals(output.success, false);
+ if (Deno.build.os === "windows") {
+ assertEquals(output.code, 1);
+ assertEquals(output.signal, null);
+ } else {
+ assertEquals(output.code, 128 + 9);
+ assertEquals(output.signal, "SIGKILL");
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ },
+ function commandSyncFailedWithSignal() {
+ const output = new Deno.Command(Deno.execPath(), {
+ args: ["eval", "--unstable", "Deno.kill(Deno.pid, 'SIGKILL')"],
+ }).outputSync();
+ assertEquals(output.success, false);
+ if (Deno.build.os === "windows") {
+ assertEquals(output.code, 1);
+ assertEquals(output.signal, null);
+ } else {
+ assertEquals(output.code, 128 + 9);
+ assertEquals(output.signal, "SIGKILL");
+ }
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandOutput() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stdout.write(new TextEncoder().encode('hello'))",
+ ],
+ }).output();
+
+ const s = new TextDecoder().decode(stdout);
+ assertEquals(s, "hello");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncOutput() {
+ const { stdout } = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stdout.write(new TextEncoder().encode('hello'))",
+ ],
+ }).outputSync();
+
+ const s = new TextDecoder().decode(stdout);
+ assertEquals(s, "hello");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandStderrOutput() {
+ const { stderr } = await new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stderr.write(new TextEncoder().encode('error'))",
+ ],
+ }).output();
+
+ const s = new TextDecoder().decode(stderr);
+ assertEquals(s, "error");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncStderrOutput() {
+ const { stderr } = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "await Deno.stderr.write(new TextEncoder().encode('error'))",
+ ],
+ }).outputSync();
+
+ const s = new TextDecoder().decode(stderr);
+ assertEquals(s, "error");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ async function commandEnv() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
+ ],
+ env: {
+ FOO: "0123",
+ BAR: "4567",
+ },
+ }).output();
+ const s = new TextDecoder().decode(stdout);
+ assertEquals(s, "01234567");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true } },
+ function commandSyncEnv() {
+ const { stdout } = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
+ ],
+ env: {
+ FOO: "0123",
+ BAR: "4567",
+ },
+ }).outputSync();
+ const s = new TextDecoder().decode(stdout);
+ assertEquals(s, "01234567");
+ },
+);
+
+Deno.test(
+ { permissions: { run: true, read: true, env: true } },
+ async function commandClearEnv() {
+ const { stdout } = await new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "-p",
+ "JSON.stringify(Deno.env.toObject())",
+ ],
+ clearEnv: true,
+ env: {
+ FOO: "23147",
+ },
+ }).output();
+
+ 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 commandSyncClearEnv() {
+ const { stdout } = new Deno.Command(Deno.execPath(), {
+ args: [
+ "eval",
+ "-p",
+ "JSON.stringify(Deno.env.toObject())",
+ ],
+ clearEnv: true,
+ env: {
+ FOO: "23147",
+ },
+ }).outputSync();
+
+ 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 commandUid() {
+ const { stdout } = await new Deno.Command("id", {
+ args: ["-u"],
+ }).output();
+
+ const currentUid = new TextDecoder().decode(stdout);
+
+ if (currentUid !== "0") {
+ await assertRejects(async () => {
+ await new Deno.Command("echo", {
+ args: ["fhqwhgads"],
+ uid: 0,
+ }).output();
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ ignore: Deno.build.os === "windows",
+ },
+ function commandSyncUid() {
+ const { stdout } = new Deno.Command("id", {
+ args: ["-u"],
+ }).outputSync();
+
+ const currentUid = new TextDecoder().decode(stdout);
+
+ if (currentUid !== "0") {
+ assertThrows(() => {
+ new Deno.Command("echo", {
+ args: ["fhqwhgads"],
+ uid: 0,
+ }).outputSync();
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ ignore: Deno.build.os === "windows",
+ },
+ async function commandGid() {
+ const { stdout } = await new Deno.Command("id", {
+ args: ["-g"],
+ }).output();
+
+ const currentGid = new TextDecoder().decode(stdout);
+
+ if (currentGid !== "0") {
+ await assertRejects(async () => {
+ await new Deno.Command("echo", {
+ args: ["fhqwhgads"],
+ gid: 0,
+ }).output();
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(
+ {
+ permissions: { run: true, read: true },
+ ignore: Deno.build.os === "windows",
+ },
+ function commandSyncGid() {
+ const { stdout } = new Deno.Command("id", {
+ args: ["-g"],
+ }).outputSync();
+
+ const currentGid = new TextDecoder().decode(stdout);
+
+ if (currentGid !== "0") {
+ assertThrows(() => {
+ new Deno.Command("echo", {
+ args: ["fhqwhgads"],
+ gid: 0,
+ }).outputSync();
+ }, Deno.errors.PermissionDenied);
+ }
+ },
+);
+
+Deno.test(function commandStdinPipedFails() {
+ assertThrows(
+ () =>
+ new Deno.Command("id", {
+ stdin: "piped",
+ }).output(),
+ TypeError,
+ "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
+ );
+});
+
+Deno.test(function spawnSyncStdinPipedFails() {
+ assertThrows(
+ () =>
+ new Deno.Command("id", {
+ stdin: "piped",
+ }).outputSync(),
+ TypeError,
+ "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
+ );
+});
+
+Deno.test(
+ // TODO(bartlomieju): this test became flaky on Windows CI
+ // raising "PermissionDenied" instead of "NotFound".
+ {
+ ignore: Deno.build.os === "windows",
+ permissions: { write: true, run: true, read: true },
+ },
+ async function commandChildUnref() {
+ const enc = new TextEncoder();
+ const cwd = await Deno.makeTempDir({ prefix: "deno_command_test" });
+
+ const programFile = "unref.ts";
+ const program = `
+const child = await new Deno.Command(Deno.execPath(), {
+ cwd: Deno.args[0],
+ stdout: "piped",
+ args: ["run", "-A", "--unstable", Deno.args[1]],
+});child.spawn();
+const readable = child.stdout.pipeThrough(new TextDecoderStream());
+const reader = readable.getReader();
+// set up an interval that will end after reading a few messages from stdout,
+// to verify that stdio streams are properly unrefed
+let count = 0;
+let interval;
+interval = setInterval(async () => {
+ count += 1;
+ if (count > 10) {
+ clearInterval(interval);
+ console.log("cleared interval");
+ }
+ const res = await reader.read();
+ if (res.done) {
+ throw new Error("stream shouldn't be done");
+ }
+ if (res.value.trim() != "hello from interval") {
+ throw new Error("invalid message received");
+ }
+}, 120);
+console.log("spawned pid", child.pid);
+child.unref();
+`;
+
+ const childProgramFile = "unref_child.ts";
+ const childProgram = `
+setInterval(() => {
+ console.log("hello from interval");
+}, 100);
+`;
+ Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program));
+ Deno.writeFileSync(`${cwd}/${childProgramFile}`, enc.encode(childProgram));
+ // In this subprocess we are spawning another subprocess which has
+ // an infite interval set. Following call would never resolve unless
+ // child process gets unrefed.
+ const { success, stdout, stderr } = await new Deno.Command(
+ Deno.execPath(),
+ {
+ cwd,
+ args: ["run", "-A", "--unstable", programFile, cwd, childProgramFile],
+ },
+ ).output();
+
+ assert(success);
+ const stdoutText = new TextDecoder().decode(stdout);
+ const stderrText = new TextDecoder().decode(stderr);
+ assert(stderrText.length == 0);
+ const [line1, line2] = stdoutText.split("\n");
+ const pidStr = line1.split(" ").at(-1);
+ assert(pidStr);
+ assertEquals(line2, "cleared interval");
+ const pid = Number.parseInt(pidStr, 10);
+ await Deno.remove(cwd, { recursive: true });
+ // Child process should have been killed when parent process exits.
+ assertThrows(() => {
+ Deno.kill(pid, "SIGTERM");
+ }, Deno.errors.NotFound);
+ },
+);
+
+Deno.test(
+ { ignore: Deno.build.os !== "windows" },
+ async function commandWindowsRawArguments() {
+ let { success, stdout } = await new Deno.Command("cmd", {
+ args: ["/d", "/s", "/c", '"deno ^"--version^""'],
+ windowsRawArguments: true,
+ }).output();
+ assert(success);
+ let stdoutText = new TextDecoder().decode(stdout);
+ assertStringIncludes(stdoutText, "deno");
+ assertStringIncludes(stdoutText, "v8");
+ assertStringIncludes(stdoutText, "typescript");
+
+ ({ success, stdout } = new Deno.Command("cmd", {
+ args: ["/d", "/s", "/c", '"deno ^"--version^""'],
+ windowsRawArguments: true,
+ }).outputSync());
+ assert(success);
+ stdoutText = new TextDecoder().decode(stdout);
+ assertStringIncludes(stdoutText, "deno");
+ assertStringIncludes(stdoutText, "v8");
+ assertStringIncludes(stdoutText, "typescript");
+ },
+);
+
+Deno.test(
+ { permissions: { read: true, run: true } },
+ async function commandWithPromisePrototypeThenOverride() {
+ const originalThen = Promise.prototype.then;
+ try {
+ Promise.prototype.then = () => {
+ throw new Error();
+ };
+ await new Deno.Command(Deno.execPath(), {
+ args: ["eval", "console.log('hello world')"],
+ }).output();
+ } finally {
+ Promise.prototype.then = originalThen;
+ }
+ },
+);
diff --git a/runtime/js/40_spawn.js b/runtime/js/40_spawn.js
index 4d2fb1607..e262a1325 100644
--- a/runtime/js/40_spawn.js
+++ b/runtime/js/40_spawn.js
@@ -291,8 +291,136 @@
};
}
+ class Command {
+ #command;
+ #options;
+
+ #child;
+
+ #consumed;
+
+ constructor(command, options) {
+ this.#command = command;
+ this.#options = options;
+ }
+
+ output() {
+ if (this.#child) {
+ return this.#child.output();
+ } else {
+ if (this.#consumed) {
+ throw new TypeError(
+ "Command instance is being or has already been consumed.",
+ );
+ }
+ if (this.#options?.stdin === "piped") {
+ throw new TypeError(
+ "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
+ );
+ }
+
+ this.#consumed = true;
+ return Deno.spawn(this.#command, this.#options);
+ }
+ }
+
+ outputSync() {
+ if (this.#consumed) {
+ throw new TypeError(
+ "Command instance is being or has already been consumed.",
+ );
+ }
+ if (this.#child) {
+ throw new TypeError("Was spawned");
+ }
+ if (this.#options?.stdin === "piped") {
+ throw new TypeError(
+ "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
+ );
+ }
+
+ this.#consumed = true;
+ return Deno.spawnSync(this.#command, this.#options);
+ }
+
+ spawn() {
+ if (this.#consumed) {
+ throw new TypeError(
+ "Command instance is being or has already been consumed.",
+ );
+ }
+
+ this.#consumed = true;
+ this.#child = Deno.spawnChild(this.#command, this.#options);
+ }
+
+ get stdin() {
+ if (!this.#child) {
+ throw new TypeError("Wasn't spawned");
+ }
+
+ return this.#child.stdin;
+ }
+
+ get stdout() {
+ if (!this.#child) {
+ throw new TypeError("Wasn't spawned");
+ }
+
+ return this.#child.stdout;
+ }
+
+ get stderr() {
+ if (!this.#child) {
+ throw new TypeError("Wasn't spawned");
+ }
+
+ return this.#child.stderr;
+ }
+
+ get status() {
+ if (!this.#child) {
+ throw new TypeError("Wasn't spawned");
+ }
+
+ return this.#child.status;
+ }
+
+ get pid() {
+ if (!this.#child) {
+ throw new TypeError("Wasn't spawned");
+ }
+
+ return this.#child.pid;
+ }
+
+ kill(signo = "SIGTERM") {
+ if (!this.#child) {
+ throw new TypeError("Wasn't spawned");
+ }
+ this.#child.kill(signo);
+ }
+
+ ref() {
+ if (!this.#child) {
+ throw new TypeError("Wasn't spawned");
+ }
+
+ this.#child.ref();
+ }
+
+ unref() {
+ if (!this.#child) {
+ throw new TypeError("Wasn't spawned");
+ }
+
+ this.#child.unref();
+ }
+ }
+
window.__bootstrap.spawn = {
Child,
+ Command,
createSpawn,
createSpawnChild,
createSpawnSync,
diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js
index 033ad421e..16dd5c72f 100644
--- a/runtime/js/90_deno_ns.js
+++ b/runtime/js/90_deno_ns.js
@@ -149,6 +149,7 @@
spawnChild: __bootstrap.spawn.spawnChild,
spawn: __bootstrap.spawn.spawn,
spawnSync: __bootstrap.spawn.spawnSync,
+ Command: __bootstrap.spawn.Command,
serve: __bootstrap.flash.serve,
upgradeHttp: __bootstrap.http.upgradeHttp,
upgradeHttpRaw: __bootstrap.flash.upgradeHttpRaw,