@decaf-ts/utils
Version:
module management utils for decaf-ts
243 lines • 9.48 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.lockify = lockify;
exports.chainAbortController = chainAbortController;
exports.spawnCommand = spawnCommand;
exports.runCommand = runCommand;
const child_process_1 = require("child_process");
const StandardOutputWriter_1 = require("./../writers/StandardOutputWriter.cjs");
const constants_1 = require("./constants.cjs");
const logging_1 = require("@decaf-ts/logging");
/**
* @description Creates a locked version of a function.
* @summary This higher-order function takes a function and returns a new function that ensures
* sequential execution of the original function, even when called multiple times concurrently.
* It uses a Promise-based locking mechanism to queue function calls.
*
* @template R - The return type of the input function.
*
* @param f - The function to be locked. It can take any number of parameters and return a value of type R.
* @return A new function with the same signature as the input function, but with sequential execution guaranteed.
*
* @function lockify
*
* @mermaid
* sequenceDiagram
* participant Caller
* participant LockedFunction
* participant OriginalFunction
* Caller->>LockedFunction: Call with params
* LockedFunction->>LockedFunction: Check current lock
* alt Lock is resolved
* LockedFunction->>OriginalFunction: Execute with params
* OriginalFunction-->>LockedFunction: Return result
* LockedFunction-->>Caller: Return result
* else Lock is pending
* LockedFunction->>LockedFunction: Queue execution
* LockedFunction-->>Caller: Return promise
* Note over LockedFunction: Wait for previous execution
* LockedFunction->>OriginalFunction: Execute with params
* OriginalFunction-->>LockedFunction: Return result
* LockedFunction-->>Caller: Resolve promise with result
* end
* LockedFunction->>LockedFunction: Update lock
*
* @memberOf module:utils
*/
function lockify(f) {
let lock = Promise.resolve();
return (...params) => {
const result = lock.then(() => f(...params));
lock = result.catch(() => { });
return result;
};
}
function chainAbortController(argument0, ...remainder) {
let signals;
let controller;
// normalize args
if (argument0 instanceof AbortSignal) {
controller = new AbortController();
signals = [argument0, ...remainder];
}
else {
controller = argument0;
signals = remainder;
}
// if the controller is already aborted, exit early
if (controller.signal.aborted) {
return controller;
}
const handler = () => controller.abort();
for (const signal of signals) {
// check before adding! (and assume there is no possible way that the signal could
// abort between the `if` check and adding the event listener)
if (signal.aborted) {
controller.abort();
break;
}
signal.addEventListener("abort", handler, {
once: true,
signal: controller.signal,
});
}
return controller;
}
/**
* @description Spawns a command as a child process with output handling.
* @summary Creates a child process to execute a command with support for piping multiple commands,
* custom output handling, and abort control. This function handles the low-level details of
* spawning processes and connecting their inputs/outputs when piping is used.
*
* @template R - The type of the processed output, defaulting to string.
* @param {StandardOutputWriter<R>} output - The output writer to handle command output.
* @param {string} command - The command to execute, can include pipe operators.
* @param {SpawnOptionsWithoutStdio} opts - Options for the spawned process.
* @param {AbortController} abort - Controller to abort the command execution.
* @param {Logger} logger - Logger for recording command execution details.
* @return {ChildProcessWithoutNullStreams} The spawned child process.
*
* @function spawnCommand
*
* @memberOf module:utils
*/
function spawnCommand(output, command, opts, abort, logger) {
function spawnInner(command, controller) {
const [cmd, argz] = output.parseCommand(command);
logger.info(`Running command: ${cmd}`);
logger.debug(`with args: ${argz.join(" ")}`);
const childProcess = (0, child_process_1.spawn)(cmd, argz, {
...opts,
cwd: opts.cwd || process.cwd(),
env: Object.assign({}, process.env, opts.env, { PATH: process.env.PATH }),
shell: opts.shell || false,
signal: controller.signal,
});
logger.verbose(`pid : ${childProcess.pid}`);
return childProcess;
}
const m = command.match(/[<>$#]/g);
if (m)
throw new Error(`Invalid command: ${command}. contains invalid characters: ${m}`);
if (command.includes(" | ")) {
const cmds = command.split(" | ");
const spawns = [];
const controllers = new Array(cmds.length);
controllers[0] = abort;
for (let i = 0; i < cmds.length; i++) {
if (i !== 0)
controllers[i] = chainAbortController(controllers[i - 1].signal);
spawns.push(spawnInner(cmds[i], controllers[i]));
if (i === 0)
continue;
spawns[i - 1].stdout.pipe(spawns[i].stdin);
}
return spawns[cmds.length - 1];
}
return spawnInner(command, abort);
}
/**
* @description Executes a command asynchronously with customizable output handling.
* @summary This function runs a shell command as a child process, providing fine-grained
* control over its execution and output handling. It supports custom output writers,
* allows for command abortion, and captures both stdout and stderr.
*
* @template R - The type of the resolved value from the command execution.
*
* @param command - The command to run, either as a string or an array of strings.
* @param opts - Spawn options for the child process. Defaults to an empty object.
* @param outputConstructor - Constructor for the output writer. Defaults to StandardOutputWriter.
* @param args - Additional arguments to pass to the output constructor.
* @return {CommandResult} A promise that resolves to the command result of type R.
*
* @function runCommand
*
* @mermaid
* sequenceDiagram
* participant Caller
* participant runCommand
* participant OutputWriter
* participant ChildProcess
* Caller->>runCommand: Call with command and options
* runCommand->>OutputWriter: Create new instance
* runCommand->>OutputWriter: Parse command
* runCommand->>ChildProcess: Spawn process
* ChildProcess-->>runCommand: Return process object
* runCommand->>ChildProcess: Set up event listeners
* loop For each stdout data
* ChildProcess->>runCommand: Emit stdout data
* runCommand->>OutputWriter: Handle stdout data
* end
* loop For each stderr data
* ChildProcess->>runCommand: Emit stderr data
* runCommand->>OutputWriter: Handle stderr data
* end
* ChildProcess->>runCommand: Emit error (if any)
* runCommand->>OutputWriter: Handle error
* ChildProcess->>runCommand: Emit exit
* runCommand->>OutputWriter: Handle exit
* OutputWriter-->>runCommand: Resolve or reject promise
* runCommand-->>Caller: Return CommandResult
*
* @memberOf module:utils
*/
function runCommand(command, opts = {}, outputConstructor = (StandardOutputWriter_1.StandardOutputWriter), ...args) {
const logger = logging_1.Logging.for(runCommand);
const abort = new AbortController();
const result = {
abort: abort,
command: command,
logs: [],
errs: [],
};
const lock = new Promise((resolve, reject) => {
let output;
try {
output = new outputConstructor(command, {
resolve,
reject,
}, ...args);
result.cmd = spawnCommand(output, command, opts, abort, logger);
}
catch (e) {
return reject(new Error(`Error running command ${command}: ${e}`));
}
result.cmd.stdout.setEncoding("utf8");
result.cmd.stdout.on("data", (chunk) => {
chunk = chunk.toString();
result.logs.push(chunk);
output.data(chunk);
});
result.cmd.stderr.on("data", (data) => {
data = data.toString();
result.errs.push(data);
output.error(data);
});
result.cmd.once("error", (err) => {
output.exit(err.message, result.errs);
});
result.cmd.once("exit", (code = 0) => {
if (abort.signal.aborted && code === null)
code = constants_1.AbortCode;
output.exit(code, code === 0 ? result.logs : result.errs);
});
});
Object.assign(result, {
promise: lock,
pipe: async (cb) => {
const l = logger.for("pipe");
try {
l.verbose(`Executing pipe function ${command}...`);
const result = await lock;
l.verbose(`Piping output to ${cb.name}: ${result}`);
return cb(result);
}
catch (e) {
l.error(`Error piping command output: ${e}`);
throw e;
}
},
});
return result;
}
//# sourceMappingURL=utils.js.map