diff --git a/cmd/display/tty.go b/cmd/display/tty.go index 447ecd61e7..96f6c65451 100644 --- a/cmd/display/tty.go +++ b/cmd/display/tty.go @@ -172,7 +172,8 @@ func (w *ttyWriter) Start(ctx context.Context, operation string) { func (w *ttyWriter) Done(operation string, success bool) { w.print() - w.done <- true + // close never blocks if the render goroutine already exited via ctx.Done() + close(w.done) w.mtx.Lock() defer w.mtx.Unlock() if w.ticker != nil { diff --git a/cmd/display/tty_test.go b/cmd/display/tty_test.go index c6d6165c31..8b71a59212 100644 --- a/cmd/display/tty_test.go +++ b/cmd/display/tty_test.go @@ -569,6 +569,26 @@ func TestDoneDeadlockFix(t *testing.T) { } } +func TestDoneAfterContextCancelDoesNotHang(t *testing.T) { + w, _ := newTestWriter() + ctx, cancel := context.WithCancel(context.Background()) + w.Start(ctx, "down") + cancel() + // Let the render goroutine observe the cancellation and exit. + time.Sleep(100 * time.Millisecond) + + finished := make(chan struct{}) + go func() { + w.Done("down", false) + close(finished) + }() + select { + case <-finished: + case <-time.After(2 * time.Second): + t.Fatal("ttyWriter.Done blocked forever after context cancellation") + } +} + // TestAdjustLineWidth_WideProgressForcesSizeInfoDrop is the unit-level // regression test for docker/compose#13595. When progress contains the // " X.XMB / Y.YMB" size suffix and the bar makes beforeStatus large enough