jsonmessage.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. package jsonmessage
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "strings"
  7. "time"
  8. "github.com/docker/docker/pkg/jsonlog"
  9. "github.com/docker/docker/pkg/term"
  10. "github.com/docker/go-units"
  11. )
  12. // JSONError wraps a concrete Code and Message, `Code` is
  13. // is an integer error code, `Message` is the error message.
  14. type JSONError struct {
  15. Code int `json:"code,omitempty"`
  16. Message string `json:"message,omitempty"`
  17. }
  18. func (e *JSONError) Error() string {
  19. return e.Message
  20. }
  21. // JSONProgress describes a Progress. terminalFd is the fd of the current terminal,
  22. // Start is the initial value for the operation. Current is the current status and
  23. // value of the progress made towards Total. Total is the end value describing when
  24. // we made 100% progress for an operation.
  25. type JSONProgress struct {
  26. terminalFd uintptr
  27. Current int64 `json:"current,omitempty"`
  28. Total int64 `json:"total,omitempty"`
  29. Start int64 `json:"start,omitempty"`
  30. }
  31. func (p *JSONProgress) String() string {
  32. var (
  33. width = 200
  34. pbBox string
  35. numbersBox string
  36. timeLeftBox string
  37. )
  38. ws, err := term.GetWinsize(p.terminalFd)
  39. if err == nil {
  40. width = int(ws.Width)
  41. }
  42. if p.Current <= 0 && p.Total <= 0 {
  43. return ""
  44. }
  45. current := units.HumanSize(float64(p.Current))
  46. if p.Total <= 0 {
  47. return fmt.Sprintf("%8v", current)
  48. }
  49. total := units.HumanSize(float64(p.Total))
  50. percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
  51. if percentage > 50 {
  52. percentage = 50
  53. }
  54. if width > 110 {
  55. // this number can't be negative gh#7136
  56. numSpaces := 0
  57. if 50-percentage > 0 {
  58. numSpaces = 50 - percentage
  59. }
  60. pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
  61. }
  62. numbersBox = fmt.Sprintf("%8v/%v", current, total)
  63. if p.Current > p.Total {
  64. // remove total display if the reported current is wonky.
  65. numbersBox = fmt.Sprintf("%8v", current)
  66. }
  67. if p.Current > 0 && p.Start > 0 && percentage < 50 {
  68. fromStart := time.Now().UTC().Sub(time.Unix(p.Start, 0))
  69. perEntry := fromStart / time.Duration(p.Current)
  70. left := time.Duration(p.Total-p.Current) * perEntry
  71. left = (left / time.Second) * time.Second
  72. if width > 50 {
  73. timeLeftBox = " " + left.String()
  74. }
  75. }
  76. return pbBox + numbersBox + timeLeftBox
  77. }
  78. // JSONMessage defines a message struct. It describes
  79. // the created time, where it from, status, ID of the
  80. // message. It's used for docker events.
  81. type JSONMessage struct {
  82. Stream string `json:"stream,omitempty"`
  83. Status string `json:"status,omitempty"`
  84. Progress *JSONProgress `json:"progressDetail,omitempty"`
  85. ProgressMessage string `json:"progress,omitempty"` //deprecated
  86. ID string `json:"id,omitempty"`
  87. From string `json:"from,omitempty"`
  88. Time int64 `json:"time,omitempty"`
  89. TimeNano int64 `json:"timeNano,omitempty"`
  90. Error *JSONError `json:"errorDetail,omitempty"`
  91. ErrorMessage string `json:"error,omitempty"` //deprecated
  92. // Aux contains out-of-band data, such as digests for push signing.
  93. Aux *json.RawMessage `json:"aux,omitempty"`
  94. }
  95. // Display displays the JSONMessage to `out`. `isTerminal` describes if `out`
  96. // is a terminal. If this is the case, it will erase the entire current line
  97. // when displaying the progressbar.
  98. func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
  99. if jm.Error != nil {
  100. if jm.Error.Code == 401 {
  101. return fmt.Errorf("Authentication is required.")
  102. }
  103. return jm.Error
  104. }
  105. var endl string
  106. if isTerminal && jm.Stream == "" && jm.Progress != nil {
  107. // <ESC>[2K = erase entire current line
  108. fmt.Fprintf(out, "%c[2K\r", 27)
  109. endl = "\r"
  110. } else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal
  111. return nil
  112. }
  113. if jm.TimeNano != 0 {
  114. fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(jsonlog.RFC3339NanoFixed))
  115. } else if jm.Time != 0 {
  116. fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(jsonlog.RFC3339NanoFixed))
  117. }
  118. if jm.ID != "" {
  119. fmt.Fprintf(out, "%s: ", jm.ID)
  120. }
  121. if jm.From != "" {
  122. fmt.Fprintf(out, "(from %s) ", jm.From)
  123. }
  124. if jm.Progress != nil && isTerminal {
  125. fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl)
  126. } else if jm.ProgressMessage != "" { //deprecated
  127. fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl)
  128. } else if jm.Stream != "" {
  129. fmt.Fprintf(out, "%s%s", jm.Stream, endl)
  130. } else {
  131. fmt.Fprintf(out, "%s%s\n", jm.Status, endl)
  132. }
  133. return nil
  134. }
  135. // DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal`
  136. // describes if `out` is a terminal. If this is the case, it will print `\n` at the end of
  137. // each line and move the cursor while displaying.
  138. func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(*json.RawMessage)) error {
  139. var (
  140. dec = json.NewDecoder(in)
  141. ids = make(map[string]int)
  142. )
  143. for {
  144. diff := 0
  145. var jm JSONMessage
  146. if err := dec.Decode(&jm); err != nil {
  147. if err == io.EOF {
  148. break
  149. }
  150. return err
  151. }
  152. if jm.Aux != nil {
  153. if auxCallback != nil {
  154. auxCallback(jm.Aux)
  155. }
  156. continue
  157. }
  158. if jm.Progress != nil {
  159. jm.Progress.terminalFd = terminalFd
  160. }
  161. if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") {
  162. line, ok := ids[jm.ID]
  163. if !ok {
  164. // NOTE: This approach of using len(id) to
  165. // figure out the number of lines of history
  166. // only works as long as we clear the history
  167. // when we output something that's not
  168. // accounted for in the map, such as a line
  169. // with no ID.
  170. line = len(ids)
  171. ids[jm.ID] = line
  172. if isTerminal {
  173. fmt.Fprintf(out, "\n")
  174. }
  175. } else {
  176. diff = len(ids) - line
  177. }
  178. if isTerminal {
  179. // NOTE: this appears to be necessary even if
  180. // diff == 0.
  181. // <ESC>[{diff}A = move cursor up diff rows
  182. fmt.Fprintf(out, "%c[%dA", 27, diff)
  183. }
  184. } else {
  185. // When outputting something that isn't progress
  186. // output, clear the history of previous lines. We
  187. // don't want progress entries from some previous
  188. // operation to be updated (for example, pull -a
  189. // with multiple tags).
  190. ids = make(map[string]int)
  191. }
  192. err := jm.Display(out, isTerminal)
  193. if jm.ID != "" && isTerminal {
  194. // NOTE: this appears to be necessary even if
  195. // diff == 0.
  196. // <ESC>[{diff}B = move cursor down diff rows
  197. fmt.Fprintf(out, "%c[%dB", 27, diff)
  198. }
  199. if err != nil {
  200. return err
  201. }
  202. }
  203. return nil
  204. }