kill.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import {setTimeout} from 'node:timers/promises';
  2. import {isErrorInstance} from '../return/final-error.js';
  3. import {normalizeSignalArgument} from './signal.js';
  4. // Normalize the `forceKillAfterDelay` option
  5. export const normalizeForceKillAfterDelay = forceKillAfterDelay => {
  6. if (forceKillAfterDelay === false) {
  7. return forceKillAfterDelay;
  8. }
  9. if (forceKillAfterDelay === true) {
  10. return DEFAULT_FORCE_KILL_TIMEOUT;
  11. }
  12. if (!Number.isFinite(forceKillAfterDelay) || forceKillAfterDelay < 0) {
  13. throw new TypeError(`Expected the \`forceKillAfterDelay\` option to be a non-negative integer, got \`${forceKillAfterDelay}\` (${typeof forceKillAfterDelay})`);
  14. }
  15. return forceKillAfterDelay;
  16. };
  17. const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5;
  18. // Monkey-patches `subprocess.kill()` to add `forceKillAfterDelay` behavior and `.kill(error)`
  19. export const subprocessKill = (
  20. {kill, options: {forceKillAfterDelay, killSignal}, onInternalError, context, controller},
  21. signalOrError,
  22. errorArgument,
  23. ) => {
  24. const {signal, error} = parseKillArguments(signalOrError, errorArgument, killSignal);
  25. emitKillError(error, onInternalError);
  26. const killResult = kill(signal);
  27. setKillTimeout({
  28. kill,
  29. signal,
  30. forceKillAfterDelay,
  31. killSignal,
  32. killResult,
  33. context,
  34. controller,
  35. });
  36. return killResult;
  37. };
  38. const parseKillArguments = (signalOrError, errorArgument, killSignal) => {
  39. const [signal = killSignal, error] = isErrorInstance(signalOrError)
  40. ? [undefined, signalOrError]
  41. : [signalOrError, errorArgument];
  42. if (typeof signal !== 'string' && !Number.isInteger(signal)) {
  43. throw new TypeError(`The first argument must be an error instance or a signal name string/integer: ${String(signal)}`);
  44. }
  45. if (error !== undefined && !isErrorInstance(error)) {
  46. throw new TypeError(`The second argument is optional. If specified, it must be an error instance: ${error}`);
  47. }
  48. return {signal: normalizeSignalArgument(signal), error};
  49. };
  50. // Fails right away when calling `subprocess.kill(error)`.
  51. // Does not wait for actual signal termination.
  52. // Uses a deferred promise instead of the `error` event on the subprocess, as this is less intrusive.
  53. const emitKillError = (error, onInternalError) => {
  54. if (error !== undefined) {
  55. onInternalError.reject(error);
  56. }
  57. };
  58. const setKillTimeout = async ({kill, signal, forceKillAfterDelay, killSignal, killResult, context, controller}) => {
  59. if (signal === killSignal && killResult) {
  60. killOnTimeout({
  61. kill,
  62. forceKillAfterDelay,
  63. context,
  64. controllerSignal: controller.signal,
  65. });
  66. }
  67. };
  68. // Forcefully terminate a subprocess after a timeout
  69. export const killOnTimeout = async ({kill, forceKillAfterDelay, context, controllerSignal}) => {
  70. if (forceKillAfterDelay === false) {
  71. return;
  72. }
  73. try {
  74. await setTimeout(forceKillAfterDelay, undefined, {signal: controllerSignal});
  75. if (kill('SIGKILL')) {
  76. context.isForcefullyTerminated ??= true;
  77. }
  78. } catch {}
  79. };