// Copyright 2018-2024 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 command = new Deno.Command(Deno.execPath(), { cwd, args: ["run", "--allow-read", programFile], stdout: "inherit", stderr: "inherit", }); const child = command.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 command = new Deno.Command(Deno.execPath(), { args: [ "eval", ` const buffer = new Uint8Array(5); await Deno.stdin.read(buffer); if (new TextDecoder().decode(buffer) !== "hello") { throw new Error('Expected \\'hello\\'') } `, ], stdin: "piped", stdout: "null", stderr: "null", }); const child = command.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 commandStdinPiped() { const command = new Deno.Command(Deno.execPath(), { args: ["info"], stdout: "null", stderr: "null", }); const child = command.spawn(); assertThrows(() => child.stdin, TypeError, "stdin is not piped"); assertThrows(() => child.stdout, TypeError, "stdout is not piped"); assertThrows(() => child.stderr, TypeError, "stderr is not piped"); await child.status; }, ); Deno.test( { permissions: { run: true, read: true } }, async function commandStdoutPiped() { const command = new Deno.Command(Deno.execPath(), { args: [ "eval", "await Deno.stdout.write(new TextEncoder().encode('hello'))", ], stderr: "null", stdout: "piped", }); const child = command.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 command = new Deno.Command(Deno.execPath(), { args: [ "eval", "await Deno.stderr.write(new TextEncoder().encode('hello'))", ], stdout: "null", stderr: "piped", }); const child = command.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 command = new Deno.Command(Deno.execPath(), { args: [ "eval", "Deno.stderr.write(new TextEncoder().encode('error\\n')); Deno.stdout.write(new TextEncoder().encode('output\\n'));", ], stdout: "piped", stderr: "piped", }); const child = command.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"; await Deno.writeTextFile(fileName, "hello"); const file = await Deno.open(fileName); const command = new Deno.Command(Deno.execPath(), { args: [ "eval", ` const buffer = new Uint8Array(5); await Deno.stdin.read(buffer); if (new TextDecoder().decode(buffer) !== "hello") { throw new Error('Expected \\'hello\\'') } `, ], stdin: "piped", stdout: "null", stderr: "null", }); const child = command.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 command = new Deno.Command(Deno.execPath(), { args: ["eval", "setTimeout(() => {}, 10000)"], stdout: "null", stderr: "null", }); const child = command.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 } }, // deno lint bug, see https://github.com/denoland/deno_lint/issues/1206 // deno-lint-ignore require-await async function childProcessExplicitResourceManagement() { let dead = undefined; { const command = new Deno.Command(Deno.execPath(), { args: ["eval", "setTimeout(() => {}, 10000)"], stdout: "null", stderr: "null", }); await using child = command.spawn(); child.status.then(({ signal }) => { dead = signal; }); } if (Deno.build.os == "windows") { assertEquals(dead, null); } else { assertEquals(dead, "SIGTERM"); } }, ); Deno.test( { permissions: { run: true, read: true } }, async function childProcessExplicitResourceManagementManualClose() { const command = new Deno.Command(Deno.execPath(), { args: ["eval", "setTimeout(() => {}, 10000)"], stdout: "null", stderr: "null", }); await using child = command.spawn(); child.kill("SIGTERM"); await child.status; }, ); Deno.test( { permissions: { run: true, read: true } }, async function commandKillFailed() { const command = new Deno.Command(Deno.execPath(), { args: ["eval", "setTimeout(() => {}, 5000)"], stdout: "null", stderr: "null", }); const child = command.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 command = new Deno.Command(Deno.execPath(), { args: ["eval", "setTimeout(() => {}, 10000)"], stdout: "null", stderr: "null", }); const child = command.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 command = new Deno.Command(Deno.execPath(), { args: [ "eval", "setTimeout(console.log, 1e8)", ], signal: ac.signal, stdout: "null", stderr: "null", }); const child = command.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 } }, function cwdNotFound() { assertThrows( () => new Deno.Command(Deno.execPath(), { cwd: Deno.cwd() + "/non-existent-directory", }).output(), Deno.errors.NotFound, "No such cwd", ); }); Deno.test( { permissions: { run: true, read: true } }, function cwdNotDirectory() { assertThrows( () => new Deno.Command(Deno.execPath(), { cwd: Deno.execPath(), }).output(), Deno.errors.NotFound, "cwd is not a directory", ); }, ); 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( // FIXME(bartlomieju): this test is very flaky on CI, fix it { ignore: true, 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 command = await new Deno.Command(Deno.execPath(), { cwd: Deno.args[0], stdout: "piped", args: ["run", "-A", "--unstable", Deno.args[1]], }); const child = command.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 infinite 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 commandWithPrototypePollution() { const originalThen = Promise.prototype.then; const originalSymbolIterator = Array.prototype[Symbol.iterator]; try { Promise.prototype.then = Array.prototype[Symbol.iterator] = () => { throw new Error(); }; await new Deno.Command(Deno.execPath(), { args: ["eval", "console.log('hello world')"], }).output(); } finally { Promise.prototype.then = originalThen; Array.prototype[Symbol.iterator] = originalSymbolIterator; } }, ); Deno.test( { permissions: { run: true, read: true } }, async function commandKillAfterStatus() { const command = new Deno.Command(Deno.execPath(), { args: ["help"], stdout: "null", stderr: "null", }); const child = command.spawn(); await child.status; assertThrows( () => child.kill(), TypeError, "Child process has already terminated.", ); }, ); Deno.test( "process that fails to spawn, prints its name in error", async () => { assertThrows( () => new Deno.Command("doesntexist").outputSync(), Error, "Failed to spawn 'doesntexist'", ); await assertRejects( async () => await new Deno.Command("doesntexist").output(), Error, "Failed to spawn 'doesntexist'", ); }, );