observable-process

High-level support for running, observing, and interacting with child processes in Node.js

Usage no npm install needed!

<script type="module">
  import observableProcess from 'https://cdn.skypack.dev/observable-process';
</script>

README

ObservableProcess

CircleCI Coverage Status install size Language grade: JavaScript

ObservableProcess enhances the slightly too low-level Node.JS ChildProcess model with functionality to observe the behavior of processes more conveniently. In particular:

  • easy access to the accumulated content of stdout and stderr
  • await content in stdout and stderr
  • combines stdout and stderr into a new output stream
  • await the process end
  • easier access to the process exit code
  • signals whether the process ended naturally or was manually terminated

ObservableProcess is for short-lived processes, for example when testing the terminal output of applications. Since ObservableProcess stores all output from the child process in memory, executing long-running processes that produce lots of output through ObservableProcess will cause high memory consumption.

Setup

Add this library to your code base:

$ npm install observable-process

Load this library into your JavaScript code:

const observableProcess = require("observable-process")

or

import * as observableProcess from "observable-process"

Starting processes

The best way to provide the command to start is in the form of an argv array:

const observable = observableProcess.start(["node", "server.js"])

You can also provide the full command line to run as a string:

const observable = observableProcess.start("node server.js")

By default, the process runs in the current directory. To set a different working directory for the subprocess:

const observable = observableProcess.start("node server.js", { cwd: "~/tmp" })

You can provide custom environment variables for the process:

const observable = observableProcess.start("node server.js", {
  env: {
    foo: "bar",
    PATH: process.env.PATH,
  },
})

Without an env parameter, ObservableProcess uses the environment variables from the parent process.

Reading output

The stdout and stderr variables of an ObservableProcess behave like normal readable streams:

// normal consumption of data from STDOUT via the event stream
observable.stdout.on("data", function () {
  // ...
})

They also provide extra functionality to access and search their aggregated content. To get all content from STDOUT as a string:

const text = observable.stdout.fullText()

To wait for text to appear in STDOUT:

const match = await observable.stdout.waitForText("server is online")
// => "server is online"

To wait for a regular expression on STDOUT:

const match = await observable.stdout.waitForRegex(/running at port \d+/)
// => "running at port 3000"

Comparable functionality is available for stderr. ObservableProcess also provides a new output stream with the combined content of STDOUT and STDERR:

observable.output.on("data", function (data) {
  // ...
})
const text = observable.output.fullText()
await observable.output.waitForText("server is online")
const port = await observable.output.waitForRegex(/running at port \d+./)

You also get a copy of the process output after it ended (see below).

Sending input to the process

ObservableProcess exposes the stdin stream of its underlying ChildProcess:

observable.stdin.write("my input\n")
observable.stdin.end()

Get the process id

observable.pid()

Stop the process

Wait until the process ends naturally:

const result = await observable.waitForEnd()
assert.equal(result, {
  status: "finished",
  exitCode: 0,
  stdText: "... content from STDOUT ...",
  errText: "... content from STDERR ...",
  combinedText: "... content from both STDOUT and STDERR ...",
})

Manually stop the process:

const result = await observable.kill()
assert.equal(result, {
  status: "killed",
  stdText: "... content from STDOUT ...",
  errText: "... content from STDERR ...",
  combinedText: "... content from both STDOUT and STDERR ...",
})

Related libraries

  • nexpect: Allows to define expectations on command output, and send it input, but doesn't allow to add more listeners to existing long-running processes, which makes declarative testing hard.

Development

If you want to hack on ObservableProcess:

  • run all tests: make test
  • run automated code repair: make fix
  • see all make commands: make help

To deploy a new version:

  • update the version in package.json and commit to master
  • run npm publish