feat(sim): linkcode-sim iOS Simulator sidecar — P0 simctl lifecycle (CODE-392)#247
feat(sim): linkcode-sim iOS Simulator sidecar — P0 simctl lifecycle (CODE-392)#247AprilNEA wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c59e250d5a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
|
|
||
| fn simctl<'a>(args: impl IntoIterator<Item = &'a str>) -> Command { | ||
| let mut cmd = Command::new(XCRUN); |
There was a problem hiding this comment.
Clear SDK overrides before invoking Apple's xcrun
When the sidecar inherits DEVELOPER_DIR or SDKROOT from devenv or another launcher, /usr/bin/xcrun still honors those overrides. The new device-loop fixture explicitly removes both variables because a foreign SDK breaks Apple tool resolution (tests/device_loop.rs:151-157), but the production commands preserve them here and in probe, causing list, boot, and other operations to fail despite full Xcode being installed. Remove both overrides from every Apple-tool Command.
AGENTS.md reference: AGENTS.md:L52-L52
Useful? React with 👍 / 👎.
| if write_frame(&mut stdout, type_byte, &body).is_err() { | ||
| // The daemon is gone; frames have nowhere to go. | ||
| break; |
There was a problem hiding this comment.
Handle oversized screenshots without disabling all responses
When a valid PNG screenshot exceeds the 16 MiB frame limit, which is possible for a high-entropy iPad-sized screen, write_frame returns InvalidInput and this branch permanently exits the sole writer thread. The main thread continues accepting requests and silently discards their responses through the disconnected channel, so the daemon sees timeouts while the sidecar remains alive and cannot be restarted via exit detection. Validate screenshot size and return a request error, or terminate the sidecar on writer failure.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR adds a Rust sidecar for controlling the iOS Simulator. The main changes are:
Confidence Score: 4/5The response writer and shutdown paths can leave the simulator sidecar unusable or running work after daemon shutdown. A large PNG can stop the sole response writer. Request concurrency has no bound. Closing stdin does not cancel active simulator commands. Inherited Xcode selection can incorrectly disable the capability. crates/linkcode-sim/src/main.rs and crates/linkcode-sim/src/simctl.rs
What T-Rex did
Important Files Changed
Sequence DiagramsequenceDiagram
participant D as Daemon
participant M as Sidecar reader
participant R as Request thread
participant X as xcrun simctl
participant W as Stdout writer
D->>M: REQUEST frame
M->>R: Spawn request thread
R->>X: Run simulator operation
X-->>R: Output or screenshot file
R->>W: Queue RESULT or SCREENSHOT
W-->>D: Framed response
Reviews (1): Last reviewed commit: "ci(desktop): stage the linkcode-sim side..." | Re-trigger Greptile |
| encode_screenshot(&request_id, &image) | ||
| .map_err(|e| OpError::new(ErrorCode::Io, e.to_string())) | ||
| }) { | ||
| Ok(body) => { |
There was a problem hiding this comment.
| match serde_json::from_slice::<Request>(&body) { | ||
| Ok(request) => { | ||
| let tx = tx.clone(); | ||
| thread::spawn(move || serve(request, &tx)); |
There was a problem hiding this comment.
Requests Spawn Unbounded Threads
Each frame immediately creates an OS thread, and most request threads keep an xcrun child and pipe readers alive for up to 30–180 seconds. A realistic retry burst or repeated boot requests can exhaust threads or file descriptors, preventing the sidecar from serving valid requests.
| let _ = tx.send(OutMsg::Stop); | ||
| let _ = writer.join(); |
There was a problem hiding this comment.
EOF Leaves Simctl Children Running
When daemon stdin closes during a boot or install, this path stops only the writer and does not cancel or join the active request threads. Their xcrun children continue until completion or the operation deadline, so the sidecar can retain simulator work and child processes after its owner has shut down.
Artifacts
Repro: executable controlled xcrun lifecycle harness
- Contains supporting evidence from the run (text/x-python; charset=utf-8).
Repro: failing run output showing the xcrun child alive after EOF and sidecar exit
- Keeps the command output available without making the summary code-heavy.
| let mut find_simctl = Command::new(XCRUN); | ||
| find_simctl.args(["--find", "simctl"]); |
There was a problem hiding this comment.
Inherited Developer Directory Breaks Probe
/usr/bin/xcrun still honors inherited variables such as DEVELOPER_DIR. When the desktop daemon inherits a stale or non-Xcode developer directory, --find simctl fails and this code reports xcodeMissing even though a usable Xcode installation is selected system-wide, incorrectly disabling simulator support.
Summary
New Rust sidecar
crates/linkcode-sim: the iOS Simulator host the daemon spawns, P0 scope — publicxcrun simctlonly, no private API.linkcode-pty([u32 LE total][u8 type][body]): JSONREQUEST/RESULT+ binarySCREENSHOTframes (no base64 for captures). Contract incrates/linkcode-sim/PROTOCOL.md; the TS client lands as@linkcode/sim(CODE-393).probe/list/boot(waitsbootstatus -b; already-booted succeeds) /shutdown/install/launch/terminate/openUrl/screenshot(jpeg/png). Per-op deadlines; children killed on timeout.xcodeMissinggates the capability;simctlFailed/timeout/invalidRequest/io). Compiles and smoke-tests green on non-macOS./usr/bin/xcrun): PATH can carry non-Apple xcrun stand-ins (nix xcbuild's breaks SDK resolution).stage-sidecar.mtsnow stages a per-platform crate list;linkcode-simships insidecar/${arch}extraResources on macOS.Verification
cargo fmt --check/clippy/testclean;pnpm check:ci+pnpm test(1734) green.cargo test -p linkcode-sim --test device_loop -- --ignored) passed locally on an iPhone 17 Pro / iOS 26.5 simulator: cold boot → install (fixture UIKit app compiled on the fly) → launch (pid) → screenshot (JPEG magic, >10 KB) → terminate → shutdown.stage-sidecar.mtsrun locally: both binaries staged underapps/desktop/sidecar/arm64/.