/*
* 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 .
*/
import {
ChildProcess,
SpawnOptions,
} from "child_process";
import * as spawn from "cross-spawn";
import * as os from "os";
import * as path from "path";
import stripAnsi from "strip-ansi";
import * as treeKill from "tree-kill";
import {
killProcess,
WritableLog,
} from "./child_process";
import { logger } from "./logger";
export { spawn };
/* tslint:disable:deprecation */
/**
* Type that can react to the exit of a spawned child process, after
* Node has terminated without reporting an error.
* This is necessary only for commands that can return
* a non-zero exit code on success.
* @return whether this result should be considered an error.
* @deprecated use @atomist/sdm version
*/
export type ErrorFinder = (code: number, signal: string, log: WritableLog) => boolean;
/**
* Default ErrorFinder that regards return code 0 as success
* @param {number} code
* @return {boolean}
* @deprecated use @atomist/sdm version
*/
export const SuccessIsReturn0ErrorFinder: ErrorFinder = code => code !== 0;
/**
* Result returned by spawnAndWatch after running a child process.
* @deprecated use @atomist/sdm SpawnLogResult
*/
export interface ChildProcessResult {
/** Will be true if the ErrorFinder returns true. */
error: boolean;
/** Exit code of process. It may be null or undefined if the process was killed. */
code: number;
/** Optional message returned by process. */
message?: string;
/** The Node.js child_process.ChildProcess created by spawnAndwatch. */
childProcess: ChildProcess;
}
/**
* spawnAndWatch specific options.
* @deprecated use @atomist/sdm SpawnLogOptions
*/
export interface SpawnWatchOptions {
/**
* If your command can return zero on failure or non-zero on
* success, you can override the default behavior of determining
* success or failure using this option. For example, if your
* command returns zero for certain types of errors, you can scan
* the log content from the command to determine if an error
* occurs.
*/
errorFinder: ErrorFinder;
/**
* Set to true if ANSI escape codes should be stripped from the
* output before sending it to the log.
*/
stripAnsi: boolean;
/**
* Amount of time in milliseconds to wait for process to exit. If
* it does not exit in the allotted time, it is kill
*/
timeout: number;
/**
* Set to true if you want the command line sent to the
* Writablelog provided to spawnAndWatch.
*/
logCommand: boolean;
}
/**
* Spawn a process, log its output, and wait for it to exit,
* asynchronously. It is spawned using cross-spawn.
*
* @param {SpawnCommand} spawnCommand command to run
* @param options standard spawn options
* @param log log to write output to
* @param {Partial} spOpts
* @return {Promise}
* @deprecated use @atomist/sdm spawnAndLog
*/
export async function spawnAndWatch(spawnCommand: SpawnCommand,
options: SpawnOptions,
log: WritableLog,
spOpts: Partial = {}): Promise {
const childProcess = spawn(spawnCommand.command, spawnCommand.args || [], options);
if (spOpts.logCommand === false) {
logger.debug(`${options.cwd || path.resolve(".")} > ${stringifySpawnCommand(spawnCommand)} (pid '${childProcess.pid}')`);
} else {
log.write(`/--\n`);
log.write(`${options.cwd || path.resolve(".")} > ${stringifySpawnCommand(spawnCommand)} (pid '${childProcess.pid}')\n`);
log.write(`\\--\n`);
}
return watchSpawned(childProcess, log, spOpts);
}
/**
* Handle the result of a spawned process, streaming back
* output to log
* @param childProcess
* @param log to write stdout and stderr to
* @param opts: Options for error parsing, ANSI code stripping etc.
* @return {Promise}
* @deprecated use @atomist/sdm spawnAndLog
*/
async function watchSpawned(childProcess: ChildProcess,
log: WritableLog,
opts: Partial = {}): Promise {
let timer: NodeJS.Timer;
let running = true;
if (opts.timeout) {
timer = setTimeout(() => {
if (running) {
logger.warn("Spawn timeout expired. Killing command with pid '%s'", childProcess.pid);
killProcess(childProcess.pid);
}
}, opts.timeout);
}
return new Promise((resolve, reject) => {
const optsToUse = {
errorFinder: SuccessIsReturn0ErrorFinder,
stripAnsi: false,
...opts,
};
if (!optsToUse.errorFinder) {
// The caller specified undefined, which is an error. Ignore them, for they know not what they do.
optsToUse.errorFinder = SuccessIsReturn0ErrorFinder;
}
function sendToLog(data: any): void {
const formatted = optsToUse.stripAnsi ? stripAnsi(data.toString()) : data.toString();
log.write(formatted);
}
childProcess.stdout.on("data", sendToLog);
childProcess.stderr.on("data", sendToLog);
childProcess.addListener("exit", (code, signal) => {
running = false;
logger.debug("Spawn exit with pid '%d': code '%d', signal '%d'", childProcess.pid, code, signal);
clearTimeout(timer);
resolve({
error: optsToUse.errorFinder(code, signal, log),
code,
childProcess,
});
});
childProcess.addListener("error", err => {
running = false;
// Process could not be spawned or killed
logger.warn("Spawn failure: %s", err);
clearTimeout(timer);
reject(err);
});
});
}
/**
* The first two arguments to Node spawn
* @deprecated not used by @atomist/sdm spawnAndLog
*/
export interface SpawnCommand {
command: string;
args?: string[];
options?: any;
}
/**
* toString for a SpawnCommand. Used for logging.
* @param {SpawnCommand} sc
* @return {string}
* @deprecated use childProcessString
*/
export function stringifySpawnCommand(sc: SpawnCommand): string {
return `${sc.command}${(!!sc.args) ? " '" + sc.args.join("' '") + "'" : ""}`;
}
/**
* Convenience function to create a spawn command from a sentence such
* as "npm run compile" Does not respect quoted arguments. Use
* spawnAndWatch passing it the command and argument array if your
* command arguments have spaces, etc.
*
* @param {string} sentence command and argument string
* @param options
* @return {SpawnCommand}
* @deprecated just pass the proper arguments to @atomist/sdm spawnAndLog
*/
export function asSpawnCommand(sentence: string, options: SpawnOptions = {}): SpawnCommand {
const split = sentence.split(" ");
return {
command: split[0],
args: split.slice(1),
options,
};
}
/**
* Kill the child process and wait for it to shut down. This can take
* a while as child processes may have shut down hooks. On win32,
* tree-kill is used and the Promise is rejected if the process(es) do(es)
* not exit within `wait` milliseconds. On other platforms, first the
* child process is sent the default signal, SIGTERM. After `wait`
* milliseconds, it is sent SIGKILL. After another `wait`
* milliseconds, an error is thrown.
*
* @param {module:child_process.ChildProcess} childProcess
* @param wait the number of milliseconds to wait before sending SIGKILL and
* then erroring, default is 30000 ms
* @return {Promise}
* @deprecated use @atomist/sdm killAndWait
*/
export async function poisonAndWait(childProcess: ChildProcess, wait: number = 30000): Promise {
return new Promise((resolve, reject) => {
const pid = childProcess.pid;
const termTimer = setTimeout(() => {
if (os.platform() === "win32") {
reject(new Error(`Failed to tree-kill child process ${pid} in ${wait} ms`));
} else {
logger.debug(`Child process ${pid} did not exit in ${wait} ms, sending SIGKILL`);
childProcess.kill("SIGKILL");
}
}, wait);
const killTimer = (os.platform() === "win32") ? undefined : setTimeout(() => {
reject(new Error(`Failed to kill child process ${pid}`));
}, 2 * wait);
childProcess.on("close", clearAndResolve(pid, resolve, termTimer, killTimer));
childProcess.on("exit", (code, signal) => {
logger.debug(`Child process ${pid} exited with code '${code}' and signal '${signal}'`);
});
childProcess.on("error", clearAndReject(pid, reject, termTimer, killTimer));
killProcess(pid);
});
}
/**
* Cross-platform kill. On win32, tree-kill is used and signal is
* ignored since win32 does not support different signals. On other
* platforms, ChildProcess.kill(signal) is used.
*
* @param cp child process to kill
* @param signal optional signal, Node.js default is used if not provided
* @deprecated use killProcess
*/
export function crossKill(cp: ChildProcess, signal?: string): void {
if (os.platform() === "win32") {
logger.debug(`Calling tree-kill on child process ${cp.pid}`);
treeKill(cp.pid);
} else {
const sig = (signal) ? `signal ${signal}` : "default signal";
logger.debug(`Sending ${sig} to child process ${cp.pid}`);
cp.kill(signal);
}
}
/**
* Clear provided timers and resolve a promise.
* @deprecated not used by spawnPromise
*/
function clearAndResolve(pid: number, resolve: () => void, ...timers: NodeJS.Timer[]): (code: number, signal: string) => void {
return (code: number, signal: string) => {
logger.debug(`Child process ${pid} closed with code '${code}' and signal '${signal}'`);
clearTimers(timers);
resolve();
};
}
/**
* Clear provided timers and reject a promise.
* @deprecated not used by spawnPromise
*/
function clearAndReject(pid: number, reject: (e: Error) => void, ...timers: NodeJS.Timer[]): (reason: Error) => void {
return (reason: Error) => {
logger.error(`Child process ${pid} errored: ${reason.message}`);
clearTimers(timers);
reject(reason);
};
}
/**
* Clear provided timers. It checks to make sure the timers are
* defined before clearing them.
*
* @param timers the timers to clear.
* @deprecated only used by deprecated functions
*/
function clearTimers(timers: NodeJS.Timer[]): void {
timers.filter(t => !!t).map(clearTimeout);
}