Skip to content

fix(tool-server): stop the simulator watcher polling simctl for orphaned or idle servers - #989

Open
suleymancelikel wants to merge 1 commit into
software-mansion:mainfrom
suleymancelikel:fix/simulator-watcher-idle-gates
Open

fix(tool-server): stop the simulator watcher polling simctl for orphaned or idle servers#989
suleymancelikel wants to merge 1 commit into
software-mansion:mainfrom
suleymancelikel:fix/simulator-watcher-idle-gates

Conversation

@suleymancelikel

@suleymancelikel suleymancelikel commented Aug 31, 2026

Copy link
Copy Markdown

The simulator watcher runs xcrun simctl list devices --json every 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 /tools every 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() checks pgrep -x launchd_sim. Every booted simulator runs a launchd_sim process, 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.ts covers 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 that launchd_sim appears within a second of simctl boot and disappears after shutdown.

No docs update needed, nothing user-facing changes.

Summary by CodeRabbit

  • Performance
    • Reduced unnecessary simulator polling when no simulator is running.
    • Pauses simulator monitoring after extended client inactivity, helping conserve system resources.
    • Resumes monitoring when activity returns, maintaining simulator state updates.
  • Reliability
    • Improved simulator detection and shutdown handling, including fallback behavior when system checks are unavailable.
  • Tests
    • Added coverage for idle monitoring, simulator detection, shutdown disposal, polling fallback, and activity resumption.

…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.
@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cba2fa3f-690a-4cce-bbfd-780812b6bc18

📥 Commits

Reviewing files that changed from the base of the PR and between 927bfc6 and 984a7e0.

📒 Files selected for processing (3)
  • packages/tool-server/src/index.ts
  • packages/tool-server/src/utils/simulator-watcher.ts
  • packages/tool-server/test/simulator-watcher-idle-gates.test.ts

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The 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.

Changes

Simulator watcher gating

Layer / File(s) Summary
Watcher polling gates
packages/tool-server/src/utils/simulator-watcher.ts
The watcher checks client activity against a 120-second window. It uses pgrep -x launchd_sim to avoid unnecessary simctl calls when no simulator is running.
HTTP activity integration and validation
packages/tool-server/src/index.ts, packages/tool-server/test/simulator-watcher-idle-gates.test.ts
The watcher receives the HTTP app activity callback after app creation. Tests cover stale-activity suppression, resumption, simulator tracking, disposal, process-detection errors, and polling without an activity callback.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🔵 Low · up to 984a7

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
Loading

Suggested reviewers: filip131311, latekvo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 3 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: it prevents unnecessary simulator watcher polling for orphaned or idle servers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

Some tools did not complete. Review the errors below.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/tool-server/src/index.ts

Parsing 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 '@docusaurus/tsconfig' not found.

packages/tool-server/src/utils/simulator-watcher.ts

Parsing 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 '@docusaurus/tsconfig' not found.

packages/tool-server/test/simulator-watcher-idle-gates.test.ts

Parsing 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 '@docusaurus/tsconfig' not found.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant