lugger
Version:
Lugger is an automation framework running on customizable Typescript DSL
232 lines (220 loc) • 10.2 kB
text/typescript
/* Firestack (c) 2020, License: MIT */
import { Class, dp, ok, promise, Result, ReturnCodeFamily, TaggedTemplateSelfChain } from 'ts-basis';
import { spawn } from 'child_process';
import * as fs from 'fs';
import { ShellCodes, ShellProcessOptions, ShellProcessOptionsGeneric, ShellProcessOptionsReturnBuffer, ShellProcessOptionsReturnExitCode, ShellProcessOptionsReturnObject, ShellProcessOptionsReturnOutput, ShellProcessOptionsReturnString, ShellProcessReturn } from './shell.model';
import { CallContextEvent, dedent, getRuntime, PostfixReturn, dslIfaceGuard } from 'ts-dsl';
import { tempFileName } from '../fs/fs.util';
import * as YAML from 'yaml';
// ====================================================
// Facade
// ====================================================
export function sh(procArgs: ShellProcessOptionsGeneric, command?: string): PostfixReturn<ShellProcessReturn>;
export function sh(procArgs: ShellProcessOptionsReturnExitCode, command?: string): PostfixReturn<number>;
export function sh(procArgs: ShellProcessOptionsReturnString | ShellProcessOptionsReturnOutput, command?: string): PostfixReturn<string>;
export function sh(procArgs: ShellProcessOptionsReturnBuffer, command?: string): PostfixReturn<Buffer>;
export function sh<T extends Object = Object>(procArgs: ShellProcessOptionsReturnObject, command?: string): PostfixReturn<T>;
export function sh(command: string): PostfixReturn<ShellProcessReturn>;
export function sh(command: string, returnType: 'string' | 'base64' | 'utf8' | 'ucs2' | 'ascii'): PostfixReturn<string>;
export function sh(command: string, returnType: 'exitcode'): PostfixReturn<number>;
export function sh(command: string, returnType: 'buffer'): PostfixReturn<Buffer>;
export function sh<T extends Object = Object>(command: string, returnType: 'object-json' | 'object-yaml'): PostfixReturn<T>;
export function sh(strArr: TemplateStringsArray, ...args: any[]): TaggedTemplateSelfChain<PostfixReturn<ShellProcessReturn>>;
export function sh(command) { dslIfaceGuard('sh', __filename); return null; }
export function echo(command: string): PostfixReturn<string>;
export function echo(strArr: TemplateStringsArray, ...args: any[]): TaggedTemplateSelfChain<PostfixReturn<string>>;
export function echo(command) { dslIfaceGuard('echo', __filename); return null; }
export namespace git {
function cloneModel() {
}
export const clone = cloneModel as (typeof cloneModel & Class<any>);
}
// ====================================================
// Implemenation
// ====================================================
export function sh__(cce: CallContextEvent, ...args): any {
const arg0 = args[0];
const arg1 = args[1];
let shArg: ShellProcessOptions = (arg0 as any);
if (typeof arg0 === 'string') {
if (cce.fromTaggedTemplate) {
shArg = { script: args.map(a => dedent(a)).join('\n') };
} else {
shArg = { script: dedent(arg0) };
if (typeof arg1 === 'string') {
shArg.returnType = arg1 as any;
}
}
} else {
if (typeof arg1 === 'string') {
shArg.script = dedent(arg1);
}
}
if (shArg.failFast === null || shArg.failFast === undefined) {
shArg.failFast = true;
}
let shRet: ShellProcessReturn = { canceled: false, timeCalled: Date.now() };
return getRuntime().scopedExec(
cce, 'lugger:shell:sh', { data: { shellArg: shArg }},
async (resolve, reject, scopeContext) => {
try {
const shFile = tempFileName('sh');
const shSpecialReturnFile = shFile + '.ret';
// const bailOutClause = `STATUS=$?\nif [ $STATUS -ne 0 ]; then\n return $STATUS 2>/dev/null || exit $STATUS;\nfi`;
const fullScript = `export RETURN='${shSpecialReturnFile}';\n${shArg.script}`;
// console.log(fullScript);
fs.writeFile(shFile, fullScript, e => {
if (e) {
const e2 = ShellCodes.error('SH_TEMP_FILE_WRITE_ERROR', e).error;
console.error(e2)
return reject(e2);
}
const flags = [];
if (shArg.printSteps) { flags.push('x'); }
if (shArg.failFast) { flags.push('e'); }
let willReturnOutput = false;
switch (shArg.returnType) {
case 'string': case 'buffer': case 'base64': case 'hex': case 'utf8': case 'ucs2': case 'ascii':
willReturnOutput = true;
break;
}
let willOutputToReturnFile = false;
switch (shArg.returnType) {
case 'object-json': case 'object-yaml':
willOutputToReturnFile = true;
break;
}
shRet.timeStarted = Date.now();
const proc = spawn('bash', flags.length === 0 ? [shFile] : [`-${flags.join('')}`, shFile]);
const allBuffers: Buffer[] = [];
let writeStream: fs.WriteStream;
proc.stdout.on('data', (chunk: Buffer) => {
if (shRet.timeEnded) { return; }
if (shArg.onStdoutData) { shArg.onStdoutData(chunk); }
allBuffers.push(chunk);
if (writeStream && !writeStream.closed) { writeStream.write(chunk); }
if (!shArg.quietConsole && (shArg.tee || !shArg.outputFile)) { process.stdout.write(chunk); }
});
proc.stderr.on('data', (chunk: Buffer) => {
if (shRet.timeEnded) { return; }
if (shArg.onStderrData) { shArg.onStderrData(chunk); }
allBuffers.push(chunk);
if (writeStream && !writeStream.closed) { writeStream.write(chunk); }
if (!shArg.quietConsole && (shArg.tee || !shArg.outputFile)) { process.stderr.write(chunk); }
});
// if (shArg.tee || !shArg.outputFile) {
// proc.stdout.pipe(process.stdout);
// proc.stderr.pipe(process.stderr);
// }
const outputFile = shArg.outputFile ? shArg.outputFile : tempFileName('log');
const mode = shArg.outputFileMode ? shArg.outputFileMode : 'a'; // default append
writeStream = fs.createWriteStream(outputFile, { encoding: 'binary', flags: mode, emitClose: true });
proc.on('close', async (code) => {
shRet.timeEnded = Date.now();
shRet.duration = shRet.timeEnded - shRet.timeStarted;
fs.unlink(shFile, e => {});
const returnFile = willOutputToReturnFile ? shSpecialReturnFile : outputFile;
let outputBuffer: Buffer;
let error: Error;
for (let i = 0; i < 5; ++i) {
try {
outputBuffer = await readFileAwaitable(returnFile);
error = null;
break;
} catch (e2) {
await promise(async resolve => setTimeout(resolve, 5));
error = e2;
}
}
const totalBuffer = Buffer.concat(allBuffers);
fs.unlink(shSpecialReturnFile, e => {});
if (writeStream) { writeStream.close(); }
if (error) {
if (willOutputToReturnFile) {
return reject(ShellCodes.error('SH_TEMP_OUTPUT_FILE_READ_ERROR', error).error);
} else {
return reject(ShellCodes.error('SH_TEMP_SPECIAL_OUTPUT_FILE_READ_ERROR', error).error);
}
}
if (shArg.returnExitCode) {
return resolve(code);
}
if (willReturnOutput) {
switch (shArg.returnType) {
case 'string':
return resolve(totalBuffer.toString('utf8'));
case 'utf8':
return resolve(totalBuffer.toString('utf8'));
case 'ucs2':
return resolve(totalBuffer.toString('ucs2'));
case 'hex':
return resolve(totalBuffer.toString('hex'));
case 'base64':
return resolve(totalBuffer.toString('base64'));
case 'ascii':
return resolve(totalBuffer.toString('ascii'));
case 'buffer':
return resolve(totalBuffer);
}
return reject(ShellCodes.error('SH_RETURN_TYPE_ERROR_SERIALIZABLE_TYPE').error);
}
if (willOutputToReturnFile) {
switch (shArg.returnType) {
case 'object-json':
try {
return resolve(JSON.parse(outputBuffer.toString('utf8')));
} catch (e2) {
return reject(ShellCodes.error('SH_RETURN_PARSE_ERROR_BAD_JSON_FORMAT', e2).error);
}
case 'object-yaml':
try {
return resolve(YAML.parse(outputBuffer.toString('utf8')));
} catch (e2) {
return reject(ShellCodes.error('SH_RETURN_PARSE_ERROR_BAD_YAML_FORMAT', e2).error);
}
}
return reject(ShellCodes.error('SH_RETURN_TYPE_ERROR_OBJECT_TYPE').error);
}
if (shArg.returnOutput) {
return resolve(totalBuffer.toString('utf8'));
}
if (code === 0) {
shRet.exitcode = code;
shRet.outputBuffer = totalBuffer;
shRet.outputText = totalBuffer.toString('utf8');
shRet.callArgs = shArg;
return resolve(shRet);
}
return reject(new Error(`sh returned non-zero exit code (${code}) while running:\n${shArg.script}`));
});
if (shArg.onProcess) {
shArg.onProcess(proc, shArg);
}
});
} catch (e) {
reject(e);
}
});
}
export function echo__(cce: CallContextEvent, ...args): any {
if (typeof args[0] === 'string') {
if (cce.fromTaggedTemplate) {
for (const arg of args) {
console.log(arg);
}
} else {
console.log(args[0]);
}
}
}
export namespace git__ {
export function clone() {}
}
function readFileAwaitable(file: string) {
return promise<Buffer>(async (resolve, reject) => {
fs.readFile(file, (e, outputBuffer) => {
if (e) { return reject(e); }
return resolve(outputBuffer);
});
});
}