github.com/moby/docker@v26.1.3+incompatible/pkg/jsonmessage/jsonmessage.go (about) 1 package jsonmessage // import "github.com/docker/docker/pkg/jsonmessage" 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "strings" 8 "time" 9 10 units "github.com/docker/go-units" 11 "github.com/moby/term" 12 "github.com/morikuni/aec" 13 ) 14 15 // RFC3339NanoFixed is time.RFC3339Nano with nanoseconds padded using zeros to 16 // ensure the formatted time isalways the same number of characters. 17 const RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" 18 19 // JSONError wraps a concrete Code and Message, Code is 20 // an integer error code, Message is the error message. 21 type JSONError struct { 22 Code int `json:"code,omitempty"` 23 Message string `json:"message,omitempty"` 24 } 25 26 func (e *JSONError) Error() string { 27 return e.Message 28 } 29 30 // JSONProgress describes a progress message in a JSON stream. 31 type JSONProgress struct { 32 // Current is the current status and value of the progress made towards Total. 33 Current int64 `json:"current,omitempty"` 34 // Total is the end value describing when we made 100% progress for an operation. 35 Total int64 `json:"total,omitempty"` 36 // Start is the initial value for the operation. 37 Start int64 `json:"start,omitempty"` 38 // HideCounts. if true, hides the progress count indicator (xB/yB). 39 HideCounts bool `json:"hidecounts,omitempty"` 40 // Units is the unit to print for progress. It defaults to "bytes" if empty. 41 Units string `json:"units,omitempty"` 42 43 // terminalFd is the fd of the current terminal, if any. It is used 44 // to get the terminal width. 45 terminalFd uintptr 46 47 // nowFunc is used to override the current time in tests. 48 nowFunc func() time.Time 49 50 // winSize is used to override the terminal width in tests. 51 winSize int 52 } 53 54 func (p *JSONProgress) String() string { 55 var ( 56 width = p.width() 57 pbBox string 58 numbersBox string 59 timeLeftBox string 60 ) 61 if p.Current <= 0 && p.Total <= 0 { 62 return "" 63 } 64 if p.Total <= 0 { 65 switch p.Units { 66 case "": 67 return fmt.Sprintf("%8v", units.HumanSize(float64(p.Current))) 68 default: 69 return fmt.Sprintf("%d %s", p.Current, p.Units) 70 } 71 } 72 73 percentage := int(float64(p.Current)/float64(p.Total)*100) / 2 74 if percentage > 50 { 75 percentage = 50 76 } 77 if width > 110 { 78 // this number can't be negative gh#7136 79 numSpaces := 0 80 if 50-percentage > 0 { 81 numSpaces = 50 - percentage 82 } 83 pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces)) 84 } 85 86 switch { 87 case p.HideCounts: 88 case p.Units == "": // no units, use bytes 89 current := units.HumanSize(float64(p.Current)) 90 total := units.HumanSize(float64(p.Total)) 91 92 numbersBox = fmt.Sprintf("%8v/%v", current, total) 93 94 if p.Current > p.Total { 95 // remove total display if the reported current is wonky. 96 numbersBox = fmt.Sprintf("%8v", current) 97 } 98 default: 99 numbersBox = fmt.Sprintf("%d/%d %s", p.Current, p.Total, p.Units) 100 101 if p.Current > p.Total { 102 // remove total display if the reported current is wonky. 103 numbersBox = fmt.Sprintf("%d %s", p.Current, p.Units) 104 } 105 } 106 107 if p.Current > 0 && p.Start > 0 && percentage < 50 { 108 fromStart := p.now().Sub(time.Unix(p.Start, 0)) 109 perEntry := fromStart / time.Duration(p.Current) 110 left := time.Duration(p.Total-p.Current) * perEntry 111 left = (left / time.Second) * time.Second 112 113 if width > 50 { 114 timeLeftBox = " " + left.String() 115 } 116 } 117 return pbBox + numbersBox + timeLeftBox 118 } 119 120 // now returns the current time in UTC, but can be overridden in tests 121 // by setting JSONProgress.nowFunc to a custom function. 122 func (p *JSONProgress) now() time.Time { 123 if p.nowFunc != nil { 124 return p.nowFunc() 125 } 126 return time.Now().UTC() 127 } 128 129 // width returns the current terminal's width, but can be overridden 130 // in tests by setting JSONProgress.winSize to a non-zero value. 131 func (p *JSONProgress) width() int { 132 if p.winSize != 0 { 133 return p.winSize 134 } 135 ws, err := term.GetWinsize(p.terminalFd) 136 if err == nil { 137 return int(ws.Width) 138 } 139 return 200 140 } 141 142 // JSONMessage defines a message struct. It describes 143 // the created time, where it from, status, ID of the 144 // message. It's used for docker events. 145 type JSONMessage struct { 146 Stream string `json:"stream,omitempty"` 147 Status string `json:"status,omitempty"` 148 Progress *JSONProgress `json:"progressDetail,omitempty"` 149 ProgressMessage string `json:"progress,omitempty"` // deprecated 150 ID string `json:"id,omitempty"` 151 From string `json:"from,omitempty"` 152 Time int64 `json:"time,omitempty"` 153 TimeNano int64 `json:"timeNano,omitempty"` 154 Error *JSONError `json:"errorDetail,omitempty"` 155 ErrorMessage string `json:"error,omitempty"` // deprecated 156 // Aux contains out-of-band data, such as digests for push signing and image id after building. 157 Aux *json.RawMessage `json:"aux,omitempty"` 158 } 159 160 func clearLine(out io.Writer) { 161 eraseMode := aec.EraseModes.All 162 cl := aec.EraseLine(eraseMode) 163 fmt.Fprint(out, cl) 164 } 165 166 func cursorUp(out io.Writer, l uint) { 167 fmt.Fprint(out, aec.Up(l)) 168 } 169 170 func cursorDown(out io.Writer, l uint) { 171 fmt.Fprint(out, aec.Down(l)) 172 } 173 174 // Display prints the JSONMessage to out. If isTerminal is true, it erases 175 // the entire current line when displaying the progressbar. It returns an 176 // error if the [JSONMessage.Error] field is non-nil. 177 func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error { 178 if jm.Error != nil { 179 return jm.Error 180 } 181 var endl string 182 if isTerminal && jm.Stream == "" && jm.Progress != nil { 183 clearLine(out) 184 endl = "\r" 185 fmt.Fprint(out, endl) 186 } else if jm.Progress != nil && jm.Progress.String() != "" { // disable progressbar in non-terminal 187 return nil 188 } 189 if jm.TimeNano != 0 { 190 fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(RFC3339NanoFixed)) 191 } else if jm.Time != 0 { 192 fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(RFC3339NanoFixed)) 193 } 194 if jm.ID != "" { 195 fmt.Fprintf(out, "%s: ", jm.ID) 196 } 197 if jm.From != "" { 198 fmt.Fprintf(out, "(from %s) ", jm.From) 199 } 200 if jm.Progress != nil && isTerminal { 201 fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl) 202 } else if jm.ProgressMessage != "" { // deprecated 203 fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl) 204 } else if jm.Stream != "" { 205 fmt.Fprintf(out, "%s%s", jm.Stream, endl) 206 } else { 207 fmt.Fprintf(out, "%s%s\n", jm.Status, endl) 208 } 209 return nil 210 } 211 212 // DisplayJSONMessagesStream reads a JSON message stream from in, and writes 213 // each [JSONMessage] to out. It returns an error if an invalid JSONMessage 214 // is received, or if a JSONMessage containers a non-zero [JSONMessage.Error]. 215 // 216 // Presentation of the JSONMessage depends on whether a terminal is attached, 217 // and on the terminal width. Progress bars ([JSONProgress]) are suppressed 218 // on narrower terminals (< 110 characters). 219 // 220 // - isTerminal describes if out is a terminal, in which case it prints 221 // a newline ("\n") at the end of each line and moves the cursor while 222 // displaying. 223 // - terminalFd is the fd of the current terminal (if any), and used 224 // to get the terminal width. 225 // - auxCallback allows handling the [JSONMessage.Aux] field. It is 226 // called if a JSONMessage contains an Aux field, in which case 227 // DisplayJSONMessagesStream does not present the JSONMessage. 228 func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(JSONMessage)) error { 229 var ( 230 dec = json.NewDecoder(in) 231 ids = make(map[string]uint) 232 ) 233 234 for { 235 var diff uint 236 var jm JSONMessage 237 if err := dec.Decode(&jm); err != nil { 238 if err == io.EOF { 239 break 240 } 241 return err 242 } 243 244 if jm.Aux != nil { 245 if auxCallback != nil { 246 auxCallback(jm) 247 } 248 continue 249 } 250 251 if jm.Progress != nil { 252 jm.Progress.terminalFd = terminalFd 253 } 254 if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") { 255 line, ok := ids[jm.ID] 256 if !ok { 257 // NOTE: This approach of using len(id) to 258 // figure out the number of lines of history 259 // only works as long as we clear the history 260 // when we output something that's not 261 // accounted for in the map, such as a line 262 // with no ID. 263 line = uint(len(ids)) 264 ids[jm.ID] = line 265 if isTerminal { 266 fmt.Fprintf(out, "\n") 267 } 268 } 269 diff = uint(len(ids)) - line 270 if isTerminal { 271 cursorUp(out, diff) 272 } 273 } else { 274 // When outputting something that isn't progress 275 // output, clear the history of previous lines. We 276 // don't want progress entries from some previous 277 // operation to be updated (for example, pull -a 278 // with multiple tags). 279 ids = make(map[string]uint) 280 } 281 err := jm.Display(out, isTerminal) 282 if jm.ID != "" && isTerminal { 283 cursorDown(out, diff) 284 } 285 if err != nil { 286 return err 287 } 288 } 289 return nil 290 } 291 292 // Stream is an io.Writer for output with utilities to get the output's file 293 // descriptor and to detect wether it's a terminal. 294 // 295 // it is subset of the streams.Out type in 296 // https://pkg.go.dev/github.com/docker/cli@v20.10.17+incompatible/cli/streams#Out 297 type Stream interface { 298 io.Writer 299 FD() uintptr 300 IsTerminal() bool 301 } 302 303 // DisplayJSONMessagesToStream prints json messages to the output Stream. It is 304 // used by the Docker CLI to print JSONMessage streams. 305 func DisplayJSONMessagesToStream(in io.Reader, stream Stream, auxCallback func(JSONMessage)) error { 306 return DisplayJSONMessagesStream(in, stream, stream.FD(), stream.IsTerminal(), auxCallback) 307 }