diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 386b2f5f..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,84 +0,0 @@ -version: 2.1 -workflows: - main: - jobs: ['linux-arm64', 'ios'] - -jobs: - # linux/arm64 - linux-arm64: - machine: - image: ubuntu-2204:2024.05.1 - resource_class: arm.medium - working_directory: ~/repo - steps: - - checkout - - - run: - name: install-go - command: | - sudo apt -y install golang - - - run: - name: test - command: | - uname -a - go version - FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./... - go test -parallel 1 -race ./... - FSNOTIFY_DEBUG=1 go test -parallel 1 -race -v ./... - - # iOS - ios: - macos: - xcode: 13.4.1 - working_directory: ~/repo - steps: - - checkout - - - run: - name: install-go - command: | - export HOMEBREW_NO_AUTO_UPDATE=1 - brew install go - - - run: - name: test - environment: - SCAN_DEVICE: iPhone 6 - SCAN_SCHEME: WebTests - command: | - export PATH=$PATH:/usr/local/Cellar/go/*/bin - uname -a - go version - FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./... - go test -parallel 1 -race ./... - - # This is just Linux x86_64; also need to get a Go with GOOS=android, but - # there aren't any pre-built versions of that on the Go site. Idk, disable for - # now; number of people using Go on Android is probably very tiny, and the - # number of people using Go with this lib smaller still. - # android: - # machine: - # image: android:2022.01.1 - # working_directory: ~/repo - # steps: - # - checkout - - # - run: - # name: install-go - # command: | - # v=1.19.2 - # curl --silent --show-error --location --fail --retry 3 --output /tmp/go${v}.tgz \ - # "https://go.dev/dl/go$v.linux-arm64.tar.gz" - # sudo tar -C /usr/local -xzf /tmp/go${v}.tgz - # rm /tmp/go${v}.tgz - - # - run: - # name: test - # command: | - # uname -a - # export PATH=/usr/local/go/bin:$PATH - # go version - # FSNOTIFY_BUFFER=4096 go test -parallel 1 -race ./... - # go test -parallel 1 -race ./... - # diff --git a/.cirrus.yml b/.cirrus.yml index f4e7dbf3..7f257e99 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,7 +1,7 @@ freebsd_task: name: 'FreeBSD' freebsd_instance: - image_family: freebsd-14-1 + image_family: freebsd-14-2 install_script: - pkg update -f - pkg install -y go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 8edbd25f..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: 'build' -on: - pull_request: - paths: ['**.go', 'go.mod', '.github/workflows/*'] - push: - branches: ['main', 'aix'] - -jobs: - cross-compile: - strategy: - fail-fast: false - matrix: - go: ['1.17', '1.23'] - runs-on: 'ubuntu-latest' - steps: - - uses: 'actions/checkout@v4' - - - uses: 'actions/setup-go@v5' - with: - go-version: ${{ matrix.go }} - - - name: build - run: | - for a in $(go tool dist list); do - export GOOS=${a%%/*} - export GOARCH=${a#*/} - - case "$GOOS" in - (android|ios) exit 0 ;; # Requires cgo to link. - (js) exit 0 ;; # No build tags in internal/ TODO: should maybe fix? - (openbsd) - case "$GOARCH" in - # Fails with: - # golang.org/x/sys/unix.syscall_syscall9: relocation target syscall.syscall10 not defined - # golang.org/x/sys/unix.kevent: relocation target syscall.syscall6 not defined - # golang.org/x/sys/unix.pipe2: relocation target syscall.rawSyscall not defined - # ... - # Works for me locally though, hmm... - (386) exit 0 ;; - esac - esac - - go test -c - go build ./cmd/fsnotify - done diff --git a/.github/workflows/staticcheck.yml b/.github/workflows/staticcheck.yml index 351e244b..55126cd6 100644 --- a/.github/workflows/staticcheck.yml +++ b/.github/workflows/staticcheck.yml @@ -3,17 +3,28 @@ on: pull_request: paths: ['**.go', 'go.mod', '.github/workflows/*'] push: - branches: ['main', 'aix'] + branches: ['main'] jobs: staticcheck: name: 'staticcheck' runs-on: 'ubuntu-latest' + env: {cache: 'staticcheck-${{ github.ref }}'} steps: - - uses: 'actions/setup-go@v5' + # Setup + - uses: 'actions/checkout@v4' + - id: 'cache-restore' + uses: 'actions/cache/restore@v4' with: - go-version: '1.23' - + key: '${{ env.cache }}' + path: | + ${{ runner.temp }}/staticcheck + /home/runner/.cache/go-build + restore-keys: | + staticcheck-${{ github.ref }} + staticcheck-refs/heads/main + - uses: 'actions/setup-go@v5' + with: {go-version: '1.24'} - uses: 'actions/cache@v4' with: key: '${{ runner.os }}-staticcheck' @@ -21,16 +32,39 @@ jobs: ${{ runner.temp }}/staticcheck ${{ steps.install_go.outputs.GOCACHE || '' }} + # Run - run: | export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck" go install honnef.co/go/tools/cmd/staticcheck@latest - $(go env GOPATH)/bin/staticcheck -matrix < 0 { - /// Point "bytes" at the first byte of the filename - bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] - /// The filename is padded with NULL bytes. TrimRight() gets rid of those. - name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") + if !w.sendEvent(ev) { + return } - if debug { - internal.Debug(name, raw.Mask, raw.Cookie) - } + // Move to the next event in the buffer + offset += unix.SizeofInotifyEvent + inEvent.Len + } + } +} - if mask&unix.IN_IGNORED != 0 { //&& event.Op != 0 - next() - continue - } +func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offset uint32) (Event, bool) { + w.mu.Lock() + defer w.mu.Unlock() - // inotify will automatically remove the watch on deletes; just need - // to clean our state here. - if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { - w.watches.remove(watch.wd) - } + /// If the event happened to the watched directory or the watched file, the + /// kernel doesn't append the filename to the event, but we would like to + /// always fill the the "Name" field with a valid filename. We retrieve the + /// path of the watch from the "paths" map. + /// + /// Can be nil if Remove() was called in another goroutine for this path + /// inbetween reading the events from the kernel and reading the internal + /// state. Not much we can do about it, so just skip. See #616. + watch := w.watches.byWd(uint32(inEvent.Wd)) + if watch == nil { + return Event{}, true + } - // We can't really update the state when a watched path is moved; - // only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove - // the watch. - if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { - if watch.recurse { - next() // Do nothing - continue - } + var ( + name = watch.path + nameLen = uint32(inEvent.Len) + ) + if nameLen > 0 { + /// Point "bytes" at the first byte of the filename + bb := *buf + bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&bb[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] + /// The filename is padded with NULL bytes. TrimRight() gets rid of those. + name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\x00") + } - err := w.remove(watch.path) - if err != nil && !errors.Is(err, ErrNonExistentWatch) { - if !w.sendError(err) { - return - } - } + if debug { + internal.Debug(name, inEvent.Mask, inEvent.Cookie) + } + + if inEvent.Mask&unix.IN_IGNORED != 0 || inEvent.Mask&unix.IN_UNMOUNT != 0 { + w.watches.remove(watch) + return Event{}, true + } + + // inotify will automatically remove the watch on deletes; just need + // to clean our state here. + if inEvent.Mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF { + w.watches.remove(watch) + } + + // We can't really update the state when a watched path is moved; only + // IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove the watch. + if inEvent.Mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF { + // Watch is set up as part of recurse: do nothing as the move gets + // registered from the parent directory. + if watch.recurse() && !watch.byUser() { + return Event{}, true + } + + err := w.remove(watch.path) + if err != nil && !errors.Is(err, ErrNonExistentWatch) { + if !w.sendError(err) { + return Event{}, false } + } - /// Skip if we're watching both this path and the parent; the parent - /// will already send a delete so no need to do it twice. - if mask&unix.IN_DELETE_SELF != 0 { - if _, ok := w.watches.path[filepath.Dir(watch.path)]; ok { - next() - continue - } + if watch.recurse() { + return Event{Name: watch.path, Op: Rename}, true + } + } + + /// Skip if we're watching both this path and the parent; the parent will + /// already send a delete so no need to do it twice. + if inEvent.Mask&unix.IN_DELETE_SELF != 0 { + _, ok := w.watches.path[filepath.Dir(watch.path)] + if ok { + return Event{}, true + } + } + + ev := w.newEvent(name, inEvent.Mask, inEvent.Cookie) + // Need to update watch path for recurse. + if watch.recurse() { + isDir := inEvent.Mask&unix.IN_ISDIR == unix.IN_ISDIR + /// New directory created: set up watch on it. + if isDir && ev.Has(Create) { + err := w.register(ev.Name, watch.flags, flagRecurse) + if !w.sendError(err) { + return Event{}, false } - ev := w.newEvent(name, mask, raw.Cookie) - // Need to update watch path for recurse. - if watch.recurse { - isDir := mask&unix.IN_ISDIR == unix.IN_ISDIR - /// New directory created: set up watch on it. - if isDir && ev.Has(Create) { - err := w.register(ev.Name, watch.flags, true) - if !w.sendError(err) { - return + // This was a directory rename, so we need to update all the + // children. + // + // TODO: this is of course pretty slow; we should use a better data + // structure for storing all of this, e.g. store children in the + // watch. I have some code for this in my kqueue refactor we can use + // in the future. For now I'm okay with this as it's not publicly + // available. Correctness first, performance second. + if ev.renamedFrom != "" { + for k, ww := range w.watches.wd { + if k == watch.wd || ww.path == ev.Name { + continue } - - // This was a directory rename, so we need to update all - // the children. - // - // TODO: this is of course pretty slow; we should use a - // better data structure for storing all of this, e.g. store - // children in the watch. I have some code for this in my - // kqueue refactor we can use in the future. For now I'm - // okay with this as it's not publicly available. - // Correctness first, performance second. - if ev.renamedFrom != "" { - w.watches.mu.Lock() - for k, ww := range w.watches.wd { - if k == watch.wd || ww.path == ev.Name { - continue - } - if strings.HasPrefix(ww.path, ev.renamedFrom) { - ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1) - w.watches.wd[k] = ww - } - } - w.watches.mu.Unlock() + if strings.HasPrefix(ww.path, ev.renamedFrom) { + ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1) + w.watches.wd[k] = ww } } } - - /// Send the events that are not ignored on the events channel - if !w.sendEvent(ev) { - return - } - next() } } -} -func (w *inotify) isRecursive(path string) bool { - ww := w.watches.byPath(path) - if ww == nil { // path could be a file, so also check the Dir. - ww = w.watches.byPath(filepath.Dir(path)) - } - return ww != nil && ww.recurse + return ev, true } func (w *inotify) newEvent(name string, mask, cookie uint32) Event { @@ -650,9 +577,9 @@ func (w *inotify) xSupports(op Op) bool { } func (w *inotify) state() { - w.watches.mu.Lock() - defer w.watches.mu.Unlock() + w.mu.Lock() + defer w.mu.Unlock() for wd, ww := range w.watches.wd { - fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path) + fmt.Fprintf(os.Stderr, "%4d: %q watchFlags=0x%x\n", wd, ww.path, ww.watchFlags) } } diff --git a/backend_kqueue.go b/backend_kqueue.go index d8de5ab7..4fc6dc5b 100644 --- a/backend_kqueue.go +++ b/backend_kqueue.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "runtime" + "sort" "sync" "time" @@ -16,14 +17,13 @@ import ( ) type kqueue struct { + *shared Events chan Event Errors chan error kq int // File descriptor (as returned by the kqueue() syscall). closepipe [2]int // Pipe used for closing kq. watches *watches - done chan struct{} - doneMu sync.Mutex } type ( @@ -132,14 +132,18 @@ func (w *watches) byPath(path string) (watch, bool) { return info, ok } -func (w *watches) updateDirFlags(path string, flags uint32) { +func (w *watches) updateDirFlags(path string, flags uint32) bool { w.mu.Lock() defer w.mu.Unlock() - fd := w.path[path] + fd, ok := w.path[path] + if !ok { // Already deleted: don't re-set it here. + return false + } info := w.wd[fd] info.dirFlags = flags w.wd[fd] = info + return true } func (w *watches) remove(fd int, path string) bool { @@ -179,22 +183,20 @@ func (w *watches) seenBefore(path string) bool { return ok } -func newBackend(ev chan Event, errs chan error) (backend, error) { - return newBufferedBackend(0, ev, errs) -} +var defaultBufferSize = 0 -func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) { +func newBackend(ev chan Event, errs chan error) (backend, error) { kq, closepipe, err := newKqueue() if err != nil { return nil, err } w := &kqueue{ + shared: newShared(ev, errs), Events: ev, Errors: errs, kq: kq, closepipe: closepipe, - done: make(chan struct{}), watches: newWatches(), } @@ -210,7 +212,7 @@ func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error // all. func newKqueue() (kq int, closepipe [2]int, err error) { kq, err = unix.Kqueue() - if kq == -1 { + if err != nil { return kq, closepipe, err } @@ -239,54 +241,17 @@ func newKqueue() (kq int, closepipe [2]int, err error) { return kq, closepipe, nil } -// Returns true if the event was sent, or false if watcher is closed. -func (w *kqueue) sendEvent(e Event) bool { - select { - case <-w.done: - return false - case w.Events <- e: - return true - } -} - -// Returns true if the error was sent, or false if watcher is closed. -func (w *kqueue) sendError(err error) bool { - if err == nil { - return true - } - select { - case <-w.done: - return false - case w.Errors <- err: - return true - } -} - -func (w *kqueue) isClosed() bool { - select { - case <-w.done: - return true - default: - return false - } -} - func (w *kqueue) Close() error { - w.doneMu.Lock() - if w.isClosed() { - w.doneMu.Unlock() + if w.shared.close() { return nil } - close(w.done) - w.doneMu.Unlock() pathsToRemove := w.watches.listPaths(false) for _, name := range pathsToRemove { w.Remove(name) } - // Send "quit" message to the reader goroutine. - unix.Close(w.closepipe[1]) + unix.Close(w.closepipe[1]) // Send "quit" message to readEvents return nil } @@ -303,7 +268,7 @@ func (w *kqueue) AddWith(name string, opts ...addOpt) error { return fmt.Errorf("%w: %s", xErrUnsupported, with.op) } - _, err := w.addWatch(name, noteAllEvents) + _, err := w.addWatch(name, noteAllEvents, false) if err != nil { return err } @@ -366,7 +331,7 @@ const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | un // described in kevent(2). // // Returns the real path to the file which was added, with symlinks resolved. -func (w *kqueue) addWatch(name string, flags uint32) (string, error) { +func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, error) { if w.isClosed() { return "", ErrClosed } @@ -385,15 +350,15 @@ func (w *kqueue) addWatch(name string, flags uint32) (string, error) { return "", nil } - // Follow symlinks. - if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + // Follow symlinks, but only for paths added with Add(), and not paths + // we're adding from internalWatch from a listdir. + if !listDir && fi.Mode()&os.ModeSymlink == os.ModeSymlink { link, err := os.Readlink(name) if err != nil { - // Return nil because Linux can add unresolvable symlinks to the - // watch list without problems, so maintain consistency with - // that. There will be no file events for broken symlinks. - // TODO: more specific check; returns os.PathError; ENOENT? - return "", nil + return "", err + } + if !filepath.IsAbs(link) { + link = filepath.Join(filepath.Dir(name), link) } _, alreadyWatching = w.watches.byPath(link) @@ -408,24 +373,16 @@ func (w *kqueue) addWatch(name string, flags uint32) (string, error) { name = link fi, err = os.Lstat(name) if err != nil { - return "", nil + return "", err } } - // Retry on EINTR; open() can return EINTR in practice on macOS. - // See #354, and Go issues 11180 and 39237. - for { - info.wd, err = unix.Open(name, openMode, 0) - if err == nil { - break - } - if errors.Is(err, unix.EINTR) { - continue - } - + info.wd, err = internal.IgnoringEINTR(func() (int, error) { + return unix.Open(name, openMode, 0) + }) + if err != nil { return "", err } - info.isDir = fi.IsDir() } @@ -444,10 +401,16 @@ func (w *kqueue) addWatch(name string, flags uint32) (string, error) { if info.isDir { watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && (!alreadyWatching || (info.dirFlags&unix.NOTE_WRITE) != unix.NOTE_WRITE) - w.watches.updateDirFlags(name, flags) + if !w.watches.updateDirFlags(name, flags) { + return "", nil + } if watchDir { - if err := w.watchDirectoryFiles(name); err != nil { + d := name + if info.linkName != "" { + d = info.linkName + } + if err := w.watchDirectoryFiles(d); err != nil { return "", err } } @@ -467,9 +430,10 @@ func (w *kqueue) readEvents() { eventBuffer := make([]unix.Kevent_t, 10) for { - kevents, err := w.read(eventBuffer) - // EINTR is okay, the syscall was interrupted before timeout expired. - if err != nil && err != unix.EINTR { + kevents, err := internal.IgnoringEINTR(func() ([]unix.Kevent_t, error) { + return w.read(eventBuffer) + }) + if err != nil { if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) { return } @@ -644,19 +608,22 @@ func (w *kqueue) dirChange(dir string) error { if errors.Is(err, os.ErrNotExist) { return nil } - return fmt.Errorf("fsnotify.dirChange: %w", err) + return fmt.Errorf("fsnotify.dirChange %q: %w", dir, err) } for _, f := range files { fi, err := f.Info() if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil + } return fmt.Errorf("fsnotify.dirChange: %w", err) } err = w.sendCreateIfNew(filepath.Join(dir, fi.Name()), fi) if err != nil { // Don't need to send an error if this file isn't readable. - if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) { + if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) || errors.Is(err, os.ErrNotExist) { return nil } return fmt.Errorf("fsnotify.dirChange: %w", err) @@ -688,11 +655,11 @@ func (w *kqueue) internalWatch(name string, fi os.FileInfo) (string, error) { // mimic Linux providing delete events for subdirectories, but preserve // the flags used if currently watching subdirectory info, _ := w.watches.byPath(name) - return w.addWatch(name, info.dirFlags|unix.NOTE_DELETE|unix.NOTE_RENAME) + return w.addWatch(name, info.dirFlags|unix.NOTE_DELETE|unix.NOTE_RENAME, true) } - // watch file to mimic Linux inotify - return w.addWatch(name, noteAllEvents) + // Watch file to mimic Linux inotify. + return w.addWatch(name, noteAllEvents, true) } // Register events with the queue. @@ -722,12 +689,28 @@ func (w *kqueue) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) { } func (w *kqueue) xSupports(op Op) bool { - if runtime.GOOS == "freebsd" { - //return true // Supports everything. - } + //if runtime.GOOS == "freebsd" { + // return true // Supports everything. + //} if op.Has(xUnportableOpen) || op.Has(xUnportableRead) || op.Has(xUnportableCloseWrite) || op.Has(xUnportableCloseRead) { return false } return true } + +func (w *kqueue) state() { + w.watches.mu.Lock() + defer w.watches.mu.Unlock() + + all := make([]int, 0, len(w.watches.wd)) + for wd := range w.watches.wd { + all = append(all, wd) + } + sort.Ints(all) + + for _, wd := range all { + ww := w.watches.wd[wd] + fmt.Fprintf(os.Stderr, "%4d %q linkname=%q\n", wd, ww.name, ww.linkName) + } +} diff --git a/backend_other.go b/backend_other.go index 5eb5dbc6..b8c0ad72 100644 --- a/backend_other.go +++ b/backend_other.go @@ -9,12 +9,11 @@ type other struct { Errors chan error } +var defaultBufferSize = 0 + func newBackend(ev chan Event, errs chan error) (backend, error) { return nil, errors.New("fsnotify not supported on the current platform") } -func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) { - return newBackend(ev, errs) -} func (w *other) Close() error { return nil } func (w *other) WatchList() []string { return nil } func (w *other) Add(name string) error { return nil } diff --git a/backend_windows.go b/backend_windows.go index c54a6308..3433642d 100644 --- a/backend_windows.go +++ b/backend_windows.go @@ -28,18 +28,16 @@ type readDirChangesW struct { port windows.Handle // Handle to completion port input chan *input // Inputs to the reader are sent on this channel - quit chan chan<- error + done chan chan<- error mu sync.Mutex // Protects access to watches, closed watches watchMap // Map of watches (key: i-number) closed bool // Set to true when Close() is first called } -func newBackend(ev chan Event, errs chan error) (backend, error) { - return newBufferedBackend(50, ev, errs) -} +var defaultBufferSize = 50 -func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error) { +func newBackend(ev chan Event, errs chan error) (backend, error) { port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) if err != nil { return nil, os.NewSyscallError("CreateIoCompletionPort", err) @@ -50,7 +48,7 @@ func newBufferedBackend(sz uint, ev chan Event, errs chan error) (backend, error port: port, watches: make(watchMap), input: make(chan *input, 1), - quit: make(chan chan<- error, 1), + done: make(chan chan<- error, 1), } go w.readEvents() return w, nil @@ -70,8 +68,8 @@ func (w *readDirChangesW) sendEvent(name, renamedFrom string, mask uint64) bool event := w.newEvent(name, uint32(mask)) event.renamedFrom = renamedFrom select { - case ch := <-w.quit: - w.quit <- ch + case ch := <-w.done: + w.done <- ch case w.Events <- event: } return true @@ -83,10 +81,10 @@ func (w *readDirChangesW) sendError(err error) bool { return true } select { + case <-w.done: + return false case w.Errors <- err: return true - case <-w.quit: - return false } } @@ -99,9 +97,9 @@ func (w *readDirChangesW) Close() error { w.closed = true w.mu.Unlock() - // Send "quit" message to the reader goroutine + // Send "done" message to the reader goroutine ch := make(chan error) - w.quit <- ch + w.done <- ch if err := w.wakeupReader(); err != nil { return err } @@ -495,7 +493,7 @@ func (w *readDirChangesW) readEvents() { watch := (*watch)(unsafe.Pointer(ov)) if watch == nil { select { - case ch := <-w.quit: + case ch := <-w.done: w.mu.Lock() var indexes []indexMap for _, index := range w.watches { diff --git a/backend_windows_test.go b/backend_windows_test.go index 850f40c0..6364f43a 100644 --- a/backend_windows_test.go +++ b/backend_windows_test.go @@ -10,7 +10,7 @@ import ( func TestRemoveState(t *testing.T) { // TODO: the Windows backend is too confusing; needs some serious attention. - return + t.Skip("broken test") var ( tmp = t.TempDir() diff --git a/cmd/fsnotify/main.go b/cmd/fsnotify/main.go index cdd9de72..5dba7b6e 100644 --- a/cmd/fsnotify/main.go +++ b/cmd/fsnotify/main.go @@ -21,7 +21,7 @@ Commands: dedup [paths] Watch the paths for changes, suppressing duplicate events. `[1:] -func exit(format string, a ...interface{}) { +func exit(format string, a ...any) { fmt.Fprintf(os.Stderr, filepath.Base(os.Args[0])+": "+format+"\n", a...) fmt.Print("\n" + usage) os.Exit(1) @@ -35,7 +35,7 @@ func help() { // Print line prefixed with the time (a bit shorter than log.Print; we don't // really need the date and ms is useful here). -func printTime(s string, args ...interface{}) { +func printTime(s string, args ...any) { fmt.Printf(time.Now().Format("15:04:05.0000")+" "+s+"\n", args...) } diff --git a/fsnotify.go b/fsnotify.go index 0760efe9..c962a0fa 100644 --- a/fsnotify.go +++ b/fsnotify.go @@ -220,7 +220,7 @@ const ( // File opened for reading was closed. // - // Only works on Linux and FreeBSD. + // Only works on Linux. xUnportableCloseRead ) @@ -244,12 +244,13 @@ var ( // ErrUnsupported is returned by AddWith() when WithOps() specified an // Unportable event that's not supported on this platform. + //lint:ignore ST1012 not relevant xErrUnsupported = errors.New("fsnotify: not supported with this backend") ) // NewWatcher creates a new Watcher. func NewWatcher() (*Watcher, error) { - ev, errs := make(chan Event), make(chan error) + ev, errs := make(chan Event, defaultBufferSize), make(chan error) b, err := newBackend(ev, errs) if err != nil { return nil, err @@ -266,8 +267,8 @@ func NewWatcher() (*Watcher, error) { // cases, and whenever possible you will be better off increasing the kernel // buffers instead of adding a large userspace buffer. func NewBufferedWatcher(sz uint) (*Watcher, error) { - ev, errs := make(chan Event), make(chan error) - b, err := newBufferedBackend(sz, ev, errs) + ev, errs := make(chan Event, sz), make(chan error) + b, err := newBackend(ev, errs) if err != nil { return nil, err } @@ -337,7 +338,8 @@ func (w *Watcher) Close() error { return w.b.Close() } // WatchList returns all paths explicitly added with [Watcher.Add] (and are not // yet removed). // -// Returns nil if [Watcher.Close] was called. +// The order is undefined, and may differ per call. Returns nil if +// [Watcher.Close] was called. func (w *Watcher) WatchList() []string { return w.b.WatchList() } // Supports reports if all the listed operations are supported by this platform. @@ -408,7 +410,6 @@ type ( withOpts struct { bufsize int op Op - noFollow bool sendCreate bool } ) @@ -467,12 +468,6 @@ func withOps(op Op) addOpt { return func(opt *withOpts) { opt.op = op } } -// WithNoFollow disables following symlinks, so the symlinks themselves are -// watched. -func withNoFollow() addOpt { - return func(opt *withOpts) { opt.noFollow = true } -} - // "Internal" option for recursive watches on inotify. func withCreate() addOpt { return func(opt *withOpts) { opt.sendCreate = true } @@ -492,3 +487,13 @@ func recursivePath(path string) (string, bool) { } return path, false } + +type watchFlag uint8 + +const ( + // Added by user with Add(), rather than an internal watch. + flagByUser = watchFlag(0x01) + // Part of recursive watch; as the top-level path added by the user or an + // "internal" watch. + flagRecurse = watchFlag(0x02) +) diff --git a/fsnotify_test.go b/fsnotify_test.go index 29d603c7..11d88b46 100644 --- a/fsnotify_test.go +++ b/fsnotify_test.go @@ -19,13 +19,7 @@ import ( "github.com/fsnotify/fsnotify/internal" ) -// Set soft open file limit to the maximum; on e.g. OpenBSD it's 512/1024. -// -// Go 1.19 will always do this when the os package is imported. -// -// https://go-review.googlesource.com/c/go/+/393354/ func init() { - internal.SetRlimit() enableRecurse = true } @@ -422,12 +416,11 @@ func TestAdd(t *testing.T) { }) t.Run("permission denied", func(t *testing.T) { + t.Parallel() if runtime.GOOS == "windows" { t.Skip("chmod doesn't work on Windows") // TODO: see if we can make a file unreadable } - t.Parallel() - tmp := t.TempDir() dir := join(tmp, "dir-unreadable") mkdir(t, dir) @@ -443,15 +436,16 @@ func TestAdd(t *testing.T) { if err == nil { t.Fatal("error is nil") } - if !errors.Is(err, internal.UnixEACCES) { + if !errors.Is(err, internal.ErrUnixEACCES) { t.Errorf("not unix.EACCESS: %T %#[1]v", err) } - if !errors.Is(err, internal.SyscallEACCES) { + if !errors.Is(err, internal.ErrSyscallEACCES) { t.Errorf("not syscall.EACCESS: %T %#[1]v", err) } }) - t.Run("add same path twice", func(t *testing.T) { + // The second Add() should be a no-op + t.Run("add same dir twice", func(t *testing.T) { tmp := t.TempDir() w := newCollector(t) if err := w.w.Add(tmp); err != nil { @@ -470,6 +464,129 @@ func TestAdd(t *testing.T) { remove /file `)) }) + t.Run("add same dir twice through symlink", func(t *testing.T) { + t.Parallel() + if isSolaris() { + t.Skip("broken: links are resolved and added twice") // TODO: should fix + } + if !internal.HasPrivilegesForSymlink() { + t.Skip("admin permissions required on Windows") + } + + tmp := t.TempDir() + mkdir(t, tmp, "dir") + symlink(t, join(tmp, "dir"), tmp, "link") + w := newCollector(t) + if err := w.w.Add(join(tmp, "dir")); err != nil { + t.Fatal(err) + } + if err := w.w.Add(join(tmp, "link")); err != nil { + t.Fatal(err) + } + + w.collect(t) + touch(t, tmp, "dir/file") + rm(t, tmp, "dir/file") + + cmpEvents(t, tmp, w.events(t), newEvents(t, ` + create /dir/file + remove /dir/file + `)) + }) + + t.Run("add same file twice", func(t *testing.T) { + t.Parallel() + tmp := t.TempDir() + touch(t, tmp, "file") + + w := newCollector(t) + if err := w.w.Add(join(tmp, "file")); err != nil { + t.Fatal(err) + } + if err := w.w.Add(join(tmp, "file")); err != nil { + t.Fatal(err) + } + + w.collect(t) + echoAppend(t, "aaa", tmp, "file") + rm(t, tmp, "file") + + cmpEvents(t, tmp, w.events(t), newEvents(t, ` + write /file + remove /file + + linux: + write /file + chmod /file + remove /file + `)) + }) + t.Run("add same file twice through symlink", func(t *testing.T) { + t.Parallel() + if isSolaris() { + t.Skip("broken: links are resolved and added twice") // TODO: should fix + } + if !internal.HasPrivilegesForSymlink() { + t.Skip("admin permissions required on Windows") + } + + tmp := t.TempDir() + touch(t, tmp, "file") + symlink(t, join(tmp, "file"), tmp, "link") + + w := newCollector(t) + if err := w.w.Add(join(tmp, "file")); err != nil { + t.Fatal(err) + } + if err := w.w.Add(join(tmp, "link")); err != nil { + t.Fatal(err) + } + + w.collect(t) + echoAppend(t, "aaa", tmp, "file") + rm(t, tmp, "file") + + cmpEvents(t, tmp, w.events(t), newEvents(t, ` + write /file + remove /file + + linux: + write /file + chmod /file + remove /file + `)) + }) + + t.Run("not reading events", func(t *testing.T) { + t.Parallel() + + w := newWatcher(t) + defer w.Close() + + tmp := t.TempDir() + mkdir(t, tmp, "/dir1") + mkdir(t, tmp, "/dir2") + addWatch(t, w, tmp, "/dir1") + addWatch(t, w, tmp, "/dir2") + + { + have, want := w.WatchList(), []string{join(tmp, "/dir1"), join(tmp, "/dir2")} + sort.Strings(have) + if !reflect.DeepEqual(have, want) { + t.Errorf("\nhave: %s\nwant: %s", have, want) + } + } + if err := w.Remove(join(tmp, "/dir1")); err != nil { + t.Fatal(err) + } + { + have, want := w.WatchList(), []string{join(tmp, "/dir2")} + sort.Strings(have) + if !reflect.DeepEqual(have, want) { + t.Errorf("\nhave: %s\nwant: %s", have, want) + } + } + }) } func TestRemove(t *testing.T) { @@ -520,6 +637,97 @@ func TestRemove(t *testing.T) { } }) + t.Run("remove same dir twice through symlink", func(t *testing.T) { + t.Parallel() + if isSolaris() { + t.Skip("broken: links are resolved and added twice") // TODO: should fix + } + if !internal.HasPrivilegesForSymlink() { + t.Skip("admin permissions required on Windows") + } + + tmp := t.TempDir() + mkdir(t, tmp, "dir") + symlink(t, join(tmp, "dir"), tmp, "link") + + w := newWatcher(t) + defer w.Close() + + addWatch(t, w, tmp, "dir") + addWatch(t, w, tmp, "link") + + if err := w.Remove(join(tmp, "dir")); err != nil { + t.Fatal(err) + } + err := w.Remove(join(tmp, "link")) + if err == nil { + t.Fatal("no error") + } + if !errors.Is(err, ErrNonExistentWatch) { + t.Fatalf("wrong error: %T", err) + } + }) + + t.Run("remove same file twice", func(t *testing.T) { + t.Parallel() + + tmp := t.TempDir() + touch(t, tmp, "file") + + w := newWatcher(t) + defer w.Close() + + addWatch(t, w, tmp, "file") + + if err := w.Remove(join(tmp, "file")); err != nil { + t.Fatal(err) + } + err := w.Remove(join(tmp, "file")) + if err == nil { + t.Fatal("no error") + } + if !errors.Is(err, ErrNonExistentWatch) { + t.Fatalf("wrong error: %T", err) + } + }) + + t.Run("remove same file twice through symlink", func(t *testing.T) { + t.Parallel() + if isSolaris() { + t.Skip("broken: links are resolved and added twice") // TODO: should fix + } + if !internal.HasPrivilegesForSymlink() { + t.Skip("admin permissions required on Windows") + } + if runtime.GOOS == "windows" { + // TODO: I'm not sure if this is due to a problem in our code, or + // just a limitation of Windows, but very few people are using links + // in Windows and even fewer links to files, so whatever. + t.Skip("fails on Windows") + } + + tmp := t.TempDir() + touch(t, tmp, "file") + symlink(t, join(tmp, "file"), tmp, "link") + + w := newWatcher(t) + defer w.Close() + + addWatch(t, w, tmp, "file") + addWatch(t, w, tmp, "link") + + if err := w.Remove(join(tmp, "file")); err != nil { + t.Fatal(err) + } + err := w.Remove(join(tmp, "link")) + if err == nil { + t.Fatal("no error") + } + if !errors.Is(err, ErrNonExistentWatch) { + t.Fatalf("wrong error: %T", err) + } + }) + // Make sure that concurrent calls to Remove() don't race. t.Run("no race", func(t *testing.T) { t.Parallel() @@ -550,7 +758,7 @@ func TestRemove(t *testing.T) { // Make sure file handles are correctly released. // // regression test for #42 see https://gist.github.com/timshannon/603f92824c5294269797 - t.Run("", func(t *testing.T) { + t.Run("release file handles", func(t *testing.T) { w := newWatcher(t) defer w.Close() @@ -822,21 +1030,130 @@ func BenchmarkAddRemove(b *testing.B) { }) } -// Would panic on inotify: https://github.com/fsnotify/fsnotify/issues/616 -func TestRemoveRace(t *testing.T) { - t.Parallel() +func TestRace(t *testing.T) { + // Would panic on inotify: https://github.com/fsnotify/fsnotify/issues/616 + t.Run("add and remove watches", func(t *testing.T) { + t.Parallel() - tmp := t.TempDir() - w := newCollector(t, tmp) - w.collect(t) + tmp := t.TempDir() + w := newCollector(t, tmp) + w.collect(t) - dir := join(tmp, "/dir") - for i := 0; i < 100; i++ { - go os.MkdirAll(dir, 0o0755) - go os.RemoveAll(dir) - go w.w.Add(dir) - go w.w.Remove(dir) + var ( + dir = join(tmp, "/dir") + wg sync.WaitGroup + ) + wg.Add(400) + for i := 0; i < 100; i++ { + go func() { defer wg.Done(); os.MkdirAll(dir, 0o0755) }() + go func() { defer wg.Done(); os.RemoveAll(dir) }() + go func() { defer wg.Done(); w.w.Add(dir) }() + go func() { defer wg.Done(); w.w.Remove(dir) }() + } + wg.Wait() + w.stop(t) + }) + + // Race when deleting watched directory, creating it again, and re-adding + // it. + t.Run("remove self", func(t *testing.T) { + t.Parallel() + + // TODO: seems to hang forever on Windows; possibly related to: + // https://github.com/fsnotify/fsnotify/issues/656 + // + // Although it seems to be on different points: + // + // goroutine 8 [chan receive]: + // github.com/fsnotify/fsnotify.(*readDirChangesW).AddWith(0xc00002ea40, {0xc0000940f0, 0x48}, {0x0, 0x0, 0xaf2e7f?}) + // C:/Users/martin/fsnotify/backend_windows.go:141 +0x38d + // github.com/fsnotify/fsnotify.(*readDirChangesW).Add(0xc000092000?, {0xc0000940f0?, 0x1?}) + // C:/Users/martin/fsnotify/backend_windows.go:111 +0x1f + // github.com/fsnotify/fsnotify.(*Watcher).Add(...) + // C:/Users/martin/fsnotify/fsnotify.go:313 + // github.com/fsnotify/fsnotify.TestRace.func2(0xc000003a40) + // C:/Users/martin/fsnotify/fsnotify_test.go:865 +0x1ee + // testing.tRunner(0xc000003a40, 0xb71930) + // C:/Program Files/Go/src/testing/testing.go:1792 +0xcb + // created by testing.(*T).Run in goroutine 7 + // C:/Program Files/Go/src/testing/testing.go:1851 +0x3f6 + // + // goroutine 9 [select, locked to thread]: + // github.com/fsnotify/fsnotify.(*readDirChangesW).sendError(...) + // C:/Users/martin/fsnotify/backend_windows.go:85 + // github.com/fsnotify/fsnotify.(*readDirChangesW).startRead(0xc00002ea40, 0xc0001a4100) + // C:/Users/martin/fsnotify/backend_windows.go:453 +0x319 + // github.com/fsnotify/fsnotify.(*readDirChangesW).readEvents(0xc00002ea40) + // C:/Users/martin/fsnotify/backend_windows.go:548 +0x290 + // created by github.com/fsnotify/fsnotify.newBufferedBackend in goroutine 8 + // C:/Users/martin/fsnotify/backend_windows.go:55 +0x198 + // + // goroutine 18 [chan send]: + // github.com/fsnotify/fsnotify.(*eventCollector).collect.func1() + // C:/Users/martin/fsnotify/helpers_test.go:436 +0x2cd + // created by github.com/fsnotify/fsnotify.(*eventCollector).collect in goroutine 8 + // C:/Users/martin/fsnotify/helpers_test.go:427 +0x67 + // + // The Windows backend hasn't really changed in a long time, so old + // problem, and not something we need to fix right now. + if runtime.GOOS == "windows" { + t.Skip("hangs on windows") + } + + // TODO: sometimes hands on "unix.Close(info.wd)" in kqueue.remove(). + // Only seems to happen on macOS and not the other kqueue platforms. + // + // Just skip for now; want to rewrite kqueue backend anyway... + if runtime.GOOS == "darwin" { + t.Skip("hangs on macOS") + } + + tmp := t.TempDir() + w := newCollector(t, tmp) + w.collect(t) + + var ( + dir = join(tmp, "/dir") + wg sync.WaitGroup + ) + w.w.Add(dir) + wg.Add(2000) + for i := 0; i < 1000; i++ { + go func() { defer wg.Done(); os.RemoveAll(dir) }() + go func() { defer wg.Done(); os.MkdirAll(dir, 0o0755) }() + w.w.Add(dir) + } + wg.Wait() + w.stop(t) + }) +} + +func TestNewWatcher(t *testing.T) { + w, err := NewWatcher() + if err != nil { + t.Fatal(err) + } + defaultSz := 0 + if runtime.GOOS == "windows" { + defaultSz = 50 + } + if c := cap(w.Events); c != defaultSz { + t.Errorf("cap of NewWatcher() is not %d but %d", defaultSz, c) + } + + w, err = NewBufferedWatcher(0) + if err != nil { + t.Fatal(err) + } + if c := cap(w.Events); c != 0 { + t.Errorf("cap of NewWatcher() is not %d but %d", 0, c) + } + + w, err = NewBufferedWatcher(42) + if err != nil { + t.Fatal(err) + } + if c := cap(w.Events); c != 42 { + t.Errorf("cap of NewWatcher() is not %d but %d", 42, c) } - time.Sleep(100 * time.Millisecond) - w.stop(t) } diff --git a/go.mod b/go.mod index fb5963e6..44bf2798 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fsnotify/fsnotify -go 1.17 +go 1.19 require golang.org/x/sys v0.13.0 diff --git a/helpers_test.go b/helpers_test.go index 8ad80455..b22edca5 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -432,7 +432,7 @@ func (w *eventCollector) collect(t *testing.T) { w.done <- struct{}{} return } - t.Error(e) + t.Errorf("eventCollector: unexpected error on Errors chan: %s", e) w.done <- struct{}{} return case e, ok := <-w.w.Events: @@ -671,15 +671,6 @@ func supportsRename() bool { } } -func supportsNofollow(t *testing.T) { - switch runtime.GOOS { - case "linux": - // Run test. - default: - t.Skip("withNoFollow() not yet supported on " + runtime.GOOS) - } -} - func tmppath(tmp, s string) string { if len(s) == 0 { return "" @@ -823,8 +814,6 @@ loop: supportsRecurse(t) case "filter": supportsFilter(t) - case "nofollow": - supportsNofollow(t) case "windows": if runtime.GOOS == "windows" { t.Skip("Skipping on Windows") @@ -840,9 +829,14 @@ loop: default: t.Fatalf("line %d: unknown %s reason: %q", c.line, c.cmd, c.args[0]) } - //case "state": - // mustArg(c, 0) - // do = append(do, func() { eventSeparator(); fmt.Fprintln(os.Stderr); w.w.state(); fmt.Fprintln(os.Stderr) }) + case "state": + mustArg(c, 0) + do = append(do, func() { + eventSeparator() + fmt.Fprintln(os.Stderr) + w.w.b.(interface{ state() }).state() + fmt.Fprintln(os.Stderr) + }) case "debug": mustArg(c, 1) switch c.args[0] { @@ -866,15 +860,6 @@ loop: continue } - var follow addOpt - for i := range c.args { - if c.args[i] == "nofollow" || c.args[i] == "no-follow" { - c.args = append(c.args[:i], c.args[i+1:]...) - follow = withNoFollow() - break - } - } - var op Op for _, o := range c.args[1:] { switch strings.ToLower(o) { @@ -904,11 +889,13 @@ loop: } do = append(do, func() { p := tmppath(tmp, c.args[0]) - err := w.w.AddWith(p, withOps(op), follow) + err := w.w.AddWith(p, withOps(op)) if err != nil { t.Fatalf("line %d: addWatch(%q): %s", c.line+1, p, err) } }) + case "print": + do = append(do, func() { fmt.Println(strings.Join(c.args, " ")) }) case "unwatch": mustArg(c, 1) do = append(do, func() { rmWatch(t, w.w, tmppath(tmp, c.args[0])) }) diff --git a/internal/darwin.go b/internal/darwin.go index b0eab100..6721aa60 100644 --- a/internal/darwin.go +++ b/internal/darwin.go @@ -9,31 +9,12 @@ import ( ) var ( - SyscallEACCES = syscall.EACCES - UnixEACCES = unix.EACCES + ErrSyscallEACCES = syscall.EACCES + ErrUnixEACCES = unix.EACCES ) var maxfiles uint64 -// Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ -func SetRlimit() { - var l syscall.Rlimit - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) - if err == nil && l.Cur != l.Max { - l.Cur = l.Max - syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) - } - maxfiles = l.Cur - - if n, err := syscall.SysctlUint32("kern.maxfiles"); err == nil && uint64(n) < maxfiles { - maxfiles = uint64(n) - } - - if n, err := syscall.SysctlUint32("kern.maxfilesperproc"); err == nil && uint64(n) < maxfiles { - maxfiles = uint64(n) - } -} - func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } diff --git a/internal/debug_darwin.go b/internal/debug_darwin.go index 928319fb..76001807 100644 --- a/internal/debug_darwin.go +++ b/internal/debug_darwin.go @@ -6,52 +6,10 @@ var names = []struct { n string m uint32 }{ - {"NOTE_ABSOLUTE", unix.NOTE_ABSOLUTE}, {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - {"NOTE_BACKGROUND", unix.NOTE_BACKGROUND}, - {"NOTE_CHILD", unix.NOTE_CHILD}, - {"NOTE_CRITICAL", unix.NOTE_CRITICAL}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, - {"NOTE_EXITSTATUS", unix.NOTE_EXITSTATUS}, - {"NOTE_EXIT_CSERROR", unix.NOTE_EXIT_CSERROR}, - {"NOTE_EXIT_DECRYPTFAIL", unix.NOTE_EXIT_DECRYPTFAIL}, - {"NOTE_EXIT_DETAIL", unix.NOTE_EXIT_DETAIL}, - {"NOTE_EXIT_DETAIL_MASK", unix.NOTE_EXIT_DETAIL_MASK}, - {"NOTE_EXIT_MEMORY", unix.NOTE_EXIT_MEMORY}, - {"NOTE_EXIT_REPARENTED", unix.NOTE_EXIT_REPARENTED}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FFAND", unix.NOTE_FFAND}, - {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, - {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, - {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, - {"NOTE_FFNOP", unix.NOTE_FFNOP}, - {"NOTE_FFOR", unix.NOTE_FFOR}, - {"NOTE_FORK", unix.NOTE_FORK}, - {"NOTE_FUNLOCK", unix.NOTE_FUNLOCK}, - {"NOTE_LEEWAY", unix.NOTE_LEEWAY}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_MACHTIME", unix.NOTE_MACHTIME}, - {"NOTE_MACH_CONTINUOUS_TIME", unix.NOTE_MACH_CONTINUOUS_TIME}, - {"NOTE_NONE", unix.NOTE_NONE}, - {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, - {"NOTE_OOB", unix.NOTE_OOB}, - //{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, -0x100000 (?!) - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, - {"NOTE_REAP", unix.NOTE_REAP}, {"NOTE_RENAME", unix.NOTE_RENAME}, - {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_SECONDS", unix.NOTE_SECONDS}, - {"NOTE_SIGNAL", unix.NOTE_SIGNAL}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, - {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, - {"NOTE_USECONDS", unix.NOTE_USECONDS}, - {"NOTE_VM_ERROR", unix.NOTE_VM_ERROR}, - {"NOTE_VM_PRESSURE", unix.NOTE_VM_PRESSURE}, - {"NOTE_VM_PRESSURE_SUDDEN_TERMINATE", unix.NOTE_VM_PRESSURE_SUDDEN_TERMINATE}, - {"NOTE_VM_PRESSURE_TERMINATE", unix.NOTE_VM_PRESSURE_TERMINATE}, {"NOTE_WRITE", unix.NOTE_WRITE}, } diff --git a/internal/debug_dragonfly.go b/internal/debug_dragonfly.go index 3186b0c3..76001807 100644 --- a/internal/debug_dragonfly.go +++ b/internal/debug_dragonfly.go @@ -7,27 +7,9 @@ var names = []struct { m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FFAND", unix.NOTE_FFAND}, - {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, - {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, - {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, - {"NOTE_FFNOP", unix.NOTE_FFNOP}, - {"NOTE_FFOR", unix.NOTE_FFOR}, - {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_OOB", unix.NOTE_OOB}, - {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, - {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, - {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, {"NOTE_WRITE", unix.NOTE_WRITE}, } diff --git a/internal/debug_freebsd.go b/internal/debug_freebsd.go index f69fdb93..b9e45f55 100644 --- a/internal/debug_freebsd.go +++ b/internal/debug_freebsd.go @@ -6,37 +6,15 @@ var names = []struct { n string m uint32 }{ - {"NOTE_ABSTIME", unix.NOTE_ABSTIME}, - {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - {"NOTE_CHILD", unix.NOTE_CHILD}, - {"NOTE_CLOSE", unix.NOTE_CLOSE}, - {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, + {"NOTE_WRITE", unix.NOTE_WRITE}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FFAND", unix.NOTE_FFAND}, - {"NOTE_FFCOPY", unix.NOTE_FFCOPY}, - {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK}, - {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK}, - {"NOTE_FFNOP", unix.NOTE_FFNOP}, - {"NOTE_FFOR", unix.NOTE_FFOR}, - {"NOTE_FILE_POLL", unix.NOTE_FILE_POLL}, - {"NOTE_FORK", unix.NOTE_FORK}, + {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_MSECONDS", unix.NOTE_MSECONDS}, - {"NOTE_NSECONDS", unix.NOTE_NSECONDS}, - {"NOTE_OPEN", unix.NOTE_OPEN}, - {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, - {"NOTE_READ", unix.NOTE_READ}, {"NOTE_RENAME", unix.NOTE_RENAME}, {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_SECONDS", unix.NOTE_SECONDS}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, - {"NOTE_TRIGGER", unix.NOTE_TRIGGER}, - {"NOTE_USECONDS", unix.NOTE_USECONDS}, - {"NOTE_WRITE", unix.NOTE_WRITE}, + {"NOTE_OPEN", unix.NOTE_OPEN}, + {"NOTE_CLOSE", unix.NOTE_CLOSE}, + {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE}, + {"NOTE_READ", unix.NOTE_READ}, } diff --git a/internal/debug_kqueue.go b/internal/debug_kqueue.go index 607e683b..5d811643 100644 --- a/internal/debug_kqueue.go +++ b/internal/debug_kqueue.go @@ -27,6 +27,6 @@ func Debug(name string, kevent *unix.Kevent_t) { if unknown > 0 { l = append(l, fmt.Sprintf("0x%x", unknown)) } - fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-60s → %q\n", + fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-20s → %q\n", time.Now().Format("15:04:05.000000000"), mask, strings.Join(l, " | "), name) } diff --git a/internal/debug_netbsd.go b/internal/debug_netbsd.go index e5b3b6f6..76001807 100644 --- a/internal/debug_netbsd.go +++ b/internal/debug_netbsd.go @@ -7,19 +7,9 @@ var names = []struct { m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, - {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_WRITE", unix.NOTE_WRITE}, } diff --git a/internal/debug_openbsd.go b/internal/debug_openbsd.go index 1dd455bc..871766d6 100644 --- a/internal/debug_openbsd.go +++ b/internal/debug_openbsd.go @@ -7,22 +7,10 @@ var names = []struct { m uint32 }{ {"NOTE_ATTRIB", unix.NOTE_ATTRIB}, - // {"NOTE_CHANGE", unix.NOTE_CHANGE}, // Not on 386? - {"NOTE_CHILD", unix.NOTE_CHILD}, {"NOTE_DELETE", unix.NOTE_DELETE}, - {"NOTE_EOF", unix.NOTE_EOF}, - {"NOTE_EXEC", unix.NOTE_EXEC}, - {"NOTE_EXIT", unix.NOTE_EXIT}, {"NOTE_EXTEND", unix.NOTE_EXTEND}, - {"NOTE_FORK", unix.NOTE_FORK}, {"NOTE_LINK", unix.NOTE_LINK}, - {"NOTE_LOWAT", unix.NOTE_LOWAT}, - {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, - {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK}, {"NOTE_RENAME", unix.NOTE_RENAME}, - {"NOTE_REVOKE", unix.NOTE_REVOKE}, - {"NOTE_TRACK", unix.NOTE_TRACK}, - {"NOTE_TRACKERR", unix.NOTE_TRACKERR}, {"NOTE_TRUNCATE", unix.NOTE_TRUNCATE}, {"NOTE_WRITE", unix.NOTE_WRITE}, } diff --git a/internal/freebsd.go b/internal/freebsd.go index 547df1df..758a2490 100644 --- a/internal/freebsd.go +++ b/internal/freebsd.go @@ -9,23 +9,12 @@ import ( ) var ( - SyscallEACCES = syscall.EACCES - UnixEACCES = unix.EACCES + ErrSyscallEACCES = syscall.EACCES + ErrUnixEACCES = unix.EACCES ) var maxfiles uint64 -func SetRlimit() { - // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ - var l syscall.Rlimit - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) - if err == nil && l.Cur != l.Max { - l.Cur = l.Max - syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) - } - maxfiles = uint64(l.Cur) -} - func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, uint64(dev)) } diff --git a/internal/unix.go b/internal/unix.go index 30976ce9..9c66f5d3 100644 --- a/internal/unix.go +++ b/internal/unix.go @@ -1,4 +1,4 @@ -//go:build !windows && !darwin && !freebsd +//go:build !windows && !darwin && !freebsd && !plan9 package internal @@ -9,23 +9,12 @@ import ( ) var ( - SyscallEACCES = syscall.EACCES - UnixEACCES = unix.EACCES + ErrSyscallEACCES = syscall.EACCES + ErrUnixEACCES = unix.EACCES ) var maxfiles uint64 -func SetRlimit() { - // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/ - var l syscall.Rlimit - err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l) - if err == nil && l.Cur != l.Max { - l.Cur = l.Max - syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l) - } - maxfiles = uint64(l.Cur) -} - func Maxfiles() uint64 { return maxfiles } func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) } func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) } diff --git a/internal/unix2.go b/internal/unix2.go index 37dfeddc..b2d89592 100644 --- a/internal/unix2.go +++ b/internal/unix2.go @@ -2,6 +2,24 @@ package internal +import "syscall" + func HasPrivilegesForSymlink() bool { return true } + +// IgnoringEINTR makes a function call and repeats it if it returns an +// EINTR error. This appears to be required even though we install all +// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846. +// Also #20400 and #36644 are issues in which a signal handler is +// installed without setting SA_RESTART. None of these are the common case, +// but there are enough of them that it seems that we can't avoid +// an EINTR loop. +func IgnoringEINTR[T any](fn func() (T, error)) (T, error) { + for { + v, err := fn() + if err != syscall.EINTR { + return v, err + } + } +} diff --git a/internal/windows.go b/internal/windows.go index a72c6495..e24d5692 100644 --- a/internal/windows.go +++ b/internal/windows.go @@ -10,11 +10,10 @@ import ( // Just a dummy. var ( - SyscallEACCES = errors.New("dummy") - UnixEACCES = errors.New("dummy") + ErrSyscallEACCES = errors.New("dummy") + ErrUnixEACCES = errors.New("dummy") ) -func SetRlimit() {} func Maxfiles() uint64 { return 1<<64 - 1 } func Mkfifo(path string, mode uint32) error { return errors.New("no FIFOs on Windows") } func Mknod(path string, mode uint32, dev int) error { return errors.New("no device nodes on Windows") } diff --git a/internal/ztest/diff.go b/internal/ztest/diff.go index ab7bfa79..e9d09af9 100644 --- a/internal/ztest/diff.go +++ b/internal/ztest/diff.go @@ -1,7 +1,3 @@ -// Copy of https://github.com/arp242/zstd/tree/master/ztest – vendored here so -// we don't add a dependency for just one file used in tests. DiffXML was -// removed as it depends on zgo.at/zstd/zxml. - // This code is based on https://github.com/pmezard/go-difflib // // Copyright (c) 2013, Patrick Mezard @@ -32,6 +28,10 @@ // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// Package ztest is a copy of https://github.com/arp242/zstd/tree/master/ztest – +// vendored here so we don't add a dependency for just one file used in tests. +// +// DiffXML was removed as it depends on zgo.at/zstd/zxml. package ztest import ( @@ -163,14 +163,14 @@ func applyOpt(have, want string, opt ...DiffOpt) (string, string) { want = "{}" } - var h interface{} + var h any haveJ, err := indentJSON([]byte(have), &h, "", " ") if err != nil { have = fmt.Sprintf("ztest.Diff: ERROR formatting have: %s\ntext: %s", err, have) } else { have = string(haveJ) } - var w interface{} + var w any wantJ, err := indentJSON([]byte(want), &w, "", " ") if err != nil { want = fmt.Sprintf("ztest.Diff: ERROR formatting want: %s\ntext: %s", err, want) @@ -548,7 +548,7 @@ func splitLines(s string) []string { return lines } -func indentJSON(data []byte, v interface{}, prefix, indent string) ([]byte, error) { +func indentJSON(data []byte, v any, prefix, indent string) ([]byte, error) { err := json.Unmarshal(data, v) if err != nil { return nil, err diff --git a/internal/ztest/diff_test.go b/internal/ztest/diff_test.go index 3b75e8e0..b9b5ea4c 100644 --- a/internal/ztest/diff_test.go +++ b/internal/ztest/diff_test.go @@ -9,7 +9,7 @@ import ( "time" ) -func assertEqual(t *testing.T, a, b interface{}) { +func assertEqual(t *testing.T, a, b any) { if !reflect.DeepEqual(a, b) { t.Errorf("%v != %v", a, b) } @@ -147,7 +147,7 @@ func TestDiffMatch(t *testing.T) { {"Hello", "He%(ANY)", ""}, {"Hello " + year + "!", "Hello %(YEAR)!", ""}, - {"Hello " + year + "!", "Hello %(YEAR)", "\n--- have\n+++ want\n@@ -1 +1 @@\n-have Hello 2024!\n+want Hello 2024\n"}, + {"Hello " + year + "!", "Hello %(YEAR)", "\n--- have\n+++ want\n@@ -1 +1 @@\n-have Hello " + year + "!\n+want Hello " + year + "\n"}, {"Hello xy", "Hello %(ANY 2)", ""}, {"Hello xy", "Hello %(ANY 2,)", ""}, diff --git a/shared.go b/shared.go new file mode 100644 index 00000000..3ee9b58f --- /dev/null +++ b/shared.go @@ -0,0 +1,64 @@ +package fsnotify + +import "sync" + +type shared struct { + Events chan Event + Errors chan error + done chan struct{} + mu sync.Mutex +} + +func newShared(ev chan Event, errs chan error) *shared { + return &shared{ + Events: ev, + Errors: errs, + done: make(chan struct{}), + } +} + +// Returns true if the event was sent, or false if watcher is closed. +func (w *shared) sendEvent(e Event) bool { + if e.Op == 0 { + return true + } + select { + case <-w.done: + return false + case w.Events <- e: + return true + } +} + +// Returns true if the error was sent, or false if watcher is closed. +func (w *shared) sendError(err error) bool { + if err == nil { + return true + } + select { + case <-w.done: + return false + case w.Errors <- err: + return true + } +} + +func (w *shared) isClosed() bool { + select { + case <-w.done: + return true + default: + return false + } +} + +// Mark as closed; returns true if it was already closed. +func (w *shared) close() bool { + w.mu.Lock() + defer w.mu.Unlock() + if w.isClosed() { + return true + } + close(w.done) + return false +} diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 00000000..8fa7351f --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1,3 @@ +checks = ['all', + '-U1000', # Don't complain about unused functions. +] diff --git a/testdata/watch-dir/create-cyclic-symlink b/testdata/watch-dir/create-cyclic-symlink index 5bede580..5f2d40cb 100644 --- a/testdata/watch-dir/create-cyclic-symlink +++ b/testdata/watch-dir/create-cyclic-symlink @@ -9,7 +9,7 @@ echo foo >>/link Output: write /link - create /link + write /link linux, windows, fen: remove /link diff --git a/testdata/watch-dir/create-fifo b/testdata/watch-dir/create-fifo index 10efeb41..a29cdd3a 100644 --- a/testdata/watch-dir/create-fifo +++ b/testdata/watch-dir/create-fifo @@ -1,5 +1,3 @@ -# FIFO - require mkfifo watch / diff --git a/testdata/watch-dir/create-file-in-subdir b/testdata/watch-dir/create-file-in-subdir new file mode 100644 index 00000000..adb79b71 --- /dev/null +++ b/testdata/watch-dir/create-file-in-subdir @@ -0,0 +1,10 @@ +watch / +mkdir /dir +touch /dir/file + +Output: + create /dir + + fen: # TODO + create /dir + write /dir diff --git a/testdata/watch-dir/dir-only b/testdata/watch-dir/dir-only index 6d7d88f9..b4452408 100644 --- a/testdata/watch-dir/dir-only +++ b/testdata/watch-dir/dir-only @@ -1,5 +1,3 @@ -# - touch /before-watch watch / diff --git a/testdata/watch-dir/only-chmod b/testdata/watch-dir/only-chmod index f108505c..d9acaf39 100644 --- a/testdata/watch-dir/only-chmod +++ b/testdata/watch-dir/only-chmod @@ -13,11 +13,11 @@ echo data >>/link touch /dir/file # Rename -mv /file /rename +mv /file /rename mv /rename /file -mv /dir /rename +mv /dir /rename mv /rename /dir -mv /link /rename +mv /link /rename mv /rename /link # Chmod diff --git a/testdata/watch-dir/remove-symlink b/testdata/watch-dir/remove-symlink index a6ec0237..e54c3389 100644 --- a/testdata/watch-dir/remove-symlink +++ b/testdata/watch-dir/remove-symlink @@ -1,4 +1,5 @@ # Remove a symlink. +require symlink touch /file ln -s /file /link diff --git a/testdata/watch-dir/rename-nested-watched-dir b/testdata/watch-dir/rename-nested-watched-dir new file mode 100644 index 00000000..ae61db10 --- /dev/null +++ b/testdata/watch-dir/rename-nested-watched-dir @@ -0,0 +1,24 @@ +# Looks like Windows doesn't allow rename if there's watch: +# mv("/001/dir", "/001/dir-rename"): rename /001/dir /001/dir-rename: Access is denied. +skip windows + +mkdir -p /dir/sub + +watch /dir +watch /dir/sub + +mv /dir /dir-rename +touch /dir-rename/sub/file + +Output: + rename /dir + # Still /dir as it's a different watch, which doesn't get updated. Could be + # considered both a feature and a bug (see #694). + create /dir/sub/file + + kqueue: # TODO: kqueue only sends rename. + rename /dir + + illumos: # TODO + rename /dir + remove /dir/sub diff --git a/testdata/watch-dir/rename-overwrite b/testdata/watch-dir/rename-overwrite index 0e6a8480..96801647 100644 --- a/testdata/watch-dir/rename-overwrite +++ b/testdata/watch-dir/rename-overwrite @@ -6,16 +6,19 @@ touch /rename watch / mv /file /rename +echo foo >>/rename Output: remove /rename rename /file create /rename ← /file + write /rename # Inotify just sends MOVED_FROM and MOVED_TO. linux: rename /file create /rename ← /file + write /rename dragonfly: remove / diff --git a/testdata/watch-dir/rename-to-unwatched b/testdata/watch-dir/rename-to-unwatched index 9adb0317..4f644cd7 100644 --- a/testdata/watch-dir/rename-to-unwatched +++ b/testdata/watch-dir/rename-to-unwatched @@ -1,9 +1,5 @@ # Rename to unwatched dir. -# if runtime.GOOS == "netbsd" && isCI() { -# t.Skip("fails in CI; see #488") // TODO -# } - mkdir /dir mkdir /unwatch watch /dir diff --git a/testdata/watch-dir/subdir b/testdata/watch-dir/subdir index f1544eb7..e8a6cd16 100644 --- a/testdata/watch-dir/subdir +++ b/testdata/watch-dir/subdir @@ -1,5 +1,3 @@ -# - watch / mkdir /sub # Create sub-directory diff --git a/testdata/watch-dir/symlink-dir b/testdata/watch-dir/symlink-dir index 04d1a9e1..d32bfca2 100644 --- a/testdata/watch-dir/symlink-dir +++ b/testdata/watch-dir/symlink-dir @@ -4,9 +4,26 @@ require symlink mkdir /dir watch / ln -s /dir /link +touch link/file # Shouldn't give any events. Output: create /link + # kqueue sends an event because it sends an event every time a directory + # changes, but fsnotify doesn't detect the symlink as a "directory". It's + # been like this for over a decade. When it was changed before people + # complained so I'm hesitant to fix it now as part of a bugfix. + # + # TODO: it shouldn't do this; it doesn't work like this on inotify. + kqueue: + create /link + write /link + + # TODO: should also fix FEN, which seems to send a write for regular dirs as + # well. + fen: + create /link + write /dir + dragonfly: # TODO: can we fix this? no-events diff --git a/testdata/watch-dir/symlink-nofollow b/testdata/watch-dir/symlink-nofollow deleted file mode 100644 index cf05990a..00000000 --- a/testdata/watch-dir/symlink-nofollow +++ /dev/null @@ -1,30 +0,0 @@ -# Create a new symlink to a watched file. -require symlink -require nofollow - -touch /file -mkdir /dir - -watch / default nofollow - -ln -s /dir /link-file -ln -s /dir /link-dir - -rm -r /dir - -echo asd >>/file -rm /file - -rm /link-file -rm /link-dir - -Output: - create /link-dir - create /link-file - - remove /dir - write /file - remove /file - - remove /link-file - remove /link-dir diff --git a/testdata/watch-dir/truncate-file b/testdata/watch-dir/truncate-file index 73cd9bc5..c3903299 100644 --- a/testdata/watch-dir/truncate-file +++ b/testdata/watch-dir/truncate-file @@ -9,9 +9,9 @@ Output: write /file # write # Truncate is chmod on kqueue, except dragonfly where it seems a write. + kqueue: + chmod /file + write /file dragonfly: write /file write /file - kqueue: - chmod /file - write /file diff --git a/testdata/watch-file/chmod b/testdata/watch-file/chmod index 95cb392f..bee71dec 100644 --- a/testdata/watch-file/chmod +++ b/testdata/watch-file/chmod @@ -7,5 +7,5 @@ chmod 700 /file Output: chmod /file - windows: + windows: # No chmod on Windows no-events diff --git a/testdata/watch-file/chmod-after-write b/testdata/watch-file/chmod-after-write index 4355a611..0e24cf4e 100644 --- a/testdata/watch-file/chmod-after-write +++ b/testdata/watch-file/chmod-after-write @@ -13,5 +13,5 @@ Output: write /file chmod /file - windows: + windows: # No chmod on Windows write /file diff --git a/testdata/watch-file/re-add-renamed-filed b/testdata/watch-file/re-add-renamed-file similarity index 100% rename from testdata/watch-file/re-add-renamed-filed rename to testdata/watch-file/re-add-renamed-file diff --git a/testdata/watch-file/remove-watched-file b/testdata/watch-file/remove-watched-file index aee4e4ac..7f01a31b 100644 --- a/testdata/watch-file/remove-watched-file +++ b/testdata/watch-file/remove-watched-file @@ -7,6 +7,6 @@ rm /file Output: remove /file - linux: # unlink always emits a chmod on linux. + linux: # unlink always emits a chmod on Linux. chmod /file remove /file diff --git a/testdata/watch-recurse/rename-dir b/testdata/watch-recurse/rename-dir index 3132a203..e4469728 100644 --- a/testdata/watch-recurse/rename-dir +++ b/testdata/watch-recurse/rename-dir @@ -15,7 +15,6 @@ Output: create /sub-rename/file # touch /sub-rename/file create /sub-rename/dir/file # touch /sub-rename/dir/file - # Same as above, but with these stupid dir writes Windows sends. # # TODO: see if we can suppress that. diff --git a/testdata/watch-recurse/rename-to-different-dir b/testdata/watch-recurse/rename-to-different-dir new file mode 100644 index 00000000..f50ad699 --- /dev/null +++ b/testdata/watch-recurse/rename-to-different-dir @@ -0,0 +1,23 @@ +# TODO: frequently (but not always) hangs in the CI until it gets killed. Not +# sure what's going on – can't reproduce locally. +skip windows + +# Make sure RenamedFrom is set when renaming from one directory to another. +require recurse + +mkdir /dir1 +mkdir /dir2 +touch /dir1/file + +watch /... + +mv /dir1/file /dir2/rename + +Output: + rename /dir1/file + create /dir2/rename ← /dir1/file + + windows: # TODO: Windows sends these stupid writes + remove /dir1/file + create /dir2/rename + write /dir2 diff --git a/testdata/watch-recurse/rename-watched-dir b/testdata/watch-recurse/rename-watched-dir new file mode 100644 index 00000000..ce68efa7 --- /dev/null +++ b/testdata/watch-recurse/rename-watched-dir @@ -0,0 +1,15 @@ +require recurse + +mkdir /dir +mkdir /dir/sub + +watch /dir/... + +mv /dir /renamed +touch /renamed/sub/file + +Output: + rename /dir + + windows: # TODO + create /dir/sub/file diff --git a/testdata/watch-symlink/nofollow-dir b/testdata/watch-symlink/nofollow-dir deleted file mode 100644 index 984f387e..00000000 --- a/testdata/watch-symlink/nofollow-dir +++ /dev/null @@ -1,17 +0,0 @@ -# Watch a symlink. -require symlink -require nofollow - -mkdir /dir -ln -s /dir /link -watch /link default nofollow - -touch /dir/file -chmod 777 /dir - -rm -r /dir -rm /link - -Output: - chmod /link - remove /link diff --git a/testdata/watch-symlink/nofollow-file b/testdata/watch-symlink/nofollow-file deleted file mode 100644 index 7d0c2a9b..00000000 --- a/testdata/watch-symlink/nofollow-file +++ /dev/null @@ -1,20 +0,0 @@ -# Watch a symlink. -require symlink -require nofollow - -touch /file -ln -s /file /link - -watch /link nofollow default - -chmod 777 /file -echo asd >>/file -rm /file - -rm /link - -touch /link - -Output: - chmod /link - remove /link diff --git a/testdata/watch-symlink/to-dir b/testdata/watch-symlink/to-dir index da8122a9..976810a8 100644 --- a/testdata/watch-symlink/to-dir +++ b/testdata/watch-symlink/to-dir @@ -2,9 +2,10 @@ require symlink mkdir /dir +touch /dir/existing ln -s /dir /link -watch /link +watch /link touch /dir/file Output: diff --git a/testdata/watch-symlink/to-dir-relative b/testdata/watch-symlink/to-dir-relative index 32ab0282..477d6a91 100644 --- a/testdata/watch-symlink/to-dir-relative +++ b/testdata/watch-symlink/to-dir-relative @@ -9,6 +9,3 @@ touch /dir/file Output: create /link/file - - kqueue: # TODO: broken - no-events diff --git a/testdata/watch-symlink/to-file-relative b/testdata/watch-symlink/to-file-relative index 786e1857..767a68bf 100644 --- a/testdata/watch-symlink/to-file-relative +++ b/testdata/watch-symlink/to-file-relative @@ -10,7 +10,5 @@ echo hello >>/file Output: write /link - kqueue: # TODO: broken - no-events windows: # TODO: investigate. no-events