Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

### Bug Fixes:
- fix(docs): corrected stale and missing API reference links in usage.json metadata([#1803](https://github.com/fastly/cli/pull/1803))
- fix(compute): `serve --watch` no longer rebuilds on attribute-only (Chmod) filesystem events, preventing an endless rebuild loop when another process changes a watched file's metadata such as its access time ([#1808](https://github.com/fastly/cli/pull/1808))
- fix(docs): expand and correct API reference links for `fastly service` subcommands in usage.json metadata ([#1810](https://github.com/fastly/cli/pull/1810))

### Enhancements:
Expand Down
13 changes: 13 additions & 0 deletions pkg/commands/compute/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,10 @@ func watchFiles(root string, gi *ignore.GitIgnore, verbose bool, s *fstexec.Stre
if !ok {
return
}
// Ignore attribute-only (Chmod) events; on macOS an atime bump alone would loop.
if !isContentChange(event.Op) {
continue
}
debounced(func() {
eventHandler(event.Name, event.Op)
})
Expand Down Expand Up @@ -1393,6 +1397,15 @@ func readIgnoreFile(path string) (lines []string) {
return strings.Split(string(bs), "\n")
}

// isContentChange reports whether op is a content or existence change
// (Create/Write/Remove/Rename) and not an attribute-only Chmod. fsnotify
// recommends ignoring Chmod; see its "Why do I get many Chmod events?" FAQ:
// https://github.com/fsnotify/fsnotify#why-do-i-get-many-chmod-events
func isContentChange(op fsnotify.Op) bool {
Comment thread
jedisct1 marked this conversation as resolved.
return op.Has(fsnotify.Create) || op.Has(fsnotify.Write) ||
op.Has(fsnotify.Remove) || op.Has(fsnotify.Rename)
}

func watchFile(path string, watcher *fsnotify.Watcher, verbose bool, out io.Writer) {
absolute, err := filepath.Abs(path)
if err != nil && verbose {
Expand Down
32 changes: 32 additions & 0 deletions pkg/commands/compute/serve_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package compute

import (
"testing"

"github.com/fsnotify/fsnotify"
)

// TestIsContentChange checks that content/existence events trigger a rebuild and
// attribute-only (Chmod) events do not.
func TestIsContentChange(t *testing.T) {
scenarios := []struct {
name string
op fsnotify.Op
want bool
}{
{name: "create", op: fsnotify.Create, want: true},
{name: "write", op: fsnotify.Write, want: true},
{name: "remove", op: fsnotify.Remove, want: true},
{name: "rename", op: fsnotify.Rename, want: true},
{name: "chmod only", op: fsnotify.Chmod, want: false},
{name: "write combined with chmod", op: fsnotify.Write | fsnotify.Chmod, want: true},
{name: "no op", op: 0, want: false},
}
for _, testcase := range scenarios {
t.Run(testcase.name, func(t *testing.T) {
if got := isContentChange(testcase.op); got != testcase.want {
t.Errorf("isContentChange(%s) = %v, want %v", testcase.op, got, testcase.want)
}
})
}
}