mirror of
https://github.com/neovim/neovim.git
synced 2026-11-06 05:16:52 +00:00
fix(event): wait() skips final condition check #42600
Problem: `wait()` and `vim.wait()` return -1 without checking the condition again if an event processed during the last poll exceed the timeout, though the docs say the condition is evaluated on internal events. So -1 may mean the condition is already true. If we "document" this, we would essentially be documenting a race condition. Solution: Check the condition once more after the last poll, like Python's `Condition.wait_for()`.
This commit is contained in:
1 parent
bca9361085
commit
1d5c87b845
4 files changed
+43
-19
No files matched your search
@@ -145,6 +145,7 @@ function vim.wait(time, callback, interval, fast_only)
|
||||
-- Flush screen updates before blocking.
|
||||
vim._core.ui_flush()
|
||||
|
||||
local expired = false
|
||||
while true do
|
||||
if vim._core.check_interrupt() then
|
||||
cleanup()
|
||||
@@ -162,6 +163,11 @@ function vim.wait(time, callback, interval, fast_only)
|
||||
end
|
||||
end
|
||||
|
||||
if expired then
|
||||
cleanup()
|
||||
return false, -1
|
||||
end
|
||||
|
||||
local poll_timeout = -1
|
||||
if has_deadline then
|
||||
local remaining_ms = time - (vim.uv.hrtime() - start) / 1e6
|
||||
@@ -175,16 +181,9 @@ function vim.wait(time, callback, interval, fast_only)
|
||||
-- polling would only resume for unrelated events or the final timeout.
|
||||
vim._core.loop_poll(poll_timeout, fast_only)
|
||||
|
||||
-- Check deadline AFTER polling (mirrors LOOP_PROCESS_EVENTS_UNTIL behavior).
|
||||
-- `got_int` may have been set during poll; let check_interrupt() catch it.
|
||||
if has_deadline and time - (vim.uv.hrtime() - start) / 1e6 <= 0 then
|
||||
if vim._core.check_interrupt() then
|
||||
cleanup()
|
||||
return false, -2
|
||||
end
|
||||
cleanup()
|
||||
return false, -1
|
||||
end
|
||||
-- Checks `callback` after the last poll, which may have run past the deadline (same as
|
||||
-- LOOP_PROCESS_EVENTS_UNTIL).
|
||||
expired = has_deadline and time - (vim.uv.hrtime() - start) / 1e6 <= 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -22,21 +22,19 @@
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// Poll for events until a condition or timeout
|
||||
// Poll for events until `condition` or timeout. Checks `condition` after the last poll, which may
|
||||
// have run past the timeout.
|
||||
#define LOOP_PROCESS_EVENTS_UNTIL(loop, multiqueue, timeout, condition) \
|
||||
do { \
|
||||
const int64_t timeout_ms = (timeout); \
|
||||
const uint64_t start = (timeout_ms > 0) ? os_hrtime() : 0; \
|
||||
const uint64_t start = (timeout_ms >= 0) ? os_hrtime() : 0; \
|
||||
int64_t remaining = timeout_ms; \
|
||||
while (!(condition)) { \
|
||||
bool expired = false; \
|
||||
while (!(condition) && !expired) { \
|
||||
LOOP_PROCESS_EVENTS(loop, multiqueue, remaining); \
|
||||
if (remaining == 0) { \
|
||||
break; \
|
||||
} else if (remaining > 0) { \
|
||||
if (timeout_ms >= 0) { \
|
||||
remaining = timeout_ms - (int64_t)((os_hrtime() - start) / 1000000); \
|
||||
if (remaining <= 0) { \
|
||||
break; \
|
||||
} \
|
||||
expired = remaining <= 0; \
|
||||
} \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
@@ -2430,6 +2430,21 @@ describe('lua stdlib', function()
|
||||
)
|
||||
end)
|
||||
|
||||
it('checks the callback after a deferred function runs past the timeout #11193', function()
|
||||
eq(
|
||||
true,
|
||||
exec_lua(function()
|
||||
vim.defer_fn(function()
|
||||
vim.uv.sleep(100)
|
||||
vim.g.timer_result = true
|
||||
end, 0)
|
||||
return vim.wait(50, function()
|
||||
return vim.g.timer_result
|
||||
end)
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it('does not crash when callback errors', function()
|
||||
local result = exec_lua [[
|
||||
return {pcall(function() vim.wait(1000, function() error("As Expected") end) end)}
|
||||
|
||||
@@ -33,6 +33,18 @@ describe('wait()', function()
|
||||
eq(0, eval('wait(0, 1)'))
|
||||
end)
|
||||
|
||||
it('checks the condition after a timer callback runs past the timeout #11193', function()
|
||||
-- Starts the timer in the same request as wait(), so it fires inside wait().
|
||||
source([[
|
||||
func StartAndWait()
|
||||
let g:_awake = 0
|
||||
call timer_start(0, { -> execute(['sleep 100m', 'let g:_awake = 1']) })
|
||||
return wait(50, { -> g:_awake })
|
||||
endfunc
|
||||
]])
|
||||
eq(0, eval('StartAndWait()'))
|
||||
end)
|
||||
|
||||
it('returns -1 on timeout', function()
|
||||
eq(-1, eval('wait(0, 0)'))
|
||||
eq(-1, eval('wait(50, 0)'))
|
||||
|
||||
Reference in new issue
Block a user