Commit e84477c
feat(ios): Migrate from tidevice to pymobiledevice3 for iOS 17+ support (#717)
## Description
Complete migration of iOS device management from
[tidevice](https://github.com/alibaba/taobao-iphone-device) to
[pymobiledevice3](https://github.com/doronz88/pymobiledevice3) to
restore iOS 17+ device support.
**Why**: tidevice is incompatible with iOS 17+ because Apple replaced
the `lockdownd`-based developer image mounting with
CoreDevice/RemoteXPC. tidevice development has stalled since 2021.
pymobiledevice3 is actively maintained and supports both legacy (iOS <
17) and modern (iOS 17+) protocols.
**What**: This PR spans 7 phases of incremental changes covering CLI
migration, video recording fixes, WDA launch for iOS 17+, XCTest runner
improvements, auto device detection polling, iOS file pull
implementation, and comprehensive documentation.
### Linked GitHub issue ID: N/A (internal iOS 17+ support requirement)
## Pull Request Checklist
- [ ] Tests for the changes have been added (for bug fixes / features)
- [x] Code compiles correctly with all tests are passed.
- [x] I've read the [contributing
guide](https://github.com/microsoft/HydraLab/blob/main/CONTRIBUTING.md#making-changes-to-the-code)
and followed the recommended practices.
- [x] [Wikis](https://github.com/microsoft/HydraLab/wiki) or
[README](https://github.com/microsoft/HydraLab/blob/main/README.md) have
been reviewed and added / updated if needed (for bug fixes / features)
### Does this introduce a breaking change?
*No breaking changes. All changes are backward compatible:*
- *JSON parsing supports both tidevice and pymobiledevice3 field names*
- *iOS < 17 devices continue to use `dvt launch` for WDA*
- *Android device management is unchanged*
- [ ] Yes
- [x] No
## How you tested it
Tested on real hardware throughout all 7 phases:
- **iOS device**: iPhone X (UDID
`c7ad90190806994c5c4d62117b4761adc37674c9`), iOS 26.2
- **Android device**: Realme RMX3834 (serial `0R15205I23100583`),
Android 13
- **Test package**: `com.6alabat.cuisineApp` (iOS), `com.talabat`
(Android)
| Scenario | Status |
|---|---|
| Device discovery (pymobiledevice3) | Pass |
| App install/uninstall | Pass |
| XCTest execution via xcodebuild | Pass |
| Video recording (ffmpeg + MJPEG) | Pass |
| Screenshot capture | Pass |
| Port forwarding (WDA, MJPEG) | Pass |
| Crash report collection | Pass |
| Syslog collection | Pass |
| Auto device detection (60s polling) | Pass |
| pullFileFromDevice (iOS, subfolder) | Pass |
| pullFileFromDevice (Android, adb pull) | Pass |
| Multi-device safety (UDID-scoped kills) | Pass |
| XCTest early completion detection | Pass |
Please check the type of change your PR introduces:
- [x] Bugfix
- [x] Feature
- [ ] Technical design
- [ ] Build related changes
- [ ] Refactoring (no functional changes, no api changes)
- [x] Code style update (formatting, renaming) or Documentation content
changes
- [ ] Other (please describe):
### Feature UI screenshots or Technical design diagrams
#### Architecture: Before (tidevice)
```
HydraLab Agent
+-- IOSUtils.java
+-- tidevice CLI (Python)
+-- lockdownd / DeveloperDiskImage protocol
+-- iOS device (USB)
```
- Device discovery: `tidevice list --json`
- Device watcher: `tidevice usbmux watch` (continuous event stream ->
IOSDeviceWatcher thread)
- WDA launch: `tidevice xctest` (instrument protocol over usbmux)
- Video recording: Appium `startRecordingScreen()` (internal ffmpeg +
MJPEG)
#### Architecture: After (pymobiledevice3)
```
HydraLab Agent
+-- IOSUtils.java
+-- pymobiledevice3 CLI (Python)
|-- lockdownd protocol (iOS < 17)
+-- CoreDevice/RemoteXPC protocol (iOS 17+)
+-- iOS device (USB)
+-- IOSDeviceDriver.java
+-- pullFileFromDevice() --> pymobiledevice3 apps pull (AFC protocol)
+-- ScheduledDeviceControlTasks.java
+-- @scheduled(60s) --> updateAllDeviceInfo() (polling replaces event stream)
+-- XCTestRunner.java
+-- xcodebuild test-without-building (iOS 17+ WDA + test execution)
```
#### Key Architectural Differences
| Aspect | tidevice (Before) | pymobiledevice3 (After) |
|---|---|---|
| Device discovery | `tidevice list --json` | `pymobiledevice3 usbmux
list` |
| Device watcher | `tidevice usbmux watch` (event stream) | `@Scheduled`
polling every 60s (no watch equivalent) |
| WDA launch (< iOS 17) | `tidevice xctest` (instrument protocol) |
`pymobiledevice3 developer dvt launch` |
| WDA launch (iOS 17+) | Not supported | `xcodebuild
test-without-building` (XCUITest session) |
| Video recording (Mac) | Appium built-in `startRecordingScreen()` |
Direct ffmpeg via pymobiledevice3 MJPEG port forward |
| File pull (iOS) | Not implemented | `pymobiledevice3 apps pull` (AFC
protocol) with subfolder org |
| Test completion | Blocked on `proc.waitFor()` (hung ~600s on iOS 17+)
| Polling loop detects completion markers, 30s grace then force-kill |
| iOS 17+ support | Broken (SIGABRT on DeveloperDiskImage mount) | Full
support via CoreDevice/RemoteXPC |
---
### Changes by Phase
**Phase 1 - Core CLI Migration** (`9cf52178`): Replace all `tidevice`
CLI invocations in `IOSUtils.java` with `pymobiledevice3` equivalents
(usbmux list, lockdown info, apps install/uninstall/list, developer dvt
launch/screenshot, syslog live, crash pull, usbmux forward). Updated
`IOSDeviceDriver.parseJsonToDevice()` for pymobiledevice3 JSON fields
with fallback to tidevice field names. Added `pymobiledevice3` to
`EnvCapability`.
**Phase 2 - Video Recording Fix** (`0e54ad41`, `0be2f9ce`): Fixed 0-byte
video files caused by race condition (pymobiledevice3 port forwarding
not ready when ffmpeg connected). Added `waitForPortToBeListening()`
polling. Switched Mac recording from Appium built-in to direct ffmpeg +
pymobiledevice3 MJPEG forwarding. Removed conflicting `mjpegServerPort`
Appium capability.
**Phase 3 - Zip Bomb Protection** (`b0656788`): Fixed zip bomb detection
logic in `ZipBombChecker.java`.
**Phase 4 - iOS 17 WDA Launch & Video** (`b0b40b1a`): iOS
version-branched WDA launch (`dvt launch` for iOS < 17, `xcodebuild
test-without-building` for iOS 17+). iOS 17+ ffmpeg uses
`scale=720:-2,setsar=1 -pix_fmt yuv420p` for QuickTime compatibility.
Added `isIOS17OrAbove()`, `getWdaProjectPath()` helpers. Added scripts:
`install_wda.sh`, `install_wda_below_ios_17.sh`, `cleanup_ios_ports.sh`.
**Phase 5 - XCTest Runner Cleanup** (`564b06d7`): Fixed XCTest hanging
~600s after tests complete on iOS 17+. Added early completion detection
in `XCTestCommandReceiver`. Replaced blocking `waitFor()` with polling
loop + 30s grace. Wrapped `finishTest()` in try/finally. Handle ObjC
test format (`-[ClassName testMethodName]`). Scoped `killProxyWDA()` to
device UDID for multi-device safety.
**Phase 6 - Auto Device Detection** (`11d2e5d3`): Added
`@Scheduled(fixedDelay=60000, initialDelay=30000)` polling in
`ScheduledDeviceControlTasks` to replace missing `tidevice usbmux watch`
equivalent.
**Phase 7 - iOS pullFileFromDevice** (`b074d368`): Implemented
`IOSDeviceDriver.pullFileFromDevice()` (was no-op stub). Parses
`bundleId:/path` format. New `IOSUtils.pullFileFromApp()` wraps
`pymobiledevice3 apps pull` (AFC protocol). Subfolder organization:
pulled files go into named subfolder matching remote path (e.g.
`/Documents/` -> `Documents/`) for parity with Android `adb pull`.
---
### Files Changed (22 files, +3492 -100)
**Java Source**
| File | Phase | Change |
|---|---|---|
| `common/.../util/IOSUtils.java` | 1,2,4,7 | All CLI commands, port
wait, WDA helpers, file pull |
| `common/.../device/impl/IOSDeviceDriver.java` | 1,7 | JSON parsing,
pullFileFromDevice implementation |
| `common/.../entity/agent/EnvCapability.java` | 1 | Added
pymobiledevice3 keyword |
| `common/.../management/AppiumServerManager.java` | 2 | Removed
mjpegServerPort capability |
| `common/.../screen/IOSAppiumScreenRecorderForMac.java` | 2,4 |
ffmpeg-based recording, iOS 17 video format |
| `common/.../util/ZipBombChecker.java` | 3 | Zip bomb detection fix |
| `agent/.../runner/xctest/XCTestRunner.java` | 5 | Completion
detection, try/finally, ObjC format |
| `agent/.../runner/xctest/XCTestCommandReceiver.java` | 5 | Early
completion markers |
| `agent/.../scheduled/ScheduledDeviceControlTasks.java` | 6 |
@scheduled polling task |
| `agent/.../service/DeviceControlService.java` | 6 | updateDeviceList()
method |
**Scripts**
| File | Description |
|---|---|
| `scripts/install_wda.sh` | WDA installation for iOS 17+ via xcodebuild
|
| `scripts/install_wda_below_ios_17.sh` | WDA installation for iOS < 17
|
| `scripts/cleanup_ios_ports.sh` | Cleanup stale pymobiledevice3 port
forwards |
**Documentation**
| File | Description |
|---|---|
| `CHANGELOG.md` | 7-phase migration changelog with detailed what/why |
| `TIDEVICE_TO_PYMOBILEDEVICE3_MIGRATION.md` | Command-by-command
migration reference |
| `docs/API-Reference.md` | Full REST API reference with curl examples
(Android + iOS) |
| `docs/Android-Testing-Guide.md` | Comprehensive Android testing guide
|
| `docs/iOS-Testing-Guide.md` | Comprehensive iOS testing guide with
device onboarding |
| `TODO.md` | Known issues and future improvements |
### Dependencies
- **pymobiledevice3**: Must be installed (`pip3 install
pymobiledevice3`)
- **ffmpeg**: Required for iOS video recording on Mac
- **Xcode**: Required for iOS 17+ WDA launch (`xcodebuild`)
Co-Authored-By: Warp <agent@warp.dev>
---------
Co-authored-by: Warp <agent@warp.dev>1 parent 156e7cd commit e84477c
22 files changed
Lines changed: 3492 additions & 100 deletions
File tree
- agent/src/main/java/com/microsoft/hydralab/agent
- runner/xctest
- scheduled
- service
- common/src
- main/java/com/microsoft/hydralab/common
- entity/agent
- management
- device/impl
- screen
- util
- test/resources
- docs
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
0 commit comments