kill.js
  1  'use strict';
  2  const os = require('os');
  3  const onExit = require('signal-exit');
  4  
  5  const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5;
  6  
  7  // Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior
  8  const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => {
  9  	const killResult = kill(signal);
 10  	setKillTimeout(kill, signal, options, killResult);
 11  	return killResult;
 12  };
 13  
 14  const setKillTimeout = (kill, signal, options, killResult) => {
 15  	if (!shouldForceKill(signal, options, killResult)) {
 16  		return;
 17  	}
 18  
 19  	const timeout = getForceKillAfterTimeout(options);
 20  	const t = setTimeout(() => {
 21  		kill('SIGKILL');
 22  	}, timeout);
 23  
 24  	// Guarded because there's no `.unref()` when `execa` is used in the renderer
 25  	// process in Electron. This cannot be tested since we don't run tests in
 26  	// Electron.
 27  	// istanbul ignore else
 28  	if (t.unref) {
 29  		t.unref();
 30  	}
 31  };
 32  
 33  const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => {
 34  	return isSigterm(signal) && forceKillAfterTimeout !== false && killResult;
 35  };
 36  
 37  const isSigterm = signal => {
 38  	return signal === os.constants.signals.SIGTERM ||
 39  		(typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM');
 40  };
 41  
 42  const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => {
 43  	if (forceKillAfterTimeout === true) {
 44  		return DEFAULT_FORCE_KILL_TIMEOUT;
 45  	}
 46  
 47  	if (!Number.isFinite(forceKillAfterTimeout) || forceKillAfterTimeout < 0) {
 48  		throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`);
 49  	}
 50  
 51  	return forceKillAfterTimeout;
 52  };
 53  
 54  // `childProcess.cancel()`
 55  const spawnedCancel = (spawned, context) => {
 56  	const killResult = spawned.kill();
 57  
 58  	if (killResult) {
 59  		context.isCanceled = true;
 60  	}
 61  };
 62  
 63  const timeoutKill = (spawned, signal, reject) => {
 64  	spawned.kill(signal);
 65  	reject(Object.assign(new Error('Timed out'), {timedOut: true, signal}));
 66  };
 67  
 68  // `timeout` option handling
 69  const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => {
 70  	if (timeout === 0 || timeout === undefined) {
 71  		return spawnedPromise;
 72  	}
 73  
 74  	if (!Number.isFinite(timeout) || timeout < 0) {
 75  		throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`);
 76  	}
 77  
 78  	let timeoutId;
 79  	const timeoutPromise = new Promise((resolve, reject) => {
 80  		timeoutId = setTimeout(() => {
 81  			timeoutKill(spawned, killSignal, reject);
 82  		}, timeout);
 83  	});
 84  
 85  	const safeSpawnedPromise = spawnedPromise.finally(() => {
 86  		clearTimeout(timeoutId);
 87  	});
 88  
 89  	return Promise.race([timeoutPromise, safeSpawnedPromise]);
 90  };
 91  
 92  // `cleanup` option handling
 93  const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => {
 94  	if (!cleanup || detached) {
 95  		return timedPromise;
 96  	}
 97  
 98  	const removeExitHandler = onExit(() => {
 99  		spawned.kill();
100  	});
101  
102  	return timedPromise.finally(() => {
103  		removeExitHandler();
104  	});
105  };
106  
107  module.exports = {
108  	spawnedKill,
109  	spawnedCancel,
110  	setupTimeout,
111  	setExitHandler
112  };