MobDev.Discovery.IOS (mob_dev v0.5.4)

Copy Markdown View Source

Discovers iOS simulators via xcrun simctl.

Physical iOS device support requires libimobiledevice (ideviceinfo, iproxy). Best-effort: works if tools are installed, degrades gracefully if not.

Summary

Functions

Returns the IPv4 addresses every connected physical device is known to reach Mac at, derived from xcrun devicectl list devices --json-output. Sources, in order

Enables the iOS accessibility system for the given simulator (or "booted").

Queries EPMD at a specific IP for any *_ios node and returns a Device, or nil if no iOS BEAM node is reachable there. Used for direct connection when the IP is already known (e.g. from xcrun devicectl) and ARP may not be warm.

Launches the app on a booted simulator.

Returns all iOS devices (simulators + physical).

Returns connected physical iOS devices.

Returns booted iOS simulators.

Parses a CoreSimulator runtime key into a human-readable version string. Exposed for testing.

Parses the JSON output of xcrun simctl list devices booted --json. Exposed for testing.

Parses the plain-text output of xcrun simctl list devices booted. Exposed for testing.

Restarts the app on a physical iOS device via xcrun devicectl. Kills any other user-installed app first (they all share EPMD port 4369 and only one can run at a time), then launches the target app fresh.

Functions

devicectl_ipv4_addresses()

@spec devicectl_ipv4_addresses() :: [String.t()]

Returns the IPv4 addresses every connected physical device is known to reach Mac at, derived from xcrun devicectl list devices --json-output. Sources, in order:

  1. connectionProperties.tunnelIPAddress if it's an IPv4 (CoreDevice USB tunnel; sometimes IPv6, which Erlang dist doesn't speak)
  2. connectionProperties.localHostnames resolved via :inet.gethostbyname/1 (mDNS hostnames like Kevins-iPhone.coredevice.local, which usually resolve to the device's WiFi IPv4)

Returns [] if xcrun isn't installed, the JSON parse fails, or no device has any IPv4. Pure of side effects beyond the temp file used to capture devicectl's JSON output.

enable_accessibility(udid)

@spec enable_accessibility(String.t()) :: :ok

Enables the iOS accessibility system for the given simulator (or "booted").

SwiftUI lazily populates its accessibility tree only when an accessibility service is active. pegleg_nif:ui_tree/0 requires this to be called once per simulator session before it can return elements. Writes the VoiceOver preference into the simulator's preference store and posts the Darwin notification that UIKit listens to.

Safe to call repeatedly — idempotent.

find_physical_at(ip)

@spec find_physical_at(String.t()) :: MobDev.Device.t() | nil

Queries EPMD at a specific IP for any *_ios node and returns a Device, or nil if no iOS BEAM node is reachable there. Used for direct connection when the IP is already known (e.g. from xcrun devicectl) and ARP may not be warm.

launch_app(udid, bundle_id, opts \\ [])

@spec launch_app(String.t(), String.t(), keyword()) :: {String.t(), non_neg_integer()}

Launches the app on a booted simulator.

Passes two env vars through to the simulator app via simctl's SIMCTL_CHILD_* mechanism (the prefix is stripped before delivery):

  • MOB_DIST_PORT — Erlang dist listen port
  • MOB_SIM_RUNTIME_DIR — directory the OTP runtime was written to, so mob_beam.m reads from the same place ios/build.sh wrote. Resolved by MobDev.Paths.sim_runtime_dir/1.

list_devices()

@spec list_devices() :: [MobDev.Device.t()]

Returns all iOS devices (simulators + physical).

list_physical()

@spec list_physical() :: [MobDev.Device.t()]

Returns connected physical iOS devices.

Always runs both USB discovery (ideviceinfo) and a LAN EPMD scan in parallel. The LAN scan finds the device's actual node IP (which is WiFi-first since mob_beam.m prefers a stable LAN address). The USB scan provides the UDID and device name. Results are merged: one device with the correct WiFi IP and full USB metadata.

If only one path finds the device, that result is used directly — so this works on USB-only setups and WiFi-only setups equally. When USB finds a device but LAN scan doesn't (cold ARP, rapid app launch, etc.), the result is enriched via xcrun devicectl — we ask for the device's known hostnames + tunnel IPs, resolve to IPv4, and probe each with EPMD. Single TCP probe per candidate, so it costs ~50 ms in the success case and doesn't slow down the no-iOS path.

list_simulators()

@spec list_simulators() :: [MobDev.Device.t()]

Returns booted iOS simulators.

parse_runtime_version(runtime)

@spec parse_runtime_version(String.t()) :: String.t()

Parses a CoreSimulator runtime key into a human-readable version string. Exposed for testing.

parse_simctl_json(json_string)

@spec parse_simctl_json(String.t()) :: [MobDev.Device.t()]

Parses the JSON output of xcrun simctl list devices booted --json. Exposed for testing.

parse_simctl_text(output)

@spec parse_simctl_text(String.t()) :: [MobDev.Device.t()]

Parses the plain-text output of xcrun simctl list devices booted. Exposed for testing.

restart_app_physical(udid, bundle_id)

@spec restart_app_physical(String.t(), String.t()) :: {String.t(), non_neg_integer()}

Restarts the app on a physical iOS device via xcrun devicectl. Kills any other user-installed app first (they all share EPMD port 4369 and only one can run at a time), then launches the target app fresh.

terminate_app(udid, bundle_id)

@spec terminate_app(String.t(), String.t()) :: {String.t(), non_neg_integer()}