Skip to content

fix(emulator): recycle iOS simulators that wedge-boot without a display framebuffer#7065

Merged
AmethystLiang merged 2 commits into
stablyai:mainfrom
nightlaro:fix/ios-emulator-wedged-display-recycle
Jul 3, 2026
Merged

fix(emulator): recycle iOS simulators that wedge-boot without a display framebuffer#7065
AmethystLiang merged 2 commits into
stablyai:mainfrom
nightlaro:fix/ios-emulator-wedged-display-recycle

Conversation

@nightlaro

@nightlaro nightlaro commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #7062.

When a CoreSimulator device wedge-boots — simctl reports Booted and HID works, but the display IO ports never come up so there is no com.apple.framebuffer.display port — the Mobile Emulator pane fails with the raw Helper failed: … Failed to start capture: No framebuffer display descriptor found dump, and Connect can never recover: ensureSimulatorBooted early-returns on the Booted state string, so every retry re-targets the same broken device. The only remedy is a manual simctl shutdown + boot (verified on a live wedged device; serve-sim's own CI works around the same phenomenon with a reboot-and-retry wrapper, EvanBacon/serve-sim#118).

User-visible change: startSession now recognizes that specific helper-failure signature, recycles the device once (shutdown + boot), and retries the helper start. If the display still doesn't come up, the pane shows an actionable erase/recreate message (with the helper log appended for diagnosis) instead of the raw log dump.

Supporting fixes surfaced by review:

  • shutdownSimulatorDevice treated every simctl shutdown failure as "already shut down" because Node's execFile error message echoes the command line, which always contains the word "shutdown". Real failures (e.g. a 30s timeout on a wedged device) were silently swallowed — which would have made the recycle a no-op and the "even after a reboot" message a lie. The match is now the intended current state idempotency check only, so genuine failures propagate.
  • The CLI's emulator attach now passes a wider client-side RPC timeout: a successful recovery (shutdown + boot + second helper start) can legitimately exceed the 60s default, and previously would have been misreported as a client timeout while the server finished attaching.

Screenshots

No visual change (main-process behavior fix). Before/after error text is in #7062.

Testing

  • pnpm lint
  • pnpm typecheck
  • pnpm test — every suite covering the changed files passes (targeted run of the emulator backends, simctl devices, and CLI projects: 376/376). The full suite on my machine fails a shifting set of process-heavy tests (pty / git-relay / subprocess) on both this branch (9) and clean origin/main (18) — machine flakes with no overlap with this diff; notably serve-sim-state-watcher's dedupe-key test fails identically on clean main
  • pnpm buildbuild:desktop and the native Swift build complete; the final codesign step fails on this machine due to a duplicated "Apple Development" identity in the local keychain (environment issue, present on main too, unrelated to this change)
  • Added high-quality regression tests:
    • recycles the device and retries when the helper reports the missing-framebuffer signature (asserts shutdown + re-boot + second start)
    • does not recycle for unrelated helper failures (asserts single start, no shutdown)
    • surfaces the actionable emulator_helper_failed error when the display stays wedged after exactly one recycle (no shutdown/boot loop)
    • propagates mid-recycle failures (e.g. simctl shutdown failing) instead of masking them with the canned message
    • new shutdownSimulatorDevice suite: resolves on the current state (already-shutdown) idempotency case, rejects on real failures — pinning the swallow-bug fix

Also reproduced end-to-end on the affected machine: a genuinely wedged device (simctl io enumerate showing no display ports) recovered fully after shutdown+boot and the pane connected.

AI Review Report

Three independent adversarial review passes (correctness/failure-modes, project-conventions, security) were run on the diff; all findings were either fixed or consciously scoped out:

  • Fixed from review: the CLI attach timeout (above); the shutdownSimulatorDevice swallow bug (above); the framebuffer predicate narrowed to EmulatorError with code emulator_error so a wrapped or non-helper error can't trigger a spurious recycle; both helper-start sites (initial start and the endpoint-readiness restart) now go through the same guarded closure so the recovery isn't asymmetric; the final actionable error preserves the helper's raw log.
  • Cross-platform (macOS/Linux/Windows): all behavior changes live in the darwin-only iOS backend (IosEmulatorBackend, gated by isSupportedOnHost/platform() !== 'darwin' throws in the simctl helpers) plus a timeout constant in the CLI handler; no new platform-dependent paths, shortcuts, labels, or path handling.
  • SSH/remote: the change is main-process + a client-side timeout; no assumption that files/processes are local beyond what the iOS backend already requires (simctl on the runtime host).
  • Agents/integrations/git providers: untouched.
  • Performance: the recovery work only runs after a failed helper start matching the wedge signature; the happy path is unchanged. Recycle is bounded to one per startSession call — no loops.

Security Audit

  • The error-string match only gates whether to recycle; it never contributes arguments to any command. All process spawns remain execFile with array argv (no shell).
  • The udid embedded in the copy-pasteable simctl erase suggestion is provably a simctl-reported device UDID: ensureSimulatorBooted throws emulator_device_not_found for anything not in simctl list before the new path can run. (Defense-in-depth option noted for a follow-up: assert UDID shape before embedding.)
  • DoS angle reviewed: the recycle is bounded to one per user-initiated attach; a hostile app inside the simulator cannot trigger repeated shutdown/boot cycles.
  • No new IPC surface, input handling, secrets, or dependency changes.

Notes

  • Behavior change beyond the pane: with the swallow-bug fix, the toolbar power button now surfaces real simctl shutdown failures instead of silently succeeding — previously they were indistinguishable from success.
  • Known bounded edge (consciously left out): if the wedge signature appears while a different Orca window holds a live session on the same device, the recycle shuts that session's device down; sessions are per-device registered and the registry re-attaches, and this mirrors what a user fixing the device manually would do.
  • X handle: @micko_solac

🤖 Generated with Claude Code


Open in Stage

…ay framebuffer

CoreSimulator can report a device Booted while its display IO ports never
came up (HID alive, no com.apple.framebuffer.display port), so
ensureSimulatorBooted passes and serve-sim --detach dies with 'No
framebuffer display descriptor found'. Reconnecting hits the same Booted
early-return, so the pane could never recover without a manual
simctl shutdown/boot.

startSession now recognizes that helper failure signature, recycles the
device once (shutdown + boot), and retries; if the display still fails to
come up it surfaces an actionable erase/recreate message instead of the
raw helper log dump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@stage-review

stage-review Bot commented Jul 2, 2026

Copy link
Copy Markdown

Ready to review this PR? Stage has broken it down into 4 individual chapters for you:

Title
1 Fix simulator shutdown idempotency check
2 Implement iOS simulator recycling for wedged boots
3 Test iOS backend recovery and error handling
4 Increase CLI attach timeout for recovery
Open in Stage

Chapters generated by Stage for commit b96d309 on Jul 2, 2026 3:21am UTC.

@coderabbitai

coderabbitai Bot commented Jul 2, 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 77a6795d-412e-4c6d-8ad6-96d024f4d92a

📥 Commits

Reviewing files that changed from the base of the PR and between b96d309 and 03ea3c3.

📒 Files selected for processing (5)
  • src/cli/handlers/emulator.test.ts
  • src/main/emulator/backends/ios-emulator-backend.test.ts
  • src/main/emulator/backends/ios-emulator-backend.ts
  • src/main/emulator/simctl-simulator-devices.test.ts
  • src/main/emulator/simctl-simulator-devices.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/emulator/backends/ios-emulator-backend.test.ts
  • src/main/emulator/backends/ios-emulator-backend.ts

📝 Walkthrough

Walkthrough

This change adds missing-framebuffer recovery to iOS emulator session startup, including a single shutdown-and-reboot retry path and new failure handling when the display remains wedged. It also narrows simulator shutdown error matching to the specific already-shutdown case and updates the emulator attach CLI to use a 180-second timeout override. Tests were added or expanded for the new recovery, shutdown, and timeout behavior.

Changes

Estimated code review effort: Medium

Related issues: None provided

Related PRs: None provided

Suggested labels: emulator, bug-fix, tests

Suggested reviewers: None provided

Sequence Diagram(s)

sequenceDiagram
  participant startSession
  participant startHelperRecyclingWedgedBoot
  participant execServeSimCommand
  participant shutdownSimulatorDevice
  participant ensureSimulatorBooted

  startSession->>startHelperRecyclingWedgedBoot: start detached helper
  startHelperRecyclingWedgedBoot->>execServeSimCommand: attempt start
  execServeSimCommand-->>startHelperRecyclingWedgedBoot: missing framebuffer error
  startHelperRecyclingWedgedBoot->>shutdownSimulatorDevice: shut down device
  startHelperRecyclingWedgedBoot->>ensureSimulatorBooted: reboot device
  startHelperRecyclingWedgedBoot->>execServeSimCommand: retry start
  execServeSimCommand-->>startSession: session result or error
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: iOS simulator recovery for wedge-boot framebuffer failures.
Description check ✅ Passed The description follows the required template and includes summary, screenshots, testing, AI review, security audit, and notes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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.

@AmethystLiang AmethystLiang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough root-cause writeup and tests. I reviewed this end to end and added a few maintainer fixes:

  • Bounded the missing-framebuffer recycle to one per startSession, including the stale-endpoint retry path.
  • Tightened simctl shutdown idempotency so only current state: Shutdown is swallowed.
  • Added CLI coverage for the wider attach timeout.

Validated with typecheck, lint, focused emulator tests, delegated review, and an Electron app-level recovery harness that exercised first helper failure -> shutdown -> boot -> retry -> successful attach.

@AmethystLiang AmethystLiang merged commit dbd53eb into stablyai:main Jul 3, 2026
6 checks passed
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.

[Bug]: Mobile Emulator cannot connect or recover when a simulator wedge-boots without a display framebuffer ("No framebuffer display descriptor found")

3 participants