Files
peardata/server/utils/exec.js
T
Raven Scott 52a1823469
CI / test (push) Successful in 1m4s
Release rolling / release (push) Successful in 7m14s
Updates
2026-07-18 20:01:47 -04:00

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 }
}