main-async.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import {setMaxListeners} from 'node:events';
  2. import {spawn} from 'node:child_process';
  3. import {MaxBufferError} from 'get-stream';
  4. import {handleCommand} from '../arguments/command.js';
  5. import {normalizeOptions} from '../arguments/options.js';
  6. import {SUBPROCESS_OPTIONS} from '../arguments/fd-options.js';
  7. import {addIpcMethods} from '../ipc/methods.js';
  8. import {makeError, makeSuccessResult} from '../return/result.js';
  9. import {handleResult} from '../return/reject.js';
  10. import {handleEarlyError} from '../return/early-error.js';
  11. import {handleStdioAsync} from '../stdio/handle-async.js';
  12. import {stripNewline} from '../io/strip-newline.js';
  13. import {pipeOutputAsync} from '../io/output-async.js';
  14. import {subprocessKill} from '../terminate/kill.js';
  15. import {cleanupOnExit} from '../terminate/cleanup.js';
  16. import {pipeToSubprocess} from '../pipe/setup.js';
  17. import {makeAllStream} from '../resolve/all-async.js';
  18. import {waitForSubprocessResult} from '../resolve/wait-subprocess.js';
  19. import {addConvertedStreams} from '../convert/add.js';
  20. import {createDeferred} from '../utils/deferred.js';
  21. import {mergePromise} from './promise.js';
  22. // Main shared logic for all async methods: `execa()`, `$`, `execaNode()`
  23. export const execaCoreAsync = (rawFile, rawArguments, rawOptions, createNested) => {
  24. const {file, commandArguments, command, escapedCommand, startTime, verboseInfo, options, fileDescriptors} = handleAsyncArguments(rawFile, rawArguments, rawOptions);
  25. const {subprocess, promise} = spawnSubprocessAsync({
  26. file,
  27. commandArguments,
  28. options,
  29. startTime,
  30. verboseInfo,
  31. command,
  32. escapedCommand,
  33. fileDescriptors,
  34. });
  35. subprocess.pipe = pipeToSubprocess.bind(undefined, {
  36. source: subprocess,
  37. sourcePromise: promise,
  38. boundOptions: {},
  39. createNested,
  40. });
  41. mergePromise(subprocess, promise);
  42. SUBPROCESS_OPTIONS.set(subprocess, {options, fileDescriptors});
  43. return subprocess;
  44. };
  45. // Compute arguments to pass to `child_process.spawn()`
  46. const handleAsyncArguments = (rawFile, rawArguments, rawOptions) => {
  47. const {command, escapedCommand, startTime, verboseInfo} = handleCommand(rawFile, rawArguments, rawOptions);
  48. const {file, commandArguments, options: normalizedOptions} = normalizeOptions(rawFile, rawArguments, rawOptions);
  49. const options = handleAsyncOptions(normalizedOptions);
  50. const fileDescriptors = handleStdioAsync(options, verboseInfo);
  51. return {
  52. file,
  53. commandArguments,
  54. command,
  55. escapedCommand,
  56. startTime,
  57. verboseInfo,
  58. options,
  59. fileDescriptors,
  60. };
  61. };
  62. // Options normalization logic specific to async methods.
  63. // Prevent passing the `timeout` option directly to `child_process.spawn()`.
  64. const handleAsyncOptions = ({timeout, signal, ...options}) => {
  65. if (signal !== undefined) {
  66. throw new TypeError('The "signal" option has been renamed to "cancelSignal" instead.');
  67. }
  68. return {...options, timeoutDuration: timeout};
  69. };
  70. const spawnSubprocessAsync = ({file, commandArguments, options, startTime, verboseInfo, command, escapedCommand, fileDescriptors}) => {
  71. let subprocess;
  72. try {
  73. subprocess = spawn(file, commandArguments, options);
  74. } catch (error) {
  75. return handleEarlyError({
  76. error,
  77. command,
  78. escapedCommand,
  79. fileDescriptors,
  80. options,
  81. startTime,
  82. verboseInfo,
  83. });
  84. }
  85. const controller = new AbortController();
  86. setMaxListeners(Number.POSITIVE_INFINITY, controller.signal);
  87. const originalStreams = [...subprocess.stdio];
  88. pipeOutputAsync(subprocess, fileDescriptors, controller);
  89. cleanupOnExit(subprocess, options, controller);
  90. const context = {};
  91. const onInternalError = createDeferred();
  92. subprocess.kill = subprocessKill.bind(undefined, {
  93. kill: subprocess.kill.bind(subprocess),
  94. options,
  95. onInternalError,
  96. context,
  97. controller,
  98. });
  99. subprocess.all = makeAllStream(subprocess, options);
  100. addConvertedStreams(subprocess, options);
  101. addIpcMethods(subprocess, options);
  102. const promise = handlePromise({
  103. subprocess,
  104. options,
  105. startTime,
  106. verboseInfo,
  107. fileDescriptors,
  108. originalStreams,
  109. command,
  110. escapedCommand,
  111. context,
  112. onInternalError,
  113. controller,
  114. });
  115. return {subprocess, promise};
  116. };
  117. // Asynchronous logic, as opposed to the previous logic which can be run synchronously, i.e. can be returned to user right away
  118. const handlePromise = async ({subprocess, options, startTime, verboseInfo, fileDescriptors, originalStreams, command, escapedCommand, context, onInternalError, controller}) => {
  119. const [
  120. errorInfo,
  121. [exitCode, signal],
  122. stdioResults,
  123. allResult,
  124. ipcOutput,
  125. ] = await waitForSubprocessResult({
  126. subprocess,
  127. options,
  128. context,
  129. verboseInfo,
  130. fileDescriptors,
  131. originalStreams,
  132. onInternalError,
  133. controller,
  134. });
  135. controller.abort();
  136. onInternalError.resolve();
  137. const stdio = stdioResults.map((stdioResult, fdNumber) => stripNewline(stdioResult, options, fdNumber));
  138. const all = stripNewline(allResult, options, 'all');
  139. const result = getAsyncResult({
  140. errorInfo,
  141. exitCode,
  142. signal,
  143. stdio,
  144. all,
  145. ipcOutput,
  146. context,
  147. options,
  148. command,
  149. escapedCommand,
  150. startTime,
  151. });
  152. return handleResult(result, verboseInfo, options);
  153. };
  154. const getAsyncResult = ({errorInfo, exitCode, signal, stdio, all, ipcOutput, context, options, command, escapedCommand, startTime}) => 'error' in errorInfo
  155. ? makeError({
  156. error: errorInfo.error,
  157. command,
  158. escapedCommand,
  159. timedOut: context.terminationReason === 'timeout',
  160. isCanceled: context.terminationReason === 'cancel' || context.terminationReason === 'gracefulCancel',
  161. isGracefullyCanceled: context.terminationReason === 'gracefulCancel',
  162. isMaxBuffer: errorInfo.error instanceof MaxBufferError,
  163. isForcefullyTerminated: context.isForcefullyTerminated,
  164. exitCode,
  165. signal,
  166. stdio,
  167. all,
  168. ipcOutput,
  169. options,
  170. startTime,
  171. isSync: false,
  172. })
  173. : makeSuccessResult({
  174. command,
  175. escapedCommand,
  176. stdio,
  177. all,
  178. ipcOutput,
  179. options,
  180. startTime,
  181. });