@@ -17,6 +17,9 @@ package compose
17
17
import (
18
18
"context"
19
19
"fmt"
20
+ "io/fs"
21
+ "os"
22
+ "path"
20
23
"path/filepath"
21
24
"strings"
22
25
"time"
@@ -50,9 +53,30 @@ type Trigger struct {
50
53
51
54
const quietPeriod = 2 * time .Second
52
55
53
- func (s * composeService ) Watch (ctx context.Context , project * types.Project , services []string , options api.WatchOptions ) error { //nolint:gocyclo
54
- needRebuild := make (chan string )
55
- needSync := make (chan api.CopyOptions , 5 )
56
+ // fileMapping contains the Compose service and modified host system path.
57
+ //
58
+ // For file sync, the container path is also included.
59
+ // For rebuild, there is no container path, so it is always empty.
60
+ type fileMapping struct {
61
+ // service that the file event is for.
62
+ service string
63
+ // hostPath that was created/modified/deleted outside the container.
64
+ //
65
+ // This is the path as seen from the user's perspective, e.g.
66
+ // - C:\Users\moby\Documents\hello-world\main.go
67
+ // - /Users/moby/Documents/hello-world/main.go
68
+ hostPath string
69
+ // containerPath for the target file inside the container (only populated
70
+ // for sync events, not rebuild).
71
+ //
72
+ // This is the path as used in Docker CLI commands, e.g.
73
+ // - /workdir/main.go
74
+ containerPath string
75
+ }
76
+
77
+ func (s * composeService ) Watch (ctx context.Context , project * types.Project , services []string , _ api.WatchOptions ) error { //nolint:gocyclo
78
+ needRebuild := make (chan fileMapping )
79
+ needSync := make (chan fileMapping )
56
80
57
81
eg , ctx := errgroup .WithContext (ctx )
58
82
eg .Go (func () error {
@@ -120,38 +144,37 @@ func (s *composeService) Watch(ctx context.Context, project *types.Project, serv
120
144
case <- ctx .Done ():
121
145
return nil
122
146
case event := <- watcher .Events ():
123
- path := event .Path ()
147
+ hostPath := event .Path ()
124
148
125
149
for _ , trigger := range config .Watch {
126
- logrus .Debugf ("change detected on %s - comparing with %s" , path , trigger .Path )
127
- if watch .IsChild (trigger .Path , path ) {
128
- fmt .Fprintf (s .stderr (), "change detected on %s\n " , path )
150
+ logrus .Debugf ("change detected on %s - comparing with %s" , hostPath , trigger .Path )
151
+ if watch .IsChild (trigger .Path , hostPath ) {
152
+ fmt .Fprintf (s .stderr (), "change detected on %s\n " , hostPath )
153
+
154
+ f := fileMapping {
155
+ hostPath : hostPath ,
156
+ service : name ,
157
+ }
129
158
130
159
switch trigger .Action {
131
160
case WatchActionSync :
132
- logrus .Debugf ("modified file %s triggered sync" , path )
133
- rel , err := filepath .Rel (trigger .Path , path )
161
+ logrus .Debugf ("modified file %s triggered sync" , hostPath )
162
+ rel , err := filepath .Rel (trigger .Path , hostPath )
134
163
if err != nil {
135
164
return err
136
165
}
137
- dest := filepath .Join (trigger .Target , rel )
138
- needSync <- api.CopyOptions {
139
- Source : path ,
140
- Destination : fmt .Sprintf ("%s:%s" , name , dest ),
141
- }
166
+ // always use Unix-style paths for inside the container
167
+ f .containerPath = path .Join (trigger .Target , rel )
168
+ needSync <- f
142
169
case WatchActionRebuild :
143
- logrus .Debugf ("modified file %s requires image to be rebuilt" , path )
144
- needRebuild <- name
170
+ logrus .Debugf ("modified file %s requires image to be rebuilt" , hostPath )
171
+ needRebuild <- f
145
172
default :
146
173
return fmt .Errorf ("watch action %q is not supported" , trigger )
147
174
}
148
175
continue WATCH
149
176
}
150
177
}
151
-
152
- // default
153
- needRebuild <- name
154
-
155
178
case err := <- watcher .Errors ():
156
179
return err
157
180
}
@@ -183,11 +206,25 @@ func loadDevelopmentConfig(service types.ServiceConfig, project *types.Project)
183
206
return config , nil
184
207
}
185
208
186
- func (s * composeService ) makeRebuildFn (ctx context.Context , project * types.Project ) func (services []string ) {
187
- return func (services []string ) {
188
- fmt .Fprintf (s .stderr (), "Updating %s after changes were detected\n " , strings .Join (services , ", " ))
209
+ func (s * composeService ) makeRebuildFn (ctx context.Context , project * types.Project ) func (services rebuildServices ) {
210
+ return func (services rebuildServices ) {
211
+ serviceNames := make ([]string , 0 , len (services ))
212
+ allPaths := make (utils.Set [string ])
213
+ for serviceName , paths := range services {
214
+ serviceNames = append (serviceNames , serviceName )
215
+ for p := range paths {
216
+ allPaths .Add (p )
217
+ }
218
+ }
219
+
220
+ fmt .Fprintf (
221
+ s .stderr (),
222
+ "Rebuilding %s after changes were detected:%s\n " ,
223
+ strings .Join (serviceNames , ", " ),
224
+ strings .Join (append ([]string {"" }, allPaths .Elements ()... ), "\n - " ),
225
+ )
189
226
imageIds , err := s .build (ctx , project , api.BuildOptions {
190
- Services : services ,
227
+ Services : serviceNames ,
191
228
})
192
229
if err != nil {
193
230
fmt .Fprintf (s .stderr (), "Build failed\n " )
@@ -201,11 +238,11 @@ func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Proje
201
238
202
239
err = s .Up (ctx , project , api.UpOptions {
203
240
Create : api.CreateOptions {
204
- Services : services ,
241
+ Services : serviceNames ,
205
242
Inherit : true ,
206
243
},
207
244
Start : api.StartOptions {
208
- Services : services ,
245
+ Services : serviceNames ,
209
246
Project : project ,
210
247
},
211
248
})
@@ -215,39 +252,61 @@ func (s *composeService) makeRebuildFn(ctx context.Context, project *types.Proje
215
252
}
216
253
}
217
254
218
- func (s * composeService ) makeSyncFn (ctx context.Context , project * types.Project , needSync chan api. CopyOptions ) func () error {
255
+ func (s * composeService ) makeSyncFn (ctx context.Context , project * types.Project , needSync <- chan fileMapping ) func () error {
219
256
return func () error {
220
257
for {
221
258
select {
222
259
case <- ctx .Done ():
223
260
return nil
224
261
case opt := <- needSync :
225
- err := s .Copy (ctx , project .Name , opt )
226
- if err != nil {
227
- return err
262
+ if fi , statErr := os .Stat (opt .hostPath ); statErr == nil && ! fi .IsDir () {
263
+ err := s .Copy (ctx , project .Name , api.CopyOptions {
264
+ Source : opt .hostPath ,
265
+ Destination : fmt .Sprintf ("%s:%s" , opt .service , opt .containerPath ),
266
+ })
267
+ if err != nil {
268
+ return err
269
+ }
270
+ fmt .Fprintf (s .stderr (), "%s updated\n " , opt .containerPath )
271
+ } else if errors .Is (statErr , fs .ErrNotExist ) {
272
+ _ , err := s .Exec (ctx , project .Name , api.RunOptions {
273
+ Service : opt .service ,
274
+ Command : []string {"rm" , "-rf" , opt .containerPath },
275
+ Index : 1 ,
276
+ })
277
+ if err != nil {
278
+ logrus .Warnf ("failed to delete %q from %s: %v" , opt .containerPath , opt .service , err )
279
+ }
280
+ fmt .Fprintf (s .stderr (), "%s deleted from container\n " , opt .containerPath )
228
281
}
229
- fmt .Fprintf (s .stderr (), "%s updated\n " , opt .Destination )
230
282
}
231
283
}
232
284
}
233
285
}
234
286
235
- func debounce (ctx context.Context , clock clockwork.Clock , delay time.Duration , input chan string , fn func (services []string )) {
236
- services := utils.Set [string ]{}
287
+ type rebuildServices map [string ]utils.Set [string ]
288
+
289
+ func debounce (ctx context.Context , clock clockwork.Clock , delay time.Duration , input <- chan fileMapping , fn func (services rebuildServices )) {
290
+ services := make (rebuildServices )
237
291
t := clock .AfterFunc (delay , func () {
238
292
if len (services ) > 0 {
239
- refresh := services . Elements ( )
240
- services . Clear ()
241
- fn ( refresh )
293
+ fn ( services )
294
+ // TODO(milas): this is a data race!
295
+ services = make ( rebuildServices )
242
296
}
243
297
})
244
298
for {
245
299
select {
246
300
case <- ctx .Done ():
247
301
return
248
- case service := <- input :
302
+ case e := <- input :
249
303
t .Reset (delay )
250
- services .Add (service )
304
+ svc , ok := services [e .service ]
305
+ if ! ok {
306
+ svc = make (utils.Set [string ])
307
+ services [e .service ] = svc
308
+ }
309
+ svc .Add (e .hostPath )
251
310
}
252
311
}
253
312
}
0 commit comments