Repository: fsnotify/fsnotify
Pull request #463 (2022-07-21 11:27:21)

Add cmd/fsnotify
This adds a little example that can be run:

	% go run ./cmd/fsnotify **/*
	12:76:11.4710 watching; press ^C to exit
	12:76:13.6549   1 REMOVE                "cmd/fsnotify/4913"
	12:76:13.6550   2 RENAME                "cmd/fsnotify/main.go"
	12:76:13.6550   3 RENAME                "cmd/fsnotify/main.go"
	12:76:13.6612   4 CREATE                "cmd/fsnotify/main.go"
	12:76:13.6612   5 WRITE      (modified) "cmd/fsnotify/main.go"
	12:76:13.6614   6 WRITE      (modified) "cmd/fsnotify/main.go"
	12:76:13.6648   7 CHMOD                 "cmd/fsnotify/main.go"
	12:76:15.2919   8 CREATE                "cmd/fsnotify/4913"
	12:76:15.2919   9 REMOVE                "cmd/fsnotify/4913"
	12:76:15.2922  10 CHMOD                 "cmd/fsnotify/main.go"
	12:76:15.2923  11 REMOVE                "cmd/fsnotify/main.go"
	12:76:15.2925  12 RENAME                "cmd/fsnotify/main.go"
	12:76:15.2986  13 CREATE                "cmd/fsnotify/main.go"
	12:76:15.2986  14 WRITE      (modified) "cmd/fsnotify/main.go"
	12:76:15.2988  15 WRITE      (modified) "cmd/fsnotify/main.go"
	12:76:15.3024  16 CHMOD                 "cmd/fsnotify/main.go"
	^C

It's mostly the same as the example in the README, except that it takes
paths from the commandline and aligns things a bit nicer.

This is useful for documentation and experimentation. For example, while
reviewing some PRs I found it useful to quickly get an overview of "what
does fsnotify do now, and what does it do with this patch?"


Patch:
diff --git a/.github/workflows/Vagrantfile.debian6 b/.github/workflows/Vagrantfile.debian6
new file mode 100644
index 0000000..fbe8f08
--- /dev/null
+++ b/.github/workflows/Vagrantfile.debian6
@@ -0,0 +1,6 @@
+Vagrant.configure("2") do |config|
+  config.vm.box = "threatstack/debian6"
+  config.vm.box_version = "1.0.0"
+
+  config.vm.define 'debian6'
+end 
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 80f3a06..0d004c1 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,26 +7,21 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        goos:
-          - android
-          - darwin
-          - freebsd
-          - ios
-          - linux
-          - windows
-        goarch:
-          - amd64
-          - arm64
+        go:
+          - '1.16'
+          - '1.18'
     runs-on: ubuntu-latest
     steps:
       - name: setup Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
-          go-version: '1.18'
+          go-version: ${{ matrix.go }}
 
       - name: checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
-      - name: build-${{ matrix.goos }}-${{ matrix.goarch }}
+      - name: build
         run: |
-          GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build
+          for a in $(go tool dist list); do
+            GOOS=${a%%/*} GOARCH=${a#*/} go build
+          done
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..5df6527
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,30 @@
+name: test
+on:
+  push:
+  pull_request:
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - name: setup Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: '1.18'
+
+      - name: checkout
+        uses: actions/checkout@v3
+
+      - name: gofmt
+        run: |
+          test -z "$(gofmt -s -d . | tee /dev/stderr)"
+
+      - name: vet
+        run: |
+          go vet ./...
+
+      - name: golangci-lint
+        uses: golangci/golangci-lint-action@v3
+        continue-on-error: true
+        with:
+          version: latest
+          skip-go-installation: true
diff --git a/.github/workflows/vagrant.yml b/.github/workflows/vagrant.yml
new file mode 100644
index 0000000..0b63f05
--- /dev/null
+++ b/.github/workflows/vagrant.yml
@@ -0,0 +1,26 @@
+name: vagrant
+on:
+  push:
+  pull_request:
+jobs:
+  test:
+    strategy:
+      fail-fast: false
+      matrix:
+        image:
+          - debian6
+    runs-on: macos-12
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: setup Go
+        uses: actions/setup-go@v3
+        with:
+          go-version: '1.18'
+
+      - name: test
+        run: |
+          cp -f .github/workflows/Vagrantfile.${{ matrix.image }} Vagrantfile
+          GOOS=linux GOARCH=amd64 go test -o fsnotify.test -c ./...
+          vagrant up
+          vagrant ssh -c "/vagrant/fsnotify.test"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc01c08..42bb7d7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,6 +40,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
    [#385](https://github.com/fsnotify/fsnotify/pull/385)
 * Go 1.14+: Fix unsafe pointer conversion [#325](https://github.com/fsnotify/fsnotify/pull/325)
 
+## [1.4.9] - 2020-03-11
+
+* Move example usage to the readme #329. This may resolve #328.
+
+## [1.4.8] - 2020-03-10
+
+* CI: test more go versions (@nathany 1d13583d846ea9d66dcabbfefbfb9d8e6fb05216)
+* Tests: Queued inotify events could have been read by the test before max_queued_events was hit (@matthias-stone #265)
+* Tests:  t.Fatalf -> t.Errorf in go routines (@gdey #266)
+* CI: Less verbosity (@nathany #267)
+* Tests: Darwin: Exchangedata is deprecated on 10.13 (@nathany #267)
+* Tests: Check if channels are closed in the example (@alexeykazakov #244)
+* CI: Only run golint on latest version of go and fix issues (@cpuguy83 #284)
+* CI: Add windows to travis matrix (@cpuguy83 #284)
+* Docs: Remover appveyor badge (@nathany 11844c0959f6fff69ba325d097fce35bd85a8e93)
+* Linux: create epoll and pipe fds with close-on-exec (@JohannesEbke #219)
+* Linux: open files with close-on-exec (@linxiulei #273)
+* Docs: Plan to support fanotify (@nathany ab058b44498e8b7566a799372a39d150d9ea0119 )
+* Project: Add go.mod (@nathany #309)
+* Project: Revise editor config (@nathany #309)
+* Project: Update copyright for 2019 (@nathany #309)
+* CI: Drop go1.8 from CI matrix (@nathany #309)
+* Docs: Updating the FAQ section for supportability with NFS & FUSE filesystems (@Pratik32 4bf2d1fec78374803a39307bfb8d340688f4f28e )
+
 ## [1.4.7] - 2018-01-09
 
 * BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine)
diff --git a/README.md b/README.md
index dc7d9d2..823e019 100644
--- a/README.md
+++ b/README.md
@@ -6,22 +6,26 @@
 
 `fsnotify` supports Windows, Linux, BSD and macOS with a common API.
 
-| Adapter               | OS                               | Status                                                                                                                          |
-| --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
-| inotify               | Linux 2.6.27 or later, Android\* | Supported |
-| kqueue                | BSD, macOS, iOS\*                | Supported |
-| ReadDirectoryChangesW | Windows                          | Supported |
-| FSEvents              | macOS                            | [Planned](https://github.com/fsnotify/fsnotify/issues/11)                                                                       |
-| FEN                   | Solaris 11                       | [In Progress](https://github.com/fsnotify/fsnotify/pull/371)                                                                   |
-| fanotify              | Linux 2.6.37+                    | [Maybe](https://github.com/fsnotify/fsnotify/issues/114)                                                                      |
-| USN Journals          | Windows                          | [Maybe](https://github.com/fsnotify/fsnotify/issues/53)                                                                         |
-| Polling               | *All*                            | [Maybe](https://github.com/fsnotify/fsnotify/issues/9)                                                                          |
+| Adapter               | OS                       | Status                                                       |
+| --------------------- | -------------------------| -------------------------------------------------------------|
+| inotify               | Linux 2.6.32+, Android\* | Supported                                                    |
+| kqueue                | BSD, macOS, iOS\*        | Supported                                                    |
+| ReadDirectoryChangesW | Windows                  | Supported                                                    |
+| FSEvents              | macOS                    | [Planned](https://github.com/fsnotify/fsnotify/issues/11)    |
+| FEN                   | Solaris 11               | [In Progress](https://github.com/fsnotify/fsnotify/pull/371) |
+| fanotify              | Linux 2.6.37+            | [Maybe](https://github.com/fsnotify/fsnotify/issues/114)     |
+| USN Journals          | Windows                  | [Maybe](https://github.com/fsnotify/fsnotify/issues/53)      |
+| Polling               | *All*                    | [Maybe](https://github.com/fsnotify/fsnotify/issues/9)       |
 
 \* Android and iOS are untested.
 
-Please see [the documentation](https://pkg.go.dev/github.com/fsnotify/fsnotify) and consult the [FAQ](#faq) for usage information.
+fsnotify requires Go 1.16 or newer. Please see
+[the documentation](https://pkg.go.dev/github.com/fsnotify/fsnotify)
+and consult the [FAQ](#faq) for usage information.
 
-NOTE: fsnotify utilizes [`golang.org/x/sys`](https://pkg.go.dev/golang.org/x/sys) rather than [`syscall`](https://pkg.go.dev/syscall) from the standard library.
+NOTE: fsnotify utilizes
+[`golang.org/x/sys`](https://pkg.go.dev/golang.org/x/sys) rather than
+[`syscall`](https://pkg.go.dev/syscall) from the standard library.
 
 ## API stability
 
@@ -76,6 +80,11 @@ func main() {
 }
 ```
 
+A slightly more expansive example can be found in [cmd/fsnotify](cmd/fsnotify), which can be run with:
+
+        # Watch the current directory (not recursive).
+        $ go run ./cmd/fsnotify .
+
 ## Contributing
 
 Please refer to [CONTRIBUTING][] before opening an issue or pull request.
@@ -94,29 +103,56 @@ No, you must add watches for any directory you want to watch (a recursive watche
 
 As of now, yes. Looking into making this single-thread friendly (see [howeyc #7][#7])
 
-**Why am I receiving multiple events for the same file on OS X?**
+**Why am I receiving multiple events for the same file on macOS?**
 
-Spotlight indexing on OS X can result in multiple events (see [howeyc #62][#62]). A temporary workaround is to add your folder(s) to the *Spotlight Privacy settings* until we have a native FSEvents implementation (see [#11][]).
+Spotlight indexing on macOS can result in multiple events (see
+[howeyc #62][#62]). A temporary workaround is to add your folder(s) to the
+*Spotlight Privacy settings* until we have a native FSEvents implementation (see
+[#11][]).
 
 **How many files can be watched at once?**
 
 There are OS-specific limits as to how many watches can be created:
-* Linux: /proc/sys/fs/inotify/max_user_watches contains the limit, reaching this limit results in a "no space left on device" error.
-* BSD / OSX: sysctl variables "kern.maxfiles" and "kern.maxfilesperproc", reaching these limits results in a "too many open files" error.
 
-**Why don't notifications work with NFS filesystems or filesystem in userspace (FUSE)?**
+* Linux: the `fs.inotify.max_user_watches` sysctl variable specifies the upper
+  limit for the number of watches per user, and `fs.inotify.max_user_instances`
+  specifies the maximum number of inotify instances per user. Every Watcher you
+  create is an "instance", and every path you add is a "watch".
+
+  These are also exposed in /proc as `/proc/sys/fs/inotify/max_user_watches` and
+  `/proc/sys/fs/inotify/max_user_instances`
+
+  To increase them you can use `sysctl` or write the value to proc file:
+
+	  # The default values on Linux 5.18
+      sysctl fs.inotify.max_user_watches=124983
+      sysctl fs.inotify.max_user_instances=128
+
+  To make the changes persist on reboot edit `/etc/sysctl.conf` or
+  `/usr/lib/sysctl.d/50-default.conf` (some systemd systems):
 
-fsnotify requires support from underlying OS to work. The current NFS protocol does not provide network level support for file notifications.
+      fs.inotify.max_user_watches=124983
+      fs.inotify.max_user_instances=128
+
+  Reaching the limit will result in a "no space left on device" or "too many
+  open files" error.
+
+* BSD / macOS: sysctl variables `kern.maxfiles` and `kern.maxfilesperproc`,
+  reaching these limits results in a "too many open files" error.
+
+**Why don't notifications work with NFS, SMB, or filesystem in userspace (FUSE)?**
+
+fsnotify requires support from underlying OS to work. The current NFS and SMB
+protocols does not provide network level support for file notifications.
 
 [#62]: https://github.com/howeyc/fsnotify/issues/62
 [#18]: https://github.com/fsnotify/fsnotify/issues/18
 [#11]: https://github.com/fsnotify/fsnotify/issues/11
 [#7]: https://github.com/howeyc/fsnotify/issues/7
 
-[contributing]: https://github.com/fsnotify/fsnotify/blob/master/CONTRIBUTING.md
+[contributing]: https://github.com/fsnotify/fsnotify/blob/main/CONTRIBUTING.md
 
 ## Related Projects
 
 * [notify](https://github.com/rjeczalik/notify)
 * [fsevents](https://github.com/fsnotify/fsevents)
-
diff --git a/cmd/fsnotify/main.go b/cmd/fsnotify/main.go
new file mode 100644
index 0000000..1b0356a
--- /dev/null
+++ b/cmd/fsnotify/main.go
@@ -0,0 +1,67 @@
+package main
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/fsnotify/fsnotify"
+)
+
+func fatal(err error) {
+	if err == nil {
+		return
+	}
+	fmt.Fprintf(os.Stderr, "%s: %s\n", filepath.Base(os.Args[0]), err)
+	os.Exit(1)
+}
+
+func line(s string, args ...interface{}) {
+	fmt.Printf(time.Now().Format("15:16:05.0000")+" "+s+"\n", args...)
+}
+
+func main() {
+	if len(os.Args) < 2 {
+		fatal(errors.New("must specify at least one path to watch"))
+	}
+
+	w, err := fsnotify.NewWatcher()
+	fatal(err)
+	defer w.Close()
+
+	go func() {
+		i := 0
+		for {
+			select {
+			case e, ok := <-w.Events:
+				if !ok {
+					return
+				}
+
+				i++
+				m := ""
+				if e.Op&fsnotify.Write == fsnotify.Write {
+					m = "(modified)"
+				}
+				line("%3d %-10s %-10s %q", i, e.Op, m, e.Name)
+			case err, ok := <-w.Errors:
+				if !ok {
+					return
+				}
+				line("ERROR: %s", err)
+			}
+		}
+	}()
+
+	for _, p := range os.Args[1:] {
+		err = w.Add(p)
+		if err != nil {
+			fatal(fmt.Errorf("%q: %w", p, err))
+		}
+	}
+
+	line("watching; press ^C to exit")
+	<-make(chan struct{})
+}
diff --git a/inotify.go b/inotify.go
index e6abddf..a42da58 100644
--- a/inotify.go
+++ b/inotify.go
@@ -22,39 +22,36 @@ import (
 
 // Watcher watches a set of files, delivering events to a channel.
 type Watcher struct {
-	Events   chan Event
-	Errors   chan error
-	mu       sync.Mutex // Map access
-	fd       int
-	poller   *fdPoller
-	watches  map[string]*watch // Map of inotify watches (key: path)
-	paths    map[int]string    // Map of watched paths (key: watch descriptor)
-	done     chan struct{}     // Channel for sending a "quit message" to the reader goroutine
-	doneResp chan struct{}     // Channel to respond to Close
+	fd          int // https://github.com/golang/go/issues/26439 can't call .Fd() on os.FIle or Read will no longer return on Close()
+	Events      chan Event
+	Errors      chan error
+	mu          sync.Mutex // Map access
+	inotifyFile *os.File
+	watches     map[string]*watch // Map of inotify watches (key: path)
+	paths       map[int]string    // Map of watched paths (key: watch descriptor)
+	done        chan struct{}     // Channel for sending a "quit message" to the reader goroutine
+	doneResp    chan struct{}     // Channel to respond to Close
 }
 
 // NewWatcher establishes a new watcher with the underlying OS and begins waiting for events.
 func NewWatcher() (*Watcher, error) {
 	// Create inotify fd
-	fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC)
+	// Need to set the FD to nonblocking mode in order for SetDeadline methods to work
+	// Otherwise, blocking i/o operations won't terminate on close
+	fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
 	if fd == -1 {
 		return nil, errno
 	}
-	// Create epoll
-	poller, err := newFdPoller(fd)
-	if err != nil {
-		unix.Close(fd)
-		return nil, err
-	}
+
 	w := &Watcher{
-		fd:       fd,
-		poller:   poller,
-		watches:  make(map[string]*watch),
-		paths:    make(map[int]string),
-		Events:   make(chan Event),
-		Errors:   make(chan error),
-		done:     make(chan struct{}),
-		doneResp: make(chan struct{}),
+		fd:          fd,
+		inotifyFile: os.NewFile(uintptr(fd), ""),
+		watches:     make(map[string]*watch),
+		paths:       make(map[int]string),
+		Events:      make(chan Event),
+		Errors:      make(chan error),
+		done:        make(chan struct{}),
+		doneResp:    make(chan struct{}),
 	}
 
 	go w.readEvents()
@@ -72,15 +69,21 @@ func (w *Watcher) isClosed() bool {
 
 // Close removes all watches and closes the events channel.
 func (w *Watcher) Close() error {
+	w.mu.Lock()
 	if w.isClosed() {
+		w.mu.Unlock()
 		return nil
 	}
 
 	// Send 'close' signal to goroutine, and set the Watcher to closed.
 	close(w.done)
+	w.mu.Unlock()
 
-	// Wake up goroutine
-	w.poller.wake()
+	// Causes any blocking reads to return with an error, provided the file still supports deadline operations
+	err := w.inotifyFile.Close()
+	if err != nil {
+		return err
+	}
 
 	// Wait for goroutine to close
 	<-w.doneResp
@@ -186,16 +189,12 @@ type watch struct {
 func (w *Watcher) readEvents() {
 	var (
 		buf   [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
-		n     int                                  // Number of bytes read with read()
 		errno error                                // Syscall errno
-		ok    bool                                 // For poller.wait
 	)
 
 	defer close(w.doneResp)
 	defer close(w.Errors)
 	defer close(w.Events)
-	defer unix.Close(w.fd)
-	defer w.poller.close()
 
 	for {
 		// See if we have been closed.
@@ -203,33 +202,19 @@ func (w *Watcher) readEvents() {
 			return
 		}
 
-		ok, errno = w.poller.wait()
-		if errno != nil {
+		n, err := w.inotifyFile.Read(buf[:])
+		switch {
+		case errors.Unwrap(err) == os.ErrClosed:
+			return
+		case err != nil:
 			select {
-			case w.Errors <- errno:
+			case w.Errors <- err:
 			case <-w.done:
 				return
 			}
 			continue
 		}
 
-		if !ok {
-			continue
-		}
-
-		n, errno = unix.Read(w.fd, buf[:])
-		// If a signal interrupted execution, see if we've been asked to close, and try again.
-		// http://man7.org/linux/man-pages/man7/signal.7.html :
-		// "Before Linux 3.8, reads from an inotify(7) file descriptor were not restartable"
-		if errno == unix.EINTR {
-			continue
-		}
-
-		// unix.Read might have been woken up by Close. If so, we're done.
-		if w.isClosed() {
-			return
-		}
-
 		if n < unix.SizeofInotifyEvent {
 			var err error
 			if n == 0 {
@@ -317,12 +302,12 @@ func (e *Event) ignoreLinux(mask uint32) bool {
 		return true
 	}
 
-	// If the event is not a DELETE or RENAME, the file must exist.
-	// Otherwise the event is ignored.
-	// *Note*: this was put in place because it was seen that a MODIFY
-	// event was sent after the DELETE. This ignores that MODIFY and
-	// assumes a DELETE will come or has come if the file doesn't exist.
-	if !(e.Op&Remove == Remove || e.Op&Rename == Rename) {
+	// If the event is Create or Write, the file must exist, or the
+	// event will be suppressed.
+	// *Note*: this was put in place because it was seen that a Write
+	// event was sent after the Remove. This ignores the Write and
+	// assumes a Remove will come or has come if the file doesn't exist.
+	if e.Op&Create == Create || e.Op&Write == Write {
 		_, statErr := os.Lstat(e.Name)
 		return os.IsNotExist(statErr)
 	}
diff --git a/inotify_poller.go b/inotify_poller.go
deleted file mode 100644
index b572a37..0000000
--- a/inotify_poller.go
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright 2015 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build linux
-// +build linux
-
-package fsnotify
-
-import (
-	"errors"
-
-	"golang.org/x/sys/unix"
-)
-
-type fdPoller struct {
-	fd   int    // File descriptor (as returned by the inotify_init() syscall)
-	epfd int    // Epoll file descriptor
-	pipe [2]int // Pipe for waking up
-}
-
-func emptyPoller(fd int) *fdPoller {
-	poller := new(fdPoller)
-	poller.fd = fd
-	poller.epfd = -1
-	poller.pipe[0] = -1
-	poller.pipe[1] = -1
-	return poller
-}
-
-// Create a new inotify poller.
-// This creates an inotify handler, and an epoll handler.
-func newFdPoller(fd int) (*fdPoller, error) {
-	var errno error
-	poller := emptyPoller(fd)
-	defer func() {
-		if errno != nil {
-			poller.close()
-		}
-	}()
-
-	// Create epoll fd
-	poller.epfd, errno = unix.EpollCreate1(unix.EPOLL_CLOEXEC)
-	if poller.epfd == -1 {
-		return nil, errno
-	}
-	// Create pipe; pipe[0] is the read end, pipe[1] the write end.
-	errno = unix.Pipe2(poller.pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC)
-	if errno != nil {
-		return nil, errno
-	}
-
-	// Register inotify fd with epoll
-	event := unix.EpollEvent{
-		Fd:     int32(poller.fd),
-		Events: unix.EPOLLIN,
-	}
-	errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.fd, &event)
-	if errno != nil {
-		return nil, errno
-	}
-
-	// Register pipe fd with epoll
-	event = unix.EpollEvent{
-		Fd:     int32(poller.pipe[0]),
-		Events: unix.EPOLLIN,
-	}
-	errno = unix.EpollCtl(poller.epfd, unix.EPOLL_CTL_ADD, poller.pipe[0], &event)
-	if errno != nil {
-		return nil, errno
-	}
-
-	return poller, nil
-}
-
-// Wait using epoll.
-// Returns true if something is ready to be read,
-// false if there is not.
-func (poller *fdPoller) wait() (bool, error) {
-	// 3 possible events per fd, and 2 fds, makes a maximum of 6 events.
-	// I don't know whether epoll_wait returns the number of events returned,
-	// or the total number of events ready.
-	// I decided to catch both by making the buffer one larger than the maximum.
-	events := make([]unix.EpollEvent, 7)
-	for {
-		n, errno := unix.EpollWait(poller.epfd, events, -1)
-		if n == -1 {
-			if errno == unix.EINTR {
-				continue
-			}
-			return false, errno
-		}
-		if n == 0 {
-			// If there are no events, try again.
-			continue
-		}
-		if n > 6 {
-			// This should never happen. More events were returned than should be possible.
-			return false, errors.New("epoll_wait returned more events than I know what to do with")
-		}
-		ready := events[:n]
-		epollhup := false
-		epollerr := false
-		epollin := false
-		for _, event := range ready {
-			if event.Fd == int32(poller.fd) {
-				if event.Events&unix.EPOLLHUP != 0 {
-					// This should not happen, but if it does, treat it as a wakeup.
-					epollhup = true
-				}
-				if event.Events&unix.EPOLLERR != 0 {
-					// If an error is waiting on the file descriptor, we should pretend
-					// something is ready to read, and let unix.Read pick up the error.
-					epollerr = true
-				}
-				if event.Events&unix.EPOLLIN != 0 {
-					// There is data to read.
-					epollin = true
-				}
-			}
-			if event.Fd == int32(poller.pipe[0]) {
-				if event.Events&unix.EPOLLHUP != 0 {
-					// Write pipe descriptor was closed, by us. This means we're closing down the
-					// watcher, and we should wake up.
-				}
-				if event.Events&unix.EPOLLERR != 0 {
-					// If an error is waiting on the pipe file descriptor.
-					// This is an absolute mystery, and should never ever happen.
-					return false, errors.New("Error on the pipe descriptor.")
-				}
-				if event.Events&unix.EPOLLIN != 0 {
-					// This is a regular wakeup, so we have to clear the buffer.
-					err := poller.clearWake()
-					if err != nil {
-						return false, err
-					}
-				}
-			}
-		}
-
-		if epollhup || epollerr || epollin {
-			return true, nil
-		}
-		return false, nil
-	}
-}
-
-// Close the write end of the poller.
-func (poller *fdPoller) wake() error {
-	buf := make([]byte, 1)
-	n, errno := unix.Write(poller.pipe[1], buf)
-	if n == -1 {
-		if errno == unix.EAGAIN {
-			// Buffer is full, poller will wake.
-			return nil
-		}
-		return errno
-	}
-	return nil
-}
-
-func (poller *fdPoller) clearWake() error {
-	// You have to be woken up a LOT in order to get to 100!
-	buf := make([]byte, 100)
-	n, errno := unix.Read(poller.pipe[0], buf)
-	if n == -1 {
-		if errno == unix.EAGAIN {
-			// Buffer is empty, someone else cleared our wake.
-			return nil
-		}
-		return errno
-	}
-	return nil
-}
-
-// Close all poller file descriptors, but not the one passed to it.
-func (poller *fdPoller) close() {
-	if poller.pipe[1] != -1 {
-		unix.Close(poller.pipe[1])
-	}
-	if poller.pipe[0] != -1 {
-		unix.Close(poller.pipe[0])
-	}
-	if poller.epfd != -1 {
-		unix.Close(poller.epfd)
-	}
-}
diff --git a/kqueue.go b/kqueue.go
index 87c25b0..5247f7d 100644
--- a/kqueue.go
+++ b/kqueue.go
@@ -237,7 +237,7 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
 			}
 		}
 
-		watchfd, err = unix.Open(name, openMode, 0700)
+		watchfd, err = unix.Open(name, openMode, 0)
 		if watchfd == -1 {
 			return "", err
 		}
@@ -429,7 +429,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
 		filePath := filepath.Join(dirPath, fileInfo.Name())
 		filePath, err = w.internalWatch(filePath, fileInfo)
 		if err != nil {
-			return err
+			return fmt.Errorf("%q: %w", filepath.Join(dirPath, fileInfo.Name()), err)
 		}
 
 		w.mu.Lock()


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 28666d2..d6fa966 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,63 +9,73 @@ jobs:
       matrix:
         os:
           - ubuntu-latest
-          - macos-latest
+          - macos-11
+          - macos-12
           - windows-latest
         go:
-          - '1.18'
-          - '1.17'
           - '1.16'
+          - '1.18'
     runs-on: ${{ matrix.os }}
     steps:
       - name: setup Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
-          stable: 'false'
           go-version: ${{ matrix.go }}
 
       - name: checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v3
 
       - name: test
         run: |
-          go test --race ./...
+          go test -race ./...
 
   testFreeBSD:
-    runs-on: macos-10.15
+    runs-on: macos-12
     name: test (freebsd, 1.18)
     steps:
-      - uses: actions/checkout@v2
+      - uses: actions/checkout@v3
       - name: test (freebsd, 1.18)
         id: test
-        uses: vmactions/freebsd-vm@v0.1.5
+        uses: vmactions/freebsd-vm@v0.2.0
         with:
           usesh: true
           prepare: pkg install -y go
           run: |
-            go test
+            go test -race ./...
 
-  lint:
-    runs-on: ubuntu-latest
+  testOpenBSD:
+    runs-on: macos-12
+    name: test (openbsd, 1.17)
     steps:
-      - name: setup Go
-        uses: actions/setup-go@v2
+      - uses: actions/checkout@v3
+      - name: test (openbsd, 1.17)
+        id: test
+        uses: vmactions/openbsd-vm@v0.0.6
         with:
-          go-version: '1.17'
-
-      - name: checkout
-        uses: actions/checkout@v2
-
-      - name: gofmt
-        run: |
-          test -z "$(gofmt -s -d . | tee /dev/stderr)"
+          prepare: pkg_add go
+          run: |
+            # Default of 512 leads to "too many open files".
+            ulimit -n 1024
 
-      - name: vet
-        run: |
-          go vet ./...
+            # No -race as the VM doesn't include the comp set.
+            #
+            # TODO: should probably add this, but on my local machine the tests
+            #       time out with -race as the waits aren't long enough (OpenBSD
+            #       is kind of slow), so should probably look into that first.
+            #       Go 1.19 is supposed to have a much faster race detector, so
+            #       maybe waiting until we have that is enough.
+            go test ./...
 
-      - name: golangci-lint
-        uses: golangci/golangci-lint-action@v2
-        continue-on-error: true
+  testNetBSD:
+    runs-on: macos-12
+    name: test (netbsd, 1.17)
+    steps:
+      - uses: actions/checkout@v3
+      - name: test (netbsd, 1.17)
+        id: test
+        uses: vmactions/netbsd-vm@v0.0.4
         with:
-          version: latest
-          skip-go-installation: true
+          prepare: pkg_add go
+          run: |
+            # TODO: no -race for the same reason as OpenBSD (the timing; it does run).
+            go117 test ./...
diff --git a/inotify_poller_test.go b/inotify_poller_test.go
deleted file mode 100644
index 110e00d..0000000
--- a/inotify_poller_test.go
+++ /dev/null
@@ -1,234 +0,0 @@
-// Copyright 2015 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-//go:build linux
-// +build linux
-
-package fsnotify
-
-import (
-	"testing"
-	"time"
-
-	"golang.org/x/sys/unix"
-)
-
-type testFd [2]int
-
-func makeTestFd(t *testing.T) testFd {
-	var tfd testFd
-	errno := unix.Pipe(tfd[:])
-	if errno != nil {
-		t.Fatalf("Failed to create pipe: %v", errno)
-	}
-	return tfd
-}
-
-func (tfd testFd) fd() int {
-	return tfd[0]
-}
-
-func (tfd testFd) closeWrite(t *testing.T) {
-	errno := unix.Close(tfd[1])
-	if errno != nil {
-		t.Fatalf("Failed to close write end of pipe: %v", errno)
-	}
-}
-
-func (tfd testFd) put(t *testing.T) {
-	buf := make([]byte, 10)
-	_, errno := unix.Write(tfd[1], buf)
-	if errno != nil {
-		t.Fatalf("Failed to write to pipe: %v", errno)
-	}
-}
-
-func (tfd testFd) get(t *testing.T) {
-	buf := make([]byte, 10)
-	_, errno := unix.Read(tfd[0], buf)
-	if errno != nil {
-		t.Fatalf("Failed to read from pipe: %v", errno)
-	}
-}
-
-func (tfd testFd) close() {
-	unix.Close(tfd[1])
-	unix.Close(tfd[0])
-}
-
-func makePoller(t *testing.T) (testFd, *fdPoller) {
-	tfd := makeTestFd(t)
-	poller, err := newFdPoller(tfd.fd())
-	if err != nil {
-		t.Fatalf("Failed to create poller: %v", err)
-	}
-	return tfd, poller
-}
-
-func TestPollerWithBadFd(t *testing.T) {
-	_, err := newFdPoller(-1)
-	if err != unix.EBADF {
-		t.Fatalf("Expected EBADF, got: %v", err)
-	}
-}
-
-func TestPollerWithData(t *testing.T) {
-	tfd, poller := makePoller(t)
-	defer tfd.close()
-	defer poller.close()
-
-	tfd.put(t)
-	ok, err := poller.wait()
-	if err != nil {
-		t.Fatalf("poller failed: %v", err)
-	}
-	if !ok {
-		t.Fatalf("expected poller to return true")
-	}
-	tfd.get(t)
-}
-
-func TestPollerWithWakeup(t *testing.T) {
-	tfd, poller := makePoller(t)
-	defer tfd.close()
-	defer poller.close()
-
-	err := poller.wake()
-	if err != nil {
-		t.Fatalf("wake failed: %v", err)
-	}
-	ok, err := poller.wait()
-	if err != nil {
-		t.Fatalf("poller failed: %v", err)
-	}
-	if ok {
-		t.Fatalf("expected poller to return false")
-	}
-}
-
-func TestPollerWithClose(t *testing.T) {
-	tfd, poller := makePoller(t)
-	defer tfd.close()
-	defer poller.close()
-
-	tfd.closeWrite(t)
-	ok, err := poller.wait()
-	if err != nil {
-		t.Fatalf("poller failed: %v", err)
-	}
-	if !ok {
-		t.Fatalf("expected poller to return true")
-	}
-}
-
-func TestPollerWithWakeupAndData(t *testing.T) {
-	tfd, poller := makePoller(t)
-	defer tfd.close()
-	defer poller.close()
-
-	tfd.put(t)
-	err := poller.wake()
-	if err != nil {
-		t.Fatalf("wake failed: %v", err)
-	}
-
-	// both data and wakeup
-	ok, err := poller.wait()
-	if err != nil {
-		t.Fatalf("poller failed: %v", err)
-	}
-	if !ok {
-		t.Fatalf("expected poller to return true")
-	}
-
-	// data is still in the buffer, wakeup is cleared
-	ok, err = poller.wait()
-	if err != nil {
-		t.Fatalf("poller failed: %v", err)
-	}
-	if !ok {
-		t.Fatalf("expected poller to return true")
-	}
-
-	tfd.get(t)
-	// data is gone, only wakeup now
-	err = poller.wake()
-	if err != nil {
-		t.Fatalf("wake failed: %v", err)
-	}
-	ok, err = poller.wait()
-	if err != nil {
-		t.Fatalf("poller failed: %v", err)
-	}
-	if ok {
-		t.Fatalf("expected poller to return false")
-	}
-}
-
-func TestPollerConcurrent(t *testing.T) {
-	tfd, poller := makePoller(t)
-	defer tfd.close()
-	defer poller.close()
-
-	oks := make(chan bool)
-	live := make(chan bool)
-	defer close(live)
-	go func() {
-		defer close(oks)
-		for {
-			ok, err := poller.wait()
-			if err != nil {
-				t.Errorf("poller failed: %v", err)
-			}
-			oks <- ok
-			if !<-live {
-				return
-			}
-		}
-	}()
-
-	// Try a write
-	select {
-	case <-time.After(50 * time.Millisecond):
-	case <-oks:
-		t.Fatalf("poller did not wait")
-	}
-	tfd.put(t)
-	if !<-oks {
-		t.Fatalf("expected true")
-	}
-	tfd.get(t)
-	live <- true
-
-	// Try a wakeup
-	select {
-	case <-time.After(50 * time.Millisecond):
-	case <-oks:
-		t.Fatalf("poller did not wait")
-	}
-	err := poller.wake()
-	if err != nil {
-		t.Fatalf("wake failed: %v", err)
-	}
-	if <-oks {
-		t.Fatalf("expected false")
-	}
-	live <- true
-
-	// Try a close
-	select {
-	case <-time.After(50 * time.Millisecond):
-	case <-oks:
-		t.Fatalf("poller did not wait")
-	}
-	tfd.closeWrite(t)
-	if !<-oks {
-		t.Fatalf("expected true")
-	}
-	tfd.get(t)
-
-	// wait for all goroutines for finish.
-	live <- false
-	<-oks
-}
diff --git a/inotify_test.go b/inotify_test.go
index 728f5c2..d9ee53f 100644
--- a/inotify_test.go
+++ b/inotify_test.go
@@ -354,14 +354,16 @@ func TestInotifyInnerMapLength(t *testing.T) {
 	_ = <-w.Events                      // consume Remove event
 	<-time.After(50 * time.Millisecond) // wait IN_IGNORE propagated
 
-	w.mu.Lock()
-	defer w.mu.Unlock()
-	if len(w.watches) != 0 {
-		t.Fatalf("Expected watches len is 0, but got: %d, %v", len(w.watches), w.watches)
-	}
-	if len(w.paths) != 0 {
-		t.Fatalf("Expected paths len is 0, but got: %d, %v", len(w.paths), w.paths)
-	}
+	func() {
+		w.mu.Lock()
+		defer w.mu.Unlock()
+		if len(w.watches) != 0 {
+			t.Fatalf("Expected watches len is 0, but got: %d, %v", len(w.watches), w.watches)
+		}
+		if len(w.paths) != 0 {
+			t.Fatalf("Expected paths len is 0, but got: %d, %v", len(w.paths), w.paths)
+		}
+	}()
 
 	w.Close()
 	wg.Wait()
@@ -499,3 +501,84 @@ func TestInotifyWatchList(t *testing.T) {
 		}
 	}
 }
+
+func TestInotifyDeleteOpenedFile(t *testing.T) {
+	testDir := tempMkdir(t)
+	defer os.RemoveAll(testDir)
+
+	testFile := filepath.Join(testDir, "testfile")
+
+	// create and open a file
+	fd, err := os.Create(testFile)
+	if err != nil {
+		t.Fatalf("Create failed: %v", err)
+	}
+	defer fd.Close()
+
+	w, err := NewWatcher()
+	if err != nil {
+		t.Fatalf("Failed to create watcher: %v", err)
+	}
+	defer w.Close()
+
+	err = w.Add(testFile)
+	if err != nil {
+		t.Fatalf("Failed to add watch for %s: %v", testFile, err)
+	}
+
+	checkEvent := func(exp Op) {
+		select {
+		case event := <-w.Events:
+			t.Logf("Event received: %s", event.Op)
+			if event.Op != exp {
+				t.Fatalf("Event expected: %s, got: %s", exp, event.Op)
+			}
+		case <-time.After(100 * time.Millisecond):
+			t.Fatalf("Expected %s event not received", exp)
+		}
+	}
+
+	// Remove the (opened) file, check Chmod event (notifying
+	// about file link count change) is received
+	err = os.Remove(testFile)
+	if err != nil {
+		t.Fatalf("Failed to remove file: %s", err)
+	}
+	checkEvent(Chmod)
+
+	// Close the file, check Remove event is received
+	fd.Close()
+	checkEvent(Remove)
+}
+
+func TestINotifyNoBlockingSyscalls(t *testing.T) {
+	getThreads := func() int {
+		d := fmt.Sprintf("/proc/%d/task", os.Getpid())
+		ls, err := os.ReadDir(d)
+		if err != nil {
+			t.Fatalf("reading %q: %s", d, err)
+		}
+		return len(ls)
+	}
+
+	w, err := NewWatcher()
+	if err != nil {
+		t.Fatalf("Failed to create watcher: %v", err)
+	}
+
+	startingThreads := getThreads()
+	// Call readEvents a bunch of times; if this function has a blocking raw syscall, it'll create many new kthreads
+	for i := 0; i <= 60; i++ {
+		go w.readEvents()
+	}
+
+	// Bad synchronization mechanism
+	time.Sleep(time.Second * 2)
+
+	endingThreads := getThreads()
+
+	// Did we spawn any new threads?
+	if diff := endingThreads - startingThreads; diff > 0 {
+		t.Fatalf("Got a nonzero diff %v. starting: %v. ending: %v", diff, startingThreads, endingThreads)
+	}
+}
diff --git a/integration_test.go b/integration_test.go
index 9dace27..c4e1d3e 100644
--- a/integration_test.go
+++ b/integration_test.go
@@ -14,6 +14,7 @@ import (
 	"path"
 	"path/filepath"
 	"runtime"
+	"strings"
 	"sync"
 	"sync/atomic"
 	"testing"
@@ -78,6 +79,10 @@ func addWatch(t *testing.T, watcher *Watcher, dir string) {
 }
 
 func TestFsnotifyMultipleOperations(t *testing.T) {
+	if runtime.GOOS == "netbsd" {
+		t.Skip("NetBSD behaviour is not fully correct") // TODO: investigate and fix.
+	}
+
 	watcher := newWatcher(t)
 
 	// Receive errors on the error channel on a separate goroutine
@@ -1244,6 +1249,25 @@ func TestRemoveWithClose(t *testing.T) {
 	}
 }
 
+// Make sure Close() doesn't race; hard to write a good reproducible test for
+// this, but running it 150 times seems to reproduce it in ~75% of cases and
+// isn't too slow (~0.06s on my system).
+func TestCloseRace(t *testing.T) {
+	for i := 0; i < 150; i++ {
+		w, err := NewWatcher()
+		if err != nil {
+			if strings.Contains(err.Error(), "too many") { // syscall.EMFILE
+				time.Sleep(100 * time.Millisecond)
+				continue
+			}
+			t.Fatal(err)
+		}
+		go w.Close()
+		go w.Close()
+		go w.Close()
+	}
+}
+
 func testRename(file1, file2 string) error {
 	switch runtime.GOOS {
 	case "windows", "plan9":