6e9a3453f7
In the existing code, "diff" has function scope and the value from the previous iteration may be used if it is not reset. This appears to be an oversight. This commit changes its scope to the for loop body. One confusing point is that the cursor movement escape sequences appear to be necessary even if the requested movement is 0. I haven't been able to figure out why this makes a difference. Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
212 lines
6.2 KiB
Go
212 lines
6.2 KiB
Go
package jsonmessage
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/pkg/term"
|
|
"github.com/docker/docker/pkg/timeutils"
|
|
"github.com/docker/docker/pkg/units"
|
|
)
|
|
|
|
// JSONError wraps a concrete Code and Message, `Code` is
|
|
// is a integer error code, `Message` is the error message.
|
|
type JSONError struct {
|
|
Code int `json:"code,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
func (e *JSONError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// JSONProgress describes a Progress. terminalFd is the fd of the current terminal,
|
|
// Start is the initial value for the operation. Current is the current status and
|
|
// value of the progress made towards Total. Total is the end value describing when
|
|
// we made 100% progress for an operation.
|
|
type JSONProgress struct {
|
|
terminalFd uintptr
|
|
Current int64 `json:"current,omitempty"`
|
|
Total int64 `json:"total,omitempty"`
|
|
Start int64 `json:"start,omitempty"`
|
|
}
|
|
|
|
func (p *JSONProgress) String() string {
|
|
var (
|
|
width = 200
|
|
pbBox string
|
|
numbersBox string
|
|
timeLeftBox string
|
|
)
|
|
|
|
ws, err := term.GetWinsize(p.terminalFd)
|
|
if err == nil {
|
|
width = int(ws.Width)
|
|
}
|
|
|
|
if p.Current <= 0 && p.Total <= 0 {
|
|
return ""
|
|
}
|
|
current := units.HumanSize(float64(p.Current))
|
|
if p.Total <= 0 {
|
|
return fmt.Sprintf("%8v", current)
|
|
}
|
|
total := units.HumanSize(float64(p.Total))
|
|
percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
|
|
if percentage > 50 {
|
|
percentage = 50
|
|
}
|
|
if width > 110 {
|
|
// this number can't be negetive gh#7136
|
|
numSpaces := 0
|
|
if 50-percentage > 0 {
|
|
numSpaces = 50 - percentage
|
|
}
|
|
pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
|
|
}
|
|
|
|
numbersBox = fmt.Sprintf("%8v/%v", current, total)
|
|
|
|
if p.Current > p.Total {
|
|
// remove total display if the reported current is wonky.
|
|
numbersBox = fmt.Sprintf("%8v", current)
|
|
}
|
|
|
|
if p.Current > 0 && p.Start > 0 && percentage < 50 {
|
|
fromStart := time.Now().UTC().Sub(time.Unix(p.Start, 0))
|
|
perEntry := fromStart / time.Duration(p.Current)
|
|
left := time.Duration(p.Total-p.Current) * perEntry
|
|
left = (left / time.Second) * time.Second
|
|
|
|
if width > 50 {
|
|
timeLeftBox = " " + left.String()
|
|
}
|
|
}
|
|
return pbBox + numbersBox + timeLeftBox
|
|
}
|
|
|
|
// JSONMessage defines a message struct. It describes
|
|
// the created time, where it from, status, ID of the
|
|
// message. It's used for docker events.
|
|
type JSONMessage struct {
|
|
Stream string `json:"stream,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Progress *JSONProgress `json:"progressDetail,omitempty"`
|
|
ProgressMessage string `json:"progress,omitempty"` //deprecated
|
|
ID string `json:"id,omitempty"`
|
|
From string `json:"from,omitempty"`
|
|
Time int64 `json:"time,omitempty"`
|
|
TimeNano int64 `json:"timeNano,omitempty"`
|
|
Error *JSONError `json:"errorDetail,omitempty"`
|
|
ErrorMessage string `json:"error,omitempty"` //deprecated
|
|
}
|
|
|
|
// Display displays the JSONMessage to `out`. `isTerminal` describes if `out`
|
|
// is a terminal. If this is the case, it will erase the entire current line
|
|
// when dislaying the progressbar.
|
|
func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
|
|
if jm.Error != nil {
|
|
if jm.Error.Code == 401 {
|
|
return fmt.Errorf("Authentication is required.")
|
|
}
|
|
return jm.Error
|
|
}
|
|
var endl string
|
|
if isTerminal && jm.Stream == "" && jm.Progress != nil {
|
|
// <ESC>[2K = erase entire current line
|
|
fmt.Fprintf(out, "%c[2K\r", 27)
|
|
endl = "\r"
|
|
} else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal
|
|
return nil
|
|
}
|
|
if jm.TimeNano != 0 {
|
|
fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(timeutils.RFC3339NanoFixed))
|
|
} else if jm.Time != 0 {
|
|
fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(timeutils.RFC3339NanoFixed))
|
|
}
|
|
if jm.ID != "" {
|
|
fmt.Fprintf(out, "%s: ", jm.ID)
|
|
}
|
|
if jm.From != "" {
|
|
fmt.Fprintf(out, "(from %s) ", jm.From)
|
|
}
|
|
if jm.Progress != nil && isTerminal {
|
|
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl)
|
|
} else if jm.ProgressMessage != "" { //deprecated
|
|
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl)
|
|
} else if jm.Stream != "" {
|
|
fmt.Fprintf(out, "%s%s", jm.Stream, endl)
|
|
} else {
|
|
fmt.Fprintf(out, "%s%s\n", jm.Status, endl)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal`
|
|
// describes if `out` is a terminal. If this is the case, it will print `\n` at the end of
|
|
// each line and move the cursor while displaying.
|
|
func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool) error {
|
|
var (
|
|
dec = json.NewDecoder(in)
|
|
ids = make(map[string]int)
|
|
)
|
|
for {
|
|
diff := 0
|
|
var jm JSONMessage
|
|
if err := dec.Decode(&jm); err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return err
|
|
}
|
|
|
|
if jm.Progress != nil {
|
|
jm.Progress.terminalFd = terminalFd
|
|
}
|
|
if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") {
|
|
line, ok := ids[jm.ID]
|
|
if !ok {
|
|
// NOTE: This approach of using len(id) to
|
|
// figure out the number of lines of history
|
|
// only works as long as we clear the history
|
|
// when we output something that's not
|
|
// accounted for in the map, such as a line
|
|
// with no ID.
|
|
line = len(ids)
|
|
ids[jm.ID] = line
|
|
if isTerminal {
|
|
fmt.Fprintf(out, "\n")
|
|
}
|
|
} else {
|
|
diff = len(ids) - line
|
|
}
|
|
if jm.ID != "" && isTerminal {
|
|
// NOTE: this appears to be necessary even if
|
|
// diff == 0.
|
|
// <ESC>[{diff}A = move cursor up diff rows
|
|
fmt.Fprintf(out, "%c[%dA", 27, diff)
|
|
}
|
|
} else {
|
|
// When outputting something that isn't progress
|
|
// output, clear the history of previous lines. We
|
|
// don't want progress entries from some previous
|
|
// operation to be updated (for example, pull -a
|
|
// with multiple tags).
|
|
ids = make(map[string]int)
|
|
}
|
|
err := jm.Display(out, isTerminal)
|
|
if jm.ID != "" && isTerminal {
|
|
// NOTE: this appears to be necessary even if
|
|
// diff == 0.
|
|
// <ESC>[{diff}B = move cursor down diff rows
|
|
fmt.Fprintf(out, "%c[%dB", 27, diff)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|