fix(tool-server): stop the simulator watcher polling simctl for orphaned or idle servers - #989
Conversation
…ned or idle servers The watcher spawned `xcrun simctl list devices --json` every 10 s from the moment a tool-server started, whether or not a simulator was booted and whether or not any client was still connected. An autospawned server outlives its MCP client (detached, 30 min idle timeout), so after an editor crash it kept polling with nobody listening. On Xcode 26 each simctl spawn makes CoreSimulator rescan the cryptex runtime volumes, so a few such orphans together can saturate a machine (mitigates software-mansion#327). Two gates, no behavior change for a live client with a booted simulator: - Client activity gate: an interval tick is skipped when the server has seen no inbound request for 2 minutes. index.ts binds it to the HTTP app's idle timer, which the MCP health check (GET /tools every 30 s) already touches. The awaited first poll(true) is not gated, so startup semantics are unchanged; callers that omit the option keep unconditional polling. - launchd_sim pre-check: every booted simulator runs a launchd_sim, so poll() runs `pgrep -x launchd_sim` first and returns without spawning simctl when nothing matches and nothing is tracked. While a tracked simulator exists it still asks simctl, so shutdown is observed and the native-devtools service disposed. pgrep exit 1 is 'no match'; any other failure falls back to simctl, so the gate can only skip work, never hide a simulator.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe simulator watcher now skips polling for stale HTTP clients and when no simulator process is running. The HTTP app provides the activity timestamp callback. Tests cover gating, simulator tracking, process detection failures, resumption, and disposal. ChangesSimulator watcher gating
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: 🔵 Low · up to The watcher now pauses inactive polling and avoids simulator enumeration when no simulator process is present, reducing unnecessary host load. A stopped simulator may remain briefly represented as available when a client returns after inactivity, so merge is appropriate with explicit owner awareness of this bounded stale-state window. Sequence Diagram(s)sequenceDiagram
participant HTTPApp
participant SimulatorWatcher
participant launchd_sim
participant simctl
HTTPApp->>SimulatorWatcher: provide getLastActivityAt callback
SimulatorWatcher->>HTTPApp: read last activity timestamp
SimulatorWatcher->>SimulatorWatcher: skip poll when activity is stale
SimulatorWatcher->>launchd_sim: run pgrep -x launchd_sim
launchd_sim-->>SimulatorWatcher: report process status
SimulatorWatcher->>simctl: list simulators when polling is required
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Some tools did not complete. Review the errors below. 🔧 ESLint
packages/tool-server/src/index.tsParsing error: Unable to parse the specified 'tsconfig' file. Ensure it's correct and has valid syntax. packages/docs/tsconfig.json(2,14): error TS6053: File ' packages/tool-server/src/utils/simulator-watcher.tsParsing error: Unable to parse the specified 'tsconfig' file. Ensure it's correct and has valid syntax. packages/docs/tsconfig.json(2,14): error TS6053: File ' packages/tool-server/test/simulator-watcher-idle-gates.test.tsParsing error: Unable to parse the specified 'tsconfig' file. Ensure it's correct and has valid syntax. packages/docs/tsconfig.json(2,14): error TS6053: File ' Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The simulator watcher runs
xcrun simctl list devices --jsonevery 10 seconds from the moment the tool-server starts, even when no simulator is booted and no client is connected anymore. An autospawned server outlives its MCP client (detached, 30 min idle timeout), so after an editor crash it keeps polling with nobody listening, and the next editor start adds another one. On Xcode 26 every simctl spawn makes CoreSimulator rescan the sealed cryptex runtime volumes, so a handful of orphaned pollers was enough to saturate my machine: load average 61 on an M1 Pro, vnode table full, three hard reboots in one evening.This mitigates #327 (it makes an orphan go quiet instead of hammering CoreSimulator, though the lifecycle problem itself stays open) and composes with #480, which serializes the simctl calls that still happen.
The change is two gates in
simulator-watcher.ts. Nothing changes for a live client with a booted simulator.First, an interval tick is skipped when the server has seen no inbound request for 2 minutes. The timestamp comes from the HTTP app's idle timer, which every request and the MCP health check (
GET /toolsevery 30 s) already touch, so a live client never hits the gate. The awaited first poll is not gated and startup semantics stay as they were; callers that don't pass the option keep the old unconditional polling.Second, before calling simctl,
poll()checkspgrep -x launchd_sim. Every booted simulator runs alaunchd_simprocess, so when nothing matches and the watcher tracks nothing there is nothing to attach or dispose, and the tick returns without spawning simctl. While a tracked simulator exists it still asks simctl, so a shutdown is observed and the native-devtools service disposed. pgrep exit 1 means "no match"; any other failure falls back to simctl, so the gate can only skip work, never hide a simulator.New
test/simulator-watcher-idle-gates.test.tscovers both gates, the pgrep fallback and the no-option path (5 tests, same execFile mock harness as the init-failure tests). Full tool-server suite,tsc --noEmit, eslint and prettier are clean. Verified on macOS 26.5 / Xcode 26.4 thatlaunchd_simappears within a second ofsimctl bootand disappears after shutdown.No docs update needed, nothing user-facing changes.
Summary by CodeRabbit