|
|
@@ -20,11 +20,13 @@ import (
|
|
|
"context"
|
|
|
"fmt"
|
|
|
"io"
|
|
|
+ "iter"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
"time"
|
|
|
|
|
|
"github.com/buger/goterm"
|
|
|
+ "github.com/docker/compose/v5/pkg/utils"
|
|
|
"github.com/docker/go-units"
|
|
|
"github.com/morikuni/aec"
|
|
|
|
|
|
@@ -60,7 +62,8 @@ type ttyWriter struct {
|
|
|
|
|
|
type task struct {
|
|
|
ID string
|
|
|
- parentID string
|
|
|
+ parent string // the resource this task receives updates from - other parents will be ignored
|
|
|
+ parents utils.Set[string] // all resources to depend on this task
|
|
|
startTime time.Time
|
|
|
endTime time.Time
|
|
|
text string
|
|
|
@@ -72,6 +75,64 @@ type task struct {
|
|
|
spinner *Spinner
|
|
|
}
|
|
|
|
|
|
+func newTask(e api.Resource) task {
|
|
|
+ t := task{
|
|
|
+ ID: e.ID,
|
|
|
+ parents: utils.NewSet[string](),
|
|
|
+ startTime: time.Now(),
|
|
|
+ text: e.Text,
|
|
|
+ details: e.Details,
|
|
|
+ status: e.Status,
|
|
|
+ current: e.Current,
|
|
|
+ percent: e.Percent,
|
|
|
+ total: e.Total,
|
|
|
+ spinner: NewSpinner(),
|
|
|
+ }
|
|
|
+ if e.ParentID != "" {
|
|
|
+ t.parent = e.ParentID
|
|
|
+ t.parents.Add(e.ParentID)
|
|
|
+ }
|
|
|
+ if e.Status == api.Done || e.Status == api.Error {
|
|
|
+ t.stop()
|
|
|
+ }
|
|
|
+ return t
|
|
|
+}
|
|
|
+
|
|
|
+// update adjusts task state based on last received event
|
|
|
+func (t *task) update(e api.Resource) {
|
|
|
+ if e.ParentID != "" {
|
|
|
+ t.parents.Add(e.ParentID)
|
|
|
+ // we may receive same event from distinct parents (typically: images sharing layers)
|
|
|
+ // to avoid status to flicker, only accept updates from our first declared parent
|
|
|
+ if t.parent != e.ParentID {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // update task based on received event
|
|
|
+ switch e.Status {
|
|
|
+ case api.Done, api.Error, api.Warning:
|
|
|
+ if t.status != e.Status {
|
|
|
+ t.stop()
|
|
|
+ }
|
|
|
+ case api.Working:
|
|
|
+ t.hasMore()
|
|
|
+ }
|
|
|
+ t.status = e.Status
|
|
|
+ t.text = e.Text
|
|
|
+ t.details = e.Details
|
|
|
+ // progress can only go up
|
|
|
+ if e.Total > t.total {
|
|
|
+ t.total = e.Total
|
|
|
+ }
|
|
|
+ if e.Current > t.current {
|
|
|
+ t.current = e.Current
|
|
|
+ }
|
|
|
+ if e.Percent > t.percent {
|
|
|
+ t.percent = e.Percent
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
func (t *task) stop() {
|
|
|
t.endTime = time.Now()
|
|
|
t.spinner.Stop()
|
|
|
@@ -81,6 +142,15 @@ func (t *task) hasMore() {
|
|
|
t.spinner.Restart()
|
|
|
}
|
|
|
|
|
|
+func (t *task) Completed() bool {
|
|
|
+ switch t.status {
|
|
|
+ case api.Done, api.Error, api.Warning:
|
|
|
+ return true
|
|
|
+ default:
|
|
|
+ return false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
func (w *ttyWriter) Start(ctx context.Context, operation string) {
|
|
|
w.ticker = time.NewTicker(100 * time.Millisecond)
|
|
|
w.operation = operation
|
|
|
@@ -137,48 +207,10 @@ func (w *ttyWriter) event(e api.Resource) {
|
|
|
}
|
|
|
|
|
|
if last, ok := w.tasks[e.ID]; ok {
|
|
|
- switch e.Status {
|
|
|
- case api.Done, api.Error, api.Warning:
|
|
|
- if last.status != e.Status {
|
|
|
- last.stop()
|
|
|
- }
|
|
|
- case api.Working:
|
|
|
- last.hasMore()
|
|
|
- }
|
|
|
- last.status = e.Status
|
|
|
- last.text = e.Text
|
|
|
- last.details = e.Details
|
|
|
- // progress can only go up
|
|
|
- if e.Total > last.total {
|
|
|
- last.total = e.Total
|
|
|
- }
|
|
|
- if e.Current > last.current {
|
|
|
- last.current = e.Current
|
|
|
- }
|
|
|
- if e.Percent > last.percent {
|
|
|
- last.percent = e.Percent
|
|
|
- }
|
|
|
- // allow set/unset of parent, but not swapping otherwise prompt is flickering
|
|
|
- if last.parentID == "" || e.ParentID == "" {
|
|
|
- last.parentID = e.ParentID
|
|
|
- }
|
|
|
+ last.update(e)
|
|
|
w.tasks[e.ID] = last
|
|
|
} else {
|
|
|
- t := task{
|
|
|
- ID: e.ID,
|
|
|
- parentID: e.ParentID,
|
|
|
- startTime: time.Now(),
|
|
|
- text: e.Text,
|
|
|
- details: e.Details,
|
|
|
- status: e.Status,
|
|
|
- current: e.Current,
|
|
|
- percent: e.Percent,
|
|
|
- total: e.Total,
|
|
|
- spinner: NewSpinner(),
|
|
|
- }
|
|
|
- if e.Status == api.Done || e.Status == api.Error {
|
|
|
- t.stop()
|
|
|
- }
|
|
|
+ t := newTask(e)
|
|
|
w.tasks[e.ID] = t
|
|
|
w.ids = append(w.ids, e.ID)
|
|
|
}
|
|
|
@@ -205,6 +237,28 @@ func (w *ttyWriter) printEvent(e api.Resource) {
|
|
|
_, _ = fmt.Fprintf(w.out, "%s %s %s\n", e.ID, color(e.Text), e.Details)
|
|
|
}
|
|
|
|
|
|
+func (w *ttyWriter) parentTasks() iter.Seq[task] {
|
|
|
+ return func(yield func(task) bool) {
|
|
|
+ for _, id := range w.ids { // iterate on ids to enforce a consistent order
|
|
|
+ t := w.tasks[id]
|
|
|
+ if len(t.parents) == 0 {
|
|
|
+ yield(t)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (w *ttyWriter) childrenTasks(parent string) iter.Seq[task] {
|
|
|
+ return func(yield func(task) bool) {
|
|
|
+ for _, id := range w.ids { // iterate on ids to enforce a consistent order
|
|
|
+ t := w.tasks[id]
|
|
|
+ if t.parents.Has(parent) {
|
|
|
+ yield(t)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
func (w *ttyWriter) print() {
|
|
|
w.mtx.Lock()
|
|
|
defer w.mtx.Unlock()
|
|
|
@@ -233,20 +287,25 @@ func (w *ttyWriter) print() {
|
|
|
var statusPadding int
|
|
|
for _, t := range w.tasks {
|
|
|
l := len(t.ID)
|
|
|
- if t.parentID == "" && statusPadding < l {
|
|
|
+ if len(t.parents) == 0 && statusPadding < l {
|
|
|
statusPadding = l
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ skipChildEvents := len(w.tasks) > goterm.Height()-2
|
|
|
numLines := 0
|
|
|
- for _, id := range w.ids { // iterate on ids to enforce a consistent order
|
|
|
- t := w.tasks[id]
|
|
|
- if t.parentID != "" {
|
|
|
- continue
|
|
|
- }
|
|
|
+ for t := range w.parentTasks() {
|
|
|
line := w.lineText(t, "", terminalWidth, statusPadding, w.dryRun)
|
|
|
_, _ = fmt.Fprint(w.out, line)
|
|
|
numLines++
|
|
|
+ if skipChildEvents {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ for child := range w.childrenTasks(t.ID) {
|
|
|
+ line := w.lineText(child, " ", terminalWidth, statusPadding-2, w.dryRun)
|
|
|
+ _, _ = fmt.Fprint(w.out, line)
|
|
|
+ numLines++
|
|
|
+ }
|
|
|
}
|
|
|
for i := numLines; i < w.numLines; i++ {
|
|
|
if numLines < goterm.Height()-2 {
|
|
|
@@ -281,18 +340,15 @@ func (w *ttyWriter) lineText(t task, pad string, terminalWidth, statusPadding in
|
|
|
|
|
|
// only show the aggregated progress while the root operation is in-progress
|
|
|
if parent := t; parent.status == api.Working {
|
|
|
- for _, id := range w.ids {
|
|
|
- child := w.tasks[id]
|
|
|
- if child.parentID == parent.ID {
|
|
|
- if child.status == api.Working && child.total == 0 {
|
|
|
- // we don't have totals available for all the child events
|
|
|
- // so don't show the total progress yet
|
|
|
- hideDetails = true
|
|
|
- }
|
|
|
- total += child.total
|
|
|
- current += child.current
|
|
|
- completion = append(completion, percentChars[(len(percentChars)-1)*child.percent/100])
|
|
|
+ for child := range w.childrenTasks(parent.ID) {
|
|
|
+ if child.status == api.Working && child.total == 0 {
|
|
|
+ // we don't have totals available for all the child events
|
|
|
+ // so don't show the total progress yet
|
|
|
+ hideDetails = true
|
|
|
}
|
|
|
+ total += child.total
|
|
|
+ current += child.current
|
|
|
+ completion = append(completion, percentChars[(len(percentChars)-1)*child.percent/100])
|
|
|
}
|
|
|
}
|
|
|
|