main-async.js 5.8 KB

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