diff --git a/lib/listeners.js b/lib/listeners.js deleted file mode 100644 index ec22ab3..0000000 --- a/lib/listeners.js +++ /dev/null @@ -1,19 +0,0 @@ -import { onSubprocessData } from './onData.js' -import { onSpawnError, onSubprocessError } from './onError.js' - -/** - * Handle events from child_process - * @param {SevenZipStream} stream - */ -export function listenStdEvents (stream) { - stream._childProcess.stdout.on('data', function (chunk) { - onSubprocessData(stream, chunk) - }) - stream._childProcess.stderr.on('data', function (chunk) { - onSubprocessError(stream, chunk) - }) - stream._childProcess.on('error', function (err) { - onSpawnError(stream, err) - }) - return stream -} diff --git a/lib/onData.js b/lib/onData.js deleted file mode 100644 index 56ae2a5..0000000 --- a/lib/onData.js +++ /dev/null @@ -1,78 +0,0 @@ -import { STAGE_HEADERS, STAGE_FILES, STAGE_FOOTER } from './references.js' -import { matchLine } from './parser.js' - -/** - * Data handling. - * @param {SevenZipStream} stream Instance of the worker - * @param {Buffer} chunk Buffer from `stdout` - * @emits SevenZipStream#progress - */ -export function onSubprocessData (stream, chunk) { - let lines = chunk.toString().split('\n') - - // When the `-bsp1` switch is specified 7zip output a line - // containing the progress percentage and the file count as far. - lines.forEach(function (line) { - const matchProgress = matchLine(line, 'progress') - if (matchProgress) { - stream.emit('progress', matchProgress.groups) - } - }) - - // Chunks of data aren't line-by-line, a chunk can begin and end in the - // middle of line. The following code insure that if a line is not - // complete it goes to the next stream - const isPreviousLastLine = (stream._lastLine) - if (isPreviousLastLine) { - lines[0] = stream._lastLine.concat(lines[0]) - } - const newLastLine = lines[lines.length - 1] - const isNewLastLineComplete = (newLastLine.indexOf('\n') === newLastLine.length - 1) - if (!isNewLastLineComplete) { - stream._lastLine = newLastLine - lines.pop() - } else { - delete stream._lastLine - } - - lines.forEach(function (line) { - // Add info to steam. Only run the regexp when the stream is on - // `STAGE_HEADERS` for perf improvment - if (stream._stage === STAGE_HEADERS) { - const matchHeadersInfo = matchLine(line, 'infoHeaders') - if (matchHeadersInfo) { - stream.info[matchHeadersInfo.property] = matchHeadersInfo.value - return - } - } - - // An empty line is created at the end of the listing of files. This empty - // line means that 7zip has reached the end of the listing, and will next - // outputs infos for the STAGE_FOOTER. - if (stream._stage === STAGE_FILES) { - const isLineEmpty = (line === '') - if (isLineEmpty) { - stream._stage = STAGE_FOOTER - } - return - } - - // Add info to steam. Only run the regexp when the stream is on - // `STAGE_FOOTER` for perf improvment - if (stream._stage === STAGE_FOOTER) { - const matchFooterInfo = matchLine(line, 'infoFooter') - if (matchFooterInfo) { - stream.info[matchFooterInfo.property] = matchFooterInfo.value - } - return - } - - // Parser function call. Only push to stream when the parser return non-null - // data. Pushing null will cause the stream to end. - const matchFile = matchLine(line, stream._parser) - if (matchFile) { - stream._stage = STAGE_FILES - stream.push(matchFile) - } - }) -} diff --git a/lib/onError.js b/lib/onError.js deleted file mode 100644 index 064edbb..0000000 --- a/lib/onError.js +++ /dev/null @@ -1,35 +0,0 @@ -// @TODO parser error using parser.js -/** - * Regexp to be match againt stderr - */ -const inLineErrorRegExp = /ERROR: (?.*)\n/ -const offLineErrorRegExp = /ERROR:\n(?.*)\n/ - -/** - * Error handling. An error from `stderr` - * @param {SevenZipStream} stream Instance of the worker - * @param {Error} chunk Error emitted by `child_process.spawn()` - * @emits SevenZipStream#error - */ -export function onSubprocessError (stream, chunk) { - const stderr = chunk.toString() - const inLineError = stderr.match(inLineErrorRegExp) - const offLineError = stderr.match(offLineErrorRegExp) - let message = 'unknown error' - message = (inLineError) ? inLineError.groups.message : message - message = (offLineError) ? offLineError.groups.message : message - const err = new Error(message) - err.stderr = stderr // @TODO doc: usage of raw stderr to get more info - stream.emit('error', err) -} - -/** - * Error handling. An error can be from the `spawn()` call - * @param {SevenZipStream} stream Instance of the worker - * @param {Error} err Error emitted by `child_process.spawn()` - * @see https://nodejs.org/api/child_process.html#child_process_event_error - * @emits SevenZipStream#error - */ -export function onSpawnError (stream, err) { - stream.emit('error', err) -} diff --git a/lib/stream.js b/lib/stream.js index 6f6cedf..dcce082 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -1,8 +1,10 @@ import { spawn } from 'cross-spawn' import { Readable } from 'stream' +import { STAGE_HEADERS, STAGE_FILES, STAGE_FOOTERS } from './references.js' import { transformPathToString, transformRawToArgs, transformWildCardsToArgs } from './special.js' import { transformSwitchesToArgs } from './switches.js' -import { STAGE_HEADERS } from './references.js' +import { matchProgress } from './parser.js' +import { add } from './infos.js' /** * Wrapper around the `child_process.spawn()` call @@ -14,7 +16,9 @@ export class SevenZipStream extends Readable { super(options) const stream = this - stream._parser = options._parser + stream._matchHeaders = options._matchHeaders + stream._matchFiles = options._matchFiles + stream._matchFooters = options._matchFooters stream._stage = STAGE_HEADERS stream.info = {} @@ -27,7 +31,17 @@ export class SevenZipStream extends Readable { .concat(wildcards) .concat(switches) .concat(raw) - stream._childProcess = spawn(pathSpawn, args) + stream._pathSpawn = pathSpawn + stream._args = args + stream._childProcess = options.$childProcess + + // When $differ option is specified the stream is constructed but the + // child_process is not spawned, nor the llisteners are attached. It allows + // easier testing with mock stdout and some advanced usages. + if (!options.$defer) { + stream.spawn() + stream.listen() + } return stream } @@ -39,4 +53,136 @@ export class SevenZipStream extends Readable { // `stdout` can be lost. // [https://github.com/nodejs/help/issues/963#issuecomment-372007824] _read (size) {} + + // Register listeners + listen () { + const stream = this + // Attach listeners @TODO end/close events + const stdout = stream._childProcess.stdout + const stderr = stream._childProcess.stderr + stdout.on('data', function (chunk) { + stream.onSubprocessData(chunk) + }) + stdout.on('end', function () { + stream.emit('end') + }) + stderr.on('data', function (chunk) { + stream.onSubprocessError(chunk) + }) + stream._childProcess.on('error', function (err) { + stream.onSpawnError(err) + }) + return this + } + + // Run child process + spawn () { + this._childProcess = spawn(this._pathSpawn, this._args) + return this + } + + addInfos (infos) { + let stream = this + infos.forEach(function (info) { + Object.assign(stream.info, info) + }) + return this + } + + onSubprocessData (chunk) { + const stream = this + + // Lines are separated by the END OF LINE symbol. When 7zip writes a + // progress value to stdout a new line is not created: Instead 7zip uses + // a combination on backpaces and spaces char. + let lines = chunk.toString().split(/\n|\x08/) + + // Chunks of data aren't line-by-line, a chunk can begin and end in the + // middle of line. The following code insure that if a line is not + // complete it goes to the next stream + const isPreviousLastLine = (stream._lastLine) + if (isPreviousLastLine) { + lines[0] = stream._lastLine.concat(lines[0]) + } + const newLastLine = lines[lines.length - 1] + const isNewLastLineComplete = (newLastLine.indexOf('\n') === newLastLine.length - 1) + if (!isNewLastLineComplete) { + stream._lastLine = newLastLine + lines.pop() + } else { + delete stream._lastLine + } + + // When the `-bsp1` switch is specified 7zip output a line + // containing the progress percentage and the file count as far. + lines.forEach(function (line) { + const progress = matchProgress(line) + if (progress) { + stream.emit('progress', progress) + + const isSymbolFile = (progress.symbol && progress.file) + if (isSymbolFile) { + stream._stage = STAGE_FILES + stream.push(progress) + return + } + } + + // Add info to steam. Only run the regexp when the stream is on + // `STAGE_HEADERS` for perf improvment + if (stream._stage === STAGE_HEADERS) { + const matchHeaders = stream._matchHeaders(line) + if (matchHeaders) { + stream.addInfos(matchHeaders) + return + } + } + + // An empty line is created at the end of the listing of files. This empty + // line means that 7zip has reached the end of the listing, and will next + // outputs infos for the STAGE_FOOTERS. + if (stream._stage === STAGE_FILES) { + const isLineEmpty = (line === '') + if (isLineEmpty) { + stream._stage = STAGE_FOOTERS + } + return + } + + // Add info to steam. Only run the regexp when the stream is on + // `STAGE_FOOTERS` for perf improvment + if (stream._stage === STAGE_FOOTERS) { + const matchFooters = stream._matchFooters(line) + if (matchFooters) { + stream.addInfos(matchFooters) + } + return + } + + }) + + return this + } + + onSubprocessError (chunk) { + const stderr = chunk.toString() + const inLineErrorRegExp = /ERROR: (?.*)\n/ + const offLineErrorRegExp = /ERROR:\n(?.*)\n/ + const inLineError = stderr.match(inLineErrorRegExp) + const offLineError = stderr.match(offLineErrorRegExp) + let message = 'unknown error' + message = (inLineError) ? inLineError.groups.message : message + message = (offLineError) ? offLineError.groups.message : message + const err = new Error(message) + err.stderr = stderr // @TODO doc: usage of raw stderr to get more info + this.emit('error', err) + + return this + } + + onSpawnError (err) { + this.emit('error', err) + + return this + } }