@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
125 lines (99 loc) • 4.02 kB
JavaScript
const cp = require('child_process');
const cds = require('../cds');
const LOG = cds.log('cli'), DEBUG = cds.debug('cli');
const { IS_WIN } = require('../init/constants');
class CommandUtil {
/**
* Execute a command and show output in console
* spawn has problems with promisify so use native approach w/ Promise
* @param cmd command
* @param args command args
* @param options spawn options
* @param collectResult if true, return complete console output as promise resolve value
* this might result in a huge string!
*
* @returns Promise
*/
async spawnCommand(cmd, args, options = {}, collectResult = false, showOutput = true) {
args = args.map(arg => arg.replace(/[&;|]/g, '')); // sanitize args
return new Promise((resolve, reject) => {
DEBUG?.(`[spawn] ${cmd} ${args.join(' ')}`);
options.env = options.env || process.env;
const child = this._getChildProcess(cmd, args, options);
// use inplace methods to ensure logger is called in context
const stdLogBuffer = new LogBuffer(showOutput || LOG._debug ? (chunk) => console.log(chunk) : undefined, collectResult);
child.stdout.on('data', (chunk) => {
stdLogBuffer.handleChunk(chunk);
});
// use inplace methods to ensure logger is called in context
// collect error log in case of Win to handle command not found (ENOENT),
const errLogBuffer = new LogBuffer(showOutput || LOG._debug ? (chunk) => console.error(chunk) : undefined, (IS_WIN || collectResult));
child.stderr.on('data', (chunk) => {
errLogBuffer.handleChunk(chunk);
});
child.on('close', (code) => {
// dump buffer
stdLogBuffer.flush();
errLogBuffer.flush();
if (code === 0) {
resolve(stdLogBuffer.getResult());
} else {
const errorLog = errLogBuffer.getResult();
const error = new Error(`${cmd} ${args.join(' ')} failed with code ${code}, ${errorLog}`)
// add code ENOENT on Win if command not found
if (IS_WIN && errorLog.includes(cmd) && /not recognized/i.exec(errorLog)) {
error.code = 'ENOENT';
error.path = cmd;
}
error.errLog = errorLog;
error.stdLog = stdLogBuffer.getResult();
reject(error);
}
});
child.on('error', (err) => {
reject(err);
});
});
}
_getChildProcess(cmd, args, options) {
if (!IS_WIN) {
return cp.spawn(cmd, args, options);
}
// always use cmd shell on Win since Win does not report ENOENT
// we need to parse output which might differ from PS to cmd shell
return cp.spawn('cmd', ['/s', '/c', cmd, ...args], {
shell: true, // for windows only
...options
});
}
}
class LogBuffer {
constructor(logMethod, collectResult) {
this.logMethod = logMethod;
this.collectResult = collectResult;
this.result = '';
this.buffer = '';
}
handleChunk(chunk) {
const chunkStr = chunk.toString();
if (this.collectResult) {
this.result = this.result + chunkStr;
}
const seq = chunkStr.split(/\r?\n/); // more robust than os.EOL
this.buffer = this.buffer + seq.shift();
for (const line of seq) {
this.flush();
this.buffer = line;
}
}
flush() {
if (this.logMethod && this.buffer) {
this.logMethod(this.buffer);
this.buffer = '';
}
}
getResult() {
return this.result.trim();
}
}
module.exports = new CommandUtil();