Skip to content

Commit aad7d41

Browse 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

File tree

5 files changed

+151
-6
lines changed

5 files changed

+151
-6
lines changed

doc/api/repl.md

Lines changed: 3 additions & 0 deletions
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

lib/internal/repl.js

Lines 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)) {

lib/repl.js

Lines 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);
Lines 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+
});

test/parallel/test-repl-sigint.js

Lines 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)