diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index cfc31c06cb7..03eb7e0d061 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -149,6 +149,11 @@ "Comment": "v2.2.0-23-g5ca8014", "Rev": "5ca80149b9d3f8b863af0e2bb6742e608603bd99" }, + { + "ImportPath": "github.com/docker/docker/pkg/jsonmessage", + "Comment": "v1.7.rc", + "Rev": "0cd6c05d8112e9246b734107d54e2855e3d5fec5" + }, { "ImportPath": "github.com/docker/docker/pkg/mount", "Comment": "v1.4.1-1714-ged66853", @@ -164,6 +169,11 @@ "Comment": "v1.4.1-1714-ged66853", "Rev": "ed6685369740035b0af9675bf9add52d0af7657b" }, + { + "ImportPath": "github.com/docker/docker/pkg/timeutils", + "Comment": "v1.7.rc", + "Rev": "0cd6c05d8112e9246b734107d54e2855e3d5fec5" + }, { "ImportPath": "github.com/docker/docker/pkg/units", "Comment": "v1.4.1-1714-ged66853", diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go new file mode 100644 index 00000000000..7db1626e48c --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/jsonmessage/jsonmessage.go @@ -0,0 +1,172 @@ +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" +) + +type JSONError struct { + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` +} + +func (e *JSONError) Error() string { + return e.Message +} + +type JSONProgress struct { + terminalFd uintptr + Current int `json:"current,omitempty"` + Total int `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 > 0 && p.Start > 0 && percentage < 50 { + fromStart := time.Now().UTC().Sub(time.Unix(int64(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 +} + +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"` + Error *JSONError `json:"errorDetail,omitempty"` + ErrorMessage string `json:"error,omitempty"` //deprecated +} + +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 { + // [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.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 +} + +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 { + // [{diff}A = move cursor up diff rows + fmt.Fprintf(out, "%c[%dA", 27, diff) + } + } + err := jm.Display(out, isTerminal) + if jm.ID != "" && isTerminal { + // [{diff}B = move cursor down diff rows + fmt.Fprintf(out, "%c[%dB", 27, diff) + } + if err != nil { + return err + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/jsonmessage/jsonmessage_test.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/jsonmessage/jsonmessage_test.go new file mode 100644 index 00000000000..4c3f5666b3f --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/jsonmessage/jsonmessage_test.go @@ -0,0 +1,38 @@ +package jsonmessage + +import ( + "testing" +) + +func TestError(t *testing.T) { + je := JSONError{404, "Not found"} + if je.Error() != "Not found" { + t.Fatalf("Expected 'Not found' got '%s'", je.Error()) + } +} + +func TestProgress(t *testing.T) { + jp := JSONProgress{} + if jp.String() != "" { + t.Fatalf("Expected empty string, got '%s'", jp.String()) + } + + expected := " 1 B" + jp2 := JSONProgress{Current: 1} + if jp2.String() != expected { + t.Fatalf("Expected %q, got %q", expected, jp2.String()) + } + + expected = "[=========================> ] 50 B/100 B" + jp3 := JSONProgress{Current: 50, Total: 100} + if jp3.String() != expected { + t.Fatalf("Expected %q, got %q", expected, jp3.String()) + } + + // this number can't be negetive gh#7136 + expected = "[==================================================>] 50 B/40 B" + jp4 := JSONProgress{Current: 50, Total: 40} + if jp4.String() != expected { + t.Fatalf("Expected %q, got %q", expected, jp4.String()) + } +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/timeutils/json.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/timeutils/json.go new file mode 100644 index 00000000000..8043d69d18f --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/timeutils/json.go @@ -0,0 +1,26 @@ +package timeutils + +import ( + "errors" + "time" +) + +const ( + // RFC3339NanoFixed is our own version of RFC339Nano because we want one + // that pads the nano seconds part with zeros to ensure + // the timestamps are aligned in the logs. + RFC3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00" + // JSONFormat is the format used by FastMarshalJSON + JSONFormat = `"` + time.RFC3339Nano + `"` +) + +// FastMarshalJSON avoids one of the extra allocations that +// time.MarshalJSON is making. +func FastMarshalJSON(t time.Time) (string, error) { + if y := t.Year(); y < 0 || y >= 10000 { + // RFC 3339 is clear that years are 4 digits exactly. + // See golang.org/issue/4556#c15 for more discussion. + return "", errors.New("time.MarshalJSON: year outside of range [0,9999]") + } + return t.Format(JSONFormat), nil +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/timeutils/utils.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/timeutils/utils.go new file mode 100644 index 00000000000..6af16a1d7fc --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/timeutils/utils.go @@ -0,0 +1,29 @@ +package timeutils + +import ( + "strconv" + "strings" + "time" +) + +// GetTimestamp tries to parse given string as RFC3339 time +// or Unix timestamp (with seconds precision), if successful +//returns a Unix timestamp as string otherwise returns value back. +func GetTimestamp(value string) string { + var format string + if strings.Contains(value, ".") { + format = time.RFC3339Nano + } else { + format = time.RFC3339 + } + + loc := time.FixedZone(time.Now().Zone()) + if len(value) < len(format) { + format = format[:len(value)] + } + t, err := time.ParseInLocation(format, value, loc) + if err != nil { + return value + } + return strconv.FormatInt(t.Unix(), 10) +} diff --git a/Godeps/_workspace/src/github.com/docker/docker/pkg/timeutils/utils_test.go b/Godeps/_workspace/src/github.com/docker/docker/pkg/timeutils/utils_test.go new file mode 100644 index 00000000000..1d724fb2ac9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/docker/docker/pkg/timeutils/utils_test.go @@ -0,0 +1,36 @@ +package timeutils + +import ( + "testing" +) + +func TestGetTimestamp(t *testing.T) { + cases := []struct{ in, expected string }{ + {"0", "-62167305600"}, // 0 gets parsed year 0 + + // Partial RFC3339 strings get parsed with second precision + {"2006-01-02T15:04:05.999999999+07:00", "1136189045"}, + {"2006-01-02T15:04:05.999999999Z", "1136214245"}, + {"2006-01-02T15:04:05.999999999", "1136214245"}, + {"2006-01-02T15:04:05", "1136214245"}, + {"2006-01-02T15:04", "1136214240"}, + {"2006-01-02T15", "1136214000"}, + {"2006-01-02T", "1136160000"}, + {"2006-01-02", "1136160000"}, + {"2006", "1136073600"}, + {"2015-05-13T20:39:09Z", "1431549549"}, + + // unix timestamps returned as is + {"1136073600", "1136073600"}, + + // String fallback + {"invalid", "invalid"}, + } + + for _, c := range cases { + o := GetTimestamp(c.in) + if o != c.expected { + t.Fatalf("wrong value for '%s'. expected:'%s' got:'%s'", c.in, c.expected, o) + } + } +}