OverviewFilesCommitsIssuesDocsReleasesPeers

host-services / lib / run.js

54 lines · javascript ref:
// 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)
    })
  })
}