Skip to content

Commit 9b67a48

Browse files
ndeloofglours
authored andcommitted
(refactoting) Move watch logic into a dedicated Watcher type
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
1 parent 0d0e12c commit 9b67a48

File tree

7 files changed

+216
-188
lines changed

7 files changed

+216
-188
lines changed

cmd/compose/watch.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,10 @@ func runWatch(ctx context.Context, dockerCli command.Cli, backend api.Service, w
117117
}
118118

119119
consumer := formatter.NewLogConsumer(ctx, dockerCli.Out(), dockerCli.Err(), false, false, false)
120-
return backend.Watch(ctx, project, services, api.WatchOptions{
121-
Build: &build,
122-
LogTo: consumer,
123-
Prune: watchOpts.prune,
120+
return backend.Watch(ctx, project, api.WatchOptions{
121+
Build: &build,
122+
LogTo: consumer,
123+
Prune: watchOpts.prune,
124+
Services: services,
124125
})
125126
}

cmd/formatter/shortcut.go

Lines changed: 84 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,7 @@ import (
2929
"github.com/compose-spec/compose-go/v2/types"
3030
"github.com/docker/compose/v2/internal/tracing"
3131
"github.com/docker/compose/v2/pkg/api"
32-
"github.com/docker/compose/v2/pkg/watch"
3332
"github.com/eiannone/keyboard"
34-
"github.com/hashicorp/go-multierror"
3533
"github.com/skratchdot/open-golang/open"
3634
)
3735

@@ -71,26 +69,13 @@ func (ke *KeyboardError) error() string {
7169
}
7270

7371
type KeyboardWatch struct {
74-
Watcher watch.Notify
7572
Watching bool
76-
WatchFn func(ctx context.Context, doneCh chan bool, project *types.Project, services []string, options api.WatchOptions) error
77-
Ctx context.Context
78-
Cancel context.CancelFunc
73+
Watcher Toggle
7974
}
8075

81-
func (kw *KeyboardWatch) isWatching() bool {
82-
return kw.Watching
83-
}
84-
85-
func (kw *KeyboardWatch) switchWatching() {
86-
kw.Watching = !kw.Watching
87-
}
88-
89-
func (kw *KeyboardWatch) newContext(ctx context.Context) context.CancelFunc {
90-
ctx, cancel := context.WithCancel(ctx)
91-
kw.Ctx = ctx
92-
kw.Cancel = cancel
93-
return cancel
76+
type Toggle interface {
77+
Start(context.Context) error
78+
Stop() error
9479
}
9580

9681
type KEYBOARD_LOG_LEVEL int
@@ -110,31 +95,21 @@ type LogKeyboard struct {
11095
signalChannel chan<- os.Signal
11196
}
11297

113-
var (
114-
KeyboardManager *LogKeyboard
115-
eg multierror.Group
116-
)
117-
118-
func NewKeyboardManager(ctx context.Context, isDockerDesktopActive, isWatchConfigured bool,
119-
sc chan<- os.Signal,
120-
watchFn func(ctx context.Context,
121-
doneCh chan bool,
122-
project *types.Project,
123-
services []string,
124-
options api.WatchOptions,
125-
) error,
126-
) {
127-
km := LogKeyboard{}
128-
km.IsDockerDesktopActive = isDockerDesktopActive
129-
km.IsWatchConfigured = isWatchConfigured
130-
km.logLevel = INFO
131-
132-
km.Watch.Watching = false
133-
km.Watch.WatchFn = watchFn
134-
135-
km.signalChannel = sc
136-
137-
KeyboardManager = &km
98+
// FIXME(ndeloof) we should avoid use of such a global reference. see use in logConsumer
99+
var KeyboardManager *LogKeyboard
100+
101+
func NewKeyboardManager(isDockerDesktopActive bool, sc chan<- os.Signal, w bool, watcher Toggle) *LogKeyboard {
102+
KeyboardManager = &LogKeyboard{
103+
Watch: KeyboardWatch{
104+
Watching: w,
105+
Watcher: watcher,
106+
},
107+
IsDockerDesktopActive: isDockerDesktopActive,
108+
IsWatchConfigured: true,
109+
logLevel: INFO,
110+
signalChannel: sc,
111+
}
112+
return KeyboardManager
138113
}
139114

140115
func (lk *LogKeyboard) ClearKeyboardInfo() {
@@ -233,48 +208,51 @@ func (lk *LogKeyboard) openDockerDesktop(ctx context.Context, project *types.Pro
233208
if !lk.IsDockerDesktopActive {
234209
return
235210
}
236-
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
237-
func(ctx context.Context) error {
238-
link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
239-
err := open.Run(link)
240-
if err != nil {
241-
err = fmt.Errorf("could not open Docker Desktop")
242-
lk.keyboardError("View", err)
243-
}
244-
return err
245-
}),
246-
)
211+
go func() {
212+
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui", tracing.SpanOptions{},
213+
func(ctx context.Context) error {
214+
link := fmt.Sprintf("docker-desktop://dashboard/apps/%s", project.Name)
215+
err := open.Run(link)
216+
if err != nil {
217+
err = fmt.Errorf("could not open Docker Desktop")
218+
lk.keyboardError("View", err)
219+
}
220+
return err
221+
})()
222+
}()
247223
}
248224

249225
func (lk *LogKeyboard) openDDComposeUI(ctx context.Context, project *types.Project) {
250226
if !lk.IsDockerDesktopActive {
251227
return
252228
}
253-
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
254-
func(ctx context.Context) error {
255-
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
256-
err := open.Run(link)
257-
if err != nil {
258-
err = fmt.Errorf("could not open Docker Desktop Compose UI")
259-
lk.keyboardError("View Config", err)
260-
}
261-
return err
262-
}),
263-
)
229+
go func() {
230+
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/composeview", tracing.SpanOptions{},
231+
func(ctx context.Context) error {
232+
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s", project.Name)
233+
err := open.Run(link)
234+
if err != nil {
235+
err = fmt.Errorf("could not open Docker Desktop Compose UI")
236+
lk.keyboardError("View Config", err)
237+
}
238+
return err
239+
})()
240+
}()
264241
}
265242

266243
func (lk *LogKeyboard) openDDWatchDocs(ctx context.Context, project *types.Project) {
267-
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
268-
func(ctx context.Context) error {
269-
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
270-
err := open.Run(link)
271-
if err != nil {
272-
err = fmt.Errorf("could not open Docker Desktop Compose UI")
273-
lk.keyboardError("Watch Docs", err)
274-
}
275-
return err
276-
}),
277-
)
244+
go func() {
245+
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/gui/watch", tracing.SpanOptions{},
246+
func(ctx context.Context) error {
247+
link := fmt.Sprintf("docker-desktop://dashboard/docker-compose/%s/watch", project.Name)
248+
err := open.Run(link)
249+
if err != nil {
250+
err = fmt.Errorf("could not open Docker Desktop Compose UI")
251+
lk.keyboardError("Watch Docs", err)
252+
}
253+
return err
254+
})()
255+
}()
278256
}
279257

280258
func (lk *LogKeyboard) keyboardError(prefix string, err error) {
@@ -288,39 +266,34 @@ func (lk *LogKeyboard) keyboardError(prefix string, err error) {
288266
}()
289267
}
290268

291-
func (lk *LogKeyboard) StartWatch(ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
269+
func (lk *LogKeyboard) ToggleWatch(ctx context.Context, options api.UpOptions) {
292270
if !lk.IsWatchConfigured {
293271
return
294272
}
295-
lk.Watch.switchWatching()
296-
if !lk.Watch.isWatching() {
297-
lk.Watch.Cancel()
273+
if lk.Watch.Watching {
274+
err := lk.Watch.Watcher.Stop()
275+
if err != nil {
276+
options.Start.Attach.Err(api.WatchLogger, err.Error())
277+
} else {
278+
lk.Watch.Watching = false
279+
}
298280
} else {
299-
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
300-
func(ctx context.Context) error {
301-
if options.Create.Build == nil {
302-
err := fmt.Errorf("cannot run watch mode with flag --no-build")
303-
lk.keyboardError("Watch", err)
281+
go func() {
282+
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
283+
func(ctx context.Context) error {
284+
err := lk.Watch.Watcher.Start(ctx)
285+
if err != nil {
286+
options.Start.Attach.Err(api.WatchLogger, err.Error())
287+
} else {
288+
lk.Watch.Watching = true
289+
}
304290
return err
305-
}
306-
307-
lk.Watch.newContext(ctx)
308-
buildOpts := *options.Create.Build
309-
buildOpts.Quiet = true
310-
err := lk.Watch.WatchFn(lk.Watch.Ctx, doneCh, project, options.Start.Services, api.WatchOptions{
311-
Build: &buildOpts,
312-
LogTo: options.Start.Attach,
313-
})
314-
if err != nil {
315-
lk.Watch.switchWatching()
316-
options.Start.Attach.Err(api.WatchLogger, err.Error())
317-
}
318-
return err
319-
}))
291+
})()
292+
}()
320293
}
321294
}
322295

323-
func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Context, doneCh chan bool, project *types.Project, options api.UpOptions) {
296+
func (lk *LogKeyboard) HandleKeyEvents(ctx context.Context, event keyboard.KeyEvent, project *types.Project, options api.UpOptions) {
324297
switch kRune := event.Rune; kRune {
325298
case 'v':
326299
lk.openDockerDesktop(ctx, project)
@@ -331,15 +304,16 @@ func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Cont
331304
lk.openDDWatchDocs(ctx, project)
332305
}
333306
// either way we mark menu/watch as an error
334-
eg.Go(tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
335-
func(ctx context.Context) error {
336-
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
337-
lk.keyboardError("Watch", err)
338-
return err
339-
}))
340-
return
307+
go func() {
308+
_ = tracing.EventWrapFuncForErrGroup(ctx, "menu/watch", tracing.SpanOptions{},
309+
func(ctx context.Context) error {
310+
err := fmt.Errorf("watch is not yet configured. Learn more: %s", ansiColor(CYAN, "https://docs.docker.com/compose/file-watch/"))
311+
lk.keyboardError("Watch", err)
312+
return err
313+
})()
314+
}()
341315
}
342-
lk.StartWatch(ctx, doneCh, project, options)
316+
lk.ToggleWatch(ctx, options)
343317
case 'o':
344318
lk.openDDComposeUI(ctx, project)
345319
}
@@ -350,10 +324,6 @@ func (lk *LogKeyboard) HandleKeyEvents(event keyboard.KeyEvent, ctx context.Cont
350324
ShowCursor()
351325

352326
lk.logLevel = NONE
353-
if lk.Watch.Watching && lk.Watch.Cancel != nil {
354-
lk.Watch.Cancel()
355-
_ = eg.Wait().ErrorOrNil() // Need to print this ?
356-
}
357327
// will notify main thread to kill and will handle gracefully
358328
lk.signalChannel <- syscall.SIGINT
359329
case keyboard.KeyEnter:

pkg/api/api.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ type Service interface {
8585
// DryRunMode defines if dry run applies to the command
8686
DryRunMode(ctx context.Context, dryRun bool) (context.Context, error)
8787
// Watch services' development context and sync/notify/rebuild/restart on changes
88-
Watch(ctx context.Context, project *types.Project, services []string, options WatchOptions) error
88+
Watch(ctx context.Context, project *types.Project, options WatchOptions) error
8989
// Viz generates a graphviz graph of the project services
9090
Viz(ctx context.Context, project *types.Project, options VizOptions) (string, error)
9191
// Wait blocks until at least one of the services' container exits
@@ -127,9 +127,10 @@ const WatchLogger = "#watch"
127127

128128
// WatchOptions group options of the Watch API
129129
type WatchOptions struct {
130-
Build *BuildOptions
131-
LogTo LogConsumer
132-
Prune bool
130+
Build *BuildOptions
131+
LogTo LogConsumer
132+
Prune bool
133+
Services []string
133134
}
134135

135136
// BuildOptions group options of the Build API

pkg/compose/up.go

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,15 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
7272
var isTerminated atomic.Bool
7373
printer := newLogPrinter(options.Start.Attach)
7474

75+
var watcher *Watcher
76+
if options.Start.Watch {
77+
watcher, err = NewWatcher(project, options, s.watch)
78+
if err != nil {
79+
return err
80+
}
81+
}
82+
83+
var navigationMenu *formatter.LogKeyboard
7584
var kEvents <-chan keyboard.KeyEvent
7685
if options.Start.NavigationMenu {
7786
kEvents, err = keyboard.GetKeys(100)
@@ -80,20 +89,14 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
8089
options.Start.NavigationMenu = false
8190
} else {
8291
defer keyboard.Close() //nolint:errcheck
83-
isWatchConfigured := s.shouldWatch(project)
8492
isDockerDesktopActive := s.isDesktopIntegrationActive()
85-
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, isWatchConfigured)
86-
formatter.NewKeyboardManager(ctx, isDockerDesktopActive, isWatchConfigured, signalChan, s.watch)
93+
tracing.KeyboardMetrics(ctx, options.Start.NavigationMenu, isDockerDesktopActive, watcher != nil)
94+
navigationMenu = formatter.NewKeyboardManager(isDockerDesktopActive, signalChan, options.Start.Watch, watcher)
8795
}
8896
}
8997

9098
doneCh := make(chan bool)
9199
eg.Go(func() error {
92-
if options.Start.NavigationMenu && options.Start.Watch {
93-
// Run watch by navigation menu, so we can interactively enable/disable
94-
formatter.KeyboardManager.StartWatch(ctx, doneCh, project, options)
95-
}
96-
97100
first := true
98101
gracefulTeardown := func() {
99102
printer.Cancel()
@@ -112,13 +115,17 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
112115
for {
113116
select {
114117
case <-doneCh:
118+
if watcher != nil {
119+
return watcher.Stop()
120+
}
115121
return nil
116122
case <-ctx.Done():
117123
if first {
118124
gracefulTeardown()
119125
}
120126
case <-signalChan:
121127
if first {
128+
keyboard.Close() //nolint:errcheck
122129
gracefulTeardown()
123130
break
124131
}
@@ -137,7 +144,7 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
137144
})
138145
return nil
139146
case event := <-kEvents:
140-
formatter.KeyboardManager.HandleKeyEvents(event, ctx, doneCh, project, options)
147+
navigationMenu.HandleKeyEvents(ctx, event, project, options)
141148
}
142149
}
143150
})
@@ -157,15 +164,11 @@ func (s *composeService) Up(ctx context.Context, project *types.Project, options
157164
return err
158165
})
159166

160-
if options.Start.Watch && !options.Start.NavigationMenu {
161-
eg.Go(func() error {
162-
buildOpts := *options.Create.Build
163-
buildOpts.Quiet = true
164-
return s.watch(ctx, doneCh, project, options.Start.Services, api.WatchOptions{
165-
Build: &buildOpts,
166-
LogTo: options.Start.Attach,
167-
})
168-
})
167+
if options.Start.Watch && watcher != nil {
168+
err = watcher.Start(ctx)
169+
if err != nil {
170+
return err
171+
}
169172
}
170173

171174
// We use the parent context without cancellation as we manage sigterm to stop the stack

0 commit comments

Comments
 (0)