Commit be10171
* [Audit v2 #353] 30s watchdog on filesystem_changed during update reload
Pre-fix, `_start_filesystem_scan` connected `filesystem_changed`
(CONNECT_ONE_SHOT) and called `fs.scan()`, then sat in
`_waiting_for_scan = true` forever if the signal never fired (slow
disk, NFS, AV holding the just-extracted addon files open). The
update completed mechanically — files on disk, plugin disabled —
but the dock stayed disabled because `_finish_scan_wait` was the
only path back to `_enable_new_plugin`. User had to kill and
restart Godot.
Add a 30-second `Timer` armed alongside the signal connect:
- `_arm_scan_watchdog`: lazy-creates a one-shot `Timer` child node
on first use, reuses it on subsequent arms (the runner does two
filesystem scans per update — new files then existing files).
- `_on_scan_watchdog_timeout`: if the wait is still open, push a
warning, disconnect the `filesystem_changed` listener so a
delayed signal can't double-call, then dispatch
`_finish_scan_wait`. `_finish_scan_wait`'s existing
`if not _waiting_for_scan: return` guard makes it idempotent
against the signal-arrives-just-before-watchdog race.
- `_finish_scan_wait` now also stops the still-running timer on the
happy path, so a queued late `timeout` can't fire after a
successful scan.
Worst case after the watchdog fires: the new files aren't visible
on the first frame the new plugin enables, but they get picked up
on the next scan. That's strictly better than the prior dock-
permanently-disabled state.
Tests in `test_project/tests/test_update_reload_runner.gd`:
- `test_watchdog_timeout_proceeds_when_signal_never_fires` — the
deadlock case the issue describes.
- `test_watchdog_no_op_when_signal_already_settled` — late-Timer
race after the signal won.
- `test_finish_scan_wait_stops_armed_watchdog` — happy path
cancels the Timer.
- `test_watchdog_timer_reused_across_arms` — second scan in the
same update doesn't leak a second Timer child.
Tests fire `_on_scan_watchdog_timeout` and `_finish_scan_wait`
directly rather than waiting for the wall-clock Timer, keeping
runs deterministic and sub-second.
Closes #353
* Fix CI: remove add_child(runner) from watchdog tests
McpTestSuite extends RefCounted (not Node), so add_child(runner) errors
at test runtime — Linux/macOS/Windows all failed with the same root
cause. The tests fire _on_scan_watchdog_timeout() directly instead of
relying on the Timer counting down, so the runner doesn't actually need
to be in a SceneTree; the Timer just needs to be a child of the runner
(works regardless of where the runner sits).
* Redesign per Copilot review: sticky bypass after watchdog + tests in tree
Two issues from Copilot's review on PR #381 + the second CI failure:
(A) Cross-scan signal race [Copilot]: scan #1 watchdog'd, scan #2 then
arms a fresh `filesystem_changed` listener. A delayed emission
from scan #1 fires on whichever listener is currently connected
to the shared signal — Godot can't tag emissions with their
source scan — so scan #2's listener falsely settles before its
actual filesystem scan completed, re-enabling the plugin against
a potentially incomplete on-disk install. Generation-counter +
Callable.bind doesn't help because a single emission still fires
every connected listener (CONNECT_ONE_SHOT only auto-disconnects
after firing).
(B) Second CI failure: `Timer.start()` requires the Timer to be
inside a SceneTree. The previous fix removed `add_child(runner)`
from tests but didn't parent the runner anywhere, so when
`_arm_scan_watchdog` called `add_child(timer)` and `timer.start()`
inside the runner, the timer wasn't in any tree.
Fix:
- Add a sticky `_scan_timed_out` flag set by `_on_scan_watchdog_timeout`.
- `_start_filesystem_scan` checks the flag at the top and bypasses the
connect + `fs.scan()` path entirely, falling straight through to
`call_deferred(deferred_step)`. With no listener armed, a delayed
emission from the timed-out scan has nothing to satisfy. Godot's
normal background scan catches up after the plugin re-enables.
- Tests now parent the runner via `(Engine.get_main_loop() as
SceneTree).root.add_child(runner)` so `Timer.start()` works.
Cleanup via a `_free_runner` helper (remove_child + free).
- New regression test `test_subsequent_scan_after_watchdog_bypasses_
listener_arm` explicitly drives scan #1 → watchdog → scan #2 and
asserts scan #2 does NOT set `_waiting_for_scan = true` (i.e. no
listener armed for a delayed scan-#1 emission to satisfy).
- Existing watchdog test `test_watchdog_no_op_when_signal_already_settled`
also asserts `_scan_timed_out` stays false on the late-Timer-after-
successful-signal path — guards against the watchdog poisoning
subsequent scans when no actual deadlock happened.
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent dfb8e02 commit be10171
2 files changed
Lines changed: 226 additions & 0 deletions
File tree
- plugin/addons/godot_ai
- test_project/tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
38 | 55 | | |
39 | 56 | | |
40 | 57 | | |
| |||
121 | 138 | | |
122 | 139 | | |
123 | 140 | | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
124 | 156 | | |
125 | 157 | | |
126 | 158 | | |
127 | 159 | | |
| 160 | + | |
128 | 161 | | |
129 | 162 | | |
130 | 163 | | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
131 | 203 | | |
132 | 204 | | |
133 | 205 | | |
| |||
392 | 464 | | |
393 | 465 | | |
394 | 466 | | |
| 467 | + | |
395 | 468 | | |
396 | 469 | | |
397 | 470 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
478 | 478 | | |
479 | 479 | | |
480 | 480 | | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
| 578 | + | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
0 commit comments