From 979b1bd1447d4d6cd6574ede37b42197f8389240 Mon Sep 17 00:00:00 2001 From: Jesse Bouwman Date: Wed, 8 Sep 2021 11:53:16 -0700 Subject: [PATCH] Post-run emitter helper - When both PostRun (output transformer) and DisplayCLI (terminal output) are present, ensure that both are run. `EmitResponse` helper in executor.go is invoked by both local and HTTP client executors: the client fibs the command.Run interface via anonymous function. --- examples/adder/cmd.go | 136 ++++++++++++++++++++++++++++++++++- examples/adder/local/main.go | 27 ++----- executor.go | 33 +++++++-- http/client.go | 23 +----- 4 files changed, 169 insertions(+), 50 deletions(-) diff --git a/examples/adder/cmd.go b/examples/adder/cmd.go index 2c859211..de2c1373 100644 --- a/examples/adder/cmd.go +++ b/examples/adder/cmd.go @@ -93,7 +93,7 @@ var RootCmd = &cmds.Command{ }), }, }, - // the best UX + // using stdio via PostRun "postRunAdd": &cmds.Command{ Arguments: []cmds.Argument{ cmds.StringArg("summands", true, true, "values that are supposed to be summed"), @@ -151,6 +151,140 @@ var RootCmd = &cmds.Command{ }, }, }, + // DisplayCLI for terminal control + "displayCliAdd": &cmds.Command{ + Arguments: []cmds.Argument{ + cmds.StringArg("summands", true, true, "values that are supposed to be summed"), + }, + // this is the same as for encoderAdd + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + sum := 0 + + for i, str := range req.Arguments { + num, err := strconv.Atoi(str) + if err != nil { + return err + } + + sum += num + err = re.Emit(&AddStatus{ + Current: sum, + Left: len(req.Arguments) - i - 1, + }) + if err != nil { + return err + } + + time.Sleep(200 * time.Millisecond) + } + return nil + }, + Type: &AddStatus{}, + DisplayCLI: func(res cmds.Response, stdout, stderr io.Writer) error { + defer fmt.Fprintln(stdout) + + // length of line at last iteration + var lastLen int + + for { + v, err := res.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + fmt.Fprint(stdout, "\r" + strings.Repeat(" ", lastLen)) + + s := v.(*AddStatus) + if s.Left > 0 { + lastLen, _ = fmt.Fprintf(stdout, "\rcalculation sum... current: %d; left: %d", s.Current, s.Left) + } else { + lastLen, _ = fmt.Fprintf(stdout, "\rsum is %d.", s.Current) + } + } + }, + }, + // PostRun and DisplayCLI: PostRun intercepts and doubles the sum + "defectiveAdd": &cmds.Command{ + Arguments: []cmds.Argument{ + cmds.StringArg("summands", true, true, "values that are supposed to be summed"), + }, + // this is the same as for encoderAdd + Run: func(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment) error { + sum := 0 + + for i, str := range req.Arguments { + num, err := strconv.Atoi(str) + if err != nil { + return err + } + + sum += num + err = re.Emit(&AddStatus{ + Current: sum, + Left: len(req.Arguments) - i - 1, + }) + if err != nil { + return err + } + + time.Sleep(200 * time.Millisecond) + } + return nil + }, + Type: &AddStatus{}, + PostRun: cmds.PostRunMap{ + cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error { + defer re.Close() + + for { + v, err := res.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + s := v.(*AddStatus) + err = re.Emit(&AddStatus{ + Current: s.Current + s.Current, + Left: s.Left, + }) + if err != nil { + return err + } + } + }, + }, + DisplayCLI: func(res cmds.Response, stdout, stderr io.Writer) error { + defer fmt.Fprintln(stdout) + + // length of line at last iteration + var lastLen int + + for { + v, err := res.Next() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + fmt.Fprint(stdout, "\r" + strings.Repeat(" ", lastLen)) + + s := v.(*AddStatus) + if s.Left > 0 { + lastLen, _ = fmt.Fprintf(stdout, "\rcalculation sum... current: %d; left: %d", s.Current, s.Left) + } else { + lastLen, _ = fmt.Fprintf(stdout, "\rsum is %d.", s.Current) + } + } + }, + }, // how to set program's return value "exitAdd": &cmds.Command{ Arguments: []cmds.Argument{ diff --git a/examples/adder/local/main.go b/examples/adder/local/main.go index 8e7eeafe..9be5595d 100644 --- a/examples/adder/local/main.go +++ b/examples/adder/local/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "os" "github.com/ipfs/go-ipfs-cmds/examples/adder" @@ -26,29 +25,11 @@ func main() { panic(err) } - wait := make(chan struct{}) - var re cmds.ResponseEmitter = cliRe - if pr, ok := req.Command.PostRun[cmds.CLI]; ok { - var ( - res cmds.Response - lower = re - ) - - re, res = cmds.NewChanResponsePair(req) - - go func() { - defer close(wait) - err := pr(res, lower) - if err != nil { - fmt.Println("error: ", err) - } - }() - } else { - close(wait) + exec := cmds.NewExecutor(adder.RootCmd) + err = exec.Execute(req, cliRe, nil) + if err != nil { + panic(err) } - adder.RootCmd.Call(req, re, nil) - <-wait - os.Exit(cliRe.Status()) } diff --git a/executor.go b/executor.go index 5304e5b9..f06ef33f 100644 --- a/executor.go +++ b/executor.go @@ -60,21 +60,38 @@ func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) er } } + return EmitResponse(cmd.Run, req, re, env) +} + +// Helper for Execute that handles post-Run emitter logic +func EmitResponse(run Function, req *Request, re ResponseEmitter, env Environment) error { + + // Keep track of the lowest emitter to select the correct + // PostRun method. + lowest := re + cmd := req.Command + // contains the error returned by DisplayCLI or PostRun errCh := make(chan error, 1) + if cmd.DisplayCLI != nil && GetEncoding(req, "json") == "text" { - var ( - res Response - ) + var res Response + // This overwrites the emitter provided as an + // argument. Maybe it's better to provide the + // 'DisplayCLI emitter' as an argument to Execute. re, res = NewChanResponsePair(req) go func() { defer close(errCh) errCh <- cmd.DisplayCLI(res, os.Stdout, os.Stderr) }() - } else if cmd.PostRun != nil { - if typer, ok := re.(interface { + } + + // 're' might now be the DisplayCLI version. see if we need + // to wrap *that* in PostRun. + if cmd.PostRun != nil { + if typer, ok := lowest.(interface { Type() PostRunType }); ok && cmd.PostRun[typer.Type()] != nil { var ( @@ -94,7 +111,11 @@ func (x *executor) Execute(req *Request, re ResponseEmitter, env Environment) er close(errCh) } - runCloseErr := re.CloseWithError(cmd.Run(req, re, env)) + // by now the complete emitter is present: run the request + // + // (might just be copying a response from a server, if we're + // running in a client) + runCloseErr := re.CloseWithError(run(req, re, env)) postCloseErr := <-errCh switch runCloseErr { case ErrClosingClosedEmitter, nil: diff --git a/http/client.go b/http/client.go index 0a0589e4..0a4a3e5a 100644 --- a/http/client.go +++ b/http/client.go @@ -7,7 +7,6 @@ import ( "net" "net/http" "net/url" - "os" "strings" "github.com/ipfs/go-ipfs-cmds" @@ -118,27 +117,11 @@ func (c *client) Execute(req *cmds.Request, re cmds.ResponseEmitter, env cmds.En return err } - if cmd.PostRun != nil { - if typer, ok := re.(interface { - Type() cmds.PostRunType - }); ok && cmd.PostRun[typer.Type()] != nil { - err := cmd.PostRun[typer.Type()](res, re) - closeErr := re.CloseWithError(err) - if closeErr == cmds.ErrClosingClosedEmitter { - // ignore double close errors - return nil - } - - return closeErr - } - } - - if cmd.DisplayCLI != nil && - cmds.GetEncoding(req, cmds.Undefined) == cmds.Text { - return cmd.DisplayCLI(res, os.Stdout, os.Stderr) + copy := func(_ *cmds.Request, re cmds.ResponseEmitter, _ cmds.Environment) error { + return cmds.Copy(re, res) } - return cmds.Copy(re, res) + return cmds.EmitResponse(copy, req, re, env) } func (c *client) toHTTPRequest(req *cmds.Request) (*http.Request, error) {