MobDev.Tunnel (mob_dev v0.7.22)

Copy Markdown View Source

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

assign_dist_port(serial, app, in_use \\ MapSet.new())

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

attach_forward(serial, port)

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

base_port(serial, app)

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

dist_port_for(device)

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

ensure_android(serial, port)

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

epmd_names()

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

setup(device)

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

teardown(device)

@spec teardown(MobDev.Device.t()) :: :ok

Tears down tunnels for a device.