Skip to content

feat(ios): Migrate from tidevice to pymobiledevice3 for iOS 17+ support#717

Open
abhishekbedi1432 wants to merge 11 commits intomicrosoft:mainfrom
abhishekbedi1432:user/abhishekbedi1432/pymobiledevice3-ios17-support-005-tti-threshold-fix
Open

feat(ios): Migrate from tidevice to pymobiledevice3 for iOS 17+ support#717
abhishekbedi1432 wants to merge 11 commits intomicrosoft:mainfrom
abhishekbedi1432:user/abhishekbedi1432/pymobiledevice3-ios17-support-005-tti-threshold-fix

Conversation

@abhishekbedi1432
Copy link

Description

Complete migration of iOS device management from tidevice to 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)
  • Code compiles correctly with all tests are passed.
  • I've read the contributing guide and followed the recommended practices.
  • Wikis or README 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

  • 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:

  • Bugfix
  • Feature
  • Technical design
  • Build related changes
  • Refactoring (no functional changes, no api changes)
  • 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

abhishekbedi1432 and others added 11 commits January 28, 2026 13:27
## Summary
Replace tidevice library with pymobiledevice3 to support iOS 17+ devices
while maintaining backward compatibility with older iOS versions.

## Problem
- tidevice is incompatible with iOS 17+ (uses deprecated DeveloperDiskImage)
- Screenshots and device operations fail on modern iOS devices
- tidevice development has stalled since 2021

## Solution
Migrate all iOS device management commands to pymobiledevice3:
- Device discovery: usbmux list
- Device info: lockdown info
- App management: apps install/uninstall/list
- Screenshots: developer dvt screenshot
- Log collection: syslog live, crash pull
- Port forwarding: usbmux forward

## Backward Compatibility
- JSON parsing supports both tidevice and pymobiledevice3 field names
- Device unlock failures are now non-fatal for XCTest execution
- Device watcher uses polling mechanism (usbmux watch not available)

## Files Modified
- IOSUtils.java: Updated all command invocations
- IOSDeviceDriver.java: Updated parsing and capability requirements
- EnvCapability.java: Added pymobiledevice3 capability keyword

## Testing
- Verified on iPhone 11 Pro (iOS 26.2)
- All core device operations functional

Ref: TIDEVICE_TO_PYMOBILEDEVICE3_MIGRATION.md for detailed command mapping

Co-Authored-By: Warp <agent@warp.dev>
## Problem
iOS screen recording on Mac was failing with 0-byte video files. The root
cause was a race condition between port forwarding setup and ffmpeg connection.

### Error Symptoms
1. ffmpeg reported 'Connection refused' when connecting to MJPEG port
2. Video files were created but remained 0 bytes
3. Appium's built-in recording failed with 'ffmpeg died unexpectedly'

### Root Cause Analysis
1. **Timing Issue**: getMjpegServerPortByUdid() started pymobiledevice3 port
   forwarding as a background process, but returned immediately without
   waiting for the port to become available. ffmpeg then tried to connect
   before the forwarding was ready, causing 'Connection refused' errors.

2. **Appium Conflict**: When mjpegServerPort capability was passed to Appium,
   it tried to set up its OWN port forwarding, conflicting with our
   pymobiledevice3 forwarding. Error: 'Cannot ensure MJPEG broadcast
   functionality by forwarding the local port XXXX'

3. **Mac Recording Approach**: The original IOSAppiumScreenRecorderForMac
   relied on Appium's iosDriver.startRecordingScreen() which internally
   uses ffmpeg + MJPEG. This had compatibility issues with pymobiledevice3.

## Solution

### 1. Added Port Readiness Wait (IOSUtils.java)
- New method: waitForPortToBeListening(port, timeoutMs, logger)
- Polls using lsof every 500ms until port is listening (max 10 seconds)
- Called after starting pymobiledevice3 forward process
- Ensures port is ready before returning to caller

### 2. Switched Mac Recording to ffmpeg-based (IOSAppiumScreenRecorderForMac.java)
- Changed from Appium's built-in recording to direct ffmpeg approach
- Uses pymobiledevice3 port forwarding (same as Windows implementation)
- ffmpeg command: ffmpeg -f mjpeg -reconnect 1 -i http://127.0.0.1:PORT ...
- Graceful shutdown with SIGINT for proper video finalization

### 3. Removed mjpegServerPort Capability (AppiumServerManager.java)
- Removed mjpegServerPort from Appium driver capabilities
- Screen recorder now handles its own port forwarding independently
- Eliminates conflict between Appium and pymobiledevice3 forwarding

## Files Changed
- common/.../util/IOSUtils.java
  - Added waitForPortToBeListening() method
  - Modified getMjpegServerPortByUdid() to wait for port readiness

- common/.../screen/IOSAppiumScreenRecorderForMac.java
  - Rewrote to use ffmpeg-based recording with pymobiledevice3
  - Added graceful ffmpeg shutdown with SIGINT
  - Added MJPEG port cleanup on recording stop

- common/.../management/AppiumServerManager.java
  - Removed mjpegServerPort capability from iOS driver creation
  - Added comment explaining screen recorder handles forwarding

- docs/iOS-Testing-Guide.md
  - Added 'Onboarding a New iOS Device' section
  - Added 'Screen Recording' documentation
  - Updated version history with v1.1.0 changes

## Testing
- Verified video recording produces valid MP4 files (1.2MB+)
- Confirmed MJPEG port becomes active within 500-600ms
- No 'Connection refused' errors in agent logs
- Video playback confirmed working

Co-Authored-By: Warp <agent@warp.dev>
…ible video recording

## Problem
iOS 17+ broke two key flows in the pymobiledevice3 migration:

1. **WDA Launch**: `pymobiledevice3 developer dvt launch` crashes WDA on iOS 17+
   with SIGABRT in XCTRunnerDaemonSession because it doesn't create a proper
   XCUITest session. The original tidevice used `xctest` which communicates
   via the device's test manager daemon over usbmux.

2. **Video Recording**: ffmpeg output had non-standard sample_aspect_ratio
   (281:1218) and yuvj420p pixel format, causing QuickTime Player to reject
   the recorded MP4 files.

## Changes

### IOSUtils.java
- Add `isIOS17OrAbove()` helper for version-based branching
- Add `getWdaProjectPath()` to locate WebDriverAgent.xcodeproj from Appium
- **proxyWDA()**: iOS < 17 keeps `dvt launch` (verified working); iOS 17+
  uses `xcodebuild test-without-building` to start WDA with proper XCUITest
  session that keeps the HTTP server alive on port 8100
- **killProxyWDA()**: Updated to kill both `dvt launch` and `xcodebuild`
  WDA processes

### IOSAppiumScreenRecorderForMac.java
- iOS < 17: keeps existing verified `scale=720:360 -vcodec h264` command
- iOS 17+: uses `scale=720:-2,setsar=1 -pix_fmt yuv420p -vcodec libx264`
  which auto-calculates height preserving aspect ratio, sets square pixels,
  and uses standard yuv420p color space for QuickTime compatibility

## Known Issue
On iOS 17+, proxyWDA() starts WDA via xcodebuild which conflicts with
XCTestRunner's xcodebuild session (two test sessions on same device).
Tests complete but xcodebuild hangs at cleanup. Next step: migrate WDA
launch to `pymobiledevice3 developer dvt xcuitest` (the pymobiledevice3
equivalent of tidevice xctest) which uses the instrument protocol over
usbmux and won't conflict with xcodebuild.

Co-Authored-By: Warp <agent@warp.dev>
…mat, ensure finishTest always runs

On iOS 17+, xcodebuild hangs for ~600s at diagnostics collection after
tests complete. This caused the agent to wait with all processes (WDA,
ffmpeg, syslog, port forwards) still running, and video files to be
corrupted when the framework eventually force-killed ffmpeg.

XCTestCommandReceiver: detect test completion markers
- Added volatile boolean testComplete flag with isTestComplete() accessor
- Detects early completion: 'Test Suite All tests {passed|failed} at ...'
  which appears immediately when tests finish, BEFORE the 600s diagnostics
- Also detects late markers: '** TEST {SUCCEEDED|FAILED|EXECUTE FAILED} **'
  which appear only after diagnostics collection completes

XCTestRunner.runXctest(): poll for completion instead of blocking
- Replaced proc.waitFor() with a polling loop that checks both stdout and
  stderr receivers for isTestComplete(), breaking immediately on detection
- After detection, gives 30s grace period then force-kills xcodebuild
- This reduces the post-test wait from ~600s to ~30s

XCTestRunner.run(): wrap test execution in try/finally
- finishTest() now runs in a finally block so it executes even if
  analysisXctestResult() or other steps throw exceptions
- Previously, an ArrayIndexOutOfBoundsException in result parsing would
  skip finishTest() entirely, leaving all processes orphaned

XCTestRunner.analysisXctestResult(): handle Objective-C test format
- Previously only handled Swift format: 'ClassName.testMethodName'
- Now also handles ObjC format: '-[ClassName testMethodName]' (no dot)
- Fixes ArrayIndexOutOfBoundsException when splitting on '.' for ObjC tests

IOSUtils.killProxyWDA(): scope kills to device UDID
- Changed 'xcodebuild test-without-building' (matches ALL devices) to
  'xcodebuild.*WebDriverAgentRunner.*<UDID>' (matches only this device)
- Added UDID to dvt launch kill pattern for multi-device safety

Co-Authored-By: Warp <agent@warp.dev>
Implement iOS pullFileFromDevice using pymobiledevice3 apps pull to
retrieve files from an app's sandboxed container after test execution.
This is the iOS equivalent of Android's adb pull.

Changes:
- IOSDeviceDriver.pullFileFromDevice(): Full implementation replacing
  the no-op stub. Supports two path formats:
  - bundleId:/path (e.g. com.6alabat.cuisineApp:/Documents/)
  - /path (uses task's pkgName as bundle ID)
- IOSUtils.pullFileFromApp(): New helper wrapping pymobiledevice3 apps
  pull with local directory creation and error handling.
- Subfolder organization: Pulled files are placed in a named subfolder
  matching the remote path (e.g. /Documents/ -> Documents/) instead of
  being dumped flat into the result folder root. Falls back to
  pulled_files/ if the remote path is just /. Ensures parity with
  Android's adb pull behavior.
- Updated CHANGELOG.md with Phase 7 details
- Updated docs/API-Reference.md with subfolder organization notes
- Updated docs/iOS-Testing-Guide.md with pullFileFromDevice section,
  result folder structure, and v1.2.0 version history

Co-Authored-By: Warp <agent@warp.dev>
Add Android-Testing-Guide.md covering:
- Prerequisites (Android SDK, ADB, ANDROID_HOME setup)
- Device onboarding (developer options, USB debugging, verification)
- Test execution for all supported types (Instrumentation/Espresso,
  Monkey, Appium, Maestro) with curl examples
- Test scope options (TEST_APP, PACKAGE, CLASS)
- Screen recording strategies (PhoneAppScreenRecorder vs ADBScreenRecorder)
- pullFileFromDevice usage with common paths and retry behavior
- Full device actions reference (14 methods with args and descriptions)
- Automatic device setup/teardown (animations, screen timeout)
- Test Orchestrator support for flaky test isolation
- Troubleshooting guide (7 common issues with solutions)
- CI/CD integration example (GitHub Actions)
- File locations and project structure

Co-Authored-By: Warp <agent@warp.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments