UNPKG

@atomist/automation-client

Version:

Atomist API for software low-level client

262 lines 10.9 kB
"use strict"; /* * Copyright © 2018 Atomist, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const spawn = require("cross-spawn"); exports.spawn = spawn; const process = require("process"); const strip_ansi_1 = require("strip-ansi"); const treeKill = require("tree-kill"); const logger_1 = require("./logger"); /** * Convert child process into an informative string. * * @param cmd Command being run. * @param args Arguments to command. * @param opts Standard child_process.SpawnOptions. */ function childProcessString(cmd, args = [], opts = {}) { return (opts.cwd ? opts.cwd : process.cwd()) + " ==> " + cmd + (args.length > 0 ? " '" + args.join("' '") + "'" : ""); } exports.childProcessString = childProcessString; /** * Cross-platform kill a process and all its children using tree-kill. * On win32, signal is ignored since win32 does not support different * signals. * * @param pid ID of process to kill. * @param signal optional signal name or number, Node.js default is used if not provided */ function killProcess(pid, signal) { const sig = (signal) ? `signal ${signal}` : "default signal"; logger_1.logger.debug(`Calling tree-kill on child process ${pid} with ${sig}`); treeKill(pid, signal); } exports.killProcess = killProcess; /** * Safely clear a timer that may be undefined. * * @param timer A timer that may not be set. */ function clearTimer(timer) { if (timer) { clearTimeout(timer); } return undefined; } /** * Call cross-spawn and return a Promise of its result. The result * has the same shape as the object returned by * `child_process.spawnSync()`, which means errors are not thrown but * returned in the `error` property of the returned object. If your * command will produce lots of output, provide a log to write it to. * * @param cmd Command to run. If it is just an executable name, paths * with be searched, batch and command files will be checked, * etc. See cross-spawn documentation for details. * @param args Arguments to command. * @param opts Standard child_process.SpawnOptions plus a few specific * to this implementation. * @return a Promise that provides information on the child process and * its execution result. If an error occurs, the `error` property * of [[SpawnPromiseReturns]] will be populated. */ function spawnPromise(cmd, args = [], opts = {}) { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { const optsToUse = Object.assign({ logCommand: true }, opts); const cmdString = childProcessString(cmd, args, optsToUse); let logEncoding = "utf8"; if (!optsToUse.encoding) { if (optsToUse.log) { optsToUse.encoding = "buffer"; } else { optsToUse.encoding = "utf8"; } } else if (optsToUse.log && optsToUse.encoding !== "buffer") { logEncoding = optsToUse.encoding; optsToUse.encoding = "buffer"; } function pLog(data) { const formatted = (optsToUse.log && optsToUse.log.stripAnsi) ? strip_ansi_1.default(data) : data; optsToUse.log.write(formatted); } function commandLog(data, l = logger_1.logger.debug) { if (optsToUse.log && optsToUse.logCommand) { const terminated = (data.endsWith("\n")) ? data : data + "\n"; pLog(terminated); } else { l(data); } } logger_1.logger.debug(`Spawning: ${cmdString}`); const childProcess = spawn(cmd, args, optsToUse); commandLog(`Spawned: ${cmdString} (PID ${childProcess.pid})`); let timer; if (optsToUse.timeout) { timer = setTimeout(() => { commandLog(`Child process timeout expired, killing command: ${cmdString}`, logger_1.logger.warn); killProcess(childProcess.pid, optsToUse.killSignal); }, optsToUse.timeout); } let stderr = ""; let stdout = ""; if (optsToUse.log) { function logData(data) { pLog(data.toString(logEncoding)); } childProcess.stderr.on("data", logData); childProcess.stdout.on("data", logData); stderr = stdout = "See log\n"; } else { childProcess.stderr.on("data", (data) => stderr += data); childProcess.stdout.on("data", (data) => stdout += data); } childProcess.on("exit", (code, signal) => { timer = clearTimer(timer); logger_1.logger.debug(`Child process exit with code ${code} and signal ${signal}: ${cmdString}`); }); /* tslint:disable:no-null-keyword */ childProcess.on("close", (code, signal) => { timer = clearTimer(timer); commandLog(`Child process close with code ${code} and signal ${signal}: ${cmdString}`); resolve({ cmdString, pid: childProcess.pid, output: [null, stdout, stderr], stdout, stderr, status: code, signal, error: null, }); }); childProcess.on("error", err => { timer = clearTimer(timer); err.message = `Failed to run command: ${cmdString}: ${err.message}`; commandLog(err.message, logger_1.logger.error); resolve({ cmdString, pid: childProcess.pid, output: [null, stdout, stderr], stdout, stderr, status: null, signal: null, error: err, }); }); /* tslint:enable:no-null-keyword */ }); }); } exports.spawnPromise = spawnPromise; /** * Error thrown when a command cannot be executed, the command is * killed by a signal, or returns a non-zero exit status. */ class ExecPromiseError extends Error { constructor( /** Message describing reason for failure. */ message, /** Command PID. */ pid, /** stdio */ output, /** Child process standard output. */ stdout, /** Child process standard error. */ stderr, /** Child process exit status. */ status, /** Signal that killed the process, if any. */ signal) { super(message); this.message = message; this.pid = pid; this.output = output; this.stdout = stdout; this.stderr = stderr; this.status = status; this.signal = signal; Object.setPrototypeOf(this, new.target.prototype); } /** Create an ExecError from a SpawnSyncReturns<string> */ static fromSpawnReturns(r) { return new ExecPromiseError(r.error.message, r.pid, r.output, r.stdout, r.stderr, r.status, r.signal); } } exports.ExecPromiseError = ExecPromiseError; /** * Run a child process using cross-spawn, capturing and returning * stdout and stderr, like exec, in a promise. If an error occurs, * the process is killed by a signal, or the process exits with a * non-zero status, the Promise is rejected. Any provided `stdio` * option is ignored, being overwritten with `["pipe","pipe","pipe"]`. * Like with child_process.exec, this is not a good choice if the * command produces a large amount of data on stdout or stderr. * * @param cmd name of command, can be a shell script or MS Windows * .bat or .cmd * @param args command arguments * @param opts standard child_process.SpawnOptions * @return Promise resolving to exec-like callback arguments having * stdout and stderr properties. If an error occurs, exits * with a non-zero status, and killed with a signal, the * Promise is rejected with an [[ExecPromiseError]]. */ function execPromise(cmd, args = [], opts = {}) { return __awaiter(this, void 0, void 0, function* () { opts.stdio = ["pipe", "pipe", "pipe"]; if (!opts.encoding) { opts.encoding = "utf8"; } const result = yield spawnPromise(cmd, args, opts); if (result.error) { throw ExecPromiseError.fromSpawnReturns(result); } if (result.status) { const msg = `Child process ${result.pid} exited with non-zero status ${result.status}: ${result.cmdString}\n${result.stderr}`; logger_1.logger.error(msg); result.error = new Error(msg); throw ExecPromiseError.fromSpawnReturns(result); } if (result.signal) { const msg = `Child process ${result.pid} received signal ${result.signal}: ${result.cmdString}\n${result.stderr}`; logger_1.logger.error(msg); result.error = new Error(msg); throw ExecPromiseError.fromSpawnReturns(result); } return { stdout: result.stdout, stderr: result.stderr }; }); } exports.execPromise = execPromise; //# sourceMappingURL=child_process.js.map