Skip to content

Commit

Permalink
fix: correct interrupt signal handling in REPL
Browse files Browse the repository at this point in the history
Avoid goroutines leak, accumulation of defered functions and
spurious resets of signal handlers. Effectively catch interrupt
signal (Ctrl-C) to cancel current eval.

Fixes #713.
  • Loading branch information
mvertes authored Aug 12, 2020
1 parent 611a8c3 commit b0cd93a
Showing 1 changed file with 45 additions and 34 deletions.
79 changes: 45 additions & 34 deletions interp/interp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package interp
import (
"bufio"
"context"
"errors"
"fmt"
"go/build"
"go/scanner"
Expand Down Expand Up @@ -493,13 +494,20 @@ func (interp *Interpreter) Use(values Exports) {
}
}

type scannerErrors scanner.ErrorList

func (sce scannerErrors) isEOF() bool {
for _, v := range sce {
return strings.HasSuffix(v.Msg, `, found 'EOF'`)
// ignoreScannerError returns true if the error from Go scanner can be safely ignored
// to let the caller grab one more line before retrying to parse its input.
func ignoreScannerError(e *scanner.Error, s string) bool {
msg := e.Msg
if strings.HasSuffix(msg, "found 'EOF'") {
return true
}
if msg == "raw string literal not terminated" {
return true
}
if strings.HasPrefix(msg, "expected operand, found '}'") && !strings.HasSuffix(s, "}") {
return true
}
return true
return false
}

// REPL performs a Read-Eval-Print-Loop on input reader.
Expand All @@ -518,42 +526,58 @@ func (interp *Interpreter) REPL(in io.Reader, out io.Writer) {
sc.sym[name] = &symbol{kind: pkgSym, typ: &itype{cat: binPkgT, path: k, scope: sc}}
}

// Set prompt.
var v reflect.Value
var err error
prompt := getPrompt(in, out)
ctx, cancel := context.WithCancel(context.Background())
end := make(chan struct{}) // channel to terminate signal handling goroutine
sig := make(chan os.Signal, 1) // channel to trap interrupt signal (Ctrl-C)
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)
prompt(v)

// Read, Eval, Print in a Loop.
src := ""
s := bufio.NewScanner(in)
for s.Scan() {
src += s.Text() + "\n"
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
handleSignal(ctx, cancel)

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

v, err = interp.EvalWithContext(ctx, src)
signal.Reset()
if err != nil {
switch e := err.(type) {
case scanner.ErrorList:
if scannerErrors(e).isEOF() {
// Early failure in the scanner: the source is incomplete
// and no AST could be produced, neither compiled / run.
// Get one more line, and retry
if len(e) == 0 || ignoreScannerError(e[0], s.Text()) {
continue
}
fmt.Fprintln(out, err)
fmt.Fprintln(out, e[0])
case Panic:
fmt.Fprintln(out, e.Value)
fmt.Fprintln(out, string(e.Stack))
default:
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?
}

Expand Down Expand Up @@ -581,16 +605,3 @@ func getPrompt(in io.Reader, out io.Writer) func(reflect.Value) {
}
return func(reflect.Value) {}
}

// handleSignal wraps signal handling for eval cancellation.
func handleSignal(ctx context.Context, cancel context.CancelFunc) {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
select {
case <-c:
cancel()
case <-ctx.Done():
}
}()
}

0 comments on commit b0cd93a

Please sign in to comment.