native.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import {readFileSync} from 'node:fs';
  2. import tty from 'node:tty';
  3. import {isStream as isNodeStream} from 'is-stream';
  4. import {STANDARD_STREAMS} from '../utils/standard-stream.js';
  5. import {bufferToUint8Array} from '../utils/uint-array.js';
  6. import {serializeOptionValue} from '../arguments/fd-options.js';
  7. // When we use multiple `stdio` values for the same streams, we pass 'pipe' to `child_process.spawn()`.
  8. // We then emulate the piping done by core Node.js.
  9. // To do so, we transform the following values:
  10. // - Node.js streams are marked as `type: nodeStream`
  11. // - 'inherit' becomes `process.stdin|stdout|stderr`
  12. // - any file descriptor integer becomes `process.stdio[fdNumber]`
  13. // All of the above transformations tell Execa to perform manual piping.
  14. export const handleNativeStream = ({stdioItem, stdioItem: {type}, isStdioArray, fdNumber, direction, isSync}) => {
  15. if (!isStdioArray || type !== 'native') {
  16. return stdioItem;
  17. }
  18. return isSync
  19. ? handleNativeStreamSync({stdioItem, fdNumber, direction})
  20. : handleNativeStreamAsync({stdioItem, fdNumber});
  21. };
  22. // Synchronous methods use a different logic.
  23. // 'inherit', file descriptors and process.std* are handled by readFileSync()/writeFileSync().
  24. const handleNativeStreamSync = ({stdioItem, stdioItem: {value, optionName}, fdNumber, direction}) => {
  25. const targetFd = getTargetFd({
  26. value,
  27. optionName,
  28. fdNumber,
  29. direction,
  30. });
  31. if (targetFd !== undefined) {
  32. return targetFd;
  33. }
  34. if (isNodeStream(value, {checkOpen: false})) {
  35. throw new TypeError(`The \`${optionName}: Stream\` option cannot both be an array and include a stream with synchronous methods.`);
  36. }
  37. return stdioItem;
  38. };
  39. const getTargetFd = ({value, optionName, fdNumber, direction}) => {
  40. const targetFdNumber = getTargetFdNumber(value, fdNumber);
  41. if (targetFdNumber === undefined) {
  42. return;
  43. }
  44. if (direction === 'output') {
  45. return {type: 'fileNumber', value: targetFdNumber, optionName};
  46. }
  47. if (tty.isatty(targetFdNumber)) {
  48. throw new TypeError(`The \`${optionName}: ${serializeOptionValue(value)}\` option is invalid: it cannot be a TTY with synchronous methods.`);
  49. }
  50. return {type: 'uint8Array', value: bufferToUint8Array(readFileSync(targetFdNumber)), optionName};
  51. };
  52. const getTargetFdNumber = (value, fdNumber) => {
  53. if (value === 'inherit') {
  54. return fdNumber;
  55. }
  56. if (typeof value === 'number') {
  57. return value;
  58. }
  59. const standardStreamIndex = STANDARD_STREAMS.indexOf(value);
  60. if (standardStreamIndex !== -1) {
  61. return standardStreamIndex;
  62. }
  63. };
  64. const handleNativeStreamAsync = ({stdioItem, stdioItem: {value, optionName}, fdNumber}) => {
  65. if (value === 'inherit') {
  66. return {type: 'nodeStream', value: getStandardStream(fdNumber, value, optionName), optionName};
  67. }
  68. if (typeof value === 'number') {
  69. return {type: 'nodeStream', value: getStandardStream(value, value, optionName), optionName};
  70. }
  71. if (isNodeStream(value, {checkOpen: false})) {
  72. return {type: 'nodeStream', value, optionName};
  73. }
  74. return stdioItem;
  75. };
  76. // Node.js does not allow to easily retrieve file descriptors beyond stdin/stdout/stderr as streams.
  77. // - `fs.createReadStream()`/`fs.createWriteStream()` with the `fd` option do not work with character devices that use blocking reads/writes (such as interactive TTYs).
  78. // - Using a TCP `Socket` would work but be rather complex to implement.
  79. // Since this is an edge case, we simply throw an error message.
  80. // See https://github.com/sindresorhus/execa/pull/643#discussion_r1435905707
  81. const getStandardStream = (fdNumber, value, optionName) => {
  82. const standardStream = STANDARD_STREAMS[fdNumber];
  83. if (standardStream === undefined) {
  84. throw new TypeError(`The \`${optionName}: ${value}\` option is invalid: no such standard stream.`);
  85. }
  86. return standardStream;
  87. };