From df916e805e56563ac1a418d688a3617f874acbf2 Mon Sep 17 00:00:00 2001 From: Megan Marsh Date: Thu, 6 Jun 2019 10:52:12 -0700 Subject: [PATCH] borrow wrappedreadline workarounds from terraform and implement a similar check for piped commands; this makes the cli experience much cleaner --- command/console.go | 51 ++++++- command/meta.go | 13 ++ go.mod | 1 + go.sum | 2 + helper/wrappedreadline/wrappedreadline.go | 125 ++++++++++++++++++ .../wrappedreadline/wrappedreadline_unix.go | 52 ++++++++ .../wrappedreadline_windows.go | 57 ++++++++ 7 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 helper/wrappedreadline/wrappedreadline.go create mode 100644 helper/wrappedreadline/wrappedreadline_unix.go create mode 100644 helper/wrappedreadline/wrappedreadline_windows.go diff --git a/command/console.go b/command/console.go index e48744b49..17f1fc1c3 100644 --- a/command/console.go +++ b/command/console.go @@ -1,11 +1,14 @@ package command import ( + "bufio" "errors" "fmt" "io" "strings" + "github.com/chzyer/readline" + "github.com/hashicorp/packer/helper/wrappedreadline" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template" "github.com/hashicorp/packer/template/interpolate" @@ -70,6 +73,11 @@ func (c *ConsoleCommand) Run(args []string) int { Core: core, } + // Determine if stdin is a pipe. If so, we evaluate directly. + if c.StdinPiped() { + return c.modePiped(session) + } + return c.modeInteractive(session) } @@ -105,12 +113,45 @@ func (*ConsoleCommand) AutocompleteFlags() complete.Flags { } } -func (c *ConsoleCommand) modeInteractive(session *REPLSession) int { +func (c *ConsoleCommand) modePiped(session *REPLSession) int { + var lastResult string + scanner := bufio.NewScanner(wrappedreadline.Stdin()) + for scanner.Scan() { + result, err := session.Handle(strings.TrimSpace(scanner.Text())) + if err != nil { + return 0 + } + // Store the last result + lastResult = result + } + + // Output the final result + c.Ui.Message(lastResult) + return 0 +} + +func (c *ConsoleCommand) modeInteractive(session *REPLSession) int { // Setup the UI so we can output directly to stdout + l, err := readline.NewEx(wrappedreadline.Override(&readline.Config{ + Prompt: "> ", + InterruptPrompt: "^C", + EOFPrompt: "exit", + HistorySearchFold: true, + })) + if err != nil { + c.Ui.Error(fmt.Sprintf( + "Error initializing console: %s", + err)) + return 1 + } for { // Read a line - line, err := c.Ui.Ask("> ") - if err == packer.ErrInterrupted { - break + line, err := l.Readline() + if err == readline.ErrInterrupt { + if len(line) == 0 { + break + } else { + continue + } } else if err == io.EOF { break } @@ -144,6 +185,8 @@ type REPLSession struct { // The return value is the output and the error to show. func (s *REPLSession) Handle(line string) (string, error) { switch { + case strings.TrimSpace(line) == "": + return "", nil case strings.TrimSpace(line) == "exit": return "", ErrSessionExit case strings.TrimSpace(line) == "help": diff --git a/command/meta.go b/command/meta.go index 8c7101347..b92116428 100644 --- a/command/meta.go +++ b/command/meta.go @@ -5,9 +5,11 @@ import ( "flag" "fmt" "io" + "os" kvflag "github.com/hashicorp/packer/helper/flag-kv" sliceflag "github.com/hashicorp/packer/helper/flag-slice" + "github.com/hashicorp/packer/helper/wrappedreadline" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template" ) @@ -140,3 +142,14 @@ func (m *Meta) ValidateFlags() error { // TODO return nil } + +// StdinPiped returns true if the input is piped. +func (m *Meta) StdinPiped() bool { + fi, err := wrappedreadline.Stdin().Stat() + if err != nil { + // If there is an error, let's just say its not piped + return false + } + + return fi.Mode()&os.ModeNamedPipe != 0 +} diff --git a/go.mod b/go.mod index 74e6a706d..8d35249dd 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/biogo/hts v0.0.0-20160420073057-50da7d4131a3 github.com/c2h5oh/datasize v0.0.0-20171227191756-4eba002a5eae github.com/cheggaaa/pb v1.0.27 + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e github.com/creack/goselect v0.1.0 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/digitalocean/godo v1.11.1 diff --git a/go.sum b/go.sum index a844f081d..776cc1a18 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/census-instrumentation/opencensus-proto v0.2.0 h1:LzQXZOgg4CQfE6bFvXG github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cheggaaa/pb v1.0.27 h1:wIkZHkNfC7R6GI5w7l/PdAdzXzlrbcI3p8OAlnkTsnc= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/goselect v0.1.0 h1:4QiXIhcpSQF50XGaBsFzesjwX/1qOY5bOveQPmN9CXY= diff --git a/helper/wrappedreadline/wrappedreadline.go b/helper/wrappedreadline/wrappedreadline.go new file mode 100644 index 000000000..3bf7db0a0 --- /dev/null +++ b/helper/wrappedreadline/wrappedreadline.go @@ -0,0 +1,125 @@ +// Shamelessly copied from the Terraform repo because it wasn't worth vendoring +// out two hundred lines of code so Packer could use it too. +// +// wrappedreadline is a package that has helpers for interacting with +// readline from a panicwrap executable. +// +// panicwrap overrides the standard file descriptors so that the child process +// no longer looks like a TTY. The helpers here access the extra file descriptors +// passed by panicwrap to fix that. +// +// panicwrap should be checked for with panicwrap.Wrapped before using this +// librar, since this library won't adapt if the binary is not wrapped. +package wrappedreadline + +import ( + "os" + "runtime" + + "github.com/chzyer/readline" + "github.com/mitchellh/panicwrap" +) + +// Override overrides the values in readline.Config that need to be +// set with wrapped values. +func Override(cfg *readline.Config) *readline.Config { + cfg.Stdin = Stdin() + cfg.Stdout = Stdout() + cfg.Stderr = Stderr() + + cfg.FuncGetWidth = TerminalWidth + cfg.FuncIsTerminal = IsTerminal + + rm := RawMode{StdinFd: int(Stdin().Fd())} + cfg.FuncMakeRaw = rm.Enter + cfg.FuncExitRaw = rm.Exit + + return cfg +} + +// IsTerminal determines if this process is attached to a TTY. +func IsTerminal() bool { + // Windows is always a terminal + if runtime.GOOS == "windows" { + return true + } + + // Same implementation as readline but with our custom fds + return readline.IsTerminal(int(Stdin().Fd())) && + (readline.IsTerminal(int(Stdout().Fd())) || + readline.IsTerminal(int(Stderr().Fd()))) +} + +// TerminalWidth gets the terminal width in characters. +func TerminalWidth() int { + if runtime.GOOS == "windows" { + return readline.GetScreenWidth() + } + + return getWidth() +} + +// RawMode is a helper for entering and exiting raw mode. +type RawMode struct { + StdinFd int + + state *readline.State +} + +func (r *RawMode) Enter() (err error) { + r.state, err = readline.MakeRaw(r.StdinFd) + return err +} + +func (r *RawMode) Exit() error { + if r.state == nil { + return nil + } + + return readline.Restore(r.StdinFd, r.state) +} + +// Package provides access to the standard OS streams +// (stdin, stdout, stderr) even if wrapped under panicwrap. +// Stdin returns the true stdin of the process. +func Stdin() *os.File { + stdin := os.Stdin + if panicwrap.Wrapped(nil) { + stdin = wrappedStdin + } + + return stdin +} + +// Stdout returns the true stdout of the process. +func Stdout() *os.File { + stdout := os.Stdout + if panicwrap.Wrapped(nil) { + stdout = wrappedStdout + } + + return stdout +} + +// Stderr returns the true stderr of the process. +func Stderr() *os.File { + stderr := os.Stderr + if panicwrap.Wrapped(nil) { + stderr = wrappedStderr + } + + return stderr +} + +// These are the wrapped standard streams. These are setup by the +// platform specific code in initPlatform. +var ( + wrappedStdin *os.File + wrappedStdout *os.File + wrappedStderr *os.File +) + +func init() { + // Initialize the platform-specific code + initPlatform() +} diff --git a/helper/wrappedreadline/wrappedreadline_unix.go b/helper/wrappedreadline/wrappedreadline_unix.go new file mode 100644 index 000000000..130f2987a --- /dev/null +++ b/helper/wrappedreadline/wrappedreadline_unix.go @@ -0,0 +1,52 @@ +// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd + +package wrappedreadline + +import ( + "os" + "syscall" + "unsafe" +) + +// getWidth impl for Unix +func getWidth() int { + stdoutFd := int(Stdout().Fd()) + stderrFd := int(Stderr().Fd()) + + w := getWidthFd(stdoutFd) + if w < 0 { + w = getWidthFd(stderrFd) + } + + return w +} + +type winsize struct { + Row uint16 + Col uint16 + Xpixel uint16 + Ypixel uint16 +} + +// get width of the terminal +func getWidthFd(stdoutFd int) int { + ws := &winsize{} + retCode, _, errno := syscall.Syscall(syscall.SYS_IOCTL, + uintptr(stdoutFd), + uintptr(syscall.TIOCGWINSZ), + uintptr(unsafe.Pointer(ws))) + + if int(retCode) == -1 { + _ = errno + return -1 + } + + return int(ws.Col) +} + +func initPlatform() { + // The standard streams are passed in via extra file descriptors. + wrappedStdin = os.NewFile(uintptr(3), "stdin") + wrappedStdout = os.NewFile(uintptr(4), "stdout") + wrappedStderr = os.NewFile(uintptr(5), "stderr") +} diff --git a/helper/wrappedreadline/wrappedreadline_windows.go b/helper/wrappedreadline/wrappedreadline_windows.go new file mode 100644 index 000000000..459479af9 --- /dev/null +++ b/helper/wrappedreadline/wrappedreadline_windows.go @@ -0,0 +1,57 @@ +// +build windows + +package wrappedreadline + +import ( + "log" + "os" + "syscall" +) + +// getWidth impl for other +func getWidth() int { + return 0 +} + +func initPlatform() { + wrappedStdin = openConsole("CONIN$", os.Stdin) + wrappedStdout = openConsole("CONOUT$", os.Stdout) + wrappedStderr = wrappedStdout +} + +// openConsole opens a console handle, using a backup if it fails. +// This is used to get the exact console handle instead of the redirected +// handles from panicwrap. +func openConsole(name string, backup *os.File) *os.File { + // Convert to UTF16 + path, err := syscall.UTF16PtrFromString(name) + if err != nil { + log.Printf("[ERROR] wrappedstreams: %s", err) + return backup + } + + // Determine the share mode + var shareMode uint32 + switch name { + case "CONIN$": + shareMode = syscall.FILE_SHARE_READ + case "CONOUT$": + shareMode = syscall.FILE_SHARE_WRITE + } + + // Get the file + h, err := syscall.CreateFile( + path, + syscall.GENERIC_READ|syscall.GENERIC_WRITE, + shareMode, + nil, + syscall.OPEN_EXISTING, + 0, 0) + if err != nil { + log.Printf("[ERROR] wrappedstreams: %s", err) + return backup + } + + // Create the Go file + return os.NewFile(uintptr(h), name) +}