/
progress.go
384 lines (319 loc) · 12.6 KB
/
progress.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
package progress
import (
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/ActiveState/cli/internal/multilog"
"github.com/go-openapi/strfmt"
"github.com/vbauerster/mpb/v7"
"golang.org/x/net/context"
"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/locale"
"github.com/ActiveState/cli/internal/logging"
"github.com/ActiveState/cli/internal/output"
"github.com/ActiveState/cli/pkg/platform/runtime/artifact"
"github.com/ActiveState/cli/pkg/platform/runtime/setup/events"
)
type step struct {
name string
verb string
priority int
}
func (s step) String() string {
return s.name
}
var (
StepBuild = step{"build", locale.T("building"), 10000} // the priority is high because the artifact progress bars need to fit in between the steps
StepDownload = step{"download", locale.T("downloading"), 20000}
StepInstall = step{"install", locale.T("installing"), 30000}
)
type artifactStepID string
type artifactStep struct {
artifactID artifact.ArtifactID
step step
}
func (a artifactStep) ID() artifactStepID {
return artifactStepID(a.artifactID.String() + a.step.String())
}
type ProgressDigester struct {
// The max width to use for the name entries of progress bars
maxNameWidth int
// Progress bars and spinners
mainProgress *mpb.Progress
buildBar *bar
downloadBar *bar
installBar *bar
solveSpinner *output.Spinner
artifactBars map[artifactStepID]*bar
// Artifact name lookup map
artifactNames artifact.Named
// Recipe that we're performing progress for
recipeID strfmt.UUID
// Track the totals required as the bars for these are only initialized for the first artifact received, at which
// time we won't have the totals unless we previously recorded them.
buildsExpected map[artifact.ArtifactID]struct{}
downloadsExpected map[artifact.ArtifactID]struct{}
installsExpected map[artifact.ArtifactID]struct{}
// Debug properties used to reduce the number of log entries generated
dbgEventLog []string
out output.Outputer
// We use a mutex because whilst this package itself doesn't do any threading; its consumers do.
mutex *sync.Mutex
// The cancel function for the mpb package
cancelMpb context.CancelFunc
// Record whether changes were made
changesMade bool
// Record whether the runtime install was successful
success bool
}
func NewProgressIndicator(w io.Writer, out output.Outputer) *ProgressDigester {
ctx, cancel := context.WithCancel(context.Background())
return &ProgressDigester{
mainProgress: mpb.NewWithContext(
ctx,
mpb.WithWidth(progressBarWidth),
mpb.WithOutput(w),
mpb.WithRefreshRate(refreshRate),
),
artifactNames: map[artifact.ArtifactID]string{},
artifactBars: map[artifactStepID]*bar{},
cancelMpb: cancel,
maxNameWidth: MaxNameWidth(),
out: out,
mutex: &sync.Mutex{},
}
}
func (p *ProgressDigester) Handle(ev events.Eventer) error {
p.dbgEventLog = append(p.dbgEventLog, fmt.Sprintf("%T", ev))
p.mutex.Lock()
defer p.mutex.Unlock()
initDownloadBar := func() {
if p.downloadBar == nil {
p.downloadBar = p.addTotalBar(locale.Tl("progress_building", "Downloading"), int64(len(p.downloadsExpected)), mpb.BarPriority(StepDownload.priority))
}
}
switch v := ev.(type) {
case events.Start:
logging.Debug("Initialize Event: %#v", v)
// Ensure Start event is first.. because otherwise the prints below will cause output to be malformed.
if p.buildBar != nil || p.downloadBar != nil || p.installBar != nil || p.solveSpinner != nil {
return errs.New("Received Start event after bars were already initialized, event log: %v", p.dbgEventLog)
}
// Report the log file we'll be using. This has to happen here and not in the BuildStarted even as there's no
// guarantee that no downloads or installs might have triggered before BuildStarted, in which case there's
// already progressbars being displayed which won't play nice with newly printed output.
if v.RequiresBuild {
p.out.Notice(locale.Tr("progress_build_log", v.LogFilePath))
}
p.recipeID = v.RecipeID
p.artifactNames = v.ArtifactNames
p.buildsExpected = artifact.ArtifactIDsToMap(v.ArtifactsToBuild)
p.downloadsExpected = artifact.ArtifactIDsToMap(v.ArtifactsToDownload)
p.installsExpected = artifact.ArtifactIDsToMap(v.ArtifactsToInstall)
if len(v.ArtifactsToBuild)+len(v.ArtifactsToDownload)+len(v.ArtifactsToInstall) == 0 {
p.out.Notice(locale.T("progress_nothing_to_do"))
} else {
p.changesMade = true
}
case events.Success:
p.success = true
case events.SolveStart:
p.out.Notice(locale.T("setup_runtime"))
p.solveSpinner = output.StartSpinner(p.out, locale.T("progress_solve"), refreshRate)
case events.SolveError:
if p.solveSpinner == nil {
return errs.New("SolveError called before solveBar was initialized")
}
p.solveSpinner.Stop(locale.T("progress_fail"))
p.solveSpinner = nil
case events.SolveSuccess:
if p.solveSpinner == nil {
return errs.New("SolveSuccess called before solveBar was initialized")
}
p.solveSpinner.Stop(locale.T("progress_success"))
p.solveSpinner = nil
case events.BuildSkipped:
if p.buildBar != nil {
return errs.New("BuildSkipped called, but buildBar was initialized.. this should not happen as they should be mutually exclusive")
}
case events.BuildStarted:
if p.buildBar != nil {
return errs.New("BuildStarted called after buildbar was already initialized")
}
p.buildBar = p.addTotalBar(locale.Tl("progress_building", "Building"), int64(len(p.buildsExpected)), mpb.BarPriority(StepBuild.priority))
case events.BuildSuccess:
if p.buildBar == nil {
return errs.New("BuildSuccess called before buildbar was initialized")
}
case events.BuildFailure:
if p.buildBar == nil {
return errs.New("BuildFailure called before buildbar was initialized")
}
logging.Debug("BuildFailure called, aborting bars")
p.buildBar.Abort(false) // mpb has been known to stick around after it was told not to
if p.downloadBar != nil {
p.downloadBar.Abort(false)
}
if p.installBar != nil {
p.installBar.Abort(false)
}
case events.ArtifactBuildStarted:
if p.buildBar == nil {
return errs.New("ArtifactBuildStarted called before buildbar was initialized")
}
if _, ok := p.buildsExpected[v.ArtifactID]; !ok {
// This should ideally be a returned error, but because buildlogstreamer still speaks recipes there is a missmatch
// and we can receive events for artifacts we're not interested in as a result.
logging.Debug("ArtifactBuildStarted called for an artifact that was not expected: %s", v.ArtifactID.String())
}
case events.ArtifactBuildSuccess:
if p.buildBar == nil {
return errs.New("ArtifactBuildSuccess called before buildbar was initialized")
}
if _, ok := p.buildsExpected[v.ArtifactID]; !ok {
// This should ideally be a returned error, but because buildlogstreamer still speaks recipes there is a missmatch
// and we can receive events for artifacts we're not interested in as a result.
logging.Debug("ArtifactBuildSuccess called for an artifact that was not expected: %s", v.ArtifactID.String())
return nil
}
if p.buildBar.Current() == p.buildBar.total {
return errs.New("Build bar is already complete, this should not happen")
}
delete(p.buildsExpected, v.ArtifactID)
p.buildBar.Increment()
case events.ArtifactDownloadStarted:
initDownloadBar()
if _, ok := p.downloadsExpected[v.ArtifactID]; !ok {
return errs.New("ArtifactDownloadStarted called for an artifact that was not expected: %s", v.ArtifactID.String())
}
if err := p.addArtifactBar(v.ArtifactID, StepDownload, int64(v.TotalSize), true); err != nil {
return errs.Wrap(err, "Failed to add or update artifact bar")
}
case events.ArtifactDownloadProgress:
if err := p.updateArtifactBar(v.ArtifactID, StepDownload, v.IncrementBySize); err != nil {
return errs.Wrap(err, "Failed to add or update artifact bar")
}
case events.ArtifactDownloadSkipped:
initDownloadBar()
delete(p.downloadsExpected, v.ArtifactID)
p.downloadBar.Increment()
case events.ArtifactDownloadSuccess:
if p.downloadBar == nil {
return errs.New("ArtifactDownloadSuccess called before downloadBar was initialized")
}
if _, ok := p.downloadsExpected[v.ArtifactID]; !ok {
return errs.New("ArtifactDownloadSuccess called for an artifact that was not expected: %s", v.ArtifactID.String())
}
if err := p.dropArtifactBar(v.ArtifactID, StepDownload); err != nil {
return errs.Wrap(err, "Failed to drop install bar")
}
if p.downloadBar.Current() == p.downloadBar.total {
return errs.New("Download bar is already complete, this should not happen")
}
delete(p.downloadsExpected, v.ArtifactID)
p.downloadBar.Increment()
case events.ArtifactInstallStarted:
if p.installBar == nil {
p.installBar = p.addTotalBar(locale.Tl("progress_building", "Installing"), int64(len(p.installsExpected)), mpb.BarPriority(StepInstall.priority))
}
if _, ok := p.installsExpected[v.ArtifactID]; !ok {
return errs.New("ArtifactInstallStarted called for an artifact that was not expected: %s", v.ArtifactID.String())
}
if err := p.addArtifactBar(v.ArtifactID, StepInstall, int64(v.TotalSize), true); err != nil {
return errs.Wrap(err, "Failed to add or update artifact bar")
}
case events.ArtifactInstallSkipped:
if p.installBar == nil {
return errs.New("ArtifactInstallSkipped called before installBar was initialized, artifact ID: %s", v.ArtifactID.String())
}
delete(p.installsExpected, v.ArtifactID)
p.installBar.Increment()
case events.ArtifactInstallSuccess:
if p.installBar == nil {
return errs.New("ArtifactInstall[Skipped|Success] called before installBar was initialized")
}
if _, ok := p.installsExpected[v.ArtifactID]; !ok {
return errs.New("ArtifactInstallSuccess called for an artifact that was not expected: %s", v.ArtifactID.String())
}
if err := p.dropArtifactBar(v.ArtifactID, StepInstall); err != nil {
return errs.Wrap(err, "Failed to drop install bar")
}
if p.installBar.Current() == p.installBar.total {
return errs.New("Install bar is already complete, this should not happen")
}
delete(p.installsExpected, v.ArtifactID)
p.installBar.Increment()
case events.ArtifactInstallProgress:
if err := p.updateArtifactBar(v.ArtifactID, StepInstall, v.IncrementBySize); err != nil {
return errs.Wrap(err, "Failed to add or update artifact bar")
}
}
return nil
}
func (p *ProgressDigester) Close() error {
mainProgressDone := make(chan struct{}, 1)
go func() {
p.mainProgress.Wait()
mainProgressDone <- struct{}{}
}()
select {
case <-mainProgressDone:
break
// Wait one second, which should be plenty as we're really just waiting for the last frame to render
// If it's not done after 1 second it's unlikely it will ever be and it means it did not receive events in a way
// that we can make sense of.
case <-time.After(time.Second):
p.cancelMpb() // mpb doesn't have a Close, just a Wait. We force it as we don't want to give it the opportunity to block.
// Only if the installation was successful do we want to verify that our progress indication was successful.
// There's no point in doing this if it failed as due to the multithreaded nature the failure can bubble up
// in different ways that are difficult to predict and thus verify.
if p.success {
bars := map[string]*bar{
"build bar": p.buildBar,
"download bar": p.downloadBar,
"install bar": p.installBar,
}
pending := 0
debugMsg := []string{}
for name, bar := range bars {
debugMsg = append(debugMsg, fmt.Sprintf("%s is at %v", name, func() string {
if bar == nil {
return "nil"
}
if !bar.Completed() {
pending++
}
return fmt.Sprintf("%d out of %d", bar.Current(), bar.total)
}()))
}
multilog.Error(`
Timed out waiting for progress bars to close. Recipe: %s
Progress bars status:
%s
Still expecting:
- Builds: %v
- Downloads: %v
- Installs: %v`,
p.recipeID.String(),
strings.Join(debugMsg, "\n"),
p.buildsExpected, p.downloadsExpected, p.installsExpected,
)
/* https://activestatef.atlassian.net/browse/DX-1831
if pending > 0 {
// We only error out if we determine the issue is down to one of our bars not completing.
// Otherwise this is an issue with the mpb package which is currently a known limitation, end goal is to get rid of mpb.
return locale.NewError("err_rtprogress_outofsync", "", constants.BugTrackerURL, logging.FilePath())
}
*/
}
}
// Success message. Can't happen in event loop as progressbar lib clears new lines when it closes.
if p.success && p.changesMade {
p.out.Notice(locale.T("progress_completed"))
}
// Blank line to separate progress from rest of output
p.out.Notice("")
return nil
}