511ee61980
This way provide both Time and TimeNano in the event. For the display of the JSONMessage, use either, but prefer TimeNano Proving only TimeNano would break Subscribers that are using the `Time` field, so both are set for backwards compatibility. The events logging uses nano formatting, but only provides a Unix() time, therefor ordering may get lost in the output. Example: ``` 2015-09-15T14:18:51.000000000-04:00 ee46febd64ac629f7de9cd8bf58582e6f263d97ff46896adc5b508db804682da: (from busybox) resize 2015-09-15T14:18:51.000000000-04:00 a78c9149b1c0474502a117efaa814541926c2ae6ec3c76607e1c931b84c3a44b: (from busybox) resize ``` By having a field just for Nano time, when set, the marshalling back to `time.Unix(sec int64, nsec int64)` has zeros exactly where it needs to. This does not break any existing use of jsonmessage.JSONMessage, but now allows for use of `UnixNano()` and get event formatting that has distinguishable order. Example: ``` 2015-09-15T15:37:23.810295632-04:00 6adcf8ed9f5f5ec059a915466cd1cde86a18b4a085fc3af405e9cc9fecbbbbaf: (from busybox) resize 2015-09-15T15:37:23.810412202-04:00 6b7c5bfdc3f902096f5a91e628f21bd4b56e32590c5b4b97044aafc005ddcb0d: (from busybox) resize ``` Including tests for TimeNano and updated event API reference doc. Signed-off-by: Vincent Batts <vbatts@redhat.com>
196 lines
5.5 KiB
Go
196 lines
5.5 KiB
Go
package jsonmessage
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/docker/docker/pkg/term"
|
|
"github.com/docker/docker/pkg/timeutils"
|
|
"github.com/docker/docker/pkg/units"
|
|
)
|
|
|
|
// JSONError wraps a concrete Code and Message, `Code` is
|
|
// is a integer error code, `Message` is the error message.
|
|
type JSONError struct {
|
|
Code int `json:"code,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
func (e *JSONError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// JSONProgress describes a Progress. terminalFd is the fd of the current terminal,
|
|
// Start is the initial value for the operation. Current is the current status and
|
|
// value of the progress made towards Total. Total is the end value describing when
|
|
// we made 100% progress for an operation.
|
|
type JSONProgress struct {
|
|
terminalFd uintptr
|
|
Current int64 `json:"current,omitempty"`
|
|
Total int64 `json:"total,omitempty"`
|
|
Start int64 `json:"start,omitempty"`
|
|
}
|
|
|
|
func (p *JSONProgress) String() string {
|
|
var (
|
|
width = 200
|
|
pbBox string
|
|
numbersBox string
|
|
timeLeftBox string
|
|
)
|
|
|
|
ws, err := term.GetWinsize(p.terminalFd)
|
|
if err == nil {
|
|
width = int(ws.Width)
|
|
}
|
|
|
|
if p.Current <= 0 && p.Total <= 0 {
|
|
return ""
|
|
}
|
|
current := units.HumanSize(float64(p.Current))
|
|
if p.Total <= 0 {
|
|
return fmt.Sprintf("%8v", current)
|
|
}
|
|
total := units.HumanSize(float64(p.Total))
|
|
percentage := int(float64(p.Current)/float64(p.Total)*100) / 2
|
|
if percentage > 50 {
|
|
percentage = 50
|
|
}
|
|
if width > 110 {
|
|
// this number can't be negetive gh#7136
|
|
numSpaces := 0
|
|
if 50-percentage > 0 {
|
|
numSpaces = 50 - percentage
|
|
}
|
|
pbBox = fmt.Sprintf("[%s>%s] ", strings.Repeat("=", percentage), strings.Repeat(" ", numSpaces))
|
|
}
|
|
|
|
numbersBox = fmt.Sprintf("%8v/%v", current, total)
|
|
|
|
if p.Current > p.Total {
|
|
// remove total display if the reported current is wonky.
|
|
numbersBox = fmt.Sprintf("%8v", current)
|
|
}
|
|
|
|
if p.Current > 0 && p.Start > 0 && percentage < 50 {
|
|
fromStart := time.Now().UTC().Sub(time.Unix(p.Start, 0))
|
|
perEntry := fromStart / time.Duration(p.Current)
|
|
left := time.Duration(p.Total-p.Current) * perEntry
|
|
left = (left / time.Second) * time.Second
|
|
|
|
if width > 50 {
|
|
timeLeftBox = " " + left.String()
|
|
}
|
|
}
|
|
return pbBox + numbersBox + timeLeftBox
|
|
}
|
|
|
|
// JSONMessage defines a message struct. It describes
|
|
// the created time, where it from, status, ID of the
|
|
// message. It's used for docker events.
|
|
type JSONMessage struct {
|
|
Stream string `json:"stream,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Progress *JSONProgress `json:"progressDetail,omitempty"`
|
|
ProgressMessage string `json:"progress,omitempty"` //deprecated
|
|
ID string `json:"id,omitempty"`
|
|
From string `json:"from,omitempty"`
|
|
Time int64 `json:"time,omitempty"`
|
|
TimeNano int64 `json:"timeNano,omitempty"`
|
|
Error *JSONError `json:"errorDetail,omitempty"`
|
|
ErrorMessage string `json:"error,omitempty"` //deprecated
|
|
}
|
|
|
|
// Display displays the JSONMessage to `out`. `isTerminal` describes if `out`
|
|
// is a terminal. If this is the case, it will erase the entire current line
|
|
// when dislaying the progressbar.
|
|
func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
|
|
if jm.Error != nil {
|
|
if jm.Error.Code == 401 {
|
|
return fmt.Errorf("Authentication is required.")
|
|
}
|
|
return jm.Error
|
|
}
|
|
var endl string
|
|
if isTerminal && jm.Stream == "" && jm.Progress != nil {
|
|
// <ESC>[2K = erase entire current line
|
|
fmt.Fprintf(out, "%c[2K\r", 27)
|
|
endl = "\r"
|
|
} else if jm.Progress != nil && jm.Progress.String() != "" { //disable progressbar in non-terminal
|
|
return nil
|
|
}
|
|
if jm.TimeNano != 0 {
|
|
fmt.Fprintf(out, "%s ", time.Unix(0, jm.TimeNano).Format(timeutils.RFC3339NanoFixed))
|
|
} else if jm.Time != 0 {
|
|
fmt.Fprintf(out, "%s ", time.Unix(jm.Time, 0).Format(timeutils.RFC3339NanoFixed))
|
|
}
|
|
if jm.ID != "" {
|
|
fmt.Fprintf(out, "%s: ", jm.ID)
|
|
}
|
|
if jm.From != "" {
|
|
fmt.Fprintf(out, "(from %s) ", jm.From)
|
|
}
|
|
if jm.Progress != nil && isTerminal {
|
|
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.Progress.String(), endl)
|
|
} else if jm.ProgressMessage != "" { //deprecated
|
|
fmt.Fprintf(out, "%s %s%s", jm.Status, jm.ProgressMessage, endl)
|
|
} else if jm.Stream != "" {
|
|
fmt.Fprintf(out, "%s%s", jm.Stream, endl)
|
|
} else {
|
|
fmt.Fprintf(out, "%s%s\n", jm.Status, endl)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal`
|
|
// describes if `out` is a terminal. If this is the case, it will print `\n` at the end of
|
|
// each line and move the cursor while displaying.
|
|
func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool) error {
|
|
var (
|
|
dec = json.NewDecoder(in)
|
|
ids = make(map[string]int)
|
|
diff = 0
|
|
)
|
|
for {
|
|
var jm JSONMessage
|
|
if err := dec.Decode(&jm); err != nil {
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
return err
|
|
}
|
|
|
|
if jm.Progress != nil {
|
|
jm.Progress.terminalFd = terminalFd
|
|
}
|
|
if jm.ID != "" && (jm.Progress != nil || jm.ProgressMessage != "") {
|
|
line, ok := ids[jm.ID]
|
|
if !ok {
|
|
line = len(ids)
|
|
ids[jm.ID] = line
|
|
if isTerminal {
|
|
fmt.Fprintf(out, "\n")
|
|
}
|
|
diff = 0
|
|
} else {
|
|
diff = len(ids) - line
|
|
}
|
|
if jm.ID != "" && isTerminal {
|
|
// <ESC>[{diff}A = move cursor up diff rows
|
|
fmt.Fprintf(out, "%c[%dA", 27, diff)
|
|
}
|
|
}
|
|
err := jm.Display(out, isTerminal)
|
|
if jm.ID != "" && isTerminal {
|
|
// <ESC>[{diff}B = move cursor down diff rows
|
|
fmt.Fprintf(out, "%c[%dB", 27, diff)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|