cli: sequence cli.Ui operations
Fixes a bug where if a command flag parsing errors, the resulting error and help usage messages get interleaved in unexpected and non-user friendly way. The reason is that we have flag parsing library effectively writes to ui.Error in a goroutine. This is problematic: first, we lose the sequencing between help usage and error message; second, cli.Ui methods are not concurrent safe. Here, we introduce a custom error writer that buffers result and calls ui.Error() in the write method and in the same goroutine. For context, we need to wrap ui.Error because it's line-oriented, while flags library expects a io.Writer which is bytes oriented.
This commit is contained in:
parent
9b08968c2e
commit
76be9b4afb
|
@ -1,6 +1,7 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
|
@ -14,6 +15,7 @@ import (
|
|||
"github.com/hashicorp/nomad/api"
|
||||
"github.com/hashicorp/nomad/jobspec"
|
||||
"github.com/kr/text"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
|
||||
"github.com/ryanuber/columnize"
|
||||
|
@ -464,3 +466,44 @@ func sanitizeUUIDPrefix(prefix string) string {
|
|||
func commandErrorText(cmd NamedCommand) string {
|
||||
return fmt.Sprintf("For additional help try 'nomad %s -help'", cmd.Name())
|
||||
}
|
||||
|
||||
// uiErrorWriter is a io.Writer that wraps underlying ui.ErrorWriter().
|
||||
// ui.ErrorWriter expects full lines as inputs and it emits its own line breaks.
|
||||
//
|
||||
// uiErrorWriter scans input for individual lines to pass to ui.ErrorWriter. If data
|
||||
// doesn't contain a new line, it buffers result until next new line or writer is closed.
|
||||
type uiErrorWriter struct {
|
||||
ui cli.Ui
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *uiErrorWriter) Write(data []byte) (int, error) {
|
||||
read := 0
|
||||
for len(data) != 0 {
|
||||
a, token, err := bufio.ScanLines(data, false)
|
||||
if err != nil {
|
||||
return read, err
|
||||
}
|
||||
|
||||
if a == 0 {
|
||||
r, err := w.buf.Write(data)
|
||||
return read + r, err
|
||||
}
|
||||
|
||||
w.ui.Error(w.buf.String() + string(token))
|
||||
data = data[a:]
|
||||
w.buf.Reset()
|
||||
read += a
|
||||
}
|
||||
|
||||
return read, nil
|
||||
}
|
||||
|
||||
func (w *uiErrorWriter) Close() error {
|
||||
// emit what's remaining
|
||||
if w.buf.Len() != 0 {
|
||||
w.ui.Error(w.buf.String())
|
||||
w.buf.Reset()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"github.com/hashicorp/nomad/helper/flatmap"
|
||||
"github.com/kr/pretty"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHelpers_FormatKV(t *testing.T) {
|
||||
|
@ -342,3 +344,51 @@ func TestPrettyTimeDiff(t *testing.T) {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
// TestUiErrorWriter asserts that writer buffers and
|
||||
func TestUiErrorWriter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var outBuf, errBuf bytes.Buffer
|
||||
ui := &cli.BasicUi{
|
||||
Writer: &outBuf,
|
||||
ErrorWriter: &errBuf,
|
||||
}
|
||||
|
||||
w := &uiErrorWriter{ui: ui}
|
||||
|
||||
inputs := []string{
|
||||
"some line\n",
|
||||
"multiple\nlines\r\nhere",
|
||||
" with followup\nand",
|
||||
" more lines ",
|
||||
" without new line ",
|
||||
"until here\nand then",
|
||||
"some more",
|
||||
}
|
||||
|
||||
partialAcc := ""
|
||||
for _, in := range inputs {
|
||||
n, err := w.Write([]byte(in))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, len(in), n)
|
||||
|
||||
// assert that writer emits partial result until last new line
|
||||
partialAcc += strings.ReplaceAll(in, "\r\n", "\n")
|
||||
lastNL := strings.LastIndex(partialAcc, "\n")
|
||||
require.Equal(t, partialAcc[:lastNL+1], errBuf.String())
|
||||
}
|
||||
|
||||
require.Empty(t, outBuf.String())
|
||||
|
||||
// note that the \r\n got replaced by \n
|
||||
expectedErr := "some line\nmultiple\nlines\nhere with followup\nand more lines without new line until here\n"
|
||||
require.Equal(t, expectedErr, errBuf.String())
|
||||
|
||||
// close emits the final line
|
||||
err := w.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedErr += "and thensome more\n"
|
||||
require.Equal(t, expectedErr, errBuf.String())
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
|
@ -83,18 +81,7 @@ func (m *Meta) FlagSet(n string, fs FlagSetFlags) *flag.FlagSet {
|
|||
|
||||
}
|
||||
|
||||
// Create an io.Writer that writes to our UI properly for errors.
|
||||
// This is kind of a hack, but it does the job. Basically: create
|
||||
// a pipe, use a scanner to break it into lines, and output each line
|
||||
// to the UI. Do this forever.
|
||||
errR, errW := io.Pipe()
|
||||
errScanner := bufio.NewScanner(errR)
|
||||
go func() {
|
||||
for errScanner.Scan() {
|
||||
m.Ui.Error(errScanner.Text())
|
||||
}
|
||||
}()
|
||||
f.SetOutput(errW)
|
||||
f.SetOutput(&uiErrorWriter{ui: m.Ui})
|
||||
|
||||
return f
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue