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

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