162 lines
4.4 KiB
JavaScript
162 lines
4.4 KiB
JavaScript
/**
|
|
* Bare-compatible process helpers.
|
|
*
|
|
* bare-subprocess (mapped from `child_process` in Bare) provides spawn /
|
|
* spawnSync only — not Node's execFile. Collectors should use this module.
|
|
*/
|
|
import { spawn, spawnSync } from 'child_process'
|
|
|
|
/**
|
|
* @param {unknown} buf
|
|
* @param {BufferEncoding|'buffer'|undefined} encoding
|
|
*/
|
|
function decode(buf, encoding = 'utf8') {
|
|
if (buf == null) return encoding === 'buffer' ? Buffer.alloc(0) : ''
|
|
if (encoding === 'buffer') {
|
|
return Buffer.isBuffer(buf) ? buf : Buffer.from(buf)
|
|
}
|
|
if (typeof buf === 'string') return buf
|
|
return Buffer.from(buf).toString(encoding || 'utf8')
|
|
}
|
|
|
|
/**
|
|
* Run a command and return stdout/stderr (Promise), Bare-safe.
|
|
*
|
|
* @param {string} file
|
|
* @param {string[]} [args]
|
|
* @param {{
|
|
* cwd?: string,
|
|
* env?: NodeJS.ProcessEnv,
|
|
* timeout?: number,
|
|
* encoding?: BufferEncoding|'buffer',
|
|
* maxBuffer?: number,
|
|
* }} [opts]
|
|
* @returns {Promise<{ stdout: string|Buffer, stderr: string|Buffer }>}
|
|
*/
|
|
export function execFile(file, args = [], opts = {}) {
|
|
const encoding = opts.encoding ?? 'utf8'
|
|
const timeout = Number(opts.timeout) || 0
|
|
const maxBuffer = Number(opts.maxBuffer) || 4 * 1024 * 1024
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let child
|
|
try {
|
|
child = spawn(file, args, {
|
|
cwd: opts.cwd,
|
|
env: opts.env,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
})
|
|
} catch (err) {
|
|
reject(err)
|
|
return
|
|
}
|
|
|
|
/** @type {Buffer[]} */
|
|
const outChunks = []
|
|
/** @type {Buffer[]} */
|
|
const errChunks = []
|
|
let outLen = 0
|
|
let errLen = 0
|
|
let settled = false
|
|
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
let timer = null
|
|
|
|
const settle = (err, result) => {
|
|
if (settled) return
|
|
settled = true
|
|
if (timer) clearTimeout(timer)
|
|
if (err) reject(err)
|
|
else resolve(result)
|
|
}
|
|
|
|
if (timeout > 0) {
|
|
timer = setTimeout(() => {
|
|
try {
|
|
child.kill()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
const err = new Error(`Command timed out: ${file}`)
|
|
err.killed = true
|
|
settle(err)
|
|
}, timeout)
|
|
}
|
|
|
|
const onChunk = (chunks, which) => (chunk) => {
|
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
if (which === 'out') {
|
|
outLen += buf.length
|
|
if (outLen > maxBuffer) {
|
|
try {
|
|
child.kill()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
settle(new Error(`stdout maxBuffer exceeded: ${file}`))
|
|
return
|
|
}
|
|
} else {
|
|
errLen += buf.length
|
|
if (errLen > maxBuffer) {
|
|
try {
|
|
child.kill()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
settle(new Error(`stderr maxBuffer exceeded: ${file}`))
|
|
return
|
|
}
|
|
}
|
|
chunks.push(buf)
|
|
}
|
|
|
|
if (child.stdout) child.stdout.on('data', onChunk(outChunks, 'out'))
|
|
if (child.stderr) child.stderr.on('data', onChunk(errChunks, 'err'))
|
|
child.on('error', (err) => settle(err))
|
|
child.on('close', (code, signal) => {
|
|
const stdout = decode(Buffer.concat(outChunks), encoding)
|
|
const stderr = decode(Buffer.concat(errChunks), encoding)
|
|
if (code && code !== 0) {
|
|
const err = new Error(`Command failed: ${file} ${args.join(' ')}`)
|
|
err.status = code
|
|
err.code = code
|
|
err.signal = signal
|
|
err.stdout = stdout
|
|
err.stderr = stderr
|
|
settle(err)
|
|
return
|
|
}
|
|
settle(null, { stdout, stderr })
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Synchronous helper for short tools (zfs/unbound-style).
|
|
*
|
|
* @param {string} file
|
|
* @param {string[]} [args]
|
|
* @param {{ cwd?: string, env?: NodeJS.ProcessEnv, encoding?: BufferEncoding|'buffer', maxBuffer?: number }} [opts]
|
|
*/
|
|
export function execFileSync(file, args = [], opts = {}) {
|
|
const encoding = opts.encoding ?? 'utf8'
|
|
const res = spawnSync(file, args, {
|
|
cwd: opts.cwd,
|
|
env: opts.env,
|
|
maxBuffer: opts.maxBuffer || 4 * 1024 * 1024,
|
|
})
|
|
if (res.error) throw res.error
|
|
const stdout = decode(res.stdout, encoding)
|
|
const stderr = decode(res.stderr, encoding)
|
|
if (res.status && res.status !== 0) {
|
|
const err = new Error(`Command failed: ${file} ${args.join(' ')}`)
|
|
err.status = res.status
|
|
err.code = res.status
|
|
err.signal = res.signal
|
|
err.stdout = stdout
|
|
err.stderr = stderr
|
|
throw err
|
|
}
|
|
return { stdout, stderr, status: res.status ?? 0 }
|
|
}
|