pymobiledevice3 11.40.1 stops its RSD connection from hanging forever; phoneshell's give-up path still never kills the process it spawned
pymobiledevice3 11.40.1, released 20 October 2026, makes a stalled RemoteServiceDiscoveryService connection fail after 10 seconds instead of waiting forever. phoneshell shells out to that exact connection through developer dvt xcuitest, and its own 85-second give-up path in cli.py never terminates the subprocess it spawned, whether the hang is 10 seconds or infinite.
pymobiledevice3 11.40.1, released 20 October 2026 as pull request #2069, makes RemoteServiceDiscoveryService.connect() raise RemoteXPCHandshakeTimeoutError after 10 seconds when the device's peer info never arrives, instead of waiting forever (issue #2068). phoneshell's fallback WDA launcher runs pymobiledevice3 developer dvt xcuitest, which rides exactly this RemoteXPC connection path. When that launch never answers, phoneshell's own bring-up command gives up after roughly 85 seconds of polling and exits, and in that exact branch it never calls .terminate() on the subprocess it spawned.
A ten-second fix for a connection that used to wait forever
pymobiledevice3's remote service discovery (RSD) step is part of the RemoteXPC handshake used to reach a device's developer services without Xcode. Before 11.40.1, RemoteServiceDiscoveryService.connect() "waited forever for the device's peer info," in the release's own description. If a tunnel dropped a large packet during the handshake and the peer info never arrived, the call simply never returned. 11.40.1 makes it behave like the handshake step before it: raise RemoteXPCHandshakeTimeoutError after 10 seconds. A companion fix in the same release, userspace_tunnel: Warn when a packet from the device can't reach the stack, logs the specific error (such as EMSGSIZE) that used to cause this kind of stall silently.
This landed as part of a dense run of releases: 11.38.0 and 11.39.0 on 19 October, 11.40.0 through 11.40.2 on 20 October. phoneshell's own requirements.txt pins pymobiledevice3==11.8.0, which predates all of them. The CLI binary phoneshell actually shells out to, via shutil.which("pymobiledevice3"), is whatever is installed from that pin, so the infinite wait this release fixes is not a historical curiosity for phoneshell. It is the shipped behaviour today, in the exact command phoneshell runs as its iOS 27 fallback.
| what happens | phoneshell source | cleans up held? |
|---|---|---|
launch_wda_dvt spawns pymobiledevice3 developer dvt xcuitest ... via subprocess.Popen, no timeout | device.py:587–605 | n/a, this is the spawn |
up stores the handle as held and polls client.is_alive() for 40 x 1s | cli.py:217–230 | no |
still dead, retries launch_wda_dvt and polls 45 more seconds | cli.py:238–244 | no |
still dead: prints "WebDriverAgent did not come up", raise typer.Exit(1) | cli.py:245–252 | no, held is never referenced here |
finally block terminates held if it is still running | cli.py:285–289 | yes, but only reached after the supervising loop starts, which requires the bridge to already be up |
The one cleanup call is scoped to the wrong branch
The only place phoneshell terminates a tethered DVT subprocess is a finally block wrapping the "supervising, ctrl-c to stop" loop that starts once client.is_alive() has already returned true. The failure branch, where the device never comes up at all, exits earlier, at line 252, with a bare raise typer.Exit(1). That exception unwinds past the point where held is ever inspected. The Python process exits with status 1. The pymobiledevice3 developer dvt xcuitest child it spawned does not receive a signal from this code at all, and keeps running as an orphan of a process that has already reported failure to the user.
What it means, and what it does not
pymobiledevice3 11.40.1 shrinks the worst case for one specific cause of this leak: an RSD handshake that never gets its peer info now fails loudly at 10 seconds instead of silently forever, so once phoneshell's pin catches up, an orphaned process from that specific cause will not outlive its parent by more than about 10 seconds past exit. It does nothing for the missing held.terminate() call itself. Any other reason the DVT session fails to answer within 85 seconds, a stuck device, a wedged testmanagerd session, a slow Mac, leaves the same orphan behind, for as long as that subprocess keeps running, because the cleanup code that exists for exactly this purpose is scoped to a branch the failure path never reaches.
We have not reproduced an orphaned process surviving a real phoneshell up failure on hardware; this is a static reading of the control flow in cli.py and device.py, stated as our judgement and flagged as such.
Check it yourself
git clone https://github.com/Blomega/phoneshell && cd phoneshell
# the pin phoneshell actually ships
grep -n "pymobiledevice3" requirements.txt
# the failure branch that never touches `held`
sed -n '227,252p' phoneshell/cli.py
# the one cleanup call, and the loop it is scoped to
sed -n '263,289p' phoneshell/cli.py
# reproduce the leak class yourself: start `phoneshell up --launcher dvt`
# against a phone that will never answer (airplane mode after the launch
# starts), wait for "WebDriverAgent did not come up", then check for the
# orphan:
ps aux | grep "pymobiledevice3 developer dvt"
What would prove this wrong
The claim is about control flow, not about pymobiledevice3's bug: in phoneshell/cli.py, the branch at lines 245-252 does not call held.terminate() or held.kill() on any path. If a maintainer adds that call, or a reader finds an existing signal handler or atexit hook elsewhere in the codebase that we missed and that actually cleans this up, the finding is wrong. We will recheck the repository on 3 November 2026.
Sources
- doronz88/pymobiledevice3, release v11.40.1, 20 October 2026. github.com/doronz88/pymobiledevice3/releases/tag/v11.40.1
- doronz88/pymobiledevice3, issue #2068, the RSD peer-info stall. github.com/doronz88/pymobiledevice3/issues/2068
- doronz88/pymobiledevice3, pull request #2069. github.com/doronz88/pymobiledevice3/pull/2069
- Blomega/phoneshell,
phoneshell/cli.py(theupcommand) andphoneshell/device.py(launch_wda_dvt). github.com/Blomega/phoneshell