1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-23 23:59:59 -05:00

feat(unstable): rework Deno.Command (#16812)

Refactors the `Deno.Command` class to not handle any state, but only being an intermediary to calling its methods, and as such any methods and properties besides `output`, `outputSync` & `spawn` have been removed. Interracting with a `spawn`ed subprocess now works by using the methods and properties on the returned class of the `spawn` method.
This commit is contained in:
Leo Kettmeir 2022-11-28 12:33:51 +01:00 committed by GitHub
parent fb04e87387
commit 1dd4843b62
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 102 additions and 148 deletions

View file

@ -31,13 +31,13 @@ tryExit();
Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program)); Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program));
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
cwd, cwd,
args: ["run", "--allow-read", programFile], args: ["run", "--allow-read", programFile],
stdout: "inherit", stdout: "inherit",
stderr: "inherit", stderr: "inherit",
}); });
child.spawn(); const child = command.spawn();
// Write the expected exit code *after* starting deno. // Write the expected exit code *after* starting deno.
// This is how we verify that `Child` is actually asynchronous. // This is how we verify that `Child` is actually asynchronous.
@ -55,7 +55,7 @@ tryExit();
Deno.test( Deno.test(
{ permissions: { run: true, read: true } }, { permissions: { run: true, read: true } },
async function commandStdinPiped() { async function commandStdinPiped() {
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: [ args: [
"eval", "eval",
"if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')", "if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
@ -64,7 +64,7 @@ Deno.test(
stdout: "null", stdout: "null",
stderr: "null", stderr: "null",
}); });
child.spawn(); const child = command.spawn();
assertThrows(() => child.stdout, TypeError, "stdout is not piped"); assertThrows(() => child.stdout, TypeError, "stdout is not piped");
assertThrows(() => child.stderr, TypeError, "stderr is not piped"); assertThrows(() => child.stderr, TypeError, "stderr is not piped");
@ -85,14 +85,14 @@ Deno.test(
Deno.test( Deno.test(
{ permissions: { run: true, read: true } }, { permissions: { run: true, read: true } },
async function commandStdoutPiped() { async function commandStdoutPiped() {
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: [ args: [
"eval", "eval",
"await Deno.stdout.write(new TextEncoder().encode('hello'))", "await Deno.stdout.write(new TextEncoder().encode('hello'))",
], ],
stderr: "null", stderr: "null",
}); });
child.spawn(); const child = command.spawn();
assertThrows(() => child.stdin, TypeError, "stdin is not piped"); assertThrows(() => child.stdin, TypeError, "stdin is not piped");
assertThrows(() => child.stderr, TypeError, "stderr is not piped"); assertThrows(() => child.stderr, TypeError, "stderr is not piped");
@ -118,14 +118,14 @@ Deno.test(
Deno.test( Deno.test(
{ permissions: { run: true, read: true } }, { permissions: { run: true, read: true } },
async function commandStderrPiped() { async function commandStderrPiped() {
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: [ args: [
"eval", "eval",
"await Deno.stderr.write(new TextEncoder().encode('hello'))", "await Deno.stderr.write(new TextEncoder().encode('hello'))",
], ],
stdout: "null", stdout: "null",
}); });
child.spawn(); const child = command.spawn();
assertThrows(() => child.stdin, TypeError, "stdin is not piped"); assertThrows(() => child.stdin, TypeError, "stdin is not piped");
assertThrows(() => child.stdout, TypeError, "stdout is not piped"); assertThrows(() => child.stdout, TypeError, "stdout is not piped");
@ -158,13 +158,13 @@ Deno.test(
write: true, write: true,
}); });
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: [ args: [
"eval", "eval",
"Deno.stderr.write(new TextEncoder().encode('error\\n')); Deno.stdout.write(new TextEncoder().encode('output\\n'));", "Deno.stderr.write(new TextEncoder().encode('error\\n')); Deno.stdout.write(new TextEncoder().encode('output\\n'));",
], ],
}); });
child.spawn(); const child = command.spawn();
await child.stdout.pipeTo(file.writable, { await child.stdout.pipeTo(file.writable, {
preventClose: true, preventClose: true,
}); });
@ -189,7 +189,7 @@ Deno.test(
await Deno.writeFile(fileName, encoder.encode("hello")); await Deno.writeFile(fileName, encoder.encode("hello"));
const file = await Deno.open(fileName); const file = await Deno.open(fileName);
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: [ args: [
"eval", "eval",
"if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')", "if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
@ -198,7 +198,7 @@ Deno.test(
stdout: "null", stdout: "null",
stderr: "null", stderr: "null",
}); });
child.spawn(); const child = command.spawn();
await file.readable.pipeTo(child.stdin, { await file.readable.pipeTo(child.stdin, {
preventClose: true, preventClose: true,
}); });
@ -212,12 +212,12 @@ Deno.test(
Deno.test( Deno.test(
{ permissions: { run: true, read: true } }, { permissions: { run: true, read: true } },
async function commandKillSuccess() { async function commandKillSuccess() {
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: ["eval", "setTimeout(() => {}, 10000)"], args: ["eval", "setTimeout(() => {}, 10000)"],
stdout: "null", stdout: "null",
stderr: "null", stderr: "null",
}); });
child.spawn(); const child = command.spawn();
child.kill("SIGKILL"); child.kill("SIGKILL");
const status = await child.status; const status = await child.status;
@ -236,12 +236,12 @@ Deno.test(
Deno.test( Deno.test(
{ permissions: { run: true, read: true } }, { permissions: { run: true, read: true } },
async function commandKillFailed() { async function commandKillFailed() {
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: ["eval", "setTimeout(() => {}, 5000)"], args: ["eval", "setTimeout(() => {}, 5000)"],
stdout: "null", stdout: "null",
stderr: "null", stderr: "null",
}); });
child.spawn(); const child = command.spawn();
assertThrows(() => { assertThrows(() => {
// @ts-expect-error testing runtime error of bad signal // @ts-expect-error testing runtime error of bad signal
@ -255,12 +255,12 @@ Deno.test(
Deno.test( Deno.test(
{ permissions: { run: true, read: true } }, { permissions: { run: true, read: true } },
async function commandKillOptional() { async function commandKillOptional() {
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: ["eval", "setTimeout(() => {}, 10000)"], args: ["eval", "setTimeout(() => {}, 10000)"],
stdout: "null", stdout: "null",
stderr: "null", stderr: "null",
}); });
child.spawn(); const child = command.spawn();
child.kill(); child.kill();
const status = await child.status; const status = await child.status;
@ -280,7 +280,7 @@ Deno.test(
{ permissions: { run: true, read: true } }, { permissions: { run: true, read: true } },
async function commandAbort() { async function commandAbort() {
const ac = new AbortController(); const ac = new AbortController();
const child = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: [ args: [
"eval", "eval",
"setTimeout(console.log, 1e8)", "setTimeout(console.log, 1e8)",
@ -289,7 +289,7 @@ Deno.test(
stdout: "null", stdout: "null",
stderr: "null", stderr: "null",
}); });
child.spawn(); const child = command.spawn();
queueMicrotask(() => ac.abort()); queueMicrotask(() => ac.abort());
const status = await child.status; const status = await child.status;
assertEquals(status.success, false); assertEquals(status.success, false);
@ -735,11 +735,12 @@ Deno.test(
const programFile = "unref.ts"; const programFile = "unref.ts";
const program = ` const program = `
const child = await new Deno.Command(Deno.execPath(), { const command = await new Deno.Command(Deno.execPath(), {
cwd: Deno.args[0], cwd: Deno.args[0],
stdout: "piped", stdout: "piped",
args: ["run", "-A", "--unstable", Deno.args[1]], args: ["run", "-A", "--unstable", Deno.args[1]],
});child.spawn(); });
const child = command.spawn();
const readable = child.stdout.pipeThrough(new TextDecoderStream()); const readable = child.stdout.pipeThrough(new TextDecoderStream());
const reader = readable.getReader(); const reader = readable.getReader();
// set up an interval that will end after reading a few messages from stdout, // set up an interval that will end after reading a few messages from stdout,

View file

@ -31,6 +31,7 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
"umask", "umask",
"spawnChild", "spawnChild",
"Child", "Child",
"ChildProcess",
"spawn", "spawn",
"spawnSync", "spawnSync",
"SpawnOptions", "SpawnOptions",

View file

@ -1639,14 +1639,14 @@ declare namespace Deno {
* ], * ],
* stdin: "piped", * stdin: "piped",
* }); * });
* command.spawn(); * const child = command.spawn();
* *
* // open a file and pipe the subprocess output to it. * // open a file and pipe the subprocess output to it.
* command.stdout.pipeTo(Deno.openSync("output").writable); * child.stdout.pipeTo(Deno.openSync("output").writable);
* *
* // manually close stdin * // manually close stdin
* command.stdin.close(); * child.stdin.close();
* const status = await command.status; * const status = await child.status;
* ``` * ```
* *
* ```ts * ```ts
@ -1678,13 +1678,6 @@ declare namespace Deno {
* @category Sub Process * @category Sub Process
*/ */
export class Command { 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); constructor(command: string | URL, options?: CommandOptions);
/** /**
* Executes the {@linkcode Deno.Command}, waiting for it to finish and * Executes the {@linkcode Deno.Command}, waiting for it to finish and
@ -1711,8 +1704,27 @@ declare namespace Deno {
/** /**
* Spawns a streamable subprocess, allowing to use the other methods. * Spawns a streamable subprocess, allowing to use the other methods.
*/ */
spawn(): void; spawn(): ChildProcess;
}
/** **UNSTABLE**: New API, yet to be vetted.
*
* The interface for handling a child process returned from
* {@linkcode Deno.Command.spawn}.
*
* @category Sub Process
*/
export class ChildProcess {
get stdin(): WritableStream<Uint8Array>;
get stdout(): ReadableStream<Uint8Array>;
get stderr(): ReadableStream<Uint8Array>;
readonly pid: number;
/** Get the status of the child. */
readonly status: Promise<CommandStatus>;
/** Waits for the child to exit completely, returning all its output and
* status. */
output(): Promise<CommandOutput>;
/** Kills the process with given {@linkcode Deno.Signal}. Defaults to /** Kills the process with given {@linkcode Deno.Signal}. Defaults to
* `"SIGTERM"`. */ * `"SIGTERM"`. */
kill(signo?: Signal): void; kill(signo?: Signal): void;

View file

@ -277,136 +277,44 @@
}; };
} }
class Command { function createCommand(spawn, spawnSync, spawnChild) {
return class Command {
#command; #command;
#options; #options;
#child;
#consumed;
constructor(command, options) { constructor(command, options) {
this.#command = command; this.#command = command;
this.#options = options; this.#options = options;
} }
output() { 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") { if (this.#options?.stdin === "piped") {
throw new TypeError( throw new TypeError(
"Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead", "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
); );
} }
return spawn(this.#command, this.#options);
this.#consumed = true;
return Deno.spawn(this.#command, this.#options);
}
} }
outputSync() { 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") { if (this.#options?.stdin === "piped") {
throw new TypeError( throw new TypeError(
"Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead", "Piped stdin is not supported for this function, use 'Deno.Command.spawn()' instead",
); );
} }
return spawnSync(this.#command, this.#options);
this.#consumed = true;
return Deno.spawnSync(this.#command, this.#options);
} }
spawn() { spawn() {
if (this.#consumed) { return spawnChild(this.#command, this.#options);
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, ChildProcess: Child,
createCommand,
createSpawn, createSpawn,
createSpawnChild, createSpawnChild,
createSpawnSync, createSpawnSync,

View file

@ -144,6 +144,7 @@
funlock: __bootstrap.fs.funlock, funlock: __bootstrap.fs.funlock,
funlockSync: __bootstrap.fs.funlockSync, funlockSync: __bootstrap.fs.funlockSync,
Child: __bootstrap.spawn.Child, Child: __bootstrap.spawn.Child,
ChildProcess: __bootstrap.spawn.ChildProcess,
spawnChild: __bootstrap.spawn.spawnChild, spawnChild: __bootstrap.spawn.spawnChild,
spawn: __bootstrap.spawn.spawn, spawn: __bootstrap.spawn.spawn,
spawnSync: __bootstrap.spawn.spawnSync, spawnSync: __bootstrap.spawn.spawnSync,

View file

@ -482,6 +482,14 @@ delete Intl.v8BreakIterator;
}, },
}); });
ObjectAssign(internals.nodeUnstable, {
Command: __bootstrap.spawn.createCommand(
internals.nodeUnstable.spawn,
internals.nodeUnstable.spawnSync,
internals.nodeUnstable.spawnChild,
),
});
const finalDenoNs = { const finalDenoNs = {
core, core,
internal: internalSymbol, internal: internalSymbol,
@ -513,6 +521,14 @@ delete Intl.v8BreakIterator;
ops.op_net_listen_unixpacket, ops.op_net_listen_unixpacket,
), ),
}); });
ObjectAssign(finalDenoNs, {
Command: __bootstrap.spawn.createCommand(
finalDenoNs.spawn,
finalDenoNs.spawnSync,
finalDenoNs.spawnChild,
),
});
} }
// Setup `Deno` global - we're actually overriding already existing global // Setup `Deno` global - we're actually overriding already existing global
@ -617,6 +633,14 @@ delete Intl.v8BreakIterator;
}, },
}); });
ObjectAssign(internals.nodeUnstable, {
Command: __bootstrap.spawn.createCommand(
internals.nodeUnstable.spawn,
internals.nodeUnstable.spawnSync,
internals.nodeUnstable.spawnChild,
),
});
const finalDenoNs = { const finalDenoNs = {
core, core,
internal: internalSymbol, internal: internalSymbol,
@ -640,6 +664,13 @@ delete Intl.v8BreakIterator;
ops.op_net_listen_unixpacket, ops.op_net_listen_unixpacket,
), ),
}); });
ObjectAssign(finalDenoNs, {
Command: __bootstrap.spawn.createCommand(
finalDenoNs.spawn,
finalDenoNs.spawnSync,
finalDenoNs.spawnChild,
),
});
} }
ObjectDefineProperties(finalDenoNs, { ObjectDefineProperties(finalDenoNs, {
pid: util.readOnly(runtimeOptions.pid), pid: util.readOnly(runtimeOptions.pid),