mirror of https://github.com/hashicorp/packer
Moved the progress bar out of packer.Ui and unlinked it out of all the packer.Ui implementations. Split up the terminal-related functions into a separate terminal.go and calculate the progress bar width by traversing through packer.Ui to avoid the issue with github.com/ugorji/go/codec serializing private members (or unsafe pointers) of structs. Shuffled some arguments around in getConsoleScreenBufferInfo in common/terminal_windows.go so that the interface forces the user to correctly declare a _CONSOLE_SCREEN_BUFFER_INFO type.
parent
dc2088318e
commit
5726927cba
@ -0,0 +1,118 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The ProgressBar interface is used for abstracting cheggaaa's progress-
|
||||
// bar, or any other progress bar. If a UI does not support a progress-
|
||||
// bar, then it must return a null progress bar.
|
||||
const (
|
||||
DefaultProgressBarWidth = 80
|
||||
)
|
||||
|
||||
type ProgressBar = *pb.ProgressBar
|
||||
|
||||
// Figure out the terminal dimensions and use it to calculate the available rendering space
|
||||
func calculateProgressBarWidth(length int) int {
|
||||
// If the UI's width is signed, then this is an interface that doesn't really benefit from a progress bar
|
||||
if length < 0 {
|
||||
log.Println("Refusing to render progress-bar for unsupported UI.")
|
||||
return length
|
||||
}
|
||||
|
||||
// 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", DefaultProgressBarWidth, newerr)
|
||||
return DefaultProgressBarWidth
|
||||
}
|
||||
|
||||
// If the terminal width is smaller than the requested length, then complain
|
||||
if width < length {
|
||||
newerr := fmt.Errorf("Terminal width (%d) is smaller than UI message width (%d).", width, length)
|
||||
log.Printf("Using default width (%d) for progress-bar due to error: %s", DefaultProgressBarWidth, newerr)
|
||||
return DefaultProgressBarWidth
|
||||
}
|
||||
|
||||
// Otherwise subtract the minimum length and return it
|
||||
return width - length
|
||||
}
|
||||
|
||||
// Get a progress bar with the default appearance
|
||||
func GetDefaultProgressBar() ProgressBar {
|
||||
bar := pb.New64(0)
|
||||
bar.ShowPercent = true
|
||||
bar.ShowCounters = true
|
||||
bar.ShowSpeed = false
|
||||
bar.ShowBar = true
|
||||
bar.ShowTimeLeft = false
|
||||
bar.ShowFinalTime = false
|
||||
bar.SetUnits(pb.U_BYTES)
|
||||
bar.Format("[=>-]")
|
||||
bar.SetRefreshRate(1 * time.Second)
|
||||
return bar
|
||||
}
|
||||
|
||||
// Return a dummy progress bar that doesn't do anything
|
||||
func GetDummyProgressBar() ProgressBar {
|
||||
bar := pb.New64(0)
|
||||
bar.ManualUpdate = true
|
||||
return bar
|
||||
}
|
||||
|
||||
// Given a packer.Ui, calculate the number of characters that a packer.Ui will
|
||||
// prefix a message with. Then we can use this to calculate the progress bar's width.
|
||||
func calculateUiPrefixLength(ui packer.Ui) int {
|
||||
var recursiveCalculateUiPrefixLength func(packer.Ui, int) int
|
||||
|
||||
// Define a recursive closure that traverses through all the known packer.Ui types
|
||||
// and aggregates the length of the message prefix from each particular type
|
||||
recursiveCalculateUiPrefixLength = func(ui packer.Ui, agg int) int {
|
||||
switch ui.(type) {
|
||||
|
||||
case *packer.ColoredUi:
|
||||
// packer.ColoredUi is simply a wrapper around .Ui
|
||||
u := ui.(*packer.ColoredUi)
|
||||
return recursiveCalculateUiPrefixLength(u.Ui, agg)
|
||||
|
||||
case *packer.TargetedUI:
|
||||
// A TargetedUI added the .Target and an arrow by default
|
||||
const targetedArrowText = "==>"
|
||||
u := ui.(*packer.TargetedUI)
|
||||
res := fmt.Sprintf("%s %s: ", targetedArrowText, u.Target)
|
||||
return recursiveCalculateUiPrefixLength(u.Ui, agg+len(res))
|
||||
|
||||
case *packer.BasicUi:
|
||||
// The standard BasicUi appends only a newline
|
||||
return agg + len("\n")
|
||||
|
||||
case *packer.MachineReadableUi:
|
||||
// MachineReadableUi doesn't emit anything...like at all
|
||||
return 0
|
||||
}
|
||||
|
||||
log.Printf("Calculating the message prefix length for packer.Ui type (%T) is not implemented. Using the current aggregated length of %d.", ui, agg)
|
||||
return agg
|
||||
}
|
||||
return recursiveCalculateUiPrefixLength(ui, 0)
|
||||
}
|
||||
|
||||
func GetProgressBar(ui packer.Ui) ProgressBar {
|
||||
uiPrefixLength := calculateUiPrefixLength(ui)
|
||||
width := calculateProgressBarWidth(uiPrefixLength)
|
||||
|
||||
log.Printf("ProgressBar: Using progress bar width: %d\n", width)
|
||||
|
||||
bar := GetDefaultProgressBar()
|
||||
bar.SetWidth(width)
|
||||
bar.Callback = func(message string) {
|
||||
ui.Message(message)
|
||||
}
|
||||
return bar
|
||||
}
|
||||
@ -0,0 +1,142 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// test packer.Ui implementation to verify that progress bar is being written
|
||||
type testProgressBarUi struct {
|
||||
messageCalled bool
|
||||
messageMessage string
|
||||
}
|
||||
|
||||
func (u *testProgressBarUi) Say(string) {}
|
||||
func (u *testProgressBarUi) Error(string) {}
|
||||
func (u *testProgressBarUi) Machine(string, ...string) {}
|
||||
|
||||
func (u *testProgressBarUi) Ask(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (u *testProgressBarUi) Message(message string) {
|
||||
u.messageCalled = true
|
||||
u.messageMessage = message
|
||||
}
|
||||
|
||||
// ..and now let's begin our actual tests
|
||||
func TestCalculateUiPrefixLength_Unknown(t *testing.T) {
|
||||
ui := &testProgressBarUi{}
|
||||
|
||||
expected := 0
|
||||
if res := calculateUiPrefixLength(ui); res != expected {
|
||||
t.Fatalf("calculateUiPrefixLength should have returned a length of %d", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateUiPrefixLength_BasicUi(t *testing.T) {
|
||||
ui := &packer.BasicUi{}
|
||||
|
||||
expected := 1
|
||||
if res := calculateUiPrefixLength(ui); res != expected {
|
||||
t.Fatalf("calculateUiPrefixLength should have returned a length of %d", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateUiPrefixLength_TargetedUI(t *testing.T) {
|
||||
ui := &packer.TargetedUI{}
|
||||
ui.Target = "TestTarget"
|
||||
arrowText := "==>"
|
||||
|
||||
expected := len(arrowText + " " + ui.Target + ": ")
|
||||
if res := calculateUiPrefixLength(ui); res != expected {
|
||||
t.Fatalf("calculateUiPrefixLength should have returned a length of %d", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateUiPrefixLength_TargetedUIWrappingBasicUi(t *testing.T) {
|
||||
ui := &packer.TargetedUI{}
|
||||
ui.Target = "TestTarget"
|
||||
ui.Ui = &packer.BasicUi{}
|
||||
arrowText := "==>"
|
||||
|
||||
expected := len(arrowText + " " + ui.Target + ": " + "\n")
|
||||
if res := calculateUiPrefixLength(ui); res != expected {
|
||||
t.Fatalf("calculateUiPrefixLength should have returned a length of %d", expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateUiPrefixLength_TargetedUIWrappingMachineUi(t *testing.T) {
|
||||
ui := &packer.TargetedUI{}
|
||||
ui.Target = "TestTarget"
|
||||
ui.Ui = &packer.MachineReadableUi{}
|
||||
|
||||
expected := 0
|
||||
if res := calculateUiPrefixLength(ui); res != expected {
|
||||
t.Fatalf("calculateUiPrefixLength should have returned a length of %d", expected)
|
||||
}
|
||||
}
|
||||
func TestDefaultProgressBar(t *testing.T) {
|
||||
var callbackCalled bool
|
||||
|
||||
// Initialize the default progress bar
|
||||
bar := GetDefaultProgressBar()
|
||||
bar.Callback = func(state string) {
|
||||
callbackCalled = true
|
||||
t.Logf("TestDefaultProgressBar emitted %#v", state)
|
||||
}
|
||||
bar.SetTotal64(1)
|
||||
|
||||
// Set it off
|
||||
progressBar := bar.Start()
|
||||
progressBar.Set64(1)
|
||||
|
||||
// Check to see that the callback was hit
|
||||
if !callbackCalled {
|
||||
t.Fatalf("TestDefaultProgressBar.Callback should be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDummyProgressBar(t *testing.T) {
|
||||
var callbackCalled bool
|
||||
|
||||
// Initialize the dummy progress bar
|
||||
bar := GetDummyProgressBar()
|
||||
bar.Callback = func(state string) {
|
||||
callbackCalled = true
|
||||
t.Logf("TestDummyProgressBar emitted %#v", state)
|
||||
}
|
||||
bar.SetTotal64(1)
|
||||
|
||||
// Now we can go
|
||||
progressBar := bar.Start()
|
||||
progressBar.Set64(1)
|
||||
|
||||
// Check to see that the callback was hit
|
||||
if callbackCalled {
|
||||
t.Fatalf("TestDummyProgressBar.Callback should not be called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUiProgressBar(t *testing.T) {
|
||||
|
||||
ui := &testProgressBarUi{}
|
||||
|
||||
// Initialize the Ui progress bar
|
||||
bar := GetProgressBar(ui, nil)
|
||||
bar.SetTotal64(1)
|
||||
|
||||
// Ensure that callback has been set to something
|
||||
if bar.Callback == nil {
|
||||
t.Fatalf("TestUiProgressBar.Callback should be initialized")
|
||||
}
|
||||
|
||||
// Now we can go
|
||||
progressBar := bar.Start()
|
||||
progressBar.Set64(1)
|
||||
|
||||
// Check to see that the callback was hit
|
||||
if !ui.messageCalled {
|
||||
t.Fatalf("TestUiProgressBar.messageCalled should be called")
|
||||
}
|
||||
t.Logf("TestUiProgressBar emitted %#v", ui.messageMessage)
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
package common
|
||||
|
||||
// call into one of the platform-specific implementations to get the current terminal dimensions
|
||||
func GetTerminalDimensions() (width, height int, err error) {
|
||||
return platformGetTerminalDimensions()
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package common
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetTerminalDimensions(t *testing.T) {
|
||||
if _, _, err := GetTerminalDimensions(); err != nil {
|
||||
t.Fatalf("Unable to get terminal dimensions: %s", err)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
// +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 to get the console screen buffer info
|
||||
func getConsoleScreenBufferInfo(csbi *_CONSOLE_SCREEN_BUFFER_INFO) (err error) {
|
||||
var (
|
||||
bi _CONSOLE_SCREEN_BUFFER_INFO
|
||||
fd syscall.Handle
|
||||
)
|
||||
|
||||
// Re-open CONOUT$ as in some instances, stdout may be closed and guaranteed an stdout
|
||||
if fd, err = syscall.Open("CONOUT$", syscall.O_RDWR, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
|
||||
// grab the dimensions for the console
|
||||
if err = kernel32_GetConsoleScreenBufferInfo(fd, &bi); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*csbi = bi
|
||||
return nil
|
||||
}
|
||||
|
||||
func platformGetTerminalDimensions() (width, height int, err error) {
|
||||
var csbi _CONSOLE_SCREEN_BUFFER_INFO
|
||||
|
||||
if err = getConsoleScreenBufferInfo(&csbi); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return int(csbi.dwSize.X), int(csbi.dwSize.Y), nil
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package packer
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Reference in new issue