17
17
package console
18
18
19
19
import (
20
+ "errors"
20
21
"fmt"
21
22
"io"
22
23
"io/ioutil"
@@ -26,6 +27,7 @@ import (
26
27
"regexp"
27
28
"sort"
28
29
"strings"
30
+ "sync"
29
31
"syscall"
30
32
31
33
"github.com/dop251/goja"
@@ -74,6 +76,13 @@ type Console struct {
74
76
histPath string // Absolute path to the console scrollback history
75
77
history []string // Scroll history maintained by the console
76
78
printer io.Writer // Output writer to serialize any display strings to
79
+
80
+ interactiveStopped chan struct {}
81
+ stopInteractiveCh chan struct {}
82
+ signalReceived chan struct {}
83
+ stopped chan struct {}
84
+ wg sync.WaitGroup
85
+ stopOnce sync.Once
77
86
}
78
87
79
88
// New initializes a JavaScript interpreted runtime environment and sets defaults
@@ -92,19 +101,27 @@ func New(config Config) (*Console, error) {
92
101
93
102
// Initialize the console and return
94
103
console := & Console {
95
- client : config .Client ,
96
- jsre : jsre .New (config .DocRoot , config .Printer ),
97
- prompt : config .Prompt ,
98
- prompter : config .Prompter ,
99
- printer : config .Printer ,
100
- histPath : filepath .Join (config .DataDir , HistoryFile ),
104
+ client : config .Client ,
105
+ jsre : jsre .New (config .DocRoot , config .Printer ),
106
+ prompt : config .Prompt ,
107
+ prompter : config .Prompter ,
108
+ printer : config .Printer ,
109
+ histPath : filepath .Join (config .DataDir , HistoryFile ),
110
+ interactiveStopped : make (chan struct {}),
111
+ stopInteractiveCh : make (chan struct {}),
112
+ signalReceived : make (chan struct {}, 1 ),
113
+ stopped : make (chan struct {}),
101
114
}
102
115
if err := os .MkdirAll (config .DataDir , 0700 ); err != nil {
103
116
return nil , err
104
117
}
105
118
if err := console .init (config .Preload ); err != nil {
106
119
return nil , err
107
120
}
121
+
122
+ console .wg .Add (1 )
123
+ go console .interruptHandler ()
124
+
108
125
return console , nil
109
126
}
110
127
@@ -337,9 +354,63 @@ func (c *Console) Evaluate(statement string) {
337
354
}
338
355
}()
339
356
c .jsre .Evaluate (statement , c .printer )
357
+
358
+ // Avoid exiting Interactive when jsre was interrupted by SIGINT.
359
+ c .clearSignalReceived ()
360
+ }
361
+
362
+ // interruptHandler runs in its own goroutine and waits for signals.
363
+ // When a signal is received, it interrupts the JS interpreter.
364
+ func (c * Console ) interruptHandler () {
365
+ defer c .wg .Done ()
366
+
367
+ // During Interactive, liner inhibits the signal while it is prompting for
368
+ // input. However, the signal will be received while evaluating JS.
369
+ //
370
+ // On unsupported terminals, SIGINT can also happen while prompting.
371
+ // Unfortunately, it is not possible to abort the prompt in this case and
372
+ // the c.readLines goroutine leaks.
373
+ sig := make (chan os.Signal , 1 )
374
+ signal .Notify (sig , syscall .SIGINT )
375
+ defer signal .Stop (sig )
376
+
377
+ for {
378
+ select {
379
+ case <- sig :
380
+ c .setSignalReceived ()
381
+ c .jsre .Interrupt (errors .New ("interrupted" ))
382
+ case <- c .stopInteractiveCh :
383
+ close (c .interactiveStopped )
384
+ c .jsre .Interrupt (errors .New ("interrupted" ))
385
+ case <- c .stopped :
386
+ return
387
+ }
388
+ }
389
+ }
390
+
391
+ func (c * Console ) setSignalReceived () {
392
+ select {
393
+ case c .signalReceived <- struct {}{}:
394
+ default :
395
+ }
396
+ }
397
+
398
+ func (c * Console ) clearSignalReceived () {
399
+ select {
400
+ case <- c .signalReceived :
401
+ default :
402
+ }
340
403
}
341
404
342
- // Interactive starts an interactive user session, where input is propted from
405
+ // StopInteractive causes Interactive to return as soon as possible.
406
+ func (c * Console ) StopInteractive () {
407
+ select {
408
+ case c .stopInteractiveCh <- struct {}{}:
409
+ case <- c .stopped :
410
+ }
411
+ }
412
+
413
+ // Interactive starts an interactive user session, where in.put is propted from
343
414
// the configured user prompter.
344
415
func (c * Console ) Interactive () {
345
416
var (
@@ -349,15 +420,11 @@ func (c *Console) Interactive() {
349
420
inputLine = make (chan string , 1 ) // receives user input
350
421
inputErr = make (chan error , 1 ) // receives liner errors
351
422
requestLine = make (chan string ) // requests a line of input
352
- interrupt = make (chan os.Signal , 1 )
353
423
)
354
424
355
- // Monitor Ctrl-C. While liner does turn on the relevant terminal mode bits to avoid
356
- // the signal, a signal can still be received for unsupported terminals. Unfortunately
357
- // there is no way to cancel the line reader when this happens. The readLines
358
- // goroutine will be leaked in this case.
359
- signal .Notify (interrupt , syscall .SIGINT , syscall .SIGTERM )
360
- defer signal .Stop (interrupt )
425
+ defer func () {
426
+ c .writeHistory ()
427
+ }()
361
428
362
429
// The line reader runs in a separate goroutine.
363
430
go c .readLines (inputLine , inputErr , requestLine )
@@ -368,7 +435,14 @@ func (c *Console) Interactive() {
368
435
requestLine <- prompt
369
436
370
437
select {
371
- case <- interrupt :
438
+ case <- c .interactiveStopped :
439
+ fmt .Fprintln (c .printer , "node is down, exiting console" )
440
+ return
441
+
442
+ case <- c .signalReceived :
443
+ // SIGINT received while prompting for input -> unsupported terminal.
444
+ // I'm not sure if the best choice would be to leave the console running here.
445
+ // Bash keeps running in this case. node.js does not.
372
446
fmt .Fprintln (c .printer , "caught interrupt, exiting" )
373
447
return
374
448
@@ -476,12 +550,19 @@ func (c *Console) Execute(path string) error {
476
550
477
551
// Stop cleans up the console and terminates the runtime environment.
478
552
func (c * Console ) Stop (graceful bool ) error {
553
+ c .stopOnce .Do (func () {
554
+ // Stop the interrupt handler.
555
+ close (c .stopped )
556
+ c .wg .Wait ()
557
+ })
558
+
559
+ c .jsre .Stop (graceful )
560
+ return nil
561
+ }
562
+
563
+ func (c * Console ) writeHistory () error {
479
564
if err := ioutil .WriteFile (c .histPath , []byte (strings .Join (c .history , "\n " )), 0600 ); err != nil {
480
565
return err
481
566
}
482
- if err := os .Chmod (c .histPath , 0600 ); err != nil { // Force 0600, even if it was different previously
483
- return err
484
- }
485
- c .jsre .Stop (graceful )
486
- return nil
567
+ return os .Chmod (c .histPath , 0600 ) // Force 0600, even if it was different previously
487
568
}
0 commit comments