123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214 |
- import {getStreamName, isStandardStream} from '../utils/standard-stream.js';
- import {normalizeTransforms} from '../transform/normalize.js';
- import {getFdObjectMode} from '../transform/object-mode.js';
- import {
- getStdioItemType,
- isRegularUrl,
- isUnknownStdioString,
- FILE_TYPES,
- } from './type.js';
- import {getStreamDirection} from './direction.js';
- import {normalizeStdioOption} from './stdio-option.js';
- import {handleNativeStream} from './native.js';
- import {handleInputOptions} from './input-option.js';
- import {filterDuplicates, getDuplicateStream} from './duplicate.js';
- // Handle `input`, `inputFile`, `stdin`, `stdout` and `stderr` options, before spawning, in async/sync mode
- // They are converted into an array of `fileDescriptors`.
- // Each `fileDescriptor` is normalized, validated and contains all information necessary for further handling.
- export const handleStdio = (addProperties, options, verboseInfo, isSync) => {
- const stdio = normalizeStdioOption(options, verboseInfo, isSync);
- const initialFileDescriptors = stdio.map((stdioOption, fdNumber) => getFileDescriptor({
- stdioOption,
- fdNumber,
- options,
- isSync,
- }));
- const fileDescriptors = getFinalFileDescriptors({
- initialFileDescriptors,
- addProperties,
- options,
- isSync,
- });
- options.stdio = fileDescriptors.map(({stdioItems}) => forwardStdio(stdioItems));
- return fileDescriptors;
- };
- const getFileDescriptor = ({stdioOption, fdNumber, options, isSync}) => {
- const optionName = getStreamName(fdNumber);
- const {stdioItems: initialStdioItems, isStdioArray} = initializeStdioItems({
- stdioOption,
- fdNumber,
- options,
- optionName,
- });
- const direction = getStreamDirection(initialStdioItems, fdNumber, optionName);
- const stdioItems = initialStdioItems.map(stdioItem => handleNativeStream({
- stdioItem,
- isStdioArray,
- fdNumber,
- direction,
- isSync,
- }));
- const normalizedStdioItems = normalizeTransforms(stdioItems, optionName, direction, options);
- const objectMode = getFdObjectMode(normalizedStdioItems, direction);
- validateFileObjectMode(normalizedStdioItems, objectMode);
- return {direction, objectMode, stdioItems: normalizedStdioItems};
- };
- // We make sure passing an array with a single item behaves the same as passing that item without an array.
- // This is what users would expect.
- // For example, `stdout: ['ignore']` behaves the same as `stdout: 'ignore'`.
- const initializeStdioItems = ({stdioOption, fdNumber, options, optionName}) => {
- const values = Array.isArray(stdioOption) ? stdioOption : [stdioOption];
- const initialStdioItems = [
- ...values.map(value => initializeStdioItem(value, optionName)),
- ...handleInputOptions(options, fdNumber),
- ];
- const stdioItems = filterDuplicates(initialStdioItems);
- const isStdioArray = stdioItems.length > 1;
- validateStdioArray(stdioItems, isStdioArray, optionName);
- validateStreams(stdioItems);
- return {stdioItems, isStdioArray};
- };
- const initializeStdioItem = (value, optionName) => ({
- type: getStdioItemType(value, optionName),
- value,
- optionName,
- });
- const validateStdioArray = (stdioItems, isStdioArray, optionName) => {
- if (stdioItems.length === 0) {
- throw new TypeError(`The \`${optionName}\` option must not be an empty array.`);
- }
- if (!isStdioArray) {
- return;
- }
- for (const {value, optionName} of stdioItems) {
- if (INVALID_STDIO_ARRAY_OPTIONS.has(value)) {
- throw new Error(`The \`${optionName}\` option must not include \`${value}\`.`);
- }
- }
- };
- // Using those `stdio` values together with others for the same stream does not make sense, so we make it fail.
- // However, we do allow it if the array has a single item.
- const INVALID_STDIO_ARRAY_OPTIONS = new Set(['ignore', 'ipc']);
- const validateStreams = stdioItems => {
- for (const stdioItem of stdioItems) {
- validateFileStdio(stdioItem);
- }
- };
- const validateFileStdio = ({type, value, optionName}) => {
- if (isRegularUrl(value)) {
- throw new TypeError(`The \`${optionName}: URL\` option must use the \`file:\` scheme.
- For example, you can use the \`pathToFileURL()\` method of the \`url\` core module.`);
- }
- if (isUnknownStdioString(type, value)) {
- throw new TypeError(`The \`${optionName}: { file: '...' }\` option must be used instead of \`${optionName}: '...'\`.`);
- }
- };
- const validateFileObjectMode = (stdioItems, objectMode) => {
- if (!objectMode) {
- return;
- }
- const fileStdioItem = stdioItems.find(({type}) => FILE_TYPES.has(type));
- if (fileStdioItem !== undefined) {
- throw new TypeError(`The \`${fileStdioItem.optionName}\` option cannot use both files and transforms in objectMode.`);
- }
- };
- // Some `stdio` values require Execa to create streams.
- // For example, file paths create file read/write streams.
- // Those transformations are specified in `addProperties`, which is both direction-specific and type-specific.
- const getFinalFileDescriptors = ({initialFileDescriptors, addProperties, options, isSync}) => {
- const fileDescriptors = [];
- try {
- for (const fileDescriptor of initialFileDescriptors) {
- fileDescriptors.push(getFinalFileDescriptor({
- fileDescriptor,
- fileDescriptors,
- addProperties,
- options,
- isSync,
- }));
- }
- return fileDescriptors;
- } catch (error) {
- cleanupCustomStreams(fileDescriptors);
- throw error;
- }
- };
- const getFinalFileDescriptor = ({
- fileDescriptor: {direction, objectMode, stdioItems},
- fileDescriptors,
- addProperties,
- options,
- isSync,
- }) => {
- const finalStdioItems = stdioItems.map(stdioItem => addStreamProperties({
- stdioItem,
- addProperties,
- direction,
- options,
- fileDescriptors,
- isSync,
- }));
- return {direction, objectMode, stdioItems: finalStdioItems};
- };
- const addStreamProperties = ({stdioItem, addProperties, direction, options, fileDescriptors, isSync}) => {
- const duplicateStream = getDuplicateStream({
- stdioItem,
- direction,
- fileDescriptors,
- isSync,
- });
- if (duplicateStream !== undefined) {
- return {...stdioItem, stream: duplicateStream};
- }
- return {
- ...stdioItem,
- ...addProperties[direction][stdioItem.type](stdioItem, options),
- };
- };
- // The stream error handling is performed by the piping logic above, which cannot be performed before subprocess spawning.
- // If the subprocess spawning fails (e.g. due to an invalid command), the streams need to be manually destroyed.
- // We need to create those streams before subprocess spawning, in case their creation fails, e.g. when passing an invalid generator as argument.
- // Like this, an exception would be thrown, which would prevent spawning a subprocess.
- export const cleanupCustomStreams = fileDescriptors => {
- for (const {stdioItems} of fileDescriptors) {
- for (const {stream} of stdioItems) {
- if (stream !== undefined && !isStandardStream(stream)) {
- stream.destroy();
- }
- }
- }
- };
- // When the `std*: Iterable | WebStream | URL | filePath`, `input` or `inputFile` option is used, we pipe to `subprocess.std*`.
- // 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*`.
- // Therefore the `std*` options must be either `pipe` or `overlapped`. Other values do not set `subprocess.std*`.
- const forwardStdio = stdioItems => {
- if (stdioItems.length > 1) {
- return stdioItems.some(({value}) => value === 'overlapped') ? 'overlapped' : 'pipe';
- }
- const [{type, value}] = stdioItems;
- return type === 'native' ? value : 'pipe';
- };
|