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