buildkit/util/progress/progressui/display.go

311 lines
6.1 KiB
Go

package progressui
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/containerd/console"
"github.com/docker/go-units"
"github.com/moby/buildkit/client"
"github.com/morikuni/aec"
digest "github.com/opencontainers/go-digest"
"golang.org/x/time/rate"
)
func DisplaySolveStatus(ctx context.Context, ch chan *client.SolveStatus) error {
c, err := console.ConsoleFromFile(os.Stdout)
if err != nil {
return err // TODO: switch to log mode
}
disp := &display{c: c}
t := newTrace()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
displayLimiter := rate.NewLimiter(rate.Every(70*time.Millisecond), 1)
var done bool
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
case ss, ok := <-ch:
if ok {
t.update(ss)
} else {
done = true
}
}
if done {
disp.print(t.displayInfo(), true)
t.printErrorLogs()
return nil
} else if displayLimiter.Allow() {
disp.print(t.displayInfo(), false)
}
}
}
type displayInfo struct {
startTime time.Time
jobs []job
countTotal int
countCompleted int
}
type job struct {
startTime *time.Time
completedTime *time.Time
name string
status string
hasError bool
isCanceled bool
}
type trace struct {
localTimeDiff time.Duration
vertexes []*vertex
byDigest map[digest.Digest]*vertex
}
type vertex struct {
*client.Vertex
statuses []*status
byID map[string]*status
logs []*client.VertexLog
}
type status struct {
*client.VertexStatus
}
func newTrace() *trace {
return &trace{
byDigest: make(map[digest.Digest]*vertex),
}
}
func (t *trace) update(s *client.SolveStatus) {
for _, v := range s.Vertexes {
prev, ok := t.byDigest[v.Digest]
if !ok {
t.byDigest[v.Digest] = &vertex{
byID: make(map[string]*status),
}
}
if v.Started != nil && (prev == nil || prev.Started == nil) {
if t.localTimeDiff == 0 {
t.localTimeDiff = time.Since(*v.Started)
}
t.vertexes = append(t.vertexes, t.byDigest[v.Digest])
}
t.byDigest[v.Digest].Vertex = v
}
for _, s := range s.Statuses {
v, ok := t.byDigest[s.Vertex]
if !ok {
continue // shouldn't happen
}
prev, ok := v.byID[s.ID]
if !ok {
v.byID[s.ID] = &status{VertexStatus: s}
}
if s.Started != nil && (prev == nil || prev.Started == nil) {
v.statuses = append(v.statuses, v.byID[s.ID])
}
v.byID[s.ID].VertexStatus = s
}
for _, l := range s.Logs {
v, ok := t.byDigest[l.Vertex]
if !ok {
continue // shouldn't happen
}
v.logs = append(v.logs, l)
}
}
func (t *trace) printErrorLogs() {
for _, v := range t.vertexes {
if v.Error != "" && !strings.HasSuffix(v.Error, context.Canceled.Error()) {
fmt.Println("------")
fmt.Printf(" > %s:\n", v.Name)
for _, l := range v.logs {
switch l.Stream {
case 1:
os.Stdout.Write(l.Data)
case 2:
os.Stderr.Write(l.Data)
}
}
fmt.Println("------")
}
}
}
func (t *trace) displayInfo() (d displayInfo) {
d.startTime = time.Now()
if t.localTimeDiff != 0 {
d.startTime = (*t.vertexes[0].Started).Add(t.localTimeDiff)
}
d.countTotal = len(t.byDigest)
for _, v := range t.byDigest {
if v.Completed != nil {
d.countCompleted++
}
}
for _, v := range t.vertexes {
j := job{
startTime: addTime(v.Started, t.localTimeDiff),
completedTime: addTime(v.Completed, t.localTimeDiff),
name: v.Name,
}
if v.Error != "" {
if strings.HasSuffix(v.Error, context.Canceled.Error()) {
j.isCanceled = true
j.name = "CANCELED " + j.name
} else {
j.hasError = true
j.name = "ERROR " + j.name
}
}
if v.Cached {
j.name = "CACHED " + j.name
}
d.jobs = append(d.jobs, j)
for _, s := range v.statuses {
j := job{
startTime: addTime(s.Started, t.localTimeDiff),
completedTime: addTime(s.Completed, t.localTimeDiff),
name: "=> " + s.ID,
}
if s.Total != 0 {
j.status = units.HumanSize(float64(s.Current)) + " / " + units.HumanSize(float64(s.Total))
}
d.jobs = append(d.jobs, j)
}
}
return d
}
func addTime(tm *time.Time, d time.Duration) *time.Time {
if tm == nil {
return nil
}
t := (*tm).Add(d)
return &t
}
type display struct {
c console.Console
lineCount int
repeated bool
}
func (disp *display) print(d displayInfo, all bool) {
// this output is inspired by Buck
width := 80
height := 10
size, err := disp.c.Size()
if err == nil {
width = int(size.Width)
height = int(size.Height)
}
if !all {
d.jobs = wrapHeight(d.jobs, height-2)
}
b := aec.EmptyBuilder
for i := 0; i <= disp.lineCount; i++ {
b = b.Up(1)
}
if !disp.repeated {
b = b.Down(1)
}
disp.repeated = true
fmt.Print(b.Column(0).ANSI)
statusStr := ""
if d.countCompleted > 0 && d.countCompleted == d.countTotal {
statusStr = "FINISHED"
}
fmt.Print(aec.Hide)
defer fmt.Print(aec.Show)
out := fmt.Sprintf("[+] Building %.1fs (%d/%d) %s", time.Since(d.startTime).Seconds(), d.countCompleted, d.countTotal, statusStr)
out = align(out, "", width)
fmt.Println(out)
lineCount := 0
for _, j := range d.jobs {
endTime := time.Now()
if j.completedTime != nil {
endTime = *j.completedTime
}
if j.startTime == nil {
continue
}
dt := endTime.Sub(*j.startTime).Seconds()
if dt < 0.05 {
dt = 0
}
pfx := " => "
timer := fmt.Sprintf(" %.1fs\n", dt)
status := j.status
showStatus := false
left := width - len(pfx) - len(timer) - 1
if status != "" {
if left+len(status) > 20 {
showStatus = true
left -= len(status) + 1
}
}
if left < 12 { // too small screen to show progress
continue
}
if len(j.name) > left {
j.name = j.name[:left]
}
out := pfx + j.name
if showStatus {
out += " " + status
}
out = align(out, timer, width)
if j.completedTime != nil {
color := aec.BlueF
if j.isCanceled {
color = aec.YellowF
} else if j.hasError {
color = aec.RedF
}
out = aec.Apply(out, color)
}
fmt.Print(out)
lineCount++
}
disp.lineCount = lineCount
}
func align(l, r string, w int) string {
return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
}
func wrapHeight(j []job, limit int) []job {
if len(j) > limit {
j = j[len(j)-limit:]
}
return j
}