Skip to content

fix(forward): reconnect automatically after device replug (ios forward) - #804

Open
danielpaulus wants to merge 2 commits into
mainfrom
fix/issue-378-forward-reconnect
Open

fix(forward): reconnect automatically after device replug (ios forward)#804
danielpaulus wants to merge 2 commits into
mainfrom
fix/issue-378-forward-reconnect

Conversation

@danielpaulus

Copy link
Copy Markdown
Owner

Problem

ios forward <hostPort> <phonePort> stops working after the device is unplugged and plugged back in. Every new client connection fails with could not connect to phone until the forward process is restarted. iproxy from libimobiledevice handles a replug in the same session.

Root cause

Forward() captures the numeric usbmux DeviceID once at startup and every proxied connection dials usbmuxd with it (forward.go, previously line 95-97). usbmuxd assigns a new device id each time a device is plugged in, so after a replug the captured id is stale and every Connect request is answered with a mux error. The device's udid (serial) is stable across replugs — only the numeric id changes.

Fix

  • Each accepted client connection still dials with the captured id first (zero extra cost on the happy path).
  • When that connect fails, the device is re-resolved by udid via an injected resolveDeviceID func (backed by ios.GetDevice(udid)), and the connect is retried once with the fresh id.
  • The usbmux socket is now closed when Connect fails (previously it leaked on that path).
  • The exported StartNewProxyConnection keeps its signature and behavior (no resolver); the resolver is wired in by Forward().

Options considered

  1. Re-resolve by udid on connect failure, retry once (chosen). Happy path unchanged, one extra usbmuxd round trip only while the id is stale, no background goroutines or listeners.
  2. Re-resolve on every accepted connection. Simpler control flow but adds a ListDevices round trip to every connection even when nothing changed.
  3. Subscribe to usbmuxd attach/detach events and update the id. Most "live", but adds a long-lived listener connection and state shared across goroutines for a case the retry handles with less machinery.

Test plan

Device-free unit tests (ios/forward/forward_reconnect_test.go) run a minimal fake usbmuxd on a temp unix socket (USBMUXD_SOCKET_ADDRESS) that rejects a stale id with mux error 2 and echoes bytes for the current id:

  • TestStaleDeviceIDIsReResolved — reproduces ios forward not reconnecting automatically if device disconnected and reconnected #378: stale id triggers exactly one re-resolve, and bytes flow end-to-end afterwards. Does not compile/pass without this change.
  • TestCurrentDeviceIDConnectsWithoutResolve — happy path never invokes the resolver.
  • TestStaleDeviceIDWithoutResolverFails — pins the exported StartNewProxyConnection behavior: stale id fails and the client conn is closed.

go build ./..., go test -race ./... and gofmt -l are clean.

Fixes #378

🤖 Generated with Claude Code

https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk

ios forward captured the numeric usbmux device id once at startup. usbmuxd
assigns a fresh id every time a device is plugged in, so after a
disconnect/reconnect every new client connection failed with 'could not
connect to phone' while iproxy kept working in the same session.

Each accepted client connection still dials with the captured id first; when
that connect fails, the device is looked up again by udid (the serial survives
a replug) and the connect is retried once with the fresh id. The retry seam is
an injected resolver func, so the behavior is unit-tested device-free against
a fake usbmuxd on a temp unix socket. Also closes the usbmux socket on a
failed Connect instead of leaking it. The exported StartNewProxyConnection
signature is unchanged.

Fixes #378

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
@danielpaulus

Copy link
Copy Markdown
Owner Author

/test-devices

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🧪 Running real-device tests on PR #804run.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

❌ Real-device tests failed — see run.

Register the temp-dir removal with t.Cleanup immediately after MkdirTemp
so a subsequent net.Listen failure (which t.Fatalf's) can no longer leak
the directory. Split the listener close into its own cleanup so ordering
stays correct (listener closed before its dir is removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8eMENxJ1nec9CeHp4tjWk
@danielpaulus

Copy link
Copy Markdown
Owner Author

Adversarial review (with codex cross-check). Verdict: ship-worthy, one small test-hygiene fix pushed to this branch.

Fix applied (85e4fbb)

  • forward_reconnect_test.go: startFakeUsbmuxd did os.MkdirTemp then net.Listen, but registered the temp-dir cleanup only after the net.Listen check. A net.Listen failure t.Fatalfs before cleanup is set up, leaking the dir. Moved t.Cleanup(os.RemoveAll) immediately after MkdirTemp and split the listener close into its own cleanup so ordering (listener closed before dir removed) is preserved.

Reviewed, no change needed

  • usbmux socket lifecycle: the real leak is fixed — dialPhonePort now calls usbmuxConn.Close() on Connect failure, which the old code never did. The NewUsbMuxConnectionSimple error path correctly does not Close (deviceConn is nil there).
  • resolver-error / stale-id-without-resolver paths close the client conn and are covered by TestStaleDeviceIDWithoutResolverFails.
  • USBMUXD_SOCKET_ADDRESS test hygiene: uses t.Setenv (auto-restored, blocks t.Parallel), no cross-test bleed.

Dismissed (codex flagged, not a bug)

Verified: go build ./..., go test -race ./ios/forward/, gofmt -l all clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ios forward not reconnecting automatically if device disconnected and reconnected

1 participant