Blolabel
← Notes
event 7 min read

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.

Abstract technical illustration of a process tree diagram where a parent node fades out while a child node, connected by a thin line, keeps pulsing and running on its own

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 happensphoneshell sourcecleans up held?
launch_wda_dvt spawns pymobiledevice3 developer dvt xcuitest ... via subprocess.Popen, no timeoutdevice.py:587–605n/a, this is the spawn
up stores the handle as held and polls client.is_alive() for 40 x 1scli.py:217–230no
still dead, retries launch_wda_dvt and polls 45 more secondscli.py:238–244no
still dead: prints "WebDriverAgent did not come up", raise typer.Exit(1)cli.py:245–252no, held is never referenced here
finally block terminates held if it is still runningcli.py:285–289yes, 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.

The failure branch exits before the only cleanup call spawn held = pymobiledevice3 dvt poll is_alive(), 85s total never alive: print error, raise typer.Exit(1) python process exits, held.terminate() never called the only held.terminate() call lives in a finally block wrapping the supervising loop, which only starts after success Before pymobiledevice3 11.40.1: a stalled RSD handshake inside the dvt subprocess ran with no timeout of its own, so the orphaned process could keep running indefinitely after phoneshell's own command had already exited 1. Source: phoneshell/cli.py lines 217-289, phoneshell/device.py launch_wda_dvt, 20 October 2026
Our judgement from reading the code, not a live reproduction: the gap is in the control flow, independent of which specific hang triggers it.
Five releases in two days; phoneshell's pin predates all of them 11.8.0 phoneshell's pin requirements.txt 11.38.0 19 Oct: restore multiple devices 11.39.0 19 Oct: XPC/libdispatch tracing 11.40.0 20 Oct: parse-live syslog filters 11.40.1 20 Oct: RSD connect times out at 10s 11.40.2 20 Oct: tunnel socket-pair fix The gap between the red dot and the blue dot is not a count of releases, it is the version gap between what phoneshell's requirements.txt installs and the release that fixes the exact hang this article describes.
phoneshell's pinned CLI predates the timeout fix, so the infinite-wait behaviour is not historical, it is what ships today.

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

  1. doronz88/pymobiledevice3, release v11.40.1, 20 October 2026. github.com/doronz88/pymobiledevice3/releases/tag/v11.40.1
  2. doronz88/pymobiledevice3, issue #2068, the RSD peer-info stall. github.com/doronz88/pymobiledevice3/issues/2068
  3. doronz88/pymobiledevice3, pull request #2069. github.com/doronz88/pymobiledevice3/pull/2069
  4. Blomega/phoneshell, phoneshell/cli.py (the up command) and phoneshell/device.py (launch_wda_dvt). github.com/Blomega/phoneshell