Repository: docker/compose
Pull request #11911 (2024-06-17 09:24:44)

stop watch process when associated up process is stopped
**What I did**
Share the `doneCh` channel of the `up` command to `watch` process so it could be stop when the `up` command is stopped by an external `down` command.

I moved the existing content of the `func (s *composeService) Watch` function to an internal `watch` function. This avoids changing the `Watch` API and eliminates the need to add the channel as a new parameter to the API.

**Related issue**
https://docker.atlassian.net/browse/COMP-621

**(not mandatory) A picture of a cute animal, if possible in relation to what you did**
![image](https://github.com/docker/compose/assets/705411/daab6a4a-f997-46ba-bb8d-aeb25e00b223)



Patch:
diff --git a/cmd/formatter/shortcut.go b/cmd/formatter/shortcut.go
index 1f8974b7d..0c78d3e8e 100644
--- a/cmd/formatter/shortcut.go
+++ b/cmd/formatter/shortcut.go
@@ -73,7 +73,7 @@ func (ke *KeyboardError) error() string {
 type KeyboardWatch struct {
 	Watcher  watch.Notify
 	Watching bool
-	WatchFn  func(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error
+	WatchFn  func(ctx context.Context, doneCh chan bool, project *types.Project, services []string, options api.WatchOptions) error
 	Ctx      context.Context
 	Cancel   context.CancelFunc
 }
@@ -117,6 +117,7 @@ var eg multierror.Group
 func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfigured, isDockerDesktopConfigActive bool,
 	sc chan<- os.Signal,
 	watchFn func(ctx context.Context,
+		doneCh chan bool,
 		project *types.Project,
 		services []string,
 		options api.WatchOptions,
@@ -272,7 +273,7 @@ func (lk *LogKeyboard) keyboardError(prefix string, err error) {
 	}()
 }
 
-func (lk *LogKeyboard) StartWatch(ctx context.Context, project *types.Project, options api.UpOptions) {
+func (lk *LogKeyboard) StartWatch(ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
 	if !lk.IsWatchConfigured {
 		eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
 			func(ctx context.Context) error {
@@ -297,7 +298,7 @@ func (lk *LogKeyboard) StartWatch(ctx context.Context, project *types.Project, o
 				lk.Watch.newContext(ctx)
 				buildOpts := *options.Create.Build
 				buildOpts.Quiet = true
-				return lk.Watch.WatchFn(lk.Watch.Ctx, project, options.Start.Services, api.WatchOptions{
+				return lk.Watch.WatchFn(lk.Watch.Ctx, doneCh, project, options.Start.Services, api.WatchOptions{
 					Build: &buildOpts,
 					LogTo: options.Start.Attach,
 				})
@@ -305,12 +306,12 @@ func (lk *LogKeyboard) StartWatch(ctx context.Context, project *types.Project, o
 	}
 }
 
-func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Context, project *types.Project, options api.UpOptions) {
+func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
 	switch kRune := event.Rune; kRune {
 	case 'v':
 		lk.openDockerDesktop(ctx, project)
 	case 'w':
-		lk.StartWatch(ctx, project, options)
+		lk.StartWatch(ctx, doneCh, project, options)
 	case 'o':
 		lk.openDDComposeUI(ctx, project)
 	}
diff --git a/pkg/compose/up.go b/pkg/compose/up.go
index 494b86154..1f7827a1e 100644
--- a/pkg/compose/up.go
+++ b/pkg/compose/up.go
@@ -101,9 +101,9 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
 				isDockerDesktopComposeUI := s.isDesktopUIEnabled()
 				tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, isWatchConfigured, isDockerDesktopComposeUI)
 
-				formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, isDockerDesktopComposeUI, signalChan, s.Watch)
+				formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, isDockerDesktopComposeUI, signalChan, s.watch)
 				if options.Start.Watch {
-					formatter.KeyboardManager.StartWatch(ctx, project, options)
+					formatter.KeyboardManager.StartWatch(ctx, doneCh, project, options)
 				}
 			}
 		}
@@ -136,7 +136,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
 				})
 				return nil
 			case event := <-kEvents:
-				formatter.KeyboardManager.HandleKeyEvents(event, ctx, project, options)
+				formatter.KeyboardManager.HandleKeyEvents(event, ctx, doneCh, project, options)
 			}
 		}
 	})
@@ -160,7 +160,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
 		eg.Go(func() error {
 			buildOpts := *options.Create.Build
 			buildOpts.Quiet = true
-			return s.Watch(ctx, project, options.Start.Services, api.WatchOptions{
+			return s.watch(ctx, doneCh, project, options.Start.Services, api.WatchOptions{
 				Build: &buildOpts,
 				LogTo: options.Start.Attach,
 			})
diff --git a/pkg/compose/watch.go b/pkg/compose/watch.go
index 3654e5e7c..26ba98faf 100644
--- a/pkg/compose/watch.go
+++ b/pkg/compose/watch.go
@@ -77,7 +77,10 @@ func (s *composeService) shouldWatch(project *types.Project) bool {
 	return shouldWatch
 }
 
-func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
+func (s *composeService) Watch(ctx context.Context, project *types.Project, services []string, options api.WatchOptions) error {
+	return s.watch(ctx, nil, project, services, options)
+}
+func (s *composeService) watch(ctx context.Context, syncChannel chan bool, project *types.Project, services []string, options api.WatchOptions) error { //nolint: gocyclo
 	var err error
 	if project, err = project.WithSelectedServices(services); err != nil {
 		return err
@@ -172,7 +175,7 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
 		watching = true
 		eg.Go(func() error {
 			defer watcher.Close() //nolint:errcheck
-			return s.watch(ctx, project, service.Name, options, watcher, syncer, config.Watch)
+			return s.watchEvents(ctx, project, service.Name, options, watcher, syncer, config.Watch)
 		})
 	}
 	if !watching {
@@ -180,10 +183,20 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
 	}
 	options.LogTo.Log(api.WatchLogger, "Watch enabled")
 
-	return eg.Wait()
+	err = eg.Wait()
+	for {
+		select {
+		case <-ctx.Done():
+			return err
+		case <-syncChannel:
+			options.LogTo.Log(api.WatchLogger, "Watch disabled")
+			ctx.Done()
+			return err
+		}
+	}
 }
 
-func (s *composeService) watch(ctx context.Context, project *types.Project, name string, options api.WatchOptions, watcher watch.Notify, syncer sync.Syncer, triggers []types.Trigger) error {
+func (s *composeService) watchEvents(ctx context.Context, project *types.Project, name string, options api.WatchOptions, watcher watch.Notify, syncer sync.Syncer, triggers []types.Trigger) error {
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 


Test patch:
diff --git a/pkg/compose/watch_test.go b/pkg/compose/watch_test.go
index 39fbf2bcb..28d5c85ff 100644
--- a/pkg/compose/watch_test.go
+++ b/pkg/compose/watch_test.go
@@ -144,7 +144,7 @@ func TestWatch_Sync(t *testing.T) {
 			dockerCli: cli,
 			clock:     clock,
 		}
-		err := service.watch(ctx, &proj, "test", api.WatchOptions{
+		err := service.watchEvents(ctx, &proj, "test", api.WatchOptions{
 			Build: &api.BuildOptions{},
 			LogTo: stdLogger{},
 		}, watcher, syncer, []types.Trigger{