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 }