OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / bare-subprocess / index.js

458 lines · javascript ref:
const EventEmitter = require('bare-events')
const Pipe = require('bare-pipe')
const os = require('bare-os')
const env = require('bare-env')
const { isURL, fileURLToPath } = require('bare-url')
const binding = require('./binding')
const Channel = require('./lib/channel')
const JSONFramer = require('./lib/framers/json')
const AdvancedFramer = require('./lib/framers/advanced')
const constants = require('./lib/constants')
const errors = require('./lib/errors')

exports.Subprocess = class Subprocess extends EventEmitter {
  constructor() {
    super()

    this.spawnfile = null
    this.spawnargs = []
    this.pid = null
    this.stdio = []
    this.exitCode = null
    this.signalCode = null
    this.killed = false
    this.channel = undefined

    this._closing = []
    this._handle = binding.init(this, this._onexit)
  }

  get connected() {
    return this.channel !== undefined
  }

  get stdin() {
    return this.stdio[0] || null
  }

  get stdout() {
    return this.stdio[1] || null
  }

  get stderr() {
    return this.stdio[2] || null
  }

  ref() {
    for (const pipe of this.stdio) {
      if (pipe && typeof pipe.ref === 'function') pipe.ref()
    }

    binding.ref(this._handle)
  }

  unref() {
    for (const pipe of this.stdio) {
      if (pipe && typeof pipe.unref === 'function') pipe.unref()
    }

    binding.unref(this._handle)
  }

  kill(signum = constants.SIGTERM) {
    if (typeof signum === 'string') {
      if (signum in constants === false) {
        throw errors.UNKNOWN_SIGNAL('Unknown signal: ' + signum)
      }

      signum = constants[signum]
    }

    binding.kill(this._handle, signum)

    this.killed = true
  }

  send(message, handle = null, cb) {
    if (typeof handle === 'function') {
      cb = handle
      handle = null
    }

    if (this.channel === undefined) {
      const err = errors.NO_IPC_CHANNEL('Subprocess has no IPC channel')
      if (cb) queueMicrotask(() => cb(err))
      return false
    }

    return this.channel.send(message, handle, cb)
  }

  disconnect() {
    if (this.channel === undefined) return
    this.channel.disconnect()
  }

  _flush() {
    for (const pipe of this.stdio) {
      if (pipe && typeof pipe.resume === 'function') pipe.resume()
    }
  }

  async _onexit(code, signal) {
    if (signal) {
      for (const [name, val] of Object.entries(os.constants.signals)) {
        if (signal === val) {
          this.signalCode = name
          break
        }
      }
    } else {
      this.exitCode = code
    }

    this.emit('exit', this.exitCode, this.signalCode)

    queueMicrotask(this._flush.bind(this)) // Defer to provide a last chance to consume

    await Promise.all(this._closing)

    this.emit('close', this.exitCode, this.signalCode)
  }
}

exports.ChildProcess = exports.Subprocess // For Node.js compatibility

exports.constants = constants
exports.errors = errors

exports.spawn = function spawn(file, args, opts) {
  if (Array.isArray(args)) {
    args = [...args]
  } else if (args === null) {
    args = []
  } else {
    opts = args
    args = []
  }

  if (!opts) opts = {}

  args = args.map(String)

  let {
    cwd = defaultCwd(),
    env = defaultEnv(),
    stdio = [],
    detached = false,
    shell = false,
    uid = -1,
    gid = -1,
    windowsHide = false,
    windowsVerbatimArguments = false,
    serialization = 'json'
  } = opts

  if (serialization !== 'json' && serialization !== 'advanced' && serialization !== 'binary') {
    throw errors.UNKNOWN_SERIALIZATION_MODE(`Unknown serialization mode '${serialization}'`)
  }

  file = toPath(file)
  cwd = toPath(cwd)

  if (shell) {
    const command = [file, ...args].join(' ')

    if (Bare.platform === 'win32') {
      if (typeof shell === 'string') {
        file = shell
      } else {
        file = env.comspec || 'cmd.exe'
      }

      if (/^(.*\\)?cmd(\.exe)?$/i.test(file)) {
        args = ['/d', '/s', '/c', `"${command}"`]
        windowsVerbatimArguments = true
      } else {
        args = ['-c', command]
      }
    } else {
      if (typeof shell === 'string') {
        file = shell
      } else if (Bare.platform === 'android') {
        file = '/system/bin/sh'
      } else {
        file = '/bin/sh'
      }

      args = ['-c', command]
    }
  }

  const pairs = []

  for (const [key, value] of Object.entries(env)) {
    pairs.push(`${key}=${value}`)
  }

  if (Array.isArray(stdio)) {
    stdio = [...stdio]
  } else if (typeof stdio === 'string') {
    stdio = [stdio, stdio, stdio]
  } else {
    stdio = []
  }

  const subprocess = new exports.Subprocess()

  subprocess.spawnfile = file
  subprocess.spawnargs = args

  let ipcSlot = -1

  for (let i = 0, n = Math.max(3, stdio.length); i < n; i++) {
    subprocess.stdio[i] = null

    let fd = stdio[i] || 'pipe'

    if (fd === 'inherit') fd = i < 3 ? i : 'ignore'

    if (fd === 'ignore') {
      stdio[i] = { flags: binding.UV_IGNORE }
    } else if (fd === 'pipe' || fd === 'overlapped') {
      const pipe = new Pipe()

      pipe._onspawn(i !== 0 /* Readable */, i === 0 || i > 2 /* Writable */)

      if (i > 0) {
        subprocess._closing.push(new Promise((resolve) => pipe.once('close', resolve)))
      }

      let flags = binding.UV_CREATE_PIPE | binding.UV_READABLE_PIPE | binding.UV_WRITABLE_PIPE

      if (fd === 'overlapped') flags |= binding.UV_NONBLOCK_PIPE

      stdio[i] = { flags, pipe: pipe._handle }

      subprocess.stdio[i] = pipe
    } else if (fd === 'ipc') {
      if (ipcSlot !== -1) {
        throw errors.IPC_CHANNEL_ALREADY_DEFINED('Only one IPC channel per subprocess')
      }

      ipcSlot = i

      const pipe = new Pipe({ ipc: true })

      pipe._onspawn(true, true)

      subprocess._closing.push(new Promise((resolve) => pipe.once('close', resolve)))

      const flags = binding.UV_CREATE_PIPE | binding.UV_READABLE_PIPE | binding.UV_WRITABLE_PIPE

      stdio[i] = { flags, pipe: pipe._handle }

      subprocess.stdio[i] = pipe
    } else {
      stdio[i] = { flags: binding.UV_INHERIT_FD, fd }
    }
  }

  if (ipcSlot !== -1) {
    pairs.push(`BARE_CHANNEL_FD=${ipcSlot}`, `BARE_CHANNEL_SERIALIZATION_MODE=${serialization}`)
  }

  subprocess.pid = binding.spawn(
    subprocess._handle,
    file,
    args,
    cwd,
    pairs,
    stdio,
    detached,
    uid,
    gid,
    windowsHide,
    windowsVerbatimArguments
  )

  if (ipcSlot !== -1 && serialization !== 'binary') {
    let framer

    if (serialization === 'json') {
      framer = new JSONFramer()
    } else if (serialization === 'advanced') {
      framer = new AdvancedFramer()
    }

    subprocess.channel = new Channel(subprocess, subprocess.stdio[ipcSlot], framer)

    subprocess.once('disconnect', () => {
      subprocess.channel = undefined
    })
  }

  return subprocess
}

exports.spawnSync = function spawn(file, args, opts) {
  if (Array.isArray(args)) {
    args = [...args]
  } else if (args === null) {
    args = []
  } else {
    opts = args
    args = []
  }

  if (!opts) opts = {}

  let {
    cwd = defaultCwd(),
    env = defaultEnv(),
    input = null,
    stdio = [],
    detached = false,
    shell = false,
    uid = -1,
    gid = -1,
    windowsHide = false,
    windowsVerbatimArguments = false,
    maxBuffer = 1024 * 1024
  } = opts

  file = toPath(file)
  cwd = toPath(cwd)

  if (shell) {
    const command = [file, ...args].join(' ')

    if (Bare.platform === 'win32') {
      if (typeof shell === 'string') {
        file = shell
      } else {
        file = env.comspec || 'cmd.exe'
      }

      if (/^(.*\\)?cmd(\.exe)?$/i.test(file)) {
        args = ['/d', '/s', '/c', `"${command}"`]
        windowsVerbatimArguments = true
      } else {
        args = ['-c', command]
      }
    } else {
      if (typeof shell === 'string') {
        file = shell
      } else if (Bare.platform === 'android') {
        file = '/system/bin/sh'
      } else {
        file = '/bin/sh'
      }

      args = ['-c', command]
    }
  }

  const pairs = []

  for (const [key, value] of Object.entries(env)) {
    pairs.push(`${key}=${value}`)
  }

  if (Array.isArray(stdio)) {
    stdio = [...stdio]
  } else if (typeof stdio === 'string') {
    stdio = [stdio, stdio, stdio]
  } else {
    stdio = []
  }

  const subprocess = new exports.Subprocess()

  if (input) {
    stdio[0] = {
      flags: binding.UV_CREATE_PIPE | binding.UV_READABLE_PIPE,
      buffer: input
    }

    subprocess.stdio[0] = null
  }

  for (let i = input ? 1 : 0, n = Math.max(3, stdio.length); i < n; i++) {
    subprocess.stdio[i] = null

    let fd = stdio[i] || 'pipe'

    if (fd === 'inherit') fd = i < 3 ? i : 'ignore'

    if (fd === 'ignore') {
      stdio[i] = { flags: binding.UV_IGNORE }
    } else if (fd === 'pipe') {
      const buffer = Buffer.alloc(maxBuffer)

      stdio[i] = {
        flags: binding.UV_CREATE_PIPE | binding.UV_WRITABLE_PIPE,
        buffer,
        written: 0
      }

      if (i > 0) subprocess.stdio[i] = buffer
    } else if (typeof fd === 'number') {
      stdio[i] = { flags: binding.UV_INHERIT_FD, fd }
    }
  }

  let error
  try {
    subprocess.pid = binding.spawnSync(
      subprocess._handle,
      file,
      args,
      cwd,
      pairs,
      stdio,
      detached,
      uid,
      gid,
      windowsHide,
      windowsVerbatimArguments
    )
  } catch (err) {
    error = err
  }

  if (error) return { status: null, signal: null, output: null, pid: 0, error }

  for (let i = 1, n = stdio.length; i < n; i++) {
    if (stdio[i].flags & binding.UV_WRITABLE_PIPE) {
      subprocess.stdio[i] = subprocess.stdio[i].subarray(0, stdio[i].written)
    }
  }

  return {
    status: subprocess.exitCode,
    signal: subprocess.signalCode,
    output: subprocess.stdio,
    pid: subprocess.pid,
    stdout: subprocess.stdout,
    stderr: subprocess.stderr
  }
}

function defaultCwd() {
  return os.cwd()
}

function defaultEnv() {
  return env
}

function toPath(filepath) {
  if (typeof filepath !== 'string') {
    if (isURL(filepath)) filepath = fileURLToPath(filepath)
    else filepath = filepath.toString()
  }

  return filepath
}