feat(provider)!: run usbmuxd inside the container - #41
Closed
Dinip wants to merge 4 commits into
Closed
Conversation
libusbmuxd documents the unix form as `UNIX:/path`, which contains a colon — so it was read as host:port, failed to resolve, and fell back to the default socket, silently ignoring the one setting that had been made. Only a bare path worked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bind-mounting the host's /var/run/usbmuxd socket is a trap in both directions. Docker binds the socket's inode, so the mount goes stale the moment usbmuxd exits at the last unplugged device — and if the socket is missing when the container starts, Docker creates a *directory* at that path, after which the host's udev-triggered usbmuxd can never bind it again. Unplugging one iPhone took iOS down until someone cleaned up by hand. So the container runs its own, the way it already runs its own adb server, and the host masks its copy. usbmuxd is supervised in a restart loop rather than fire-and-forget, since exiting at the last device is its normal behaviour. Hotplug inside a container needs a nudge: usbmuxd polls the bus each second only until it registers for libusb hotplug events, and that registration succeeds in a container while delivering nothing — libusb on Debian watches udevd's netlink broadcast, which reaches only udevd's own netns. It runs as `usbmuxd -f -z` with a SIGUSR2 every two seconds, which means "rescan the bus": the same device-list walk the poll it replaces was doing. `-n` is not the answer, since disabling hotplug disables the poll along with it. /var/lib/lockdown is a volume for the same reason /root/.android is. It holds the SystemBUID/HostID the host pairs under and one pair record per device, so an empty one is a new computer to every iPhone in the rack — each asking to Trust This Computer again, by hand, at the device. PROVIDER_START_ADB and PROVIDER_START_USBMUXD gate the two daemons, defaulting to `auto`: start it if /dev/bus/usb is mounted, so the macOS shape still points at the host's daemons untouched. Setting one to `no` splits a host into a provider per platform, which does not contend for the bus — a claim is per USB interface, and adb and usbmuxd open disjoint devices. BREAKING CHANGE: the provider's env vars lose the YARD_ prefix, since the entrypoint's own knobs are not the binary's config. YARD_PROVIDER_TOKEN is now PROVIDER_TOKEN, YARD_LOG is PROVIDER_LOG_LEVEL and YARD_CONFIG is PROVIDER_CONFIG. A provider host's .env needs renaming before the next pull; an unset PROVIDER_TOKEN fails at startup rather than at the first request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unplugging an iPhone and plugging it back in took it out until the container was restarted: usbmuxd sat retrying the address the device used to be at, logging LIBUSB_ERROR_IO with errno=19 every rescan, and never noticed the one it came back as. The SIGUSR2 rescan cannot fix that, because the list it walks is libusb's, and libusb's is frozen. It learns about a plug or an unplug from udevd's netlink broadcast, which reaches only udevd's own network namespace — so inside the container the list is whatever the bus held when usbmuxd started, forever. Only a fresh process sees the bus as it now is; hotplug appeared to work before only because usbmuxd ran with -z and exited at the last unplug, which the restart loop then papered over. So the watchdog watches sysfs, which is the host's and always current: every /sys/bus/usb/devices entry with Apple's vendor ID plus its devnum, since re-enumeration is what a replug looks like. A change restarts usbmuxd. Because that costs every other iPhone its connection, a change has to survive one more tick before it counts — a re-enumerating device passes through intermediate states that would otherwise each be their own restart. -z goes with the nudge: supervised, usbmuxd should stay up with no devices so the socket the backend polls is always there. PROVIDER_USBMUXD_RESCAN_SECONDS is now PROVIDER_USBMUXD_WATCH_SECONDS, which is what it now paces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Unplugging one iPhone from a Linux provider host takes iOS down until someone cleans up by hand. Bind-mounting the host's
/var/run/usbmuxdis the cause: Docker binds the socket's inode, so the mount goes stale when usbmuxd exits at the last unplugged device, and a missing socket at container start has Docker create a directory there that the host's usbmuxd can then never bind.So the container runs its own usbmuxd, like it already runs its own adb server, supervised in a restart loop. Mask the host's copy.
docs/PROVIDER.mdhas the why.Hotplug means restarting usbmuxd, not rescanning
A rescan cannot see a replug, because the list it walks is libusb's and libusb's is frozen. libusb learns about a plug or an unplug from udevd's netlink broadcast, and that reaches only udevd's own network namespace — so inside a container its device list is whatever the bus held when usbmuxd started. A phone that comes back on a new bus address is invisible, and the one it replaced stays in the list forever, failing to open on every attempt with
LIBUSB_ERROR_IOanderrno=19. Only a fresh libusb — meaning a fresh process — sees the bus as it now is.sysfs, unlike libusb's cache, is the host's and always current, so that is what a watchdog polls: every
/sys/bus/usb/devicesentry with Apple's vendor ID, plus itsdevnum, since re-enumeration is what a replug looks like. When that set changes, usbmuxd is restarted. Because a restart costs every other iPhone its usbmuxd connection, a change has to survive one more tick before it counts — a re-enumerating device passes through intermediate states that would otherwise each be their own restart.PROVIDER_USBMUXD_WATCH_SECONDSpaces it, default 2.-zgoes with the rescan it replaced: supervised, usbmuxd should stay up with no devices attached so the socket the backend polls is always there. adb needs none of this — its server walks/dev/bus/usbitself once a second rather than asking libusb./var/lib/lockdownis now a volume, holding the pair records. Without it every iPhone asks to Trust This Computer again after a recreate.PROVIDER_START_ADBandPROVIDER_START_USBMUXDgate the daemons, so one host can run a provider per platform. Also fixesUSBMUXD_SOCKET_ADDRESSnever honouring theUNIX:/pathform.Breaking
The provider's env vars lose the
YARD_prefix:PROVIDER_TOKEN,PROVIDER_LOG_LEVEL,PROVIDER_CONFIG. Rename them in a provider host's.envbefore pulling.Test plan
cargo test --workspace,cargo clippy --workspace --all-targets -- -D warningsbun run check,bun run typecheckdocker compose --profile provider config, and with the macOS overlay/dev/bus/usbhas to be bind-mounted for any of this to work.privileged: truealone gives the container a snapshot of the host's device nodes, taken at creation — it survives until the first replug and never recovers after one🤖 Generated with Claude Code