GeezerVison is showing github.com — repl: break on sigint/ctrl+c · nodejs/node@da8e510
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/repl.md +3Lines changed: 3 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -390,6 +390,9 @@ added: v0.1.91 | 390 | 390 | equivalent to prefacing every repl statement with `'use strict'`. | 391 | 391 | * `repl.REPL_MODE_MAGIC` - attempt to evaluates expressions in default | 392 | 392 | mode. If expressions fail to parse, re-try in strict mode. | | 393 | + * `breakEvalOnSigint` - Stop evaluating the current piece of code when | | 394 | + `SIGINT` is received, i.e. `Ctrl+C` is pressed. This cannot be used together | | 395 | + with a custom `eval` function. Defaults to `false`. | 393 | 396 |
| 394 | 397 | The `repl.start()` method creates and starts a `repl.REPLServer` instance. | 395 | 398 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/repl.js +2-1Lines changed: 2 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -22,7 +22,8 @@ function createRepl(env, opts, cb) { | 22 | 22 | opts = opts || { | 23 | 23 | ignoreUndefined: false, | 24 | 24 | terminal: process.stdout.isTTY, | 25 | | - useGlobal: true | | 25 | + useGlobal: true, | | 26 | + breakEvalOnSigint: true | 26 | 27 | }; | 27 | 28 |
| 28 | 29 | if (parseInt(env.NODE_NO_READLINE)) { | |
Collapse file Copy file name to clipboardExpand all lines: lib/repl.js +46-5Lines changed: 46 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 24 | 24 | const internalModule = require('internal/module'); | 25 | 25 | const internalUtil = require('internal/util'); | 26 | 26 | const util = require('util'); | | 27 | +const utilBinding = process.binding('util'); | 27 | 28 | const inherits = util.inherits; | 28 | 29 | const Stream = require('stream'); | 29 | 30 | const vm = require('vm'); | @@ -178,7 +179,7 @@ function REPLServer(prompt, | 178 | 179 | replMode); | 179 | 180 | } | 180 | 181 |
| 181 | | - var options, input, output, dom; | | 182 | + var options, input, output, dom, breakEvalOnSigint; | 182 | 183 | if (prompt !== null && typeof prompt === 'object') { | 183 | 184 | // an options object was given | 184 | 185 | options = prompt; | @@ -191,10 +192,17 @@ function REPLServer(prompt, | 191 | 192 | prompt = options.prompt; | 192 | 193 | dom = options.domain; | 193 | 194 | replMode = options.replMode; | | 195 | + breakEvalOnSigint = options.breakEvalOnSigint; | 194 | 196 | } else { | 195 | 197 | options = {}; | 196 | 198 | } | 197 | 199 |
| | 200 | + if (breakEvalOnSigint && eval_) { | | 201 | + // Allowing this would not reflect user expectations. | | 202 | + // breakEvalOnSigint affects only the behaviour of the default eval(). | | 203 | + throw new Error('Cannot specify both breakEvalOnSigint and eval for REPL'); | | 204 | + } | | 205 | + | 198 | 206 | var self = this; | 199 | 207 |
| 200 | 208 | self._domain = dom || domain.create(); | @@ -204,6 +212,7 @@ function REPLServer(prompt, | 204 | 212 | self.replMode = replMode || exports.REPL_MODE_SLOPPY; | 205 | 213 | self.underscoreAssigned = false; | 206 | 214 | self.last = undefined; | | 215 | + self.breakEvalOnSigint = !!breakEvalOnSigint; | 207 | 216 |
| 208 | 217 | self._inTemplateLiteral = false; | 209 | 218 |
| @@ -267,14 +276,46 @@ function REPLServer(prompt, | 267 | 276 | regExMatcher.test(savedRegExMatches.join(sep)); | 268 | 277 |
| 269 | 278 | if (!err) { | | 279 | + // Unset raw mode during evaluation so that Ctrl+C raises a signal. | | 280 | + let previouslyInRawMode; | | 281 | + if (self.breakEvalOnSigint) { | | 282 | + // Start the SIGINT watchdog before entering raw mode so that a very | | 283 | + // quick Ctrl+C doesn’t lead to aborting the process completely. | | 284 | + utilBinding.startSigintWatchdog(); | | 285 | + previouslyInRawMode = self._setRawMode(false); | | 286 | + } | | 287 | + | 270 | 288 | try { | 271 | | - if (self.useGlobal) { | 272 | | - result = script.runInThisContext({ displayErrors: false }); | 273 | | - } else { | 274 | | - result = script.runInContext(context, { displayErrors: false }); | | 289 | + try { | | 290 | + const scriptOptions = { | | 291 | + displayErrors: false, | | 292 | + breakOnSigint: self.breakEvalOnSigint | | 293 | + }; | | 294 | + | | 295 | + if (self.useGlobal) { | | 296 | + result = script.runInThisContext(scriptOptions); | | 297 | + } else { | | 298 | + result = script.runInContext(context, scriptOptions); | | 299 | + } | | 300 | + } finally { | | 301 | + if (self.breakEvalOnSigint) { | | 302 | + // Reset terminal mode to its previous value. | | 303 | + self._setRawMode(previouslyInRawMode); | | 304 | + | | 305 | + // Returns true if there were pending SIGINTs *after* the script | | 306 | + // has terminated without being interrupted itself. | | 307 | + if (utilBinding.stopSigintWatchdog()) { | | 308 | + self.emit('SIGINT'); | | 309 | + } | | 310 | + } | 275 | 311 | } | 276 | 312 | } catch (e) { | 277 | 313 | err = e; | | 314 | + if (err.message === 'Script execution interrupted.') { | | 315 | + // The stack trace for this case is not very useful anyway. | | 316 | + Object.defineProperty(err, 'stack', { value: '' }); | | 317 | + } | | 318 | + | 278 | 319 | if (err && process.domain) { | 279 | 320 | debug('not recoverable, send to domain'); | 280 | 321 | process.domain.emit('error', err); | |
Collapse file Copy file name to clipboard +50Lines changed: 50 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | +const common = require('../common'); | | 3 | +const assert = require('assert'); | | 4 | + | | 5 | +const spawn = require('child_process').spawn; | | 6 | + | | 7 | +if (process.platform === 'win32') { | | 8 | + // No way to send CTRL_C_EVENT to processes from JS right now. | | 9 | + common.skip('platform not supported'); | | 10 | + return; | | 11 | +} | | 12 | + | | 13 | +process.env.REPL_TEST_PPID = process.pid; | | 14 | +const child = spawn(process.execPath, [ '-i' ], { | | 15 | + stdio: [null, null, 2] | | 16 | +}); | | 17 | + | | 18 | +let stdout = ''; | | 19 | +child.stdout.setEncoding('utf8'); | | 20 | +child.stdout.pipe(process.stdout); | | 21 | +child.stdout.on('data', function(c) { | | 22 | + stdout += c; | | 23 | +}); | | 24 | + | | 25 | +child.stdin.write = ((original) => { | | 26 | + return (chunk) => { | | 27 | + process.stderr.write(chunk); | | 28 | + return original.call(child.stdin, chunk); | | 29 | + }; | | 30 | +})(child.stdin.write); | | 31 | + | | 32 | +child.stdout.once('data', common.mustCall(() => { | | 33 | + process.on('SIGUSR2', common.mustCall(() => { | | 34 | + process.kill(child.pid, 'SIGINT'); | | 35 | + child.stdout.once('data', common.mustCall(() => { | | 36 | + // Make sure REPL still works. | | 37 | + child.stdin.end('"foobar"\n'); | | 38 | + })); | | 39 | + })); | | 40 | + | | 41 | + child.stdin.write('process.kill(+process.env.REPL_TEST_PPID, "SIGUSR2");' + | | 42 | + 'vm.runInThisContext("while(true){}", ' + | | 43 | + '{ breakOnSigint: true });\n'); | | 44 | +})); | | 45 | + | | 46 | +child.on('close', function(code) { | | 47 | + assert.strictEqual(code, 0); | | 48 | + assert.notStrictEqual(stdout.indexOf('Script execution interrupted.'), -1); | | 49 | + assert.notStrictEqual(stdout.indexOf('foobar'), -1); | | 50 | +}); |
Collapse file Copy file name to clipboard +50Lines changed: 50 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | +const common = require('../common'); | | 3 | +const assert = require('assert'); | | 4 | + | | 5 | +const spawn = require('child_process').spawn; | | 6 | + | | 7 | +if (process.platform === 'win32') { | | 8 | + // No way to send CTRL_C_EVENT to processes from JS right now. | | 9 | + common.skip('platform not supported'); | | 10 | + return; | | 11 | +} | | 12 | + | | 13 | +process.env.REPL_TEST_PPID = process.pid; | | 14 | +const child = spawn(process.execPath, [ '-i' ], { | | 15 | + stdio: [null, null, 2] | | 16 | +}); | | 17 | + | | 18 | +let stdout = ''; | | 19 | +child.stdout.setEncoding('utf8'); | | 20 | +child.stdout.pipe(process.stdout); | | 21 | +child.stdout.on('data', function(c) { | | 22 | + stdout += c; | | 23 | +}); | | 24 | + | | 25 | +child.stdin.write = ((original) => { | | 26 | + return (chunk) => { | | 27 | + process.stderr.write(chunk); | | 28 | + return original.call(child.stdin, chunk); | | 29 | + }; | | 30 | +})(child.stdin.write); | | 31 | + | | 32 | +child.stdout.once('data', common.mustCall(() => { | | 33 | + process.on('SIGUSR2', common.mustCall(() => { | | 34 | + process.kill(child.pid, 'SIGINT'); | | 35 | + child.stdout.once('data', common.mustCall(() => { | | 36 | + // Make sure state from before the interruption is still available. | | 37 | + child.stdin.end('a*2*3*7\n'); | | 38 | + })); | | 39 | + })); | | 40 | + | | 41 | + child.stdin.write('a = 1001;' + | | 42 | + 'process.kill(+process.env.REPL_TEST_PPID, "SIGUSR2");' + | | 43 | + 'while(true){}\n'); | | 44 | +})); | | 45 | + | | 46 | +child.on('close', function(code) { | | 47 | + assert.strictEqual(code, 0); | | 48 | + assert.notStrictEqual(stdout.indexOf('Script execution interrupted.\n'), -1); | | 49 | + assert.notStrictEqual(stdout.indexOf('42042\n'), -1); | | 50 | +}); |
|