@atomist/automation-client
Version:
Atomist API for software low-level client
262 lines • 10.9 kB
JavaScript
;
/*
* 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