| package jsonmessage // import "github.com/docker/docker/pkg/jsonmessage" | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "strings" | |
| "time" | |
| units "github.com/docker/go-units" | |
| "github.com/moby/term" | |
| "github.com/morikuni/aec" | |
| ) | |
| // RFC3339NanoFixed is time.RFC3339Nano with nanoseconds padded using zeros to | |
| // ensure the formatted time isalways the same number of characters. | |
| const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" | |
| // JSONError wraps a concrete Code and Message, Code is | |
| // an 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 message in a JSON stream. | |
| type JSONProgress struct { | |
| // Current is the current status and value of the progress made towards Total. | |
| Current int64 `json:"current,omitempty"` | |
| // Total is the end value describing when we made 100% progress for an operation. | |
| Total int64 `json:"total,omitempty"` | |
| // Start is the initial value for the operation. | |
| Start int64 `json:"start,omitempty"` | |
| // HideCounts. if true, hides the progress count indicator (xB/yB). | |
| HideCounts bool `json:"hidecounts,omitempty"` | |
| // Units is the unit to print for progress. It defaults to "bytes" if empty. | |
| Units string `json:"units,omitempty"` | |
| // terminalFd is the fd of the current terminal, if any. It is used | |
| // to get the terminal width. | |
| terminalFd uintptr | |
| // nowFunc is used to override the current time in tests. | |
| nowFunc func() time.Time | |
| // winSize is used to override the terminal width in tests. | |
| winSize int | |
| } | |
| func (p *JSONProgress) String() string { | |
| var ( | |
| width = p.width() | |
| pbBox string | |
| numbersBox string | |
| timeLeftBox string | |
| ) | |
| if p.Current <= 0 && p.Total <= 0 { | |
| return "" | |
| } | |
| if p.Total <= 0 { | |
| switch p.Units { | |
| case "": | |
| return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current))) | |
| default: | |
| return fmt.Sprintf("%d %s", p.Current, p.Units) | |
| } | |
| } | |
| percentage := int(float64(p.Current)/float64(p.Total)*100) / 2 | |
| if percentage > 50 { | |
| percentage = 50 | |
| } | |
| if width > 110 { | |
| // this number can't be negative gh#7136 | |
| numSpaces := 0 | |
| if 50-percentage > 0 { | |
| numSpaces = 50 - percentage | |
| } | |
| pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)) | |
| } | |
| switch { | |
| case p.HideCounts: | |
| case p.Units == "": // no units, use bytes | |
| current := units.HumanSize(float64(p.Current)) | |
| total := units.HumanSize(float64(p.Total)) | |
| 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) | |
| } | |
| default: | |
| numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units) | |
| if p.Current > p.Total { | |
| // remove total display if the reported current is wonky. | |
| numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units) | |
| } | |
| } | |
| if p.Current > 0 && p.Start > 0 && percentage < 50 { | |
| fromStart := p.now().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 | |
| } | |
| // now returns the current time in UTC, but can be overridden in tests | |
| // by setting JSONProgress.nowFunc to a custom function. | |
| func (p *JSONProgress) now() time.Time { | |
| if p.nowFunc != nil { | |
| return p.nowFunc() | |
| } | |
| return time.Now().UTC() | |
| } | |
| // width returns the current terminal's width, but can be overridden | |
| // in tests by setting JSONProgress.winSize to a non-zero value. | |
| func (p *JSONProgress) width() int { | |
| if p.winSize != 0 { | |
| return p.winSize | |
| } | |
| ws, err := term.GetWinsize(p.terminalFd) | |
| if err == nil { | |
| return int(ws.Width) | |
| } | |
| return 200 | |
| } | |
| // 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 | |
| // Aux contains out-of-band data, such as digests for push signing and image id after building. | |
| Aux *json.RawMessage `json:"aux,omitempty"` | |
| } | |
| func clearLine(out io.Writer) { | |
| eraseMode := aec.EraseModes.All | |
| cl := aec.EraseLine(eraseMode) | |
| fmt.Fprint(out, cl) | |
| } | |
| func cursorUp(out io.Writer, l uint) { | |
| fmt.Fprint(out, aec.Up(l)) | |
| } | |
| func cursorDown(out io.Writer, l uint) { | |
| fmt.Fprint(out, aec.Down(l)) | |
| } | |
| // Display prints the JSONMessage to out. If isTerminal is true, it erases | |
| // the entire current line when displaying the progressbar. It returns an | |
| // error if the [JSONMessage.Error] field is non-nil. | |
| func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error { | |
| if jm.Error != nil { | |
| return jm.Error | |
| } | |
| var endl string | |
| if isTerminal && jm.Stream == "" && jm.Progress != nil { | |
| clearLine(out) | |
| endl = "\r" | |
| fmt.Fprint(out, endl) | |
| } 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(RFC3339NanoFixed)) | |
| } else if jm.Time != 0 { | |
| fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(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 reads a JSON message stream from in, and writes | |
| // each [JSONMessage] to out. It returns an error if an invalid JSONMessage | |
| // is received, or if a JSONMessage containers a non-zero [JSONMessage.Error]. | |
| // | |
| // Presentation of the JSONMessage depends on whether a terminal is attached, | |
| // and on the terminal width. Progress bars ([JSONProgress]) are suppressed | |
| // on narrower terminals (< 110 characters). | |
| // | |
| // - isTerminal describes if out is a terminal, in which case it prints | |
| // a newline ("\n") at the end of each line and moves the cursor while | |
| // displaying. | |
| // - terminalFd is the fd of the current terminal (if any), and used | |
| // to get the terminal width. | |
| // - auxCallback allows handling the [JSONMessage.Aux] field. It is | |
| // called if a JSONMessage contains an Aux field, in which case | |
| // DisplayJSONMessagesStream does not present the JSONMessage. | |
| func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(JSONMessage)) error { | |
| var ( | |
| dec = json.NewDecoder(in) | |
| ids = make(map[string]uint) | |
| ) | |
| for { | |
| var diff uint | |
| var jm JSONMessage | |
| if err := dec.Decode(&jm); err != nil { | |
| if err == io.EOF { | |
| break | |
| } | |
| return err | |
| } | |
| if jm.Aux != nil { | |
| if auxCallback != nil { | |
| auxCallback(jm) | |
| } | |
| continue | |
| } | |
| 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 = uint(len(ids)) | |
| ids[jm.ID] = line | |
| if isTerminal { | |
| fmt.Fprintf(out, "\n") | |
| } | |
| } | |
| diff = uint(len(ids)) - line | |
| if isTerminal { | |
| cursorUp(out, 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]uint) | |
| } | |
| err := jm.Display(out, isTerminal) | |
| if jm.ID != "" && isTerminal { | |
| cursorDown(out, diff) | |
| } | |
| if err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| // Stream is an io.Writer for output with utilities to get the output's file | |
| // descriptor and to detect wether it's a terminal. | |
| // | |
| // it is subset of the streams.Out type in | |
| // https://pkg.go.dev/github.com/docker/cli@v20.10.17+incompatible/cli/streams#Out | |
| type Stream interface { | |
| io.Writer | |
| FD() uintptr | |
| IsTerminal() bool | |
| } | |
| // DisplayJSONMessagesToStream prints json messages to the output Stream. It is | |
| // used by the Docker CLI to print JSONMessage streams. | |
| func DisplayJSONMessagesToStream(in io.Reader, stream Stream, auxCallback func(JSONMessage)) error { | |
| return DisplayJSONMessagesStream(in, stream, stream.FD(), stream.IsTerminal(), auxCallback) | |
| } |