Skip to content

Commit fd3e0bb

Browse files
nicksndeloof
authored andcommitted
watch: use the recursive watcher on windows (docker#3306)
1 parent d2d4d05 commit fd3e0bb

File tree

6 files changed

+224
-66
lines changed

6 files changed

+224
-66
lines changed

pkg/watch/notify_test.go

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
// +build !windows
2-
31
package watch
42

53
import (
@@ -14,9 +12,8 @@ import (
1412
"testing"
1513
"time"
1614

17-
"github.com/stretchr/testify/require"
18-
1915
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
2017

2118
"github.com/windmilleng/tilt/internal/dockerignore"
2219
"github.com/windmilleng/tilt/internal/testutils/tempdir"
@@ -41,6 +38,11 @@ func TestNoWatches(t *testing.T) {
4138
}
4239

4340
func TestEventOrdering(t *testing.T) {
41+
if runtime.GOOS == "windows" {
42+
// https://qualapps.blogspot.com/2010/05/understanding-readdirectorychangesw_19.html
43+
t.Skip("Windows doesn't make great guarantees about duplicate/out-of-order events")
44+
return
45+
}
4446
f := newNotifyFixture(t)
4547
defer f.tearDown()
4648

@@ -143,10 +145,7 @@ func TestWatchesAreRecursive(t *testing.T) {
143145
f.events = nil
144146
// change sub directory
145147
changeFilePath := filepath.Join(subPath, "change")
146-
_, err := os.OpenFile(changeFilePath, os.O_RDONLY|os.O_CREATE, 0666)
147-
if err != nil {
148-
t.Fatal(err)
149-
}
148+
f.WriteFile(changeFilePath, "change")
150149

151150
f.assertEvents(changeFilePath)
152151
}
@@ -278,6 +277,9 @@ func TestSingleFile(t *testing.T) {
278277
}
279278

280279
func TestWriteBrokenLink(t *testing.T) {
280+
if runtime.GOOS == "windows" {
281+
t.Skip("no user-space symlinks on windows")
282+
}
281283
f := newNotifyFixture(t)
282284
defer f.tearDown()
283285

@@ -292,6 +294,9 @@ func TestWriteBrokenLink(t *testing.T) {
292294
}
293295

294296
func TestWriteGoodLink(t *testing.T) {
297+
if runtime.GOOS == "windows" {
298+
t.Skip("no user-space symlinks on windows")
299+
}
295300
f := newNotifyFixture(t)
296301
defer f.tearDown()
297302

@@ -311,6 +316,9 @@ func TestWriteGoodLink(t *testing.T) {
311316
}
312317

313318
func TestWatchBrokenLink(t *testing.T) {
319+
if runtime.GOOS == "windows" {
320+
t.Skip("no user-space symlinks on windows")
321+
}
314322
f := newNotifyFixture(t)
315323
defer f.tearDown()
316324

@@ -399,6 +407,9 @@ func TestWatchNonexistentDirectory(t *testing.T) {
399407
f := newNotifyFixture(t)
400408
defer f.tearDown()
401409

410+
ignore, _ := dockerignore.NewDockerPatternMatcher(f.paths[0], []string{"./"})
411+
f.setIgnore(ignore)
412+
402413
root := f.JoinPath("root")
403414
err := os.Mkdir(root, 0777)
404415
if err != nil {
@@ -422,20 +433,19 @@ func TestWatchNonexistentDirectory(t *testing.T) {
422433
} else {
423434
f.assertEvents(parent)
424435
}
436+
f.events = nil
425437
f.WriteFile(file, "hello")
426438

427-
if runtime.GOOS == "darwin" {
428-
// mac doesn't return the dir change as part of file creation
429-
f.assertEvents(file)
430-
} else {
431-
f.assertEvents(parent, file)
432-
}
439+
f.assertEvents(file)
433440
}
434441

435442
func TestWatchNonexistentFileInNonexistentDirectory(t *testing.T) {
436443
f := newNotifyFixture(t)
437444
defer f.tearDown()
438445

446+
ignore, _ := dockerignore.NewDockerPatternMatcher(f.paths[0], []string{"./"})
447+
f.setIgnore(ignore)
448+
439449
root := f.JoinPath("root")
440450
err := os.Mkdir(root, 0777)
441451
if err != nil {
@@ -469,7 +479,7 @@ func TestWatchCountInnerFile(t *testing.T) {
469479
f.assertEvents(a, b, file)
470480

471481
expectedWatches := 3
472-
if runtime.GOOS == "darwin" {
482+
if isRecursiveWatcher() {
473483
expectedWatches = 1
474484
}
475485
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
@@ -493,7 +503,7 @@ func TestWatchCountInnerFileWithIgnore(t *testing.T) {
493503
f.assertEvents(b, file)
494504

495505
expectedWatches := 3
496-
if runtime.GOOS == "darwin" {
506+
if isRecursiveWatcher() {
497507
expectedWatches = 1
498508
}
499509
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
@@ -514,7 +524,7 @@ func TestIgnoreCreatedDir(t *testing.T) {
514524
f.assertEvents(a)
515525

516526
expectedWatches := 2
517-
if runtime.GOOS == "darwin" {
527+
if isRecursiveWatcher() {
518528
expectedWatches = 1
519529
}
520530
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
@@ -540,7 +550,7 @@ func TestIgnoreCreatedDirWithExclusions(t *testing.T) {
540550
f.assertEvents(a)
541551

542552
expectedWatches := 2
543-
if runtime.GOOS == "darwin" {
553+
if isRecursiveWatcher() {
544554
expectedWatches = 1
545555
}
546556
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
@@ -563,14 +573,20 @@ func TestIgnoreInitialDir(t *testing.T) {
563573
f.assertEvents()
564574

565575
expectedWatches := 3
566-
if runtime.GOOS == "darwin" {
576+
if isRecursiveWatcher() {
567577
expectedWatches = 2
568578
}
569579
assert.Equal(t, expectedWatches, int(numberOfWatches.Value()))
570580
}
571581

582+
func isRecursiveWatcher() bool {
583+
return runtime.GOOS == "darwin" || runtime.GOOS == "windows"
584+
}
585+
572586
type notifyFixture struct {
573-
out *bytes.Buffer
587+
ctx context.Context
588+
cancel func()
589+
out *bytes.Buffer
574590
*tempdir.TempDirFixture
575591
notify Notify
576592
ignore PathMatcher
@@ -580,7 +596,10 @@ type notifyFixture struct {
580596

581597
func newNotifyFixture(t *testing.T) *notifyFixture {
582598
out := bytes.NewBuffer(nil)
599+
ctx, cancel := context.WithCancel(context.Background())
583600
nf := &notifyFixture{
601+
ctx: ctx,
602+
cancel: cancel,
584603
TempDirFixture: tempdir.NewTempDirFixture(t),
585604
paths: []string{},
586605
ignore: EmptyMatcher{},
@@ -621,6 +640,11 @@ func (f *notifyFixture) rebuildWatcher() {
621640

622641
func (f *notifyFixture) assertEvents(expected ...string) {
623642
f.fsync()
643+
if runtime.GOOS == "windows" {
644+
// NOTE(nick): It's unclear to me why an extra fsync() helps
645+
// here, but it makes the I/O way more predictable.
646+
f.fsync()
647+
}
624648

625649
if len(f.events) != len(expected) {
626650
f.T().Fatalf("Got %d events (expected %d): %v %v", len(f.events), len(expected), f.events, expected)
@@ -639,6 +663,9 @@ func (f *notifyFixture) consumeEventsInBackground(ctx context.Context) chan erro
639663
go func() {
640664
for {
641665
select {
666+
case <-f.ctx.Done():
667+
close(done)
668+
return
642669
case <-ctx.Done():
643670
close(done)
644671
return
@@ -672,6 +699,8 @@ func (f *notifyFixture) fsyncWithRetryCount(retryCount int) {
672699
F:
673700
for {
674701
select {
702+
case <-f.ctx.Done():
703+
return
675704
case err := <-f.notify.Errors():
676705
f.T().Fatal(err)
677706

@@ -714,13 +743,15 @@ func (f *notifyFixture) closeWatcher() {
714743
for range notify.Events() {
715744
}
716745
}()
746+
717747
go func() {
718748
for range notify.Errors() {
719749
}
720750
}()
721751
}
722752

723753
func (f *notifyFixture) tearDown() {
754+
f.cancel()
724755
f.closeWatcher()
725756
f.TempDirFixture.TearDown()
726757
numberOfWatches.Set(0)

pkg/watch/paths.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package watch
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/pkg/errors"
9+
10+
"github.com/windmilleng/tilt/internal/ospath"
11+
)
12+
13+
func greatestExistingAncestors(paths []string) ([]string, error) {
14+
result := []string{}
15+
for _, p := range paths {
16+
newP, err := greatestExistingAncestor(p)
17+
if err != nil {
18+
return nil, fmt.Errorf("Finding ancestor of %s: %v", p, err)
19+
}
20+
result = append(result, newP)
21+
}
22+
return result, nil
23+
}
24+
func greatestExistingAncestor(path string) (string, error) {
25+
if path == string(filepath.Separator) ||
26+
path == fmt.Sprintf("%s%s", filepath.VolumeName(path), string(filepath.Separator)) {
27+
return "", fmt.Errorf("cannot watch root directory")
28+
}
29+
30+
_, err := os.Stat(path)
31+
if err != nil && !os.IsNotExist(err) {
32+
return "", errors.Wrapf(err, "os.Stat(%q)", path)
33+
}
34+
35+
if os.IsNotExist(err) {
36+
return greatestExistingAncestor(filepath.Dir(path))
37+
}
38+
39+
return path, nil
40+
}
41+
42+
// If we're recursively watching a path, it doesn't
43+
// make sense to watch any of its descendants.
44+
func dedupePathsForRecursiveWatcher(paths []string) []string {
45+
result := []string{}
46+
for _, current := range paths {
47+
isCovered := false
48+
hasRemovals := false
49+
50+
for i, existing := range result {
51+
if ospath.IsChild(existing, current) {
52+
// The path is already covered, so there's no need to include it
53+
isCovered = true
54+
break
55+
}
56+
57+
if ospath.IsChild(current, existing) {
58+
// Mark the element empty fo removal.
59+
result[i] = ""
60+
hasRemovals = true
61+
}
62+
}
63+
64+
if !isCovered {
65+
result = append(result, current)
66+
}
67+
68+
if hasRemovals {
69+
// Remove all the empties
70+
newResult := []string{}
71+
for _, r := range result {
72+
if r != "" {
73+
newResult = append(newResult, r)
74+
}
75+
}
76+
result = newResult
77+
}
78+
}
79+
return result
80+
}

pkg/watch/paths_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package watch
2+
3+
import (
4+
"runtime"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/windmilleng/tilt/internal/testutils/tempdir"
10+
)
11+
12+
func TestGreatestExistingAncestor(t *testing.T) {
13+
f := tempdir.NewTempDirFixture(t)
14+
defer f.TearDown()
15+
16+
p, err := greatestExistingAncestor(f.Path())
17+
assert.NoError(t, err)
18+
assert.Equal(t, f.Path(), p)
19+
20+
p, err = greatestExistingAncestor(f.JoinPath("missing"))
21+
assert.NoError(t, err)
22+
assert.Equal(t, f.Path(), p)
23+
24+
missingTopLevel := "/missingDir/a/b/c"
25+
if runtime.GOOS == "windows" {
26+
missingTopLevel = "C:\\missingDir\\a\\b\\c"
27+
}
28+
_, err = greatestExistingAncestor(missingTopLevel)
29+
assert.Contains(t, err.Error(), "cannot watch root directory")
30+
}

pkg/watch/watcher_darwin.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
"github.com/pkg/errors"
88

9-
"github.com/windmilleng/tilt/internal/ospath"
109
"github.com/windmilleng/tilt/pkg/logger"
1110

1211
"github.com/windmilleng/fsevents"
@@ -72,14 +71,6 @@ func (d *darwinNotify) loop() {
7271

7372
// Add a path to be watched. Should only be called during initialization.
7473
func (d *darwinNotify) initAdd(name string) {
75-
// Check if this is a subdirectory of any of the paths
76-
// we're already watching.
77-
for _, parent := range d.stream.Paths {
78-
if ospath.IsChild(parent, name) {
79-
return
80-
}
81-
}
82-
8374
d.stream.Paths = append(d.stream.Paths, name)
8475

8576
if d.pathsWereWatching == nil {
@@ -136,6 +127,7 @@ func newWatcher(paths []string, ignore PathMatcher, l logger.Logger) (*darwinNot
136127
stop: make(chan struct{}),
137128
}
138129

130+
paths = dedupePathsForRecursiveWatcher(paths)
139131
for _, path := range paths {
140132
path, err := filepath.Abs(path)
141133
if err != nil {

0 commit comments

Comments
 (0)