Creating a subprocess

Concepts

  • Deno is capable of spawning a subprocess via Deno.run.
  • --allow-run permission is required to spawn a subprocess.
  • Spawned subprocesses do not run in a security sandbox.
  • Communicate with the subprocess via the stdin, stdout and stderr streams.
  • Use a specific shell by providing its path/name and its string input switch, e.g. Deno.run({cmd: ["bash", "-c", "ls -la"]});

Simple example

This example is the equivalent of running 'echo hello' from the command line.

  1. /**
  2. * subprocess_simple.ts
  3. */
  4. // define command used to create the subprocess
  5. const cmd = ["echo", "hello"];
  6. // create subprocess
  7. const p = Deno.run({ cmd });
  8. // await its completion
  9. await p.status();

Note: If using Windows, the command above would need to be written differently because echo is not an executable binary (rather, it is a built-in shell command):

  1. // define command used to create the subprocess
  2. const cmd = ["cmd", "/c", "echo hello"];

Run it:

  1. $ deno run --allow-run ./subprocess_simple.ts
  2. hello

Security

The --allow-run permission is required for creation of a subprocess. Be aware that subprocesses are not run in a Deno sandbox and therefore have the same permissions as if you were to run the command from the command line yourself.

Communicating with subprocesses

By default when you use Deno.run() the subprocess inherits stdin, stdout and stderr of the parent process. If you want to communicate with started subprocess you can use "piped" option.

  1. /**
  2. * subprocess.ts
  3. */
  4. const fileNames = Deno.args;
  5. const p = Deno.run({
  6. cmd: [
  7. "deno",
  8. "run",
  9. "--allow-read",
  10. "https://deno.land/std@$STD_VERSION/examples/cat.ts",
  11. ...fileNames,
  12. ],
  13. stdout: "piped",
  14. stderr: "piped",
  15. });
  16. const { code } = await p.status();
  17. // Reading the outputs closes their pipes
  18. const rawOutput = await p.output();
  19. const rawError = await p.stderrOutput();
  20. if (code === 0) {
  21. await Deno.stdout.write(rawOutput);
  22. } else {
  23. const errorString = new TextDecoder().decode(rawError);
  24. console.log(errorString);
  25. }
  26. Deno.exit(code);

When you run it:

  1. $ deno run --allow-run ./subprocess.ts <somefile>
  2. [file content]
  3. $ deno run --allow-run ./subprocess.ts non_existent_file.md
  4. Uncaught NotFound: No such file or directory (os error 2)
  5. at DenoError (deno/js/errors.ts:22:5)
  6. at maybeError (deno/js/errors.ts:41:12)
  7. at handleAsyncMsgFromRust (deno/js/dispatch.ts:27:17)

Piping to files

This example is the equivalent of running yes &> ./process_output in bash.

  1. /**
  2. * subprocess_piping_to_file.ts
  3. */
  4. import {
  5. readableStreamFromReader,
  6. writableStreamFromWriter,
  7. } from "https://deno.land/std@$STD_VERSION/streams/conversion.ts";
  8. import { mergeReadableStreams } from "https://deno.land/std@$STD_VERSION/streams/merge.ts";
  9. // create the file to attach the process to
  10. const file = await Deno.open("./process_output.txt", {
  11. read: true,
  12. write: true,
  13. create: true,
  14. });
  15. const fileWriter = await writableStreamFromWriter(file);
  16. // start the process
  17. const process = Deno.run({
  18. cmd: ["yes"],
  19. stdout: "piped",
  20. stderr: "piped",
  21. });
  22. // example of combining stdout and stderr while sending to a file
  23. const stdout = readableStreamFromReader(process.stdout);
  24. const stderr = readableStreamFromReader(process.stderr);
  25. const joined = mergeReadableStreams(stdout, stderr);
  26. // returns a promise that resolves when the process is killed/closed
  27. joined.pipeTo(fileWriter).then(() => console.log("pipe join done"));
  28. // manually stop process "yes" will never end on its own
  29. setTimeout(async () => {
  30. process.kill("SIGINT");
  31. }, 100);

Run it:

  1. $ deno run --allow-run ./subprocess_piping_to_file.ts