kill.js 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. 'use strict';
  2. const os = require('os');
  3. const onExit = require('signal-exit');
  4. const pFinally = require('p-finally');
  5. const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5;
  6. // Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior
  7. const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => {
  8. const killResult = kill(signal);
  9. setKillTimeout(kill, signal, options, killResult);
  10. return killResult;
  11. };
  12. const setKillTimeout = (kill, signal, options, killResult) => {
  13. if (!shouldForceKill(signal, options, killResult)) {
  14. return;
  15. }
  16. const timeout = getForceKillAfterTimeout(options);
  17. setTimeout(() => {
  18. kill('SIGKILL');
  19. }, timeout).unref();
  20. };
  21. const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => {
  22. return isSigterm(signal) && forceKillAfterTimeout !== false && killResult;
  23. };
  24. const isSigterm = signal => {
  25. return signal === os.constants.signals.SIGTERM ||
  26. (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM');
  27. };
  28. const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => {
  29. if (forceKillAfterTimeout === true) {
  30. return DEFAULT_FORCE_KILL_TIMEOUT;
  31. }
  32. if (!Number.isInteger(forceKillAfterTimeout) || forceKillAfterTimeout < 0) {
  33. throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`);
  34. }
  35. return forceKillAfterTimeout;
  36. };
  37. // `childProcess.cancel()`
  38. const spawnedCancel = (spawned, context) => {
  39. const killResult = spawned.kill();
  40. if (killResult) {
  41. context.isCanceled = true;
  42. }
  43. };
  44. const timeoutKill = (spawned, signal, reject) => {
  45. spawned.kill(signal);
  46. reject(Object.assign(new Error('Timed out'), {timedOut: true, signal}));
  47. };
  48. // `timeout` option handling
  49. const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => {
  50. if (timeout === 0 || timeout === undefined) {
  51. return spawnedPromise;
  52. }
  53. if (!Number.isInteger(timeout) || timeout < 0) {
  54. throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`);
  55. }
  56. let timeoutId;
  57. const timeoutPromise = new Promise((resolve, reject) => {
  58. timeoutId = setTimeout(() => {
  59. timeoutKill(spawned, killSignal, reject);
  60. }, timeout);
  61. });
  62. const safeSpawnedPromise = pFinally(spawnedPromise, () => {
  63. clearTimeout(timeoutId);
  64. });
  65. return Promise.race([timeoutPromise, safeSpawnedPromise]);
  66. };
  67. // `cleanup` option handling
  68. const setExitHandler = (spawned, {cleanup, detached}, timedPromise) => {
  69. if (!cleanup || detached) {
  70. return timedPromise;
  71. }
  72. const removeExitHandler = onExit(() => {
  73. spawned.kill();
  74. });
  75. // TODO: Use native "finally" syntax when targeting Node.js 10
  76. return pFinally(timedPromise, removeExitHandler);
  77. };
  78. module.exports = {
  79. spawnedKill,
  80. spawnedCancel,
  81. setupTimeout,
  82. setExitHandler
  83. };