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 — WiFi/LAN preferred, USB link-local fallback):
mob_beam.m finds the device's own IP via getifaddrs() and starts the BEAM
as <app>_ios@<device-ip>. The in-process EPMD binds 0.0.0.0:4369 so Mac
can query it at any of the device's IPs. The dist port is directly reachable.
A USB-discovered device's node is read from EPMD, not predicted
(MobDev.Discovery.IOS.resolve_usb_node/1).
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.
Forwards host port to the same port on serial, to reach a node that is
already running there. Refuses ({:error, _}) when the host port already
forwards to a different device, rather than taking it from that session.
Makes sure an Android app started now can join distribution: adb reverse
for EPMD (so the device BEAM registers in the Mac's EPMD) and a forward of
the dist port. Both are gone after an emulator reboot or an adbd restart,
and an app launched without them gives up on dist after 10 s. Idempotent.
The {name, port} pairs registered in the Mac's EPMD, which every Android
device and iOS simulator registers into. Empty when EPMD isn't reachable.
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 attach_forward(String.t(), pos_integer()) :: :ok | {:error, String.t()}
Forwards host port to the same port on serial, to reach a node that is
already running there. Refuses ({:error, _}) when the host port already
forwards to a different device, rather than taking it from that session.
@spec ensure_android(String.t(), pos_integer()) :: :ok | {:error, String.t()}
Makes sure an Android app started now can join distribution: adb reverse
for EPMD (so the device BEAM registers in the Mac's EPMD) and a forward of
the dist port. Both are gone after an emulator reboot or an adbd restart,
and an app launched without them gives up on dist after 10 s. Idempotent.
@spec epmd_names() :: [{String.t(), pos_integer()}]
The {name, port} pairs registered in the Mac's EPMD, which every Android
device and iOS simulator registers into. Empty when EPMD isn't reachable.
@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.