Fixes hacks from progressreader refactor
related to #10959 Signed-off-by: bobby abbott <ttobbaybbob@gmail.com>
This commit is contained in:
parent
0ee2488659
commit
98e8ec854e
5 changed files with 396 additions and 26 deletions
172
jsonmessage/jsonmessage.go
Normal file
172
jsonmessage/jsonmessage.go
Normal file
|
@ -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 {
|
||||
// <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.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 {
|
||||
// <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
|
||||
}
|
38
jsonmessage/jsonmessage_test.go
Normal file
38
jsonmessage/jsonmessage_test.go
Normal file
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -1,37 +1,16 @@
|
|||
package progressreader
|
||||
|
||||
import (
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"github.com/docker/docker/pkg/streamformatter"
|
||||
"io"
|
||||
)
|
||||
|
||||
type StreamFormatter interface {
|
||||
FormatProg(string, string, interface{}) []byte
|
||||
FormatStatus(string, string, ...interface{}) []byte
|
||||
FormatError(error) []byte
|
||||
}
|
||||
|
||||
type PR_JSONProgress interface {
|
||||
GetCurrent() int
|
||||
GetTotal() int
|
||||
}
|
||||
|
||||
type JSONProg struct {
|
||||
Current int
|
||||
Total int
|
||||
}
|
||||
|
||||
func (j *JSONProg) GetCurrent() int {
|
||||
return j.Current
|
||||
}
|
||||
func (j *JSONProg) GetTotal() int {
|
||||
return j.Total
|
||||
}
|
||||
|
||||
// Reader with progress bar
|
||||
type Config struct {
|
||||
In io.ReadCloser // Stream to read from
|
||||
Out io.Writer // Where to send progress bar to
|
||||
Formatter StreamFormatter
|
||||
Formatter *streamformatter.StreamFormatter
|
||||
Size int
|
||||
Current int
|
||||
LastUpdate int
|
||||
|
@ -54,7 +33,7 @@ func (config *Config) Read(p []byte) (n int, err error) {
|
|||
}
|
||||
}
|
||||
if config.Current-config.LastUpdate > updateEvery || err != nil {
|
||||
config.Out.Write(config.Formatter.FormatProg(config.ID, config.Action, &JSONProg{Current: config.Current, Total: config.Size}))
|
||||
config.Out.Write(config.Formatter.FormatProgress(config.ID, config.Action, &jsonmessage.JSONProgress{Current: config.Current, Total: config.Size}))
|
||||
config.LastUpdate = config.Current
|
||||
}
|
||||
// Send newline when complete
|
||||
|
@ -64,6 +43,6 @@ func (config *Config) Read(p []byte) (n int, err error) {
|
|||
return read, err
|
||||
}
|
||||
func (config *Config) Close() error {
|
||||
config.Out.Write(config.Formatter.FormatProg(config.ID, config.Action, &JSONProg{Current: config.Current, Total: config.Size}))
|
||||
config.Out.Write(config.Formatter.FormatProgress(config.ID, config.Action, &jsonmessage.JSONProgress{Current: config.Current, Total: config.Size}))
|
||||
return config.In.Close()
|
||||
}
|
||||
|
|
113
streamformatter/streamformatter.go
Normal file
113
streamformatter/streamformatter.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package streamformatter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"io"
|
||||
)
|
||||
|
||||
type StreamFormatter struct {
|
||||
json bool
|
||||
}
|
||||
|
||||
func NewStreamFormatter(json bool) *StreamFormatter {
|
||||
return &StreamFormatter{json}
|
||||
}
|
||||
|
||||
const streamNewline = "\r\n"
|
||||
|
||||
var streamNewlineBytes = []byte(streamNewline)
|
||||
|
||||
func (sf *StreamFormatter) FormatStream(str string) []byte {
|
||||
if sf.json {
|
||||
b, err := json.Marshal(&jsonmessage.JSONMessage{Stream: str})
|
||||
if err != nil {
|
||||
return sf.FormatError(err)
|
||||
}
|
||||
return append(b, streamNewlineBytes...)
|
||||
}
|
||||
return []byte(str + "\r")
|
||||
}
|
||||
|
||||
func (sf *StreamFormatter) FormatStatus(id, format string, a ...interface{}) []byte {
|
||||
str := fmt.Sprintf(format, a...)
|
||||
if sf.json {
|
||||
b, err := json.Marshal(&jsonmessage.JSONMessage{ID: id, Status: str})
|
||||
if err != nil {
|
||||
return sf.FormatError(err)
|
||||
}
|
||||
return append(b, streamNewlineBytes...)
|
||||
}
|
||||
return []byte(str + streamNewline)
|
||||
}
|
||||
|
||||
func (sf *StreamFormatter) FormatError(err error) []byte {
|
||||
if sf.json {
|
||||
jsonError, ok := err.(*jsonmessage.JSONError)
|
||||
if !ok {
|
||||
jsonError = &jsonmessage.JSONError{Message: err.Error()}
|
||||
}
|
||||
if b, err := json.Marshal(&jsonmessage.JSONMessage{Error: jsonError, ErrorMessage: err.Error()}); err == nil {
|
||||
return append(b, streamNewlineBytes...)
|
||||
}
|
||||
return []byte("{\"error\":\"format error\"}" + streamNewline)
|
||||
}
|
||||
return []byte("Error: " + err.Error() + streamNewline)
|
||||
}
|
||||
|
||||
func (sf *StreamFormatter) FormatProgress(id, action string, progress *jsonmessage.JSONProgress) []byte {
|
||||
if progress == nil {
|
||||
progress = &jsonmessage.JSONProgress{}
|
||||
}
|
||||
if sf.json {
|
||||
|
||||
b, err := json.Marshal(&jsonmessage.JSONMessage{
|
||||
Status: action,
|
||||
ProgressMessage: progress.String(),
|
||||
Progress: progress,
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
}
|
||||
endl := "\r"
|
||||
if progress.String() == "" {
|
||||
endl += "\n"
|
||||
}
|
||||
return []byte(action + " " + progress.String() + endl)
|
||||
}
|
||||
|
||||
func (sf *StreamFormatter) Json() bool {
|
||||
return sf.json
|
||||
}
|
||||
|
||||
type StdoutFormater struct {
|
||||
io.Writer
|
||||
*StreamFormatter
|
||||
}
|
||||
|
||||
func (sf *StdoutFormater) Write(buf []byte) (int, error) {
|
||||
formattedBuf := sf.StreamFormatter.FormatStream(string(buf))
|
||||
n, err := sf.Writer.Write(formattedBuf)
|
||||
if n != len(formattedBuf) {
|
||||
return n, io.ErrShortWrite
|
||||
}
|
||||
return len(buf), err
|
||||
}
|
||||
|
||||
type StderrFormater struct {
|
||||
io.Writer
|
||||
*StreamFormatter
|
||||
}
|
||||
|
||||
func (sf *StderrFormater) Write(buf []byte) (int, error) {
|
||||
formattedBuf := sf.StreamFormatter.FormatStream("\033[91m" + string(buf) + "\033[0m")
|
||||
n, err := sf.Writer.Write(formattedBuf)
|
||||
if n != len(formattedBuf) {
|
||||
return n, io.ErrShortWrite
|
||||
}
|
||||
return len(buf), err
|
||||
}
|
68
streamformatter/streamformatter_test.go
Normal file
68
streamformatter/streamformatter_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package streamformatter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatStream(t *testing.T) {
|
||||
sf := NewStreamFormatter(true)
|
||||
res := sf.FormatStream("stream")
|
||||
if string(res) != `{"stream":"stream"}`+"\r\n" {
|
||||
t.Fatalf("%q", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatStatus(t *testing.T) {
|
||||
sf := NewStreamFormatter(true)
|
||||
res := sf.FormatStatus("ID", "%s%d", "a", 1)
|
||||
if string(res) != `{"status":"a1","id":"ID"}`+"\r\n" {
|
||||
t.Fatalf("%q", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSimpleError(t *testing.T) {
|
||||
sf := NewStreamFormatter(true)
|
||||
res := sf.FormatError(errors.New("Error for formatter"))
|
||||
if string(res) != `{"errorDetail":{"message":"Error for formatter"},"error":"Error for formatter"}`+"\r\n" {
|
||||
t.Fatalf("%q", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatJSONError(t *testing.T) {
|
||||
sf := NewStreamFormatter(true)
|
||||
err := &jsonmessage.JSONError{Code: 50, Message: "Json error"}
|
||||
res := sf.FormatError(err)
|
||||
if string(res) != `{"errorDetail":{"code":50,"message":"Json error"},"error":"Json error"}`+"\r\n" {
|
||||
t.Fatalf("%q", res)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatProgress(t *testing.T) {
|
||||
sf := NewStreamFormatter(true)
|
||||
progress := &jsonmessage.JSONProgress{
|
||||
Current: 15,
|
||||
Total: 30,
|
||||
Start: 1,
|
||||
}
|
||||
res := sf.FormatProgress("id", "action", progress)
|
||||
msg := &jsonmessage.JSONMessage{}
|
||||
if err := json.Unmarshal(res, msg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg.ID != "id" {
|
||||
t.Fatalf("ID must be 'id', got: %s", msg.ID)
|
||||
}
|
||||
if msg.Status != "action" {
|
||||
t.Fatalf("Status must be 'action', got: %s", msg.Status)
|
||||
}
|
||||
if msg.ProgressMessage != progress.String() {
|
||||
t.Fatalf("ProgressMessage must be %s, got: %s", progress.String(), msg.ProgressMessage)
|
||||
}
|
||||
if !reflect.DeepEqual(msg.Progress, progress) {
|
||||
t.Fatal("Original progress not equals progress from FormatProgress")
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue