-
Notifications
You must be signed in to change notification settings - Fork 12
/
index.js
218 lines (184 loc) · 6.03 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
'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 resolve, reject
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve
reject = _reject
})
// Create error here so we have a more useful stack trace when rejecting
const closeError = new Error('command failed')
const stdout = []
const stderr = []
const getResult = (result) => ({
cmd,
args,
...result,
...stdioResult(stdout, stderr, opts),
...extra,
})
const rejectWithOpts = (er, erOpts) => {
const resultError = getResult(erOpts)
reject(Object.assign(er, resultError))
}
const proc = spawn(cmd, args, opts)
promise.stdin = proc.stdin
promise.process = proc
proc.on('error', rejectWithOpts)
if (proc.stdout) {
proc.stdout.on('data', c => stdout.push(c))
proc.stdout.on('error', rejectWithOpts)
}
if (proc.stderr) {
proc.stderr.on('data', c => stderr.push(c))
proc.stderr.on('error', rejectWithOpts)
}
proc.on('close', (code, signal) => {
if (code || signal) {
rejectWithOpts(closeError, { code, signal })
} else {
resolve(getResult({ code, signal }))
}
})
return promise
}
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
// open the argument with sensible-browser which is pre-installed
// In WSL, set the default browser using, for example,
// export BROWSER="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
// or
// export BROWSER="/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
// To permanently set the default browser, add the appropriate entry to your shell's
// RC file, e.g. .bashrc or .zshrc.
if (platform === 'linux' && os.release().toLowerCase().includes('microsoft')) {
platform = 'wsl'
if (!process.env.BROWSER) {
return Promise.reject(
new Error('Set the BROWSER environment variable to your desired browser.'))
}
}
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 === 'wsl') {
command = 'sensible-browser'
} 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