Commit Graph
3 Commits
Author SHA1 Message Date
Brennan Benson 64be819790 fix(runtime): harden watcher and PTY teardown ownership (#8661)
* fix(runtime): retain watcher and PTY teardown ownership

* fix(runtime): restore watchers after interrupted cleanup

* fix(runtime): prevent stale watcher revival

* test(runtime): cover watcher shutdown ownership

* test(daemon): model physical PTY exit

* fix(daemon): keep shutdown terminating when disposal cannot prove exit

A rejecting host.dispose() (unreapable child past its exit deadline) left
the shutdown RPC without its process.nextTick(shutdown) and skipped socket
cleanup in shutdown(), stranding the daemon as an unreachable orphan after
the stale-daemon replacement flow unlinks its socket. Log and continue:
daemon exit reparents the child to init instead of blocking on it.

* fix(runtime): keep local watching alive after an idle-kill deadline miss

An idle child that outlived the exit deadline set shutdownRequested on the
shared desktop supervisor, which has no retire-and-replace path — every
later subscribe rejected supervisor_disposed and the roots were cached
unwatchable, silently ending local file watching for the session. The idle
path owns zero records, so there is no double-watch hazard; the zombie
keeps its capacity reservation until physical exit and the next subscribe
gets a fresh child.

* fix(renderer): resync replayed paired-web file watches

Transparent replay removed the implicit resync the old close-and-rebuild
path provided: a replayed files.watch only reports changes from its own
native setup, so changes during the reconnect gap were silently lost.
Deliver a conservative overflow to consumers once the replayed watch is
ready, matching the overflow-after-interruption contract everywhere else.

* fix(runtime): address teardown review findings

* fix(runtime): retry watches after teardown deadlines

* Fix PTY descendant leaks on forced teardown

* Fix jitter-sensitive terminal lifecycle test
2026-07-15 15:23:35 -07:00
Jinjing f93e92646c P2 watcher lifecycle bounds (#8640)
* fix watcher lifecycle cancellation bounds

* fix(review): guard remote watcher installs against post-shutdown resurrection

closeAllWatchers aborted the in-flight install *tokens* it could see, but a
same-key joiner awaiting a 'cancelled' resolution (and a fired retry tick)
calls installRemoteWatcher directly and, on the fresh-generation recursion,
builds a brand-new non-aborted AbortController and calls provider.watch()
after teardown — leaking an SSH watcher into the just-cleared remoteWatchers
map. Latch the subsystem shut in closeAllWatchers and refuse installs while
latched; a genuine new fs:watchWorktree clears it. Adds a regression test
(fails without the latch) plus a test for the same-tick handoff-revival guard
that had no coverage.

Also extract the duplicated isolated-quarantine-vs-fuse branch shared by
retireSlot and releaseFailedRoot into quarantineOrFuse (behavior-preserving).

* Add lifecycle generation guard to refuse stale remote-watcher joiners

- A boolean latch alone can't distinguish a pre-shutdown joiner from a
  fresh call once a genuine new watch reopens the subsystem, letting a
  stale joiner recurse and register a post-shutdown provider.watch()
- Each installRemoteWatcher call now captures a generation counter that
  closeAllWatchers bumps, so a waiter that resumes after a later
  shutdown+reopen is refused instead of resurrecting
- Adds a regression test covering the shutdown-then-reopen race
2026-07-13 19:12:30 -07:00
Jinjing 433df4be3c fix(runtime): prevent file watcher SIGSEGV from crashing orca serve (#8370)
* Fix crash-isolated file watcher process pool for orca-serve SIGSEGV afte

Replace the worker-thread runtime file watcher with a forked, crash-isolated
@parcel/watcher child process pool so a native FSEvents fault can no longer
take down the main/serve process, and add bounded event batching, delivery
backpressure, and quarantine-based recovery for faulty watch roots.

* Fix crash-isolated file watcher teardown and shutdown leaks

- Fault harness could throw before mkdtemp/realpath completed, skipping
  cleanup; now tracks each temp path independently and races an async
  watcher-callback error so it can't escape the try/finally unhandled.
- In-process fallback swallowed unsubscribe failures via a bare rejection
  handler that could still throw; use .catch() instead.
- Watcher process entry's cancel-subscribe handler now reuses the async
  unsubscribe path when a crawl already finished, releasing the native
  handle instead of leaking it (blocks worktree unlock on Windows).
- Runtime watcher process pool exposed no real dispose(); shutdown now
  kills pooled children so they don't outlive the main process.

* Fix disposeSlot double-iteration bug in file watcher pool teardown

Remove the unnecessary array snapshot in dispose(): disposeSlot mutates
allSlots by deleting the slot being visited, and deleting the
in-progress element during Set iteration is well-defined, so the spread
copy was dead weight left over from prior debugging.

* Fix pending file watcher installs not aborting on unsubscribe

- Local/WSL watcher installs and SSH fs.watch setup now honor the
  in-flight AbortSignal, so the last unwatch cancels a slow native
  subscribe or remote setup instead of waiting for it to finish.
- Thread signal through IFilesystemProvider.watch and SSH-backed
  file explorer watches for the same early-cancel behavior.

* Fix crash-resubscribe hangs and SSH watch teardown races in file watcher

- Add a bounded deadline for post-crash resubscription crawls so one
  stuck root quarantines instead of pinning its whole shard forever.
- Report FSEvents overflow as recoverable so delivery continues after
  a dropped-events error instead of surfacing as terminal.
- Make WSL watcher abort errors real DOMException instances so
  AbortSignal-based cancellation checks recognize them.
- Rework SSH watch registration so ownership of the shared setup
  request (not just the first caller) decides teardown, preventing
  one caller's abort from cancelling another's shared watch and
  guaranteeing exactly one fs.unwatch per registration.
- Reformat reliability-gates.jsonc arrays and refresh WSL/SSH coverage
  entries and evidence runs to match the above.

* Add CI gate to run the file-watcher SIGSEGV fault harness under Electron

- The reliability gate and release workflows (mac, Linux) previously only
  exercised the crash-isolation harness under vanilla Node, which doesn't
  catch runtime differences in the actual Electron binary that ships to
  users.
- Adds an `ELECTRON_RUN_AS_NODE=1 pnpm exec electron ...` run of the same
  harness alongside the existing Node run, so #8212's SIGSEGV-survival
  contract is proven against both runtimes before packaging.

* Add CI gate blocking Linux/macOS release packaging on watcher fault reco

Adds a contract test asserting release-cut.yml and release-mac-build.yml
run the runtime-file-watcher-fault-harness after building and before
publishing artifacts, so a regression in watcher process fault recovery
fails release packaging instead of shipping silently.

* Fix use-after-clear crash in failAllWatcherSubscriptions

Snapshot the records map before iterating, since onTerminalError
hooks can dispose the supervisor and clear `records` mid-loop,
causing a crash. Also update the matching test to assert against
the shared buildParcelWatcherIgnoreOptions helper instead of a
loose arrayContaining match.

* Fix use-after-clear crash in failAllWatcherSubscriptions

Snapshot watcher records with Array.from instead of spread, since
spread syntax over an iterator that's mutated mid-loop by
onTerminalError hooks can produce inconsistent results.
2026-07-12 18:15:15 -07:00