teen_process
Version:
A grown up version of Node's spawn/exec
158 lines • 6.11 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.exec = exec;
const node_child_process_1 = require("node:child_process");
const shell_quote_1 = require("shell-quote");
const helpers_1 = require("./helpers");
const circular_buffer_1 = require("./circular-buffer");
/**
* Spawns a child process and collects its output.
*
* This is a promisified version of Node's spawn that collects stdout and stderr,
* handles timeouts, and provides error context.
*
* @template T - The options type extending TeenProcessExecOptions
* @param cmd - The command to execute
* @param args - Array of arguments to pass to the command (default: [])
* @param originalOpts - Execution options including timeout, encoding, environment, etc.
* @returns Promise resolving to an object with stdout, stderr, and exit code
*
* @throws {ExecError} When the process exits with non-zero code or times out
*
* @example
* ```typescript
* // Simple execution
* const {stdout, stderr, code} = await exec('ls', ['-la']);
*
* // With timeout and custom encoding
* const result = await exec('long-running-cmd', [], {
* timeout: 5000,
* encoding: 'utf8',
* cwd: '/custom/path'
* });
*
* // Return output as Buffer
* const {stdout} = await exec('cat', ['image.png'], {isBuffer: true});
* ```
*/
async function exec(cmd, args = [], originalOpts = {}) {
// get a quoted representation of the command for error strings
const rep = (0, shell_quote_1.quote)([cmd, ...args]);
const defaults = {
timeout: undefined,
encoding: 'utf8',
killSignal: 'SIGTERM',
cwd: undefined,
env: process.env,
ignoreOutput: false,
stdio: 'inherit',
isBuffer: false,
shell: undefined,
logger: undefined,
maxStdoutBufferSize: circular_buffer_1.MAX_BUFFER_SIZE,
maxStderrBufferSize: circular_buffer_1.MAX_BUFFER_SIZE,
};
const opts = applyDefaults(defaults, originalOpts);
const isBuffer = Boolean(opts.isBuffer);
const spawnOpts = buildSpawnOptions(opts, originalOpts);
return await new Promise((resolve, reject) => {
const proc = (0, node_child_process_1.spawn)(cmd, args, spawnOpts);
const stdoutBuffer = new circular_buffer_1.CircularBuffer(opts.maxStdoutBufferSize);
const stderrBuffer = new circular_buffer_1.CircularBuffer(opts.maxStderrBufferSize);
let timer = null;
proc.on('error', async (err) => {
let error = err;
if (error.code === 'ENOENT') {
error = await (0, helpers_1.formatEnoent)(error, cmd, opts.cwd?.toString());
}
reject(error);
});
if (proc.stdin) {
proc.stdin.on('error', (err) => {
reject(new Error(`Standard input '${err.syscall}' error: ${err.stack}`));
});
}
const handleStream = (streamType, buffer) => {
const stream = proc[streamType];
if (!stream) {
return;
}
stream.on('error', (err) => {
const capitalizedStreamType = streamType.charAt(0).toUpperCase() + streamType.slice(1).toLowerCase();
reject(new Error(`${capitalizedStreamType} '${err.syscall}' error: ${err.stack}`));
});
if (opts.ignoreOutput) {
// https://github.com/nodejs/node/issues/4236
stream.on('data', () => { });
return;
}
stream.on('data', (chunk) => {
buffer.add(chunk);
if (typeof opts.logger?.debug === 'function') {
opts.logger.debug(chunk.toString());
}
});
};
handleStream('stdout', stdoutBuffer);
handleStream('stderr', stderrBuffer);
function getStdio(wantBuffer) {
const stdout = wantBuffer
? stdoutBuffer.value()
: stdoutBuffer.value().toString(opts.encoding);
const stderr = wantBuffer
? stderrBuffer.value()
: stderrBuffer.value().toString(opts.encoding);
return { stdout, stderr };
}
proc.on('close', (code) => {
if (timer) {
clearTimeout(timer);
}
const { stdout, stderr } = getStdio(isBuffer);
if (code === 0) {
resolve({ stdout, stderr, code });
}
else {
const err = Object.assign(new Error(`Command '${rep}' exited with code ${code}`), {
stdout,
stderr,
code,
});
reject(err);
}
});
if (opts.timeout) {
timer = setTimeout(() => {
const { stdout, stderr } = getStdio(isBuffer);
const err = Object.assign(new Error(`Command '${rep}' timed out after ${opts.timeout}ms`), {
stdout,
stderr,
code: null,
});
reject(err);
proc.kill(opts.killSignal ?? 'SIGTERM');
}, opts.timeout);
}
});
}
function applyDefaults(defaults, originalOpts) {
const normalizedOriginalOpts = originalOpts !== null && typeof originalOpts === 'object' ? originalOpts : {};
const definedOriginalOpts = Object.fromEntries(Object.entries(normalizedOriginalOpts).filter(([, value]) => value !== undefined));
return { ...defaults, ...definedOriginalOpts };
}
function buildSpawnOptions(opts, originalOpts) {
return {
cwd: opts.cwd,
env: opts.env,
shell: opts.shell,
argv0: opts.argv0,
uid: opts.uid,
gid: opts.gid,
detached: opts.detached,
windowsHide: opts.windowsHide,
windowsVerbatimArguments: opts.windowsVerbatimArguments,
signal: opts.signal,
...(Object.prototype.hasOwnProperty.call(originalOpts, 'stdio') ? { stdio: opts.stdio } : {}),
};
}
//# sourceMappingURL=exec.js.map