Skip to content

Commit

Permalink
feature: improve handling of interrupt signal in REPL
Browse files Browse the repository at this point in the history
The input scanning is now performed in a sub goroutine and
the interrupt is listened in another goroutine, either to cancel Eval
or to cancel the current line scan.
Entering a '\n' after a 'Ctrl-C` to get the prompt is
not necessary anymore.
  • Loading branch information
mvertes committed Aug 27, 2020
1 parent f3f9ffa commit b1279d0
Showing 1 changed file with 30 additions and 17 deletions.
47 changes: 30 additions & 17 deletions interp/interp.go
Expand Up @@ -506,8 +506,8 @@ func (interp *Interpreter) EvalWithContext(ctx context.Context, src string) (ref
interp.stop()
return reflect.Value{}, ctx.Err()
case <-done:
return v, err
}
return v, err
}

// stop sends a semaphore to all running frames and closes the chan
Expand Down Expand Up @@ -581,34 +581,55 @@ func (interp *Interpreter) REPL(in io.Reader, out io.Writer) {
}

ctx, cancel := context.WithCancel(context.Background())
end := make(chan struct{}) // channel to terminate signal handling goroutine
end := make(chan struct{}) // channel to terminate the REPL
sig := make(chan os.Signal, 1) // channel to trap interrupt signal (Ctrl-C)
lines := make(chan string) // channel to read REPL input lines
prompt := getPrompt(in, out) // prompt activated on tty like IO stream
s := bufio.NewScanner(in) // read input stream line by line
var v reflect.Value // result value from eval
var err error // error from eval
src := "" // source string to evaluate

signal.Notify(sig, os.Interrupt)
defer signal.Stop(sig)
prompt(v)

// Read, Eval, Print in a Loop.
for s.Scan() {
src += s.Text() + "\n"
go func() {
defer close(end)
for s.Scan() {
lines <- s.Text()
}
// TODO(mpl): log s.Err() if not nil?
}()

// The following goroutine handles interrupt signal by canceling eval.
go func() {
go func() {
for {
select {
case <-sig:
cancel()
lines <- ""
case <-end:
return
}
}()
}
}()

for {
var line string

select {
case <-end:
cancel()
return
case line = <-lines:
src += line + "\n"
}

v, err = interp.EvalWithContext(ctx, src)
if err != nil {
switch e := err.(type) {
case scanner.ErrorList:
if len(e) == 0 || ignoreScannerError(e[0], s.Text()) {
if len(e) == 0 || ignoreScannerError(e[0], line) {
continue
}
fmt.Fprintln(out, e[0])
Expand All @@ -619,20 +640,12 @@ func (interp *Interpreter) REPL(in io.Reader, out io.Writer) {
fmt.Fprintln(out, err)
}
}

if errors.Is(err, context.Canceled) {
// Eval has been interrupted by the above signal handling goroutine.
ctx, cancel = context.WithCancel(context.Background())
} else {
// No interrupt, release the above signal handling goroutine.
end <- struct{}{}
}

src = ""
prompt(v)
}
cancel() // Do not defer, as cancel func may change over time.
// TODO(mpl): log s.Err() if not nil?
}

// Repl performs a Read-Eval-Print-Loop on input file descriptor.
Expand Down

0 comments on commit b1279d0

Please sign in to comment.