kill.js 3.0 KB

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