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