// Run a command, collect stdout, never hang.
//
// Launched via `sh -c 'exec "$0" "$@"'` (as cthuloid-web's discovery does): in
// bare-subprocess 6.1.0 a spawn of a missing binary throws ENOENT AFTER
// creating its stdio pipes and never closes them. sh always exists; a missing
// command becomes exit 127 instead.
import { spawn } from 'bare-subprocess'
export function run (cmd, args = [], { timeoutMs = 15000, maxBytes = 4 * 1024 * 1024, env = null } = {}) {
return new Promise((resolve) => {
let proc
try {
const opts = { stdio: ['ignore', 'pipe', 'pipe'] }
if (env) opts.env = env
proc = spawn('/bin/sh', ['-c', 'exec "$0" "$@"', cmd, ...args], opts)
} catch (err) {
return resolve({ code: -1, stdout: '', stderr: err.message || String(err) })
}
const out = []
const errs = []
let size = 0
let done = false
const finish = (code) => {
if (done) return
done = true
clearTimeout(timer)
resolve({ code, stdout: Buffer.concat(out).toString(), stderr: Buffer.concat(errs).toString() })
}
const timer = setTimeout(() => {
try { proc.kill() } catch {}
finish(-2)
}, timeoutMs)
// 'exit' can arrive before the last stdout chunk: finish only when both
// the process has exited and its stdout has ended.
let exitCode = null
let ended = false
proc.stdout.on('data', (d) => {
size += d.length
if (size <= maxBytes) out.push(d)
})
proc.stdout.on('end', () => {
ended = true
if (exitCode !== null) finish(exitCode)
})
proc.stderr.on('data', (d) => errs.push(d))
proc.on('error', () => finish(-1))
proc.on('exit', (code) => {
exitCode = code
if (ended) finish(code)
})
})
}