jsonmessage.go 9.2 KB

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