Skip to main content
jg-packages

Search

    Child processes

    jg-process

    Run and stop child processes without a shell: timeouts, output limits, abort signals, and whole process trees.

    v1.3.0Node.jsESMMIT

    npm install jg-process

    Usage

    Run a command to completion and read its output. The run is bounded by a timeout and an output limit, and both stop the whole process tree:

    ts
    import { runProcess } from 'jg-process';
    
    const { exitCode, stdout, stderr, durationMs } = await runProcess({
      command: 'git',
      args: ['status', '--porcelain'],
      cwd: repositoryDirectory,
      timeoutSeconds: 30,
      signal: AbortSignal.timeout(60_000),
    });

    A non-zero exit code resolves, so callers decide what counts as failure. A timeout, too much output, or a failure to start rejects with PROCESS_TIMED_OUT, PROCESS_OUTPUT_TOO_LARGE, or PROCESS_START_FAILED.

    Start a long-running process that you own, and stop it together with everything it started:

    ts
    import { startProcess, stopProcessTree } from 'jg-process';
    
    const server = startProcess({ command: 'node', args: ['server.js'], cwd: 'apps/api' });
    process.once('SIGINT', () => void stopProcessTree(server));

    Start a server and wait until it answers, or stop everything it started when it doesn’t:

    ts
    import { startProcessAndWait } from 'jg-process';
    
    const api = await startProcessAndWait(
      { command: process.execPath, args: ['server.js'], cwd: 'apps/api' },
      { readyWhen: { url: 'http://127.0.0.1:3000/health' }, timeoutSeconds: 60 },
    );

    Run a package manager without a shell on every platform, including Windows, where its .cmd shim can’t be spawned directly:

    ts
    import { resolvePackageManagerCommand, runProcess } from 'jg-process';
    
    await runProcess({ ...resolvePackageManagerCommand(['run', 'build']), timeoutSeconds: 600 });

    Reserve a free port before handing it to a server, keeping the preferred one when it’s free:

    ts
    import { reservePort } from 'jg-process';
    
    const reservation = await reservePort({ preferredPort: 3000 }); // 3000, or another free port
    await reservation.release(); // right before the server binds it

    Generated from TSDoc

    API reference

    Every export of [email protected], 25 in all, with its signature, its errors, and an example.

    Functions

    reservePort

    function

    Reserves a free TCP port by listening on it, so nothing else can take it until it's released, such as before passing it to a dev server or a test fixture. The preferred port is kept when it's free, and any free port is reserved when it's taken. Connections to the reserved port are closed at once, and the reservation doesn't keep the process running.

    A child process that needs the port itself can't bind it while it's reserved, so release it right before starting the child. Another process can take the port in between.

    Signature
    reservePort(options?: IReservePortOptions): Promise<IPortReservation>

    Parameters

    options?IReservePortOptions

    The preferred port and the address. The address defaults to 127.0.0.1.

    Defaults to {}.

    Returns

    Promise<IPortReservation>

    A promise that resolves to the reservation.

    Throws

    • INVALID_PORTThe preferred port must be an integer from 1 to 65535.
    • PORT_RESERVATION_FAILEDThe port couldn't be reserved.
    Example
    import { reservePort } from 'jg-process';
    
    const reservation = await reservePort({ preferredPort: 3000 });
    console.log(`The port is ${reservation.port}`); // 3000, or another free port when it's taken
    await reservation.release();

    resolvePackageManagerCommand

    function

    Builds the command that runs a package manager with the given arguments, without a shell, so it can be passed to runProcess or startProcess as it is on Windows, macOS, and Linux. It looks, in order, at: 1. npm_execpath, which a package manager sets for the scripts it runs, when it names the requested package manager, or any one when none is requested 2. the package manager installed in the project, resolved from cwd 3. PATH: an executable on POSIX, and on Windows an .exe, or a .cmd or .ps1 shim whose script it runs through Node.js, because Node.js refuses to spawn a .cmd file without a shell

    A JavaScript entry, such as pnpm.cjs, runs through the current Node.js. Without a requested package manager or npm_execpath, there's nothing to look for, so it throws.

    Signature
    resolvePackageManagerCommand(
      args: string[],
      options?: IResolvePackageManagerOptions,
    ): IPackageManagerCommand

    Parameters

    argsstring[]

    The arguments for the package manager, such as ['run', 'build'].

    options?IResolvePackageManagerOptions

    The package manager, the environment, and the directory to resolve from.

    Defaults to {}.

    Returns

    IPackageManagerCommand

    The command and its arguments.

    Throws

    • UNSUPPORTED_PACKAGE_MANAGERThe package manager must be pnpm, npm, yarn, or bun.
    • PACKAGE_MANAGER_NOT_FOUNDThe package manager's executable couldn't be found.
    Example
    import { resolvePackageManagerCommand, runProcess } from 'jg-process';
    
    const command = resolvePackageManagerCommand(['run', 'build'], { packageManager: 'pnpm' });
    const { exitCode } = await runProcess({ ...command, timeoutSeconds: 600 });

    runProcess

    function

    Runs a command to completion and resolves with its exit code, signal and output. It runs without a shell and, on POSIX, as the leader of its own process group.

    Every exit resolves, whatever its code: callers decide which codes are acceptable, so process output never ends up in an error message. The call rejects only when the command can't start, the timeout passes, the output exceeds maxOutputBytes, or the signal aborts; in those cases the process tree is stopped first, so the rejection can arrive up to the grace period plus two seconds after the trigger.

    Input that the child never reads is discarded without an error. When the child exits, descendants left in its process group are stopped. If one that couldn't be reached, such as a descendant that left the group with setsid or one orphaned on Windows, keeps stdout or stderr open for longer than the grace period, the call rejects instead of hanging.

    Because the child leads its own group, a terminal's Ctrl+C reaches only the parent: CLIs should abort the signal when they receive SIGINT or SIGTERM.

    Signature
    runProcess(options: IRunProcessOptions): Promise<IProcessResult>

    Parameters

    optionsIRunProcessOptions

    The command, its arguments, and the limits of the run.

    Returns

    Promise<IProcessResult>

    A promise that resolves with the exit code, the signal and the output.

    Throws

    • PROCESS_START_FAILEDUnable to start the command
    • PROCESS_TIMED_OUTThe process didn't finish within the timeout
    • PROCESS_OUTPUT_TOO_LARGEThe process wrote more output than allowed
    • PROCESS_TREE_SURVIVEDThe process tree is still running after it was force-stopped
    • ErrorThe signal's reason when the signal aborts.
    Example
    import { runProcess } from 'jg-process';
    
    const { exitCode, stdout } = await runProcess({ command: 'git', args: ['status', '--porcelain'], timeoutSeconds: 30 });
    if (exitCode !== 0) {
      // handle the failure
    }

    startProcess

    function

    Starts a long-running child that the caller owns, without a shell and, on POSIX, as the leader of its own process group, so stopProcessTree can later stop it together with its descendants. Because the child leads its own group, a terminal's Ctrl+C reaches only the parent: CLIs should stop the child when they receive SIGINT or SIGTERM. A command that can't start emits an 'error' event on the returned child.

    Signature
    startProcess(
      command: string,
      args: string[],
      options?: IStartProcessOptions,
    ): ChildProcess

    Parameters

    commandstring

    The executable to run.

    argsstring[]

    The arguments passed to the executable.

    options?IStartProcessOptions

    The working directory, the environment and the stdio configuration ('inherit' by default).

    Returns

    ChildProcess

    The started child process.

    Starts a long-running child from a command object, the form runProcess takes, with the same behavior as the positional form.

    Signature
    startProcess(command: IStartProcessCommand): ChildProcess

    Parameters

    commandIStartProcessCommand

    The executable, its arguments, and the working directory, environment, and stdio.

    Returns

    ChildProcess

    The started child process.

    Example 1
    import { startProcess, stopProcessTree } from 'jg-process';
    
    const server = startProcess('node', ['server.js'], { stdio: 'inherit' });
    process.once('SIGINT', () => void stopProcessTree(server));
    Example 2
    import { startProcess } from 'jg-process';
    
    const server = startProcess({ command: 'node', args: ['server.js'], cwd: 'apps/api' });

    startProcessAndWait

    function

    Starts a long-running process, such as a dev server, with startProcess, and resolves with it once it's ready: its port accepts connections, its URL answers, or a probe returns a value other than null, undefined, or false. When it exits, fails to start, isn't ready in time, or the signal aborts, its whole process tree is stopped before the call rejects, so nothing is left running.

    Signature
    startProcessAndWait(
      command: IStartProcessCommand,
      options: IStartProcessAndWaitOptions,
    ): Promise<ChildProcess>

    Parameters

    commandIStartProcessCommand

    The command, its arguments, and the working directory, environment, and stdio.

    optionsIStartProcessAndWaitOptions

    What ready means, the deadline and pace, the signal, and the grace period stopProcessTree gets on failure.

    Returns

    Promise<ChildProcess>

    A promise that resolves to the ready child process, which the caller owns.

    Throws

    • INVALID_PORTThe port must be an integer from 1 to 65535.
    • INVALID_READINESS_OPTIONSThe readiness options need finite, positive seconds.
    • PROCESS_NOT_READYThe process wasn't ready in time.
    • PROCESS_EXITED_BEFORE_READYThe process exited before it was ready.
    • PROCESS_START_FAILEDUnable to start the process.
    • PROCESS_TREE_SURVIVEDThe process tree is still running after it was force-stopped.
    • ErrorThe signal's reason when the signal aborts.
    Example
    import { startProcessAndWait, stopProcessTree } from 'jg-process';
    
    const server = await startProcessAndWait(
      { command: process.execPath, args: ['server.js'], cwd: 'apps/api' },
      { readyWhen: { url: 'http://127.0.0.1:3000/health' }, timeoutSeconds: 60 },
    );
    await stopProcessTree(server);

    stopProcessTree

    function

    Stops a child started by startProcess or runProcess together with its descendants, and resolves once they are gone. It resolves immediately for a child without a pid.

    On POSIX, it sends SIGTERM to the child's process group, waits up to the grace period, then sends SIGKILL and waits up to two more seconds. It is designed for children that lead their own process group, as startProcess and runProcess start them; for any other child, it signals the child alone. Descendants that start their own session or process group, for example with setsid, aren't stopped.

    On Windows, it runs taskkill /T /F, which reaches only descendants of a root process that is still running: when the root has already exited, it resolves and orphaned descendants may remain.

    Signature
    stopProcessTree(child: ChildProcess, gracePeriodSeconds?: number): Promise<void>

    Parameters

    childChildProcess

    The child process to stop.

    gracePeriodSeconds?number

    No description.

    Defaults to DEFAULT_GRACE_PERIOD_SECONDS.

    Returns

    Promise<void>

    A promise that resolves once the process tree is gone.

    Throws

    • PROCESS_TREE_SURVIVEDThe process tree is still running after it was force-stopped.
    Example
    import { startProcess, stopProcessTree } from 'jg-process';
    
    const server = startProcess('node', ['server.js']);
    await stopProcessTree(server, 10); // SIGTERM, then SIGKILL after 10 seconds

    waitForPort

    function

    Waits until a TCP port accepts connections, such as a database or a dev server that a script started, probing every intervalSeconds until timeoutSeconds pass. With a child, its exit or failure to start ends the wait at once, since it can no longer become ready.

    Signature
    waitForPort(port: number, options: IWaitForPortOptions): Promise<void>

    Parameters

    portnumber

    The port, from 1 to 65535.

    optionsIWaitForPortOptions

    The address, the deadline and pace, the signal, and the child to watch.

    Returns

    Promise<void>

    A promise that resolves once a connection succeeds.

    Throws

    • INVALID_PORTThe port must be an integer from 1 to 65535.
    • INVALID_READINESS_OPTIONSThe readiness options need finite, positive seconds.
    • PROCESS_NOT_READYThe process wasn't ready in time.
    • PROCESS_EXITED_BEFORE_READYThe process exited before it was ready.
    • PROCESS_START_FAILEDUnable to start the process.
    • ErrorThe signal's reason when the signal aborts.
    Example
    import { waitForPort } from 'jg-process';
    
    await waitForPort(5432, { timeoutSeconds: 30 }); // PostgreSQL accepts connections

    waitForUrl

    function

    Waits until a URL answers in a way that means ready, by default with a 2xx status, such as a health endpoint. Redirects aren't followed, a network error means not ready yet, and every response's body is discarded. With a child, its exit or failure to start ends the wait at once.

    Signature
    waitForUrl(url: string | URL, options: IWaitForUrlOptions): Promise<void>

    Parameters

    urlstring | URL

    The URL.

    optionsIWaitForUrlOptions

    The deadline and pace, the request timeout, the predicate, the signal, and the child to watch.

    Returns

    Promise<void>

    A promise that resolves once a response means ready.

    Throws

    • INVALID_READINESS_OPTIONSThe readiness options need finite, positive seconds.
    • PROCESS_NOT_READYThe process wasn't ready in time.
    • PROCESS_EXITED_BEFORE_READYThe process exited before it was ready.
    • PROCESS_START_FAILEDUnable to start the process.
    • ErrorThe signal's reason when the signal aborts.
    Example
    import { waitForUrl } from 'jg-process';
    
    await waitForUrl('http://127.0.0.1:3000/health', { timeoutSeconds: 60 });

    Constants

    DEFAULT_GRACE_PERIOD_SECONDS

    const

    the seconds stopProcessTree waits after SIGTERM before it sends SIGKILL

    Declaration
    const DEFAULT_GRACE_PERIOD_SECONDS: 5

    DEFAULT_MAX_OUTPUT_BYTES

    const

    the bytes stdout and stderr can hold together when runProcess gets no maxOutputBytes (1 MiB)

    Declaration
    const DEFAULT_MAX_OUTPUT_BYTES: 1048576

    ERRORS

    const

    the codes for every error thrown by the package

    Declaration
    const ERRORS: { PROCESS_START_FAILED: "PROCESS_START_FAILED"; PROCESS_TIMED_OUT: "PROCESS_TIMED_OUT"; PROCESS_OUTPUT_TOO_LARGE: "PROCESS_OUTPUT_TOO_LARGE"; PROCESS_TREE_SURVIVED: "PROCESS_TREE_SURVIVED"; INVALID_PORT: "INVALID_PORT"; PORT_RESERVATION_FAILED: "PORT_RESERVATION_FAILED"; UNSUPPORTED_PACKAGE_MANAGER: "UNSUPPORTED_PACKAGE_MANAGER"; PACKAGE_MANAGER_NOT_FOUND: "PACKAGE_MANAGER_NOT_FOUND"; INVALID_READINESS_OPTIONS: "INVALID_READINESS_OPTIONS"; PROCESS_NOT_READY: "PROCESS_NOT_READY"; PROCESS_EXITED_BEFORE_READY: "PROCESS_EXITED_BEFORE_READY" }

    Types

    IPackageManager

    type

    the package managers resolvePackageManagerCommand finds

    Declaration
    type IPackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';

    IPackageManagerCommand

    type

    a command to spawn without a shell, which runProcess and startProcess take as they are

    Declaration
    type IPackageManagerCommand = {
        command: string;
        args: string[];
    };

    Members

    • commandproperty
      string
    • argsproperty
      string[]

    IPortReservation

    type

    a port held until it's released, so nothing else can take it meanwhile

    Declaration
    type IPortReservation = {
        port: number;
        host: string;
        release: () => Promise<void>;
    };

    Members

    • portproperty
      number

      the reserved port

    • hostproperty
      string

      the address it's reserved on

    • releaseproperty
      () => Promise<void>

      stops holding the port; calling it again does nothing

    IProcessResult

    type

    the outcome of a process that ran to completion

    Declaration
    type IProcessResult = {
        exitCode: number | null;
        signalCode: NodeJS.Signals | null;
        stdout: string;
        stderr: string;
        durationMs: number;
    };

    Members

    • exitCodeproperty
      number | null

      the exit code, or null when a signal ended the process

    • signalCodeproperty
      NodeJS.Signals | null

      the signal that ended the process, or null when it exited on its own

    • stdoutproperty
      string

      the output, decoded as UTF-8

    • stderrproperty
      string
    • durationMsproperty
      number

      the milliseconds between the start and the end of the run

    IReadinessWaitOptions

    type

    what a readiness wait shares: its deadline, its pace, and what ends it early

    Declaration
    type IReadinessWaitOptions = {
        timeoutSeconds: number;
        intervalSeconds?: number | undefined;
        signal?: AbortSignal | undefined;
        child?: ChildProcess | undefined;
    };

    Members

    • timeoutSecondsproperty
      number

      the most seconds the whole wait can take

    • intervalSecondsproperty
      number

      the seconds between probes. Defaults to 0.25

    • signalproperty
      AbortSignal

      stops the wait and rejects with the signal's reason when it aborts

    • childproperty
      ChildProcess

      a process whose exit or start failure ends the wait, because it can no longer become ready

    IReadyWhen

    type

    what startProcessAndWait waits for: a port, a URL, or a probe returning other than null, undefined, or false

    Declaration
    type IReadyWhen = {
        port: number;
        host?: string | undefined;
    } | {
        url: string | URL;
        isReady?: IWaitForUrlOptions['isReady'];
    } | {
        probe: (signal: AbortSignal) => unknown;
    };

    IReservePortOptions

    type

    the options reservePort takes

    Declaration
    type IReservePortOptions = {
        preferredPort?: number | undefined;
        host?: string | undefined;
    };

    Members

    • preferredPortproperty
      number

      the port to try first, from 1 to 65535; another free one is reserved when it's taken

    • hostproperty
      string

      the address to listen on. Defaults to 127.0.0.1

    IResolvePackageManagerOptions

    type

    the options resolvePackageManagerCommand takes

    Declaration
    type IResolvePackageManagerOptions = {
        packageManager?: IPackageManager | undefined;
        env?: NodeJS.ProcessEnv | undefined;
        cwd?: string | undefined;
    };

    Members

    • packageManagerproperty
      IPackageManager

      The package manager to run. Defaults to the one running the current package script, which npm_execpath names; without one, a package manager has to be named.

    • envproperty
      NodeJS.ProcessEnv

      the environment that holds npm_execpath and PATH. Defaults to process.env

    • cwdproperty
      string

      the directory a local install is resolved from. Defaults to the current one

    IRunProcessOptions

    type

    the options accepted by runProcess

    Declaration
    type IRunProcessOptions = {
        command: string;
        args: string[];
        cwd?: string;
        env?: NodeJS.ProcessEnv;
        input?: string | Uint8Array;
        timeoutSeconds?: number;
        maxOutputBytes?: number;
        signal?: AbortSignal;
    };

    Members

    • commandproperty
      string

      the executable to run, without a shell

    • argsproperty
      string[]

      the arguments passed to the executable

    • cwdproperty
      string

      the working directory. Defaults to the current one

    • envproperty
      NodeJS.ProcessEnv

      the environment variables. Defaults to process.env

    • inputproperty
      string | Uint8Array

      the data written to the process's stdin before it is closed

    • timeoutSecondsproperty
      number

      the seconds after which the process tree is stopped and the call rejects

    • maxOutputBytesproperty
      number

      the bytes stdout and stderr can hold together. Defaults to DEFAULT_MAX_OUTPUT_BYTES

    • signalproperty
      AbortSignal

      stops the process tree and rejects with the signal's reason when it aborts

    IStartProcessAndWaitOptions

    type

    the options startProcessAndWait takes

    Declaration
    type IStartProcessAndWaitOptions = Omit<IReadinessWaitOptions, 'child'> & {
        readyWhen: IReadyWhen;
        gracePeriodSeconds?: number | undefined;
    };

    IStartProcessCommand

    type

    a command and its options in one object, the form runProcess takes, accepted by startProcess too

    Declaration
    type IStartProcessCommand = IStartProcessOptions & {
        command: string;
        args: string[];
    };

    IStartProcessOptions

    type

    the options accepted by startProcess

    Declaration
    type IStartProcessOptions = {
        cwd?: string;
        env?: NodeJS.ProcessEnv;
        stdio?: StdioOptions;
    };

    Members

    • cwdproperty
      string

      the working directory. Defaults to the current one

    • envproperty
      NodeJS.ProcessEnv

      the environment variables. Defaults to process.env

    • stdioproperty
      StdioOptions

      the stdio configuration. Defaults to 'inherit'

    IWaitForPortOptions

    type

    the options waitForPort takes

    Declaration
    type IWaitForPortOptions = IReadinessWaitOptions & {
        host?: string | undefined;
        connectTimeoutSeconds?: number | undefined;
    };

    IWaitForUrlOptions

    type

    the options waitForUrl takes

    Declaration
    type IWaitForUrlOptions = IReadinessWaitOptions & {
        requestTimeoutSeconds?: number | undefined;
        isReady?: ((response: Response) => boolean | Promise<boolean>) | undefined;
    };