Skip to main content

SimctlClient

Struct SimctlClient 

Source
pub struct SimctlClient { /* private fields */ }
Expand description

Stateless wrapper around xcrun simctl. Methods are free functions in spirit (no instance state beyond optionally-cached xcrun path); kept as a struct for API ergonomics + future caching.

Since v1.0.4, the client also holds a ScreenshotPacer that throttles xcrun simctl io screenshot under high-frequency load. Defaults are conservative (100 ms interval floor); consumers whose flows are already loose are unaffected.

Implementations§

Source§

impl SimctlClient

Source

pub fn new() -> Self

Construct a new client with default screenshot pacing (100 ms interval floor, adaptive slow-path lift to 1500 ms, circuit breaker on ≥ 1500 ms walls or failures).

Source

pub fn with_screenshot_pacer(self, config: ScreenshotPacerConfig) -> Self

Override the screenshot pacer with a custom config.

Since smix 1.0.4.

Source

pub fn with_sim_health(self, monitor: SimHealthMonitor) -> Self

Attach a smix_sim_health::SimHealthMonitor to receive screenshot wall-time observations from every screenshot call. Composes with the pacer — the pacer still enforces its interval / circuit locally, and the monitor sees the same walls for global state classification.

Since smix 1.0.4.

Source

pub async fn list_runtimes(&self) -> Result<Vec<SimctlRuntime>, SimctlError>

xcrun simctl list runtimes -jVec<SimctlRuntime>.

Source

pub async fn list_devices(&self) -> Result<Vec<SimctlDevice>, SimctlError>

xcrun simctl list devices -j → flattened Vec<SimctlDevice>.

Source

pub async fn boot(&self, udid: &str) -> Result<(), SimctlError>

xcrun simctl boot <udid> — fire-and-forget boot request.

Source

pub async fn shutdown(&self, udid: &str) -> Result<(), SimctlError>

xcrun simctl shutdown <udid>.

Source

pub async fn current_locale( &self, udid: &str, ) -> Result<Option<String>, SimctlError>

Read the sim’s current BCP-47 locale (first entry of NSGlobalDomain AppleLanguages). Returns Ok(None) when the preference is unset (defaults read exits non-zero) or unparseable. Wire format: simctl spawn <udid> defaults read -g AppleLanguages stdout looks like "(\n \"en-US\"\n)\n"; we extract the first quoted token.

Source

pub async fn user_defaults_delete( &self, udid: &str, bundle_id: &str, key: &str, ) -> Result<bool, SimctlError>

v1.0.27 — delete a single key from an app’s NSUserDefaults domain via simctl spawn <udid> defaults delete <bundleId> <key>. Running defaults INSIDE the sim (spawn) goes through the sim’s cfprefsd, so the deletion is coherent with what the app reads on next launch (editing the container plist from the host would race cfprefsd’s cache).

Returns Ok(true) when the key existed and was deleted, Ok(false) when the key (or the whole domain) was absent — the verb contract is “ensure key absent”, so an already-absent key is success, not an error. Any other failure surfaces as the underlying SimctlError.

Motivating case (insight round-5 Ask 12): expo-dev-launcher persists the most recent deep link and re-delivers it after every JS bundle load; deleting its storage key between terminate and relaunch neutralizes the replay at the source.

Terminate the app first — a running process has its defaults cached in-memory and may rewrite the key at exit.

Source

pub async fn set_locale( &self, udid: &str, locale: &str, ) -> Result<(), SimctlError>

Write AppleLanguages (array) + AppleLocale (scalar) to the sim’s NSGlobalDomain so SpringBoard + apps re-localize on next launch. AppleLocale is BCP-47 with hyphen replaced by underscore (en_US); AppleLanguages is the BCP-47 tag verbatim. The caller must shutdown + reboot the sim for the change to take effect — running apps cache the locale at process start.

Source

pub async fn boot_and_wait( &self, udid: &str, timeout: Duration, ) -> Result<(), SimctlError>

Boot + poll device state == “Booted” within timeout. Tries every 500 ms until success or timeout_ms elapses. Idempotent on already-booted devices (xcrun simctl boot returns non-zero when the device is already booted; we swallow that).

Source

pub async fn erase(&self, udid: &str) -> Result<(), SimctlError>

xcrun simctl erase <udid> — wipe device contents.

Source

pub async fn install( &self, udid: &str, app_path: &str, ) -> Result<(), SimctlError>

xcrun simctl install <udid> <app-path> — install a .app bundle.

Source

pub async fn uninstall( &self, udid: &str, bundle_id: &str, ) -> Result<(), SimctlError>

xcrun simctl uninstall <udid> <bundle-id>.

Source

pub async fn terminate( &self, udid: &str, bundle_id: &str, ) -> Result<(), SimctlError>

xcrun simctl terminate <udid> <bundle-id> — kill a running app.

Source

pub async fn launch( &self, udid: &str, bundle_id: &str, ) -> Result<LaunchResult, SimctlError>

xcrun simctl launch <udid> <bundleId> → parse "<bundle>: <pid>".

Source

pub async fn launch_with_args( &self, udid: &str, bundle_id: &str, args: &[String], ) -> Result<LaunchResult, SimctlError>

xcrun simctl launch <udid> <bundleId> -- <arg>... — launch with a process-level argument vector. Empty args is equivalent to Self::launch. Mirrors maestro yaml launchApp.arguments.

Source

pub async fn launch_with_args_and_env( &self, udid: &str, bundle_id: &str, args: &[String], child_env: &[(&str, &str)], ) -> Result<LaunchResult, SimctlError>

Like Self::launch_with_args but also sets SIMCTL_CHILD_* envp on the simctl process so the launched app can read deploy-time vars via ProcessInfo().environment["KEY"]. child_env keys without the SIMCTL_CHILD_ prefix get it added automatically (per compose_child_env semantics). Useful for prelaunching an app before any openLink so iOS treats the subsequent URL handoff as in-app routing instead of cross-app, side-stepping the SpringBoard “Open in ‘<App>’?” confirmation dialog.

Source

pub async fn privacy_reset_all( &self, udid: &str, bundle_id: &str, ) -> Result<(), SimctlError>

v1.0.4 §D12 — reset every privacy permission granted to bundle_id on the sim: xcrun simctl privacy <udid> reset all <bundle-id>. Companion to Self::clear_app_sandbox on the in-place launchApp: clearState: true path that replaces simctl uninstall + install (which triggers iOS 26.5 XCUITest binding loss + ReportCrash’s “Insight quit unexpectedly” dialog).

Source

pub async fn clear_app_sandbox( &self, udid: &str, bundle_id: &str, ) -> Result<(), SimctlError>

v1.0.4 §D12 — wipe the app’s sandbox on the sim: locate the Data container via simctl get_app_container <udid> <bundle> data, then simctl spawn <udid> rm -rf <container>/Documents <container>/Library <container>/tmp. The app remains installed (no simctl uninstall), so the XCUITest binding is preserved and macOS ReportCrash does not misinterpret a missing install-receipt as a crash.

Source

pub async fn open_url(&self, udid: &str, url: &str) -> Result<(), SimctlError>

xcrun simctl openurl <udid> <url> — open a URL on the device.

URL bytes are passed to xcrun simctl verbatim — no parsing, no percent-encoding rewrite, no query-string stripping. Verified by [openurl_argv] (test-visible helper) and its unit test asserting query-params like ?url=http%3A%2F%2Flocalhost%3A8081 reach the argv byte-for-byte. Feedback §G verification — if the target app’s URL router (e.g. expo-dev-client 57.0.5) shows a picker instead of auto-connecting, the URL made it through smix intact and the finding lives on the URL-router side.

Source§

impl SimctlClient

Source

pub async fn send_push( &self, udid: &str, bundle_id: &str, apns_json_path: &str, ) -> Result<(), SimctlError>

xcrun simctl push <udid> <bundle-id> <apns-json-path>. Deliver an APNS payload to a sim-installed app. The payload file is a JSON document whose top-level dictionary mirrors what an APNS provider would send; aps.alert.body / aps.alert.title surface as banner content and reach the app’s UNUserNotificationCenterDelegate.

Source

pub async fn set_appearance( &self, udid: &str, mode: Appearance, ) -> Result<(), SimctlError>

xcrun simctl ui <udid> appearance <light|dark> — set UI appearance.

Source

pub async fn grant_permission( &self, udid: &str, permission: SimctlPermission, bundle_id: &str, ) -> Result<(), SimctlError>

xcrun simctl privacy <udid> grant <perm> <bundle-id>.

Source

pub async fn revoke_permission( &self, udid: &str, permission: SimctlPermission, bundle_id: &str, ) -> Result<(), SimctlError>

xcrun simctl privacy <udid> revoke <perm> <bundle-id> — explicitly deny the permission. Mirrors maestro yaml permissions: { x: deny } (the reverse of grant). Distinct from reset, which returns the permission to “not determined”.

Source

pub async fn location_set( &self, udid: &str, latitude: f64, longitude: f64, ) -> Result<(), SimctlError>

xcrun simctl location <udid> set <lat>,<lng> — set sim location to a fixed point. Mirrors maestro setLocation.

Source

pub async fn location_start( &self, udid: &str, points: &[(f64, f64)], speed_mps: Option<f64>, ) -> Result<(), SimctlError>

xcrun simctl location <udid> start [--speed=<m/s>] <waypoints> — interpolate sim location along waypoints. Fire-and-return: simctl injects scenario and returns; sim continues interpolation in background. Mirrors maestro travel.

Source

pub async fn location_clear(&self, udid: &str) -> Result<(), SimctlError>

xcrun simctl location <udid> clear — reset active location scenario.

Source

pub async fn add_media( &self, udid: &str, paths: &[String], ) -> Result<(), SimctlError>

xcrun simctl addmedia <udid> <path>... — add photos / videos / contacts to sim library. Mirrors maestro addMedia (scalar or array form already flattened on adapter side).

Source

pub async fn record_video_start( &self, udid: &str, path: &str, ) -> Result<RecordingHandle, SimctlError>

Start recording sim display to path. Spawns xcrun simctl io <udid> recordVideo <path> as a long-running child; returns handle immediately. Caller must pair with Self::record_video_stop for clean SIGINT-and-wait shutdown — dropping the handle would SIGKILL via tokio + lose mp4 trailer.

Source

pub async fn record_video_stop( &self, handle: RecordingHandle, ) -> Result<(), SimctlError>

Stop a recording via SIGINT + wait (≤10s). SIGINT lets simctl trap and flush the mp4 trailer; SIGKILL would corrupt output. Timeout escalates to SIGKILL with explicit error mentioning truncation.

Source

pub async fn reset_permission( &self, udid: &str, permission: SimctlPermission, bundle_id: &str, ) -> Result<(), SimctlError>

xcrun simctl privacy <udid> reset <perm> <bundle-id> — return the permission to “not determined” so the next request re-prompts. May terminate a running instance of the target app (Apple behavior) — call before launch, not mid-flow.

Source

pub async fn keychain_reset(&self, udid: &str) -> Result<(), SimctlError>

xcrun simctl keychain <udid> reset — clear all keychain entries.

Source

pub async fn pasteboard_get(&self, udid: &str) -> Result<String, SimctlError>

xcrun simctl pbpaste <udid> — read clipboard contents.

Source

pub async fn pasteboard_set( &self, udid: &str, text: &str, ) -> Result<(), SimctlError>

xcrun simctl pbcopy <udid> — write clipboard contents (via piped stdin).

Source

pub async fn set_reduce_motion( &self, udid: &str, enabled: bool, ) -> Result<(), SimctlError>

Toggle “Reduce Motion” accessibility setting via defaults write.

Source

pub async fn screenshot(&self, udid: &str) -> Result<Vec<u8>, SimctlError>

xcrun simctl io <udid> screenshot <tmpfile> → raw PNG bytes, with a byte-level sRGB metadata splice if the produced PNG lacks an sRGB chunk.

Goes through a temp file: current Xcode’s screenshot - does not treat - as stdout — it writes a literal file named - in cwd and emits nothing on stdout (observed on Xcode/iOS 26.5).

Pixel-preservation invariant: the returned bytes are byte-identical to whatever simctl io screenshot wrote to disk EXCEPT for one narrow case — if the PNG does not carry an sRGB ancillary chunk (observed on iOS 26.5 sub-builds mid-2026), a 13-byte sRGB chunk is spliced in immediately before the first IDAT. Pixel data (IDAT bytes) is never decoded or modified. See ensure_srgb_chunk for the exact splice operation.

Source

pub async fn create_device( &self, name: &str, device_type: &str, runtime_id: &str, ) -> Result<String, SimctlError>

xcrun simctl create <name> <device-type-id> <runtime-id> → udid.

Source

pub async fn delete_device(&self, udid: &str) -> Result<(), SimctlError>

xcrun simctl delete <udid> — delete a simulator device.

Trait Implementations§

Source§

impl Debug for SimctlClient

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for SimctlClient

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.