HydraLab's iOS device management originally relied on tidevice, 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.
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.
Branch: pymobiledevice3-ios17-support-001
IOSUtils.java: Replaced alltideviceCLI invocations withpymobiledevice3equivalents:tidevice list --json→pymobiledevice3 usbmux listtidevice -u <udid> info --json→pymobiledevice3 lockdown info --udid <udid>tidevice -u <udid> applist→pymobiledevice3 apps list --udid <udid>tidevice -u <udid> install <path>→pymobiledevice3 apps install --udid <udid> <path>tidevice -u <udid> uninstall <pkg>→pymobiledevice3 apps uninstall --udid <udid> <pkg>tidevice -u <udid> launch <pkg>→pymobiledevice3 developer dvt launch --udid <udid> <pkg>tidevice -u <udid> relay <local> <remote>→pymobiledevice3 usbmux forward --serial <udid> <local> <remote>tidevice -u <udid> screenshot <path>→pymobiledevice3 developer dvt screenshot --udid <udid> <path>tidevice -u <udid> syslog→pymobiledevice3 syslog live --udid <udid>tidevice -u <udid> crashreport <dir>→pymobiledevice3 crash pull --udid <udid> <dir>
IOSDeviceDriver.java: UpdatedparseJsonToDevice()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).EnvCapability.java: Addedpymobiledevice3as a recognized environment capability keyword.- Device watcher:
tidevice usbmux watch(continuous USB event stream) was removed because pymobiledevice3 has no equivalentwatchcommand. Replaced with a one-timeupdateAllDeviceInfo()call at startup.
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+).
Branch: pymobiledevice3-ios17-support-001
IOSAppiumScreenRecorderForMac.java: Fixed video recording failure on macOS.
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.
Branch: pymobiledevice3-ios17-support-001
ZipBombChecker.java: Fixed zip bomb detection logic.
Unrelated security hardening fix included in the branch — ensures uploaded test packages are validated against zip bomb attacks before extraction.
Branch: pymobiledevice3-ios17-support-002
IOSUtils.java—proxyWDA(): Added iOS version-branched WDA (WebDriverAgent) launch strategy:- iOS < 17: Uses
pymobiledevice3 developer dvt launchto start WDA (same as before). - iOS 17+: Uses
xcodebuild test-without-buildingto start WDA. On iOS 17+,dvt launchcrashes WDA because it doesn't create a proper XCUITest session.xcodebuildproperly bootstraps the XCTest framework and keeps WDA's HTTP server alive.
- iOS < 17: Uses
IOSUtils.java—killProxyWDA(): Updated to kill bothxcodebuild-based anddvt launch-based WDA processes, scoped by device UDID.IOSUtils.java: AddedisIOS17OrAbove()helper and WDA project path discovery (getWdaProjectPath()).IOSAppiumScreenRecorderForMac.java: Replaced ffmpeg-based frame stitching with QuickTime-compatible screen recording usingscreencapture -von macOS, which is more reliable for iOS 17+ devices.AppiumServerManager.java: Minor adjustments for iOS driver initialization.- Scripts added:
scripts/install_wda.sh— Automates WDA installation for iOS 17+ devices viaxcodebuild.scripts/install_wda_below_ios_17.sh— WDA installation for iOS < 17 devices.scripts/cleanup_ios_ports.sh— Cleans up stale port forwarding and WDA processes.
docs/iOS-Testing-Guide.md: Comprehensive guide for setting up iOS testing with HydraLab.
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+.
Branch: pymobiledevice3-ios17-support-003-app-cleanup-fix
XCTestRunner.java:- Fixed test completion detection to handle Objective-C formatted output (e.g.,
Executed X testspattern) fromxcodebuild test-without-building. - Added early completion detection so the runner doesn't wait for the full timeout when tests have already finished.
- Ensured
finishTest()always runs via try/finally, preventing resource leaks when tests fail or the process exits unexpectedly.
- Fixed test completion detection to handle Objective-C formatted output (e.g.,
XCTestCommandReceiver.java: Improved command output parsing for the new xcodebuild-based test execution.
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.
Branch: pymobiledevice3-ios17-support-004-auto-device-detection
ScheduledDeviceControlTasks.java: Added a@Scheduledtask that callsdeviceControlService.updateDeviceList()every 60 seconds (with a 30-second initial delay after agent startup).DeviceControlService.java: AddedupdateDeviceList()method that delegates todeviceDriverManager.updateAllDeviceInfo().
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.
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.
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.
IOSDeviceWatcher.java: Still exists in the codebase but is no longer invoked. Can be removed in a future cleanup.
Branch: pymobiledevice3-ios17-support-005-tti-threshold-fix
IOSDeviceDriver.java—pullFileFromDevice(): Replaced the no-op stub ("Nothing Implemented for iOS") with a full implementation that:- Parses the
pathOnDeviceargument in two formats:bundleId:/path(recommended) — explicit bundle ID and container path, e.g.com.6alabat.cuisineApp:/Documents//path(fallback) — uses the running task'spkgNameas the bundle ID
- Retrieves the current
TestRunfromTestRunThreadContextto determine the result folder - 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 topulled_files/if the remote path is just/. - Delegates the actual file transfer to
IOSUtils.pullFileFromApp()
- Parses the
IOSUtils.java—pullFileFromApp(): New static helper method that:- Creates the local target directory if it doesn't exist (
mkdirs()) - Executes
python3 -m pymobiledevice3 apps pull --udid <UDID> <bundleId> <remotePath> <localPath> - 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
- Creates the local target directory if it doesn't exist (
Remote Path → Subfolder Name → Local Path
/Documents/ → Documents → <resultFolder>/Documents/
/Library/Caches/ → Caches → <resultFolder>/Caches/
/tmp/logs/ → logs → <resultFolder>/logs/
/ → pulled_files → <resultFolder>/pulled_files/
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.
The subfolder enhancement ensures parity with Android's behavior: pulled files are cleanly separated in a named subdirectory.
Add a pullFileFromDevice tearDown action to your test task:
{
"deviceActions": {
"tearDown": [
{
"deviceType": "IOS",
"method": "pullFileFromDevice",
"args": ["com.6alabat.cuisineApp:/Documents/"]
}
]
}
}Result folder structure after test execution:
storage/test/result/YYYY/MM/DD/<timestamp>/<udid>/
├── Documents/ ← pulled files land here
│ ├── tti_performance.json
│ ├── fwfv2_db_cache.json
│ └── ...
├── Crash/ ← crash reports (separate)
├── LegacyCrash/ ← legacy crash reports (separate)
├── merged_test.mp4 ← video recording
├── xctest_output.log ← test logs
└── ...
| File | Phases |
|---|---|
common/.../entity/agent/EnvCapability.java |
1 |
common/.../management/device/impl/IOSDeviceDriver.java |
1 |
common/.../util/IOSUtils.java |
1, 2, 4 |
common/.../util/ZipBombChecker.java |
3 |
common/.../screen/IOSAppiumScreenRecorderForMac.java |
2, 4 |
common/.../management/AppiumServerManager.java |
4 |
agent/.../runner/xctest/XCTestRunner.java |
5 |
agent/.../runner/xctest/XCTestCommandReceiver.java |
5 |
agent/.../scheduled/ScheduledDeviceControlTasks.java |
6 |
agent/.../service/DeviceControlService.java |
6 |
common/.../management/device/impl/IOSDeviceDriver.java |
1, 7 |
common/.../util/IOSUtils.java |
1, 2, 4, 7 |
scripts/install_wda.sh |
4 |
scripts/install_wda_below_ios_17.sh |
4 |
scripts/cleanup_ios_ports.sh |
4 |
docs/iOS-Testing-Guide.md |
4, 7 |
TIDEVICE_TO_PYMOBILEDEVICE3_MIGRATION.md |
1 |