mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 16:49:18 -05:00
feat(runtime): two-tier subprocess API (#11618)
This commit is contained in:
parent
8b25807054
commit
8a7539cab3
13 changed files with 1323 additions and 5 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
@ -236,7 +236,7 @@ jobs:
|
||||||
~/.cargo/registry/index
|
~/.cargo/registry/index
|
||||||
~/.cargo/registry/cache
|
~/.cargo/registry/cache
|
||||||
~/.cargo/git/db
|
~/.cargo/git/db
|
||||||
key: 8-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }}
|
key: 9-cargo-home-${{ matrix.os }}-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
# In main branch, always creates fresh cache
|
# In main branch, always creates fresh cache
|
||||||
- name: Cache build output (main)
|
- name: Cache build output (main)
|
||||||
|
@ -252,7 +252,7 @@ jobs:
|
||||||
!./target/*/*.zip
|
!./target/*/*.zip
|
||||||
!./target/*/*.tar.gz
|
!./target/*/*.tar.gz
|
||||||
key: |
|
key: |
|
||||||
8-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }}
|
9-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-${{ github.sha }}
|
||||||
|
|
||||||
# Restore cache from the latest 'main' branch build.
|
# Restore cache from the latest 'main' branch build.
|
||||||
- name: Cache build output (PR)
|
- name: Cache build output (PR)
|
||||||
|
@ -268,7 +268,7 @@ jobs:
|
||||||
!./target/*/*.tar.gz
|
!./target/*/*.tar.gz
|
||||||
key: never_saved
|
key: never_saved
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
8-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-
|
9-cargo-target-${{ matrix.os }}-${{ matrix.profile }}-
|
||||||
|
|
||||||
# Don't save cache after building PRs or branches other than 'main'.
|
# Don't save cache after building PRs or branches other than 'main'.
|
||||||
- name: Skip save cache (PR)
|
- name: Skip save cache (PR)
|
||||||
|
|
|
@ -66,6 +66,12 @@ const UNSTABLE_DENO_PROPS: &[&str] = &[
|
||||||
"umask",
|
"umask",
|
||||||
"utime",
|
"utime",
|
||||||
"utimeSync",
|
"utimeSync",
|
||||||
|
"spawnChild",
|
||||||
|
"Child",
|
||||||
|
"spawn",
|
||||||
|
"spawnSync",
|
||||||
|
"ChildStatus",
|
||||||
|
"SpawnOutput",
|
||||||
];
|
];
|
||||||
|
|
||||||
static MSG_MISSING_PROPERTY_DENO: Lazy<Regex> = Lazy::new(|| {
|
static MSG_MISSING_PROPERTY_DENO: Lazy<Regex> = Lazy::new(|| {
|
||||||
|
|
139
cli/dts/lib.deno.unstable.d.ts
vendored
139
cli/dts/lib.deno.unstable.d.ts
vendored
|
@ -1361,6 +1361,145 @@ declare namespace Deno {
|
||||||
export function upgradeHttp(
|
export function upgradeHttp(
|
||||||
request: Request,
|
request: Request,
|
||||||
): Promise<[Deno.Conn, Uint8Array]>;
|
): Promise<[Deno.Conn, Uint8Array]>;
|
||||||
|
|
||||||
|
export interface SpawnOptions {
|
||||||
|
/** Arguments to pass to the process. */
|
||||||
|
args?: string[];
|
||||||
|
/**
|
||||||
|
* The working directory of the process.
|
||||||
|
* If not specified, the cwd of the parent process is used.
|
||||||
|
*/
|
||||||
|
cwd?: string | URL;
|
||||||
|
/**
|
||||||
|
* Clear environmental variables from parent process.
|
||||||
|
* Doesn't guarantee that only `opt.env` variables are present,
|
||||||
|
* as the OS may set environmental variables for processes.
|
||||||
|
*/
|
||||||
|
clearEnv?: boolean;
|
||||||
|
/** Environmental variables to pass to the subprocess. */
|
||||||
|
env?: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Sets the child process’s user ID. This translates to a setuid call
|
||||||
|
* in the child process. Failure in the setuid call will cause the spawn to fail.
|
||||||
|
*/
|
||||||
|
uid?: number;
|
||||||
|
/** Similar to `uid`, but sets the group ID of the child process. */
|
||||||
|
gid?: number;
|
||||||
|
|
||||||
|
/** Defaults to "null". */
|
||||||
|
stdin?: "piped" | "inherit" | "null";
|
||||||
|
/** Defaults to "piped". */
|
||||||
|
stdout?: "piped" | "inherit" | "null";
|
||||||
|
/** Defaults to "piped". */
|
||||||
|
stderr?: "piped" | "inherit" | "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawns a child process.
|
||||||
|
*
|
||||||
|
* If stdin is set to "piped", the stdin WritableStream needs to be closed manually.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const child = Deno.spawnChild(Deno.execPath(), {
|
||||||
|
* args: [
|
||||||
|
* "eval",
|
||||||
|
* "console.log('Hello World')",
|
||||||
|
* ],
|
||||||
|
* stdin: "piped",
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* // open a file and pipe the subprocess output to it.
|
||||||
|
* child.stdout.pipeTo(Deno.openSync("output").writable);
|
||||||
|
*
|
||||||
|
* // manually close stdin
|
||||||
|
* child.stdin.close();
|
||||||
|
* const status = await child.status;
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function spawnChild<T extends SpawnOptions = SpawnOptions>(
|
||||||
|
command: string | URL,
|
||||||
|
options?: T,
|
||||||
|
): Child<T>;
|
||||||
|
|
||||||
|
export class Child<T extends SpawnOptions> {
|
||||||
|
readonly stdin: T["stdin"] extends "piped" ? WritableStream<Uint8Array>
|
||||||
|
: null;
|
||||||
|
readonly stdout: T["stdout"] extends "inherit" | "null" ? null
|
||||||
|
: ReadableStream<Uint8Array>;
|
||||||
|
readonly stderr: T["stderr"] extends "inherit" | "null" ? null
|
||||||
|
: ReadableStream<Uint8Array>;
|
||||||
|
|
||||||
|
readonly pid: number;
|
||||||
|
/** Get the status of the child. */
|
||||||
|
readonly status: Promise<ChildStatus>;
|
||||||
|
|
||||||
|
/** Waits for the child to exit completely, returning all its output and status. */
|
||||||
|
output(): Promise<SpawnOutput<T>>;
|
||||||
|
/** Kills the process with given Signal. */
|
||||||
|
kill(signo: Signal): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a subprocess, waiting for it to finish and
|
||||||
|
* collecting all of its output.
|
||||||
|
* The stdio options are ignored.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const { status, stdout, stderr } = await Deno.spawn(Deno.execPath(), {
|
||||||
|
* args: [
|
||||||
|
* "eval",
|
||||||
|
* "console.log('hello'); console.error('world')",
|
||||||
|
* ],
|
||||||
|
* });
|
||||||
|
* console.assert(status.code === 0);
|
||||||
|
* console.assert("hello\n" === new TextDecoder().decode(stdout));
|
||||||
|
* console.assert("world\n" === new TextDecoder().decode(stderr));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function spawn<T extends SpawnOptions = SpawnOptions>(
|
||||||
|
command: string | URL,
|
||||||
|
options?: T,
|
||||||
|
): Promise<SpawnOutput<T>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronously executes a subprocess, waiting for it to finish and
|
||||||
|
* collecting all of its output.
|
||||||
|
* The stdio options are ignored.
|
||||||
|
*
|
||||||
|
* * ```ts
|
||||||
|
* const { status, stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
|
||||||
|
* args: [
|
||||||
|
* "eval",
|
||||||
|
* "console.log('hello'); console.error('world')",
|
||||||
|
* ],
|
||||||
|
* });
|
||||||
|
* console.assert(status.code === 0);
|
||||||
|
* console.assert("hello\n" === new TextDecoder().decode(stdout));
|
||||||
|
* console.assert("world\n" === new TextDecoder().decode(stderr));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function spawnSync<T extends SpawnOptions = SpawnOptions>(
|
||||||
|
command: string | URL,
|
||||||
|
options?: T,
|
||||||
|
): SpawnOutput<T>;
|
||||||
|
|
||||||
|
export type ChildStatus =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
code: 0;
|
||||||
|
signal: null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
code: number;
|
||||||
|
signal: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SpawnOutput<T extends SpawnOptions> {
|
||||||
|
status: ChildStatus;
|
||||||
|
stdout: T["stdout"] extends "inherit" | "null" ? null : Uint8Array;
|
||||||
|
stderr: T["stderr"] extends "inherit" | "null" ? null : Uint8Array;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare function fetch(
|
declare function fetch(
|
||||||
|
|
687
cli/tests/unit/command_test.ts
Normal file
687
cli/tests/unit/command_test.ts
Normal file
|
@ -0,0 +1,687 @@
|
||||||
|
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||||
|
import {
|
||||||
|
assert,
|
||||||
|
assertEquals,
|
||||||
|
assertRejects,
|
||||||
|
assertStringIncludes,
|
||||||
|
assertThrows,
|
||||||
|
} from "./test_util.ts";
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { write: true, run: true, read: true } },
|
||||||
|
async function spawnWithCwdIsAsync() {
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
const cwd = await Deno.makeTempDir({ prefix: "deno_command_test" });
|
||||||
|
|
||||||
|
const exitCodeFile = "deno_was_here";
|
||||||
|
const programFile = "poll_exit.ts";
|
||||||
|
const program = `
|
||||||
|
async function tryExit() {
|
||||||
|
try {
|
||||||
|
const code = parseInt(await Deno.readTextFile("${exitCodeFile}"));
|
||||||
|
Deno.exit(code);
|
||||||
|
} catch {
|
||||||
|
// Retry if we got here before deno wrote the file.
|
||||||
|
setTimeout(tryExit, 0.01);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tryExit();
|
||||||
|
`;
|
||||||
|
|
||||||
|
Deno.writeFileSync(`${cwd}/${programFile}`, enc.encode(program));
|
||||||
|
|
||||||
|
const child = Deno.spawnChild(Deno.execPath(), {
|
||||||
|
cwd,
|
||||||
|
args: ["run", "--allow-read", programFile],
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Write the expected exit code *after* starting deno.
|
||||||
|
// This is how we verify that `Child` is actually asynchronous.
|
||||||
|
const code = 84;
|
||||||
|
Deno.writeFileSync(`${cwd}/${exitCodeFile}`, enc.encode(`${code}`));
|
||||||
|
|
||||||
|
const status = await child.status;
|
||||||
|
await Deno.remove(cwd, { recursive: true });
|
||||||
|
assertEquals(status.success, false);
|
||||||
|
assertEquals(status.code, code);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnStdinPiped() {
|
||||||
|
const child = Deno.spawnChild(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
|
||||||
|
],
|
||||||
|
stdin: "piped",
|
||||||
|
stdout: "null",
|
||||||
|
stderr: "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(child.stdin !== null);
|
||||||
|
assert(child.stdout === null);
|
||||||
|
assert(child.stderr === null);
|
||||||
|
|
||||||
|
const msg = new TextEncoder().encode("hello");
|
||||||
|
const writer = child.stdin.getWriter();
|
||||||
|
await writer.write(msg);
|
||||||
|
writer.releaseLock();
|
||||||
|
|
||||||
|
await child.stdin.close();
|
||||||
|
const status = await child.status;
|
||||||
|
assertEquals(status.success, true);
|
||||||
|
assertEquals(status.code, 0);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnStdoutPiped() {
|
||||||
|
const child = Deno.spawnChild(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"await Deno.stdout.write(new TextEncoder().encode('hello'))",
|
||||||
|
],
|
||||||
|
stderr: "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(child.stdin === null);
|
||||||
|
assert(child.stdout !== null);
|
||||||
|
assert(child.stderr === null);
|
||||||
|
|
||||||
|
const readable = child.stdout.pipeThrough(new TextDecoderStream());
|
||||||
|
const reader = readable.getReader();
|
||||||
|
const res = await reader.read();
|
||||||
|
assert(!res.done);
|
||||||
|
assertEquals(res.value, "hello");
|
||||||
|
|
||||||
|
const resEnd = await reader.read();
|
||||||
|
assert(resEnd.done);
|
||||||
|
assertEquals(resEnd.value, undefined);
|
||||||
|
reader.releaseLock();
|
||||||
|
|
||||||
|
const status = await child.status;
|
||||||
|
assertEquals(status.success, true);
|
||||||
|
assertEquals(status.code, 0);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnStderrPiped() {
|
||||||
|
const child = Deno.spawnChild(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"await Deno.stderr.write(new TextEncoder().encode('hello'))",
|
||||||
|
],
|
||||||
|
stderr: "piped",
|
||||||
|
stdout: "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(child.stdin === null);
|
||||||
|
assert(child.stdout === null);
|
||||||
|
assert(child.stderr !== null);
|
||||||
|
|
||||||
|
const readable = child.stderr.pipeThrough(new TextDecoderStream());
|
||||||
|
const reader = readable.getReader();
|
||||||
|
const res = await reader.read();
|
||||||
|
assert(!res.done);
|
||||||
|
assertEquals(res.value, "hello");
|
||||||
|
|
||||||
|
const resEnd = await reader.read();
|
||||||
|
assert(resEnd.done);
|
||||||
|
assertEquals(resEnd.value, undefined);
|
||||||
|
reader.releaseLock();
|
||||||
|
|
||||||
|
const status = await child.status;
|
||||||
|
assertEquals(status.success, true);
|
||||||
|
assertEquals(status.code, 0);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, write: true, read: true } },
|
||||||
|
async function spawnRedirectStdoutStderr() {
|
||||||
|
const tempDir = await Deno.makeTempDir();
|
||||||
|
const fileName = tempDir + "/redirected_stdio.txt";
|
||||||
|
const file = await Deno.open(fileName, {
|
||||||
|
create: true,
|
||||||
|
write: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const child = Deno.spawnChild(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"Deno.stderr.write(new TextEncoder().encode('error\\n')); Deno.stdout.write(new TextEncoder().encode('output\\n'));",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await child.stdout.pipeTo(file.writable, {
|
||||||
|
preventClose: true,
|
||||||
|
});
|
||||||
|
await child.stderr.pipeTo(file.writable);
|
||||||
|
await child.status;
|
||||||
|
|
||||||
|
const fileContents = await Deno.readFile(fileName);
|
||||||
|
const decoder = new TextDecoder();
|
||||||
|
const text = decoder.decode(fileContents);
|
||||||
|
|
||||||
|
assertStringIncludes(text, "error");
|
||||||
|
assertStringIncludes(text, "output");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, write: true, read: true } },
|
||||||
|
async function spawnRedirectStdin() {
|
||||||
|
const tempDir = await Deno.makeTempDir();
|
||||||
|
const fileName = tempDir + "/redirected_stdio.txt";
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
await Deno.writeFile(fileName, encoder.encode("hello"));
|
||||||
|
const file = await Deno.open(fileName);
|
||||||
|
|
||||||
|
const child = Deno.spawnChild(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"if (new TextDecoder().decode(await Deno.readAll(Deno.stdin)) !== 'hello') throw new Error('Expected \\'hello\\'')",
|
||||||
|
],
|
||||||
|
stdin: "piped",
|
||||||
|
stdout: "null",
|
||||||
|
stderr: "null",
|
||||||
|
});
|
||||||
|
await file.readable.pipeTo(child.stdin, {
|
||||||
|
preventClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
await child.stdin.close();
|
||||||
|
const status = await child.status;
|
||||||
|
assertEquals(status.code, 0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnKillSuccess() {
|
||||||
|
const child = Deno.spawnChild(Deno.execPath(), {
|
||||||
|
args: ["eval", "setTimeout(() => {}, 10000)"],
|
||||||
|
stdout: "null",
|
||||||
|
stderr: "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
child.kill("SIGKILL");
|
||||||
|
const status = await child.status;
|
||||||
|
|
||||||
|
assertEquals(status.success, false);
|
||||||
|
if (Deno.build.os === "windows") {
|
||||||
|
assertEquals(status.code, 1);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
} else {
|
||||||
|
assertEquals(status.code, 137);
|
||||||
|
assertEquals(status.signal, 9);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnKillFailed() {
|
||||||
|
const child = Deno.spawnChild(Deno.execPath(), {
|
||||||
|
args: ["eval", "setTimeout(() => {}, 5000)"],
|
||||||
|
stdout: "null",
|
||||||
|
stderr: "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThrows(() => {
|
||||||
|
// @ts-expect-error testing runtime error of bad signal
|
||||||
|
child.kill("foobar");
|
||||||
|
}, TypeError);
|
||||||
|
|
||||||
|
await child.status;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { read: true, run: false } },
|
||||||
|
async function spawnPermissions() {
|
||||||
|
await assertRejects(async () => {
|
||||||
|
await Deno.spawn(Deno.execPath(), {
|
||||||
|
args: ["eval", "console.log('hello world')"],
|
||||||
|
});
|
||||||
|
}, Deno.errors.PermissionDenied);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { read: true, run: false } },
|
||||||
|
function spawnSyncPermissions() {
|
||||||
|
assertThrows(() => {
|
||||||
|
Deno.spawnSync(Deno.execPath(), {
|
||||||
|
args: ["eval", "console.log('hello world')"],
|
||||||
|
});
|
||||||
|
}, Deno.errors.PermissionDenied);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnSuccess() {
|
||||||
|
const { status } = await Deno.spawn(Deno.execPath(), {
|
||||||
|
args: ["eval", "console.log('hello world')"],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(status.success, true);
|
||||||
|
assertEquals(status.code, 0);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
function spawnSyncSuccess() {
|
||||||
|
const { status } = Deno.spawnSync(Deno.execPath(), {
|
||||||
|
args: ["eval", "console.log('hello world')"],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertEquals(status.success, true);
|
||||||
|
assertEquals(status.code, 0);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnUrl() {
|
||||||
|
const { status, stdout } = await Deno.spawn(
|
||||||
|
new URL(`file:///${Deno.execPath()}`),
|
||||||
|
{
|
||||||
|
args: ["eval", "console.log('hello world')"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(new TextDecoder().decode(stdout), "hello world\n");
|
||||||
|
|
||||||
|
assertEquals(status.success, true);
|
||||||
|
assertEquals(status.code, 0);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
function spawnSyncUrl() {
|
||||||
|
const { status, stdout } = Deno.spawnSync(
|
||||||
|
new URL(`file:///${Deno.execPath()}`),
|
||||||
|
{
|
||||||
|
args: ["eval", "console.log('hello world')"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals(new TextDecoder().decode(stdout), "hello world\n");
|
||||||
|
|
||||||
|
assertEquals(status.success, true);
|
||||||
|
assertEquals(status.code, 0);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test({ permissions: { run: true } }, async function spawnNotFound() {
|
||||||
|
await assertRejects(
|
||||||
|
() => Deno.spawn("this file hopefully doesn't exist"),
|
||||||
|
Deno.errors.NotFound,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test({ permissions: { run: true } }, function spawnSyncNotFound() {
|
||||||
|
assertThrows(
|
||||||
|
() => Deno.spawnSync("this file hopefully doesn't exist"),
|
||||||
|
Deno.errors.NotFound,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnFailedWithCode() {
|
||||||
|
const { status } = await Deno.spawn(Deno.execPath(), {
|
||||||
|
args: ["eval", "Deno.exit(41 + 1)"],
|
||||||
|
});
|
||||||
|
assertEquals(status.success, false);
|
||||||
|
assertEquals(status.code, 42);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
function spawnSyncFailedWithCode() {
|
||||||
|
const { status } = Deno.spawnSync(Deno.execPath(), {
|
||||||
|
args: ["eval", "Deno.exit(41 + 1)"],
|
||||||
|
});
|
||||||
|
assertEquals(status.success, false);
|
||||||
|
assertEquals(status.code, 42);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{
|
||||||
|
permissions: { run: true, read: true },
|
||||||
|
},
|
||||||
|
async function spawnFailedWithSignal() {
|
||||||
|
const { status } = await Deno.spawn(Deno.execPath(), {
|
||||||
|
args: ["eval", "--unstable", "Deno.kill(Deno.pid, 'SIGKILL')"],
|
||||||
|
});
|
||||||
|
assertEquals(status.success, false);
|
||||||
|
if (Deno.build.os === "windows") {
|
||||||
|
assertEquals(status.code, 1);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
} else {
|
||||||
|
assertEquals(status.code, 128 + 9);
|
||||||
|
assertEquals(status.signal, 9);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{
|
||||||
|
permissions: { run: true, read: true },
|
||||||
|
},
|
||||||
|
function spawnSyncFailedWithSignal() {
|
||||||
|
const { status } = Deno.spawnSync(Deno.execPath(), {
|
||||||
|
args: ["eval", "--unstable", "Deno.kill(Deno.pid, 'SIGKILL')"],
|
||||||
|
});
|
||||||
|
assertEquals(status.success, false);
|
||||||
|
if (Deno.build.os === "windows") {
|
||||||
|
assertEquals(status.code, 1);
|
||||||
|
assertEquals(status.signal, null);
|
||||||
|
} else {
|
||||||
|
assertEquals(status.code, 128 + 9);
|
||||||
|
assertEquals(status.signal, 9);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnOutput() {
|
||||||
|
const { stdout } = await Deno.spawn(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"await Deno.stdout.write(new TextEncoder().encode('hello'))",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const s = new TextDecoder().decode(stdout);
|
||||||
|
assertEquals(s, "hello");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
function spawnSyncOutput() {
|
||||||
|
const { stdout } = Deno.spawnSync(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"await Deno.stdout.write(new TextEncoder().encode('hello'))",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const s = new TextDecoder().decode(stdout);
|
||||||
|
assertEquals(s, "hello");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnStderrOutput() {
|
||||||
|
const { stderr } = await Deno.spawn(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"await Deno.stderr.write(new TextEncoder().encode('error'))",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const s = new TextDecoder().decode(stderr);
|
||||||
|
assertEquals(s, "error");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
function spawnSyncStderrOutput() {
|
||||||
|
const { stderr } = Deno.spawnSync(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"await Deno.stderr.write(new TextEncoder().encode('error'))",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const s = new TextDecoder().decode(stderr);
|
||||||
|
assertEquals(s, "error");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnOverrideStdio() {
|
||||||
|
const { stdout, stderr } = await Deno.spawn(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"console.log('hello'); console.error('world')",
|
||||||
|
],
|
||||||
|
stdin: "piped",
|
||||||
|
stdout: "null",
|
||||||
|
stderr: "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore: for testing
|
||||||
|
assertEquals(new TextDecoder().decode(stdout), "hello\n");
|
||||||
|
// @ts-ignore: for testing
|
||||||
|
assertEquals(new TextDecoder().decode(stderr), "world\n");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
function spawnSyncOverrideStdio() {
|
||||||
|
const { stdout, stderr } = Deno.spawnSync(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"console.log('hello'); console.error('world')",
|
||||||
|
],
|
||||||
|
stdin: "piped",
|
||||||
|
stdout: "null",
|
||||||
|
stderr: "null",
|
||||||
|
});
|
||||||
|
|
||||||
|
// @ts-ignore: for testing
|
||||||
|
assertEquals(new TextDecoder().decode(stdout), "hello\n");
|
||||||
|
// @ts-ignore: for testing
|
||||||
|
assertEquals(new TextDecoder().decode(stderr), "world\n");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
async function spawnEnv() {
|
||||||
|
const { stdout } = await Deno.spawn(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
FOO: "0123",
|
||||||
|
BAR: "4567",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const s = new TextDecoder().decode(stdout);
|
||||||
|
assertEquals(s, "01234567");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true } },
|
||||||
|
function spawnEnv() {
|
||||||
|
const { stdout } = Deno.spawnSync(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"Deno.stdout.write(new TextEncoder().encode(Deno.env.get('FOO') + Deno.env.get('BAR')))",
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
FOO: "0123",
|
||||||
|
BAR: "4567",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const s = new TextDecoder().decode(stdout);
|
||||||
|
assertEquals(s, "01234567");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true, env: true } },
|
||||||
|
async function spawnClearEnv() {
|
||||||
|
const { stdout } = await Deno.spawn(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"-p",
|
||||||
|
"JSON.stringify(Deno.env.toObject())",
|
||||||
|
],
|
||||||
|
clearEnv: true,
|
||||||
|
env: {
|
||||||
|
FOO: "23147",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const obj = JSON.parse(new TextDecoder().decode(stdout));
|
||||||
|
|
||||||
|
// can't check for object equality because the OS may set additional env
|
||||||
|
// vars for processes, so we check if PATH isn't present as that is a common
|
||||||
|
// env var across OS's and isn't set for processes.
|
||||||
|
assertEquals(obj.FOO, "23147");
|
||||||
|
assert(!("PATH" in obj));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{ permissions: { run: true, read: true, env: true } },
|
||||||
|
function spawnSyncClearEnv() {
|
||||||
|
const { stdout } = Deno.spawnSync(Deno.execPath(), {
|
||||||
|
args: [
|
||||||
|
"eval",
|
||||||
|
"-p",
|
||||||
|
"JSON.stringify(Deno.env.toObject())",
|
||||||
|
],
|
||||||
|
clearEnv: true,
|
||||||
|
env: {
|
||||||
|
FOO: "23147",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const obj = JSON.parse(new TextDecoder().decode(stdout));
|
||||||
|
|
||||||
|
// can't check for object equality because the OS may set additional env
|
||||||
|
// vars for processes, so we check if PATH isn't present as that is a common
|
||||||
|
// env var across OS's and isn't set for processes.
|
||||||
|
assertEquals(obj.FOO, "23147");
|
||||||
|
assert(!("PATH" in obj));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{
|
||||||
|
permissions: { run: true, read: true },
|
||||||
|
ignore: Deno.build.os === "windows",
|
||||||
|
},
|
||||||
|
async function spawnUid() {
|
||||||
|
const { stdout } = await Deno.spawn("id", {
|
||||||
|
args: ["-u"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUid = new TextDecoder().decode(stdout);
|
||||||
|
|
||||||
|
if (currentUid !== "0") {
|
||||||
|
await assertRejects(async () => {
|
||||||
|
await Deno.spawn("echo", {
|
||||||
|
args: ["fhqwhgads"],
|
||||||
|
uid: 0,
|
||||||
|
});
|
||||||
|
}, Deno.errors.PermissionDenied);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{
|
||||||
|
permissions: { run: true, read: true },
|
||||||
|
ignore: Deno.build.os === "windows",
|
||||||
|
},
|
||||||
|
function spawnSyncUid() {
|
||||||
|
const { stdout } = Deno.spawnSync("id", {
|
||||||
|
args: ["-u"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentUid = new TextDecoder().decode(stdout);
|
||||||
|
|
||||||
|
if (currentUid !== "0") {
|
||||||
|
assertThrows(() => {
|
||||||
|
Deno.spawnSync("echo", {
|
||||||
|
args: ["fhqwhgads"],
|
||||||
|
uid: 0,
|
||||||
|
});
|
||||||
|
}, Deno.errors.PermissionDenied);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{
|
||||||
|
permissions: { run: true, read: true },
|
||||||
|
ignore: Deno.build.os === "windows",
|
||||||
|
},
|
||||||
|
async function spawnGid() {
|
||||||
|
const { stdout } = await Deno.spawn("id", {
|
||||||
|
args: ["-g"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentGid = new TextDecoder().decode(stdout);
|
||||||
|
|
||||||
|
if (currentGid !== "0") {
|
||||||
|
await assertRejects(async () => {
|
||||||
|
await Deno.spawn("echo", {
|
||||||
|
args: ["fhqwhgads"],
|
||||||
|
gid: 0,
|
||||||
|
});
|
||||||
|
}, Deno.errors.PermissionDenied);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Deno.test(
|
||||||
|
{
|
||||||
|
permissions: { run: true, read: true },
|
||||||
|
ignore: Deno.build.os === "windows",
|
||||||
|
},
|
||||||
|
function spawnSyncGid() {
|
||||||
|
const { stdout } = Deno.spawnSync("id", {
|
||||||
|
args: ["-g"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentGid = new TextDecoder().decode(stdout);
|
||||||
|
|
||||||
|
if (currentGid !== "0") {
|
||||||
|
assertThrows(() => {
|
||||||
|
Deno.spawnSync("echo", {
|
||||||
|
args: ["fhqwhgads"],
|
||||||
|
gid: 0,
|
||||||
|
});
|
||||||
|
}, Deno.errors.PermissionDenied);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
|
@ -557,8 +557,9 @@ Deno.test(
|
||||||
|
|
||||||
const obj = JSON.parse(new TextDecoder().decode(await p.output()));
|
const obj = JSON.parse(new TextDecoder().decode(await p.output()));
|
||||||
|
|
||||||
// can't check for object equality because the OS may set additional env vars for processes
|
// can't check for object equality because the OS may set additional env
|
||||||
// so we check if PATH isn't present as that is a common env var across OS's and isn't set for processes.
|
// vars for processes, so we check if PATH isn't present as that is a common
|
||||||
|
// env var across OS's and isn't set for processes.
|
||||||
assertEquals(obj.FOO, "23147");
|
assertEquals(obj.FOO, "23147");
|
||||||
assert(!("PATH" in obj));
|
assert(!("PATH" in obj));
|
||||||
|
|
||||||
|
|
1
ext/web/lib.deno_web.d.ts
vendored
1
ext/web/lib.deno_web.d.ts
vendored
|
@ -630,6 +630,7 @@ interface WritableStreamErrorCallback {
|
||||||
interface WritableStream<W = any> {
|
interface WritableStream<W = any> {
|
||||||
readonly locked: boolean;
|
readonly locked: boolean;
|
||||||
abort(reason?: any): Promise<void>;
|
abort(reason?: any): Promise<void>;
|
||||||
|
close(): Promise<void>;
|
||||||
getWriter(): WritableStreamDefaultWriter<W>;
|
getWriter(): WritableStreamDefaultWriter<W>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
206
runtime/js/40_spawn.js
Normal file
206
runtime/js/40_spawn.js
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
((window) => {
|
||||||
|
const core = window.Deno.core;
|
||||||
|
const { pathFromURL } = window.__bootstrap.util;
|
||||||
|
const { illegalConstructorKey } = window.__bootstrap.webUtil;
|
||||||
|
const {
|
||||||
|
ArrayPrototypeMap,
|
||||||
|
ObjectEntries,
|
||||||
|
String,
|
||||||
|
TypeError,
|
||||||
|
Uint8Array,
|
||||||
|
PromiseAll,
|
||||||
|
} = window.__bootstrap.primordials;
|
||||||
|
const { readableStreamForRid, writableStreamForRid } =
|
||||||
|
window.__bootstrap.streamUtils;
|
||||||
|
|
||||||
|
function spawnChild(command, {
|
||||||
|
args = [],
|
||||||
|
cwd = undefined,
|
||||||
|
clearEnv = false,
|
||||||
|
env = {},
|
||||||
|
uid = undefined,
|
||||||
|
gid = undefined,
|
||||||
|
stdin = "null",
|
||||||
|
stdout = "piped",
|
||||||
|
stderr = "piped",
|
||||||
|
} = {}) {
|
||||||
|
const child = core.opSync("op_spawn_child", {
|
||||||
|
cmd: pathFromURL(command),
|
||||||
|
args: ArrayPrototypeMap(args, String),
|
||||||
|
cwd: pathFromURL(cwd),
|
||||||
|
clearEnv,
|
||||||
|
env: ObjectEntries(env),
|
||||||
|
uid,
|
||||||
|
gid,
|
||||||
|
stdin,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
});
|
||||||
|
return new Child(illegalConstructorKey, child);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collectOutput(readableStream) {
|
||||||
|
if (!(readableStream instanceof ReadableStream)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bufs = [];
|
||||||
|
let size = 0;
|
||||||
|
for await (const chunk of readableStream) {
|
||||||
|
bufs.push(chunk);
|
||||||
|
size += chunk.byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = new Uint8Array(size);
|
||||||
|
let offset = 0;
|
||||||
|
for (const chunk of bufs) {
|
||||||
|
buffer.set(chunk, offset);
|
||||||
|
offset += chunk.byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Child {
|
||||||
|
#rid;
|
||||||
|
|
||||||
|
#pid;
|
||||||
|
get pid() {
|
||||||
|
return this.#pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stdinRid;
|
||||||
|
#stdin = null;
|
||||||
|
get stdin() {
|
||||||
|
return this.#stdin;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stdoutRid;
|
||||||
|
#stdout = null;
|
||||||
|
get stdout() {
|
||||||
|
return this.#stdout;
|
||||||
|
}
|
||||||
|
|
||||||
|
#stderrRid;
|
||||||
|
#stderr = null;
|
||||||
|
get stderr() {
|
||||||
|
return this.#stderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(key = null, {
|
||||||
|
rid,
|
||||||
|
pid,
|
||||||
|
stdinRid,
|
||||||
|
stdoutRid,
|
||||||
|
stderrRid,
|
||||||
|
} = null) {
|
||||||
|
if (key !== illegalConstructorKey) {
|
||||||
|
throw new TypeError("Illegal constructor.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#rid = rid;
|
||||||
|
this.#pid = pid;
|
||||||
|
|
||||||
|
if (stdinRid !== null) {
|
||||||
|
this.#stdinRid = stdinRid;
|
||||||
|
this.#stdin = writableStreamForRid(stdinRid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stdoutRid !== null) {
|
||||||
|
this.#stdoutRid = stdoutRid;
|
||||||
|
this.#stdout = readableStreamForRid(stdoutRid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderrRid !== null) {
|
||||||
|
this.#stderrRid = stderrRid;
|
||||||
|
this.#stderr = readableStreamForRid(stderrRid);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#status = core.opAsync("op_spawn_wait", this.#rid).then((res) => {
|
||||||
|
this.#rid = null;
|
||||||
|
return res;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#status;
|
||||||
|
get status() {
|
||||||
|
return this.#status;
|
||||||
|
}
|
||||||
|
|
||||||
|
async output() {
|
||||||
|
if (this.#rid === null) {
|
||||||
|
throw new TypeError("Child process has already terminated.");
|
||||||
|
}
|
||||||
|
if (this.#stdout?.locked) {
|
||||||
|
throw new TypeError(
|
||||||
|
"Can't collect output because stdout is locked",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.#stderr?.locked) {
|
||||||
|
throw new TypeError(
|
||||||
|
"Can't collect output because stderr is locked",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [status, stdout, stderr] = await PromiseAll([
|
||||||
|
this.#status,
|
||||||
|
collectOutput(this.#stdout),
|
||||||
|
collectOutput(this.#stderr),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
kill(signo) {
|
||||||
|
if (this.#rid === null) {
|
||||||
|
throw new TypeError("Child process has already terminated.");
|
||||||
|
}
|
||||||
|
core.opSync("op_kill", this.#pid, signo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawn(command, options) { // TODO(@crowlKats): more options (like input)?
|
||||||
|
return spawnChild(command, {
|
||||||
|
...options,
|
||||||
|
stdin: "null",
|
||||||
|
stdout: "piped",
|
||||||
|
stderr: "piped",
|
||||||
|
}).output();
|
||||||
|
}
|
||||||
|
|
||||||
|
function spawnSync(command, {
|
||||||
|
args = [],
|
||||||
|
cwd = undefined,
|
||||||
|
clearEnv = false,
|
||||||
|
env = {},
|
||||||
|
uid = undefined,
|
||||||
|
gid = undefined,
|
||||||
|
} = {}) { // TODO(@crowlKats): more options (like input)?
|
||||||
|
return core.opSync("op_spawn_sync", {
|
||||||
|
cmd: pathFromURL(command),
|
||||||
|
args: ArrayPrototypeMap(args, String),
|
||||||
|
cwd: pathFromURL(cwd),
|
||||||
|
clearEnv,
|
||||||
|
env: ObjectEntries(env),
|
||||||
|
uid,
|
||||||
|
gid,
|
||||||
|
stdin: "null",
|
||||||
|
stdout: "piped",
|
||||||
|
stderr: "piped",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__bootstrap.spawn = {
|
||||||
|
Child,
|
||||||
|
spawnChild,
|
||||||
|
spawn,
|
||||||
|
spawnSync,
|
||||||
|
};
|
||||||
|
})(this);
|
|
@ -151,5 +151,9 @@
|
||||||
funlockSync: __bootstrap.fs.funlockSync,
|
funlockSync: __bootstrap.fs.funlockSync,
|
||||||
refTimer: __bootstrap.timers.refTimer,
|
refTimer: __bootstrap.timers.refTimer,
|
||||||
unrefTimer: __bootstrap.timers.unrefTimer,
|
unrefTimer: __bootstrap.timers.unrefTimer,
|
||||||
|
Child: __bootstrap.spawn.Child,
|
||||||
|
spawnChild: __bootstrap.spawn.spawnChild,
|
||||||
|
spawn: __bootstrap.spawn.spawn,
|
||||||
|
spawnSync: __bootstrap.spawn.spawnSync,
|
||||||
};
|
};
|
||||||
})(this);
|
})(this);
|
||||||
|
|
|
@ -134,6 +134,10 @@ where
|
||||||
stream.shutdown().await?;
|
stream.shutdown().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> S {
|
||||||
|
self.stream.into_inner()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -178,6 +182,10 @@ where
|
||||||
.await?;
|
.await?;
|
||||||
Ok((nread, buf))
|
Ok((nread, buf))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_inner(self) -> S {
|
||||||
|
self.stream.into_inner()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ChildStdinResource = WriteOnlyResource<process::ChildStdin>;
|
pub type ChildStdinResource = WriteOnlyResource<process::ChildStdin>;
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub mod permissions;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
pub mod signal;
|
pub mod signal;
|
||||||
|
pub mod spawn;
|
||||||
pub mod tty;
|
pub mod tty;
|
||||||
mod utils;
|
mod utils;
|
||||||
pub mod web_worker;
|
pub mod web_worker;
|
||||||
|
|
263
runtime/ops/spawn.rs
Normal file
263
runtime/ops/spawn.rs
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use super::io::ChildStderrResource;
|
||||||
|
use super::io::ChildStdinResource;
|
||||||
|
use super::io::ChildStdoutResource;
|
||||||
|
use crate::permissions::Permissions;
|
||||||
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::op;
|
||||||
|
use deno_core::Extension;
|
||||||
|
use deno_core::OpState;
|
||||||
|
use deno_core::Resource;
|
||||||
|
use deno_core::ResourceId;
|
||||||
|
use deno_core::ZeroCopyBuf;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::process::ExitStatus;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::prelude::ExitStatusExt;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
|
||||||
|
pub fn init() -> Extension {
|
||||||
|
Extension::builder()
|
||||||
|
.ops(vec![
|
||||||
|
op_spawn_child::decl(),
|
||||||
|
op_spawn_wait::decl(),
|
||||||
|
op_spawn_sync::decl(),
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChildResource(tokio::process::Child);
|
||||||
|
|
||||||
|
impl Resource for ChildResource {
|
||||||
|
fn name(&self) -> Cow<str> {
|
||||||
|
"child".into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum Stdio {
|
||||||
|
Inherit,
|
||||||
|
Piped,
|
||||||
|
Null,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn subprocess_stdio_map(s: &Stdio) -> Result<std::process::Stdio, AnyError> {
|
||||||
|
match s {
|
||||||
|
Stdio::Inherit => Ok(std::process::Stdio::inherit()),
|
||||||
|
Stdio::Piped => Ok(std::process::Stdio::piped()),
|
||||||
|
Stdio::Null => Ok(std::process::Stdio::null()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SpawnArgs {
|
||||||
|
cmd: String,
|
||||||
|
args: Vec<String>,
|
||||||
|
cwd: Option<String>,
|
||||||
|
clear_env: bool,
|
||||||
|
env: Vec<(String, String)>,
|
||||||
|
#[cfg(unix)]
|
||||||
|
gid: Option<u32>,
|
||||||
|
#[cfg(unix)]
|
||||||
|
uid: Option<u32>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
stdio: ChildStdio,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ChildStdio {
|
||||||
|
stdin: Stdio,
|
||||||
|
stdout: Stdio,
|
||||||
|
stderr: Stdio,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ChildStatus {
|
||||||
|
success: bool,
|
||||||
|
code: i32,
|
||||||
|
signal: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<std::process::ExitStatus> for ChildStatus {
|
||||||
|
fn from(status: ExitStatus) -> Self {
|
||||||
|
let code = status.code();
|
||||||
|
#[cfg(unix)]
|
||||||
|
let signal = status.signal();
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let signal = None;
|
||||||
|
|
||||||
|
if let Some(signal) = signal {
|
||||||
|
ChildStatus {
|
||||||
|
success: false,
|
||||||
|
code: 128 + signal,
|
||||||
|
signal: Some(signal),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let code = code.expect("Should have either an exit code or a signal.");
|
||||||
|
|
||||||
|
ChildStatus {
|
||||||
|
success: code == 0,
|
||||||
|
code,
|
||||||
|
signal: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SpawnOutput {
|
||||||
|
status: ChildStatus,
|
||||||
|
stdout: Option<ZeroCopyBuf>,
|
||||||
|
stderr: Option<ZeroCopyBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_command(
|
||||||
|
state: &mut OpState,
|
||||||
|
args: SpawnArgs,
|
||||||
|
) -> Result<std::process::Command, AnyError> {
|
||||||
|
super::check_unstable(state, "Deno.spawn");
|
||||||
|
state.borrow_mut::<Permissions>().run.check(&args.cmd)?;
|
||||||
|
|
||||||
|
let mut command = std::process::Command::new(args.cmd);
|
||||||
|
command.args(args.args);
|
||||||
|
|
||||||
|
if let Some(cwd) = args.cwd {
|
||||||
|
command.current_dir(cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.clear_env {
|
||||||
|
command.env_clear();
|
||||||
|
}
|
||||||
|
command.envs(args.env);
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
if let Some(gid) = args.gid {
|
||||||
|
super::check_unstable(state, "Deno.spawn.gid");
|
||||||
|
command.gid(gid);
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
if let Some(uid) = args.uid {
|
||||||
|
super::check_unstable(state, "Deno.spawn.uid");
|
||||||
|
command.uid(uid);
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
unsafe {
|
||||||
|
command.pre_exec(|| {
|
||||||
|
libc::setgroups(0, std::ptr::null());
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
command.stdin(subprocess_stdio_map(&args.stdio.stdin)?);
|
||||||
|
command.stdout(subprocess_stdio_map(&args.stdio.stdout)?);
|
||||||
|
command.stderr(subprocess_stdio_map(&args.stdio.stderr)?);
|
||||||
|
|
||||||
|
Ok(command)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
struct Child {
|
||||||
|
rid: ResourceId,
|
||||||
|
pid: u32,
|
||||||
|
stdin_rid: Option<ResourceId>,
|
||||||
|
stdout_rid: Option<ResourceId>,
|
||||||
|
stderr_rid: Option<ResourceId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[op]
|
||||||
|
fn op_spawn_child(
|
||||||
|
state: &mut OpState,
|
||||||
|
args: SpawnArgs,
|
||||||
|
) -> Result<Child, AnyError> {
|
||||||
|
let mut command = tokio::process::Command::from(create_command(state, args)?);
|
||||||
|
// TODO(@crowlkats): allow detaching processes.
|
||||||
|
// currently deno will orphan a process when exiting with an error or Deno.exit()
|
||||||
|
// We want to kill child when it's closed
|
||||||
|
command.kill_on_drop(true);
|
||||||
|
|
||||||
|
let mut child = command.spawn()?;
|
||||||
|
let pid = child.id().expect("Process ID should be set.");
|
||||||
|
|
||||||
|
let stdin_rid = child
|
||||||
|
.stdin
|
||||||
|
.take()
|
||||||
|
.map(|stdin| state.resource_table.add(ChildStdinResource::from(stdin)));
|
||||||
|
|
||||||
|
let stdout_rid = child
|
||||||
|
.stdout
|
||||||
|
.take()
|
||||||
|
.map(|stdout| state.resource_table.add(ChildStdoutResource::from(stdout)));
|
||||||
|
|
||||||
|
let stderr_rid = child
|
||||||
|
.stderr
|
||||||
|
.take()
|
||||||
|
.map(|stderr| state.resource_table.add(ChildStderrResource::from(stderr)));
|
||||||
|
|
||||||
|
let child_rid = state.resource_table.add(ChildResource(child));
|
||||||
|
|
||||||
|
Ok(Child {
|
||||||
|
rid: child_rid,
|
||||||
|
pid,
|
||||||
|
stdin_rid,
|
||||||
|
stdout_rid,
|
||||||
|
stderr_rid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[op]
|
||||||
|
async fn op_spawn_wait(
|
||||||
|
state: Rc<RefCell<OpState>>,
|
||||||
|
rid: ResourceId,
|
||||||
|
) -> Result<ChildStatus, AnyError> {
|
||||||
|
let resource = state
|
||||||
|
.borrow_mut()
|
||||||
|
.resource_table
|
||||||
|
.take::<ChildResource>(rid)?;
|
||||||
|
Ok(
|
||||||
|
Rc::try_unwrap(resource)
|
||||||
|
.ok()
|
||||||
|
.unwrap()
|
||||||
|
.0
|
||||||
|
.wait()
|
||||||
|
.await?
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[op]
|
||||||
|
fn op_spawn_sync(
|
||||||
|
state: &mut OpState,
|
||||||
|
args: SpawnArgs,
|
||||||
|
) -> Result<SpawnOutput, AnyError> {
|
||||||
|
let stdout = matches!(args.stdio.stdout, Stdio::Piped);
|
||||||
|
let stderr = matches!(args.stdio.stderr, Stdio::Piped);
|
||||||
|
let output = create_command(state, args)?.output()?;
|
||||||
|
|
||||||
|
Ok(SpawnOutput {
|
||||||
|
status: output.status.into(),
|
||||||
|
stdout: if stdout {
|
||||||
|
Some(output.stdout.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
stderr: if stderr {
|
||||||
|
Some(output.stderr.into())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -427,6 +427,7 @@ impl WebWorker {
|
||||||
.enabled(options.use_deno_namespace),
|
.enabled(options.use_deno_namespace),
|
||||||
ops::permissions::init().enabled(options.use_deno_namespace),
|
ops::permissions::init().enabled(options.use_deno_namespace),
|
||||||
ops::process::init().enabled(options.use_deno_namespace),
|
ops::process::init().enabled(options.use_deno_namespace),
|
||||||
|
ops::spawn::init().enabled(options.use_deno_namespace),
|
||||||
ops::signal::init().enabled(options.use_deno_namespace),
|
ops::signal::init().enabled(options.use_deno_namespace),
|
||||||
ops::tty::init().enabled(options.use_deno_namespace),
|
ops::tty::init().enabled(options.use_deno_namespace),
|
||||||
deno_http::init().enabled(options.use_deno_namespace),
|
deno_http::init().enabled(options.use_deno_namespace),
|
||||||
|
|
|
@ -132,6 +132,7 @@ impl MainWorker {
|
||||||
options.create_web_worker_cb.clone(),
|
options.create_web_worker_cb.clone(),
|
||||||
options.web_worker_preload_module_cb.clone(),
|
options.web_worker_preload_module_cb.clone(),
|
||||||
),
|
),
|
||||||
|
ops::spawn::init(),
|
||||||
ops::fs_events::init(),
|
ops::fs_events::init(),
|
||||||
ops::fs::init(),
|
ops::fs::init(),
|
||||||
ops::io::init(),
|
ops::io::init(),
|
||||||
|
|
Loading…
Reference in a new issue