mirror of
https://github.com/denoland/deno.git
synced 2024-12-24 08:09:08 -05:00
feat(unstable): "Deno.Command()" API (#16516)
Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
parent
15db936348
commit
52dc3ef1a4
5 changed files with 1186 additions and 2 deletions
|
@ -33,8 +33,14 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
|
||||||
"Child",
|
"Child",
|
||||||
"spawn",
|
"spawn",
|
||||||
"spawnSync",
|
"spawnSync",
|
||||||
|
"SpawnOptions",
|
||||||
"ChildStatus",
|
"ChildStatus",
|
||||||
"SpawnOutput",
|
"SpawnOutput",
|
||||||
|
"command",
|
||||||
|
"Command",
|
||||||
|
"CommandOptions",
|
||||||
|
"CommandStatus",
|
||||||
|
"CommandOutput",
|
||||||
"serve",
|
"serve",
|
||||||
"ServeInit",
|
"ServeInit",
|
||||||
"ServeTlsInit",
|
"ServeTlsInit",
|
||||||
|
|
209
cli/dts/lib.deno.unstable.d.ts
vendored
209
cli/dts/lib.deno.unstable.d.ts
vendored
|
@ -1395,6 +1395,8 @@ declare namespace Deno {
|
||||||
export function upgradeHttpRaw(request: Request): [Deno.Conn, Uint8Array];
|
export function upgradeHttpRaw(request: Request): [Deno.Conn, Uint8Array];
|
||||||
|
|
||||||
/** **UNSTABLE**: New API, yet to be vetted.
|
/** **UNSTABLE**: New API, yet to be vetted.
|
||||||
|
*
|
||||||
|
* @deprecated Use the Deno.Command API instead.
|
||||||
*
|
*
|
||||||
* Options which can be set when calling {@linkcode Deno.spawn},
|
* Options which can be set when calling {@linkcode Deno.spawn},
|
||||||
* {@linkcode Deno.spawnSync}, and {@linkcode Deno.spawnChild}.
|
* {@linkcode Deno.spawnSync}, and {@linkcode Deno.spawnChild}.
|
||||||
|
@ -1454,6 +1456,8 @@ declare namespace Deno {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** **UNSTABLE**: New API, yet to be vetted.
|
/** **UNSTABLE**: New API, yet to be vetted.
|
||||||
|
*
|
||||||
|
* @deprecated Use the Deno.Command API instead.
|
||||||
*
|
*
|
||||||
* Spawns a child process.
|
* Spawns a child process.
|
||||||
*
|
*
|
||||||
|
@ -1488,6 +1492,8 @@ declare namespace Deno {
|
||||||
): Child;
|
): Child;
|
||||||
|
|
||||||
/** **UNSTABLE**: New API, yet to be vetted.
|
/** **UNSTABLE**: New API, yet to be vetted.
|
||||||
|
*
|
||||||
|
* @deprecated Use the Deno.Command API instead.
|
||||||
*
|
*
|
||||||
* The interface for handling a child process returned from
|
* The interface for handling a child process returned from
|
||||||
* {@linkcode Deno.spawnChild}.
|
* {@linkcode Deno.spawnChild}.
|
||||||
|
@ -1518,6 +1524,8 @@ declare namespace Deno {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** **UNSTABLE**: New API, yet to be vetted.
|
/** **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
|
* Executes a subprocess, waiting for it to finish and collecting all of its
|
||||||
* output.
|
* output.
|
||||||
|
@ -1531,7 +1539,7 @@ declare namespace Deno {
|
||||||
* const { code, stdout, stderr } = await Deno.spawn(Deno.execPath(), {
|
* const { code, stdout, stderr } = await Deno.spawn(Deno.execPath(), {
|
||||||
* args: [
|
* args: [
|
||||||
* "eval",
|
* "eval",
|
||||||
* "console.log('hello'); console.error('world')",
|
* "console.log('hello'); console.error('world')",
|
||||||
* ],
|
* ],
|
||||||
* });
|
* });
|
||||||
* console.assert(code === 0);
|
* console.assert(code === 0);
|
||||||
|
@ -1547,6 +1555,8 @@ declare namespace Deno {
|
||||||
): Promise<SpawnOutput>;
|
): Promise<SpawnOutput>;
|
||||||
|
|
||||||
/** **UNSTABLE**: New API, yet to be vetted.
|
/** **UNSTABLE**: New API, yet to be vetted.
|
||||||
|
*
|
||||||
|
* @deprecated Use the Deno.Command API instead.
|
||||||
*
|
*
|
||||||
* Synchronously executes a subprocess, waiting for it to finish and
|
* Synchronously executes a subprocess, waiting for it to finish and
|
||||||
* collecting all of its output.
|
* collecting all of its output.
|
||||||
|
@ -1560,7 +1570,7 @@ declare namespace Deno {
|
||||||
* const { code, stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
|
* const { code, stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
|
||||||
* args: [
|
* args: [
|
||||||
* "eval",
|
* "eval",
|
||||||
* "console.log('hello'); console.error('world')",
|
* "console.log('hello'); console.error('world')",
|
||||||
* ],
|
* ],
|
||||||
* });
|
* });
|
||||||
* console.assert(code === 0);
|
* console.assert(code === 0);
|
||||||
|
@ -1576,6 +1586,8 @@ declare namespace Deno {
|
||||||
): SpawnOutput;
|
): SpawnOutput;
|
||||||
|
|
||||||
/** **UNSTABLE**: New API, yet to be vetted.
|
/** **UNSTABLE**: New API, yet to be vetted.
|
||||||
|
*
|
||||||
|
* @deprecated Use the Deno.Command API instead.
|
||||||
*
|
*
|
||||||
* @category Sub Process
|
* @category Sub Process
|
||||||
*/
|
*/
|
||||||
|
@ -1591,6 +1603,8 @@ declare namespace Deno {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** **UNSTABLE**: New API, yet to be vetted.
|
/** **UNSTABLE**: New API, yet to be vetted.
|
||||||
|
*
|
||||||
|
* @deprecated Use the Deno.Command API instead.
|
||||||
*
|
*
|
||||||
* The interface returned from calling {@linkcode Deno.spawn} or
|
* The interface returned from calling {@linkcode Deno.spawn} or
|
||||||
* {@linkcode Deno.spawnSync} which represents the result of spawning the
|
* {@linkcode Deno.spawnSync} which represents the result of spawning the
|
||||||
|
@ -1604,6 +1618,197 @@ declare namespace Deno {
|
||||||
/** The buffered output from the child processes `stderr`. */
|
/** The buffered output from the child processes `stderr`. */
|
||||||
readonly stderr: Uint8Array;
|
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.
|
/** **UNSTABLE**: New API, yet to be vetted.
|
||||||
|
|
844
cli/tests/unit/command_test.ts
Normal file
844
cli/tests/unit/command_test.ts
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
|
@ -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 = {
|
window.__bootstrap.spawn = {
|
||||||
Child,
|
Child,
|
||||||
|
Command,
|
||||||
createSpawn,
|
createSpawn,
|
||||||
createSpawnChild,
|
createSpawnChild,
|
||||||
createSpawnSync,
|
createSpawnSync,
|
||||||
|
|
|
@ -149,6 +149,7 @@
|
||||||
spawnChild: __bootstrap.spawn.spawnChild,
|
spawnChild: __bootstrap.spawn.spawnChild,
|
||||||
spawn: __bootstrap.spawn.spawn,
|
spawn: __bootstrap.spawn.spawn,
|
||||||
spawnSync: __bootstrap.spawn.spawnSync,
|
spawnSync: __bootstrap.spawn.spawnSync,
|
||||||
|
Command: __bootstrap.spawn.Command,
|
||||||
serve: __bootstrap.flash.serve,
|
serve: __bootstrap.flash.serve,
|
||||||
upgradeHttp: __bootstrap.http.upgradeHttp,
|
upgradeHttp: __bootstrap.http.upgradeHttp,
|
||||||
upgradeHttpRaw: __bootstrap.flash.upgradeHttpRaw,
|
upgradeHttpRaw: __bootstrap.flash.upgradeHttpRaw,
|
||||||
|
|
Loading…
Reference in a new issue