fix(emulator): recycle iOS simulators that wedge-boot without a display framebuffer#7065
Conversation
…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>
|
Ready to review this PR? Stage has broken it down into 4 individual chapters for you:
Chapters generated by Stage for commit b96d309 on Jul 2, 2026 3:21am UTC. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughThis 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. ChangesEstimated 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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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 |
AmethystLiang
left a comment
There was a problem hiding this comment.
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.
Summary
Fixes #7062.
When a CoreSimulator device wedge-boots —
simctlreportsBootedand HID works, but the display IO ports never come up so there is nocom.apple.framebuffer.displayport — the Mobile Emulator pane fails with the rawHelper failed: … Failed to start capture: No framebuffer display descriptor founddump, and Connect can never recover:ensureSimulatorBootedearly-returns on theBootedstate string, so every retry re-targets the same broken device. The only remedy is a manualsimctl 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:
startSessionnow 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:
shutdownSimulatorDevicetreated everysimctl shutdownfailure as "already shut down" because Node'sexecFileerror 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 intendedcurrent stateidempotency check only, so genuine failures propagate.emulator attachnow 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 lintpnpm typecheckpnpm 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 cleanorigin/main(18) — machine flakes with no overlap with this diff; notablyserve-sim-state-watcher's dedupe-key test fails identically on cleanmainpnpm build—build:desktopand 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 onmaintoo, unrelated to this change)emulator_helper_failederror when the display stays wedged after exactly one recycle (no shutdown/boot loop)simctl shutdownfailing) instead of masking them with the canned messageshutdownSimulatorDevicesuite: resolves on thecurrent state(already-shutdown) idempotency case, rejects on real failures — pinning the swallow-bug fixAlso reproduced end-to-end on the affected machine: a genuinely wedged device (
simctl io enumerateshowing 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:
shutdownSimulatorDeviceswallow bug (above); the framebuffer predicate narrowed toEmulatorErrorwith codeemulator_errorso 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.IosEmulatorBackend, gated byisSupportedOnHost/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.startSessioncall — no loops.Security Audit
execFilewith array argv (no shell).udidembedded in the copy-pasteablesimctl erasesuggestion is provably a simctl-reported device UDID:ensureSimulatorBootedthrowsemulator_device_not_foundfor anything not insimctl listbefore the new path can run. (Defense-in-depth option noted for a follow-up: assert UDID shape before embedding.)Notes
simctl shutdownfailures instead of silently succeeding — previously they were indistinguishable from success.🤖 Generated with Claude Code