Skip to content

Commit da8e510

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 72d659a commit da8e510

5 files changed

Lines changed: 151 additions & 6 deletions

File tree

‎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
constinternalModule=require('internal/module');
2525
constinternalUtil=require('internal/util');
2626
constutil=require('util');
27+
constutilBinding=process.binding('util');
2728
constinherits=util.inherits;
2829
constStream=require('stream');
2930
constvm=require('vm');
@@ -178,7 +179,7 @@ function REPLServer(prompt,
178179
replMode);
179180
}
180181

181-
varoptions,input,output,dom;
182+
varoptions,input,output,dom,breakEvalOnSigint;
182183
if(prompt!==null&&typeofprompt==='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+
thrownewError('Cannot specify both breakEvalOnSigint and eval for REPL');
204+
}
205+
198206
varself=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+
letpreviouslyInRawMode;
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+
constscriptOptions={
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+
constcommon=require('../common');
3+
constassert=require('assert');
4+
5+
constspawn=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+
constchild=spawn(process.execPath,['-i'],{
15+
stdio: [null,null,2]
16+
});
17+
18+
letstdout='';
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+
returnoriginal.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+
constcommon=require('../common');
3+
constassert=require('assert');
4+
5+
constspawn=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+
constchild=spawn(process.execPath,['-i'],{
15+
stdio: [null,null,2]
16+
});
17+
18+
letstdout='';
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+
returnoriginal.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)