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 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._closing = []
this._handle = binding.init(this, this._onexit)
}
_flush() {
for (const pipe of this.stdio) {
if (pipe && typeof pipe.resume === 'function') pipe.resume()
}
}
async _onexit(code, signal) {
this.exitCode = code
this.signalCode = signal
this.emit('exit', code, signal)
queueMicrotask(this._flush.bind(this)) // Defer to provide a last chance to consume
await Promise.all(this._closing)
this.emit('close', code, signal)
}
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
}
}
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
} = 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()
subprocess.spawnfile = file
subprocess.spawnargs = args
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 {
stdio[i] = { flags: binding.UV_INHERIT_FD, fd }
}
}
subprocess.pid = binding.spawn(
subprocess._handle,
file,
args,
cwd,
pairs,
stdio,
detached,
uid,
gid,
windowsHide,
windowsVerbatimArguments
)
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 }
}
}
subprocess.pid = binding.spawnSync(
subprocess._handle,
file,
args,
cwd,
pairs,
stdio,
detached,
uid,
gid,
windowsHide,
windowsVerbatimArguments
)
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
}