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 and app, 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). Keying on the serial alone
then gave two apps on the SAME device the same port, and the second one's
dist failed with :nodistribution. Now the port is derived from the device
serial and the app name (base_port/2, a crc32 hash into 9100..9899), so a
given app on a given device always gets the same port regardless of run, and
assign_dist_port/3 bumps past any port another live node or another
device's forward already holds (a hash collision or a non-mob node).
Summary
Functions
base_port/2, bumped to the next free slot if in_use already claims it (a
crc32 collision with another app/device, or a node mob_dev didn't start).
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.
Stable, deterministic dist port for app on the device serial — a crc32
hash of both into [9100, 9100 + 800). Same serial and app → same port
across runs; two apps on one device → (almost always) different ports.
The dist port this project's app uses on device: assign_dist_port/3 over
the live state of this Mac (EPMD and adb forward --list), ignoring the
app's own node and the device's own forwards so a redeploy reclaims its port.
mix mob.deploy and mix mob.connect both resolve the port through here, so
they agree.
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.
Assigns the device's dist port for the current project's app and sets up tunnels for it.
Tears down tunnels for a device.
Functions
@spec assign_dist_port(String.t(), String.t(), MapSet.t()) :: pos_integer()
base_port/2, bumped to the next free slot if in_use already claims it (a
crc32 collision with another app/device, or a node mob_dev didn't start).
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 base_port(String.t(), String.t()) :: pos_integer()
Stable, deterministic dist port for app on the device serial — a crc32
hash of both into [9100, 9100 + 800). Same serial and app → same port
across runs; two apps on one device → (almost always) different ports.
@spec dist_port_for(MobDev.Device.t()) :: pos_integer()
The dist port this project's app uses on device: assign_dist_port/3 over
the live state of this Mac (EPMD and adb forward --list), ignoring the
app's own node and the device's own forwards so a redeploy reclaims its port.
mix mob.deploy and mix mob.connect both resolve the port through here, so
they agree.
@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 setup(MobDev.Device.t()) :: {:ok, MobDev.Device.t()} | {:error, String.t()}
Assigns the device's dist port for the current project's app and sets up tunnels for it.
Cleans this device's stale dist forwards first (never one a live node of
another app on the same device is using), then picks a port that no other
live node or other device's 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.