Skip to content

Commit e84477c

Browse files
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

CHANGELOG.md

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
# Changelog — tidevice to pymobiledevice3 Migration
2+
3+
## Background
4+
5+
HydraLab's iOS device management originally relied on [tidevice](https://github.com/alibaba/taobao-iphone-device), an open-source tool by Alibaba for communicating with iOS devices over USB. Starting with **iOS 17**, Apple introduced significant changes to its developer tooling protocol (replacing the legacy `lockdownd`-based developer image mounting with a new CoreDevice/RemoteXPC framework). tidevice does not support these changes and has not been actively maintained to address them.
6+
7+
**[pymobiledevice3](https://github.com/doronz88/pymobiledevice3)** is an actively maintained, pure-Python implementation that supports iOS 17+ and the new Apple protocols. This migration replaces all tidevice CLI calls with their pymobiledevice3 equivalents across the HydraLab agent and common modules.
8+
9+
---
10+
11+
## Phase 1 — Core Migration (`9cf52178`)
12+
13+
**Branch:** `pymobiledevice3-ios17-support-001`
14+
15+
### What changed
16+
- **`IOSUtils.java`**: Replaced all `tidevice` CLI invocations with `pymobiledevice3` equivalents:
17+
- `tidevice list --json``pymobiledevice3 usbmux list`
18+
- `tidevice -u <udid> info --json``pymobiledevice3 lockdown info --udid <udid>`
19+
- `tidevice -u <udid> applist``pymobiledevice3 apps list --udid <udid>`
20+
- `tidevice -u <udid> install <path>``pymobiledevice3 apps install --udid <udid> <path>`
21+
- `tidevice -u <udid> uninstall <pkg>``pymobiledevice3 apps uninstall --udid <udid> <pkg>`
22+
- `tidevice -u <udid> launch <pkg>``pymobiledevice3 developer dvt launch --udid <udid> <pkg>`
23+
- `tidevice -u <udid> relay <local> <remote>``pymobiledevice3 usbmux forward --serial <udid> <local> <remote>`
24+
- `tidevice -u <udid> screenshot <path>``pymobiledevice3 developer dvt screenshot --udid <udid> <path>`
25+
- `tidevice -u <udid> syslog``pymobiledevice3 syslog live --udid <udid>`
26+
- `tidevice -u <udid> crashreport <dir>``pymobiledevice3 crash pull --udid <udid> <dir>`
27+
- **`IOSDeviceDriver.java`**: Updated `parseJsonToDevice()` to handle pymobiledevice3's JSON field names (`Identifier`, `DeviceName`, `ProductType`, `ProductVersion`) with fallback to tidevice's old field names (`udid`, `name`, `market_name`, `product_version`).
28+
- **`EnvCapability.java`**: Added `pymobiledevice3` as a recognized environment capability keyword.
29+
- **Device watcher**: `tidevice usbmux watch` (continuous USB event stream) was removed because pymobiledevice3 has no equivalent `watch` command. Replaced with a one-time `updateAllDeviceInfo()` call at startup.
30+
31+
### Why
32+
tidevice cannot communicate with iOS 17+ devices. The lockdown protocol and developer image mounting mechanism changed fundamentally in iOS 17. pymobiledevice3 supports both the legacy protocol (iOS < 17) and the new CoreDevice/RemoteXPC protocol (iOS 17+).
33+
34+
---
35+
36+
## Phase 2 — Video Recording Fix (`0e54ad41`, `0be2f9ce`)
37+
38+
**Branch:** `pymobiledevice3-ios17-support-001`
39+
40+
### What changed
41+
- **`IOSAppiumScreenRecorderForMac.java`**: Fixed video recording failure on macOS.
42+
43+
### Why
44+
After the migration, the `pymobiledevice3 developer dvt screenshot` command has different output behavior than tidevice's screenshot command. The screen recorder on Mac needed adjustments to correctly capture frames and produce video output via ffmpeg.
45+
46+
---
47+
48+
## Phase 3 — Zip Bomb Protection (`b0656788`)
49+
50+
**Branch:** `pymobiledevice3-ios17-support-001`
51+
52+
### What changed
53+
- **`ZipBombChecker.java`**: Fixed zip bomb detection logic.
54+
55+
### Why
56+
Unrelated security hardening fix included in the branch — ensures uploaded test packages are validated against zip bomb attacks before extraction.
57+
58+
---
59+
60+
## Phase 4 — iOS 17 WDA Launch & QuickTime Video (`b0b40b1a`)
61+
62+
**Branch:** `pymobiledevice3-ios17-support-002`
63+
64+
### What changed
65+
- **`IOSUtils.java``proxyWDA()`**: Added iOS version-branched WDA (WebDriverAgent) launch strategy:
66+
- **iOS < 17**: Uses `pymobiledevice3 developer dvt launch` to start WDA (same as before).
67+
- **iOS 17+**: Uses `xcodebuild test-without-building` to start WDA. On iOS 17+, `dvt launch` crashes WDA because it doesn't create a proper XCUITest session. `xcodebuild` properly bootstraps the XCTest framework and keeps WDA's HTTP server alive.
68+
- **`IOSUtils.java``killProxyWDA()`**: Updated to kill both `xcodebuild`-based and `dvt launch`-based WDA processes, scoped by device UDID.
69+
- **`IOSUtils.java`**: Added `isIOS17OrAbove()` helper and WDA project path discovery (`getWdaProjectPath()`).
70+
- **`IOSAppiumScreenRecorderForMac.java`**: Replaced ffmpeg-based frame stitching with QuickTime-compatible screen recording using `screencapture -v` on macOS, which is more reliable for iOS 17+ devices.
71+
- **`AppiumServerManager.java`**: Minor adjustments for iOS driver initialization.
72+
- **Scripts added**:
73+
- `scripts/install_wda.sh` — Automates WDA installation for iOS 17+ devices via `xcodebuild`.
74+
- `scripts/install_wda_below_ios_17.sh` — WDA installation for iOS < 17 devices.
75+
- `scripts/cleanup_ios_ports.sh` — Cleans up stale port forwarding and WDA processes.
76+
- **`docs/iOS-Testing-Guide.md`**: Comprehensive guide for setting up iOS testing with HydraLab.
77+
78+
### Why
79+
iOS 17 fundamentally changed how developer tools interact with devices. The old approach of launching WDA via `dvt launch` no longer works because iOS 17's XCTest framework requires a proper test session context. `xcodebuild test-without-building` is Apple's supported mechanism for running XCUITest bundles and is the only reliable way to keep WDA alive on iOS 17+.
80+
81+
---
82+
83+
## Phase 5 — XCTest Runner Cleanup Fix (`564b06d7`)
84+
85+
**Branch:** `pymobiledevice3-ios17-support-003-app-cleanup-fix`
86+
87+
### What changed
88+
- **`XCTestRunner.java`**:
89+
- Fixed test completion detection to handle Objective-C formatted output (e.g., `Executed X tests` pattern) from `xcodebuild test-without-building`.
90+
- Added early completion detection so the runner doesn't wait for the full timeout when tests have already finished.
91+
- Ensured `finishTest()` always runs via try/finally, preventing resource leaks when tests fail or the process exits unexpectedly.
92+
- **`XCTestCommandReceiver.java`**: Improved command output parsing for the new xcodebuild-based test execution.
93+
94+
### Why
95+
When running XCTest via `xcodebuild test-without-building`, the output format differs from the old Appium-based execution. The runner was not detecting test completion correctly, causing it to either hang until timeout or skip cleanup. The fix ensures reliable detection of test results and guaranteed cleanup of device resources.
96+
97+
---
98+
99+
## Phase 6 — Auto Device Detection (`123cda5e` + uncommitted)
100+
101+
**Branch:** `pymobiledevice3-ios17-support-004-auto-device-detection`
102+
103+
### What changed
104+
- **`ScheduledDeviceControlTasks.java`**: Added a `@Scheduled` task that calls `deviceControlService.updateDeviceList()` every **60 seconds** (with a 30-second initial delay after agent startup).
105+
- **`DeviceControlService.java`**: Added `updateDeviceList()` method that delegates to `deviceDriverManager.updateAllDeviceInfo()`.
106+
107+
### Why
108+
The original tidevice integration used `tidevice usbmux watch`, which spawned a long-running process that streamed USB connect/disconnect events in real time. The `IOSDeviceWatcher` thread read this stream and called `updateAllDeviceInfo()` on every `MessageType` event, enabling instant hot-plug detection.
109+
110+
pymobiledevice3's `usbmux` subcommand only supports `list` and `forward` — there is no `watch` or `listen` equivalent. During the Phase 1 migration, the continuous watcher was replaced with a single `updateAllDeviceInfo()` call at startup, with a comment stating "Device monitoring is now handled through periodic polling" — but **no polling was actually implemented**.
111+
112+
This meant newly connected iOS devices were invisible to the agent until it was restarted. The fix adds a simple periodic poll (every 60 seconds) as a pragmatic replacement. While not as responsive as the old event-driven approach, it ensures devices are discovered automatically within a reasonable time window.
113+
114+
### Removed / Deprecated
115+
- **`IOSDeviceWatcher.java`**: Still exists in the codebase but is no longer invoked. Can be removed in a future cleanup.
116+
117+
---
118+
119+
## Phase 7 — iOS `pullFileFromDevice` Implementation (`uncommitted`)
120+
121+
**Branch:** `pymobiledevice3-ios17-support-005-tti-threshold-fix`
122+
123+
### What changed
124+
- **`IOSDeviceDriver.java``pullFileFromDevice()`**: Replaced the no-op stub (`"Nothing Implemented for iOS"`) with a full implementation that:
125+
- Parses the `pathOnDevice` argument in two formats:
126+
- **`bundleId:/path`** (recommended) — explicit bundle ID and container path, e.g. `com.6alabat.cuisineApp:/Documents/`
127+
- **`/path`** (fallback) — uses the running task's `pkgName` as the bundle ID
128+
- Retrieves the current `TestRun` from `TestRunThreadContext` to determine the result folder
129+
- **Creates a named subfolder** matching the last component of the remote path (e.g. `/Documents/``Documents/`, `/Library/Caches/``Caches/`) so pulled files are organized separately from logs, crash reports, and video recordings. Falls back to `pulled_files/` if the remote path is just `/`.
130+
- Delegates the actual file transfer to `IOSUtils.pullFileFromApp()`
131+
- **`IOSUtils.java``pullFileFromApp()`**: New static helper method that:
132+
- Creates the local target directory if it doesn't exist (`mkdirs()`)
133+
- Executes `python3 -m pymobiledevice3 apps pull --udid <UDID> <bundleId> <remotePath> <localPath>`
134+
- This is the iOS equivalent of Android's `adb pull` — it uses the AFC (Apple File Conduit) protocol to access the app's sandboxed container
135+
136+
### Path Resolution Logic
137+
```
138+
Remote Path → Subfolder Name → Local Path
139+
/Documents/ → Documents → <resultFolder>/Documents/
140+
/Library/Caches/ → Caches → <resultFolder>/Caches/
141+
/tmp/logs/ → logs → <resultFolder>/logs/
142+
/ → pulled_files → <resultFolder>/pulled_files/
143+
```
144+
145+
### Why
146+
HydraLab's Android implementation uses `adb pull <path> <resultFolder>`, where `adb pull` preserves the directory name automatically. The iOS equivalent (`pymobiledevice3 apps pull`) dumps files flat into the target directory. Without the subfolder logic, pulled files (e.g. `tti_performance.json`, `fwfv2_db_*`) would mix with test artifacts like crash reports (`Crash/`), videos (`merged_test.mp4`), and log files in the result folder root — making it difficult to distinguish test output from HydraLab-generated artifacts.
147+
148+
The subfolder enhancement ensures parity with Android's behavior: pulled files are cleanly separated in a named subdirectory.
149+
150+
### How to use
151+
Add a `pullFileFromDevice` tearDown action to your test task:
152+
```json
153+
{
154+
"deviceActions": {
155+
"tearDown": [
156+
{
157+
"deviceType": "IOS",
158+
"method": "pullFileFromDevice",
159+
"args": ["com.6alabat.cuisineApp:/Documents/"]
160+
}
161+
]
162+
}
163+
}
164+
```
165+
166+
Result folder structure after test execution:
167+
```
168+
storage/test/result/YYYY/MM/DD/<timestamp>/<udid>/
169+
├── Documents/ ← pulled files land here
170+
│ ├── tti_performance.json
171+
│ ├── fwfv2_db_cache.json
172+
│ └── ...
173+
├── Crash/ ← crash reports (separate)
174+
├── LegacyCrash/ ← legacy crash reports (separate)
175+
├── merged_test.mp4 ← video recording
176+
├── xctest_output.log ← test logs
177+
└── ...
178+
```
179+
180+
---
181+
182+
## Summary of All Files Modified
183+
184+
| File | Phases |
185+
|-------------------------------------------------------------------------|---------|
186+
| `common/.../entity/agent/EnvCapability.java` | 1 |
187+
| `common/.../management/device/impl/IOSDeviceDriver.java` | 1 |
188+
| `common/.../util/IOSUtils.java` | 1, 2, 4 |
189+
| `common/.../util/ZipBombChecker.java` | 3 |
190+
| `common/.../screen/IOSAppiumScreenRecorderForMac.java` | 2, 4 |
191+
| `common/.../management/AppiumServerManager.java` | 4 |
192+
| `agent/.../runner/xctest/XCTestRunner.java` | 5 |
193+
| `agent/.../runner/xctest/XCTestCommandReceiver.java` | 5 |
194+
| `agent/.../scheduled/ScheduledDeviceControlTasks.java` | 6 |
195+
| `agent/.../service/DeviceControlService.java` | 6 |
196+
| `common/.../management/device/impl/IOSDeviceDriver.java` | 1, 7 |
197+
| `common/.../util/IOSUtils.java` | 1, 2, 4, 7 |
198+
| `scripts/install_wda.sh` | 4 |
199+
| `scripts/install_wda_below_ios_17.sh` | 4 |
200+
| `scripts/cleanup_ios_ports.sh` | 4 |
201+
| `docs/iOS-Testing-Guide.md` | 4, 7 |
202+
| `TIDEVICE_TO_PYMOBILEDEVICE3_MIGRATION.md` | 1 |

0 commit comments

Comments
 (0)