Manages port tunnels for Android and physical iOS devices.
Android (adb): adb reverse tcp:4369 tcp:4369 — Android BEAM registers in Mac's EPMD adb forward tcp:<dist> tcp:<dist> — Mac reaches the device's dist port (1:1)
Physical iOS (direct networking — USB preferred, WiFi/LAN fallback): mob_beam.m finds the device's own IP via getifaddrs() and starts the BEAM as mob_qa_ios@<device-ip>. The in-process EPMD binds 0.0.0.0:4369 so Mac can query it at <device-ip>:4369. The dist port is directly reachable.
iOS simulator: Shares Mac network stack — no tunnels needed.
Dist ports are keyed by device serial, not run index
The Mac runs ONE EPMD (port 4369) that every device — across every project
and every mix mob.connect run — registers into. Assigning dist ports by
per-run index (9100 + index) meant project A's device-0 and project B's
device-0 both claimed 9100: two nodes at the same port in the shared EPMD,
but adb forward tcp:9100 can only point at one device → the other resolved
to the wrong phone or nothing (silent timeout). Now the port is derived from
the device serial (serial_base_port/1, a crc32 hash into 9100..9899), so a
given phone always gets the same unique port regardless of project/run, and
assign_dist_port/2 bumps past any port another live node/forward already
holds (cross-project or hash collision).
Summary
Functions
The serial's base port, bumped to the next free slot if in_use already
claims it (a cross-project collision or a crc32 hash collision between two
serials). Walks the window from the base; falls back to the base if the whole
window is somehow taken. Pure — in_use is gathered by the caller.
Stable, deterministic dist port for a device serial — a crc32 hash into
[9100, 9100 + 800). Same serial → same port across runs and projects, so
the port a device is deployed to listen on matches what mix mob.connect
later forwards to.
Assigns a serial-derived dist port and sets up tunnels for a device.
Tears down tunnels for a device.
Functions
@spec assign_dist_port(String.t(), MapSet.t()) :: pos_integer()
The serial's base port, bumped to the next free slot if in_use already
claims it (a cross-project collision or a crc32 hash collision between two
serials). Walks the window from the base; falls back to the base if the whole
window is somehow taken. Pure — in_use is gathered by the caller.
@spec serial_base_port(String.t()) :: pos_integer()
Stable, deterministic dist port for a device serial — a crc32 hash into
[9100, 9100 + 800). Same serial → same port across runs and projects, so
the port a device is deployed to listen on matches what mix mob.connect
later forwards to.
@spec setup(MobDev.Device.t()) :: {:ok, MobDev.Device.t()} | {:error, String.t()}
Assigns a serial-derived dist port and sets up tunnels for a device.
Cleans the device's own stale forwards first, then picks a port that no other
live node/forward on this Mac is using. Returns {:ok, %Device{}} with
dist_port (and host_ip for USB iOS) filled in, or {:error, reason}.
@spec teardown(MobDev.Device.t()) :: :ok
Tears down tunnels for a device.