jsonmessage.go 8.0 KB

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