Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

repl: fix freeze if util.inspect throws & clean up error handling a bit #1968

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 20 additions & 24 deletions lib/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,6 @@ function REPLServer(prompt,
}
} catch (e) {
err = e;
if (err && process.domain) {
debug('not recoverable, send to domain');
process.domain.emit('error', err);
process.domain.exit();
return;
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is moved to finish so all of the error handling can be in one place. It doesn't seem safe to do this here anyway since the evaluation function can be overriden. The explicit process.domain.exit is not carried over as 1) I don't understand what use it serves & I don't like to blindly c/p, 2) the other existing domain emit didn't come with an exit and 3) the repl+domain test passes without it. @isaacs you added this 2 years ago, do you remember?

}

Expand Down Expand Up @@ -314,40 +308,42 @@ function REPLServer(prompt,
debug('finish', e, ret);
self.memory(cmd);

if (e && !self.bufferedCommand && cmd.trim().match(/^npm /)) {
self.outputStream.write('npm should be run outside of the ' +
'node repl, in your normal shell.\n' +
'(Press Control-D to exit.)\n');
self.bufferedCommand = '';
self.displayPrompt();
return;
}

// If error was SyntaxError and not JSON.parse error
if (e) {
if (e instanceof Recoverable) {
if (!self.bufferedCommand && cmd.trim().match(/^npm /)) {
self.outputStream.write('npm should be run outside of the ' +
'node repl, in your normal shell.\n' +
'(Press Control-D to exit.)\n');
self.bufferedCommand = '';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We always shortcircuit if there is an error; group with the rest of the error handling.

self.displayPrompt();
} else if (e instanceof Recoverable) {
// Start buffering data like that:
// {
// ... x: 1
// ... }
self.bufferedCommand += cmd + '\n';
self.displayPrompt();
return;
} else {
self._domain.emit('error', e);
debug('not recoverable, send to domain');
process.domain.emit('error', e);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of sometimes delegating to self._domain and sometimes to process.domain, always use process.domain. Note the evaluation function is bound to self._domain and the only way the self._domain.emit case could have been hit is via a non-recoverable error in vm.createScript, so there is no chance that a different domain could have been installed. (And even if there was, which domain to report the error to is underspecified so we should do the simplest thing possible)

}
return;
}

// Clear buffer if no SyntaxErrors
self.bufferedCommand = '';

// If we got any output - print it (if no error)
if (!e && (!self.ignoreUndefined || ret !== undefined)) {
// If we got any output - print it
if (!self.ignoreUndefined || ret !== undefined) {
self.context._ = ret;
self.outputStream.write(self.writer(ret) + '\n');
try {
var s = self.writer(ret);
} catch(e) {
process.domain.emit('error', e);
return;
}
self.outputStream.write(s + '\n');
}

// Display prompt again
self.bufferedCommand = '';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In every other case modifying self.bufferedCommand is grouped with self.displayPrompt. Do the same here. Having self.bufferedCommand run before calling self.writer implies that it is necessary for maintaining the integrity of our state, since we could shortcircuit before self.displayPrompt. This is not the case since the domain error handler will reset self.bufferedCommand.

self.displayPrompt();
}
});
Expand Down
51 changes: 51 additions & 0 deletions test/parallel/test-repl-inspect-throw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict';
var assert = require('assert');
var common = require('../common');
var util = require('util');
var repl = require('repl');

// A stream to push an array into a REPL
function ArrayStream() {
this.run = function(data) {
var self = this;
data.forEach(function(line) {
self.emit('data', line + '\n');
});
};
}
util.inherits(ArrayStream, require('stream').Stream);
ArrayStream.prototype.readable = true;
ArrayStream.prototype.writable = true;
ArrayStream.prototype.resume = function() {};
ArrayStream.prototype.write = function() {};

var putIn = new ArrayStream();
var testMe = repl.start({
input: putIn,
output: putIn,
terminal: true
});

// Regression test: an exception raised during the 'keypress' event in readline
// would corrupt the state of the escape sequence decoder, rendering the REPL
// (and possibly your terminal) completely unusable.
//
// We can trigger this by triggering a throw inside util.inspect, which is used
// to print a string representation of what you evaluate in the REPL.
testMe.context.throwObj = {
inspect: function() {
throw new Error('inspect error');
}
};
testMe.context.reached = false;

// Exceptions for whatever reason do not reach the normal error handler after
// the inspect throw, but will work if run in another tick.
process.nextTick(function() {
assert(testMe.context.reached);
});

putIn.run([
'throwObj',
'reached = true'
]);