handle.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import {getStreamName, isStandardStream} from '../utils/standard-stream.js';
  2. import {normalizeTransforms} from '../transform/normalize.js';
  3. import {getFdObjectMode} from '../transform/object-mode.js';
  4. import {
  5. getStdioItemType,
  6. isRegularUrl,
  7. isUnknownStdioString,
  8. FILE_TYPES,
  9. } from './type.js';
  10. import {getStreamDirection} from './direction.js';
  11. import {normalizeStdioOption} from './stdio-option.js';
  12. import {handleNativeStream} from './native.js';
  13. import {handleInputOptions} from './input-option.js';
  14. import {filterDuplicates, getDuplicateStream} from './duplicate.js';
  15. // Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in async/sync mode
  16. // They are converted into an array of `fileDescriptors`.
  17. // Each `fileDescriptor` is normalized, validated and contains all information necessary for further handling.
  18. export const handleStdio = (addProperties, options, verboseInfo, isSync) => {
  19. const stdio = normalizeStdioOption(options, verboseInfo, isSync);
  20. const initialFileDescriptors = stdio.map((stdioOption, fdNumber) => getFileDescriptor({
  21. stdioOption,
  22. fdNumber,
  23. options,
  24. isSync,
  25. }));
  26. const fileDescriptors = getFinalFileDescriptors({
  27. initialFileDescriptors,
  28. addProperties,
  29. options,
  30. isSync,
  31. });
  32. options.stdio = fileDescriptors.map(({stdioItems}) => forwardStdio(stdioItems));
  33. return fileDescriptors;
  34. };
  35. const getFileDescriptor = ({stdioOption, fdNumber, options, isSync}) => {
  36. const optionName = getStreamName(fdNumber);
  37. const {stdioItems: initialStdioItems, isStdioArray} = initializeStdioItems({
  38. stdioOption,
  39. fdNumber,
  40. options,
  41. optionName,
  42. });
  43. const direction = getStreamDirection(initialStdioItems, fdNumber, optionName);
  44. const stdioItems = initialStdioItems.map(stdioItem => handleNativeStream({
  45. stdioItem,
  46. isStdioArray,
  47. fdNumber,
  48. direction,
  49. isSync,
  50. }));
  51. const normalizedStdioItems = normalizeTransforms(stdioItems, optionName, direction, options);
  52. const objectMode = getFdObjectMode(normalizedStdioItems, direction);
  53. validateFileObjectMode(normalizedStdioItems, objectMode);
  54. return {direction, objectMode, stdioItems: normalizedStdioItems};
  55. };
  56. // We make sure passing an array with a single item behaves the same as passing that item without an array.
  57. // This is what users would expect.
  58. // For example, `stdout: ['ignore']` behaves the same as `stdout: 'ignore'`.
  59. const initializeStdioItems = ({stdioOption, fdNumber, options, optionName}) => {
  60. const values = Array.isArray(stdioOption) ? stdioOption : [stdioOption];
  61. const initialStdioItems = [
  62. ...values.map(value => initializeStdioItem(value, optionName)),
  63. ...handleInputOptions(options, fdNumber),
  64. ];
  65. const stdioItems = filterDuplicates(initialStdioItems);
  66. const isStdioArray = stdioItems.length > 1;
  67. validateStdioArray(stdioItems, isStdioArray, optionName);
  68. validateStreams(stdioItems);
  69. return {stdioItems, isStdioArray};
  70. };
  71. const initializeStdioItem = (value, optionName) => ({
  72. type: getStdioItemType(value, optionName),
  73. value,
  74. optionName,
  75. });
  76. const validateStdioArray = (stdioItems, isStdioArray, optionName) => {
  77. if (stdioItems.length === 0) {
  78. throw new TypeError(`The \`${optionName}\` option must not be an empty array.`);
  79. }
  80. if (!isStdioArray) {
  81. return;
  82. }
  83. for (const {value, optionName} of stdioItems) {
  84. if (INVALID_STDIO_ARRAY_OPTIONS.has(value)) {
  85. throw new Error(`The \`${optionName}\` option must not include \`${value}\`.`);
  86. }
  87. }
  88. };
  89. // Using those `stdio` values together with others for the same stream does not make sense, so we make it fail.
  90. // However, we do allow it if the array has a single item.
  91. const INVALID_STDIO_ARRAY_OPTIONS = new Set(['ignore', 'ipc']);
  92. const validateStreams = stdioItems => {
  93. for (const stdioItem of stdioItems) {
  94. validateFileStdio(stdioItem);
  95. }
  96. };
  97. const validateFileStdio = ({type, value, optionName}) => {
  98. if (isRegularUrl(value)) {
  99. throw new TypeError(`The \`${optionName}: URL\` option must use the \`file:\` scheme.
  100. For example, you can use the \`pathToFileURL()\` method of the \`url\` core module.`);
  101. }
  102. if (isUnknownStdioString(type, value)) {
  103. throw new TypeError(`The \`${optionName}: { file: '...' }\` option must be used instead of \`${optionName}: '...'\`.`);
  104. }
  105. };
  106. const validateFileObjectMode = (stdioItems, objectMode) => {
  107. if (!objectMode) {
  108. return;
  109. }
  110. const fileStdioItem = stdioItems.find(({type}) => FILE_TYPES.has(type));
  111. if (fileStdioItem !== undefined) {
  112. throw new TypeError(`The \`${fileStdioItem.optionName}\` option cannot use both files and transforms in objectMode.`);
  113. }
  114. };
  115. // Some `stdio` values require Execa to create streams.
  116. // For example, file paths create file read/write streams.
  117. // Those transformations are specified in `addProperties`, which is both direction-specific and type-specific.
  118. const getFinalFileDescriptors = ({initialFileDescriptors, addProperties, options, isSync}) => {
  119. const fileDescriptors = [];
  120. try {
  121. for (const fileDescriptor of initialFileDescriptors) {
  122. fileDescriptors.push(getFinalFileDescriptor({
  123. fileDescriptor,
  124. fileDescriptors,
  125. addProperties,
  126. options,
  127. isSync,
  128. }));
  129. }
  130. return fileDescriptors;
  131. } catch (error) {
  132. cleanupCustomStreams(fileDescriptors);
  133. throw error;
  134. }
  135. };
  136. const getFinalFileDescriptor = ({
  137. fileDescriptor: {direction, objectMode, stdioItems},
  138. fileDescriptors,
  139. addProperties,
  140. options,
  141. isSync,
  142. }) => {
  143. const finalStdioItems = stdioItems.map(stdioItem => addStreamProperties({
  144. stdioItem,
  145. addProperties,
  146. direction,
  147. options,
  148. fileDescriptors,
  149. isSync,
  150. }));
  151. return {direction, objectMode, stdioItems: finalStdioItems};
  152. };
  153. const addStreamProperties = ({stdioItem, addProperties, direction, options, fileDescriptors, isSync}) => {
  154. const duplicateStream = getDuplicateStream({
  155. stdioItem,
  156. direction,
  157. fileDescriptors,
  158. isSync,
  159. });
  160. if (duplicateStream !== undefined) {
  161. return {...stdioItem, stream: duplicateStream};
  162. }
  163. return {
  164. ...stdioItem,
  165. ...addProperties[direction][stdioItem.type](stdioItem, options),
  166. };
  167. };
  168. // The stream error handling is performed by the piping logic above, which cannot be performed before subprocess spawning.
  169. // If the subprocess spawning fails (e.g. due to an invalid command), the streams need to be manually destroyed.
  170. // We need to create those streams before subprocess spawning, in case their creation fails, e.g. when passing an invalid generator as argument.
  171. // Like this, an exception would be thrown, which would prevent spawning a subprocess.
  172. export const cleanupCustomStreams = fileDescriptors => {
  173. for (const {stdioItems} of fileDescriptors) {
  174. for (const {stream} of stdioItems) {
  175. if (stream !== undefined && !isStandardStream(stream)) {
  176. stream.destroy();
  177. }
  178. }
  179. }
  180. };
  181. // When the `std*: Iterable | WebStream | URL | filePath`, `input` or `inputFile` option is used, we pipe to `subprocess.std*`.
  182. // When the `std*: Array` option is used, we emulate some of the native values ('inherit', Node.js stream and file descriptor integer). To do so, we also need to pipe to `subprocess.std*`.
  183. // Therefore the `std*` options must be either `pipe` or `overlapped`. Other values do not set `subprocess.std*`.
  184. const forwardStdio = stdioItems => {
  185. if (stdioItems.length > 1) {
  186. return stdioItems.some(({value}) => value === 'overlapped') ? 'overlapped' : 'pipe';
  187. }
  188. const [{type, value}] = stdioItems;
  189. return type === 'native' ? value : 'pipe';
  190. };