index.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. 'use strict'
  2. const { spawn } = require('child_process')
  3. const os = require('os')
  4. const which = require('which')
  5. const escape = require('./escape.js')
  6. // 'extra' object is for decorating the error a bit more
  7. const promiseSpawn = (cmd, args, opts = {}, extra = {}) => {
  8. if (opts.shell) {
  9. return spawnWithShell(cmd, args, opts, extra)
  10. }
  11. let proc
  12. const p = new Promise((res, rej) => {
  13. proc = spawn(cmd, args, opts)
  14. const stdout = []
  15. const stderr = []
  16. const reject = er => rej(Object.assign(er, {
  17. cmd,
  18. args,
  19. ...stdioResult(stdout, stderr, opts),
  20. ...extra,
  21. }))
  22. proc.on('error', reject)
  23. if (proc.stdout) {
  24. proc.stdout.on('data', c => stdout.push(c)).on('error', reject)
  25. proc.stdout.on('error', er => reject(er))
  26. }
  27. if (proc.stderr) {
  28. proc.stderr.on('data', c => stderr.push(c)).on('error', reject)
  29. proc.stderr.on('error', er => reject(er))
  30. }
  31. proc.on('close', (code, signal) => {
  32. const result = {
  33. cmd,
  34. args,
  35. code,
  36. signal,
  37. ...stdioResult(stdout, stderr, opts),
  38. ...extra,
  39. }
  40. if (code || signal) {
  41. rej(Object.assign(new Error('command failed'), result))
  42. } else {
  43. res(result)
  44. }
  45. })
  46. })
  47. p.stdin = proc.stdin
  48. p.process = proc
  49. return p
  50. }
  51. const spawnWithShell = (cmd, args, opts, extra) => {
  52. let command = opts.shell
  53. // if shell is set to true, we use a platform default. we can't let the core
  54. // spawn method decide this for us because we need to know what shell is in use
  55. // ahead of time so that we can escape arguments properly. we don't need coverage here.
  56. if (command === true) {
  57. // istanbul ignore next
  58. command = process.platform === 'win32' ? process.env.ComSpec : 'sh'
  59. }
  60. const options = { ...opts, shell: false }
  61. const realArgs = []
  62. let script = cmd
  63. // first, determine if we're in windows because if we are we need to know if we're
  64. // running an .exe or a .cmd/.bat since the latter requires extra escaping
  65. const isCmd = /(?:^|\\)cmd(?:\.exe)?$/i.test(command)
  66. if (isCmd) {
  67. let doubleEscape = false
  68. // find the actual command we're running
  69. let initialCmd = ''
  70. let insideQuotes = false
  71. for (let i = 0; i < cmd.length; ++i) {
  72. const char = cmd.charAt(i)
  73. if (char === ' ' && !insideQuotes) {
  74. break
  75. }
  76. initialCmd += char
  77. if (char === '"' || char === "'") {
  78. insideQuotes = !insideQuotes
  79. }
  80. }
  81. let pathToInitial
  82. try {
  83. pathToInitial = which.sync(initialCmd, {
  84. path: (options.env && options.env.PATH) || process.env.PATH,
  85. pathext: (options.env && options.env.PATHEXT) || process.env.PATHEXT,
  86. }).toLowerCase()
  87. } catch (err) {
  88. pathToInitial = initialCmd.toLowerCase()
  89. }
  90. doubleEscape = pathToInitial.endsWith('.cmd') || pathToInitial.endsWith('.bat')
  91. for (const arg of args) {
  92. script += ` ${escape.cmd(arg, doubleEscape)}`
  93. }
  94. realArgs.push('/d', '/s', '/c', script)
  95. options.windowsVerbatimArguments = true
  96. } else {
  97. for (const arg of args) {
  98. script += ` ${escape.sh(arg)}`
  99. }
  100. realArgs.push('-c', script)
  101. }
  102. return promiseSpawn(command, realArgs, options, extra)
  103. }
  104. // open a file with the default application as defined by the user's OS
  105. const open = (_args, opts = {}, extra = {}) => {
  106. const options = { ...opts, shell: true }
  107. const args = [].concat(_args)
  108. let platform = process.platform
  109. // process.platform === 'linux' may actually indicate WSL, if that's the case
  110. // we want to treat things as win32 anyway so the host can open the argument
  111. if (platform === 'linux' && os.release().toLowerCase().includes('microsoft')) {
  112. platform = 'win32'
  113. }
  114. let command = options.command
  115. if (!command) {
  116. if (platform === 'win32') {
  117. // spawnWithShell does not do the additional os.release() check, so we
  118. // have to force the shell here to make sure we treat WSL as windows.
  119. options.shell = process.env.ComSpec
  120. // also, the start command accepts a title so to make sure that we don't
  121. // accidentally interpret the first arg as the title, we stick an empty
  122. // string immediately after the start command
  123. command = 'start ""'
  124. } else if (platform === 'darwin') {
  125. command = 'open'
  126. } else {
  127. command = 'xdg-open'
  128. }
  129. }
  130. return spawnWithShell(command, args, options, extra)
  131. }
  132. promiseSpawn.open = open
  133. const isPipe = (stdio = 'pipe', fd) => {
  134. if (stdio === 'pipe' || stdio === null) {
  135. return true
  136. }
  137. if (Array.isArray(stdio)) {
  138. return isPipe(stdio[fd], fd)
  139. }
  140. return false
  141. }
  142. const stdioResult = (stdout, stderr, { stdioString = true, stdio }) => {
  143. const result = {
  144. stdout: null,
  145. stderr: null,
  146. }
  147. // stdio is [stdin, stdout, stderr]
  148. if (isPipe(stdio, 1)) {
  149. result.stdout = Buffer.concat(stdout)
  150. if (stdioString) {
  151. result.stdout = result.stdout.toString().trim()
  152. }
  153. }
  154. if (isPipe(stdio, 2)) {
  155. result.stderr = Buffer.concat(stderr)
  156. if (stdioString) {
  157. result.stderr = result.stderr.toString().trim()
  158. }
  159. }
  160. return result
  161. }
  162. module.exports = promiseSpawn