205 lines
5.3 KiB
JavaScript
Executable file
205 lines
5.3 KiB
JavaScript
Executable file
'use strict'
|
|
|
|
const { spawn } = require('child_process')
|
|
const os = require('os')
|
|
const which = require('which')
|
|
|
|
const escape = require('./escape.js')
|
|
|
|
// 'extra' object is for decorating the error a bit more
|
|
const promiseSpawn = (cmd, args, opts = {}, extra = {}) => {
|
|
if (opts.shell) {
|
|
return spawnWithShell(cmd, args, opts, extra)
|
|
}
|
|
|
|
let proc
|
|
|
|
const p = new Promise((res, rej) => {
|
|
proc = spawn(cmd, args, opts)
|
|
|
|
const stdout = []
|
|
const stderr = []
|
|
|
|
const reject = er => rej(Object.assign(er, {
|
|
cmd,
|
|
args,
|
|
...stdioResult(stdout, stderr, opts),
|
|
...extra,
|
|
}))
|
|
|
|
proc.on('error', reject)
|
|
|
|
if (proc.stdout) {
|
|
proc.stdout.on('data', c => stdout.push(c)).on('error', reject)
|
|
proc.stdout.on('error', er => reject(er))
|
|
}
|
|
|
|
if (proc.stderr) {
|
|
proc.stderr.on('data', c => stderr.push(c)).on('error', reject)
|
|
proc.stderr.on('error', er => reject(er))
|
|
}
|
|
|
|
proc.on('close', (code, signal) => {
|
|
const result = {
|
|
cmd,
|
|
args,
|
|
code,
|
|
signal,
|
|
...stdioResult(stdout, stderr, opts),
|
|
...extra,
|
|
}
|
|
|
|
if (code || signal) {
|
|
rej(Object.assign(new Error('command failed'), result))
|
|
} else {
|
|
res(result)
|
|
}
|
|
})
|
|
})
|
|
|
|
p.stdin = proc.stdin
|
|
p.process = proc
|
|
return p
|
|
}
|
|
|
|
const spawnWithShell = (cmd, args, opts, extra) => {
|
|
let command = opts.shell
|
|
// if shell is set to true, we use a platform default. we can't let the core
|
|
// spawn method decide this for us because we need to know what shell is in use
|
|
// ahead of time so that we can escape arguments properly. we don't need coverage here.
|
|
if (command === true) {
|
|
// istanbul ignore next
|
|
command = process.platform === 'win32' ? process.env.ComSpec : 'sh'
|
|
}
|
|
|
|
const options = { ...opts, shell: false }
|
|
const realArgs = []
|
|
let script = cmd
|
|
|
|
// first, determine if we're in windows because if we are we need to know if we're
|
|
// running an .exe or a .cmd/.bat since the latter requires extra escaping
|
|
const isCmd = /(?:^|\\)cmd(?:\.exe)?$/i.test(command)
|
|
if (isCmd) {
|
|
let doubleEscape = false
|
|
|
|
// find the actual command we're running
|
|
let initialCmd = ''
|
|
let insideQuotes = false
|
|
for (let i = 0; i < cmd.length; ++i) {
|
|
const char = cmd.charAt(i)
|
|
if (char === ' ' && !insideQuotes) {
|
|
break
|
|
}
|
|
|
|
initialCmd += char
|
|
if (char === '"' || char === "'") {
|
|
insideQuotes = !insideQuotes
|
|
}
|
|
}
|
|
|
|
let pathToInitial
|
|
try {
|
|
pathToInitial = which.sync(initialCmd, {
|
|
path: (options.env && findInObject(options.env, 'PATH')) || process.env.PATH,
|
|
pathext: (options.env && findInObject(options.env, 'PATHEXT')) || process.env.PATHEXT,
|
|
}).toLowerCase()
|
|
} catch (err) {
|
|
pathToInitial = initialCmd.toLowerCase()
|
|
}
|
|
|
|
doubleEscape = pathToInitial.endsWith('.cmd') || pathToInitial.endsWith('.bat')
|
|
for (const arg of args) {
|
|
script += ` ${escape.cmd(arg, doubleEscape)}`
|
|
}
|
|
realArgs.push('/d', '/s', '/c', script)
|
|
options.windowsVerbatimArguments = true
|
|
} else {
|
|
for (const arg of args) {
|
|
script += ` ${escape.sh(arg)}`
|
|
}
|
|
realArgs.push('-c', script)
|
|
}
|
|
|
|
return promiseSpawn(command, realArgs, options, extra)
|
|
}
|
|
|
|
// open a file with the default application as defined by the user's OS
|
|
const open = (_args, opts = {}, extra = {}) => {
|
|
const options = { ...opts, shell: true }
|
|
const args = [].concat(_args)
|
|
|
|
let platform = process.platform
|
|
// process.platform === 'linux' may actually indicate WSL, if that's the case
|
|
// we want to treat things as win32 anyway so the host can open the argument
|
|
if (platform === 'linux' && os.release().toLowerCase().includes('microsoft')) {
|
|
platform = 'win32'
|
|
}
|
|
|
|
let command = options.command
|
|
if (!command) {
|
|
if (platform === 'win32') {
|
|
// spawnWithShell does not do the additional os.release() check, so we
|
|
// have to force the shell here to make sure we treat WSL as windows.
|
|
options.shell = process.env.ComSpec
|
|
// also, the start command accepts a title so to make sure that we don't
|
|
// accidentally interpret the first arg as the title, we stick an empty
|
|
// string immediately after the start command
|
|
command = 'start ""'
|
|
} else if (platform === 'darwin') {
|
|
command = 'open'
|
|
} else {
|
|
command = 'xdg-open'
|
|
}
|
|
}
|
|
|
|
return spawnWithShell(command, args, options, extra)
|
|
}
|
|
promiseSpawn.open = open
|
|
|
|
const isPipe = (stdio = 'pipe', fd) => {
|
|
if (stdio === 'pipe' || stdio === null) {
|
|
return true
|
|
}
|
|
|
|
if (Array.isArray(stdio)) {
|
|
return isPipe(stdio[fd], fd)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
const stdioResult = (stdout, stderr, { stdioString = true, stdio }) => {
|
|
const result = {
|
|
stdout: null,
|
|
stderr: null,
|
|
}
|
|
|
|
// stdio is [stdin, stdout, stderr]
|
|
if (isPipe(stdio, 1)) {
|
|
result.stdout = Buffer.concat(stdout)
|
|
if (stdioString) {
|
|
result.stdout = result.stdout.toString().trim()
|
|
}
|
|
}
|
|
|
|
if (isPipe(stdio, 2)) {
|
|
result.stderr = Buffer.concat(stderr)
|
|
if (stdioString) {
|
|
result.stderr = result.stderr.toString().trim()
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// case insensitive lookup in an object
|
|
const findInObject = (obj, key) => {
|
|
key = key.toLowerCase()
|
|
for (const objKey of Object.keys(obj).sort()) {
|
|
if (objKey.toLowerCase() === key) {
|
|
return obj[objKey]
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = promiseSpawn
|