diff --git a/etc/EGLOT-NEWS b/etc/EGLOT-NEWS index ffe45baad0a..f050e0bc294 100644 --- a/etc/EGLOT-NEWS +++ b/etc/EGLOT-NEWS @@ -20,6 +20,13 @@ https://github.com/joaotavora/eglot/issues/1234. * Changes to upcoming Eglot +** File watch limits to prevent resource exhaustion (github#1568) + +The new variable 'eglot-max-file-watches' limits the number of file +watches that can be created. Some language servers request watching +for a very large number of directories (e.g. Python virtualenvs), which +can exhaust system resources and cause slow startup. + ** Support for complex workspace edits (create/rename/delete files) Eglot now advertises support for file resource operations in workspace diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index dc30a4e1d34..6c10f9a5512 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -4680,11 +4680,28 @@ at point. With prefix argument, prompt for ACTION-KIND." ;;; File watchers (aka didChangeWatchedFiles) ;;; (defvar eglot-watch-files-outside-project-root t - "If non-nil, allow watching files outside project root") + "If non-nil, allow watching files outside project root.") + +(defvar eglot-max-file-watches 10000 + "Maximum number of file watches across all Eglot servers. +If this limit is reached, a warning is issued and further watches +are not added. Set to nil for unlimited watches.") + +(defun eglot--count-file-watches () + "Count total file watches across all Eglot servers." + (let ((count 0)) + (maphash (lambda (_proj servers) + (dolist (server servers) + (maphash (lambda (_id descs) + (cl-incf count (length descs))) + (eglot--file-watches server)))) + eglot--servers-by-project) + count)) (cl-defun eglot--watch-globs (server id globs dir in-root &aux (project (eglot--project server)) - success) + success + (watch-count (eglot--count-file-watches))) "Set up file watching for relative file names matching GLOBS under DIR. GLOBS is a list of (COMPILED-GLOB . KIND) pairs, where COMPILED-GLOB is a compiled glob predicate and KIND is a bitmask of change types. DIR is @@ -4727,9 +4744,18 @@ happens to be inside or matching the project root." (handle-event `(,desc deleted ,file)) (handle-event `(,desc created ,file1)))))) (add-watch (subdir) - (when (file-readable-p subdir) - (push (file-notify-add-watch subdir '(change) #'handle-event) - (gethash id (eglot--file-watches server)))))) + (cond ((not (file-readable-p subdir))) + ((and eglot-max-file-watches + (>= watch-count eglot-max-file-watches)) + (eglot--warn "Reached `eglot-max-file-watches' limit of %d, \ +not watching some directories" eglot-max-file-watches) + ;; Could `(setq success t)' here to keep partial watches. + (jsonrpc-error "Reached `eglot-max-file-watches' limit of %d" + eglot-max-file-watches)) + (t + (push (file-notify-add-watch subdir '(change) #'handle-event) + (gethash id (eglot--file-watches server))) + (cl-incf watch-count))))) (let ((subdirs (if (or (null dir) in-root) (subdirs-using-project) (condition-case _ (subdirs-using-find)