Skip to content

Commit

Permalink
Post-run emitter helper
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
jbouwman committed Sep 8, 2021
1 parent 0b9f1a5 commit 979b1bd
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 50 deletions.
136 changes: 135 additions & 1 deletion examples/adder/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down Expand Up @@ -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{
Expand Down
27 changes: 4 additions & 23 deletions examples/adder/local/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"fmt"
"os"

"github.com/ipfs/go-ipfs-cmds/examples/adder"
Expand All @@ -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())
}
33 changes: 27 additions & 6 deletions executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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:
Expand Down
23 changes: 3 additions & 20 deletions http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"net"
"net/http"
"net/url"
"os"
"strings"

"github.com/ipfs/go-ipfs-cmds"
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 979b1bd

Please sign in to comment.