mirror of
https://github.com/docker/compose.git
synced 2026-08-04 14:47:40 +00:00
Move compose v2 implementation under pkg/compose with dependencies
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
parent
e2ea24ceb7
commit
49e7f2d45d
93 changed files with 261 additions and 331 deletions
133
pkg/progress/event.go
Normal file
133
pkg/progress/event.go
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import "time"
|
||||
|
||||
// EventStatus indicates the status of an action
|
||||
type EventStatus int
|
||||
|
||||
const (
|
||||
// Working means that the current task is working
|
||||
Working EventStatus = iota
|
||||
// Done means that the current task is done
|
||||
Done
|
||||
// Error means that the current task has errored
|
||||
Error
|
||||
)
|
||||
|
||||
// Event represents a progress event.
|
||||
type Event struct {
|
||||
ID string
|
||||
ParentID string
|
||||
Text string
|
||||
Status EventStatus
|
||||
StatusText string
|
||||
|
||||
startTime time.Time
|
||||
endTime time.Time
|
||||
spinner *spinner
|
||||
}
|
||||
|
||||
// ErrorMessageEvent creates a new Error Event with message
|
||||
func ErrorMessageEvent(ID string, msg string) Event {
|
||||
return NewEvent(ID, Error, msg)
|
||||
}
|
||||
|
||||
// ErrorEvent creates a new Error Event
|
||||
func ErrorEvent(ID string) Event {
|
||||
return NewEvent(ID, Error, "Error")
|
||||
}
|
||||
|
||||
// CreatingEvent creates a new Create in progress Event
|
||||
func CreatingEvent(ID string) Event {
|
||||
return NewEvent(ID, Working, "Creating")
|
||||
}
|
||||
|
||||
// StartingEvent creates a new Starting in progress Event
|
||||
func StartingEvent(ID string) Event {
|
||||
return NewEvent(ID, Working, "Starting")
|
||||
}
|
||||
|
||||
// StartedEvent creates a new Started in progress Event
|
||||
func StartedEvent(ID string) Event {
|
||||
return NewEvent(ID, Done, "Started")
|
||||
}
|
||||
|
||||
// RestartingEvent creates a new Restarting in progress Event
|
||||
func RestartingEvent(ID string) Event {
|
||||
return NewEvent(ID, Working, "Restarting")
|
||||
}
|
||||
|
||||
// RestartedEvent creates a new Restarted in progress Event
|
||||
func RestartedEvent(ID string) Event {
|
||||
return NewEvent(ID, Done, "Restarted")
|
||||
}
|
||||
|
||||
// RunningEvent creates a new Running in progress Event
|
||||
func RunningEvent(ID string) Event {
|
||||
return NewEvent(ID, Done, "Running")
|
||||
}
|
||||
|
||||
// CreatedEvent creates a new Created (done) Event
|
||||
func CreatedEvent(ID string) Event {
|
||||
return NewEvent(ID, Done, "Created")
|
||||
}
|
||||
|
||||
// StoppingEvent creates a new Stopping in progress Event
|
||||
func StoppingEvent(ID string) Event {
|
||||
return NewEvent(ID, Working, "Stopping")
|
||||
}
|
||||
|
||||
// StoppedEvent creates a new Stopping in progress Event
|
||||
func StoppedEvent(ID string) Event {
|
||||
return NewEvent(ID, Done, "Stopped")
|
||||
}
|
||||
|
||||
// KillingEvent creates a new Killing in progress Event
|
||||
func KillingEvent(ID string) Event {
|
||||
return NewEvent(ID, Working, "Killing")
|
||||
}
|
||||
|
||||
// KilledEvent creates a new Killed in progress Event
|
||||
func KilledEvent(ID string) Event {
|
||||
return NewEvent(ID, Done, "Killed")
|
||||
}
|
||||
|
||||
// RemovingEvent creates a new Removing in progress Event
|
||||
func RemovingEvent(ID string) Event {
|
||||
return NewEvent(ID, Working, "Removing")
|
||||
}
|
||||
|
||||
// RemovedEvent creates a new removed (done) Event
|
||||
func RemovedEvent(ID string) Event {
|
||||
return NewEvent(ID, Done, "Removed")
|
||||
}
|
||||
|
||||
// NewEvent new event
|
||||
func NewEvent(ID string, status EventStatus, statusText string) Event {
|
||||
return Event{
|
||||
ID: ID,
|
||||
Status: status,
|
||||
StatusText: statusText,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Event) stop() {
|
||||
e.endTime = time.Now()
|
||||
e.spinner.Stop()
|
||||
}
|
||||
37
pkg/progress/noop.go
Normal file
37
pkg/progress/noop.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type noopWriter struct {
|
||||
}
|
||||
|
||||
func (p *noopWriter) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *noopWriter) Event(e Event) {
|
||||
}
|
||||
|
||||
func (p *noopWriter) TailMsgf(_ string, _ ...interface{}) {
|
||||
}
|
||||
|
||||
func (p *noopWriter) Stop() {
|
||||
}
|
||||
49
pkg/progress/plain.go
Normal file
49
pkg/progress/plain.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type plainWriter struct {
|
||||
out io.Writer
|
||||
done chan bool
|
||||
}
|
||||
|
||||
func (p *plainWriter) Start(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-p.done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *plainWriter) Event(e Event) {
|
||||
fmt.Fprintln(p.out, e.ID, e.Text, e.StatusText)
|
||||
}
|
||||
|
||||
func (p *plainWriter) TailMsgf(m string, args ...interface{}) {
|
||||
fmt.Fprintln(p.out, append([]interface{}{m}, args...)...)
|
||||
}
|
||||
|
||||
func (p *plainWriter) Stop() {
|
||||
p.done <- true
|
||||
}
|
||||
66
pkg/progress/spinner.go
Normal file
66
pkg/progress/spinner.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type spinner struct {
|
||||
time time.Time
|
||||
index int
|
||||
chars []string
|
||||
stop bool
|
||||
done string
|
||||
}
|
||||
|
||||
func newSpinner() *spinner {
|
||||
chars := []string{
|
||||
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
|
||||
}
|
||||
done := "⠿"
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
chars = []string{"-"}
|
||||
done = "-"
|
||||
}
|
||||
|
||||
return &spinner{
|
||||
index: 0,
|
||||
time: time.Now(),
|
||||
chars: chars,
|
||||
done: done,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *spinner) String() string {
|
||||
if s.stop {
|
||||
return s.done
|
||||
}
|
||||
|
||||
d := time.Since(s.time)
|
||||
if d.Milliseconds() > 100 {
|
||||
s.index = (s.index + 1) % len(s.chars)
|
||||
}
|
||||
|
||||
return s.chars[s.index]
|
||||
}
|
||||
|
||||
func (s *spinner) Stop() {
|
||||
s.stop = true
|
||||
}
|
||||
235
pkg/progress/tty.go
Normal file
235
pkg/progress/tty.go
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/compose-cli/pkg/utils"
|
||||
|
||||
"github.com/buger/goterm"
|
||||
"github.com/morikuni/aec"
|
||||
)
|
||||
|
||||
type ttyWriter struct {
|
||||
out io.Writer
|
||||
events map[string]Event
|
||||
eventIDs []string
|
||||
repeated bool
|
||||
numLines int
|
||||
done chan bool
|
||||
mtx *sync.RWMutex
|
||||
tailEvents []string
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Start(ctx context.Context) error {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
w.print()
|
||||
w.printTailEvents()
|
||||
return ctx.Err()
|
||||
case <-w.done:
|
||||
w.print()
|
||||
w.printTailEvents()
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
w.print()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Stop() {
|
||||
w.done <- true
|
||||
}
|
||||
|
||||
func (w *ttyWriter) Event(e Event) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
if !utils.StringContains(w.eventIDs, e.ID) {
|
||||
w.eventIDs = append(w.eventIDs, e.ID)
|
||||
}
|
||||
if _, ok := w.events[e.ID]; ok {
|
||||
last := w.events[e.ID]
|
||||
switch e.Status {
|
||||
case Done, Error:
|
||||
if last.Status != e.Status {
|
||||
last.stop()
|
||||
}
|
||||
}
|
||||
last.Status = e.Status
|
||||
last.Text = e.Text
|
||||
last.StatusText = e.StatusText
|
||||
last.ParentID = e.ParentID
|
||||
w.events[e.ID] = last
|
||||
} else {
|
||||
e.startTime = time.Now()
|
||||
e.spinner = newSpinner()
|
||||
if e.Status == Done || e.Status == Error {
|
||||
e.stop()
|
||||
}
|
||||
w.events[e.ID] = e
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) TailMsgf(msg string, args ...interface{}) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
w.tailEvents = append(w.tailEvents, fmt.Sprintf(msg, args...))
|
||||
}
|
||||
|
||||
func (w *ttyWriter) printTailEvents() {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
for _, msg := range w.tailEvents {
|
||||
fmt.Fprintln(w.out, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *ttyWriter) print() {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
if len(w.eventIDs) == 0 {
|
||||
return
|
||||
}
|
||||
terminalWidth := goterm.Width()
|
||||
b := aec.EmptyBuilder
|
||||
for i := 0; i <= w.numLines; i++ {
|
||||
b = b.Up(1)
|
||||
}
|
||||
if !w.repeated {
|
||||
b = b.Down(1)
|
||||
}
|
||||
w.repeated = true
|
||||
fmt.Fprint(w.out, b.Column(0).ANSI)
|
||||
|
||||
// Hide the cursor while we are printing
|
||||
fmt.Fprint(w.out, aec.Hide)
|
||||
defer fmt.Fprint(w.out, aec.Show)
|
||||
|
||||
firstLine := fmt.Sprintf("[+] Running %d/%d", numDone(w.events), w.numLines)
|
||||
if w.numLines != 0 && numDone(w.events) == w.numLines {
|
||||
firstLine = aec.Apply(firstLine, aec.BlueF)
|
||||
}
|
||||
fmt.Fprintln(w.out, firstLine)
|
||||
|
||||
var statusPadding int
|
||||
for _, v := range w.eventIDs {
|
||||
event := w.events[v]
|
||||
l := len(fmt.Sprintf("%s %s", event.ID, event.Text))
|
||||
if statusPadding < l {
|
||||
statusPadding = l
|
||||
}
|
||||
if event.ParentID != "" {
|
||||
statusPadding -= 2
|
||||
}
|
||||
}
|
||||
|
||||
numLines := 0
|
||||
for _, v := range w.eventIDs {
|
||||
event := w.events[v]
|
||||
if event.ParentID != "" {
|
||||
continue
|
||||
}
|
||||
line := lineText(event, "", terminalWidth, statusPadding, runtime.GOOS != "windows")
|
||||
// nolint: errcheck
|
||||
fmt.Fprint(w.out, line)
|
||||
numLines++
|
||||
for _, v := range w.eventIDs {
|
||||
ev := w.events[v]
|
||||
if ev.ParentID == event.ID {
|
||||
line := lineText(ev, " ", terminalWidth, statusPadding, runtime.GOOS != "windows")
|
||||
// nolint: errcheck
|
||||
fmt.Fprint(w.out, line)
|
||||
numLines++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.numLines = numLines
|
||||
}
|
||||
|
||||
func lineText(event Event, pad string, terminalWidth, statusPadding int, color bool) string {
|
||||
endTime := time.Now()
|
||||
if event.Status != Working {
|
||||
endTime = event.startTime
|
||||
if (event.endTime != time.Time{}) {
|
||||
endTime = event.endTime
|
||||
}
|
||||
}
|
||||
|
||||
elapsed := endTime.Sub(event.startTime).Seconds()
|
||||
|
||||
textLen := len(fmt.Sprintf("%s %s", event.ID, event.Text))
|
||||
padding := statusPadding - textLen
|
||||
if padding < 0 {
|
||||
padding = 0
|
||||
}
|
||||
// calculate the max length for the status text, on errors it
|
||||
// is 2-3 lines long and breaks the line formating
|
||||
maxStatusLen := terminalWidth - textLen - statusPadding - 15
|
||||
status := event.StatusText
|
||||
// in some cases (debugging under VS Code), terminalWidth is set to zero by goterm.Width() ; ensuring we don't tweak strings with negative char index
|
||||
if maxStatusLen > 0 && len(status) > maxStatusLen {
|
||||
status = status[:maxStatusLen] + "..."
|
||||
}
|
||||
text := fmt.Sprintf("%s %s %s %s%s %s",
|
||||
pad,
|
||||
event.spinner.String(),
|
||||
event.ID,
|
||||
event.Text,
|
||||
strings.Repeat(" ", padding),
|
||||
status,
|
||||
)
|
||||
timer := fmt.Sprintf("%.1fs\n", elapsed)
|
||||
o := align(text, timer, terminalWidth)
|
||||
|
||||
if color {
|
||||
color := aec.WhiteF
|
||||
if event.Status == Done {
|
||||
color = aec.BlueF
|
||||
}
|
||||
if event.Status == Error {
|
||||
color = aec.RedF
|
||||
}
|
||||
return aec.Apply(o, color)
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func numDone(events map[string]Event) int {
|
||||
i := 0
|
||||
for _, e := range events {
|
||||
if e.Status == Done {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func align(l, r string, w int) string {
|
||||
return fmt.Sprintf("%-[2]*[1]s %[3]s", l, w-len(r)-1, r)
|
||||
}
|
||||
105
pkg/progress/tty_test.go
Normal file
105
pkg/progress/tty_test.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestLineText(t *testing.T) {
|
||||
now := time.Now()
|
||||
ev := Event{
|
||||
ID: "id",
|
||||
Text: "Text",
|
||||
Status: Working,
|
||||
StatusText: "Status",
|
||||
endTime: now,
|
||||
startTime: now,
|
||||
spinner: &spinner{
|
||||
chars: []string{"."},
|
||||
},
|
||||
}
|
||||
|
||||
lineWidth := len(fmt.Sprintf("%s %s", ev.ID, ev.Text))
|
||||
|
||||
out := lineText(ev, "", 50, lineWidth, true)
|
||||
assert.Equal(t, out, "\x1b[37m . id Text Status 0.0s\n\x1b[0m")
|
||||
|
||||
out = lineText(ev, "", 50, lineWidth, false)
|
||||
assert.Equal(t, out, " . id Text Status 0.0s\n")
|
||||
|
||||
ev.Status = Done
|
||||
out = lineText(ev, "", 50, lineWidth, true)
|
||||
assert.Equal(t, out, "\x1b[34m . id Text Status 0.0s\n\x1b[0m")
|
||||
|
||||
ev.Status = Error
|
||||
out = lineText(ev, "", 50, lineWidth, true)
|
||||
assert.Equal(t, out, "\x1b[31m . id Text Status 0.0s\n\x1b[0m")
|
||||
}
|
||||
|
||||
func TestLineTextSingleEvent(t *testing.T) {
|
||||
now := time.Now()
|
||||
ev := Event{
|
||||
ID: "id",
|
||||
Text: "Text",
|
||||
Status: Done,
|
||||
StatusText: "Status",
|
||||
startTime: now,
|
||||
spinner: &spinner{
|
||||
chars: []string{"."},
|
||||
},
|
||||
}
|
||||
|
||||
lineWidth := len(fmt.Sprintf("%s %s", ev.ID, ev.Text))
|
||||
|
||||
out := lineText(ev, "", 50, lineWidth, true)
|
||||
assert.Equal(t, out, "\x1b[34m . id Text Status 0.0s\n\x1b[0m")
|
||||
}
|
||||
|
||||
func TestErrorEvent(t *testing.T) {
|
||||
w := &ttyWriter{
|
||||
events: map[string]Event{},
|
||||
mtx: &sync.RWMutex{},
|
||||
}
|
||||
e := Event{
|
||||
ID: "id",
|
||||
Text: "Text",
|
||||
Status: Working,
|
||||
StatusText: "Working",
|
||||
startTime: time.Now(),
|
||||
spinner: &spinner{
|
||||
chars: []string{"."},
|
||||
},
|
||||
}
|
||||
// Fire "Working" event and check end time isn't touched
|
||||
w.Event(e)
|
||||
event, ok := w.events[e.ID]
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, event.endTime.Equal(time.Time{}))
|
||||
|
||||
// Fire "Error" event and check end time is set
|
||||
e.Status = Error
|
||||
w.Event(e)
|
||||
event, ok = w.events[e.ID]
|
||||
assert.Assert(t, ok)
|
||||
assert.Assert(t, event.endTime.After(time.Now().Add(-10*time.Second)))
|
||||
}
|
||||
116
pkg/progress/writer.go
Normal file
116
pkg/progress/writer.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/moby/term"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// Writer can write multiple progress events
|
||||
type Writer interface {
|
||||
Start(context.Context) error
|
||||
Stop()
|
||||
Event(Event)
|
||||
TailMsgf(string, ...interface{})
|
||||
}
|
||||
|
||||
type writerKey struct{}
|
||||
|
||||
// WithContextWriter adds the writer to the context
|
||||
func WithContextWriter(ctx context.Context, writer Writer) context.Context {
|
||||
return context.WithValue(ctx, writerKey{}, writer)
|
||||
}
|
||||
|
||||
// ContextWriter returns the writer from the context
|
||||
func ContextWriter(ctx context.Context) Writer {
|
||||
s, ok := ctx.Value(writerKey{}).(Writer)
|
||||
if !ok {
|
||||
return &noopWriter{}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
type progressFunc func(context.Context) error
|
||||
|
||||
type progressFuncWithStatus func(context.Context) (string, error)
|
||||
|
||||
// Run will run a writer and the progress function in parallel
|
||||
func Run(ctx context.Context, pf progressFunc) error {
|
||||
_, err := RunWithStatus(ctx, func(ctx context.Context) (string, error) {
|
||||
return "", pf(ctx)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// RunWithStatus will run a writer and the progress function in parallel and return a status
|
||||
func RunWithStatus(ctx context.Context, pf progressFuncWithStatus) (string, error) {
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
w, err := NewWriter(os.Stderr)
|
||||
var result string
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
eg.Go(func() error {
|
||||
return w.Start(context.Background())
|
||||
})
|
||||
|
||||
ctx = WithContextWriter(ctx, w)
|
||||
|
||||
eg.Go(func() error {
|
||||
defer w.Stop()
|
||||
s, err := pf(ctx)
|
||||
if err == nil {
|
||||
result = s
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
err = eg.Wait()
|
||||
return result, err
|
||||
}
|
||||
|
||||
// NewWriter returns a new multi-progress writer
|
||||
func NewWriter(out console.File) (Writer, error) {
|
||||
_, isTerminal := term.GetFdInfo(out)
|
||||
|
||||
if isTerminal {
|
||||
con, err := console.ConsoleFromFile(out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ttyWriter{
|
||||
out: con,
|
||||
eventIDs: []string{},
|
||||
events: map[string]Event{},
|
||||
repeated: false,
|
||||
done: make(chan bool),
|
||||
mtx: &sync.RWMutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return &plainWriter{
|
||||
out: out,
|
||||
done: make(chan bool),
|
||||
}, nil
|
||||
}
|
||||
31
pkg/progress/writer_test.go
Normal file
31
pkg/progress/writer_test.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
Copyright 2020 Docker Compose CLI authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestNoopWriter(t *testing.T) {
|
||||
todo := context.TODO()
|
||||
writer := ContextWriter(todo)
|
||||
|
||||
assert.Equal(t, writer, &noopWriter{})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue