mirror of
https://github.com/docker/compose.git
synced 2025-07-23 21:54:40 +02:00
keep containers attached on stop to capture termination logs
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
parent
0b0242d0ac
commit
29308cb97e
@ -28,49 +28,49 @@ func ansi(code string) string {
|
|||||||
return fmt.Sprintf("\033%s", code)
|
return fmt.Sprintf("\033%s", code)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveCursor() {
|
func saveCursor() {
|
||||||
if disableAnsi {
|
if disableAnsi {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Print(ansi("7"))
|
fmt.Print(ansi("7"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func RestoreCursor() {
|
func restoreCursor() {
|
||||||
if disableAnsi {
|
if disableAnsi {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Print(ansi("8"))
|
fmt.Print(ansi("8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func HideCursor() {
|
func hideCursor() {
|
||||||
if disableAnsi {
|
if disableAnsi {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Print(ansi("[?25l"))
|
fmt.Print(ansi("[?25l"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShowCursor() {
|
func showCursor() {
|
||||||
if disableAnsi {
|
if disableAnsi {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Print(ansi("[?25h"))
|
fmt.Print(ansi("[?25h"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func MoveCursor(y, x int) {
|
func moveCursor(y, x int) {
|
||||||
if disableAnsi {
|
if disableAnsi {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Print(ansi(fmt.Sprintf("[%d;%dH", y, x)))
|
fmt.Print(ansi(fmt.Sprintf("[%d;%dH", y, x)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func MoveCursorX(pos int) {
|
func carriageReturn() {
|
||||||
if disableAnsi {
|
if disableAnsi {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Print(ansi(fmt.Sprintf("[%dG", pos)))
|
fmt.Print(ansi(fmt.Sprintf("[%dG", 0)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClearLine() {
|
func clearLine() {
|
||||||
if disableAnsi {
|
if disableAnsi {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -78,7 +78,7 @@ func ClearLine() {
|
|||||||
fmt.Print(ansi("[2K"))
|
fmt.Print(ansi("[2K"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func MoveCursorUp(lines int) {
|
func moveCursorUp(lines int) {
|
||||||
if disableAnsi {
|
if disableAnsi {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -86,7 +86,7 @@ func MoveCursorUp(lines int) {
|
|||||||
fmt.Print(ansi(fmt.Sprintf("[%dA", lines)))
|
fmt.Print(ansi(fmt.Sprintf("[%dA", lines)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func MoveCursorDown(lines int) {
|
func moveCursorDown(lines int) {
|
||||||
if disableAnsi {
|
if disableAnsi {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -94,7 +94,7 @@ func MoveCursorDown(lines int) {
|
|||||||
fmt.Print(ansi(fmt.Sprintf("[%dB", lines)))
|
fmt.Print(ansi(fmt.Sprintf("[%dB", lines)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLine() {
|
func newLine() {
|
||||||
// Like \n
|
// Like \n
|
||||||
fmt.Print("\012")
|
fmt.Print("\012")
|
||||||
}
|
}
|
||||||
|
@ -73,9 +73,12 @@ func (l *logConsumer) register(name string) *presenter {
|
|||||||
} else {
|
} else {
|
||||||
cf := monochrome
|
cf := monochrome
|
||||||
if l.color {
|
if l.color {
|
||||||
if name == api.WatchLogger {
|
switch name {
|
||||||
|
case "":
|
||||||
|
cf = monochrome
|
||||||
|
case api.WatchLogger:
|
||||||
cf = makeColorFunc("92")
|
cf = makeColorFunc("92")
|
||||||
} else {
|
default:
|
||||||
cf = nextColor()
|
cf = nextColor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,8 +48,8 @@ func (ke *KeyboardError) printError(height int, info string) {
|
|||||||
if ke.shouldDisplay() {
|
if ke.shouldDisplay() {
|
||||||
errMessage := ke.err.Error()
|
errMessage := ke.err.Error()
|
||||||
|
|
||||||
MoveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
|
moveCursor(height-1-extraLines(info)-extraLines(errMessage), 0)
|
||||||
ClearLine()
|
clearLine()
|
||||||
|
|
||||||
fmt.Print(errMessage)
|
fmt.Print(errMessage)
|
||||||
}
|
}
|
||||||
@ -133,7 +133,7 @@ func (lk *LogKeyboard) createBuffer(lines int) {
|
|||||||
|
|
||||||
if lines > 0 {
|
if lines > 0 {
|
||||||
allocateSpace(lines)
|
allocateSpace(lines)
|
||||||
MoveCursorUp(lines)
|
moveCursorUp(lines)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,17 +146,17 @@ func (lk *LogKeyboard) printNavigationMenu() {
|
|||||||
height := goterm.Height()
|
height := goterm.Height()
|
||||||
menu := lk.navigationMenu()
|
menu := lk.navigationMenu()
|
||||||
|
|
||||||
MoveCursorX(0)
|
carriageReturn()
|
||||||
SaveCursor()
|
saveCursor()
|
||||||
|
|
||||||
lk.kError.printError(height, menu)
|
lk.kError.printError(height, menu)
|
||||||
|
|
||||||
MoveCursor(height-extraLines(menu), 0)
|
moveCursor(height-extraLines(menu), 0)
|
||||||
ClearLine()
|
clearLine()
|
||||||
fmt.Print(menu)
|
fmt.Print(menu)
|
||||||
|
|
||||||
MoveCursorX(0)
|
carriageReturn()
|
||||||
RestoreCursor()
|
restoreCursor()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,15 +188,15 @@ func (lk *LogKeyboard) navigationMenu() string {
|
|||||||
|
|
||||||
func (lk *LogKeyboard) clearNavigationMenu() {
|
func (lk *LogKeyboard) clearNavigationMenu() {
|
||||||
height := goterm.Height()
|
height := goterm.Height()
|
||||||
MoveCursorX(0)
|
carriageReturn()
|
||||||
SaveCursor()
|
saveCursor()
|
||||||
|
|
||||||
// ClearLine()
|
// clearLine()
|
||||||
for i := 0; i < height; i++ {
|
for i := 0; i < height; i++ {
|
||||||
MoveCursorDown(1)
|
moveCursorDown(1)
|
||||||
ClearLine()
|
clearLine()
|
||||||
}
|
}
|
||||||
RestoreCursor()
|
restoreCursor()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
|
func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Project) {
|
||||||
@ -316,13 +316,13 @@ func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEv
|
|||||||
case keyboard.KeyCtrlC:
|
case keyboard.KeyCtrlC:
|
||||||
_ = keyboard.Close()
|
_ = keyboard.Close()
|
||||||
lk.clearNavigationMenu()
|
lk.clearNavigationMenu()
|
||||||
ShowCursor()
|
showCursor()
|
||||||
|
|
||||||
lk.logLevel = NONE
|
lk.logLevel = NONE
|
||||||
// will notify main thread to kill and will handle gracefully
|
// will notify main thread to kill and will handle gracefully
|
||||||
lk.signalChannel <- syscall.SIGINT
|
lk.signalChannel <- syscall.SIGINT
|
||||||
case keyboard.KeyEnter:
|
case keyboard.KeyEnter:
|
||||||
NewLine()
|
newLine()
|
||||||
lk.printNavigationMenu()
|
lk.printNavigationMenu()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -336,9 +336,9 @@ func (lk *LogKeyboard) EnableWatch(enabled bool, watcher Feature) {
|
|||||||
|
|
||||||
func allocateSpace(lines int) {
|
func allocateSpace(lines int) {
|
||||||
for i := 0; i < lines; i++ {
|
for i := 0; i < lines; i++ {
|
||||||
ClearLine()
|
clearLine()
|
||||||
NewLine()
|
newLine()
|
||||||
MoveCursorX(0)
|
carriageReturn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
119
cmd/formatter/stopping.go
Normal file
119
cmd/formatter/stopping.go
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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 formatter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/buger/goterm"
|
||||||
|
"github.com/docker/compose/v2/pkg/api"
|
||||||
|
"github.com/docker/compose/v2/pkg/progress"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Stopping struct {
|
||||||
|
api.LogConsumer
|
||||||
|
enabled bool
|
||||||
|
spinner *progress.Spinner
|
||||||
|
ticker *time.Ticker
|
||||||
|
startedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStopping(l api.LogConsumer) *Stopping {
|
||||||
|
s := &Stopping{}
|
||||||
|
s.LogConsumer = logDecorator{
|
||||||
|
decorated: l,
|
||||||
|
Before: s.clear,
|
||||||
|
After: s.print,
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stopping) ApplicationTermination() {
|
||||||
|
if progress.Mode != progress.ModeAuto {
|
||||||
|
// User explicitly opted for output format
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if disableAnsi {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.enabled = true
|
||||||
|
s.spinner = progress.NewSpinner()
|
||||||
|
hideCursor()
|
||||||
|
s.startedAt = time.Now()
|
||||||
|
s.ticker = time.NewTicker(100 * time.Millisecond)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
<-s.ticker.C
|
||||||
|
s.print()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stopping) Close() {
|
||||||
|
showCursor()
|
||||||
|
if s.ticker != nil {
|
||||||
|
s.ticker.Stop()
|
||||||
|
}
|
||||||
|
s.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stopping) clear() {
|
||||||
|
if !s.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
height := goterm.Height()
|
||||||
|
carriageReturn()
|
||||||
|
saveCursor()
|
||||||
|
|
||||||
|
// clearLine()
|
||||||
|
for i := 0; i < height; i++ {
|
||||||
|
moveCursorDown(1)
|
||||||
|
clearLine()
|
||||||
|
}
|
||||||
|
restoreCursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
const stoppingBanner = "Gracefully Stopping... (press Ctrl+C again to force)"
|
||||||
|
|
||||||
|
func (s *Stopping) print() {
|
||||||
|
if !s.enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
height := goterm.Height()
|
||||||
|
width := goterm.Width()
|
||||||
|
carriageReturn()
|
||||||
|
saveCursor()
|
||||||
|
|
||||||
|
moveCursor(height, 0)
|
||||||
|
clearLine()
|
||||||
|
elapsed := time.Since(s.startedAt).Seconds()
|
||||||
|
timer := fmt.Sprintf("%.1fs ", elapsed)
|
||||||
|
pad := width - len(timer) - len(stoppingBanner) - 5
|
||||||
|
fmt.Printf("%s %s %s %s",
|
||||||
|
progress.CountColor(s.spinner.String()),
|
||||||
|
stoppingBanner,
|
||||||
|
strings.Repeat(" ", pad),
|
||||||
|
progress.TimerColor(timer),
|
||||||
|
)
|
||||||
|
|
||||||
|
carriageReturn()
|
||||||
|
restoreCursor()
|
||||||
|
}
|
@ -236,7 +236,7 @@ func (c *convergence) stopDependentContainers(ctx context.Context, project *type
|
|||||||
err := c.service.stop(ctx, project.Name, api.StopOptions{
|
err := c.service.stop(ctx, project.Name, api.StopOptions{
|
||||||
Services: dependents,
|
Services: dependents,
|
||||||
Project: project,
|
Project: project,
|
||||||
})
|
}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1428,7 +1428,7 @@ func (s *composeService) removeDivergedNetwork(ctx context.Context, project *typ
|
|||||||
err := s.stop(ctx, project.Name, api.StopOptions{
|
err := s.stop(ctx, project.Name, api.StopOptions{
|
||||||
Services: services,
|
Services: services,
|
||||||
Project: project,
|
Project: project,
|
||||||
})
|
}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1599,7 +1599,7 @@ func (s *composeService) removeDivergedVolume(ctx context.Context, name string,
|
|||||||
err := s.stop(ctx, project.Name, api.StopOptions{
|
err := s.stop(ctx, project.Name, api.StopOptions{
|
||||||
Services: services,
|
Services: services,
|
||||||
Project: project,
|
Project: project,
|
||||||
})
|
}, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -298,13 +298,17 @@ func (s *composeService) removeVolume(ctx context.Context, id string, w progress
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, service *types.ServiceConfig, ctr containerType.Summary, timeout *time.Duration) error {
|
func (s *composeService) stopContainer(
|
||||||
|
ctx context.Context, w progress.Writer,
|
||||||
|
service *types.ServiceConfig, ctr containerType.Summary,
|
||||||
|
timeout *time.Duration, listener api.ContainerEventListener,
|
||||||
|
) error {
|
||||||
eventName := getContainerProgressName(ctr)
|
eventName := getContainerProgressName(ctr)
|
||||||
w.Event(progress.StoppingEvent(eventName))
|
w.Event(progress.StoppingEvent(eventName))
|
||||||
|
|
||||||
if service != nil {
|
if service != nil {
|
||||||
for _, hook := range service.PreStop {
|
for _, hook := range service.PreStop {
|
||||||
err := s.runHook(ctx, ctr, *service, hook, nil)
|
err := s.runHook(ctx, ctr, *service, hook, listener)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Ignore errors indicating that some containers were already stopped or removed.
|
// Ignore errors indicating that some containers were already stopped or removed.
|
||||||
if cerrdefs.IsNotFound(err) || cerrdefs.IsConflict(err) {
|
if cerrdefs.IsNotFound(err) || cerrdefs.IsConflict(err) {
|
||||||
@ -325,11 +329,15 @@ func (s *composeService) stopContainer(ctx context.Context, w progress.Writer, s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *composeService) stopContainers(ctx context.Context, w progress.Writer, serv *types.ServiceConfig, containers []containerType.Summary, timeout *time.Duration) error {
|
func (s *composeService) stopContainers(
|
||||||
|
ctx context.Context, w progress.Writer,
|
||||||
|
serv *types.ServiceConfig, containers []containerType.Summary,
|
||||||
|
timeout *time.Duration, listener api.ContainerEventListener,
|
||||||
|
) error {
|
||||||
eg, ctx := errgroup.WithContext(ctx)
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
return s.stopContainer(ctx, w, serv, ctr, timeout)
|
return s.stopContainer(ctx, w, serv, ctr, timeout, listener)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return eg.Wait()
|
return eg.Wait()
|
||||||
@ -348,7 +356,7 @@ func (s *composeService) removeContainers(ctx context.Context, containers []cont
|
|||||||
func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr containerType.Summary, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
|
func (s *composeService) stopAndRemoveContainer(ctx context.Context, ctr containerType.Summary, service *types.ServiceConfig, timeout *time.Duration, volumes bool) error {
|
||||||
w := progress.ContextWriter(ctx)
|
w := progress.ContextWriter(ctx)
|
||||||
eventName := getContainerProgressName(ctr)
|
eventName := getContainerProgressName(ctr)
|
||||||
err := s.stopContainer(ctx, w, service, ctr, timeout)
|
err := s.stopContainer(ctx, w, service, ctr, timeout, nil)
|
||||||
if cerrdefs.IsNotFound(err) {
|
if cerrdefs.IsNotFound(err) {
|
||||||
w.Event(progress.RemovedEvent(eventName))
|
w.Event(progress.RemovedEvent(eventName))
|
||||||
return nil
|
return nil
|
||||||
|
@ -149,9 +149,7 @@ func (p *printer) Run(cascade api.Cascade, exitCodeFrom string, stopFn func() er
|
|||||||
return exitCode, nil
|
return exitCode, nil
|
||||||
}
|
}
|
||||||
case api.ContainerEventLog, api.HookEventLog:
|
case api.ContainerEventLog, api.HookEventLog:
|
||||||
if !aborting {
|
p.consumer.Log(container, event.Line)
|
||||||
p.consumer.Log(container, event.Line)
|
|
||||||
}
|
|
||||||
case api.ContainerEventErr:
|
case api.ContainerEventErr:
|
||||||
if !aborting {
|
if !aborting {
|
||||||
p.consumer.Err(container, event.Line)
|
p.consumer.Err(container, event.Line)
|
||||||
|
@ -27,11 +27,11 @@ import (
|
|||||||
|
|
||||||
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
|
func (s *composeService) Stop(ctx context.Context, projectName string, options api.StopOptions) error {
|
||||||
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
return progress.RunWithTitle(ctx, func(ctx context.Context) error {
|
||||||
return s.stop(ctx, strings.ToLower(projectName), options)
|
return s.stop(ctx, strings.ToLower(projectName), options, nil)
|
||||||
}, s.stdinfo(), "Stopping")
|
}, s.stdinfo(), "Stopping")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions) error {
|
func (s *composeService) stop(ctx context.Context, projectName string, options api.StopOptions, event api.ContainerEventListener) error {
|
||||||
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true)
|
containers, err := s.getContainers(ctx, projectName, oneOffExclude, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -55,6 +55,6 @@ func (s *composeService) stop(ctx context.Context, projectName string, options a
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
serv := project.Services[service]
|
serv := project.Services[service]
|
||||||
return s.stopContainers(ctx, w, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout)
|
return s.stopContainers(ctx, w, &serv, containers.filter(isService(service)).filter(isNotOneOff), options.Timeout, event)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,10 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tui := formatter.NewStopping(logConsumer)
|
||||||
|
defer tui.Close()
|
||||||
|
logConsumer = tui
|
||||||
|
|
||||||
watcher, err := NewWatcher(project, options, s.watch, logConsumer)
|
watcher, err := NewWatcher(project, options, s.watch, logConsumer)
|
||||||
if err != nil && options.Start.Watch {
|
if err != nil && options.Start.Watch {
|
||||||
return err
|
return err
|
||||||
@ -105,16 +109,16 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
|||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
first := true
|
first := true
|
||||||
gracefulTeardown := func() {
|
gracefulTeardown := func() {
|
||||||
printer.Cancel()
|
tui.ApplicationTermination()
|
||||||
_, _ = fmt.Fprintln(s.stdinfo(), "Gracefully stopping... (press Ctrl+C again to force)")
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
err := s.Stop(context.WithoutCancel(ctx), project.Name, api.StopOptions{
|
return progress.RunWithLog(context.WithoutCancel(ctx), func(ctx context.Context) error {
|
||||||
Services: options.Create.Services,
|
return s.stop(ctx, project.Name, api.StopOptions{
|
||||||
Project: project,
|
Services: options.Create.Services,
|
||||||
})
|
Project: project,
|
||||||
isTerminated.Store(true)
|
}, printer.HandleEvent)
|
||||||
return err
|
}, s.stdinfo(), logConsumer)
|
||||||
})
|
})
|
||||||
|
isTerminated.Store(true)
|
||||||
first = false
|
first = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,12 +163,15 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
|
|||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
code, err := printer.Run(options.Start.OnExit, options.Start.ExitCodeFrom, func() error {
|
code, err := printer.Run(options.Start.OnExit, options.Start.ExitCodeFrom, func() error {
|
||||||
_, _ = fmt.Fprintln(s.stdinfo(), "Aborting on container exit...")
|
_, _ = fmt.Fprintln(s.stdinfo(), "Aborting on container exit...")
|
||||||
return progress.Run(ctx, func(ctx context.Context) error {
|
eg.Go(func() error {
|
||||||
return s.Stop(ctx, project.Name, api.StopOptions{
|
return progress.RunWithLog(context.WithoutCancel(ctx), func(ctx context.Context) error {
|
||||||
Services: options.Create.Services,
|
return s.stop(ctx, project.Name, api.StopOptions{
|
||||||
Project: project,
|
Services: options.Create.Services,
|
||||||
})
|
Project: project,
|
||||||
}, s.stdinfo())
|
}, printer.HandleEvent)
|
||||||
|
}, s.stdinfo(), logConsumer)
|
||||||
|
})
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
exitCode = code
|
exitCode = code
|
||||||
return err
|
return err
|
||||||
|
9
pkg/e2e/fixtures/stop/compose.yaml
Normal file
9
pkg/e2e/fixtures/stop/compose.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
service1:
|
||||||
|
image: alpine
|
||||||
|
command: /bin/true
|
||||||
|
service2:
|
||||||
|
image: alpine
|
||||||
|
command: ping -c 2 localhost
|
||||||
|
pre_stop:
|
||||||
|
- command: echo "stop hook running..."
|
@ -206,3 +206,20 @@ func TestUpImageID(t *testing.T) {
|
|||||||
c = NewCLI(t, WithEnv(fmt.Sprintf("ID=%s", id)))
|
c = NewCLI(t, WithEnv(fmt.Sprintf("ID=%s", id)))
|
||||||
c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/id.yaml", "--project-name", projectName, "up")
|
c.RunDockerComposeCmd(t, "-f", "./fixtures/simple-composefile/id.yaml", "--project-name", projectName, "up")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpStopWithLogsMixed(t *testing.T) {
|
||||||
|
c := NewCLI(t)
|
||||||
|
const projectName = "compose-e2e-stop-logs"
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
c.RunDockerComposeCmd(t, "--project-name", projectName, "down", "-v")
|
||||||
|
})
|
||||||
|
|
||||||
|
res := c.RunDockerComposeCmd(t, "-f", "./fixtures/stop/compose.yaml", "--project-name", projectName, "up", "--abort-on-container-exit")
|
||||||
|
// assert we still get service2 logs after service 1 Stopped event
|
||||||
|
res.Assert(t, icmd.Expected{
|
||||||
|
Err: "Container compose-e2e-stop-logs-service1-1 Stopped",
|
||||||
|
})
|
||||||
|
// assert we get stop hook logs
|
||||||
|
res.Assert(t, icmd.Expected{Out: "service2-1 -> | stop hook running...\nservice2-1 | 64 bytes"})
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
//
|
//
|
||||||
// mockgen -destination pkg/mocks/mock_docker_api.go -package mocks github.com/docker/docker/client APIClient
|
// mockgen -destination pkg/mocks/mock_docker_api.go -package mocks github.com/docker/docker/client APIClient
|
||||||
//
|
//
|
||||||
|
|
||||||
// Package mocks is a generated GoMock package.
|
// Package mocks is a generated GoMock package.
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ import (
|
|||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
types "github.com/docker/docker/api/types"
|
types "github.com/docker/docker/api/types"
|
||||||
|
build "github.com/docker/docker/api/types/build"
|
||||||
checkpoint "github.com/docker/docker/api/types/checkpoint"
|
checkpoint "github.com/docker/docker/api/types/checkpoint"
|
||||||
common "github.com/docker/docker/api/types/common"
|
common "github.com/docker/docker/api/types/common"
|
||||||
container "github.com/docker/docker/api/types/container"
|
container "github.com/docker/docker/api/types/container"
|
||||||
@ -56,10 +58,10 @@ func (m *MockAPIClient) EXPECT() *MockAPIClientMockRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BuildCachePrune mocks base method.
|
// BuildCachePrune mocks base method.
|
||||||
func (m *MockAPIClient) BuildCachePrune(arg0 context.Context, arg1 types.BuildCachePruneOptions) (*types.BuildCachePruneReport, error) {
|
func (m *MockAPIClient) BuildCachePrune(arg0 context.Context, arg1 build.CachePruneOptions) (*build.CachePruneReport, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "BuildCachePrune", arg0, arg1)
|
ret := m.ctrl.Call(m, "BuildCachePrune", arg0, arg1)
|
||||||
ret0, _ := ret[0].(*types.BuildCachePruneReport)
|
ret0, _ := ret[0].(*build.CachePruneReport)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
@ -156,10 +158,10 @@ func (mr *MockAPIClientMockRecorder) Close() *gomock.Call {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ConfigCreate mocks base method.
|
// ConfigCreate mocks base method.
|
||||||
func (m *MockAPIClient) ConfigCreate(arg0 context.Context, arg1 swarm.ConfigSpec) (types.ConfigCreateResponse, error) {
|
func (m *MockAPIClient) ConfigCreate(arg0 context.Context, arg1 swarm.ConfigSpec) (swarm.ConfigCreateResponse, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "ConfigCreate", arg0, arg1)
|
ret := m.ctrl.Call(m, "ConfigCreate", arg0, arg1)
|
||||||
ret0, _ := ret[0].(types.ConfigCreateResponse)
|
ret0, _ := ret[0].(swarm.ConfigCreateResponse)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
@ -187,7 +189,7 @@ func (mr *MockAPIClientMockRecorder) ConfigInspectWithRaw(arg0, arg1 any) *gomoc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ConfigList mocks base method.
|
// ConfigList mocks base method.
|
||||||
func (m *MockAPIClient) ConfigList(arg0 context.Context, arg1 types.ConfigListOptions) ([]swarm.Config, error) {
|
func (m *MockAPIClient) ConfigList(arg0 context.Context, arg1 swarm.ConfigListOptions) ([]swarm.Config, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "ConfigList", arg0, arg1)
|
ret := m.ctrl.Call(m, "ConfigList", arg0, arg1)
|
||||||
ret0, _ := ret[0].([]swarm.Config)
|
ret0, _ := ret[0].([]swarm.Config)
|
||||||
@ -802,10 +804,10 @@ func (mr *MockAPIClientMockRecorder) HTTPClient() *gomock.Call {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ImageBuild mocks base method.
|
// ImageBuild mocks base method.
|
||||||
func (m *MockAPIClient) ImageBuild(arg0 context.Context, arg1 io.Reader, arg2 types.ImageBuildOptions) (types.ImageBuildResponse, error) {
|
func (m *MockAPIClient) ImageBuild(arg0 context.Context, arg1 io.Reader, arg2 build.ImageBuildOptions) (build.ImageBuildResponse, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "ImageBuild", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "ImageBuild", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].(types.ImageBuildResponse)
|
ret0, _ := ret[0].(build.ImageBuildResponse)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
@ -1220,7 +1222,7 @@ func (mr *MockAPIClientMockRecorder) NodeInspectWithRaw(arg0, arg1 any) *gomock.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NodeList mocks base method.
|
// NodeList mocks base method.
|
||||||
func (m *MockAPIClient) NodeList(arg0 context.Context, arg1 types.NodeListOptions) ([]swarm.Node, error) {
|
func (m *MockAPIClient) NodeList(arg0 context.Context, arg1 swarm.NodeListOptions) ([]swarm.Node, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "NodeList", arg0, arg1)
|
ret := m.ctrl.Call(m, "NodeList", arg0, arg1)
|
||||||
ret0, _ := ret[0].([]swarm.Node)
|
ret0, _ := ret[0].([]swarm.Node)
|
||||||
@ -1235,7 +1237,7 @@ func (mr *MockAPIClientMockRecorder) NodeList(arg0, arg1 any) *gomock.Call {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NodeRemove mocks base method.
|
// NodeRemove mocks base method.
|
||||||
func (m *MockAPIClient) NodeRemove(arg0 context.Context, arg1 string, arg2 types.NodeRemoveOptions) error {
|
func (m *MockAPIClient) NodeRemove(arg0 context.Context, arg1 string, arg2 swarm.NodeRemoveOptions) error {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "NodeRemove", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "NodeRemove", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].(error)
|
ret0, _ := ret[0].(error)
|
||||||
@ -1439,10 +1441,10 @@ func (mr *MockAPIClientMockRecorder) RegistryLogin(arg0, arg1 any) *gomock.Call
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SecretCreate mocks base method.
|
// SecretCreate mocks base method.
|
||||||
func (m *MockAPIClient) SecretCreate(arg0 context.Context, arg1 swarm.SecretSpec) (types.SecretCreateResponse, error) {
|
func (m *MockAPIClient) SecretCreate(arg0 context.Context, arg1 swarm.SecretSpec) (swarm.SecretCreateResponse, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SecretCreate", arg0, arg1)
|
ret := m.ctrl.Call(m, "SecretCreate", arg0, arg1)
|
||||||
ret0, _ := ret[0].(types.SecretCreateResponse)
|
ret0, _ := ret[0].(swarm.SecretCreateResponse)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
@ -1470,7 +1472,7 @@ func (mr *MockAPIClientMockRecorder) SecretInspectWithRaw(arg0, arg1 any) *gomoc
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SecretList mocks base method.
|
// SecretList mocks base method.
|
||||||
func (m *MockAPIClient) SecretList(arg0 context.Context, arg1 types.SecretListOptions) ([]swarm.Secret, error) {
|
func (m *MockAPIClient) SecretList(arg0 context.Context, arg1 swarm.SecretListOptions) ([]swarm.Secret, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SecretList", arg0, arg1)
|
ret := m.ctrl.Call(m, "SecretList", arg0, arg1)
|
||||||
ret0, _ := ret[0].([]swarm.Secret)
|
ret0, _ := ret[0].([]swarm.Secret)
|
||||||
@ -1528,7 +1530,7 @@ func (mr *MockAPIClientMockRecorder) ServerVersion(arg0 any) *gomock.Call {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServiceCreate mocks base method.
|
// ServiceCreate mocks base method.
|
||||||
func (m *MockAPIClient) ServiceCreate(arg0 context.Context, arg1 swarm.ServiceSpec, arg2 types.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) {
|
func (m *MockAPIClient) ServiceCreate(arg0 context.Context, arg1 swarm.ServiceSpec, arg2 swarm.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "ServiceCreate", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "ServiceCreate", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].(swarm.ServiceCreateResponse)
|
ret0, _ := ret[0].(swarm.ServiceCreateResponse)
|
||||||
@ -1543,7 +1545,7 @@ func (mr *MockAPIClientMockRecorder) ServiceCreate(arg0, arg1, arg2 any) *gomock
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServiceInspectWithRaw mocks base method.
|
// ServiceInspectWithRaw mocks base method.
|
||||||
func (m *MockAPIClient) ServiceInspectWithRaw(arg0 context.Context, arg1 string, arg2 types.ServiceInspectOptions) (swarm.Service, []byte, error) {
|
func (m *MockAPIClient) ServiceInspectWithRaw(arg0 context.Context, arg1 string, arg2 swarm.ServiceInspectOptions) (swarm.Service, []byte, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "ServiceInspectWithRaw", arg0, arg1, arg2)
|
ret := m.ctrl.Call(m, "ServiceInspectWithRaw", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].(swarm.Service)
|
ret0, _ := ret[0].(swarm.Service)
|
||||||
@ -1559,7 +1561,7 @@ func (mr *MockAPIClientMockRecorder) ServiceInspectWithRaw(arg0, arg1, arg2 any)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServiceList mocks base method.
|
// ServiceList mocks base method.
|
||||||
func (m *MockAPIClient) ServiceList(arg0 context.Context, arg1 types.ServiceListOptions) ([]swarm.Service, error) {
|
func (m *MockAPIClient) ServiceList(arg0 context.Context, arg1 swarm.ServiceListOptions) ([]swarm.Service, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "ServiceList", arg0, arg1)
|
ret := m.ctrl.Call(m, "ServiceList", arg0, arg1)
|
||||||
ret0, _ := ret[0].([]swarm.Service)
|
ret0, _ := ret[0].([]swarm.Service)
|
||||||
@ -1603,7 +1605,7 @@ func (mr *MockAPIClientMockRecorder) ServiceRemove(arg0, arg1 any) *gomock.Call
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ServiceUpdate mocks base method.
|
// ServiceUpdate mocks base method.
|
||||||
func (m *MockAPIClient) ServiceUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ServiceSpec, arg4 types.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
|
func (m *MockAPIClient) ServiceUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ServiceSpec, arg4 swarm.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "ServiceUpdate", arg0, arg1, arg2, arg3, arg4)
|
ret := m.ctrl.Call(m, "ServiceUpdate", arg0, arg1, arg2, arg3, arg4)
|
||||||
ret0, _ := ret[0].(swarm.ServiceUpdateResponse)
|
ret0, _ := ret[0].(swarm.ServiceUpdateResponse)
|
||||||
@ -1618,10 +1620,10 @@ func (mr *MockAPIClientMockRecorder) ServiceUpdate(arg0, arg1, arg2, arg3, arg4
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SwarmGetUnlockKey mocks base method.
|
// SwarmGetUnlockKey mocks base method.
|
||||||
func (m *MockAPIClient) SwarmGetUnlockKey(arg0 context.Context) (types.SwarmUnlockKeyResponse, error) {
|
func (m *MockAPIClient) SwarmGetUnlockKey(arg0 context.Context) (swarm.UnlockKeyResponse, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "SwarmGetUnlockKey", arg0)
|
ret := m.ctrl.Call(m, "SwarmGetUnlockKey", arg0)
|
||||||
ret0, _ := ret[0].(types.SwarmUnlockKeyResponse)
|
ret0, _ := ret[0].(swarm.UnlockKeyResponse)
|
||||||
ret1, _ := ret[1].(error)
|
ret1, _ := ret[1].(error)
|
||||||
return ret0, ret1
|
return ret0, ret1
|
||||||
}
|
}
|
||||||
@ -1735,7 +1737,7 @@ func (mr *MockAPIClientMockRecorder) TaskInspectWithRaw(arg0, arg1 any) *gomock.
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TaskList mocks base method.
|
// TaskList mocks base method.
|
||||||
func (m *MockAPIClient) TaskList(arg0 context.Context, arg1 types.TaskListOptions) ([]swarm.Task, error) {
|
func (m *MockAPIClient) TaskList(arg0 context.Context, arg1 swarm.TaskListOptions) ([]swarm.Task, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "TaskList", arg0, arg1)
|
ret := m.ctrl.Call(m, "TaskList", arg0, arg1)
|
||||||
ret0, _ := ret[0].([]swarm.Task)
|
ret0, _ := ret[0].([]swarm.Task)
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
//
|
//
|
||||||
// mockgen -destination pkg/mocks/mock_docker_cli.go -package mocks github.com/docker/cli/cli/command Cli
|
// mockgen -destination pkg/mocks/mock_docker_cli.go -package mocks github.com/docker/cli/cli/command Cli
|
||||||
//
|
//
|
||||||
|
|
||||||
// Package mocks is a generated GoMock package.
|
// Package mocks is a generated GoMock package.
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
//
|
//
|
||||||
// mockgen -destination pkg/mocks/mock_docker_compose_api.go -package mocks -source=./pkg/api/api.go Service
|
// mockgen -destination pkg/mocks/mock_docker_compose_api.go -package mocks -source=./pkg/api/api.go Service
|
||||||
//
|
//
|
||||||
|
|
||||||
// Package mocks is a generated GoMock package.
|
// Package mocks is a generated GoMock package.
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ type Event struct {
|
|||||||
Total int64
|
Total int64
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
endTime time.Time
|
endTime time.Time
|
||||||
spinner *spinner
|
spinner *Spinner
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorMessageEvent creates a new Error Event with message
|
// ErrorMessageEvent creates a new Error Event with message
|
||||||
|
76
pkg/progress/mixed.go
Normal file
76
pkg/progress/mixed.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/streams"
|
||||||
|
"github.com/docker/compose/v2/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewMixedWriter creates a Writer which allows to mix output from progress.Writer with a api.LogConsumer
|
||||||
|
func NewMixedWriter(out *streams.Out, consumer api.LogConsumer, dryRun bool) Writer {
|
||||||
|
isTerminal := out.IsTerminal()
|
||||||
|
if Mode != ModeAuto || !isTerminal {
|
||||||
|
return &plainWriter{
|
||||||
|
out: out,
|
||||||
|
done: make(chan bool),
|
||||||
|
dryRun: dryRun,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &mixedWriter{
|
||||||
|
out: consumer,
|
||||||
|
done: make(chan bool),
|
||||||
|
dryRun: dryRun,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mixedWriter struct {
|
||||||
|
done chan bool
|
||||||
|
dryRun bool
|
||||||
|
out api.LogConsumer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *mixedWriter) Start(ctx context.Context) error {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-p.done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *mixedWriter) Event(e Event) {
|
||||||
|
p.out.Status("", fmt.Sprintf("%s %s %s", e.ID, e.Text, SuccessColor(e.StatusText)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *mixedWriter) Events(events []Event) {
|
||||||
|
for _, e := range events {
|
||||||
|
p.Event(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *mixedWriter) TailMsgf(msg string, args ...interface{}) {
|
||||||
|
msg = fmt.Sprintf(msg, args...)
|
||||||
|
p.out.Status("", WarningColor(msg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *mixedWriter) Stop() {
|
||||||
|
p.done <- true
|
||||||
|
}
|
@ -21,7 +21,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type spinner struct {
|
type Spinner struct {
|
||||||
time time.Time
|
time time.Time
|
||||||
index int
|
index int
|
||||||
chars []string
|
chars []string
|
||||||
@ -29,7 +29,7 @@ type spinner struct {
|
|||||||
done string
|
done string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSpinner() *spinner {
|
func NewSpinner() *Spinner {
|
||||||
chars := []string{
|
chars := []string{
|
||||||
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
|
"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏",
|
||||||
}
|
}
|
||||||
@ -40,7 +40,7 @@ func newSpinner() *spinner {
|
|||||||
done = "-"
|
done = "-"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &spinner{
|
return &Spinner{
|
||||||
index: 0,
|
index: 0,
|
||||||
time: time.Now(),
|
time: time.Now(),
|
||||||
chars: chars,
|
chars: chars,
|
||||||
@ -48,7 +48,7 @@ func newSpinner() *spinner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *spinner) String() string {
|
func (s *Spinner) String() string {
|
||||||
if s.stop {
|
if s.stop {
|
||||||
return s.done
|
return s.done
|
||||||
}
|
}
|
||||||
@ -61,10 +61,10 @@ func (s *spinner) String() string {
|
|||||||
return s.chars[s.index]
|
return s.chars[s.index]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *spinner) Stop() {
|
func (s *Spinner) Stop() {
|
||||||
s.stop = true
|
s.stop = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *spinner) Restart() {
|
func (s *Spinner) Restart() {
|
||||||
s.stop = false
|
s.stop = false
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ func (w *ttyWriter) event(e Event) {
|
|||||||
w.events[e.ID] = last
|
w.events[e.ID] = last
|
||||||
} else {
|
} else {
|
||||||
e.startTime = time.Now()
|
e.startTime = time.Now()
|
||||||
e.spinner = newSpinner()
|
e.spinner = NewSpinner()
|
||||||
if e.Status == Done || e.Status == Error {
|
if e.Status == Done || e.Status == Error {
|
||||||
e.stop()
|
e.stop()
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ func TestLineText(t *testing.T) {
|
|||||||
StatusText: "Status",
|
StatusText: "Status",
|
||||||
endTime: now,
|
endTime: now,
|
||||||
startTime: now,
|
startTime: now,
|
||||||
spinner: &spinner{
|
spinner: &Spinner{
|
||||||
chars: []string{"."},
|
chars: []string{"."},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -65,7 +65,7 @@ func TestLineTextSingleEvent(t *testing.T) {
|
|||||||
Status: Done,
|
Status: Done,
|
||||||
StatusText: "Status",
|
StatusText: "Status",
|
||||||
startTime: now,
|
startTime: now,
|
||||||
spinner: &spinner{
|
spinner: &Spinner{
|
||||||
chars: []string{"."},
|
chars: []string{"."},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -87,7 +87,7 @@ func TestErrorEvent(t *testing.T) {
|
|||||||
Status: Working,
|
Status: Working,
|
||||||
StatusText: "Working",
|
StatusText: "Working",
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
spinner: &spinner{
|
spinner: &Spinner{
|
||||||
chars: []string{"."},
|
chars: []string{"."},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -116,7 +116,7 @@ func TestWarningEvent(t *testing.T) {
|
|||||||
Status: Working,
|
Status: Working,
|
||||||
StatusText: "Working",
|
StatusText: "Working",
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
spinner: &spinner{
|
spinner: &Spinner{
|
||||||
chars: []string{"."},
|
chars: []string{"."},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,25 @@ func Run(ctx context.Context, pf progressFunc, out *streams.Out) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RunWithLog(ctx context.Context, pf progressFunc, out *streams.Out, logConsumer api.LogConsumer) error {
|
||||||
|
dryRun, ok := ctx.Value(api.DryRunKey{}).(bool)
|
||||||
|
if !ok {
|
||||||
|
dryRun = false
|
||||||
|
}
|
||||||
|
w := NewMixedWriter(out, logConsumer, dryRun)
|
||||||
|
eg, _ := errgroup.WithContext(ctx)
|
||||||
|
eg.Go(func() error {
|
||||||
|
return w.Start(context.Background())
|
||||||
|
})
|
||||||
|
eg.Go(func() error {
|
||||||
|
defer w.Stop()
|
||||||
|
ctx = WithContextWriter(ctx, w)
|
||||||
|
err := pf(ctx)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return eg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
func RunWithTitle(ctx context.Context, pf progressFunc, out *streams.Out, progressTitle string) error {
|
func RunWithTitle(ctx context.Context, pf progressFunc, out *streams.Out, progressTitle string) error {
|
||||||
_, err := RunWithStatus(ctx, func(ctx context.Context) (string, error) {
|
_, err := RunWithStatus(ctx, func(ctx context.Context) (string, error) {
|
||||||
return "", pf(ctx)
|
return "", pf(ctx)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user