From fd2fef87386228aa9b6541c99dd6332a2d5511e7 Mon Sep 17 00:00:00 2001 From: Ali Rizvi-Santiago Date: Tue, 7 Aug 2018 15:31:52 -0500 Subject: [PATCH] Added support for the progress bar to automatically determine its width using the minimum length from a packer.UI and the terminal dimensions via kernel32.GetConsoleScreenBufferInfo or an ioctl (TIOCGWINSZ) to "/dev/tty". --- common/progress.go | 45 ++++++++++++++- common/progress_posix.go | 32 +++++++++++ common/progress_windows.go | 82 ++++++++++++++++++++++++++++ common/step_download.go | 9 +-- packer/rpc/ui.go | 13 +++++ packer/rpc/ui_test.go | 38 +++++++++---- packer/ui.go | 19 +++++++ provisioner/ansible-local/ui_stub.go | 3 + provisioner/ansible/adapter_test.go | 4 ++ provisioner/ansible/provisioner.go | 4 ++ provisioner/file/provisioner.go | 25 +++++++-- provisioner/file/provisioner_test.go | 4 ++ 12 files changed, 255 insertions(+), 23 deletions(-) create mode 100644 common/progress_posix.go create mode 100644 common/progress_windows.go diff --git a/common/progress.go b/common/progress.go index c90810c9d..476eff55d 100644 --- a/common/progress.go +++ b/common/progress.go @@ -1,9 +1,15 @@ package common -import "github.com/cheggaaa/pb" +import ( + "fmt" + "github.com/cheggaaa/pb" + "github.com/hashicorp/packer/packer" + "log" + "time" +) // Default progress bar appearance -func GetDefaultProgressBar() pb.ProgressBar { +func GetNewProgressBar(ui *packer.Ui) pb.ProgressBar { bar := pb.New64(0) bar.ShowPercent = true bar.ShowCounters = true @@ -16,5 +22,40 @@ func GetDefaultProgressBar() pb.ProgressBar { bar.SetRefreshRate(1 * time.Second) bar.SetWidth(80) + // If there's no UI set, then the progress bar doesn't need anything else + if ui == nil { + return *bar + } + UI := *ui + + // Now check the UI's width to adjust the progress bar + uiWidth := UI.GetMinimumLength() + len("\n") + + // If the UI's width is signed, then this interface doesn't really + // benefit from a progress bar + if uiWidth < 0 { + log.Println("Refusing to render progress-bar for unsupported UI.") + return *bar + } + bar.Callback = UI.Message + + // Figure out the terminal width if possible + width, _, err := GetTerminalDimensions() + if err != nil { + newerr := fmt.Errorf("Unable to determine terminal dimensions: %v", err) + log.Printf("Using default width (%d) for progress-bar due to error: %s", bar.GetWidth(), newerr) + return *bar + } + + // Adjust the progress bar's width according to the terminal size + // and whatever width is returned from the UI + if width > uiWidth { + width -= uiWidth + bar.SetWidth(width) + } else { + newerr := fmt.Errorf("Terminal width (%d) is smaller than UI message width (%d).", width, uiWidth) + log.Printf("Using default width (%d) for progress-bar due to error: %s", bar.GetWidth(), newerr) + } + return *bar } diff --git a/common/progress_posix.go b/common/progress_posix.go new file mode 100644 index 000000000..317307635 --- /dev/null +++ b/common/progress_posix.go @@ -0,0 +1,32 @@ +// +build !windows + +package common + +// Imports for determining terminal information across platforms +import ( + "golang.org/x/sys/unix" + "os" +) + +// posix api +func GetTerminalDimensions() (width, height int, err error) { + + // open up a handle to the current tty + tty, err := os.Open("/dev/tty") + if err != nil { + return 0, 0, err + } + defer tty.Close() + + // convert the handle into a file descriptor + fd := int(tty.Fd()) + + // use it to make an Ioctl + ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) + if err != nil { + return 0, 0, err + } + + // return the width and height + return int(ws.Col), int(ws.Row), nil +} diff --git a/common/progress_windows.go b/common/progress_windows.go new file mode 100644 index 000000000..676ab538d --- /dev/null +++ b/common/progress_windows.go @@ -0,0 +1,82 @@ +// +build windows + +package common + +import ( + "syscall" + "unsafe" +) + +// windows constants and structures pulled from msdn +const ( + STD_INPUT_HANDLE = -10 + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 +) + +type ( + SHORT int16 + WORD uint16 + + SMALL_RECT struct { + Left, Top, Right, Bottom SHORT + } + COORD struct { + X, Y SHORT + } + CONSOLE_SCREEN_BUFFER_INFO struct { + dwSize, dwCursorPosition COORD + wAttributes WORD + srWindow SMALL_RECT + dwMaximumWindowSize COORD + } +) + +// Low-level functions that call into Windows API for getting console info +var KERNEL32 = syscall.NewLazyDLL("kernel32.dll") +var KERNEL32_GetStdHandleProc = KERNEL32.NewProc("GetStdHandle") +var KERNEL32_GetConsoleScreenBufferInfoProc = KERNEL32.NewProc("GetConsoleScreenBufferInfo") + +func KERNEL32_GetStdHandle(nStdHandle int32) (syscall.Handle, error) { + res, _, err := KERNEL32_GetStdHandleProc.Call(uintptr(nStdHandle)) + if res == uintptr(syscall.InvalidHandle) { + return syscall.InvalidHandle, error(err) + } + return syscall.Handle(res), nil +} + +func KERNEL32_GetConsoleScreenBufferInfo(hConsoleOutput syscall.Handle, info *CONSOLE_SCREEN_BUFFER_INFO) error { + ok, _, err := KERNEL32_GetConsoleScreenBufferInfoProc.Call(uintptr(hConsoleOutput), uintptr(unsafe.Pointer(info))) + if int(ok) == 0 { + return error(err) + } + return nil +} + +// windows api +func GetTerminalDimensions() (width, height int, err error) { + var ( + fd syscall.Handle + csbi CONSOLE_SCREEN_BUFFER_INFO + ) + + // grab the handle for stdout + /* + if fd, err = KERNEL32_GetStdHandle(STD_OUTPUT_HANDLE); err != nil { + return 0, 0, err + } + */ + + if fd, err = syscall.Open("CONOUT$", syscall.O_RDWR, 0); err != nil { + return 0, 0, err + } + defer syscall.Close(fd) + + // grab the dimensions for the console + if err = KERNEL32_GetConsoleScreenBufferInfo(fd, &csbi); err != nil { + return 0, 0, err + } + + // whee... + return int(csbi.dwSize.X), int(csbi.dwSize.Y), nil +} diff --git a/common/step_download.go b/common/step_download.go index c920cf5d2..efd4607a7 100644 --- a/common/step_download.go +++ b/common/step_download.go @@ -8,7 +8,6 @@ import ( "log" "time" - "github.com/cheggaaa/pb" "github.com/hashicorp/packer/helper/multistep" "github.com/hashicorp/packer/helper/useragent" "github.com/hashicorp/packer/packer" @@ -65,8 +64,7 @@ func (s *StepDownload) Run(_ context.Context, state multistep.StateBag) multiste ui.Say(fmt.Sprintf("Retrieving %s", s.Description)) // Get a default-looking progress bar and connect it to the ui. - bar := GetDefaultProgressBar() - bar.Callback = ui.Message + bar := GetNewProgressBar(&ui) // First try to use any already downloaded file // If it fails, proceed to regular download logic @@ -145,9 +143,8 @@ func (s *StepDownload) download(config *DownloadConfig, state multistep.StateBag var path string ui := state.Get("ui").(packer.Ui) - // Get a default looking progress bar and connect it to the ui. - bar := GetDefaultProgressBar() - bar.Callback = ui.Message + // Get a default-looking progress bar and connect it to the ui. + bar := GetNewProgressBar(&ui) // Create download client with config and progress bar download := NewDownloadClient(config, bar) diff --git a/packer/rpc/ui.go b/packer/rpc/ui.go index 1c6356a65..19553cc30 100644 --- a/packer/rpc/ui.go +++ b/packer/rpc/ui.go @@ -60,6 +60,13 @@ func (u *Ui) Say(message string) { } } +func (u *Ui) GetMinimumLength() (result int) { + if err := u.client.Call("Ui.GetMinimumLength", nil, &result); err != nil { + log.Printf("Error in Ui RPC call: %s", err) + } + return +} + func (u *UiServer) Ask(query string, reply *string) (err error) { *reply, err = u.ui.Ask(query) return @@ -91,3 +98,9 @@ func (u *UiServer) Say(message *string, reply *interface{}) error { *reply = nil return nil } + +func (u *UiServer) GetMinimumLength(noargs *interface{}, reply *int) error { + res := u.ui.GetMinimumLength() + *reply = res + return nil +} diff --git a/packer/rpc/ui_test.go b/packer/rpc/ui_test.go index 4dd2d7036..42f9d6ae0 100644 --- a/packer/rpc/ui_test.go +++ b/packer/rpc/ui_test.go @@ -6,17 +6,19 @@ import ( ) type testUi struct { - askCalled bool - askQuery string - errorCalled bool - errorMessage string - machineCalled bool - machineType string - machineArgs []string - messageCalled bool - messageMessage string - sayCalled bool - sayMessage string + askCalled bool + askQuery string + errorCalled bool + errorMessage string + machineCalled bool + machineType string + machineArgs []string + messageCalled bool + messageMessage string + sayCalled bool + sayMessage string + getMinimumLengthCalled bool + getMinimumLengthValue int } func (u *testUi) Ask(query string) (string, error) { @@ -46,6 +48,12 @@ func (u *testUi) Say(message string) { u.sayMessage = message } +func (u *testUi) GetMinimumLength() int { + u.getMinimumLengthCalled = true + u.getMinimumLengthValue = -1 + return u.getMinimumLengthValue +} + func TestUiRPC(t *testing.T) { // Create the UI to test ui := new(testUi) @@ -93,6 +101,14 @@ func TestUiRPC(t *testing.T) { t.Fatal("machine should be called") } + uiClient.GetMinimumLength() + if !ui.getMinimumLengthCalled { + t.Fatal("getminimumlength should be called") + } + if ui.getMinimumLengthValue != -1 { + t.Fatal("getminimumlength should be -1") + } + if ui.machineType != "foo" { t.Fatalf("bad type: %#v", ui.machineType) } diff --git a/packer/ui.go b/packer/ui.go index c16a2eae0..73b07ea0e 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -37,6 +37,7 @@ type Ui interface { Message(string) Error(string) Machine(string, ...string) + GetMinimumLength() int } // ColoredUi is a UI that is colored using terminal colors. @@ -132,6 +133,10 @@ func (u *ColoredUi) supportsColors() bool { return cygwin } +func (u *ColoredUi) GetMinimumLength() int { + return u.Ui.GetMinimumLength() +} + func (u *TargetedUI) Ask(query string) (string, error) { return u.Ui.Ask(u.prefixLines(true, query)) } @@ -168,6 +173,12 @@ func (u *TargetedUI) prefixLines(arrow bool, message string) string { return strings.TrimRightFunc(result.String(), unicode.IsSpace) } +func (u *TargetedUI) GetMinimumLength() int { + var dummy string + dummy = u.prefixLines(false, "") + return len(dummy) + len("\n") +} + func (rw *BasicUi) Ask(query string) (string, error) { rw.l.Lock() defer rw.l.Unlock() @@ -260,6 +271,10 @@ func (rw *BasicUi) Machine(t string, args ...string) { log.Printf("machine readable: %s %#v", t, args) } +func (u *BasicUi) GetMinimumLength() int { + return 0 +} + func (u *MachineReadableUi) Ask(query string) (string, error) { return "", errors.New("machine-readable UI can't ask") } @@ -305,3 +320,7 @@ func (u *MachineReadableUi) Machine(category string, args ...string) { } } } + +func (u *MachineReadableUi) GetMinimumLength() int { + return -1 +} diff --git a/provisioner/ansible-local/ui_stub.go b/provisioner/ansible-local/ui_stub.go index 4faa2a215..86d3cba86 100644 --- a/provisioner/ansible-local/ui_stub.go +++ b/provisioner/ansible-local/ui_stub.go @@ -13,3 +13,6 @@ func (su *uiStub) Machine(string, ...string) {} func (su *uiStub) Message(string) {} func (su *uiStub) Say(msg string) {} +func (su *uiStub) GetMinimumLength() int { + return -1 +} diff --git a/provisioner/ansible/adapter_test.go b/provisioner/ansible/adapter_test.go index 2ab2d350b..f8be403f5 100644 --- a/provisioner/ansible/adapter_test.go +++ b/provisioner/ansible/adapter_test.go @@ -123,6 +123,10 @@ func (u *ui) Machine(s1 string, s2 ...string) { } } +func (u *ui) GetMinimumLength() int { + return -1 +} + type communicator struct{} func (c communicator) Start(*packer.RemoteCmd) error { diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go index 99c4d07d9..7b32e81ed 100644 --- a/provisioner/ansible/provisioner.go +++ b/provisioner/ansible/provisioner.go @@ -599,3 +599,7 @@ func (ui *Ui) Machine(t string, args ...string) { ui.ui.Machine(t, args...) <-ui.sem } + +func (ui *Ui) GetMinimumLength() int { + return -1 +} diff --git a/provisioner/file/provisioner.go b/provisioner/file/provisioner.go index a0e8d2465..d734eaf06 100644 --- a/provisioner/file/provisioner.go +++ b/provisioner/file/provisioner.go @@ -3,6 +3,7 @@ package file import ( "errors" "fmt" + "io" "os" "path/filepath" "strings" @@ -125,8 +126,16 @@ func (p *Provisioner) ProvisionDownload(ui packer.Ui, comm packer.Communicator) } defer f.Close() - err = comm.Download(src, f) - if err != nil { + // Get a default progress bar + pb := common.GetNewProgressBar(&ui) + bar := pb.Start() + defer bar.Finish() + + // Create MultiWriter for the current progress + pf := io.MultiWriter(f, bar) + + // Download the file + if err = comm.Download(src, pf); err != nil { ui.Error(fmt.Sprintf("Download failed: %s", err)) return err } @@ -166,8 +175,16 @@ func (p *Provisioner) ProvisionUpload(ui packer.Ui, comm packer.Communicator) er dst = filepath.Join(dst, filepath.Base(src)) } - err = comm.Upload(dst, f, &fi) - if err != nil { + // Get a default progress bar + pb := common.GetNewProgressBar(&ui) + bar := pb.Start() + defer bar.Finish() + + // Create ProxyReader for the current progress + pf := bar.NewProxyReader(f) + + // Upload the file + if err = comm.Upload(dst, pf, &fi); err != nil { ui.Error(fmt.Sprintf("Upload failed: %s", err)) return err } diff --git a/provisioner/file/provisioner_test.go b/provisioner/file/provisioner_test.go index 175082aae..6c749f1e5 100644 --- a/provisioner/file/provisioner_test.go +++ b/provisioner/file/provisioner_test.go @@ -120,6 +120,10 @@ func (su *stubUi) Say(msg string) { su.sayMessages += msg } +func (su *stubUi) GetMinimumLength() int { + return -1 +} + func TestProvisionerProvision_SendsFile(t *testing.T) { var p Provisioner tf, err := ioutil.TempFile("", "packer")