@@ -2,12 +2,14 @@ package watcher
import (
"context"
+ "errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"sync"
"sync/atomic"
+ "syscall"
"time"
"github.com/charmbracelet/crush/internal/config"
@@ -113,8 +115,24 @@ func Start() error {
events := notify.Create | notify.Write | notify.Remove | notify.Rename
if err := notify.Watch(watchPath, gw.events, events); err != nil {
+ // Check if the error might be due to file descriptor limits
+ if isFileLimitError(err) {
+ slog.Warn("lsp watcher: Hit file descriptor limit, attempting to increase", "error", err)
+ if newLimit, rlimitErr := MaximizeOpenFileLimit(); rlimitErr == nil {
+ slog.Info("lsp watcher: Increased file descriptor limit", "limit", newLimit)
+ // Retry the watch operation
+ if err = notify.Watch(watchPath, gw.events, events); err == nil {
+ slog.Info("lsp watcher: Successfully set up watch after increasing limit")
+ goto watchSuccess
+ }
+ err = fmt.Errorf("still failed after increasing limit: %w", err)
+ } else {
+ slog.Warn("lsp watcher: Failed to increase file descriptor limit", "error", rlimitErr)
+ }
+ }
return fmt.Errorf("lsp watcher: error setting up recursive watch on %s: %w", root, err)
}
+watchSuccess:
slog.Info("lsp watcher: Started recursive watching", "root", root)
return nil
@@ -335,3 +353,12 @@ func (gw *global) shutdown() {
func Shutdown() {
instance().shutdown()
}
+
+// isFileLimitError checks if an error is related to file descriptor limits
+func isFileLimitError(err error) bool {
+ if err == nil {
+ return false
+ }
+ // Check for common file limit errors
+ return errors.Is(err, syscall.EMFILE) || errors.Is(err, syscall.ENFILE)
+}
@@ -0,0 +1,12 @@
+//go:build !unix
+
+package watcher
+
+// MaximizeOpenFileLimit is a no-op on non-Unix systems.
+// Returns a high value to indicate no practical limit.
+func MaximizeOpenFileLimit() (int, error) {
+ // Windows and other non-Unix systems don't have file descriptor limits
+ // in the same way Unix systems do. Return a high value to indicate
+ // there's no practical limit to worry about.
+ return 1<<20, nil // 1M, effectively unlimited
+}
@@ -0,0 +1,57 @@
+//go:build unix
+
+// This file contains code inspired by Syncthing's rlimit implementation
+// Syncthing is licensed under the Mozilla Public License Version 2.0
+// See: https://github.com/syncthing/syncthing/blob/main/LICENSE
+
+package watcher
+
+import (
+ "runtime"
+ "syscall"
+)
+
+const (
+ // macOS has a specific limit for RLIMIT_NOFILE
+ darwinOpenMax = 10240
+)
+
+// MaximizeOpenFileLimit tries to set the resource limit RLIMIT_NOFILE (number
+// of open file descriptors) to the max (hard limit), if the current (soft
+// limit) is below the max. Returns the new (though possibly unchanged) limit,
+// or an error if it could not be changed.
+func MaximizeOpenFileLimit() (int, error) {
+ // Get the current limit on number of open files.
+ var lim syscall.Rlimit
+ if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
+ return 0, err
+ }
+
+ // If we're already at max, there's no need to try to raise the limit.
+ if lim.Cur >= lim.Max {
+ return int(lim.Cur), nil
+ }
+
+ // macOS doesn't like a soft limit greater than OPEN_MAX
+ if runtime.GOOS == "darwin" && lim.Max > darwinOpenMax {
+ lim.Max = darwinOpenMax
+ }
+
+ // Try to increase the limit to the max.
+ oldLimit := lim.Cur
+ lim.Cur = lim.Max
+ if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
+ return int(oldLimit), err
+ }
+
+ // If the set succeeded, perform a new get to see what happened. We might
+ // have gotten a value lower than the one in lim.Max, if lim.Max was
+ // something that indicated "unlimited" (i.e. intmax).
+ if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim); err != nil {
+ // We don't really know the correct value here since Getrlimit
+ // mysteriously failed after working once... Shouldn't ever happen.
+ return 0, err
+ }
+
+ return int(lim.Cur), nil
+}