diff --git a/README.md b/README.md index 96e1b69..0fce35b 100644 --- a/README.md +++ b/README.md @@ -315,7 +315,7 @@ Fires when the process has been terminated, with an error or not. #### event: `pythonError` -Fires when the process terminates with a non-zero exit code. +Fires when the process terminates with a non-zero exit code or an output parser throws before normal completion. Parser exceptions are also passed to `.end()` and reject `PythonShell.run()`. #### event: `error` diff --git a/index.ts b/index.ts index 9569354..14418dd 100644 --- a/index.ts +++ b/index.ts @@ -169,6 +169,7 @@ export class PythonShell extends EventEmitter { let self = this; let errorData = ''; + let parserError: Error; EventEmitter.call(this); options = extend({}, PythonShell.defaultOptions, options); @@ -205,7 +206,15 @@ export class PythonShell extends EventEmitter { // note that setting the encoding turns the chunk into a string stdoutSplitter.setEncoding(options.encoding || 'utf8'); this.stdout.pipe(stdoutSplitter).on('data', (chunk: string) => { - this.emit('message', self.parser(chunk)); + if (parserError) return; + let parsedChunk: any; + try { + parsedChunk = self.parser(chunk); + } catch (err) { + parserError = err instanceof Error ? err : new Error(String(err)); + return; + } + this.emit('message', parsedChunk); }); } @@ -215,7 +224,15 @@ export class PythonShell extends EventEmitter { // note that setting the encoding turns the chunk into a string stderrSplitter.setEncoding(options.encoding || 'utf8'); this.stderr.pipe(stderrSplitter).on('data', (chunk: string) => { - this.emit('stderr', self.stderrParser(chunk)); + if (parserError) return; + let parsedChunk: any; + try { + parsedChunk = self.stderrParser(chunk); + } catch (err) { + parserError = err instanceof Error ? err : new Error(String(err)); + return; + } + this.emit('stderr', parsedChunk); }); } @@ -266,6 +283,12 @@ export class PythonShell extends EventEmitter { 'process exited with code ' + self.exitCode, ); } + } else if (parserError) { + err = new PythonShellError(parserError.message); + err.stack = parserError.stack; + } + + if (err) { err = extend(err, { executable: pythonPath, options: pythonOptions.length ? pythonOptions : null, diff --git a/test/test-python-shell.ts b/test/test-python-shell.ts index 3d5de81..4540d73 100644 --- a/test/test-python-shell.ts +++ b/test/test-python-shell.ts @@ -1,5 +1,5 @@ import * as should from 'should'; -import { PythonShell } from '..'; +import { NewlineTransformer, PythonShell } from '..'; import { sep, join } from 'path'; import { EOL as newline } from 'os'; import { chdir, cwd } from 'process'; @@ -415,6 +415,51 @@ describe('PythonShell', function () { }) .end(done); }); + it('should report JSON parser errors through the end callback', function (done) { + let pyshell = new PythonShell('echo_text.py', { + mode: 'json', + formatter: 'text', + }); + pyshell.send('not-json').end(function (err) { + should.exist(err); + err.should.be.an.Error; + err.message.should.match(/JSON|Unexpected token/); + done(); + }); + }); + it('should not treat message listener exceptions as parser errors', function (done) { + let splitter = new NewlineTransformer(); + let pyshell = new PythonShell('exit-code.py', { mode: 'text' }, splitter); + let listenerError = new Error('message listener failed'); + let thrownError: Error; + + pyshell.on('message', function () { + throw listenerError; + }); + + try { + splitter.emit('data', 'hello'); + } catch (err) { + thrownError = err; + } + + should.exist(thrownError); + thrownError.should.be.exactly(listenerError); + pyshell.end(function (err) { + should.not.exist(err); + done(); + }); + }); + it('should prefer process errors over parser errors on non-zero exit', function (done) { + PythonShell.run('echo_hi_then_error.py', { mode: 'json' }).then( + () => done('expected the process to reject'), + (err) => { + err.message.should.be.exactly('Exception: fibble-fah'); + err.stack.should.containEql('----- Python Traceback -----'); + done(); + }, + ); + }); it('should properly buffer partial messages', function (done) { // echo_text_with_newline_control echoes text with $'s replaced with newlines let pyshell = new PythonShell('echo_text_with_newline_control.py', { @@ -494,6 +539,46 @@ describe('PythonShell', function () { .send('world') .end(done); }); + it('should report stderr parser errors through the end callback', function (done) { + let pyshell = new PythonShell('stderrLogging.py', { + stderrParser: function () { + throw new Error('stderr parser failed'); + }, + }); + pyshell.end(function (err) { + should.exist(err); + err.message.should.be.exactly('stderr parser failed'); + done(); + }); + }); + it('should not treat stderr listener exceptions as parser errors', function (done) { + let splitter = new NewlineTransformer(); + let pyshell = new PythonShell( + 'exit-code.py', + { mode: 'text' }, + null, + splitter, + ); + let listenerError = new Error('stderr listener failed'); + let thrownError: Error; + + pyshell.on('stderr', function () { + throw listenerError; + }); + + try { + splitter.emit('data', 'hello'); + } catch (err) { + thrownError = err; + } + + should.exist(thrownError); + thrownError.should.be.exactly(listenerError); + pyshell.end(function (err) { + should.not.exist(err); + done(); + }); + }); it('should not be invoked when mode is "binary"', function (done) { let pyshell = new PythonShell('stderrLogging.py', { stderrParser: 'binary',