Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit aad7d41

Browse filesBrowse files
addaleaxFishrock123
authored andcommitted
repl: break on sigint/ctrl+c
Adds the ability to stop execution of the current REPL command when receiving SIGINT. This applies only to the default eval function. Fixes: #6612 PR-URL: #6635 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
1 parent da1dffb commit aad7d41
Copy full SHA for aad7d41

File tree

Expand file treeCollapse file tree

5 files changed

+151
-6
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

5 files changed

+151
-6
lines changed
Open diff view settings
Collapse file

‎doc/api/repl.md‎

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 numberDiff line numberDiff line change
@@ -390,6 +390,9 @@ added: v0.1.91
390390
equivalent to prefacing every repl statement with `'use strict'`.
391391
* `repl.REPL_MODE_MAGIC` - attempt to evaluates expressions in default
392392
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`.
393396

394397
The `repl.start()` method creates and starts a `repl.REPLServer` instance.
395398

Collapse file

‎lib/internal/repl.js‎

Copy file name to clipboardExpand all lines: lib/internal/repl.js
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ function createRepl(env, opts, cb) {
2222
opts = opts || {
2323
ignoreUndefined: false,
2424
terminal: process.stdout.isTTY,
25-
useGlobal: true
25+
useGlobal: true,
26+
breakEvalOnSigint: true
2627
};
2728

2829
if (parseInt(env.NODE_NO_READLINE)) {
Collapse file

‎lib/repl.js‎

Copy file name to clipboardExpand all lines: lib/repl.js
+46-5Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
const internalModule = require('internal/module');
2525
const internalUtil = require('internal/util');
2626
const util = require('util');
27+
const utilBinding = process.binding('util');
2728
const inherits = util.inherits;
2829
const Stream = require('stream');
2930
const vm = require('vm');
@@ -178,7 +179,7 @@ function REPLServer(prompt,
178179
replMode);
179180
}
180181

181-
var options, input, output, dom;
182+
var options, input, output, dom, breakEvalOnSigint;
182183
if (prompt !== null && typeof prompt === 'object') {
183184
// an options object was given
184185
options = prompt;
@@ -191,10 +192,17 @@ function REPLServer(prompt,
191192
prompt = options.prompt;
192193
dom = options.domain;
193194
replMode = options.replMode;
195+
breakEvalOnSigint = options.breakEvalOnSigint;
194196
} else {
195197
options = {};
196198
}
197199

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+
198206
var self = this;
199207

200208
self._domain = dom || domain.create();
@@ -204,6 +212,7 @@ function REPLServer(prompt,
204212
self.replMode = replMode || exports.REPL_MODE_SLOPPY;
205213
self.underscoreAssigned = false;
206214
self.last = undefined;
215+
self.breakEvalOnSigint = !!breakEvalOnSigint;
207216

208217
self._inTemplateLiteral = false;
209218

@@ -267,14 +276,46 @@ function REPLServer(prompt,
267276
regExMatcher.test(savedRegExMatches.join(sep));
268277

269278
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+
270288
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+
}
275311
}
276312
} catch (e) {
277313
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+
278319
if (err && process.domain) {
279320
debug('not recoverable, send to domain');
280321
process.domain.emit('error', err);
Collapse file
+50Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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

‎test/parallel/test-repl-sigint.js‎

Copy file name to clipboard
+50Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
});

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.