node-process-utils
v1.0.1
Published
The node-process-utils package runs child processes with bounded time and output, and stops the process trees it owns: graceful then forced process-group shutdown on POSIX, and taskkill on Windows. Exit codes are returned rather than thrown, so callers de
Maintainers
Readme
Node Process Utils
The node-process-utils package runs child processes with bounded time and output, and stops the process trees it owns. It covers the mechanics every tool that spawns processes needs: no shell, a cap on collected output, timeouts and abort signals, and shutdown of the child together with its descendants.
Exit codes are returned rather than thrown, so callers decide which codes are acceptable and process output never ends up in an error message. Policies that depend on the caller, such as Git environment variables, credential handling or redacting diagnostics, stay in the caller's code.
The package runs on Node.js 24 and later.
Getting Started
Install the package:
npm i -S node-process-utilsExamples
Run a command and check its exit code:
import { runProcess } from 'node-process-utils';
const { exitCode, stdout } = await runProcess({
command: 'git',
args: ['rev-parse', 'HEAD'],
cwd: '/path/to/repository',
timeoutSeconds: 30,
});
if (exitCode !== 0) {
throw new Error(`git rev-parse failed with exit code ${exitCode}`);
}
console.log(stdout.trim()); // '4c0e2814b449d5c1cee5012f7f46fb9fb1c21df0'Stop the run when a CLI receives Ctrl+C. The child leads its own process group, so the terminal's signal reaches only your process:
import { runProcess } from 'node-process-utils';
const controller = new AbortController();
process.once('SIGINT', () => controller.abort(new Error('Interrupted')));
process.once('SIGTERM', () => controller.abort(new Error('Terminated')));
await runProcess({ command: 'npm', args: ['test'], signal: controller.signal });Start a long-running process and stop it, with its descendants, when you are done:
import { startProcess, stopProcessTree } from 'node-process-utils';
const server = startProcess('node', ['server.js'], { env: { ...process.env, PORT: '3000' } });
// ...
await stopProcessTree(server);API Reference
Runs one command to completion, without a shell and, on POSIX, as the leader of its own process group. It resolves with an IProcessResult: the exit code, the terminating signal, stdout and stderr decoded as UTF-8, and the duration in milliseconds.
- Exit codes: every exit resolves, whatever its code.
- Input:
inputis written to stdin, which is then closed. Input that the child never reads, because it exits first, is discarded without an error. - Output: stdout and stderr together can hold at most
maxOutputBytesbytes (DEFAULT_MAX_OUTPUT_BYTES, 1 MiB, by default). - Descendants: when the child exits, descendants left in its process group are stopped. If a descendant that couldn't be reached keeps stdout or stderr open for longer than the grace period, the call rejects with
PROCESS_TREE_SURVIVEDinstead of hanging. Examples are a descendant that left the group withsetsid, or one orphaned on Windows. - Rejections: the call rejects only in these cases, and stops the process tree first, so the rejection can arrive up to the grace period plus two seconds after the trigger:
PROCESS_START_FAILEDwhen the command can't start, with the underlying error ascausePROCESS_TIMED_OUTwhentimeoutSecondspassesPROCESS_OUTPUT_TOO_LARGEwhen the output exceedsmaxOutputBytes- the signal's reason when
signalaborts PROCESS_TREE_SURVIVEDwhen the process tree can't be stopped
- Ctrl+C: because the child leads its own group, a terminal's Ctrl+C reaches only the parent. CLIs should abort the signal on
SIGINTandSIGTERM, as in the example above.
import { runProcess } from 'node-process-utils';
await runProcess({
command: 'node',
args: ['-e', 'process.stdin.pipe(process.stdout)'],
input: 'Hello World!',
});
// {
// exitCode: 0,
// signalCode: null,
// stdout: 'Hello World!',
// stderr: '',
// durationMs: 41
// }Starts a long-running child that you own and returns its ChildProcess synchronously. It runs without a shell, with stdio set to 'inherit' by default, and, on POSIX, as the leader of its own process group, so stopProcessTree can stop it together with its descendants. A command that can't start emits an 'error' event on the returned child.
import { startProcess } from 'node-process-utils';
const watcher = startProcess('npm', ['run', 'watch'], { cwd: 'packages/app' });
watcher.once('exit', (code) => console.log(`watch exited with ${code}`));Stops a child together with its descendants and resolves once they are gone. It resolves immediately for a child without a pid.
- POSIX: it sends
SIGTERMto the child's process group, waits up togracePeriodSeconds(DEFAULT_GRACE_PERIOD_SECONDS, 5, by default), then sendsSIGKILLand waits up to two more seconds. It resolves only once the child has exited and its group is gone. - Precondition: it is designed for children from
startProcessorrunProcess, which lead their own process group. For any other child, it stops the child alone. - Windows: it runs
taskkill /PID <pid> /T /Fand waits up to 10 seconds for the exit.taskkillfinds descendants by walking down from a root process that is still running, so when the root has already exited, the call resolves and orphaned descendants may remain. - Limits: descendants that start their own session or process group, for example with
setsid, aren't stopped on POSIX, and orphaned descendants aren't stopped on Windows. - Failure: it rejects with
PROCESS_TREE_SURVIVEDwhen anything it can observe outlives the forced stop.
import { startProcess, stopProcessTree } from 'node-process-utils';
const child = startProcess('node', ['server.js']);
await stopProcessTree(child, 10);Constants
The number of bytes stdout and stderr can hold together when runProcess gets no maxOutputBytes.
const DEFAULT_MAX_OUTPUT_BYTES = 1048576; // 1 MiBThe number of seconds stopProcessTree waits after SIGTERM before it sends SIGKILL. runProcess uses it too.
const DEFAULT_GRACE_PERIOD_SECONDS = 5;Types
The options accepted by runProcess.
type IRunProcessOptions = {
// the executable to run, without a shell
command: string;
// the arguments passed to the executable
args: string[];
// the working directory. Defaults to the current one
cwd?: string;
// the environment variables. Defaults to process.env
env?: NodeJS.ProcessEnv;
// the data written to the process's stdin before it is closed
input?: string | Uint8Array;
// the seconds after which the process tree is stopped and the call rejects
timeoutSeconds?: number;
// the bytes stdout and stderr can hold together. Defaults to DEFAULT_MAX_OUTPUT_BYTES
maxOutputBytes?: number;
// stops the process tree and rejects with the signal's reason when it aborts
signal?: AbortSignal;
};The outcome of a process that ran to completion.
type IProcessResult = {
// the exit code, or null when a signal ended the process
exitCode: number | null;
// the signal that ended the process, or null when it exited on its own
signalCode: NodeJS.Signals | null;
// the output, decoded as UTF-8
stdout: string;
stderr: string;
// the milliseconds between the start and the end of the run
durationMs: number;
};The options accepted by startProcess.
type IStartProcessOptions = {
// the working directory. Defaults to the current one
cwd?: string;
// the environment variables. Defaults to process.env
env?: NodeJS.ProcessEnv;
// the stdio configuration. Defaults to 'inherit'
stdio?: StdioOptions;
};Built With
- TypeScript
node:child_process
Running the Tests
The integration tests start real processes and run on POSIX systems; the process-group tests are skipped on Windows.
# unit and integration tests
npm run test
# tests, type checking, linting and formatting
npm run verify
# unit tests
npm run test:unit
# integration tests
npm run test:integration