From f851855c0c1fd2e4a419158a795388b23a6f64da Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Sat, 31 Aug 2024 15:21:40 -0700 Subject: [PATCH 1/4] Use the new terminal to handle ^C --- extensions/extension.go | 15 ++------------- go.mod | 2 +- go.sum | 4 ++-- main.go | 3 ++- repl/repl.go | 27 ++++++++++++++++++--------- repl/repl_test.go | 3 ++- wasm/wasm_main.go | 3 ++- 7 files changed, 29 insertions(+), 28 deletions(-) diff --git a/extensions/extension.go b/extensions/extension.go index 3d2b397a..4fc99639 100644 --- a/extensions/extension.go +++ b/extensions/extension.go @@ -4,7 +4,6 @@ package extensions import ( "bytes" - "context" "encoding/json" "fmt" "io" @@ -17,6 +16,7 @@ import ( "unicode/utf8" "fortio.org/log" + "fortio.org/terminal" "github.com/rivo/uniseg" "grol.io/grol/eval" "grol.io/grol/lexer" @@ -390,7 +390,7 @@ func createMisc() { } durDur := time.Duration(durSec * 1e9) log.Infof("Sleeping for %v", durDur) - return s.Error(SleepWithContext(s.Context, durDur)) + return s.Error(terminal.SleepWithContext(s.Context, durDur)) }, }) MustCreate(object.Extension{ @@ -434,17 +434,6 @@ func createMisc() { // --- implementation of the functions that aren't inlined in lambdas above. -func SleepWithContext(ctx context.Context, duration time.Duration) error { - select { - case <-time.After(duration): - // Completed the sleep duration - return nil - case <-ctx.Done(): - // Context was canceled - return ctx.Err() - } -} - func pow(args []object.Object) object.Object { // Arg len check already done through MinArgs and MaxArgs // and so is type check through ArgTypes. diff --git a/go.mod b/go.mod index 0f33a0d7..68750d39 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( fortio.org/log v1.16.0 fortio.org/sets v1.2.0 fortio.org/struct2env v0.4.1 - fortio.org/terminal v0.7.3 + fortio.org/terminal v0.8.0-pre1 fortio.org/testscript v0.3.1 // only for tests fortio.org/version v1.0.4 github.com/rivo/uniseg v0.4.7 diff --git a/go.sum b/go.sum index cb1bc805..11d604c8 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ fortio.org/struct2env v0.4.1 h1:rJludAMO5eBvpWplWEQNqoVDFZr4RWMQX7RUapgZyc0= fortio.org/struct2env v0.4.1/go.mod h1:lENUe70UwA1zDUCX+8AsO663QCFqYaprk5lnPhjD410= fortio.org/term v0.23.0-fortio-6 h1:pKrUX0tKOxyEhkhLV50oJYucTVx94rzFrXc24lIuLvk= fortio.org/term v0.23.0-fortio-6/go.mod h1:7buBfn81wEJUGWiVjFNiUE/vxWs5FdM9c7PyZpZRS30= -fortio.org/terminal v0.7.3 h1:tQV8YikqowjuIxk3Ayfglf/AQWs2ywlBWuggAzCY6Dc= -fortio.org/terminal v0.7.3/go.mod h1:hRs4ZM5C06GyfLRD6JHD96yjJGgm1mXQH75Ze8CMLjw= +fortio.org/terminal v0.8.0-pre1 h1:OBqmHpVYhmDXjFQasMg9I/weAnXYcdCf+9tabk+spW0= +fortio.org/terminal v0.8.0-pre1/go.mod h1:hRs4ZM5C06GyfLRD6JHD96yjJGgm1mXQH75Ze8CMLjw= fortio.org/testscript v0.3.1 h1:MmRO64AsmzaU1KlYMzAbotJIMKRGxD1XXssJnBRiMGQ= fortio.org/testscript v0.3.1/go.mod h1:7OJ+U4avooRNqc7p/VHKJadYgj9fA6+N0SbGU8FVWGs= fortio.org/version v1.0.4 h1:FWUMpJ+hVTNc4RhvvOJzb0xesrlRmG/a+D6bjbQ4+5U= diff --git a/main.go b/main.go index 0aeb7bf7..2f8c4f79 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( + "context" "flag" "fmt" "io" @@ -123,7 +124,7 @@ func Main() (retcode int) { return log.FErrf("Error initializing extensions: %v", err) } if *commandFlag != "" { - res, errs, _ := repl.EvalStringWithOption(options, *commandFlag) + res, errs, _ := repl.EvalStringWithOption(context.Background(), options, *commandFlag) // Only parsing errors are already logged, eval errors aren't, we (re)log everything: numErrs := len(errs) if numErrs > 0 { diff --git a/repl/repl.go b/repl/repl.go index 321e9b49..ec60283d 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -147,7 +147,7 @@ func EvalAll(s *eval.State, in io.Reader, out io.Writer, options Options) []stri if options.PreInput != nil { options.PreInput(s) } - _, _, errs, _ := EvalOne(s, what, out, options) //nolint:dogsled // as mentioned we should refactor EvalOne. + _, _, errs, _ := EvalOne(context.Background(), s, what, out, options) //nolint:dogsled // as mentioned we should refactor EvalOne. return errs } @@ -162,7 +162,7 @@ func EvalStringOptions() Options { // occurred. // Default options are EvalStringOptions()'s. func EvalString(what string) (string, []string, string) { - return EvalStringWithOption(EvalStringOptions(), what) + return EvalStringWithOption(context.Background(), EvalStringOptions(), what) } // EvalStringWithOption can be used from playground etc for single eval. @@ -172,7 +172,7 @@ func EvalString(what string) (string, []string, string) { // Following options should be set (starting from EvalStringOptions()) to // additionally control the behavior: // AutoLoad, AutoSave, Compact. -func EvalStringWithOption(o Options, what string) (res string, errs []string, formatted string) { +func EvalStringWithOption(ctx context.Context, o Options, what string) (res string, errs []string, formatted string) { s := eval.NewState() if o.MaxDepth > 0 { s.MaxDepth = o.MaxDepth @@ -188,7 +188,7 @@ func EvalStringWithOption(o Options, what string) (res string, errs []string, fo } panicked := false incomplete := false - incomplete, panicked, errs, formatted = EvalOne(s, what, out, o) + incomplete, panicked, errs, formatted = EvalOne(ctx, s, what, out, o) if incomplete && len(errs) == 0 { errs = append(errs, "Incomplete input") } @@ -214,7 +214,7 @@ func Interactive(options Options) int { s := eval.NewState() s.MaxDepth = options.MaxDepth s.MaxValueLen = options.MaxValueLen // 0 is unlimited so ok to copy as is. - term, err := terminal.Open() + term, err := terminal.Open(context.Background()) if err != nil { return log.FErrf("Error creating readline: %v", err) } @@ -248,10 +248,15 @@ func Interactive(options Options) int { for { rd, err := term.ReadLine() if errors.Is(err, io.EOF) { - log.Infof("Exit requested") // Don't say EOF as ^C comes through as EOF as well. + log.Infof("EOF, exiting") _ = AutoSave(s, options) return 0 } + if errors.Is(err, terminal.ErrInterrupted) { + log.Debugf("Interrupted error") + term.ResetInterrupts(context.Background()) + continue + } if err != nil { return log.FErrf("Error reading line: %v", err) } @@ -280,10 +285,14 @@ func Interactive(options Options) int { fmt.Fprintln(term.Out, "Type 'history' to see history, '!n' to repeat history n, 'info' for language builtins, use for completion.") continue + case l == "exit": + log.Infof("Exit requested") + _ = AutoSave(s, options) + return 0 } // normal errors are already logged but not the panic recoveries // Note this is the only case that can get contNeeded (EOL instead of EOF mode) - contNeeded, _, _, formatted := EvalOne(s, l, term.Out, options) + contNeeded, _, _, formatted := EvalOne(term.Context, s, l, term.Out, options) if contNeeded { prev = l + "\n" term.SetPrompt(CONTINUATION) @@ -347,7 +356,7 @@ func (g *Grol) Run(out io.Writer) error { // errs is the list of errors, formatted is the normalized input. // If a panic occurs, panicked is true and errs contains the one panic message. // TODO: this one size fits 3 different calls (file, interactive, bot) is getting spaghetti. -func EvalOne(s *eval.State, what string, out io.Writer, options Options) ( +func EvalOne(ctx context.Context, s *eval.State, what string, out io.Writer, options Options) ( continuation, panicked bool, errs []string, formatted string, @@ -369,7 +378,7 @@ func EvalOne(s *eval.State, what string, out io.Writer, options Options) ( } }() } - s.SetContext(context.Background(), options.MaxDuration) + s.SetContext(ctx, options.MaxDuration) continuation, errs, formatted = evalOne(s, what, out, options) return } diff --git a/repl/repl_test.go b/repl/repl_test.go index 873ea5bb..d873a6a8 100644 --- a/repl/repl_test.go +++ b/repl/repl_test.go @@ -1,6 +1,7 @@ package repl_test import ( + "context" "testing" "grol.io/grol/eval" @@ -165,7 +166,7 @@ func TestPreInputHook(t *testing.T) { } inp := `testHook()` expected := "42\n" - res, errs, _ := repl.EvalStringWithOption(opts, inp) + res, errs, _ := repl.EvalStringWithOption(context.Background(), opts, inp) if res != expected || len(errs) > 0 { t.Errorf("EvalString() got %v\n---\n%s\n---want---\n%s\n---", errs, res, expected) } diff --git a/wasm/wasm_main.go b/wasm/wasm_main.go index 0c449c05..3a831749 100644 --- a/wasm/wasm_main.go +++ b/wasm/wasm_main.go @@ -8,6 +8,7 @@ Web assembly main for grol, exposing grol (repl.EvalString for now) to JS package main import ( + "context" "runtime" "runtime/debug" "strings" @@ -53,7 +54,7 @@ func jsEval(this js.Value, args []js.Value) interface{} { // as not exactly responsive to PRs nor issues folks (everyone trying their best yet...). opts.MaxDepth = WasmMaxDepth opts.MaxDuration = WasmMaxDuration - res, errs, formatted := repl.EvalStringWithOption(opts, input) + res, errs, formatted := repl.EvalStringWithOption(context.Background(), opts, input) result := make(map[string]any) result["result"] = strings.TrimSuffix(res, "\n") // transfer errors to []any (!) From 28a71304775dfc745f73dde0f12f4b91073b6173 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Sat, 31 Aug 2024 15:50:04 -0700 Subject: [PATCH 2/4] Use the return value --- repl/repl.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/repl/repl.go b/repl/repl.go index ec60283d..a709cb9c 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -209,12 +209,13 @@ func extractHistoryNumber(input string) (int, bool) { return 0, false } -func Interactive(options Options) int { +func Interactive(options Options) int { //nolint:funlen // it's a repl, it's supposed to be long. options.NilAndErr = true s := eval.NewState() s.MaxDepth = options.MaxDepth s.MaxValueLen = options.MaxValueLen // 0 is unlimited so ok to copy as is. term, err := terminal.Open(context.Background()) + ctx := term.Context if err != nil { return log.FErrf("Error creating readline: %v", err) } @@ -254,7 +255,7 @@ func Interactive(options Options) int { } if errors.Is(err, terminal.ErrInterrupted) { log.Debugf("Interrupted error") - term.ResetInterrupts(context.Background()) + ctx, _ = term.ResetInterrupts(context.Background()) //nolint:fatcontext // we only get a new one after the previous one is done. continue } if err != nil { @@ -292,7 +293,7 @@ func Interactive(options Options) int { } // normal errors are already logged but not the panic recoveries // Note this is the only case that can get contNeeded (EOL instead of EOF mode) - contNeeded, _, _, formatted := EvalOne(term.Context, s, l, term.Out, options) + contNeeded, _, _, formatted := EvalOne(ctx, s, l, term.Out, options) if contNeeded { prev = l + "\n" term.SetPrompt(CONTINUATION) From 1b092b00705857be60a00e7c92bf9f350aaff37b Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Sat, 31 Aug 2024 16:05:28 -0700 Subject: [PATCH 3/4] Use release version --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 68750d39..10687b3d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( fortio.org/log v1.16.0 fortio.org/sets v1.2.0 fortio.org/struct2env v0.4.1 - fortio.org/terminal v0.8.0-pre1 + fortio.org/terminal v0.8.0 fortio.org/testscript v0.3.1 // only for tests fortio.org/version v1.0.4 github.com/rivo/uniseg v0.4.7 @@ -17,7 +17,7 @@ require ( fortio.org/term v0.23.0-fortio-6 // indirect github.com/kortschak/goroutine v1.1.2 // indirect golang.org/x/crypto/x509roots/fallback v0.0.0-20240806160748-b2d3a6a4b4d3 // indirect - golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/tools v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index 11d604c8..2fcdef1a 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ fortio.org/struct2env v0.4.1 h1:rJludAMO5eBvpWplWEQNqoVDFZr4RWMQX7RUapgZyc0= fortio.org/struct2env v0.4.1/go.mod h1:lENUe70UwA1zDUCX+8AsO663QCFqYaprk5lnPhjD410= fortio.org/term v0.23.0-fortio-6 h1:pKrUX0tKOxyEhkhLV50oJYucTVx94rzFrXc24lIuLvk= fortio.org/term v0.23.0-fortio-6/go.mod h1:7buBfn81wEJUGWiVjFNiUE/vxWs5FdM9c7PyZpZRS30= -fortio.org/terminal v0.8.0-pre1 h1:OBqmHpVYhmDXjFQasMg9I/weAnXYcdCf+9tabk+spW0= -fortio.org/terminal v0.8.0-pre1/go.mod h1:hRs4ZM5C06GyfLRD6JHD96yjJGgm1mXQH75Ze8CMLjw= +fortio.org/terminal v0.8.0 h1:NSaa/JQZ4lIK08QEfBExwuzNY/bXfkzZzpfuPB7MlEA= +fortio.org/terminal v0.8.0/go.mod h1:hRs4ZM5C06GyfLRD6JHD96yjJGgm1mXQH75Ze8CMLjw= fortio.org/testscript v0.3.1 h1:MmRO64AsmzaU1KlYMzAbotJIMKRGxD1XXssJnBRiMGQ= fortio.org/testscript v0.3.1/go.mod h1:7OJ+U4avooRNqc7p/VHKJadYgj9fA6+N0SbGU8FVWGs= fortio.org/version v1.0.4 h1:FWUMpJ+hVTNc4RhvvOJzb0xesrlRmG/a+D6bjbQ4+5U= @@ -22,8 +22,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= golang.org/x/crypto/x509roots/fallback v0.0.0-20240806160748-b2d3a6a4b4d3 h1:oWb21rU9Q9XrRwXLB7jHc1rbp6EiiimZZv5MLxpu4T0= golang.org/x/crypto/x509roots/fallback v0.0.0-20240806160748-b2d3a6a4b4d3/go.mod h1:kNa9WdvYnzFwC79zRpLRMJbdEFlhyM5RPFBBZp/wWH8= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= -golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= From b5271590e633059b26dd47b6e81f63108939de88 Mon Sep 17 00:00:00 2001 From: Laurent Demailly Date: Sat, 31 Aug 2024 16:21:42 -0700 Subject: [PATCH 4/4] non ai comment --- repl/repl.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repl/repl.go b/repl/repl.go index a709cb9c..e2db1048 100644 --- a/repl/repl.go +++ b/repl/repl.go @@ -209,7 +209,7 @@ func extractHistoryNumber(input string) (int, bool) { return 0, false } -func Interactive(options Options) int { //nolint:funlen // it's a repl, it's supposed to be long. +func Interactive(options Options) int { //nolint:funlen // we do have quite a few cases. options.NilAndErr = true s := eval.NewState() s.MaxDepth = options.MaxDepth