Skip to content

Commit

Permalink
Print stderr and stdout separately
Browse files Browse the repository at this point in the history
Change so backends return a exec.Cmd
instead of actually running the command.

Refactor the calling to a setup-phase
where any errors abort the entire
run first (creating the body-temp file
if any).

Then either CallAsString and be done
with it, or run the returned command.

If running the command, embellish
somewhat an error message if
the deadline is exceeded or
if the backend returns an error.

Then the last phase is to remove
the temp-file (if any). If the
temp-file cannot be removed, print
an error.

Then print any backen-error
unless the context was cancelled.

Then print any output from the command
as separate streams to stderr or stdout.

Then unless context was cancelled or
dead line exceeded, echo
back the backend error-status. Else
return 1.

Test plan:
* Have a template with a [Body} section.
  Add an .env file and set TMPDIR=
  to some non-existent directory.
  Run go run cmd/ain/main.go template.ain
  and verify error on temp-file canot
  be created. Verify exit status 1.
* Verify -p with a standard template
  with a [Body] section
  still works for all backends.
  Compare the result with
  the previous version of ain
  and verify they are the same.
* Hack the backendInput.RemoveBodyTempFile
  and add some chars to the filename.
  Run go run cmd/ain/main.go.
  Verify an error on removal of
  temp-file on stderr.
* Have the same template as above
  and remove the temp-file
  hack. Set a high
  Timeout= and press ctrl+c.
  Verify no error is printed
  and that exit code 128+2.
* Set the timeout to a low
  value as above and have
  the backend time out.
  Verify that an error message
  on backend timed out
  and an exit status of 1.
* Cause some regular backend
  error (such as calling a
  non-existant host). Verify
  backend error and stdout
  relayed to the terminal
  and that the exit code is
  the same as the backend
  would give.
* Same setup as above
  but some good case, such
  as an existing host.
  Verify stderr is
  echoed back to the
  terminal and the exit code
  copied.
* Have a template with a
  valid [Host], curl
  as the backend and
  pass in the [BackendOption]
  -D /dev/stderr.
  Call go run cmd/ain/main.go
  template.ain 2> stderr.txt
  1> stdout.txt. Verify
  stderr now copies the
  stderr stream of the backend
  binary, and that stdout
  does the same.
  • Loading branch information
jonaslu committed Mar 30, 2024
1 parent 1754be1 commit eb9fa6c
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 78 deletions.
41 changes: 32 additions & 9 deletions cmd/ain/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,28 +106,51 @@ Project home page: https://github.com/jonaslu/ain`
}

if fatal != "" {
// Is this valid?
checkSignalRaisedAndExit(assembledCtx, signalRaised)

fmt.Fprintln(os.Stderr, fatal)
os.Exit(1)
}

backendInput.PrintCommand = printCommand

call, err := call.Setup(backendInput)
if err != nil {
fmt.Fprint(os.Stderr, err.Error())
os.Exit(1)
}

if printCommand {
// Tempfile always left when calling as string
fmt.Fprint(os.Stdout, call.CallAsString())
return
}

backendInput.LeaveTempFile = leaveTmpFile
backendOutput, err := call.CallAsCmd(assembledCtx)

backendOutput, err := call.CallBackend(assembledCtx, backendInput)
teardownErr := call.Teardown()
if teardownErr != nil {
fmt.Fprint(os.Stderr, teardownErr.Error())
}

if err != nil {
fmt.Fprint(os.Stderr, err)
checkSignalRaisedAndExit(assembledCtx, signalRaised)
if err != nil && assembledCtx.Err() != context.Canceled {
fmt.Fprint(os.Stderr, err.Error())
}

var backendErr *call.BackedErr
if errors.As(err, &backendErr) {
os.Exit(backendErr.ExitCode)
}
if backendOutput != nil {
// It's customary to print stderr first
// to get the users attention on the error
fmt.Fprint(os.Stderr, backendOutput.Stderr)
fmt.Fprint(os.Stdout, backendOutput.Stdout)
}

checkSignalRaisedAndExit(assembledCtx, signalRaised)

if assembledCtx.Err() == context.DeadlineExceeded || teardownErr != nil {
os.Exit(1)
}

fmt.Fprint(os.Stdout, backendOutput)
os.Exit(backendOutput.ExitCode)
}
87 changes: 52 additions & 35 deletions internal/pkg/call/call.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
package call

import (
"bytes"
"context"
"fmt"
"strings"
"os/exec"

"github.com/jonaslu/ain/internal/pkg/data"
"github.com/jonaslu/ain/internal/pkg/utils"
"github.com/pkg/errors"
)

type BackedErr struct {
Err error
ExitCode int
}

type backendConstructor struct {
BinaryName string
constructor func(*data.BackendInput, string) backend
Expand All @@ -35,15 +29,17 @@ var ValidBackends = map[string]backendConstructor{
},
}

func (err *BackedErr) Error() string {
return fmt.Sprintf("Error: %v, exit code: %d\n", err.Err, err.ExitCode)
}

type backend interface {
runAsCmd(context.Context) ([]byte, error)
getAsCmd(context.Context) *exec.Cmd
getAsString() string
}

type BackendOutput struct {
Stderr string
Stdout string
ExitCode int
}

func getBackend(backendInput *data.BackendInput) (backend, error) {
requestedBackend := backendInput.Backend

Expand All @@ -62,44 +58,65 @@ func ValidBackend(backendName string) bool {
return false
}

func CallBackend(ctx context.Context, backendInput *data.BackendInput) (string, error) {
type Call struct {
backendInput *data.BackendInput
backend backend
forceRemoveTempFile bool
}

func Setup(backendInput *data.BackendInput) (*Call, error) {
call := Call{backendInput: backendInput}

backend, err := getBackend(backendInput)
if err != nil {
return "", errors.Wrapf(err, "Could not instantiate backend: %s", backendInput.Backend)
return nil, err
}

call.backend = backend

if err := backendInput.CreateBodyTempFile(); err != nil {
return "", err
return nil, err
}

if backendInput.PrintCommand {
return backend.getAsString(), nil
}
return &call, nil
}

func (c *Call) CallAsString() string {
return c.backend.getAsString()
}

func (c *Call) CallAsCmd(ctx context.Context) (*BackendOutput, error) {
backendCmd := c.backend.getAsCmd(ctx)

output, err := backend.runAsCmd(ctx)
removeTmpFileErr := backendInput.RemoveBodyTempFile(err != nil)
var stdout, stderr bytes.Buffer
backendCmd.Stdout = &stdout
backendCmd.Stderr = &stderr

if ctx.Err() == context.Canceled {
return "", removeTmpFileErr
err := backendCmd.Run()

c.forceRemoveTempFile = err != nil

backendOutput := &BackendOutput{
Stderr: stderr.String(),
Stdout: stdout.String(),
ExitCode: backendCmd.ProcessState.ExitCode(),
}

if ctx.Err() == context.DeadlineExceeded {
return "", utils.CascadeErrorMessage(
errors.Errorf("Backend-call: %s timed out after %d seconds", backendInput.Backend, ctx.Value(data.TimeoutContextValueKey{})),
removeTmpFileErr,
)
err = errors.Errorf("Backend-call: %s timed out after %d seconds",
c.backendInput.Backend,
ctx.Value(data.TimeoutContextValueKey{}))

return backendOutput, err
}

if err != nil {
return "", utils.CascadeErrorMessage(
errors.Wrapf(err, "Error running: %s\n%s", backendInput.Backend, strings.TrimSpace(string(output))),
removeTmpFileErr,
)
return backendOutput, errors.Wrapf(err, "Error running: %s", c.backendInput.Backend)
}

if removeTmpFileErr != nil {
return "", errors.Wrapf(removeTmpFileErr, "Error running: %s\n%s", backendInput.Backend, strings.TrimSpace(string(output)))
}
return backendOutput, nil
}

return string(output), nil
func (c *Call) Teardown() error {
return c.backendInput.RemoveBodyTempFile(c.forceRemoveTempFile)
}
17 changes: 4 additions & 13 deletions internal/pkg/call/curl.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (curl *curl) getBodyArgument() []string {
return []string{}
}

func (curl *curl) runAsCmd(ctx context.Context) ([]byte, error) {
func (curl *curl) getAsCmd(ctx context.Context) *exec.Cmd {
args := []string{}
for _, backendOpt := range curl.backendInput.BackendOptions {
args = append(args, backendOpt...)
Expand All @@ -70,16 +70,7 @@ func (curl *curl) runAsCmd(ctx context.Context) ([]byte, error) {
args = append(args, curl.getBodyArgument()...)
args = append(args, curl.backendInput.Host.String())

curlCmd := exec.CommandContext(ctx, curl.binaryName, args...)
output, err := curlCmd.CombinedOutput()
if err != nil {
return output, &BackedErr{
Err: err,
ExitCode: curlCmd.ProcessState.ExitCode(),
}
}

return output, err
return exec.CommandContext(ctx, curl.binaryName, args...)
}

func (curl *curl) getAsString() string {
Expand All @@ -101,7 +92,7 @@ func (curl *curl) getAsString() string {
utils.EscapeForShell(curl.backendInput.Host.String()),
})

output := curl.binaryName + " " + utils.PrettyPrintStringsForShell(args)
cmdAsString := curl.binaryName + " " + utils.PrettyPrintStringsForShell(args)

return output
return cmdAsString
}
13 changes: 2 additions & 11 deletions internal/pkg/call/httpie.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (httpie *httpie) getBodyArgument() []string {
return []string{}
}

func (httpie *httpie) runAsCmd(ctx context.Context) ([]byte, error) {
func (httpie *httpie) getAsCmd(ctx context.Context) *exec.Cmd {
args := []string{}
for _, backendOpt := range httpie.backendInput.BackendOptions {
args = append(args, backendOpt...)
Expand All @@ -66,16 +66,7 @@ func (httpie *httpie) runAsCmd(ctx context.Context) ([]byte, error) {
args = append(args, httpie.getBodyArgument()...)

httpCmd := exec.CommandContext(ctx, httpie.binaryName, args...)
output, err := httpCmd.CombinedOutput()

if err != nil {
return output, &BackedErr{
Err: err,
ExitCode: httpCmd.ProcessState.ExitCode(),
}
}

return output, nil
return httpCmd
}

func (httpie *httpie) getAsString() string {
Expand Down
12 changes: 2 additions & 10 deletions internal/pkg/call/wget.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func (wget *wget) getBodyArgument() []string {
return []string{}
}

func (wget *wget) runAsCmd(ctx context.Context) ([]byte, error) {
func (wget *wget) getAsCmd(ctx context.Context) *exec.Cmd {
args := []string{}
for _, backendOpt := range wget.backendInput.BackendOptions {
args = append(args, backendOpt...)
Expand All @@ -92,15 +92,7 @@ func (wget *wget) runAsCmd(ctx context.Context) ([]byte, error) {
args = append(args, wget.backendInput.Host.String())

wgetCmd := exec.CommandContext(ctx, wget.binaryName, args...)
output, err := wgetCmd.CombinedOutput()
if err != nil {
return output, &BackedErr{
Err: err,
ExitCode: wgetCmd.ProcessState.ExitCode(),
}
}

return output, nil
return wgetCmd
}

func (wget *wget) getAsString() string {
Expand Down

0 comments on commit eb9fa6c

Please sign in to comment.