Repository: fsnotify/fsnotify
Pull request #695 (2025-05-15 04:38:39)

Use Go 1.19, add ignoringEINTR helper
We can use generic for our helper and remove the rlimit stuff (as that's done automatically since Go 1.19).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6468d2c..b854c45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+Unreleased
+----------
+This version of fsnotify needs Go 1.19.
+
+
 1.9.0 2024-04-04
 ----------------
 
diff --git a/backend_fen.go b/backend_fen.go
index 57fc692..e43c6d0 100644
--- a/backend_fen.go
+++ b/backend_fen.go
@@ -158,7 +158,9 @@ func (w *fen) readEvents() {
 
 	pevents := make([]unix.PortEvent, 8)
 	for {
-		count, err := w.port.Get(pevents, 1, nil)
+		count, err := internal.IgnoringEINTR(func() (int, error) {
+			return w.port.Get(pevents, 1, nil)
+		})
 		if err != nil && err != unix.ETIME {
 			// Interrupted system call (count should be 0) ignore and continue
 			if errors.Is(err, unix.EINTR) && count == 0 {
diff --git a/backend_kqueue.go b/backend_kqueue.go
index d411dc0..4fc6dc5 100644
--- a/backend_kqueue.go
+++ b/backend_kqueue.go
@@ -377,19 +377,12 @@ func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, erro
 			}
 		}
 
-		// 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()
 	}
 
@@ -437,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
 			}
diff --git a/cmd/fsnotify/main.go b/cmd/fsnotify/main.go
index cdd9de7..5dba7b6 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 f64be4b..dc5364c 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
 )
 
diff --git a/go.mod b/go.mod
index fb5963e..44bf279 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/internal/darwin.go b/internal/darwin.go
index 0b01bc1..6721aa6 100644
--- a/internal/darwin.go
+++ b/internal/darwin.go
@@ -15,25 +15,6 @@ var (
 
 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 = 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/freebsd.go b/internal/freebsd.go
index 5ac8b50..758a249 100644
--- a/internal/freebsd.go
+++ b/internal/freebsd.go
@@ -15,17 +15,6 @@ var (
 
 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 b251fb8..9c66f5d 100644
--- a/internal/unix.go
+++ b/internal/unix.go
@@ -15,17 +15,6 @@ var (
 
 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 37dfedd..b2d8959 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 896bc2e..e24d569 100644
--- a/internal/windows.go
+++ b/internal/windows.go
@@ -14,7 +14,6 @@ var (
 	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") }


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fecaf54..5b4be86 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,8 +10,8 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        os: ['ubuntu-20.04', 'ubuntu-latest', 'ubuntu-24.04-arm']
-        go: ['1.17', '1.24']
+        os: ['ubuntu-22.04', 'ubuntu-latest', 'ubuntu-24.04-arm']
+        go: ['1.19', '1.24']
     runs-on: '${{ matrix.os }}'
     timeout-minutes: 10
     steps:
@@ -20,6 +20,7 @@ jobs:
         with: {go-version: '${{ matrix.go }}'}
       - name: test
         run: |
+          go version
           FSNOTIFY_BUFFER=4096 go test -parallel 1 -race    ./...
                                go test -parallel 1 -race    ./...
           FSNOTIFY_DEBUG=1     go test -parallel 1 -race -v ./...
@@ -29,7 +30,7 @@ jobs:
       fail-fast: false
       matrix:
         os: ['windows-latest']
-        go: ['1.17', '1.24']
+        go: ['1.19', '1.24']
     runs-on: '${{ matrix.os }}'
     timeout-minutes: 10
     steps:
@@ -38,6 +39,7 @@ jobs:
         with: {go-version: '${{ matrix.go }}'}
       - name: test
         run: |
+          go version
           go test -parallel 1 -race    ./...
 
           $Env:FSNOTIFY_BUFFER = 4096
@@ -46,27 +48,13 @@ jobs:
           $Env:FSNOTIFY_DEBUG = 1
           go test -parallel 1 -race -v ./...
 
-  # Test gccgo
-  gcc:
-    runs-on: 'ubuntu-24.04'
-    name:    'test (ubuntu-24.04, gccgo 13.2)'
-    timeout-minutes: 10
-    steps:
-      - uses: 'actions/checkout@v4'
-      - name: test
-        run: |
-          sudo apt-get -y install gccgo-13
-          FSNOTIFY_BUFFER=4096 go-13 test -parallel 1    ./...
-                               go-13 test -parallel 1    ./...
-          FSNOTIFY_DEBUG=1     go-13 test -parallel 1 -v ./...
-
   macos:
     name: 'test'
     strategy:
       fail-fast: false
       matrix:
         os: ['macos-13', 'macos-latest']
-        go: ['1.17', '1.24']
+        go: ['1.19', '1.24']
     runs-on: '${{ matrix.os }}'
     timeout-minutes: 10
     steps:
@@ -75,6 +63,7 @@ jobs:
         with: {go-version: '${{ matrix.go }}'}
       - name: test
         run: |
+          go version
           FSNOTIFY_BUFFER=4096 go test -parallel 1 -race    ./...
                                go test -parallel 1 -race    ./...
           FSNOTIFY_DEBUG=1     go test -parallel 1 -race -v ./...
@@ -88,17 +77,18 @@ jobs:
   #       enough.
   openbsd:
     runs-on: 'ubuntu-latest'
-    name:    'test (openbsd, 1.17)'
+    name:    'test (openbsd, 1.24)'
     timeout-minutes: 10
     steps:
       - uses: 'actions/checkout@v4'
-      - name: 'test (openbsd, 1.17)'
+      - name: 'test (openbsd, 1.24)'
         id:   'openbsd'
         uses: 'vmactions/openbsd-vm@v1'
         with:
           prepare: pkg_add go
           run: |
             useradd -mG wheel action
+            su action -c 'go version'
             FSNOTIFY_BUFFER=4096 su action -c 'go test -parallel 1    ./...'
                                  su action -c 'go test -parallel 1    ./...'
             FSNOTIFY_DEBUG=1     su action -c 'go test -parallel 1 -v ./...'
@@ -118,6 +108,7 @@ jobs:
           # TODO: no -race for the same reason as OpenBSD (the timing; it does run).
           run: |
             useradd -mG wheel action
+            su action -c '/usr/pkg/bin/go??? version'
             FSNOTIFY_BUFFER=4096 su action -c '/usr/pkg/bin/go??? test -parallel 1    ./...'
                                  su action -c '/usr/pkg/bin/go??? test -parallel 1    ./...'
             FSNOTIFY_DEBUG=1     su action -c '/usr/pkg/bin/go??? test -parallel 1 -v ./...'
@@ -125,11 +116,11 @@ jobs:
   # DragonFlyBSD
   dragonflybsd:
     runs-on: 'ubuntu-latest'
-    name:    'test (dragonflybsd, 1.20)'
+    name:    'test (dragonflybsd, 1.21)'
     timeout-minutes: 10
     steps:
       - uses: 'actions/checkout@v4'
-      - name: 'test (dragonflybsd, 1.20)'
+      - name: 'test (dragonflybsd, 1.21)'
         id:   'dragonflybsd'
         uses: 'vmactions/dragonflybsd-vm@v1'
         with:
@@ -137,6 +128,7 @@ jobs:
           # TODO: no -race for the same reason as OpenBSD (the timing; it does run).
           run: |
             pw user add -mG wheel -n action
+            su action -c 'go version'
             env FSNOTIFY_BUFFER=4096 su action -c 'go test -parallel 1    ./...'
                                      su action -c 'go test -parallel 1    ./...'
             env FSNOTIFY_DEBUG=1     su action -c 'go test -parallel 1 -v ./...'
@@ -157,29 +149,7 @@ jobs:
           useradd action
           export GOCACHE=/tmp/go-cache
           export GOPATH=/tmp/go-path
+          su action -c 'go version'
           FSNOTIFY_BUFFER=4096 su action -c '/opt/ooce/go-1.22/bin/go test -parallel 1    ./...'
                                su action -c '/opt/ooce/go-1.22/bin/go test -parallel 1    ./...'
           FSNOTIFY_DEBUG=1     su action -c '/opt/ooce/go-1.22/bin/go test -parallel 1 -v ./...'
-
-
-  # Solaris
-  # TODO: latest version is go 1.7(!) Need to find a good way to install a more
-  # recent version; the go.dev doesn't have binaries for Solaris.
-  # solaris:
-  #   runs-on: 'ubuntu-latest'
-  #   name: 'test (solaris, 1.17)'
-  #   timeout-minutes: 10
-  #   steps:
-  #   - uses: 'actions/checkout@v4'
-  #   - name: 'test (solaris, 1.17)'
-  #     id:   'solaris'
-  #     uses: 'vmactions/solaris-vm@v1'
-  #     with:
-  #       prepare: pkg install pkg://solaris/developer/golang-17
-  #       run: |
-  #         useradd action
-  #         export GOCACHE=/tmp/go-cache
-  #         export GOPATH=/tmp/go-path
-  #         FSNOTIFY_BUFFER=4096 su action -c 'go test -parallel 1    ./...'
-  #                              su action -c 'go test -parallel 1    ./...'
-  #         FSNOTIFY_DEBUG=1     su action -c 'go test -parallel 1 -v ./...'
diff --git a/fsnotify_test.go b/fsnotify_test.go
index 89c4ec0..11d88b4 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
 }
 
diff --git a/internal/ztest/diff.go b/internal/ztest/diff.go
index f635d6a..e9d09af 100644
--- a/internal/ztest/diff.go
+++ b/internal/ztest/diff.go
@@ -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 0b1f0c6..b9b5ea4 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)
 	}