UNPKG

lugger

Version:

Lugger is an automation framework running on customizable Typescript DSL

232 lines (220 loc) 10.2 kB
/* 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); }); }); }