jsonmessage.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. package jsonmessage
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "os"
  7. "strings"
  8. "time"
  9. "github.com/Nvveen/Gotty"
  10. "github.com/docker/docker/pkg/jsonlog"
  11. "github.com/docker/docker/pkg/term"
  12. "github.com/docker/go-units"
  13. )
  14. // JSONError wraps a concrete Code and Message, `Code` is
  15. // is an integer error code, `Message` is the error message.
  16. type JSONError struct {
  17. Code int `json:"code,omitempty"`
  18. Message string `json:"message,omitempty"`
  19. }
  20. func (e *JSONError) Error() string {
  21. return e.Message
  22. }
  23. // JSONProgress describes a Progress. terminalFd is the fd of the current terminal,
  24. // Start is the initial value for the operation. Current is the current status and
  25. // value of the progress made towards Total. Total is the end value describing when
  26. // we made 100% progress for an operation.
  27. type JSONProgress struct {
  28. terminalFd uintptr
  29. Current int64 `json:"current,omitempty"`
  30. Total int64 `json:"total,omitempty"`
  31. Start int64 `json:"start,omitempty"`
  32. // If true, don't show xB/yB
  33. HideCounts bool `json:"hidecounts,omitempty"`
  34. }
  35. func (p *JSONProgress) String() string {
  36. var (
  37. width = 200
  38. pbBox string
  39. numbersBox string
  40. timeLeftBox string
  41. )
  42. ws, err := term.GetWinsize(p.terminalFd)
  43. if err == nil {
  44. width = int(ws.Width)
  45. }
  46. if p.Current <= 0 && p.Total <= 0 {
  47. return ""
  48. }
  49. current := units.HumanSize(float64(p.Current))
  50. if p.Total <= 0 {
  51. return fmt.Sprintf("%8v", current)
  52. }
  53. total := units.HumanSize(float64(p.Total))
  54. percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
  55. if percentage > 50 {
  56. percentage = 50
  57. }
  58. if width > 110 {
  59. // this number can't be negative gh#7136
  60. numSpaces := 0
  61. if 50-percentage > 0 {
  62. numSpaces = 50 - percentage
  63. }
  64. pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
  65. }
  66. if !p.HideCounts {
  67. numbersBox = fmt.Sprintf("%8v/%v", current, total)
  68. if p.Current > p.Total {
  69. // remove total display if the reported current is wonky.
  70. numbersBox = fmt.Sprintf("%8v", current)
  71. }
  72. }
  73. if p.Current > 0 && p.Start > 0 && percentage < 50 {
  74. fromStart := time.Now().UTC().Sub(time.Unix(p.Start, 0))
  75. perEntry := fromStart / time.Duration(p.Current)
  76. left := time.Duration(p.Total-p.Current) * perEntry
  77. left = (left / time.Second) * time.Second
  78. if width > 50 {
  79. timeLeftBox = " " + left.String()
  80. }
  81. }
  82. return pbBox + numbersBox + timeLeftBox
  83. }
  84. // JSONMessage defines a message struct. It describes
  85. // the created time, where it from, status, ID of the
  86. // message. It's used for docker events.
  87. type JSONMessage struct {
  88. Stream string `json:"stream,omitempty"`
  89. Status string `json:"status,omitempty"`
  90. Progress *JSONProgress `json:"progressDetail,omitempty"`
  91. ProgressMessage string `json:"progress,omitempty"` //deprecated
  92. ID string `json:"id,omitempty"`
  93. From string `json:"from,omitempty"`
  94. Time int64 `json:"time,omitempty"`
  95. TimeNano int64 `json:"timeNano,omitempty"`
  96. Error *JSONError `json:"errorDetail,omitempty"`
  97. ErrorMessage string `json:"error,omitempty"` //deprecated
  98. // Aux contains out-of-band data, such as digests for push signing.
  99. Aux *json.RawMessage `json:"aux,omitempty"`
  100. }
  101. /* Satisfied by gotty.TermInfo as well as noTermInfo from below */
  102. type termInfo interface {
  103. Parse(attr string, params ...interface{}) (string, error)
  104. }
  105. type noTermInfo struct{} // canary used when no terminfo.
  106. func (ti *noTermInfo) Parse(attr string, params ...interface{}) (string, error) {
  107. return "", fmt.Errorf("noTermInfo")
  108. }
  109. func clearLine(out io.Writer, ti termInfo) {
  110. // el2 (clear whole line) is not exposed by terminfo.
  111. // First clear line from beginning to cursor
  112. if attr, err := ti.Parse("el1"); err == nil {
  113. fmt.Fprintf(out, "%s", attr)
  114. } else {
  115. fmt.Fprintf(out, "\x1b[1K")
  116. }
  117. // Then clear line from cursor to end
  118. if attr, err := ti.Parse("el"); err == nil {
  119. fmt.Fprintf(out, "%s", attr)
  120. } else {
  121. fmt.Fprintf(out, "\x1b[K")
  122. }
  123. }
  124. func cursorUp(out io.Writer, ti termInfo, l int) {
  125. if l == 0 { // Should never be the case, but be tolerant
  126. return
  127. }
  128. if attr, err := ti.Parse("cuu", l); err == nil {
  129. fmt.Fprintf(out, "%s", attr)
  130. } else {
  131. fmt.Fprintf(out, "\x1b[%dA", l)
  132. }
  133. }
  134. func cursorDown(out io.Writer, ti termInfo, l int) {
  135. if l == 0 { // Should never be the case, but be tolerant
  136. return
  137. }
  138. if attr, err := ti.Parse("cud", l); err == nil {
  139. fmt.Fprintf(out, "%s", attr)
  140. } else {
  141. fmt.Fprintf(out, "\x1b[%dB", l)
  142. }
  143. }
  144. // Display displays the JSONMessage to `out`. `termInfo` is non-nil if `out`
  145. // is a terminal. If this is the case, it will erase the entire current line
  146. // when displaying the progressbar.
  147. func (jm *JSONMessage) Display(out io.Writer, termInfo termInfo) error {
  148. if jm.Error != nil {
  149. if jm.Error.Code == 401 {
  150. return fmt.Errorf("Authentication is required.")
  151. }
  152. return jm.Error
  153. }
  154. var endl string
  155. if termInfo != nil && jm.Stream == "" && jm.Progress != nil {
  156. clearLine(out, termInfo)
  157. endl = "\r"
  158. fmt.Fprintf(out, endl)
  159. } else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal
  160. return nil
  161. }
  162. if jm.TimeNano != 0 {
  163. fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(jsonlog.RFC3339NanoFixed))
  164. } else if jm.Time != 0 {
  165. fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(jsonlog.RFC3339NanoFixed))
  166. }
  167. if jm.ID != "" {
  168. fmt.Fprintf(out, "%s: ", jm.ID)
  169. }
  170. if jm.From != "" {
  171. fmt.Fprintf(out, "(from %s) ", jm.From)
  172. }
  173. if jm.Progress != nil && termInfo != nil {
  174. fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl)
  175. } else if jm.ProgressMessage != "" { //deprecated
  176. fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl)
  177. } else if jm.Stream != "" {
  178. fmt.Fprintf(out, "%s%s", jm.Stream, endl)
  179. } else {
  180. fmt.Fprintf(out, "%s%s\n", jm.Status, endl)
  181. }
  182. return nil
  183. }
  184. // DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal`
  185. // describes if `out` is a terminal. If this is the case, it will print `\n` at the end of
  186. // each line and move the cursor while displaying.
  187. func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(*json.RawMessage)) error {
  188. var (
  189. dec = json.NewDecoder(in)
  190. ids = make(map[string]int)
  191. )
  192. var termInfo termInfo
  193. if isTerminal {
  194. term := os.Getenv("TERM")
  195. if term == "" {
  196. term = "vt102"
  197. }
  198. var err error
  199. if termInfo, err = gotty.OpenTermInfo(term); err != nil {
  200. termInfo = &noTermInfo{}
  201. }
  202. }
  203. for {
  204. diff := 0
  205. var jm JSONMessage
  206. if err := dec.Decode(&jm); err != nil {
  207. if err == io.EOF {
  208. break
  209. }
  210. return err
  211. }
  212. if jm.Aux != nil {
  213. if auxCallback != nil {
  214. auxCallback(jm.Aux)
  215. }
  216. continue
  217. }
  218. if jm.Progress != nil {
  219. jm.Progress.terminalFd = terminalFd
  220. }
  221. if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") {
  222. line, ok := ids[jm.ID]
  223. if !ok {
  224. // NOTE: This approach of using len(id) to
  225. // figure out the number of lines of history
  226. // only works as long as we clear the history
  227. // when we output something that's not
  228. // accounted for in the map, such as a line
  229. // with no ID.
  230. line = len(ids)
  231. ids[jm.ID] = line
  232. if termInfo != nil {
  233. fmt.Fprintf(out, "\n")
  234. }
  235. }
  236. diff = len(ids) - line
  237. if termInfo != nil {
  238. cursorUp(out, termInfo, diff)
  239. }
  240. } else {
  241. // When outputting something that isn't progress
  242. // output, clear the history of previous lines. We
  243. // don't want progress entries from some previous
  244. // operation to be updated (for example, pull -a
  245. // with multiple tags).
  246. ids = make(map[string]int)
  247. }
  248. err := jm.Display(out, termInfo)
  249. if jm.ID != "" && termInfo != nil {
  250. cursorDown(out, termInfo, diff)
  251. }
  252. if err != nil {
  253. return err
  254. }
  255. }
  256. return nil
  257. }
  258. type stream interface {
  259. io.Writer
  260. FD() uintptr
  261. IsTerminal() bool
  262. }
  263. // DisplayJSONMessagesToStream prints json messages to the output stream
  264. func DisplayJSONMessagesToStream(in io.Reader, stream stream, auxCallback func(*json.RawMessage)) error {
  265. return DisplayJSONMessagesStream(in, stream, stream.FD(), stream.IsTerminal(), auxCallback)
  266. }