import * as events from 'events' import * as stream from 'stream' import * as jschardet from 'jschardet' import * as iconv from 'iconv-lite' import os from 'os' import child, { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio } from 'child_process' import platform from "../platform"; import process from "process"; export class CommandInvoker extends events.EventEmitter { constructor(command: string, args?: string[], options?: CommandOptions) { super() this.command = command this.args = args || [] this.options = options || {} this.spawn = child.spawn } setSpawn(spawn: (command: string,args?: ReadonlyArray, options?: SpawnOptionsWithoutStdio) => ChildProcessWithoutNullStreams) { this.spawn = spawn } private readonly command: string private readonly args: string[] private readonly options: CommandOptions private spawn: (command: string,args?: ReadonlyArray, options?: SpawnOptionsWithoutStdio) => ChildProcessWithoutNullStreams private stdoutBuffer: Buffer = Buffer.alloc(0) private stderrBuffer: Buffer = Buffer.alloc(0) private detectedEncoding = 'utf8' private isEncodingDetected = false private aggregateData( data: Buffer, restData: Buffer, onLineData: OnLineFunc ): Buffer { try { // 如果是linux,强制使用 UTF-8 编码 if (platform.PLATFORM_LINUX === platform.getPlatform()) { // 支持指定参数 fallback 回检测逻辑 const flowDetectedEncoding = process.env['FLOW_RUNNER_DETECTED_ENCODING'] if (flowDetectedEncoding === undefined || flowDetectedEncoding === "") { this.detectedEncoding = 'utf8' this.isEncodingDetected = true } } const concatBuffer: Buffer = Buffer.concat([restData, data]) if (concatBuffer.length < 128 && !this.isEncodingDetected) { // 如果长度小于128, 且没有检测到编码,那么先不做聚合 return concatBuffer } let content = this.bufferToString(concatBuffer) let eolIndex = content.indexOf(os.EOL) while (eolIndex > -1) { const line = content.substring(0, eolIndex) onLineData(line) content = content.substring(eolIndex + os.EOL.length) eolIndex = content.indexOf(os.EOL) } return this.stringToBuffer(content) } catch (err) { return Buffer.from('') } } private bufferToString(data: Buffer): string { if (this.isEncodingDetected && this.detectedEncoding.length != 0) { return iconv.decode(data, this.detectedEncoding) } const iDetectedMap = jschardet.detect(data); let detectedEncoding = iDetectedMap.encoding if (detectedEncoding == null || detectedEncoding == 'ascii') { // fallback to utf8 detectedEncoding = 'utf8' } else if (detectedEncoding == 'windows-1252') { detectedEncoding = 'GB2312' } this.isEncodingDetected = true this.detectedEncoding = detectedEncoding // 只有检测到非 utf8 编码时才输出以下日志,减少干扰 if (detectedEncoding !== 'utf8') { console.log(` +-- Detected Encoding: ${detectedEncoding} --+`) console.log(` +-- Detected Confidence: ${iDetectedMap.confidence} --+`) } return iconv.decode(data, this.detectedEncoding) } private stringToBuffer(content: string): Buffer { return iconv.encode(content, this.detectedEncoding) } async callCommand(): Promise { return new Promise((resolve, reject) => { if (this.options.outStream) { this.options.outStream.write(this.getCommandString() + os.EOL) } const commandProcess = this.spawn(this.command, this.args, this.options) if (commandProcess.stdout) { commandProcess.stdout.on('data', (data: Buffer) => { if (this.options.listener && this.options.listener.stdout) { this.options.listener.stdout(data) } if (this.options.outStream) { this.options.outStream.write(data) } this.stdoutBuffer = this.aggregateData( data, this.stdoutBuffer, (line: string) => { if ( this.options.listener && this.options.listener.stdoutLine ) { this.options.listener.stdoutLine(line) } } ) }) } if (commandProcess.stderr) { commandProcess.stderr.on('data', (data: Buffer) => { if (this.options.listener && this.options.listener.stderr) { this.options.listener.stderr(data) } if (this.options.errStream) { this.options.errStream.write(data) } this.stderrBuffer = this.aggregateData( data, this.stderrBuffer, (line: string) => { if ( this.options.listener && this.options.listener.stderrLine ) { this.options.listener.stderrLine(line) } } ) }) } const onErrorFunc = (err: Error): void => { this.flushBuffer() reject(err) } const onFinishFunc = (code: number): void => { this.flushBuffer() resolve(code) } commandProcess.on('error', onErrorFunc) commandProcess.on('exit', onFinishFunc) commandProcess.on('close', onFinishFunc) }) } private flushBuffer() { if ( this.stderrBuffer.length > 0 && this.options.listener && this.options.listener.stderrLine ) { const lines = this.bufferToLines(this.stderrBuffer) for (const line of lines) { this.options.listener.stderrLine(line) } this.stderrBuffer = iconv.encode('', this.detectedEncoding) } if ( this.stdoutBuffer.length > 0 && this.options.listener && this.options.listener.stdoutLine ) { const lines = this.bufferToLines(this.stdoutBuffer) for (const line of lines) { this.options.listener.stdoutLine(line) } this.stdoutBuffer = iconv.encode('', this.detectedEncoding) } } private bufferToLines(buffer: Buffer): string[] { const stdoutContent = this.bufferToString(buffer) return stdoutContent.split(/\r?\n/) } private getCommandString(): string { let cmdStr = '' cmdStr += this.command for (let i = this.args.length - 1; i >= 0; i--) { cmdStr += ` ${this.args[i]}` } return cmdStr } } export class CommandOptions { outStream?: stream.Writable errStream?: stream.Writable listener?: CommandListener cwd?: string } export interface CommandResult { exitCode: number stdoutContent: string stderrContent: string } export interface CommandListener { stdout?: DataHandlerFunc stderr?: DataHandlerFunc stdoutLine?: LineHandlerFunc stderrLine?: LineHandlerFunc } export type DataHandlerFunc = (data: Buffer) => void export type LineHandlerFunc = (line: string) => void export type OnLineFunc = (line: string) => void