Skip to content

feat(sim): linkcode-sim iOS Simulator sidecar — P0 simctl lifecycle (CODE-392)#247

Open
AprilNEA wants to merge 3 commits into
masterfrom
xuan/code-392
Open

feat(sim): linkcode-sim iOS Simulator sidecar — P0 simctl lifecycle (CODE-392)#247
AprilNEA wants to merge 3 commits into
masterfrom
xuan/code-392

Conversation

@AprilNEA

Copy link
Copy Markdown
Member

Summary

New Rust sidecar crates/linkcode-sim: the iOS Simulator host the daemon spawns, P0 scope — public xcrun simctl only, no private API.

  • Framed stdio protocol mirroring linkcode-pty ([u32 LE total][u8 type][body]): JSON REQUEST/RESULT + binary SCREENSHOT frames (no base64 for captures). Contract in crates/linkcode-sim/PROTOCOL.md; the TS client lands as @linkcode/sim (CODE-393).
  • Ops: probe / list / boot (waits bootstatus -b; already-booted succeeds) / shutdown / install / launch / terminate / openUrl / screenshot (jpeg/png). Per-op deadlines; children killed on timeout.
  • Each request runs on its own thread — a slow boot never blocks a screenshot; single writer thread owns stdout.
  • Structured errors with stable codes (xcodeMissing gates the capability; simctlFailed/timeout/invalidRequest/io). Compiles and smoke-tests green on non-macOS.
  • Apple tools invoked by absolute path (/usr/bin/xcrun): PATH can carry non-Apple xcrun stand-ins (nix xcbuild's breaks SDK resolution).
  • CI/packaging: stage-sidecar.mts now stages a per-platform crate list; linkcode-sim ships in sidecar/${arch} extraResources on macOS.

Verification

  • cargo fmt --check / clippy / test clean; pnpm check:ci + pnpm test (1734) green.
  • Protocol smoke tests (all platforms): malformed-request recovery, unknown-frame tolerance, probe shape, EOF shutdown.
  • Real-device acceptance loop (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.mts run locally: both binaries staged under apps/desktop/sidecar/arm64/.

@linear-code

linear-code Bot commented Jul 22, 2026

Copy link
Copy Markdown

CODE-392

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Comment on lines +31 to +33
if write_frame(&mut stdout, type_byte, &body).is_err() {
// The daemon is gone; frames have nowhere to go.
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds a Rust sidecar for controlling the iOS Simulator. The main changes are:

  • A framed JSON and binary screenshot protocol over stdio.
  • Concurrent simctl operations with deadlines and structured errors.
  • Simulator lifecycle, app control, URL, list, and screenshot commands.
  • macOS sidecar staging and Electron packaging for both architectures.
  • Unit, smoke, and ignored real-simulator acceptance tests.

Confidence Score: 4/5

The 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

T-Rex T-Rex Logs

What T-Rex did

  • Ran the oversized capture test and observed that a 16,777,228-byte framed input blocked responses, with no stdout frame produced within two seconds, leaving the sidecar alive.
  • Executed the controlled xcrun lifecycle harness to verify that after closing sidecar stdin the xcrun child stayed alive and was reparented to PID 1, with the cancellation assertion failing before cleanup.
  • Captured baseline idle stdin EOF behavior, including a full framed transcript and SHA-256 for the tested binary, with exit status 0 and no stderr; macOS host limitation was noted in the run.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
crates/linkcode-sim/src/main.rs Adds concurrent request dispatch and serialized output, but oversized responses, unbounded threads, and shutdown handling can disrupt the sidecar lifecycle.
crates/linkcode-sim/src/proto.rs Adds bounded frame encoding and decoding plus raw screenshot bodies; screenshot size is not checked before writer dispatch.
crates/linkcode-sim/src/rpc.rs Adds camelCase request, operation, result, and structured error types.
crates/linkcode-sim/src/simctl.rs Adds timed public simctl operations, output draining, parsing, and screenshot cleanup; inherited Xcode selection variables can disable a valid installation.
apps/desktop/scripts/stage-sidecar.mts Builds and stages both sidecars for native and cross-architecture macOS packages.
apps/desktop/electron-builder.yml Packages the architecture-specific sidecar directory as external executable resources.

Sequence Diagram

sequenceDiagram
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
Loading

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) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Oversized Capture Stops All Responses

A high-resolution PNG can make the encoded screenshot exceed the 16 MiB frame limit. The body is still queued as a success, so write_frame rejects it and the sole writer exits; this request and every later request then receive no response and time out.

match serde_json::from_slice::<Request>(&body) {
Ok(request) => {
let tx = tx.clone();
thread::spawn(move || serve(request, &tx));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment on lines +80 to +81
let _ = tx.send(OutMsg::Stop);
let _ = writer.join();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

View artifacts

T-Rex Ran code and verified through T-Rex

Comment on lines +50 to +51
let mut find_simctl = Command::new(XCRUN);
find_simctl.args(["--find", "simctl"]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

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