Repository navigation
iOS simulator rotation through Simulator.app #1020
Description
Part of #1013.
Scope
After this PR, a rotation sent on WS /v1/leases/{id}/input turns an iOS simulator's screen, if the one-off check passes. If it fails, the PR delivers only a docs line and a follow-up issue, and rotation stays dropped on iOS. Either way, it settles what 0x07 does on iOS.
In short
serve-sim rotates a simulator with a message to a port that exists only while Simulator.app is attached. So this task starts with one check on one worker. The worker starts Simulator.app for a simulator in Simlock's own device set, then sends a rotation. Only if the picture turns does rotation ship.
flowchart TD
C{one-off check: Simulator.app rotates a simulator in Simlock's set?}
C -- yes --> R[0x07 → helper → PurpleWorkspacePort; Simulator.app started on first rotation, left open]
C -- no --> D[docs line + follow-up issue; 0x07 stays dropped]
- one-off check: a single run of
e2e/slow-ios-rotation.test.tson one worker before any rotation code. - A rotation is
0x07 {"orientation":"landscape_left"}. The four names areportrait,portrait_upside_down,landscape_leftandlandscape_right.
If this goes wrong, a person sees a Simulator.app window open on a worker, or a rotation that does nothing.
Technical spec
Facts
Commit e1f2f43.
| # | Fact | Source |
|---|---|---|
| F1 | serve-sim sends a rotation as a GSEvent to the simulator's PurpleWorkspacePort, found with -[SimDevice lookup:error:]. When the port is missing it prints "Simulator.app must be running." |
EvanBacon/serve-sim v6d009686:packages/serve-sim/Sources/SimNative/HIDInjector.swift:649-700 |
| F2 | The orientation values are portrait 1, upside down 2, landscape right 3, landscape left 4. | EvanBacon/serve-sim v6d009686:packages/serve-sim/Sources/SimNative/HIDInjector.swift:631-634 |
| F3 | Expo serve-sim maps the four names to those values, and drops an unknown name. | expo/expo-device-hub v1357114:packages/serve-sim/packages/serve-sim/src/device-session.ts:190-195, v1357114:packages/serve-sim/packages/serve-sim/src/device-session.ts:1620-1630 |
| F4 | Simlock's simulators live in their own device set, named to the capture program by SIMLOCK_IOS_DEVICE_SET. |
src/drivers/ios/screen-capture.ts:120-124, vendor/screen-capture/Sources/simlock-screen-capture/FrameCapture.swift:425-436 |
| F5 | The worker runs as a per-user LaunchAgent in the gui/<uid> launchd domain. |
src/ports/service-manager.ts:74, src/ports/service-manager.ts:215-221 |
| F6 | Nothing in src/ starts Simulator.app today: a search for Simulator.app, open -a Simulator, com.apple.iphonesimulator and CurrentDeviceSet finds nothing. |
grep -rn over src/ at e1f2f43, 0 hits |
Modules touched
Searches: sendOrientation, PurpleWorkspacePort, Simulator.app, open -a, 0x07, and #1019's input mode, in src/, vendor/ and e2e/.
If the check passes:
vendor/screen-capture/Sources/simlock-screen-capture/: the input mode takes0x07and callssendOrientation, which is already in the re-vendoredHIDInjector.swift(iOS simulator input through the HID injector #1019).src/drivers/ios/input.ts: maps the name, starts Simulator.app on the first rotation, forwards0x07.src/drivers/ios/AGENTS.md,website/src/content/docs/docs/using-devices/watching-the-screen.mdx.
Either way:
e2e/slow-ios-rotation.test.ts(new): the one-off check, kept as the slow-lane test.
sequenceDiagram
participant M as input module
participant I as iOS device input
participant S as Simulator.app
participant H as helper --input
M->>I: 0x07 {orientation: landscape_left}
I->>S: open -a Simulator (first rotation only)
I->>H: [len][0x07]{orientation: 4}
H->>H: lookup PurpleWorkspacePort, send GSEvent
Note over H: port missing
H-->>I: dropped (logged)
Contract and event changes
none. Searched src/contract/: 0x07 is #1017's core/input.ts. No event.
Other code on the same state
Searches: Simulator.app, iphonesimulator, PurpleWorkspacePort in src/ and vendor/ (0 hits before this task).
- A capture of the same simulator: rotation changes the picture's shape. The slow test checks the next frame; how the capture follows a size change is not changed here.
- agent-device rotating through its own XCUITest runner: whichever runs last wins.
- A person using Simulator.app on the worker's screen: Simlock opens it once and never closes it.
Failure modes
| Call | Outcome | What happens | Limit, counted from |
|---|---|---|---|
open -a Simulator |
ok | Simulator.app runs; rotations go through | 30 s, from the call |
open -a Simulator |
fails or times out | this rotation is dropped and logged; the next one tries again | 30 s, from the call |
helper sendOrientation |
port missing | rotation dropped and logged; socket stays open | 5 s, from the write |
| unknown orientation name | — | dropped, as serve-sim does (F3) | none |
A worker restart leaves Simulator.app running, which is harmless. Two sockets of one lease share one device input (#1017), so two starts never race for one lease. Two leases on one worker may both run open.
Rules in play
- architecture rule 11, "Every wait that crosses a process boundary is bounded":
openand the helper write have limits. - architecture rule 2, "iOS and Android are encapsulated in their own driver modules": Simulator.app is known only to
src/drivers/ios/. - testing rule 7, "Prove each claim once, at the cheapest level that can prove it": the port behaviour is only provable on a Mac, so the check is a slow-lane test, and the mapping is a unit test.
Tests
Seam: createIosDeviceInput with a fake launcher and a scripted helper; model on src/drivers/ios/screen-capture.test.ts.
- A rotation on an iOS simulator turns the screen through Simulator.app, started on first use: each of the four orientations is written to the helper as 1 to 4; Simulator.app not yet running (started once); start fails (dropped).
Done when
e2e/slow-ios-rotation.test.tshas run once on one worker, and its result is on this PR (slow lane, iOS simulator).- If it passed: the Tests line passes in
pnpm test, the same slow test turns a simulator to landscape and the stream's next frame is wider than it is tall, andwatching-the-screen.mdxsays Simulator.app opens on a worker on the first rotation. - If it failed:
watching-the-screen.mdxsays rotation does nothing on iOS simulators, a follow-up issue records what the check saw, and no rotation code merges.
Out of scope
- Closing Simulator.app.
- Rotation on physical iPhones.
- Rotation through agent-device.
Depends on
Written by an agent.
Activity
V3RON commentedon Oct 26, 2026
Closed: rotation left #1013 on both platforms. On Xcode 27 neither Simulator.app (not shipped) nor CoreDevice's vendor channel (cannot see a custom device set; no effect on a non-foldable iPhone) rotates a Simlock simulator. The experiments are recorded in #1024.
Written by an agent.