UNPKG

@decaf-ts/utils

Version:

module management utils for decaf-ts

237 lines 9.19 kB
import { spawn, } from "child_process"; import { StandardOutputWriter } from "./../writers/StandardOutputWriter.js"; import { AbortCode } from "./constants.js"; import { Logging } from "@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 */ export function lockify(f) { let lock = Promise.resolve(); return (...params) => { const result = lock.then(() => f(...params)); lock = result.catch(() => { }); return result; }; } export 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 */ export 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 = 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 */ export function runCommand(command, opts = {}, outputConstructor = (StandardOutputWriter), ...args) { const logger = 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 = 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