Restored image layer download progress details on pull.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
Nicolas De Loof 2025-12-12 09:22:44 +01:00 committed by Guillaume Lours
parent 4f419e5098
commit 02008a0097
3 changed files with 152 additions and 93 deletions

View File

@ -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])
}
}

View File

@ -38,33 +38,35 @@ const (
const ResourceCompose = "Compose"
const (
StatusError = "Error"
StatusCreating = "Creating"
StatusStarting = "Starting"
StatusStarted = "Started"
StatusWaiting = "Waiting"
StatusHealthy = "Healthy"
StatusExited = "Exited"
StatusRestarting = "Restarting"
StatusRestarted = "Restarted"
StatusRunning = "Running"
StatusCreated = "Created"
StatusStopping = "Stopping"
StatusStopped = "Stopped"
StatusKilling = "Killing"
StatusKilled = "Killed"
StatusRemoving = "Removing"
StatusRemoved = "Removed"
StatusBuilding = "Building"
StatusBuilt = "Built"
StatusPulling = "Pulling"
StatusPulled = "Pulled"
StatusCommitting = "Committing"
StatusCommitted = "Committed"
StatusCopying = "Copying"
StatusCopied = "Copied"
StatusExporting = "Exporting"
StatusExported = "Exported"
StatusError = "Error"
StatusCreating = "Creating"
StatusStarting = "Starting"
StatusStarted = "Started"
StatusWaiting = "Waiting"
StatusHealthy = "Healthy"
StatusExited = "Exited"
StatusRestarting = "Restarting"
StatusRestarted = "Restarted"
StatusRunning = "Running"
StatusCreated = "Created"
StatusStopping = "Stopping"
StatusStopped = "Stopped"
StatusKilling = "Killing"
StatusKilled = "Killed"
StatusRemoving = "Removing"
StatusRemoved = "Removed"
StatusBuilding = "Building"
StatusBuilt = "Built"
StatusPulling = "Pulling"
StatusPulled = "Pulled"
StatusCommitting = "Committing"
StatusCommitted = "Committed"
StatusCopying = "Copying"
StatusCopied = "Copied"
StatusExporting = "Exporting"
StatusExported = "Exported"
StatusDownloading = "Downloading"
StatusDownloadComplete = "Download complete"
)
// Resource represents status change and progress for a compose resource.

View File

@ -398,14 +398,14 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events api.E
}
var (
text string
total int64
percent int
current int64
status = api.Working
progress string
total int64
percent int
current int64
status = api.Working
)
text = jm.Progress.String()
progress = jm.Progress.String()
switch jm.Status {
case PreparingPhase, WaitingPhase, PullingFsPhase:
@ -431,7 +431,7 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events api.E
if jm.Error != nil {
status = api.Error
text = jm.Error.Message
progress = jm.Error.Message
}
events.On(api.Resource{
@ -441,6 +441,7 @@ func toPullProgressEvent(parent string, jm jsonmessage.JSONMessage, events api.E
Total: total,
Percent: percent,
Status: status,
Text: text,
Text: jm.Status,
Details: progress,
})
}