diff --git a/internal/lsp/watcher/global_watcher.go b/internal/lsp/watcher/global_watcher.go index 0e60027daee654483f8fd7fb54a76587455ba5cf..7045cb04837b2fc33b0696c089f12adc220db587 100644 --- a/internal/lsp/watcher/global_watcher.go +++ b/internal/lsp/watcher/global_watcher.go @@ -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) +} diff --git a/internal/lsp/watcher/rlimit_stub.go b/internal/lsp/watcher/rlimit_stub.go new file mode 100644 index 0000000000000000000000000000000000000000..965e6bba89c82ca35331dcd1588e27da7aac29e7 --- /dev/null +++ b/internal/lsp/watcher/rlimit_stub.go @@ -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 +} \ No newline at end of file diff --git a/internal/lsp/watcher/rlimit_unix.go b/internal/lsp/watcher/rlimit_unix.go new file mode 100644 index 0000000000000000000000000000000000000000..29f99c4fdba2870ea5e8f8e68273e99c4e98e3de --- /dev/null +++ b/internal/lsp/watcher/rlimit_unix.go @@ -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 +}