feat(lsp): drop fswatch, use inotifywait (#29374)

This patch replaces fswatch with inotifywait from inotify-toools:

https://github.com/inotify-tools/inotify-tools

fswatch takes ~1min to set up recursively for the Samba source code
directory. inotifywait needs less than a second to do the same thing.

https://github.com/emcrisostomo/fswatch/issues/321

Also it fswatch seems to be unmaintained in the meantime.

Signed-off-by: Andreas Schneider <asn@cryptomilk.org>
This commit is contained in:
Andreas Schneider 2024-07-06 11:44:19 +02:00 committed by GitHub
parent 91e5dcae3d
commit 55e4301036
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 54 additions and 53 deletions

View File

@ -30,7 +30,7 @@ if [[ $os == Linux ]]; then
fi fi
if [[ -n $TEST ]]; then if [[ -n $TEST ]]; then
sudo apt-get install -y locales-all cpanminus attr libattr1-dev gdb fswatch sudo apt-get install -y locales-all cpanminus attr libattr1-dev gdb inotify-tools
# Use default CC to avoid compilation problems when installing Python modules # Use default CC to avoid compilation problems when installing Python modules
CC=cc python3 -m pip -q install --user --upgrade pynvim CC=cc python3 -m pip -q install --user --upgrade pynvim

View File

@ -543,16 +543,19 @@ Example: File-change detection *watch-file*
vim.api.nvim_command( vim.api.nvim_command(
"command! -nargs=1 Watch call luaeval('watch_file(_A)', expand('<args>'))") "command! -nargs=1 Watch call luaeval('watch_file(_A)', expand('<args>'))")
< <
*fswatch-limitations* *inotify-limitations*
When on Linux and using fswatch, you may need to increase the maximum number When on Linux you may need to increase the maximum number of `inotify` watches
of `inotify` watches and queued events as the default limit can be too low. To and queued events as the default limit can be too low. To increase the limit,
increase the limit, run: >sh run: >sh
sysctl fs.inotify.max_user_watches=100000 sysctl fs.inotify.max_user_watches=494462
sysctl fs.inotify.max_queued_events=100000
< <
This will increase the limit to 100000 watches and queued events. These lines This will increase the limit to 494462 watches and queued events. These lines
can be added to `/etc/sysctl.conf` to make the changes persistent. can be added to `/etc/sysctl.conf` to make the changes persistent.
Note that each watch is a structure in the Kernel, thus available memory is
also a bottleneck for using inotify. In fact, a watch can take up to 1KB of
space. This means a million watches could result in 1GB of extra RAM usage.
Example: TCP echo-server *tcp-server* Example: TCP echo-server *tcp-server*
1. Save this code to a file. 1. Save this code to a file.
2. Execute it with ":luafile %". 2. Execute it with ":luafile %".

View File

@ -227,11 +227,12 @@ end
--- @param data string --- @param data string
--- @param opts vim._watch.Opts? --- @param opts vim._watch.Opts?
--- @param callback vim._watch.Callback --- @param callback vim._watch.Callback
local function fswatch_output_handler(data, opts, callback) local function on_inotifywait_output(data, opts, callback)
local d = vim.split(data, '%s+') local d = vim.split(data, '%s+')
-- only consider the last reported event -- only consider the last reported event
local fullpath, event = d[1], d[#d] local path, event, file = d[1], d[2], d[#d]
local fullpath = vim.fs.joinpath(path, file)
if skip(fullpath, opts) then if skip(fullpath, opts) then
return return
@ -240,20 +241,16 @@ local function fswatch_output_handler(data, opts, callback)
--- @type integer --- @type integer
local change_type local change_type
if event == 'Created' then if event == 'CREATE' then
change_type = M.FileChangeType.Created change_type = M.FileChangeType.Created
elseif event == 'Removed' then elseif event == 'DELETE' then
change_type = M.FileChangeType.Deleted change_type = M.FileChangeType.Deleted
elseif event == 'Updated' then elseif event == 'MODIFY' then
change_type = M.FileChangeType.Changed change_type = M.FileChangeType.Changed
elseif event == 'Renamed' then elseif event == 'MOVED_FROM' then
local _, staterr, staterrname = uv.fs_stat(fullpath) change_type = M.FileChangeType.Deleted
if staterrname == 'ENOENT' then elseif event == 'MOVED_TO' then
change_type = M.FileChangeType.Deleted change_type = M.FileChangeType.Created
else
assert(not staterr, staterr)
change_type = M.FileChangeType.Created
end
end end
if change_type then if change_type then
@ -265,24 +262,22 @@ end
--- @param opts vim._watch.Opts? --- @param opts vim._watch.Opts?
--- @param callback vim._watch.Callback Callback for new events --- @param callback vim._watch.Callback Callback for new events
--- @return fun() cancel Stops the watcher --- @return fun() cancel Stops the watcher
function M.fswatch(path, opts, callback) function M.inotify(path, opts, callback)
-- debounce isn't the same as latency but close enough
local latency = 0.5 -- seconds
if opts and opts.debounce then
latency = opts.debounce / 1000
end
local obj = vim.system({ local obj = vim.system({
'fswatch', 'inotifywait',
'--event=Created', '--quiet', -- suppress startup messages
'--event=Removed', '--no-dereference', -- don't follow symlinks
'--event=Updated', '--monitor', -- keep listening for events forever
'--event=Renamed',
'--event-flags',
'--recursive', '--recursive',
'--latency=' .. tostring(latency), '--event',
'--exclude', 'create',
'/.git/', '--event',
'delete',
'--event',
'modify',
'--event',
'move',
'@.git', -- ignore git directory
path, path,
}, { }, {
stderr = function(err, data) stderr = function(err, data)
@ -292,11 +287,11 @@ function M.fswatch(path, opts, callback)
if data and #vim.trim(data) > 0 then if data and #vim.trim(data) > 0 then
vim.schedule(function() vim.schedule(function()
if vim.fn.has('linux') == 1 and vim.startswith(data, 'Event queue overflow') then if vim.fn.has('linux') == 1 and vim.startswith(data, 'Failed to watch') then
data = 'inotify(7) limit reached, see :h fswatch-limitations for more info.' data = 'inotify(7) limit reached, see :h inotify-limitations for more info.'
end end
vim.notify('fswatch: ' .. data, vim.log.levels.ERROR) vim.notify('inotify: ' .. data, vim.log.levels.ERROR)
end) end)
end end
end, end,
@ -306,7 +301,7 @@ function M.fswatch(path, opts, callback)
end end
for line in vim.gsplit(data or '', '\n', { plain = true, trimempty = true }) do for line in vim.gsplit(data or '', '\n', { plain = true, trimempty = true }) do
fswatch_output_handler(line, opts, callback) on_inotifywait_output(line, opts, callback)
end end
end, end,
-- --latency is locale dependent but tostring() isn't and will always have '.' as decimal point. -- --latency is locale dependent but tostring() isn't and will always have '.' as decimal point.

View File

@ -9,8 +9,8 @@ local M = {}
if vim.fn.has('win32') == 1 or vim.fn.has('mac') == 1 then if vim.fn.has('win32') == 1 or vim.fn.has('mac') == 1 then
M._watchfunc = watch.watch M._watchfunc = watch.watch
elseif vim.fn.executable('fswatch') == 1 then elseif vim.fn.executable('inotifywait') == 1 then
M._watchfunc = watch.fswatch M._watchfunc = watch.inotify
else else
M._watchfunc = watch.watchdirs M._watchfunc = watch.watchdirs
end end

View File

@ -90,8 +90,8 @@ local function check_watcher()
watchfunc_name = 'libuv-watch' watchfunc_name = 'libuv-watch'
elseif watchfunc == vim._watch.watchdirs then elseif watchfunc == vim._watch.watchdirs then
watchfunc_name = 'libuv-watchdirs' watchfunc_name = 'libuv-watchdirs'
elseif watchfunc == vim._watch.fswatch then elseif watchfunc == vim._watch.inotifywait then
watchfunc_name = 'fswatch' watchfunc_name = 'inotifywait'
else else
local nm = debug.getinfo(watchfunc, 'S').source local nm = debug.getinfo(watchfunc, 'S').source
watchfunc_name = string.format('Custom (%s)', nm) watchfunc_name = string.format('Custom (%s)', nm)
@ -99,7 +99,7 @@ local function check_watcher()
report_info('File watch backend: ' .. watchfunc_name) report_info('File watch backend: ' .. watchfunc_name)
if watchfunc_name == 'libuv-watchdirs' then if watchfunc_name == 'libuv-watchdirs' then
report_warn('libuv-watchdirs has known performance issues. Consider installing fswatch.') report_warn('libuv-watchdirs has known performance issues. Consider installing inotify-tools.')
end end
end end

View File

@ -23,10 +23,13 @@ describe('vim._watch', function()
local function run(watchfunc) local function run(watchfunc)
it('detects file changes (watchfunc=' .. watchfunc .. '())', function() it('detects file changes (watchfunc=' .. watchfunc .. '())', function()
if watchfunc == 'fswatch' then if watchfunc == 'inotify' then
skip(is_os('win'), 'not supported on windows') skip(is_os('win'), 'not supported on windows')
skip(is_os('mac'), 'flaky test on mac') skip(is_os('mac'), 'flaky test on mac')
skip(not is_ci() and n.fn.executable('fswatch') == 0, 'fswatch not installed and not on CI') skip(
not is_ci() and n.fn.executable('inotifywait') == 0,
'inotify-tools not installed and not on CI'
)
end end
if watchfunc == 'watch' then if watchfunc == 'watch' then
@ -123,5 +126,5 @@ describe('vim._watch', function()
run('watch') run('watch')
run('watchdirs') run('watchdirs')
run('fswatch') run('inotify')
end) end)

View File

@ -5128,12 +5128,12 @@ describe('LSP', function()
it( it(
string.format('sends notifications when files change (watchfunc=%s)', watchfunc), string.format('sends notifications when files change (watchfunc=%s)', watchfunc),
function() function()
if watchfunc == 'fswatch' then if watchfunc == 'inotify' then
skip(is_os('win'), 'not supported on windows') skip(is_os('win'), 'not supported on windows')
skip(is_os('mac'), 'flaky test on mac') skip(is_os('mac'), 'flaky test on mac')
skip( skip(
not is_ci() and fn.executable('fswatch') == 0, not is_ci() and fn.executable('inotifywait') == 0,
'fswatch not installed and not on CI' 'inotify-tools not installed and not on CI'
) )
end end
@ -5265,7 +5265,7 @@ describe('LSP', function()
test_filechanges('watch') test_filechanges('watch')
test_filechanges('watchdirs') test_filechanges('watchdirs')
test_filechanges('fswatch') test_filechanges('inotify')
it('correctly registers and unregisters', function() it('correctly registers and unregisters', function()
local root_dir = '/some_dir' local root_dir = '/some_dir'