From c0721bcd66829356950b58cc532d6e3d8bbfc641 Mon Sep 17 00:00:00 2001 From: isaacs Date: Thu, 14 Mar 2013 14:16:13 -0700 Subject: [PATCH] repl: Use a domain to catch async errors safely Fix #2031 --- lib/repl.js | 24 ++++++-- test/simple/test-repl-domain.js | 3 + test/simple/test-repl-options.js | 1 - test/simple/test-repl-timeout-throw.js | 77 ++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 test/simple/test-repl-timeout-throw.js diff --git a/lib/repl.js b/lib/repl.js index f7be0c5b86..52893cddcb 100644 --- a/lib/repl.js +++ b/lib/repl.js @@ -49,6 +49,7 @@ var fs = require('fs'); var rl = require('readline'); var Console = require('console').Console; var EventEmitter = require('events').EventEmitter; +var domain = require('domain'); // If obj.hasOwnProperty has been overridden, then calling // obj.hasOwnProperty(prop) will break. @@ -81,7 +82,7 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) { EventEmitter.call(this); - var options, input, output; + var options, input, output, dom; if (typeof prompt == 'object') { // an options object was given options = prompt; @@ -92,6 +93,7 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) { useGlobal = options.useGlobal; ignoreUndefined = options.ignoreUndefined; prompt = options.prompt; + dom = options.domain; } else if (typeof prompt != 'string') { throw new Error('An options Object, or a prompt String are required'); } else { @@ -100,10 +102,14 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) { var self = this; + self._domain = dom || domain.create(); + self.useGlobal = !!useGlobal; self.ignoreUndefined = !!ignoreUndefined; - self.eval = eval_ || function(code, context, file, cb) { + eval_ = eval_ || defaultEval; + + function defaultEval(code, context, file, cb) { var err, result; try { if (self.useGlobal) { @@ -114,14 +120,22 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) { } catch (e) { err = e; } - if (err && process.domain) { + if (err && process.domain && !isSyntaxError(err)) { process.domain.emit('error', err); process.domain.exit(); } else { cb(err, result); } - }; + } + + self.eval = self._domain.bind(eval_); + + self._domain.on('error', function(e) { + self.outputStream.write((e.stack || e) + '\n'); + self.bufferedCommand = ''; + self.displayPrompt(); + }); if (!input && !output) { // legacy API, passing a 'stream'/'socket' option @@ -279,7 +293,7 @@ function REPLServer(prompt, stream, eval_, useGlobal, ignoreUndefined) { self.displayPrompt(); return; } else if (e) { - self.outputStream.write((e.stack || e) + '\n'); + self._domain.emit('error', e); } // Clear buffer if no SyntaxErrors diff --git a/test/simple/test-repl-domain.js b/test/simple/test-repl-domain.js index 347664ac38..55b7dc475e 100644 --- a/test/simple/test-repl-domain.js +++ b/test/simple/test-repl-domain.js @@ -19,6 +19,9 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE // USE OR OTHER DEALINGS IN THE SOFTWARE. +var assert = require('assert'); +var common = require('../common.js'); + var util = require('util'); var repl = require('repl'); diff --git a/test/simple/test-repl-options.js b/test/simple/test-repl-options.js index d3fcd588ee..f9175e7df2 100644 --- a/test/simple/test-repl-options.js +++ b/test/simple/test-repl-options.js @@ -67,5 +67,4 @@ assert.equal(r2.rli.terminal, false); assert.equal(r2.useColors, true); assert.equal(r2.useGlobal, true); assert.equal(r2.ignoreUndefined, true); -assert.equal(r2.eval, evaler); assert.equal(r2.writer, writer); diff --git a/test/simple/test-repl-timeout-throw.js b/test/simple/test-repl-timeout-throw.js new file mode 100644 index 0000000000..0cafa4dcbe --- /dev/null +++ b/test/simple/test-repl-timeout-throw.js @@ -0,0 +1,77 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +var assert = require('assert'); +var common = require('../common.js'); + +var spawn = require('child_process').spawn; + +var child = spawn(process.execPath, [ '-i' ], { + stdio: [null, null, 2] +}); + +var stdout = ''; +child.stdout.setEncoding('utf8'); +child.stdout.on('data', function(c) { + process.stdout.write(c); + stdout += c; +}); + +child.stdin.write = function(original) { return function(c) { + process.stderr.write(c); + return original.call(child.stdin, c); +}}(child.stdin.write); + +child.stdout.once('data', function() { + child.stdin.write('var throws = 0;'); + child.stdin.write('process.on("exit",function(){console.log(throws)});'); + child.stdin.write('function thrower(){console.log("THROW",throws++);XXX};'); + child.stdin.write('setTimeout(thrower);""\n'); + + setTimeout(fsTest, 50); + function fsTest() { + var f = JSON.stringify(__filename); + child.stdin.write('fs.readFile(' + f + ', thrower);\n'); + setTimeout(eeTest, 50); + } + + function eeTest() { + child.stdin.write('setTimeout(function() {\n' + + ' var events = require("events");\n' + + ' var e = new events.EventEmitter;\n' + + ' process.nextTick(function() {\n' + + ' e.on("x", thrower);\n' + + ' setTimeout(function() {\n' + + ' e.emit("x");\n' + + ' });\n' + + ' });\n' + + '});"";\n'); + + setTimeout(child.stdin.end.bind(child.stdin), 50); + } +}); + +child.on('exit', function(c) { + assert(!c); + // make sure we got 3 throws, in the end. + var lastLine = stdout.trim().split(/\r?\n/).pop(); + assert.equal(lastLine, '> 3'); +});