Merge pull request #5851 from arizvisa/multi-proto-progressbar
Add progress-bar to common/step_download.go
This commit is contained in:
commit
c1b50d365b
|
@ -54,7 +54,7 @@ func SupportedProtocol(u *url.URL) bool {
|
|||
|
||||
// build a dummy NewDownloadClient since this is the only place that valid
|
||||
// protocols are actually exposed.
|
||||
cli := NewDownloadClient(&DownloadConfig{})
|
||||
cli := NewDownloadClient(&DownloadConfig{}, nil)
|
||||
|
||||
// Iterate through each downloader to see if a protocol was found.
|
||||
ok := false
|
||||
|
@ -186,7 +186,7 @@ func FileExistsLocally(original string) bool {
|
|||
|
||||
// First create a dummy downloader so we can figure out which
|
||||
// protocol to use.
|
||||
cli := NewDownloadClient(&DownloadConfig{})
|
||||
cli := NewDownloadClient(&DownloadConfig{}, nil)
|
||||
d, ok := cli.config.DownloaderMap[u.Scheme]
|
||||
if !ok {
|
||||
return false
|
||||
|
|
|
@ -81,22 +81,28 @@ func HashForType(t string) hash.Hash {
|
|||
|
||||
// NewDownloadClient returns a new DownloadClient for the given
|
||||
// configuration.
|
||||
func NewDownloadClient(c *DownloadConfig) *DownloadClient {
|
||||
func NewDownloadClient(c *DownloadConfig, bar ProgressBar) *DownloadClient {
|
||||
const mtu = 1500 /* ethernet */ - 20 /* ipv4 */ - 20 /* tcp */
|
||||
|
||||
// If bar is nil, then use a dummy progress bar that doesn't do anything
|
||||
if bar == nil {
|
||||
bar = GetDummyProgressBar()
|
||||
}
|
||||
|
||||
// Create downloader map if it hasn't been specified already.
|
||||
if c.DownloaderMap == nil {
|
||||
c.DownloaderMap = map[string]Downloader{
|
||||
"file": &FileDownloader{bufferSize: nil},
|
||||
"http": &HTTPDownloader{userAgent: c.UserAgent},
|
||||
"https": &HTTPDownloader{userAgent: c.UserAgent},
|
||||
"smb": &SMBDownloader{bufferSize: nil},
|
||||
"file": &FileDownloader{progress: bar, bufferSize: nil},
|
||||
"http": &HTTPDownloader{progress: bar, userAgent: c.UserAgent},
|
||||
"https": &HTTPDownloader{progress: bar, userAgent: c.UserAgent},
|
||||
"smb": &SMBDownloader{progress: bar, bufferSize: nil},
|
||||
}
|
||||
}
|
||||
return &DownloadClient{config: c}
|
||||
}
|
||||
|
||||
// A downloader implements the ability to transfer, cancel, or resume a file.
|
||||
// A downloader implements the ability to transfer a file, and cancel or resume
|
||||
// it.
|
||||
type Downloader interface {
|
||||
Resume()
|
||||
Cancel()
|
||||
|
@ -205,14 +211,6 @@ func (d *DownloadClient) Get() (string, error) {
|
|||
return finalPath, err
|
||||
}
|
||||
|
||||
func (d *DownloadClient) PercentProgress() int {
|
||||
if d.downloader == nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return int((float64(d.downloader.Progress()) / float64(d.downloader.Total())) * 100)
|
||||
}
|
||||
|
||||
// VerifyChecksum tests that the path matches the checksum for the
|
||||
// download.
|
||||
func (d *DownloadClient) VerifyChecksum(path string) (bool, error) {
|
||||
|
@ -238,6 +236,8 @@ type HTTPDownloader struct {
|
|||
current uint64
|
||||
total uint64
|
||||
userAgent string
|
||||
|
||||
progress ProgressBar
|
||||
}
|
||||
|
||||
func (d *HTTPDownloader) Cancel() {
|
||||
|
@ -330,6 +330,11 @@ func (d *HTTPDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
|
||||
d.total = d.current + uint64(resp.ContentLength)
|
||||
|
||||
bar := d.progress
|
||||
bar.SetTotal64(int64(d.total))
|
||||
progressBar := bar.Start()
|
||||
progressBar.Set64(int64(d.current))
|
||||
|
||||
var buffer [4096]byte
|
||||
for {
|
||||
n, err := resp.Body.Read(buffer[:])
|
||||
|
@ -338,6 +343,7 @@ func (d *HTTPDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
}
|
||||
|
||||
d.current += uint64(n)
|
||||
progressBar.Set64(int64(d.current))
|
||||
|
||||
if _, werr := dst.Write(buffer[:n]); werr != nil {
|
||||
return werr
|
||||
|
@ -347,6 +353,7 @@ func (d *HTTPDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
break
|
||||
}
|
||||
}
|
||||
progressBar.Finish()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -366,6 +373,8 @@ type FileDownloader struct {
|
|||
active bool
|
||||
current uint64
|
||||
total uint64
|
||||
|
||||
progress ProgressBar
|
||||
}
|
||||
|
||||
func (d *FileDownloader) Progress() uint64 {
|
||||
|
@ -456,6 +465,11 @@ func (d *FileDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
}
|
||||
d.total = uint64(fi.Size())
|
||||
|
||||
bar := d.progress
|
||||
bar.SetTotal64(int64(d.total))
|
||||
progressBar := bar.Start()
|
||||
progressBar.Set64(int64(d.current))
|
||||
|
||||
// no bufferSize specified, so copy synchronously.
|
||||
if d.bufferSize == nil {
|
||||
var n int64
|
||||
|
@ -463,6 +477,7 @@ func (d *FileDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
d.active = false
|
||||
|
||||
d.current += uint64(n)
|
||||
progressBar.Set64(int64(d.current))
|
||||
|
||||
// use a goro in case someone else wants to enable cancel/resume
|
||||
} else {
|
||||
|
@ -475,6 +490,7 @@ func (d *FileDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
}
|
||||
|
||||
d.current += uint64(n)
|
||||
progressBar.Set64(int64(d.current))
|
||||
}
|
||||
d.active = false
|
||||
e <- err
|
||||
|
@ -483,6 +499,7 @@ func (d *FileDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
// ...and we spin until it's done
|
||||
err = <-errch
|
||||
}
|
||||
progressBar.Finish()
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
@ -495,6 +512,8 @@ type SMBDownloader struct {
|
|||
active bool
|
||||
current uint64
|
||||
total uint64
|
||||
|
||||
progress ProgressBar
|
||||
}
|
||||
|
||||
func (d *SMBDownloader) Progress() uint64 {
|
||||
|
@ -567,6 +586,11 @@ func (d *SMBDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
}
|
||||
d.total = uint64(fi.Size())
|
||||
|
||||
bar := d.progress
|
||||
bar.SetTotal64(int64(d.total))
|
||||
progressBar := bar.Start()
|
||||
progressBar.Set64(int64(d.current))
|
||||
|
||||
// no bufferSize specified, so copy synchronously.
|
||||
if d.bufferSize == nil {
|
||||
var n int64
|
||||
|
@ -574,6 +598,7 @@ func (d *SMBDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
d.active = false
|
||||
|
||||
d.current += uint64(n)
|
||||
progressBar.Set64(int64(d.current))
|
||||
|
||||
// use a goro in case someone else wants to enable cancel/resume
|
||||
} else {
|
||||
|
@ -586,6 +611,7 @@ func (d *SMBDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
}
|
||||
|
||||
d.current += uint64(n)
|
||||
progressBar.Set64(int64(d.current))
|
||||
}
|
||||
d.active = false
|
||||
e <- err
|
||||
|
@ -594,6 +620,7 @@ func (d *SMBDownloader) Download(dst *os.File, src *url.URL) error {
|
|||
// ...and as usual we spin until it's done
|
||||
err = <-errch
|
||||
}
|
||||
progressBar.Finish()
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ func TestDownloadClientVerifyChecksum(t *testing.T) {
|
|||
Checksum: checksum,
|
||||
}
|
||||
|
||||
d := NewDownloadClient(config)
|
||||
d := NewDownloadClient(config, nil)
|
||||
result, err := d.VerifyChecksum(tf.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("Verify err: %s", err)
|
||||
|
@ -59,7 +59,7 @@ func TestDownloadClient_basic(t *testing.T) {
|
|||
Url: ts.URL + "/basic.txt",
|
||||
TargetPath: tf.Name(),
|
||||
CopyFile: true,
|
||||
})
|
||||
}, nil)
|
||||
|
||||
path, err := client.Get()
|
||||
if err != nil {
|
||||
|
@ -95,7 +95,7 @@ func TestDownloadClient_checksumBad(t *testing.T) {
|
|||
Hash: HashForType("md5"),
|
||||
Checksum: checksum,
|
||||
CopyFile: true,
|
||||
})
|
||||
}, nil)
|
||||
|
||||
if _, err := client.Get(); err == nil {
|
||||
t.Fatal("should error")
|
||||
|
@ -121,7 +121,7 @@ func TestDownloadClient_checksumGood(t *testing.T) {
|
|||
Hash: HashForType("md5"),
|
||||
Checksum: checksum,
|
||||
CopyFile: true,
|
||||
})
|
||||
}, nil)
|
||||
|
||||
path, err := client.Get()
|
||||
if err != nil {
|
||||
|
@ -153,7 +153,7 @@ func TestDownloadClient_checksumNoDownload(t *testing.T) {
|
|||
Hash: HashForType("md5"),
|
||||
Checksum: checksum,
|
||||
CopyFile: true,
|
||||
})
|
||||
}, nil)
|
||||
path, err := client.Get()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
|
@ -183,7 +183,7 @@ func TestDownloadClient_notFound(t *testing.T) {
|
|||
client := NewDownloadClient(&DownloadConfig{
|
||||
Url: ts.URL + "/not-found.txt",
|
||||
TargetPath: tf.Name(),
|
||||
})
|
||||
}, nil)
|
||||
|
||||
if _, err := client.Get(); err == nil {
|
||||
t.Fatal("should error")
|
||||
|
@ -211,7 +211,7 @@ func TestDownloadClient_resume(t *testing.T) {
|
|||
Url: ts.URL,
|
||||
TargetPath: tf.Name(),
|
||||
CopyFile: true,
|
||||
})
|
||||
}, nil)
|
||||
|
||||
path, err := client.Get()
|
||||
if err != nil {
|
||||
|
@ -273,7 +273,7 @@ func TestDownloadClient_usesDefaultUserAgent(t *testing.T) {
|
|||
CopyFile: true,
|
||||
}
|
||||
|
||||
client := NewDownloadClient(config)
|
||||
client := NewDownloadClient(config, nil)
|
||||
_, err = client.Get()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -306,7 +306,7 @@ func TestDownloadClient_setsUserAgent(t *testing.T) {
|
|||
CopyFile: true,
|
||||
}
|
||||
|
||||
client := NewDownloadClient(config)
|
||||
client := NewDownloadClient(config, nil)
|
||||
_, err = client.Get()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -405,7 +405,7 @@ func TestDownloadFileUrl(t *testing.T) {
|
|||
CopyFile: false,
|
||||
}
|
||||
|
||||
client := NewDownloadClient(config)
|
||||
client := NewDownloadClient(config, nil)
|
||||
|
||||
// Verify that we fail to match the checksum
|
||||
_, err = client.Get()
|
||||
|
@ -436,7 +436,7 @@ func SimulateFileUriDownload(t *testing.T, uri string) (string, error) {
|
|||
}
|
||||
|
||||
// go go go
|
||||
client := NewDownloadClient(config)
|
||||
client := NewDownloadClient(config, nil)
|
||||
path, err := client.Get()
|
||||
|
||||
// ignore any non-important checksum errors if it's not a unc path
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/packer/rpc"
|
||||
"log"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This is the arrow from packer/ui.go -> TargetedUI.prefixLines
|
||||
const targetedUIArrowText = "==>"
|
||||
|
||||
// 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 adds the .Target and an arrow by default
|
||||
u := ui.(*packer.TargetedUI)
|
||||
res := fmt.Sprintf("%s %s: ", targetedUIArrowText, u.Target)
|
||||
return recursiveCalculateUiPrefixLength(u.Ui, agg+len(res))
|
||||
|
||||
case *packer.BasicUi:
|
||||
// The standard BasicUi appends only a newline
|
||||
return agg + len("\n")
|
||||
|
||||
// packer.rpc.Ui returns 0 here to trigger the hack described later
|
||||
case *rpc.Ui:
|
||||
return 0
|
||||
|
||||
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 GetPackerConfigFromStateBag(state multistep.StateBag) *PackerConfig {
|
||||
config := state.Get("config")
|
||||
rConfig := reflect.Indirect(reflect.ValueOf(config))
|
||||
iPackerConfig := rConfig.FieldByName("PackerConfig").Interface()
|
||||
packerConfig := iPackerConfig.(PackerConfig)
|
||||
return &packerConfig
|
||||
}
|
||||
|
||||
func GetProgressBar(ui packer.Ui, config *PackerConfig) ProgressBar {
|
||||
// Figure out the prefix length by quering the UI
|
||||
uiPrefixLength := calculateUiPrefixLength(ui)
|
||||
|
||||
// hack to deal with packer.rpc.Ui courtesy of @Swampdragons
|
||||
if _, ok := ui.(*rpc.Ui); uiPrefixLength == 0 && config != nil && ok {
|
||||
res := fmt.Sprintf("%s %s: \n", targetedUIArrowText, config.PackerBuildName)
|
||||
uiPrefixLength = len(res)
|
||||
}
|
||||
|
||||
// Now we can use the prefix length to calculate the progress bar width
|
||||
width := calculateProgressBarWidth(uiPrefixLength)
|
||||
|
||||
log.Printf("ProgressBar: Using progress bar width: %d\n", width)
|
||||
|
||||
// Get a default progress bar and set some output defaults
|
||||
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)
|
||||
}
|
|
@ -63,6 +63,9 @@ func (s *StepDownload) Run(_ context.Context, state multistep.StateBag) multiste
|
|||
|
||||
ui.Say(fmt.Sprintf("Retrieving %s", s.Description))
|
||||
|
||||
// Get a progress bar from the ui so we can hand it off to the download client
|
||||
bar := GetProgressBar(ui, GetPackerConfigFromStateBag(state))
|
||||
|
||||
// First try to use any already downloaded file
|
||||
// If it fails, proceed to regular download logic
|
||||
|
||||
|
@ -96,7 +99,7 @@ func (s *StepDownload) Run(_ context.Context, state multistep.StateBag) multiste
|
|||
}
|
||||
downloadConfigs[i] = config
|
||||
|
||||
if match, _ := NewDownloadClient(config).VerifyChecksum(config.TargetPath); match {
|
||||
if match, _ := NewDownloadClient(config, bar).VerifyChecksum(config.TargetPath); match {
|
||||
ui.Message(fmt.Sprintf("Found already downloaded, initial checksum matched, no download needed: %s", url))
|
||||
finalPath = config.TargetPath
|
||||
break
|
||||
|
@ -139,7 +142,12 @@ func (s *StepDownload) Cleanup(multistep.StateBag) {}
|
|||
func (s *StepDownload) download(config *DownloadConfig, state multistep.StateBag) (string, error, bool) {
|
||||
var path string
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
download := NewDownloadClient(config)
|
||||
|
||||
// Get a progress bar and hand it off to the download client
|
||||
bar := GetProgressBar(ui, GetPackerConfigFromStateBag(state))
|
||||
|
||||
// Create download client with config and progress bar
|
||||
download := NewDownloadClient(config, bar)
|
||||
|
||||
downloadCompleteCh := make(chan error, 1)
|
||||
go func() {
|
||||
|
@ -148,12 +156,11 @@ func (s *StepDownload) download(config *DownloadConfig, state multistep.StateBag
|
|||
downloadCompleteCh <- err
|
||||
}()
|
||||
|
||||
progressTicker := time.NewTicker(5 * time.Second)
|
||||
defer progressTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-downloadCompleteCh:
|
||||
bar.Finish()
|
||||
|
||||
if err != nil {
|
||||
return "", err, true
|
||||
}
|
||||
|
@ -164,13 +171,10 @@ func (s *StepDownload) download(config *DownloadConfig, state multistep.StateBag
|
|||
}
|
||||
|
||||
return path, nil, true
|
||||
case <-progressTicker.C:
|
||||
progress := download.PercentProgress()
|
||||
if progress >= 0 {
|
||||
ui.Message(fmt.Sprintf("Download progress: %d%%", progress))
|
||||
}
|
||||
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||
bar.Finish()
|
||||
ui.Say("Interrupt received. Cancelling download...")
|
||||
return "", nil, false
|
||||
}
|
||||
|
|
|
@ -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,38 @@
|
|||
// +build !windows
|
||||
|
||||
package common
|
||||
|
||||
// Imports for determining terminal information across platforms
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
"os"
|
||||
)
|
||||
|
||||
// posix api
|
||||
func platformGetTerminalDimensions() (width, height int, err error) {
|
||||
|
||||
// grab the handle to stdin
|
||||
// XXX: in some cases, packer closes stdin, so the following can't be guaranteed
|
||||
/*
|
||||
tty := os.Stdin
|
||||
*/
|
||||
|
||||
// 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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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.GetProgressBar(ui, &p.config.PackerConfig)
|
||||
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.GetProgressBar(ui, &p.config.PackerConfig)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
Copyright (c) 2012-2015, Sergey Cherepanov
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,179 @@
|
|||
# Terminal progress bar for Go
|
||||
|
||||
[![Join the chat at https://gitter.im/cheggaaa/pb](https://badges.gitter.im/cheggaaa/pb.svg)](https://gitter.im/cheggaaa/pb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
Simple progress bar for console programs.
|
||||
|
||||
Please check the new version https://github.com/cheggaaa/pb/tree/v2 (currently, it's beta)
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
go get gopkg.in/cheggaaa/pb.v1
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```Go
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/cheggaaa/pb.v1"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
count := 100000
|
||||
bar := pb.StartNew(count)
|
||||
for i := 0; i < count; i++ {
|
||||
bar.Increment()
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
bar.FinishPrint("The End!")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Result will be like this:
|
||||
|
||||
```
|
||||
> go run test.go
|
||||
37158 / 100000 [================>_______________________________] 37.16% 1m11s
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
```Go
|
||||
// create bar
|
||||
bar := pb.New(count)
|
||||
|
||||
// refresh info every second (default 200ms)
|
||||
bar.SetRefreshRate(time.Second)
|
||||
|
||||
// show percents (by default already true)
|
||||
bar.ShowPercent = true
|
||||
|
||||
// show bar (by default already true)
|
||||
bar.ShowBar = true
|
||||
|
||||
// no counters
|
||||
bar.ShowCounters = false
|
||||
|
||||
// show "time left"
|
||||
bar.ShowTimeLeft = true
|
||||
|
||||
// show average speed
|
||||
bar.ShowSpeed = true
|
||||
|
||||
// sets the width of the progress bar
|
||||
bar.SetWidth(80)
|
||||
|
||||
// sets the width of the progress bar, but if terminal size smaller will be ignored
|
||||
bar.SetMaxWidth(80)
|
||||
|
||||
// convert output to readable format (like KB, MB)
|
||||
bar.SetUnits(pb.U_BYTES)
|
||||
|
||||
// and start
|
||||
bar.Start()
|
||||
```
|
||||
|
||||
## Progress bar for IO Operations
|
||||
|
||||
```go
|
||||
// create and start bar
|
||||
bar := pb.New(myDataLen).SetUnits(pb.U_BYTES)
|
||||
bar.Start()
|
||||
|
||||
// my io.Reader
|
||||
r := myReader
|
||||
|
||||
// my io.Writer
|
||||
w := myWriter
|
||||
|
||||
// create proxy reader
|
||||
reader := bar.NewProxyReader(r)
|
||||
|
||||
// and copy from pb reader
|
||||
io.Copy(w, reader)
|
||||
|
||||
```
|
||||
|
||||
```go
|
||||
// create and start bar
|
||||
bar := pb.New(myDataLen).SetUnits(pb.U_BYTES)
|
||||
bar.Start()
|
||||
|
||||
// my io.Reader
|
||||
r := myReader
|
||||
|
||||
// my io.Writer
|
||||
w := myWriter
|
||||
|
||||
// create multi writer
|
||||
writer := io.MultiWriter(w, bar)
|
||||
|
||||
// and copy
|
||||
io.Copy(writer, r)
|
||||
|
||||
bar.Finish()
|
||||
```
|
||||
|
||||
## Custom Progress Bar Look-and-feel
|
||||
|
||||
```go
|
||||
bar.Format("<.- >")
|
||||
```
|
||||
|
||||
## Multiple Progress Bars (experimental and unstable)
|
||||
|
||||
Do not print to terminal while pool is active.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gopkg.in/cheggaaa/pb.v1"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// create bars
|
||||
first := pb.New(200).Prefix("First ")
|
||||
second := pb.New(200).Prefix("Second ")
|
||||
third := pb.New(200).Prefix("Third ")
|
||||
// start pool
|
||||
pool, err := pb.StartPool(first, second, third)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// update bars
|
||||
wg := new(sync.WaitGroup)
|
||||
for _, bar := range []*pb.ProgressBar{first, second, third} {
|
||||
wg.Add(1)
|
||||
go func(cb *pb.ProgressBar) {
|
||||
for n := 0; n < 200; n++ {
|
||||
cb.Increment()
|
||||
time.Sleep(time.Millisecond * time.Duration(rand.Intn(100)))
|
||||
}
|
||||
cb.Finish()
|
||||
wg.Done()
|
||||
}(bar)
|
||||
}
|
||||
wg.Wait()
|
||||
// close pool
|
||||
pool.Stop()
|
||||
}
|
||||
```
|
||||
|
||||
The result will be as follows:
|
||||
|
||||
```
|
||||
$ go run example/multiple.go
|
||||
First 34 / 200 [=========>---------------------------------------------] 17.00% 00m08s
|
||||
Second 42 / 200 [===========>------------------------------------------] 21.00% 00m06s
|
||||
Third 36 / 200 [=========>---------------------------------------------] 18.00% 00m08s
|
||||
```
|
|
@ -0,0 +1,125 @@
|
|||
package pb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Units int
|
||||
|
||||
const (
|
||||
// U_NO are default units, they represent a simple value and are not formatted at all.
|
||||
U_NO Units = iota
|
||||
// U_BYTES units are formatted in a human readable way (B, KiB, MiB, ...)
|
||||
U_BYTES
|
||||
// U_BYTES_DEC units are like U_BYTES, but base 10 (B, KB, MB, ...)
|
||||
U_BYTES_DEC
|
||||
// U_DURATION units are formatted in a human readable way (3h14m15s)
|
||||
U_DURATION
|
||||
)
|
||||
|
||||
const (
|
||||
KiB = 1024
|
||||
MiB = 1048576
|
||||
GiB = 1073741824
|
||||
TiB = 1099511627776
|
||||
|
||||
KB = 1e3
|
||||
MB = 1e6
|
||||
GB = 1e9
|
||||
TB = 1e12
|
||||
)
|
||||
|
||||
func Format(i int64) *formatter {
|
||||
return &formatter{n: i}
|
||||
}
|
||||
|
||||
type formatter struct {
|
||||
n int64
|
||||
unit Units
|
||||
width int
|
||||
perSec bool
|
||||
}
|
||||
|
||||
func (f *formatter) To(unit Units) *formatter {
|
||||
f.unit = unit
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *formatter) Width(width int) *formatter {
|
||||
f.width = width
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *formatter) PerSec() *formatter {
|
||||
f.perSec = true
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *formatter) String() (out string) {
|
||||
switch f.unit {
|
||||
case U_BYTES:
|
||||
out = formatBytes(f.n)
|
||||
case U_BYTES_DEC:
|
||||
out = formatBytesDec(f.n)
|
||||
case U_DURATION:
|
||||
out = formatDuration(f.n)
|
||||
default:
|
||||
out = fmt.Sprintf(fmt.Sprintf("%%%dd", f.width), f.n)
|
||||
}
|
||||
if f.perSec {
|
||||
out += "/s"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Convert bytes to human readable string. Like 2 MiB, 64.2 KiB, 52 B
|
||||
func formatBytes(i int64) (result string) {
|
||||
switch {
|
||||
case i >= TiB:
|
||||
result = fmt.Sprintf("%.02f TiB", float64(i)/TiB)
|
||||
case i >= GiB:
|
||||
result = fmt.Sprintf("%.02f GiB", float64(i)/GiB)
|
||||
case i >= MiB:
|
||||
result = fmt.Sprintf("%.02f MiB", float64(i)/MiB)
|
||||
case i >= KiB:
|
||||
result = fmt.Sprintf("%.02f KiB", float64(i)/KiB)
|
||||
default:
|
||||
result = fmt.Sprintf("%d B", i)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Convert bytes to base-10 human readable string. Like 2 MB, 64.2 KB, 52 B
|
||||
func formatBytesDec(i int64) (result string) {
|
||||
switch {
|
||||
case i >= TB:
|
||||
result = fmt.Sprintf("%.02f TB", float64(i)/TB)
|
||||
case i >= GB:
|
||||
result = fmt.Sprintf("%.02f GB", float64(i)/GB)
|
||||
case i >= MB:
|
||||
result = fmt.Sprintf("%.02f MB", float64(i)/MB)
|
||||
case i >= KB:
|
||||
result = fmt.Sprintf("%.02f KB", float64(i)/KB)
|
||||
default:
|
||||
result = fmt.Sprintf("%d B", i)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func formatDuration(n int64) (result string) {
|
||||
d := time.Duration(n)
|
||||
if d > time.Hour*24 {
|
||||
result = fmt.Sprintf("%dd", d/24/time.Hour)
|
||||
d -= (d / time.Hour / 24) * (time.Hour * 24)
|
||||
}
|
||||
if d > time.Hour {
|
||||
result = fmt.Sprintf("%s%dh", result, d/time.Hour)
|
||||
d -= d / time.Hour * time.Hour
|
||||
}
|
||||
m := d / time.Minute
|
||||
d -= m * time.Minute
|
||||
s := d / time.Second
|
||||
result = fmt.Sprintf("%s%02dm%02ds", result, m, s)
|
||||
return
|
||||
}
|
|
@ -0,0 +1,500 @@
|
|||
// Simple console progress bars
|
||||
package pb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Current version
|
||||
const Version = "1.0.25"
|
||||
|
||||
const (
|
||||
// Default refresh rate - 200ms
|
||||
DEFAULT_REFRESH_RATE = time.Millisecond * 200
|
||||
FORMAT = "[=>-]"
|
||||
)
|
||||
|
||||
// DEPRECATED
|
||||
// variables for backward compatibility, from now do not work
|
||||
// use pb.Format and pb.SetRefreshRate
|
||||
var (
|
||||
DefaultRefreshRate = DEFAULT_REFRESH_RATE
|
||||
BarStart, BarEnd, Empty, Current, CurrentN string
|
||||
)
|
||||
|
||||
// Create new progress bar object
|
||||
func New(total int) *ProgressBar {
|
||||
return New64(int64(total))
|
||||
}
|
||||
|
||||
// Create new progress bar object using int64 as total
|
||||
func New64(total int64) *ProgressBar {
|
||||
pb := &ProgressBar{
|
||||
Total: total,
|
||||
RefreshRate: DEFAULT_REFRESH_RATE,
|
||||
ShowPercent: true,
|
||||
ShowCounters: true,
|
||||
ShowBar: true,
|
||||
ShowTimeLeft: true,
|
||||
ShowElapsedTime: false,
|
||||
ShowFinalTime: true,
|
||||
Units: U_NO,
|
||||
ManualUpdate: false,
|
||||
finish: make(chan struct{}),
|
||||
}
|
||||
return pb.Format(FORMAT)
|
||||
}
|
||||
|
||||
// Create new object and start
|
||||
func StartNew(total int) *ProgressBar {
|
||||
return New(total).Start()
|
||||
}
|
||||
|
||||
// Callback for custom output
|
||||
// For example:
|
||||
// bar.Callback = func(s string) {
|
||||
// mySuperPrint(s)
|
||||
// }
|
||||
//
|
||||
type Callback func(out string)
|
||||
|
||||
type ProgressBar struct {
|
||||
current int64 // current must be first member of struct (https://code.google.com/p/go/issues/detail?id=5278)
|
||||
previous int64
|
||||
|
||||
Total int64
|
||||
RefreshRate time.Duration
|
||||
ShowPercent, ShowCounters bool
|
||||
ShowSpeed, ShowTimeLeft, ShowBar bool
|
||||
ShowFinalTime, ShowElapsedTime bool
|
||||
Output io.Writer
|
||||
Callback Callback
|
||||
NotPrint bool
|
||||
Units Units
|
||||
Width int
|
||||
ForceWidth bool
|
||||
ManualUpdate bool
|
||||
AutoStat bool
|
||||
|
||||
// Default width for the time box.
|
||||
UnitsWidth int
|
||||
TimeBoxWidth int
|
||||
|
||||
finishOnce sync.Once //Guards isFinish
|
||||
finish chan struct{}
|
||||
isFinish bool
|
||||
|
||||
startTime time.Time
|
||||
startValue int64
|
||||
|
||||
changeTime time.Time
|
||||
|
||||
prefix, postfix string
|
||||
|
||||
mu sync.Mutex
|
||||
lastPrint string
|
||||
|
||||
BarStart string
|
||||
BarEnd string
|
||||
Empty string
|
||||
Current string
|
||||
CurrentN string
|
||||
|
||||
AlwaysUpdate bool
|
||||
}
|
||||
|
||||
// Start print
|
||||
func (pb *ProgressBar) Start() *ProgressBar {
|
||||
pb.startTime = time.Now()
|
||||
pb.startValue = atomic.LoadInt64(&pb.current)
|
||||
if atomic.LoadInt64(&pb.Total) == 0 {
|
||||
pb.ShowTimeLeft = false
|
||||
pb.ShowPercent = false
|
||||
pb.AutoStat = false
|
||||
}
|
||||
if !pb.ManualUpdate {
|
||||
pb.Update() // Initial printing of the bar before running the bar refresher.
|
||||
go pb.refresher()
|
||||
}
|
||||
return pb
|
||||
}
|
||||
|
||||
// Increment current value
|
||||
func (pb *ProgressBar) Increment() int {
|
||||
return pb.Add(1)
|
||||
}
|
||||
|
||||
// Get current value
|
||||
func (pb *ProgressBar) Get() int64 {
|
||||
c := atomic.LoadInt64(&pb.current)
|
||||
return c
|
||||
}
|
||||
|
||||
// Set current value
|
||||
func (pb *ProgressBar) Set(current int) *ProgressBar {
|
||||
return pb.Set64(int64(current))
|
||||
}
|
||||
|
||||
// Set64 sets the current value as int64
|
||||
func (pb *ProgressBar) Set64(current int64) *ProgressBar {
|
||||
atomic.StoreInt64(&pb.current, current)
|
||||
return pb
|
||||
}
|
||||
|
||||
// Add to current value
|
||||
func (pb *ProgressBar) Add(add int) int {
|
||||
return int(pb.Add64(int64(add)))
|
||||
}
|
||||
|
||||
func (pb *ProgressBar) Add64(add int64) int64 {
|
||||
return atomic.AddInt64(&pb.current, add)
|
||||
}
|
||||
|
||||
// Set prefix string
|
||||
func (pb *ProgressBar) Prefix(prefix string) *ProgressBar {
|
||||
pb.prefix = prefix
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set postfix string
|
||||
func (pb *ProgressBar) Postfix(postfix string) *ProgressBar {
|
||||
pb.postfix = postfix
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set custom format for bar
|
||||
// Example: bar.Format("[=>_]")
|
||||
// Example: bar.Format("[\x00=\x00>\x00-\x00]") // \x00 is the delimiter
|
||||
func (pb *ProgressBar) Format(format string) *ProgressBar {
|
||||
var formatEntries []string
|
||||
if utf8.RuneCountInString(format) == 5 {
|
||||
formatEntries = strings.Split(format, "")
|
||||
} else {
|
||||
formatEntries = strings.Split(format, "\x00")
|
||||
}
|
||||
if len(formatEntries) == 5 {
|
||||
pb.BarStart = formatEntries[0]
|
||||
pb.BarEnd = formatEntries[4]
|
||||
pb.Empty = formatEntries[3]
|
||||
pb.Current = formatEntries[1]
|
||||
pb.CurrentN = formatEntries[2]
|
||||
}
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set bar refresh rate
|
||||
func (pb *ProgressBar) SetRefreshRate(rate time.Duration) *ProgressBar {
|
||||
pb.RefreshRate = rate
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set units
|
||||
// bar.SetUnits(U_NO) - by default
|
||||
// bar.SetUnits(U_BYTES) - for Mb, Kb, etc
|
||||
func (pb *ProgressBar) SetUnits(units Units) *ProgressBar {
|
||||
pb.Units = units
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set max width, if width is bigger than terminal width, will be ignored
|
||||
func (pb *ProgressBar) SetMaxWidth(width int) *ProgressBar {
|
||||
pb.Width = width
|
||||
pb.ForceWidth = false
|
||||
return pb
|
||||
}
|
||||
|
||||
// Set bar width
|
||||
func (pb *ProgressBar) SetWidth(width int) *ProgressBar {
|
||||
pb.Width = width
|
||||
pb.ForceWidth = true
|
||||
return pb
|
||||
}
|
||||
|
||||
// End print
|
||||
func (pb *ProgressBar) Finish() {
|
||||
//Protect multiple calls
|
||||
pb.finishOnce.Do(func() {
|
||||
close(pb.finish)
|
||||
pb.write(atomic.LoadInt64(&pb.Total), atomic.LoadInt64(&pb.current))
|
||||
pb.mu.Lock()
|
||||
defer pb.mu.Unlock()
|
||||
switch {
|
||||
case pb.Output != nil:
|
||||
fmt.Fprintln(pb.Output)
|
||||
case !pb.NotPrint:
|
||||
fmt.Println()
|
||||
}
|
||||
pb.isFinish = true
|
||||
})
|
||||
}
|
||||
|
||||
// IsFinished return boolean
|
||||
func (pb *ProgressBar) IsFinished() bool {
|
||||
pb.mu.Lock()
|
||||
defer pb.mu.Unlock()
|
||||
return pb.isFinish
|
||||
}
|
||||
|
||||
// End print and write string 'str'
|
||||
func (pb *ProgressBar) FinishPrint(str string) {
|
||||
pb.Finish()
|
||||
if pb.Output != nil {
|
||||
fmt.Fprintln(pb.Output, str)
|
||||
} else {
|
||||
fmt.Println(str)
|
||||
}
|
||||
}
|
||||
|
||||
// implement io.Writer
|
||||
func (pb *ProgressBar) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
pb.Add(n)
|
||||
return
|
||||
}
|
||||
|
||||
// implement io.Reader
|
||||
func (pb *ProgressBar) Read(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
pb.Add(n)
|
||||
return
|
||||
}
|
||||
|
||||
// Create new proxy reader over bar
|
||||
// Takes io.Reader or io.ReadCloser
|
||||
func (pb *ProgressBar) NewProxyReader(r io.Reader) *Reader {
|
||||
return &Reader{r, pb}
|
||||
}
|
||||
|
||||
func (pb *ProgressBar) write(total, current int64) {
|
||||
width := pb.GetWidth()
|
||||
|
||||
var percentBox, countersBox, timeLeftBox, timeSpentBox, speedBox, barBox, end, out string
|
||||
|
||||
// percents
|
||||
if pb.ShowPercent {
|
||||
var percent float64
|
||||
if total > 0 {
|
||||
percent = float64(current) / (float64(total) / float64(100))
|
||||
} else {
|
||||
percent = float64(current) / float64(100)
|
||||
}
|
||||
percentBox = fmt.Sprintf(" %6.02f%%", percent)
|
||||
}
|
||||
|
||||
// counters
|
||||
if pb.ShowCounters {
|
||||
current := Format(current).To(pb.Units).Width(pb.UnitsWidth)
|
||||
if total > 0 {
|
||||
totalS := Format(total).To(pb.Units).Width(pb.UnitsWidth)
|
||||
countersBox = fmt.Sprintf(" %s / %s ", current, totalS)
|
||||
} else {
|
||||
countersBox = fmt.Sprintf(" %s / ? ", current)
|
||||
}
|
||||
}
|
||||
|
||||
// time left
|
||||
pb.mu.Lock()
|
||||
currentFromStart := current - pb.startValue
|
||||
fromStart := time.Now().Sub(pb.startTime)
|
||||
lastChangeTime := pb.changeTime
|
||||
fromChange := lastChangeTime.Sub(pb.startTime)
|
||||
pb.mu.Unlock()
|
||||
|
||||
if pb.ShowElapsedTime {
|
||||
timeSpentBox = fmt.Sprintf(" %s ", (fromStart/time.Second)*time.Second)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-pb.finish:
|
||||
if pb.ShowFinalTime {
|
||||
var left time.Duration
|
||||
left = (fromStart / time.Second) * time.Second
|
||||
timeLeftBox = fmt.Sprintf(" %s", left.String())
|
||||
}
|
||||
default:
|
||||
if pb.ShowTimeLeft && currentFromStart > 0 {
|
||||
perEntry := fromChange / time.Duration(currentFromStart)
|
||||
var left time.Duration
|
||||
if total > 0 {
|
||||
left = time.Duration(total-currentFromStart) * perEntry
|
||||
left -= time.Since(lastChangeTime)
|
||||
left = (left / time.Second) * time.Second
|
||||
} else {
|
||||
left = time.Duration(currentFromStart) * perEntry
|
||||
left = (left / time.Second) * time.Second
|
||||
}
|
||||
if left > 0 {
|
||||
timeLeft := Format(int64(left)).To(U_DURATION).String()
|
||||
timeLeftBox = fmt.Sprintf(" %s", timeLeft)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(timeLeftBox) < pb.TimeBoxWidth {
|
||||
timeLeftBox = fmt.Sprintf("%s%s", strings.Repeat(" ", pb.TimeBoxWidth-len(timeLeftBox)), timeLeftBox)
|
||||
}
|
||||
|
||||
// speed
|
||||
if pb.ShowSpeed && currentFromStart > 0 {
|
||||
fromStart := time.Now().Sub(pb.startTime)
|
||||
speed := float64(currentFromStart) / (float64(fromStart) / float64(time.Second))
|
||||
speedBox = " " + Format(int64(speed)).To(pb.Units).Width(pb.UnitsWidth).PerSec().String()
|
||||
}
|
||||
|
||||
barWidth := escapeAwareRuneCountInString(countersBox + pb.BarStart + pb.BarEnd + percentBox + timeSpentBox + timeLeftBox + speedBox + pb.prefix + pb.postfix)
|
||||
// bar
|
||||
if pb.ShowBar {
|
||||
size := width - barWidth
|
||||
if size > 0 {
|
||||
if total > 0 {
|
||||
curSize := int(math.Ceil((float64(current) / float64(total)) * float64(size)))
|
||||
emptySize := size - curSize
|
||||
barBox = pb.BarStart
|
||||
if emptySize < 0 {
|
||||
emptySize = 0
|
||||
}
|
||||
if curSize > size {
|
||||
curSize = size
|
||||
}
|
||||
|
||||
cursorLen := escapeAwareRuneCountInString(pb.Current)
|
||||
if emptySize <= 0 {
|
||||
barBox += strings.Repeat(pb.Current, curSize/cursorLen)
|
||||
} else if curSize > 0 {
|
||||
cursorEndLen := escapeAwareRuneCountInString(pb.CurrentN)
|
||||
cursorRepetitions := (curSize - cursorEndLen) / cursorLen
|
||||
barBox += strings.Repeat(pb.Current, cursorRepetitions)
|
||||
barBox += pb.CurrentN
|
||||
}
|
||||
|
||||
emptyLen := escapeAwareRuneCountInString(pb.Empty)
|
||||
barBox += strings.Repeat(pb.Empty, emptySize/emptyLen)
|
||||
barBox += pb.BarEnd
|
||||
} else {
|
||||
pos := size - int(current)%int(size)
|
||||
barBox = pb.BarStart
|
||||
if pos-1 > 0 {
|
||||
barBox += strings.Repeat(pb.Empty, pos-1)
|
||||
}
|
||||
barBox += pb.Current
|
||||
if size-pos-1 > 0 {
|
||||
barBox += strings.Repeat(pb.Empty, size-pos-1)
|
||||
}
|
||||
barBox += pb.BarEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check len
|
||||
out = pb.prefix + timeSpentBox + countersBox + barBox + percentBox + speedBox + timeLeftBox + pb.postfix
|
||||
if cl := escapeAwareRuneCountInString(out); cl < width {
|
||||
end = strings.Repeat(" ", width-cl)
|
||||
}
|
||||
|
||||
// and print!
|
||||
pb.mu.Lock()
|
||||
defer pb.mu.Unlock()
|
||||
pb.lastPrint = out + end
|
||||
isFinish := pb.isFinish
|
||||
|
||||
switch {
|
||||
case isFinish:
|
||||
return
|
||||
case pb.Output != nil:
|
||||
fmt.Fprint(pb.Output, "\r"+out+end)
|
||||
case pb.Callback != nil:
|
||||
pb.Callback(out + end)
|
||||
case !pb.NotPrint:
|
||||
fmt.Print("\r" + out + end)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTerminalWidth - returns terminal width for all platforms.
|
||||
func GetTerminalWidth() (int, error) {
|
||||
return terminalWidth()
|
||||
}
|
||||
|
||||
func (pb *ProgressBar) GetWidth() int {
|
||||
if pb.ForceWidth {
|
||||
return pb.Width
|
||||
}
|
||||
|
||||
width := pb.Width
|
||||
termWidth, _ := terminalWidth()
|
||||
if width == 0 || termWidth <= width {
|
||||
width = termWidth
|
||||
}
|
||||
|
||||
return width
|
||||
}
|
||||
|
||||
// Write the current state of the progressbar
|
||||
func (pb *ProgressBar) Update() {
|
||||
c := atomic.LoadInt64(&pb.current)
|
||||
p := atomic.LoadInt64(&pb.previous)
|
||||
t := atomic.LoadInt64(&pb.Total)
|
||||
if p != c {
|
||||
pb.mu.Lock()
|
||||
pb.changeTime = time.Now()
|
||||
pb.mu.Unlock()
|
||||
atomic.StoreInt64(&pb.previous, c)
|
||||
}
|
||||
pb.write(t, c)
|
||||
if pb.AutoStat {
|
||||
if c == 0 {
|
||||
pb.startTime = time.Now()
|
||||
pb.startValue = 0
|
||||
} else if c >= t && pb.isFinish != true {
|
||||
pb.Finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String return the last bar print
|
||||
func (pb *ProgressBar) String() string {
|
||||
pb.mu.Lock()
|
||||
defer pb.mu.Unlock()
|
||||
return pb.lastPrint
|
||||
}
|
||||
|
||||
// SetTotal atomically sets new total count
|
||||
func (pb *ProgressBar) SetTotal(total int) *ProgressBar {
|
||||
return pb.SetTotal64(int64(total))
|
||||
}
|
||||
|
||||
// SetTotal64 atomically sets new total count
|
||||
func (pb *ProgressBar) SetTotal64(total int64) *ProgressBar {
|
||||
atomic.StoreInt64(&pb.Total, total)
|
||||
return pb
|
||||
}
|
||||
|
||||
// Reset bar and set new total count
|
||||
// Does effect only on finished bar
|
||||
func (pb *ProgressBar) Reset(total int) *ProgressBar {
|
||||
pb.mu.Lock()
|
||||
defer pb.mu.Unlock()
|
||||
if pb.isFinish {
|
||||
pb.SetTotal(total).Set(0)
|
||||
atomic.StoreInt64(&pb.previous, 0)
|
||||
}
|
||||
return pb
|
||||
}
|
||||
|
||||
// Internal loop for refreshing the progressbar
|
||||
func (pb *ProgressBar) refresher() {
|
||||
for {
|
||||
select {
|
||||
case <-pb.finish:
|
||||
return
|
||||
case <-time.After(pb.RefreshRate):
|
||||
pb.Update()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
// +build windows
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var tty = os.Stdin
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
// GetConsoleScreenBufferInfo retrieves information about the
|
||||
// specified console screen buffer.
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx
|
||||
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||
|
||||
// GetConsoleMode retrieves the current input mode of a console's
|
||||
// input buffer or the current output mode of a console screen buffer.
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx
|
||||
getConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||
|
||||
// SetConsoleMode sets the input mode of a console's input buffer
|
||||
// or the output mode of a console screen buffer.
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx
|
||||
setConsoleMode = kernel32.NewProc("SetConsoleMode")
|
||||
|
||||
// SetConsoleCursorPosition sets the cursor position in the
|
||||
// specified console screen buffer.
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx
|
||||
setConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
|
||||
)
|
||||
|
||||
type (
|
||||
// Defines the coordinates of the upper left and lower right corners
|
||||
// of a rectangle.
|
||||
// See
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms686311(v=vs.85).aspx
|
||||
smallRect struct {
|
||||
Left, Top, Right, Bottom int16
|
||||
}
|
||||
|
||||
// Defines the coordinates of a character cell in a console screen
|
||||
// buffer. The origin of the coordinate system (0,0) is at the top, left cell
|
||||
// of the buffer.
|
||||
// See
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms682119(v=vs.85).aspx
|
||||
coordinates struct {
|
||||
X, Y int16
|
||||
}
|
||||
|
||||
word int16
|
||||
|
||||
// Contains information about a console screen buffer.
|
||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx
|
||||
consoleScreenBufferInfo struct {
|
||||
dwSize coordinates
|
||||
dwCursorPosition coordinates
|
||||
wAttributes word
|
||||
srWindow smallRect
|
||||
dwMaximumWindowSize coordinates
|
||||
}
|
||||
)
|
||||
|
||||
// terminalWidth returns width of the terminal.
|
||||
func terminalWidth() (width int, err error) {
|
||||
var info consoleScreenBufferInfo
|
||||
_, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0)
|
||||
if e != 0 {
|
||||
return 0, error(e)
|
||||
}
|
||||
return int(info.dwSize.X) - 1, nil
|
||||
}
|
||||
|
||||
func getCursorPos() (pos coordinates, err error) {
|
||||
var info consoleScreenBufferInfo
|
||||
_, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&info)), 0)
|
||||
if e != 0 {
|
||||
return info.dwCursorPosition, error(e)
|
||||
}
|
||||
return info.dwCursorPosition, nil
|
||||
}
|
||||
|
||||
func setCursorPos(pos coordinates) error {
|
||||
_, _, e := syscall.Syscall(setConsoleCursorPosition.Addr(), 2, uintptr(syscall.Stdout), uintptr(uint32(uint16(pos.Y))<<16|uint32(uint16(pos.X))), 0)
|
||||
if e != 0 {
|
||||
return error(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var ErrPoolWasStarted = errors.New("Bar pool was started")
|
||||
|
||||
var echoLocked bool
|
||||
var echoLockMutex sync.Mutex
|
||||
|
||||
var oldState word
|
||||
|
||||
func lockEcho() (shutdownCh chan struct{}, err error) {
|
||||
echoLockMutex.Lock()
|
||||
defer echoLockMutex.Unlock()
|
||||
if echoLocked {
|
||||
err = ErrPoolWasStarted
|
||||
return
|
||||
}
|
||||
echoLocked = true
|
||||
|
||||
if _, _, e := syscall.Syscall(getConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(unsafe.Pointer(&oldState)), 0); e != 0 {
|
||||
err = fmt.Errorf("Can't get terminal settings: %v", e)
|
||||
return
|
||||
}
|
||||
|
||||
newState := oldState
|
||||
const ENABLE_ECHO_INPUT = 0x0004
|
||||
const ENABLE_LINE_INPUT = 0x0002
|
||||
newState = newState & (^(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT))
|
||||
if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(newState), 0); e != 0 {
|
||||
err = fmt.Errorf("Can't set terminal settings: %v", e)
|
||||
return
|
||||
}
|
||||
|
||||
shutdownCh = make(chan struct{})
|
||||
return
|
||||
}
|
||||
|
||||
func unlockEcho() (err error) {
|
||||
echoLockMutex.Lock()
|
||||
defer echoLockMutex.Unlock()
|
||||
if !echoLocked {
|
||||
return
|
||||
}
|
||||
echoLocked = false
|
||||
if _, _, e := syscall.Syscall(setConsoleMode.Addr(), 2, uintptr(syscall.Stdout), uintptr(oldState), 0); e != 0 {
|
||||
err = fmt.Errorf("Can't set terminal settings")
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
// +build linux darwin freebsd netbsd openbsd solaris dragonfly
|
||||
// +build !appengine
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var ErrPoolWasStarted = errors.New("Bar pool was started")
|
||||
|
||||
var (
|
||||
echoLockMutex sync.Mutex
|
||||
origTermStatePtr *unix.Termios
|
||||
tty *os.File
|
||||
)
|
||||
|
||||
func init() {
|
||||
echoLockMutex.Lock()
|
||||
defer echoLockMutex.Unlock()
|
||||
|
||||
var err error
|
||||
tty, err = os.Open("/dev/tty")
|
||||
if err != nil {
|
||||
tty = os.Stdin
|
||||
}
|
||||
}
|
||||
|
||||
// terminalWidth returns width of the terminal.
|
||||
func terminalWidth() (int, error) {
|
||||
echoLockMutex.Lock()
|
||||
defer echoLockMutex.Unlock()
|
||||
|
||||
fd := int(tty.Fd())
|
||||
|
||||
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(ws.Col), nil
|
||||
}
|
||||
|
||||
func lockEcho() (shutdownCh chan struct{}, err error) {
|
||||
echoLockMutex.Lock()
|
||||
defer echoLockMutex.Unlock()
|
||||
if origTermStatePtr != nil {
|
||||
return shutdownCh, ErrPoolWasStarted
|
||||
}
|
||||
|
||||
fd := int(tty.Fd())
|
||||
|
||||
origTermStatePtr, err = unix.IoctlGetTermios(fd, ioctlReadTermios)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Can't get terminal settings: %v", err)
|
||||
}
|
||||
|
||||
oldTermios := *origTermStatePtr
|
||||
newTermios := oldTermios
|
||||
newTermios.Lflag &^= syscall.ECHO
|
||||
newTermios.Lflag |= syscall.ICANON | syscall.ISIG
|
||||
newTermios.Iflag |= syscall.ICRNL
|
||||
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, &newTermios); err != nil {
|
||||
return nil, fmt.Errorf("Can't set terminal settings: %v", err)
|
||||
}
|
||||
|
||||
shutdownCh = make(chan struct{})
|
||||
go catchTerminate(shutdownCh)
|
||||
return
|
||||
}
|
||||
|
||||
func unlockEcho() error {
|
||||
echoLockMutex.Lock()
|
||||
defer echoLockMutex.Unlock()
|
||||
if origTermStatePtr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fd := int(tty.Fd())
|
||||
|
||||
if err := unix.IoctlSetTermios(fd, ioctlWriteTermios, origTermStatePtr); err != nil {
|
||||
return fmt.Errorf("Can't set terminal settings: %v", err)
|
||||
}
|
||||
|
||||
origTermStatePtr = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// listen exit signals and restore terminal state
|
||||
func catchTerminate(shutdownCh chan struct{}) {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGKILL)
|
||||
defer signal.Stop(sig)
|
||||
select {
|
||||
case <-shutdownCh:
|
||||
unlockEcho()
|
||||
case <-sig:
|
||||
unlockEcho()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
// +build linux darwin freebsd netbsd openbsd solaris dragonfly windows
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Create and start new pool with given bars
|
||||
// You need call pool.Stop() after work
|
||||
func StartPool(pbs ...*ProgressBar) (pool *Pool, err error) {
|
||||
pool = new(Pool)
|
||||
if err = pool.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
pool.Add(pbs...)
|
||||
return
|
||||
}
|
||||
|
||||
// NewPool initialises a pool with progress bars, but
|
||||
// doesn't start it. You need to call Start manually
|
||||
func NewPool(pbs ...*ProgressBar) (pool *Pool) {
|
||||
pool = new(Pool)
|
||||
pool.Add(pbs...)
|
||||
return
|
||||
}
|
||||
|
||||
type Pool struct {
|
||||
Output io.Writer
|
||||
RefreshRate time.Duration
|
||||
bars []*ProgressBar
|
||||
lastBarsCount int
|
||||
shutdownCh chan struct{}
|
||||
workerCh chan struct{}
|
||||
m sync.Mutex
|
||||
finishOnce sync.Once
|
||||
}
|
||||
|
||||
// Add progress bars.
|
||||
func (p *Pool) Add(pbs ...*ProgressBar) {
|
||||
p.m.Lock()
|
||||
defer p.m.Unlock()
|
||||
for _, bar := range pbs {
|
||||
bar.ManualUpdate = true
|
||||
bar.NotPrint = true
|
||||
bar.Start()
|
||||
p.bars = append(p.bars, bar)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) Start() (err error) {
|
||||
p.RefreshRate = DefaultRefreshRate
|
||||
p.shutdownCh, err = lockEcho()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
p.workerCh = make(chan struct{})
|
||||
go p.writer()
|
||||
return
|
||||
}
|
||||
|
||||
func (p *Pool) writer() {
|
||||
var first = true
|
||||
defer func() {
|
||||
if first == false {
|
||||
p.print(false)
|
||||
} else {
|
||||
p.print(true)
|
||||
p.print(false)
|
||||
}
|
||||
close(p.workerCh)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-time.After(p.RefreshRate):
|
||||
if p.print(first) {
|
||||
p.print(false)
|
||||
return
|
||||
}
|
||||
first = false
|
||||
case <-p.shutdownCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore terminal state and close pool
|
||||
func (p *Pool) Stop() error {
|
||||
p.finishOnce.Do(func() {
|
||||
close(p.shutdownCh)
|
||||
})
|
||||
|
||||
// Wait for the worker to complete
|
||||
select {
|
||||
case <-p.workerCh:
|
||||
}
|
||||
|
||||
return unlockEcho()
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// +build windows
|
||||
|
||||
package pb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
func (p *Pool) print(first bool) bool {
|
||||
p.m.Lock()
|
||||
defer p.m.Unlock()
|
||||
var out string
|
||||
if !first {
|
||||
coords, err := getCursorPos()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
coords.Y -= int16(p.lastBarsCount)
|
||||
if coords.Y < 0 {
|
||||
coords.Y = 0
|
||||
}
|
||||
coords.X = 0
|
||||
|
||||
err = setCursorPos(coords)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
}
|
||||
isFinished := true
|
||||
for _, bar := range p.bars {
|
||||
if !bar.IsFinished() {
|
||||
isFinished = false
|
||||
}
|
||||
bar.Update()
|
||||
out += fmt.Sprintf("\r%s\n", bar.String())
|
||||
}
|
||||
if p.Output != nil {
|
||||
fmt.Fprint(p.Output, out)
|
||||
} else {
|
||||
fmt.Print(out)
|
||||
}
|
||||
p.lastBarsCount = len(p.bars)
|
||||
return isFinished
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// +build linux darwin freebsd netbsd openbsd solaris dragonfly
|
||||
|
||||
package pb
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (p *Pool) print(first bool) bool {
|
||||
p.m.Lock()
|
||||
defer p.m.Unlock()
|
||||
var out string
|
||||
if !first {
|
||||
out = fmt.Sprintf("\033[%dA", p.lastBarsCount)
|
||||
}
|
||||
isFinished := true
|
||||
for _, bar := range p.bars {
|
||||
if !bar.IsFinished() {
|
||||
isFinished = false
|
||||
}
|
||||
bar.Update()
|
||||
out += fmt.Sprintf("\r%s\n", bar.String())
|
||||
}
|
||||
if p.Output != nil {
|
||||
fmt.Fprint(p.Output, out)
|
||||
} else {
|
||||
fmt.Print(out)
|
||||
}
|
||||
p.lastBarsCount = len(p.bars)
|
||||
return isFinished
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package pb
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// It's proxy reader, implement io.Reader
|
||||
type Reader struct {
|
||||
io.Reader
|
||||
bar *ProgressBar
|
||||
}
|
||||
|
||||
func (r *Reader) Read(p []byte) (n int, err error) {
|
||||
n, err = r.Reader.Read(p)
|
||||
r.bar.Add(n)
|
||||
return
|
||||
}
|
||||
|
||||
// Close the reader when it implements io.Closer
|
||||
func (r *Reader) Close() (err error) {
|
||||
if closer, ok := r.Reader.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package pb
|
||||
|
||||
import (
|
||||
"github.com/mattn/go-runewidth"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Finds the control character sequences (like colors)
|
||||
var ctrlFinder = regexp.MustCompile("\x1b\x5b[0-9]+\x6d")
|
||||
|
||||
func escapeAwareRuneCountInString(s string) int {
|
||||
n := runewidth.StringWidth(s)
|
||||
for _, sm := range ctrlFinder.FindAllString(s, -1) {
|
||||
n -= runewidth.StringWidth(sm)
|
||||
}
|
||||
return n
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// +build darwin freebsd netbsd openbsd dragonfly
|
||||
// +build !appengine
|
||||
|
||||
package pb
|
||||
|
||||
import "syscall"
|
||||
|
||||
const ioctlReadTermios = syscall.TIOCGETA
|
||||
const ioctlWriteTermios = syscall.TIOCSETA
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux solaris
|
||||
// +build !appengine
|
||||
|
||||
package pb
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
const ioctlReadTermios = unix.TCGETS
|
||||
const ioctlWriteTermios = unix.TCSETS
|
|
@ -585,6 +585,14 @@
|
|||
"path": "github.com/biogo/hts/bgzf",
|
||||
"revision": "50da7d4131a3b5c9d063932461cab4d1fafb20b0"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "7GMQgpfoSQv4QcaREE2GzSZvlQI=",
|
||||
"path": "github.com/cheggaaa/pb",
|
||||
"revision": "2af8bbdea9e99e83b3ac400d8f6b6d1b8cbbf338",
|
||||
"revisionTime": "2018-05-21T09:56:06Z",
|
||||
"version": "v1.0.25",
|
||||
"versionExact": "v1.0.25"
|
||||
},
|
||||
{
|
||||
"checksumSHA1": "X2/71FBrn4pA3WcA620ySVO0uHU=",
|
||||
"path": "github.com/creack/goselect",
|
||||
|
|
Loading…
Reference in New Issue