github.com/jfrog/jfrog-cli-core/v2@v2.51.0/utils/progressbar/progressbarmng.go (about)

     1  package progressbar
     2  
     3  import (
     4  	golangLog "log"
     5  	"math"
     6  	"os"
     7  	"strconv"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/gookit/color"
    13  	artifactoryutils "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
    14  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    15  	corelog "github.com/jfrog/jfrog-cli-core/v2/utils/log"
    16  	"github.com/jfrog/jfrog-client-go/utils"
    17  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    18  	"github.com/jfrog/jfrog-client-go/utils/log"
    19  	"github.com/vbauerster/mpb/v7"
    20  	"github.com/vbauerster/mpb/v7/decor"
    21  	"golang.org/x/term"
    22  )
    23  
    24  const (
    25  	ProgressBarWidth     = 20
    26  	longProgressBarWidth = 100
    27  	ProgressRefreshRate  = 200 * time.Millisecond
    28  )
    29  
    30  type Color int64
    31  
    32  const (
    33  	WHITE Color = iota
    34  	GREEN       = 1
    35  )
    36  
    37  var terminalWidth int
    38  
    39  type ProgressBarMng struct {
    40  	// A container of all external mpb bar objects to be displayed.
    41  	container *mpb.Progress
    42  	// A synchronization lock object.
    43  	barsRWMutex sync.RWMutex
    44  	// A wait group for all progress bars.
    45  	barsWg *sync.WaitGroup
    46  	// The log file
    47  	logFile *os.File
    48  }
    49  
    50  func NewBarsMng() (mng *ProgressBarMng, shouldInit bool, err error) {
    51  	// Determine whether the progress bar should be displayed or not
    52  	shouldInit, err = ShouldInitProgressBar()
    53  	if !shouldInit || err != nil {
    54  		return
    55  	}
    56  	mng = &ProgressBarMng{}
    57  	// Init log file
    58  	mng.logFile, err = corelog.CreateLogFile()
    59  	if err != nil {
    60  		return
    61  	}
    62  	log.Info("Log path:", mng.logFile.Name())
    63  	log.SetLogger(log.NewLoggerWithFlags(corelog.GetCliLogLevel(), mng.logFile, golangLog.Ldate|golangLog.Ltime|golangLog.Lmsgprefix))
    64  
    65  	mng.barsWg = new(sync.WaitGroup)
    66  	mng.container = mpb.New(
    67  		mpb.WithOutput(os.Stderr),
    68  		mpb.WithWidth(longProgressBarWidth),
    69  		mpb.WithWaitGroup(mng.barsWg),
    70  		mpb.WithRefreshRate(ProgressRefreshRate))
    71  	return
    72  }
    73  
    74  // Initializing a new Tasks with headline progress bar
    75  // Initialize a progress bar that can show the status of two different values, and a headline above it
    76  func (bm *ProgressBarMng) newDoubleHeadLineProgressBar(headline, val1HeadLine, val2HeadLine string, getVal func() (firstNumerator, firstDenominator, secondNumerator, secondDenominator *int64, err error)) *TasksWithHeadlineProg {
    77  	bm.barsWg.Add(1)
    78  	prog := TasksWithHeadlineProg{}
    79  	prog.headlineBar = bm.NewHeadlineBar(headline)
    80  	prog.tasksProgressBar = bm.newDoubleValueProgressBar(getVal, val1HeadLine, val2HeadLine)
    81  	prog.emptyLine = bm.NewHeadlineBar("")
    82  
    83  	return &prog
    84  }
    85  
    86  // Initialize a progress bar that can show the status of two different values
    87  func (bm *ProgressBarMng) newDoubleValueProgressBar(getVal func() (firstNumerator, firstDenominator, secondNumerator, secondDenominator *int64, err error), firstValueLine, secondValueLine string) *TasksProgressBar {
    88  	pb := TasksProgressBar{}
    89  	windows := coreutils.IsWindows()
    90  	padding, filler := paddingAndFiller(windows)
    91  	pb.bar = bm.container.New(0,
    92  		mpb.BarStyle().Lbound("|").Tip(filler).Padding(padding).Filler(filler).Refiller("").Rbound("|"),
    93  		mpb.BarRemoveOnComplete(),
    94  		mpb.AppendDecorators(
    95  			decor.Name(" "+firstValueLine+": "),
    96  			decor.Any(func(statistics decor.Statistics) string {
    97  				firstNumerator, firstDenominator, _, _, err := getVal()
    98  				if err != nil {
    99  					log.Error(err)
   100  				}
   101  				s1 := artifactoryutils.ConvertIntToStorageSizeString(*firstNumerator)
   102  				s2 := artifactoryutils.ConvertIntToStorageSizeString(*firstDenominator)
   103  				return color.Green.Render(s1 + "/" + s2)
   104  			}), decor.Name(" "+secondValueLine+": "), decor.Any(func(statistics decor.Statistics) string {
   105  				_, _, secondNumerator, secondDenominator, err := getVal()
   106  				if err != nil {
   107  					log.Error(err)
   108  				}
   109  				s1 := strconv.Itoa(int(*secondNumerator))
   110  				s2 := strconv.Itoa(int(*secondDenominator))
   111  				return color.Green.Render(s1 + "/" + s2)
   112  			}),
   113  		),
   114  	)
   115  	return &pb
   116  }
   117  
   118  // Initialize a regular tasks progress bar, with a headline above it
   119  func (bm *ProgressBarMng) newHeadlineTaskProgressBar(getVal func() (numerator, denominator *int64), headLine, valHeadLine string) *TasksWithHeadlineProg {
   120  	bm.barsWg.Add(1)
   121  	prog := TasksWithHeadlineProg{}
   122  	prog.headlineBar = bm.NewHeadlineBar(headLine)
   123  	prog.tasksProgressBar = bm.newTasksProgressBar(getVal, valHeadLine)
   124  	prog.emptyLine = bm.NewHeadlineBar("")
   125  	return &prog
   126  }
   127  
   128  // Initialize a regular tasks progress bar, with a headline above it
   129  func (bm *ProgressBarMng) NewTasksWithHeadlineProgressBar(totalTasks int64, headline string, spinner bool, windows bool, taskType string) *TasksWithHeadlineProg {
   130  	bm.barsWg.Add(1)
   131  	prog := TasksWithHeadlineProg{}
   132  	if spinner {
   133  		prog.headlineBar = bm.NewHeadlineBarWithSpinner(headline)
   134  	} else {
   135  		prog.headlineBar = bm.NewHeadlineBar(headline)
   136  	}
   137  	// If totalTasks is 0 - phase is already finished in previous run.
   138  	if totalTasks == 0 {
   139  		prog.tasksProgressBar = bm.newDoneTasksProgressBar()
   140  	} else {
   141  		prog.tasksProgressBar = bm.NewTasksProgressBar(totalTasks, windows, taskType)
   142  	}
   143  	prog.emptyLine = bm.NewHeadlineBar("")
   144  	return &prog
   145  }
   146  
   147  func (bm *ProgressBarMng) QuitTasksWithHeadlineProgressBar(prog *TasksWithHeadlineProg) {
   148  	prog.headlineBar.Abort(true)
   149  	prog.headlineBar = nil
   150  	prog.tasksProgressBar.bar.Abort(true)
   151  	prog.tasksProgressBar = nil
   152  	prog.emptyLine.Abort(true)
   153  	prog.emptyLine = nil
   154  	bm.barsWg.Done()
   155  }
   156  
   157  // NewHeadlineBar Initializes a new progress bar for headline, with an optional spinner
   158  func (bm *ProgressBarMng) NewHeadlineBarWithSpinner(msg string) *mpb.Bar {
   159  	return bm.container.New(1,
   160  		mpb.SpinnerStyle("∙∙∙∙∙∙", "●∙∙∙∙∙", "∙●∙∙∙∙", "∙∙●∙∙∙", "∙∙∙●∙∙", "∙∙∙∙●∙", "∙∙∙∙∙●", "∙∙∙∙∙∙").PositionLeft(),
   161  		mpb.BarRemoveOnComplete(),
   162  		mpb.PrependDecorators(
   163  			decor.Name(msg),
   164  		),
   165  	)
   166  }
   167  
   168  func (bm *ProgressBarMng) NewUpdatableHeadlineBarWithSpinner(updateFn func() string) *mpb.Bar {
   169  	return bm.container.New(1,
   170  		mpb.SpinnerStyle("∙∙∙∙∙∙", "●∙∙∙∙∙", "∙●∙∙∙∙", "∙∙●∙∙∙", "∙∙∙●∙∙", "∙∙∙∙●∙", "∙∙∙∙∙●", "∙∙∙∙∙∙").PositionLeft(),
   171  		mpb.BarRemoveOnComplete(),
   172  		mpb.PrependDecorators(
   173  			decor.Any(func(statistics decor.Statistics) string {
   174  				return updateFn()
   175  			}),
   176  		),
   177  	)
   178  }
   179  
   180  func (bm *ProgressBarMng) NewHeadlineBar(msg string) *mpb.Bar {
   181  	return bm.container.Add(1,
   182  		nil,
   183  		mpb.BarRemoveOnComplete(),
   184  		mpb.PrependDecorators(
   185  			decor.Name(msg),
   186  		),
   187  	)
   188  }
   189  
   190  // Increment increments completed tasks count by 1.
   191  func (bm *ProgressBarMng) Increment(prog *TasksWithHeadlineProg) {
   192  	bm.barsRWMutex.RLock()
   193  	defer bm.barsRWMutex.RUnlock()
   194  	prog.tasksProgressBar.bar.Increment()
   195  	prog.tasksProgressBar.tasksCount++
   196  }
   197  
   198  // Increment increments completed tasks count by n.
   199  func (bm *ProgressBarMng) IncBy(n int, prog *TasksWithHeadlineProg) {
   200  	bm.barsRWMutex.RLock()
   201  	defer bm.barsRWMutex.RUnlock()
   202  	prog.tasksProgressBar.bar.IncrBy(n)
   203  	prog.tasksProgressBar.tasksCount += int64(n)
   204  }
   205  
   206  // DoneTask increase tasks counter to the number of totalTasks.
   207  func (bm *ProgressBarMng) DoneTask(prog *TasksWithHeadlineProg) {
   208  	bm.barsRWMutex.RLock()
   209  	defer bm.barsRWMutex.RUnlock()
   210  	diff := prog.tasksProgressBar.total - prog.tasksProgressBar.tasksCount
   211  	// diff is int64, but we can increase the progress up to math.MaxInt in a time
   212  	for ; diff > math.MaxInt; diff -= math.MaxInt {
   213  		prog.tasksProgressBar.bar.IncrBy(math.MaxInt)
   214  	}
   215  	prog.tasksProgressBar.bar.IncrBy(int(diff))
   216  }
   217  
   218  func (bm *ProgressBarMng) NewTasksProgressBar(totalTasks int64, windows bool, taskType string) *TasksProgressBar {
   219  	padding, filler := paddingAndFiller(windows)
   220  	pb := &TasksProgressBar{}
   221  	if taskType == "" {
   222  		taskType = "Tasks"
   223  	}
   224  	pb.bar = bm.container.New(0,
   225  		mpb.BarStyle().Lbound("|").Tip(filler).Padding(padding).Filler(filler).Refiller("").Rbound("|"),
   226  		mpb.BarRemoveOnComplete(),
   227  		mpb.AppendDecorators(
   228  			decor.Name(" "+taskType+": "),
   229  			decor.CountersNoUnit(getRenderedFormattedCounters("%d")),
   230  		),
   231  	)
   232  	pb.IncGeneralProgressTotalBy(totalTasks)
   233  	return pb
   234  }
   235  
   236  func (bm *ProgressBarMng) newTasksProgressBar(getVal func() (numerator, denominator *int64), headLine string) *TasksProgressBar {
   237  	padding, filler := paddingAndFiller(coreutils.IsWindows())
   238  	pb := &TasksProgressBar{}
   239  	numerator, denominator := getVal()
   240  	pb.bar = bm.container.New(0,
   241  		mpb.BarStyle().Lbound("|").Tip(filler).Padding(padding).Filler(filler).Refiller("").Rbound("|"),
   242  		mpb.BarRemoveOnComplete(),
   243  		mpb.AppendDecorators(
   244  			decor.Name(" "+headLine+": "),
   245  			decor.Any(func(statistics decor.Statistics) string {
   246  				numeratorString := strconv.Itoa(int(*numerator))
   247  				denominatorString := strconv.Itoa(int(*denominator))
   248  				return color.Green.Render(numeratorString + "/" + denominatorString)
   249  			}),
   250  		),
   251  	)
   252  	return pb
   253  }
   254  
   255  // Initializing a counter progress bar
   256  func (bm *ProgressBarMng) newCounterProgressBar(getVal func() (value int, err error), headLine string, counterDescription decor.Decorator) *TasksProgressBar {
   257  	pb := &TasksProgressBar{}
   258  	pb.bar = bm.container.Add(0,
   259  		nil,
   260  		mpb.BarRemoveOnComplete(),
   261  		mpb.PrependDecorators(
   262  			decor.Name(headLine),
   263  			decor.Any(func(decor.Statistics) string {
   264  				value, err := getVal()
   265  				if err != nil {
   266  					log.Error(err)
   267  				}
   268  				s1 := strconv.Itoa(value)
   269  				return color.Green.Render(s1)
   270  			}),
   271  		),
   272  		mpb.AppendDecorators(counterDescription),
   273  	)
   274  	return pb
   275  }
   276  
   277  // Initializing a progress bar that shows Done status
   278  func (bm *ProgressBarMng) newDoneTasksProgressBar() *TasksProgressBar {
   279  	pb := &TasksProgressBar{}
   280  	pb.bar = bm.container.Add(1,
   281  		nil,
   282  		mpb.BarRemoveOnComplete(),
   283  		mpb.PrependDecorators(
   284  			decor.Name("Done ✅"),
   285  		),
   286  	)
   287  	return pb
   288  }
   289  
   290  func (bm *ProgressBarMng) NewStringProgressBar(headline string, updateFn func() string) *TasksProgressBar {
   291  	pb := &TasksProgressBar{}
   292  	pb.bar = bm.container.Add(1,
   293  		nil,
   294  		mpb.BarRemoveOnComplete(),
   295  		mpb.PrependDecorators(
   296  			decor.Name(headline), decor.Any(func(statistics decor.Statistics) string {
   297  				return updateFn()
   298  			}),
   299  		),
   300  	)
   301  	return pb
   302  }
   303  
   304  func getRenderedFormattedCounters(formatDirective string) string {
   305  	return color.Green.Render(strings.Join([]string{formatDirective, formatDirective}, "/"))
   306  }
   307  
   308  func (bm *ProgressBarMng) GetBarsWg() *sync.WaitGroup {
   309  	return bm.barsWg
   310  }
   311  
   312  func (bm *ProgressBarMng) GetLogFile() *os.File {
   313  	return bm.logFile
   314  }
   315  
   316  func paddingAndFiller(windows bool) (padding, filler string) {
   317  	padding = ".."
   318  	filler = "●"
   319  	if !windows {
   320  		padding = "⬛"
   321  		filler = "🟩"
   322  	}
   323  	return padding, filler
   324  }
   325  
   326  // The ShouldInitProgressBar func is used to determine whether the progress bar should be displayed.
   327  // This default implementation will init the progress bar if the following conditions are met:
   328  // CI == false (or unset) and Stderr is a terminal.
   329  var ShouldInitProgressBar = func() (bool, error) {
   330  	ci, err := utils.GetBoolEnvValue(coreutils.CI, false)
   331  	if ci || err != nil {
   332  		return false, err
   333  	}
   334  	if !log.IsStdErrTerminal() {
   335  		return false, err
   336  	}
   337  	err = setTerminalWidth()
   338  	if err != nil {
   339  		return false, err
   340  	}
   341  	return true, nil
   342  }
   343  
   344  func setTerminalWidth() error {
   345  	width, _, err := term.GetSize(int(os.Stderr.Fd()))
   346  	if err != nil {
   347  		return errorutils.CheckError(err)
   348  	}
   349  	// -5 to avoid edges
   350  	terminalWidth = width - 5
   351  	if terminalWidth <= 0 {
   352  		terminalWidth = 5
   353  	}
   354  	return err
   355  }
   356  
   357  func GetTerminalWidth() int {
   358  	return terminalWidth
   359  }