index.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  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 resolve, reject
  12. const promise = new Promise((_resolve, _reject) => {
  13. resolve = _resolve
  14. reject = _reject
  15. })
  16. // Create error here so we have a more useful stack trace when rejecting
  17. const closeError = new Error('command failed')
  18. const stdout = []
  19. const stderr = []
  20. const getResult = (result) => ({
  21. cmd,
  22. args,
  23. ...result,
  24. ...stdioResult(stdout, stderr, opts),
  25. ...extra,
  26. })
  27. const rejectWithOpts = (er, erOpts) => {
  28. const resultError = getResult(erOpts)
  29. reject(Object.assign(er, resultError))
  30. }
  31. const proc = spawn(cmd, args, opts)
  32. promise.stdin = proc.stdin
  33. promise.process = proc
  34. proc.on('error', rejectWithOpts)
  35. if (proc.stdout) {
  36. proc.stdout.on('data', c => stdout.push(c))
  37. proc.stdout.on('error', rejectWithOpts)
  38. }
  39. if (proc.stderr) {
  40. proc.stderr.on('data', c => stderr.push(c))
  41. proc.stderr.on('error', rejectWithOpts)
  42. }
  43. proc.on('close', (code, signal) => {
  44. if (code || signal) {
  45. rejectWithOpts(closeError, { code, signal })
  46. } else {
  47. resolve(getResult({ code, signal }))
  48. }
  49. })
  50. return promise
  51. }
  52. const spawnWithShell = (cmd, args, opts, extra) => {
  53. let command = opts.shell
  54. // if shell is set to true, we use a platform default. we can't let the core
  55. // spawn method decide this for us because we need to know what shell is in use
  56. // ahead of time so that we can escape arguments properly. we don't need coverage here.
  57. if (command === true) {
  58. // istanbul ignore next
  59. command = process.platform === 'win32' ? process.env.ComSpec : 'sh'
  60. }
  61. const options = { ...opts, shell: false }
  62. const realArgs = []
  63. let script = cmd
  64. // first, determine if we're in windows because if we are we need to know if we're
  65. // running an .exe or a .cmd/.bat since the latter requires extra escaping
  66. const isCmd = /(?:^|\\)cmd(?:\.exe)?$/i.test(command)
  67. if (isCmd) {
  68. let doubleEscape = false
  69. // find the actual command we're running
  70. let initialCmd = ''
  71. let insideQuotes = false
  72. for (let i = 0; i < cmd.length; ++i) {
  73. const char = cmd.charAt(i)
  74. if (char === ' ' && !insideQuotes) {
  75. break
  76. }
  77. initialCmd += char
  78. if (char === '"' || char === "'") {
  79. insideQuotes = !insideQuotes
  80. }
  81. }
  82. let pathToInitial
  83. try {
  84. pathToInitial = which.sync(initialCmd, {
  85. path: (options.env && findInObject(options.env, 'PATH')) || process.env.PATH,
  86. pathext: (options.env && findInObject(options.env, 'PATHEXT')) || process.env.PATHEXT,
  87. }).toLowerCase()
  88. } catch (err) {
  89. pathToInitial = initialCmd.toLowerCase()
  90. }
  91. doubleEscape = pathToInitial.endsWith('.cmd') || pathToInitial.endsWith('.bat')
  92. for (const arg of args) {
  93. script += ` ${escape.cmd(arg, doubleEscape)}`
  94. }
  95. realArgs.push('/d', '/s', '/c', script)
  96. options.windowsVerbatimArguments = true
  97. } else {
  98. for (const arg of args) {
  99. script += ` ${escape.sh(arg)}`
  100. }
  101. realArgs.push('-c', script)
  102. }
  103. return promiseSpawn(command, realArgs, options, extra)
  104. }
  105. // open a file with the default application as defined by the user's OS
  106. const open = (_args, opts = {}, extra = {}) => {
  107. const options = { ...opts, shell: true }
  108. const args = [].concat(_args)
  109. let platform = process.platform
  110. // process.platform === 'linux' may actually indicate WSL, if that's the case
  111. // open the argument with sensible-browser which is pre-installed
  112. // In WSL, set the default browser using, for example,
  113. // export BROWSER="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
  114. // or
  115. // export BROWSER="/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
  116. // To permanently set the default browser, add the appropriate entry to your shell's
  117. // RC file, e.g. .bashrc or .zshrc.
  118. if (platform === 'linux' && os.release().toLowerCase().includes('microsoft')) {
  119. platform = 'wsl'
  120. if (!process.env.BROWSER) {
  121. return Promise.reject(
  122. new Error('Set the BROWSER environment variable to your desired browser.'))
  123. }
  124. }
  125. let command = options.command
  126. if (!command) {
  127. if (platform === 'win32') {
  128. // spawnWithShell does not do the additional os.release() check, so we
  129. // have to force the shell here to make sure we treat WSL as windows.
  130. options.shell = process.env.ComSpec
  131. // also, the start command accepts a title so to make sure that we don't
  132. // accidentally interpret the first arg as the title, we stick an empty
  133. // string immediately after the start command
  134. command = 'start ""'
  135. } else if (platform === 'wsl') {
  136. command = 'sensible-browser'
  137. } else if (platform === 'darwin') {
  138. command = 'open'
  139. } else {
  140. command = 'xdg-open'
  141. }
  142. }
  143. return spawnWithShell(command, args, options, extra)
  144. }
  145. promiseSpawn.open = open
  146. const isPipe = (stdio = 'pipe', fd) => {
  147. if (stdio === 'pipe' || stdio === null) {
  148. return true
  149. }
  150. if (Array.isArray(stdio)) {
  151. return isPipe(stdio[fd], fd)
  152. }
  153. return false
  154. }
  155. const stdioResult = (stdout, stderr, { stdioString = true, stdio }) => {
  156. const result = {
  157. stdout: null,
  158. stderr: null,
  159. }
  160. // stdio is [stdin, stdout, stderr]
  161. if (isPipe(stdio, 1)) {
  162. result.stdout = Buffer.concat(stdout)
  163. if (stdioString) {
  164. result.stdout = result.stdout.toString().trim()
  165. }
  166. }
  167. if (isPipe(stdio, 2)) {
  168. result.stderr = Buffer.concat(stderr)
  169. if (stdioString) {
  170. result.stderr = result.stderr.toString().trim()
  171. }
  172. }
  173. return result
  174. }
  175. // case insensitive lookup in an object
  176. const findInObject = (obj, key) => {
  177. key = key.toLowerCase()
  178. for (const objKey of Object.keys(obj).sort()) {
  179. if (objKey.toLowerCase() === key) {
  180. return obj[objKey]
  181. }
  182. }
  183. }
  184. module.exports = promiseSpawn