Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

exits

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

exits

Run arbitrary functions & commands asynchronously before process termination, programatically or via CLI

  • 0.1.2
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
13K
increased by642.11%
Maintainers
1
Weekly downloads
 
Created
Source

exits

Version Types Build Status Coverage Dependencies Vulnerabilities License

Run arbitrary functions & commands asynchronously before process termination, programatically or via CLI.

exits can conditionally intercept signals ('SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGTERM'), uncaughtExceptions, unhandledRejections, or end of execution (beforeExit) on Node.

The only instance in which it will not be able to complete work scheduled via add() before end of execution is if process.exit() or SIGKILL -meant to terminate the process forcefully- are explicitly called.

It can also be used as an executable, allowing you to run a command after a previous one exits, regardless of the cause.

Install

npm install exits

If global CLI usage is intended, you can either globally install with npm (recommended if you already have Node.js installed: npm install -g exits) or download the latest executable for your system.

CLI

Run a command after a main command terminates.

Usage: exits [options] <mainCmd> <...mainArgs> -- <afterCmd> <...afterArgs>

Options:
  --stdio <stdio>
        stdio options to spawn children processes with.
        Can be inherit, pipe, ignore, or a comma separated combination for stdin,stdout,stderr.
        Default: inherit.
        Example: --stdio pipe,inherit,inherit
  --at <at>
        In which termination cases of the main process should the after command run.
        Can be signal, error, success, or a comma separated combination of those.
        Default: signal,error,success.
        Example: --at signal,error
  --log <level>
        Logging level, one of trace, debug, info, warn, error, or silent.
        Default: warn
        Example: --logger info
  -h, --help       output usage information
  -V, --version    output the version number

Programatic Usage

  • attach() starts listening to termination events.
  • unattach() stops listening to termination events.
  • add() adds and removes tasks to be run on listened to events before termination.
  • clear() removes all added tasks.
  • options() sets exits options.
  • state() returns an object with the current exits state.
  • on() subscribes to state changes.
  • control() controls async execution flow in order to stop parallel execution on triggered termination events.
  • exit() explicitly terminates execution while still waiting for exits tasks to finish.
  • spawn() safely handles execution of child processes.

Basic usage

import { attach, add } from 'exits';

// By default, attach() will intercept signals, exceptions,
// unhandled rejections, and end of execution events.
// Once we call attach, all hooks added via add() will execute before
// process termination.
attach();

// We can add() before or after we call attach(). Just keep in
// mind only after attach() is called will we intercept process termination.
// add() can be passed a sync or an async function.
add(async () => {
  // Do any async op.
  await new Promise((resolve) => setTimeout(resolve, 3000));
  console.log('Second task has finished');
});

// Tasks added via add() execute serially
// in reverse order of addition by default (LIFO).
add(async () => {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  console.log('First task has finished');
});

attach(opts?: object): void

Starts listening to termination events. By default, attach() will listen to all available events. Calling attach() several times will have no effect if a particular event is already being listened for.

  • opts: Object, optional with keys:
    • signal: boolean; default: true. Whether to listen to and intercept 'SIGINT', 'SIGHUP', 'SIGQUIT', and 'SIGTERM' events.
    • exception: boolean; default: true. Whether to listen to and intercept uncaughtException (errors).
    • rejection: boolean; default: true. Whether to listen to and intercept unhandledRejection (unhandled promise rejections).
    • exit: boolean; default: true. Whether to listen to and intercept beforeExit.
import { attach } from 'exits';

// In order to listen to all events we just do:
attach();

// These are all equivalent
attach();
attach({ signal: true, rejection: true });
attach({ signal: true, exception: true, rejection: false, exit: true });

// If we wanted to listen ONLY to exceptions
// we'd do either of these (they are equivalent):
attach({ signal: false, rejection: false, exit: false });
attach({ signal: false, exception: true, rejection: false, exit: false });

unattach(opts?: object): void

Stop listening to all or some termination events. By default, it will stop listening to all currently being listened to. It will only have effect if we are currently listening to some or all of the events passed. It will be automatically called when all tasks have run in order to allow for process exit.

  • opts:
    • signal: boolean; default: true.
    • exception: boolean; default: true.
    • rejection: boolean; default: true.
    • exit: boolean; default: true.
import { unattach } from 'exits';

// Stop listening to all
unattach();

// Stop listening only to exceptions
unattach({ signal: false, rejection: false, exit: false });

add(cb: function, priority?: number | null, opts?: object): function

Adds a task to be run on attach()ed events. Returns a removal function.

  • cb: function for the task to run on the attach()ed events, with signature (can be async): (type: string, arg: any, context: any) => Promise<void> | void:
    • if type is 'signal', arg will be any of 'SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGTERM'.
    • if type is either 'exception' or 'rejection', arg will be an Error.
    • if type is 'exit', arg will be the exit code number.
    • context is an initially empty object that is passed to all tasks. Tasks can share state by mutating the object.
  • priority: number | null; default: 0. For an equal priority, tasks added via add() will execute serially, in reverse order of addition (LIFO). Tasks with higher (larger) priority will always execute first.
  • opts: A task can be marked to apply only for some cases. This would allow, as an example, to only run certain tasks if the process throws an exception, others, if the process is terminated via signal, and others in all cases. It will only act for signals that have been attach()ed.
    • signal: boolean; default: true.
    • exception: boolean; default: true.
    • rejection: boolean; default: true.
    • exit: boolean; default: true.
import { add } from 'exits';

// Some cleanup task that won't run on signals ('SIGINT',
// 'SIGHUP', 'SIGQUIT', 'SIGTERM')
add(async (type, arg, context) => {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  console.log('Cleanup is done');
}, null, { signal: false });

// We'll add a task and then remove it.
const remove = add(() => {
  // Some task...
});
remove();

clear(): void

Removes all tasks scheduled via add(). If run inside a task, it will prevent any other following task from executing.

import { clear } from 'exits';

clear();

options(opts?: object): void

Sets exits global options.

  • opts: object, with optional properties:
    • logger: string, any of 'trace', 'debug', 'info', 'warn', 'error', 'silent'. Sets exits logging level. Default: 'warn'.
    • spawned: object, determines exits behavior in relation to spawned commands. See spawn().
    • resolver: a resolver function.
import { options } from 'exits';

options({
  logger: 'debug'
});

state(): object

Returns an object with the current exits state. It will not be mutated on updates, so you need to call state() each time you want to check it.

The returned object will have properties:

  • attached: Which events was exits attached to via attach(). object, with keys:
    • signal: boolean
    • exception: boolean
    • rejection: boolean
    • exit: boolean
  • triggered: Whether tasks have started running. null if not, otherwise an object containing information as to the termination event that caused it, with keys:
    • type: One of 'signal', 'exception', 'rejection', 'exit';
    • arg:
      • One of 'SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGTERM' if type is 'signal'.
      • An Error if type is 'exception' or 'rejection'.
      • A number if type is exit signaling the exit code.
  • done: boolean, whether all task calls have run and finished.
import { state } from 'exits';

state();

Initial state:

{
  attached: {
    signal: false,
    exception: false,
    rejection: false,
    exit: false
  },
  triggered: null,
  done: false
}

on(event: string, cb: function): void

Subscribes to state changes.

  • event: string, any of:
    • 'attached': cb will be called whenever the attach() method is called and successfully attaches to one or more new events, receiving an object with the current attachments, with keys:
      • signal: boolean
      • exception: boolean
      • rejection: boolean
      • exit: boolean
    • 'triggered': cb will be called on the first occasion exits tasks are called (as the rest are ignored), receiving an object as an argument, with keys:
      • type: any of 'signal', 'exception', 'rejection', and 'exit'.
      • arg: the signal ('SIGINT', 'SIGHUP', 'SIGQUIT', 'SIGTERM'), Error, or exit code number.
    • 'done': cb will be called once all exits tasks complete.
import { on } from 'exits';

on('triggered', ({ type, arg }) => {
  // do something
});

control(fn: generator): Promise<any>

Used to control async flow. It might occur that some function throws or reject within your library, hence you'd expect exits task to run, but all other ongoing processes to terminate, particularly if doing costly async operations. For that use case, generate will return an async function from a generator. When the generator is run, it will only continue yielding if exits tasks have not been triggered.

import { control } from 'exits';

const myAsyncFunction = control(function*(n = 10) {
  // You can use yield as you'd use await; res = 20
  let res = yield Promise.resolve(n * 2);
  // If tasks have been triggered by some event this won't execute
  res = yield Promise.resolve(res * 5);
  // res = 100
  return res;
});

myAsyncFunction(10).then(console.log) // 100

exit(code: number): Promise<void>

As any explicit call to process.exit() will terminate the process without running exits tasks, exit() is provided as a replacement for explicit exit calls. It will run all tasks associated with the exit event and call the resolver function. with the exit code passed.

import { exit } from 'exits';

exit(1);

spawn(cmd: string, args?: string[], opts?: object): object

Spawning child processes in a way that behaves coherently with exits tasks is tricky. To simplify it, even while still offering a relatively low level api, spawn() is available.

spawn() returns an object, with keys:

  • ps: a Node.js ChildProcess.
  • promise: a promise, that will:
    • reject:
      • if the child process terminates on error,
      • if it exits with a code other than 0,
      • or if it terminates with a signal other than 'SIGINT', 'SIGHUP', 'SIGQUIT' or 'SIGTERM'.
    • resolve:
      • with a null value if the process exits with code 0,
      • or with any of 'SIGINT', 'SIGHUP', 'SIGQUIT' or 'SIGTERM' if the child process terminates on any of those signals.
import { spawn } from 'exits';

const { ps, promise } = spawn('echo', ['hello'], { stdio: 'inherit' });

The way we deal with spawned processes and exits tasks initialization is defined through spawned options():.

  • signals: string, whether to stop listening for 'SIGINT', 'SIGHUP', 'SIGQUIT' and 'SIGTERM' signals on the main process while there is a spawned process running. Can be any of:
    • 'all': stop listening to signals if there is any spawned process running.
    • 'bind': stop listening only if there is at least one non detached spawned process running.
    • 'detached': stop listening only if there is at least one detached spawned process running.
    • 'none': don't stop listening.
  • wait: string, whether to wait for spawned processes to exit before and after executing exits tasks on main process termination. Can be any of:
    • 'all': wait for all spawned processes to exit.
    • 'bind': wait for all non detached spawned processes to exit.
    • 'detached': wait for all detached spawned processes to exit.
    • 'none': don't wait for any process to exit.
  • sigterm: null or number, timeout in milliseconds before sending a 'SIGTERM' signal to all waited for processes that haven't exited by then. If null, it won't be sent. Only valid when wait is not 'none'.
  • sigkill: null or number, timeout in milliseconds before sending a 'SIGKILL' signal to all waited for processes that haven't exited by then. If sigterm is not null, it starts counting after the 'SIGTERM' signal has been sent. If null, it won't be sent. Only valid when wait is not 'none'. Bear in mind if processes are waited for and no 'SIGKILL' is sent, the waiting process might carry on indefinitely if their dueful termination is not handled manually.

These are the defaults:

import { options } from 'exits';

options({
  spawned: {
    signals: 'bind',
    wait: 'bind',
    sigterm: 5000,
    sigkill: 10000
  }
});

Resolver function

The resolver function gets called whenever exits tasks finalize in order to terminate the current process in a way that is coherent with the first event that caused the tasks to initialize. Hence, it takes two arguments: type and arg, in the same fashion as the add() cb.

You can switch this function globally by passing a resolver key to options().

Simplified default implementation:

import { options } from 'exits';

options({
  resolver(type, arg) {
    switch (type) {
      case 'signal':
        process.kill(process.pid, arg);
        break;
      case 'exit':
        process.exit(Number(arg));
      case 'exception':
      case 'rejection':
        setImmediate(() => {
          throw arg;
        });
        break;
      default:
        break;
    }
  }
});

Forceful process termination

There are two instances in which exits tasks won't run: when calling process.exit() explicitly, and when a SIGKILL signal is received.

// This will immediately exit the process with a 0 code
process.exit(0);

// This will terminate the process with a SIGKILL signal
process.kill(process.pid, 'SIGKILL');

Keywords

FAQs

Package last updated on 28 Jan 2019

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc