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
impl SimctlClient
Sourcepub fn new() -> Self
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).
Sourcepub fn with_screenshot_pacer(self, config: ScreenshotPacerConfig) -> Self
pub fn with_screenshot_pacer(self, config: ScreenshotPacerConfig) -> Self
Override the screenshot pacer with a custom config.
Since smix 1.0.4.
Sourcepub fn with_sim_health(self, monitor: SimHealthMonitor) -> Self
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.
Sourcepub async fn list_runtimes(&self) -> Result<Vec<SimctlRuntime>, SimctlError>
pub async fn list_runtimes(&self) -> Result<Vec<SimctlRuntime>, SimctlError>
xcrun simctl list runtimes -j → Vec<SimctlRuntime>.
Sourcepub async fn list_devices(&self) -> Result<Vec<SimctlDevice>, SimctlError>
pub async fn list_devices(&self) -> Result<Vec<SimctlDevice>, SimctlError>
xcrun simctl list devices -j → flattened Vec<SimctlDevice>.
Sourcepub async fn boot(&self, udid: &str) -> Result<(), SimctlError>
pub async fn boot(&self, udid: &str) -> Result<(), SimctlError>
xcrun simctl boot <udid> — fire-and-forget boot request.
Sourcepub async fn shutdown(&self, udid: &str) -> Result<(), SimctlError>
pub async fn shutdown(&self, udid: &str) -> Result<(), SimctlError>
xcrun simctl shutdown <udid>.
Sourcepub async fn current_locale(
&self,
udid: &str,
) -> Result<Option<String>, SimctlError>
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.
Sourcepub async fn user_defaults_delete(
&self,
udid: &str,
bundle_id: &str,
key: &str,
) -> Result<bool, SimctlError>
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.
Sourcepub async fn set_locale(
&self,
udid: &str,
locale: &str,
) -> Result<(), SimctlError>
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.
Sourcepub async fn boot_and_wait(
&self,
udid: &str,
timeout: Duration,
) -> Result<(), SimctlError>
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).
Sourcepub async fn erase(&self, udid: &str) -> Result<(), SimctlError>
pub async fn erase(&self, udid: &str) -> Result<(), SimctlError>
xcrun simctl erase <udid> — wipe device contents.
Sourcepub async fn install(
&self,
udid: &str,
app_path: &str,
) -> Result<(), SimctlError>
pub async fn install( &self, udid: &str, app_path: &str, ) -> Result<(), SimctlError>
xcrun simctl install <udid> <app-path> — install a .app bundle.
Sourcepub async fn uninstall(
&self,
udid: &str,
bundle_id: &str,
) -> Result<(), SimctlError>
pub async fn uninstall( &self, udid: &str, bundle_id: &str, ) -> Result<(), SimctlError>
xcrun simctl uninstall <udid> <bundle-id>.
Sourcepub async fn terminate(
&self,
udid: &str,
bundle_id: &str,
) -> Result<(), SimctlError>
pub async fn terminate( &self, udid: &str, bundle_id: &str, ) -> Result<(), SimctlError>
xcrun simctl terminate <udid> <bundle-id> — kill a running app.
Sourcepub async fn launch(
&self,
udid: &str,
bundle_id: &str,
) -> Result<LaunchResult, SimctlError>
pub async fn launch( &self, udid: &str, bundle_id: &str, ) -> Result<LaunchResult, SimctlError>
xcrun simctl launch <udid> <bundleId> → parse "<bundle>: <pid>".
Sourcepub async fn launch_with_args(
&self,
udid: &str,
bundle_id: &str,
args: &[String],
) -> Result<LaunchResult, SimctlError>
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.
Sourcepub async fn launch_with_args_and_env(
&self,
udid: &str,
bundle_id: &str,
args: &[String],
child_env: &[(&str, &str)],
) -> Result<LaunchResult, SimctlError>
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.
Sourcepub async fn privacy_reset_all(
&self,
udid: &str,
bundle_id: &str,
) -> Result<(), SimctlError>
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).
Sourcepub async fn clear_app_sandbox(
&self,
udid: &str,
bundle_id: &str,
) -> Result<(), SimctlError>
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.
Sourcepub async fn open_url(&self, udid: &str, url: &str) -> Result<(), SimctlError>
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
impl SimctlClient
Sourcepub async fn send_push(
&self,
udid: &str,
bundle_id: &str,
apns_json_path: &str,
) -> Result<(), SimctlError>
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.
Sourcepub async fn set_appearance(
&self,
udid: &str,
mode: Appearance,
) -> Result<(), SimctlError>
pub async fn set_appearance( &self, udid: &str, mode: Appearance, ) -> Result<(), SimctlError>
xcrun simctl ui <udid> appearance <light|dark> — set UI appearance.
Sourcepub async fn grant_permission(
&self,
udid: &str,
permission: SimctlPermission,
bundle_id: &str,
) -> Result<(), SimctlError>
pub async fn grant_permission( &self, udid: &str, permission: SimctlPermission, bundle_id: &str, ) -> Result<(), SimctlError>
xcrun simctl privacy <udid> grant <perm> <bundle-id>.
Sourcepub async fn revoke_permission(
&self,
udid: &str,
permission: SimctlPermission,
bundle_id: &str,
) -> Result<(), SimctlError>
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”.
Sourcepub async fn location_set(
&self,
udid: &str,
latitude: f64,
longitude: f64,
) -> Result<(), SimctlError>
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.
Sourcepub async fn location_start(
&self,
udid: &str,
points: &[(f64, f64)],
speed_mps: Option<f64>,
) -> Result<(), SimctlError>
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.
Sourcepub async fn location_clear(&self, udid: &str) -> Result<(), SimctlError>
pub async fn location_clear(&self, udid: &str) -> Result<(), SimctlError>
xcrun simctl location <udid> clear — reset active location
scenario.
Sourcepub async fn add_media(
&self,
udid: &str,
paths: &[String],
) -> Result<(), SimctlError>
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).
Sourcepub async fn record_video_start(
&self,
udid: &str,
path: &str,
) -> Result<RecordingHandle, SimctlError>
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.
Sourcepub async fn record_video_stop(
&self,
handle: RecordingHandle,
) -> Result<(), SimctlError>
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.
Sourcepub async fn reset_permission(
&self,
udid: &str,
permission: SimctlPermission,
bundle_id: &str,
) -> Result<(), SimctlError>
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.
Sourcepub async fn keychain_reset(&self, udid: &str) -> Result<(), SimctlError>
pub async fn keychain_reset(&self, udid: &str) -> Result<(), SimctlError>
xcrun simctl keychain <udid> reset — clear all keychain entries.
Sourcepub async fn pasteboard_get(&self, udid: &str) -> Result<String, SimctlError>
pub async fn pasteboard_get(&self, udid: &str) -> Result<String, SimctlError>
xcrun simctl pbpaste <udid> — read clipboard contents.
Sourcepub async fn pasteboard_set(
&self,
udid: &str,
text: &str,
) -> Result<(), SimctlError>
pub async fn pasteboard_set( &self, udid: &str, text: &str, ) -> Result<(), SimctlError>
xcrun simctl pbcopy <udid> — write clipboard contents (via piped stdin).
Sourcepub async fn set_reduce_motion(
&self,
udid: &str,
enabled: bool,
) -> Result<(), SimctlError>
pub async fn set_reduce_motion( &self, udid: &str, enabled: bool, ) -> Result<(), SimctlError>
Toggle “Reduce Motion” accessibility setting via defaults write.
Sourcepub async fn screenshot(&self, udid: &str) -> Result<Vec<u8>, SimctlError>
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.
Sourcepub async fn create_device(
&self,
name: &str,
device_type: &str,
runtime_id: &str,
) -> Result<String, SimctlError>
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.
Sourcepub async fn delete_device(&self, udid: &str) -> Result<(), SimctlError>
pub async fn delete_device(&self, udid: &str) -> Result<(), SimctlError>
xcrun simctl delete <udid> — delete a simulator device.