Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Run arbitrary functions & commands asynchronously before process termination, programatically or via CLI
Run arbitrary functions & commands asynchronously before process termination, programatically or via CLI.
exits
can conditionally intercept signals ('SIGINT'
, 'SIGHUP'
, 'SIGQUIT'
, 'SIGTERM'
), uncaughtException
s, unhandledRejection
s, 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.
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.
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
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.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
:
type
is 'signal'
, arg
will be any of 'SIGINT'
, 'SIGHUP'
, 'SIGQUIT'
, 'SIGTERM'
.type
is either 'exception'
or 'rejection'
, arg
will be an Error.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
: booleanexception
: booleanrejection
: booleanexit
: booleantriggered
: 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
:
'SIGINT'
, 'SIGHUP'
, 'SIGQUIT'
, 'SIGTERM'
if type
is 'signal'
.type
is 'exception'
or 'rejection'
.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
: booleanexception
: booleanrejection
: booleanexit
: 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.
cmd
: string, a command to run.args
: string array, optional, arguments for cmd
.opts
: object, optional, Node.js' child_process.spawn
options.spawn()
returns an object, with keys:
ps
: a Node.js ChildProcess
.promise
: a promise, that will:
'SIGINT'
, 'SIGHUP'
, 'SIGQUIT'
or 'SIGTERM'
.null
value if the process exits with code 0,'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
}
});
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;
}
}
});
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');
FAQs
Run arbitrary functions & commands asynchronously before process termination, programatically or via CLI
The npm package exits receives a total of 12,665 weekly downloads. As such, exits popularity was classified as popular.
We found that exits demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
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.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.