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