diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index df7f22240c..58544b72c2 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -28,25 +28,43 @@ jobs: fail-fast: false matrix: test_targets: - # https://github.com/actions/runner-images/issues/14404 + # iOS - HOST_OS: 'xcode-27' XCODE_VERSION: '27.0-beta' - IOS_VERSION: '27.0' - IOS_MODEL: 'iPhone 17' - - HOST_OS: 'macos-26' - XCODE_VERSION: '26.4' - IOS_VERSION: '26.4' - IOS_MODEL: 'iPhone 17' + PLATFORM_NAME: 'iOS' + PLATFORM_VERSION: '27.0' + DEVICE_NAME: 'iPhone 17' - HOST_OS: 'macos-15' XCODE_VERSION: '16.4' - IOS_VERSION: '18.5' - IOS_MODEL: 'iPhone 16 Plus' - - HOST_OS: 'macos-14' - XCODE_VERSION: '15.4' - IOS_VERSION: '17.5' - IOS_MODEL: 'iPhone 15 Plus' + PLATFORM_NAME: 'iOS' + PLATFORM_VERSION: '18.5' + DEVICE_NAME: 'iPhone 16 Plus' - # https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md + # tvOS + - HOST_OS: 'xcode-27' + XCODE_VERSION: '27.0-beta' + PLATFORM_NAME: 'tvOS' + PLATFORM_VERSION: '27.0' + DEVICE_NAME: 'Apple TV 4K (3rd generation)' + - HOST_OS: 'macos-15' + XCODE_VERSION: '16.4' + PLATFORM_NAME: 'tvOS' + PLATFORM_VERSION: '18.5' + DEVICE_NAME: 'Apple TV 4K (3rd generation)' + + # WatchOS + - HOST_OS: 'xcode-27' + XCODE_VERSION: '27.0-beta' + PLATFORM_NAME: 'watchOS' + PLATFORM_VERSION: '27.0' + DEVICE_NAME: 'Apple Watch Series 11 (46mm)' + - HOST_OS: 'macos-15' + XCODE_VERSION: '16.4' + PLATFORM_NAME: 'watchOS' + PLATFORM_VERSION: '11.5' + DEVICE_NAME: 'Apple Watch Series 10 (46mm)' + + # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-Readme.md runs-on: ${{matrix.test_targets.HOST_OS}} steps: - uses: actions/checkout@v7 @@ -62,18 +80,32 @@ jobs: mkdir -p ./Resources/WebDriverAgent.bundle name: Install dev dependencies - - name: Prepare iOS simulator - if: ${{ matrix.test_targets.XCODE_VERSION != '27.0-beta' }} - env: - DEVICE_NAME: ${{matrix.test_targets.IOS_MODEL}} - PLATFORM_VERSION: ${{matrix.test_targets.IOS_VERSION}} + - run: xcrun simctl list devices available + name: List Installed Simulators + - run: xcrun simctl list runtimes + name: List Runtimes + - name: Start Simulator UI + # Xcode 27 replaced Simulator.app with DeviceHub.app, one directory level up from + # Simulator.app's location (Contents/Applications instead of Contents/Developer/Applications). run: | - xcrun simctl list devices available - open -Fn "$(xcode-select -p)/Applications/Simulator.app" - udid=$(xcrun simctl list devices available -j | \ - node -p "Object.entries(JSON.parse(fs.readFileSync(0)).devices).filter((x) => x[0].includes('$PLATFORM_VERSION'.replace('.', '-'))).reduce((acc, x) => [...acc, ...x[1]], []).find(({name}) => name === '$DEVICE_NAME').udid") - xcrun simctl bootstatus $udid -b - xcrun simctl shutdown $udid + if [ "${PLATFORM_VERSION%%.*}" -ge 27 ]; then + open -Fn "$(xcode-select --print-path)/../Applications/DeviceHub.app" + else + open -Fn "$(xcode-select --print-path)/Applications/Simulator.app" + fi + env: + PLATFORM_VERSION: ${{matrix.test_targets.PLATFORM_VERSION}} + - name: Prepare simulator + id: prepareSimulator + uses: futureware-tech/simulator-action@v5 + with: + model: '${{matrix.test_targets.DEVICE_NAME}}' + os: '${{matrix.test_targets.PLATFORM_NAME}}' + os_version: '${{matrix.test_targets.PLATFORM_VERSION}}' + shutdown_after_job: false + wait_for_boot: true + - name: Finalize simulator boot + run: node Scripts/ci/wait-for-simulator-idle.mjs "${{steps.prepareSimulator.outputs.udid}}" - run: npm run e2e-test name: Run functional tests @@ -81,5 +113,7 @@ jobs: CI: true _FORCE_LOGS: 1 _LOG_TIMESTAMP: 1 - DEVICE_NAME: ${{matrix.test_targets.IOS_MODEL}} - PLATFORM_VERSION: ${{matrix.test_targets.IOS_VERSION}} + DEVICE_NAME: ${{matrix.test_targets.DEVICE_NAME}} + PLATFORM_NAME: ${{matrix.test_targets.PLATFORM_NAME}} + PLATFORM_VERSION: ${{matrix.test_targets.PLATFORM_VERSION}} + SIMULATOR_UDID: ${{steps.prepareSimulator.outputs.udid}} diff --git a/.github/workflows/publish.js.yml b/.github/workflows/publish.js.yml index 0099b44fb3..bcce0282c0 100644 --- a/.github/workflows/publish.js.yml +++ b/.github/workflows/publish.js.yml @@ -23,6 +23,7 @@ env: # Available destination for simulators depends on Xcode version. DESTINATION_SIM: platform=iOS Simulator,name=iPhone 17 DESTINATION_SIM_TVOS: platform=tvOS Simulator,name=Apple TV 4K (3rd generation) + DESTINATION_SIM_WATCHOS: platform=watchOS Simulator,name=Apple Watch Series 11 (46mm) jobs: build_matrix: @@ -43,7 +44,9 @@ jobs: {"name": "iOS Simulator arm64", "build_script": "build-sim.sh", "scheme": "WebDriverAgentRunner", "destination": "${{ env.DESTINATION_SIM }}", "derived_data_path": "appium_wda_ios_sim_arm64", "simulator_name": "Debug-iphonesimulator", "wd": "appium_wda_ios_sim_arm64/Build/Products/Debug-iphonesimulator", "zip_name": "WebDriverAgentRunner-Build-Sim-arm64.zip", "artifact_name": "WebDriverAgentRunner-Build-Sim-arm64", "archs": "arm64"}, {"name": "iOS Simulator x86_64", "build_script": "build-sim.sh", "scheme": "WebDriverAgentRunner", "destination": "${{ env.DESTINATION_SIM }}", "derived_data_path": "appium_wda_ios_sim_x86_64", "simulator_name": "Debug-iphonesimulator", "wd": "appium_wda_ios_sim_x86_64/Build/Products/Debug-iphonesimulator", "zip_name": "WebDriverAgentRunner-Build-Sim-x86_64.zip", "artifact_name": "WebDriverAgentRunner-Build-Sim-x86_64", "archs": "x86_64"}, {"name": "tvOS Simulator arm64", "build_script": "build-sim.sh", "scheme": "WebDriverAgentRunner_tvOS", "destination": "${{ env.DESTINATION_SIM_TVOS }}", "derived_data_path": "appium_wda_tvos_sim_arm64", "simulator_name": "Debug-appletvsimulator", "wd": "appium_wda_tvos_sim_arm64/Build/Products/Debug-appletvsimulator", "zip_name": "WebDriverAgentRunner_tvOS-Build-Sim-arm64.zip", "artifact_name": "WebDriverAgentRunner_tvOS-Build-Sim-arm64", "archs": "arm64"}, - {"name": "tvOS Simulator x86_64", "build_script": "build-sim.sh", "scheme": "WebDriverAgentRunner_tvOS", "destination": "${{ env.DESTINATION_SIM_TVOS }}", "derived_data_path": "appium_wda_tvos_sim_x86_64", "simulator_name": "Debug-appletvsimulator", "wd": "appium_wda_tvos_sim_x86_64/Build/Products/Debug-appletvsimulator", "zip_name": "WebDriverAgentRunner_tvOS-Build-Sim-x86_64.zip", "artifact_name": "WebDriverAgentRunner_tvOS-Build-Sim-x86_64", "archs": "x86_64"} + {"name": "tvOS Simulator x86_64", "build_script": "build-sim.sh", "scheme": "WebDriverAgentRunner_tvOS", "destination": "${{ env.DESTINATION_SIM_TVOS }}", "derived_data_path": "appium_wda_tvos_sim_x86_64", "simulator_name": "Debug-appletvsimulator", "wd": "appium_wda_tvos_sim_x86_64/Build/Products/Debug-appletvsimulator", "zip_name": "WebDriverAgentRunner_tvOS-Build-Sim-x86_64.zip", "artifact_name": "WebDriverAgentRunner_tvOS-Build-Sim-x86_64", "archs": "x86_64"}, + {"name": "watchOS Simulator arm64", "build_script": "build-sim.sh", "scheme": "WebDriverAgentRunner_watchOS", "destination": "${{ env.DESTINATION_SIM_WATCHOS }}", "derived_data_path": "appium_wda_watchos_sim_arm64", "simulator_name": "Debug-watchsimulator", "wd": "appium_wda_watchos_sim_arm64/Build/Products/Debug-watchsimulator", "zip_name": "WebDriverAgentRunner_watchOS-Build-Sim-arm64.zip", "artifact_name": "WebDriverAgentRunner_watchOS-Build-Sim-arm64", "archs": "arm64"}, + {"name": "watchOS Simulator x86_64", "build_script": "build-sim.sh", "scheme": "WebDriverAgentRunner_watchOS", "destination": "${{ env.DESTINATION_SIM_WATCHOS }}", "derived_data_path": "appium_wda_watchos_sim_x86_64", "simulator_name": "Debug-watchsimulator", "wd": "appium_wda_watchos_sim_x86_64/Build/Products/Debug-watchsimulator", "zip_name": "WebDriverAgentRunner_watchOS-Build-Sim-x86_64.zip", "artifact_name": "WebDriverAgentRunner_watchOS-Build-Sim-x86_64", "archs": "x86_64"} ] MATRIX_JSON echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT diff --git a/.github/workflows/wda-tests.yml b/.github/workflows/wda-tests.yml index 965bee6765..a41bc0367d 100644 --- a/.github/workflows/wda-tests.yml +++ b/.github/workflows/wda-tests.yml @@ -24,12 +24,14 @@ concurrency: cancel-in-progress: true env: - MIN_VM_IMAGE: macos-14 - MIN_XCODE_VERSION: "15.4" - MIN_PLATFORM_VERSION: "17.5" - MIN_TV_PLATFORM_VERSION: "17.5" + MIN_VM_IMAGE: macos-15 + MIN_XCODE_VERSION: "16.4" + MIN_PLATFORM_VERSION: "18.5" + MIN_TV_PLATFORM_VERSION: "18.5" MIN_TV_DEVICE_NAME: "Apple TV 4K (3rd generation)" - MIN_IPHONE_DEVICE_NAME: "iPhone 15 Plus" + MIN_WATCH_PLATFORM_VERSION: "11.5" + MIN_WATCH_DEVICE_NAME: "Apple Watch Series 10 (46mm)" + MIN_IPHONE_DEVICE_NAME: "iPhone 16" MIN_IPAD_DEVICE_NAME: "iPad Air 13-inch (M2)" MAX_VM_IMAGE: xcode-27 MAX_XCODE_VERSION: "27.0-beta" @@ -106,18 +108,22 @@ jobs: run: | cat <<'MATRIX_JSON' | jq -c . > matrix.json [ - {"name": "iphone_int_test_1_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_1", "dest": "iphone", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, - {"name": "iphone_int_test_2_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_2", "dest": "iphone", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, - {"name": "iphone_int_test_3_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_3", "dest": "iphone", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, - {"name": "ipad_int_test_1_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_1", "dest": "ipad", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, - {"name": "ipad_int_test_2_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_2", "dest": "ipad", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, - {"name": "ipad_int_test_3_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_3", "dest": "ipad", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, - {"name": "iphone_int_test_1_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_1", "dest": "iphone", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, - {"name": "iphone_int_test_2_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_2", "dest": "iphone", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, - {"name": "iphone_int_test_3_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_3", "dest": "iphone", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, - {"name": "ipad_int_test_1_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_1", "dest": "ipad", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, - {"name": "ipad_int_test_2_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_2", "dest": "ipad", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, - {"name": "ipad_int_test_3_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_3", "dest": "ipad", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"} + {"name": "iphone_int_test_1_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_1", "dest": "iphone", "sdk": "sim", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "iphone_int_test_2_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_2", "dest": "iphone", "sdk": "sim", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "iphone_int_test_3_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_3", "dest": "iphone", "sdk": "sim", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "ipad_int_test_1_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_1", "dest": "ipad", "sdk": "sim", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "ipad_int_test_2_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_2", "dest": "ipad", "sdk": "sim", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "ipad_int_test_3_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "int_test_3", "dest": "ipad", "sdk": "sim", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "iphone_int_test_1_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_1", "dest": "iphone", "sdk": "sim", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, + {"name": "iphone_int_test_2_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_2", "dest": "iphone", "sdk": "sim", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, + {"name": "iphone_int_test_3_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_3", "dest": "iphone", "sdk": "sim", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, + {"name": "ipad_int_test_1_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_1", "dest": "ipad", "sdk": "sim", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, + {"name": "ipad_int_test_2_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_2", "dest": "ipad", "sdk": "sim", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, + {"name": "ipad_int_test_3_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "int_test_3", "dest": "ipad", "sdk": "sim", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, + {"name": "tv_int_test_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "tv_int_test", "dest": "tv", "sdk": "tv_sim", "tv_model": "${{ env.MAX_TV_DEVICE_NAME }}", "tv_version": "${{ env.MAX_TV_PLATFORM_VERSION }}"}, + {"name": "tv_int_test_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "tv_int_test", "dest": "tv", "sdk": "tv_sim", "tv_model": "${{ env.MIN_TV_DEVICE_NAME }}", "tv_version": "${{ env.MIN_TV_PLATFORM_VERSION }}"}, + {"name": "watch_int_test_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "watch_int_test", "dest": "watch", "sdk": "watch_sim", "watch_model": "${{ env.MAX_WATCH_DEVICE_NAME }}", "watch_version": "${{ env.MAX_WATCH_PLATFORM_VERSION }}"}, + {"name": "watch_int_test_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "watch_int_test", "dest": "watch", "sdk": "watch_sim", "watch_model": "${{ env.MIN_WATCH_DEVICE_NAME }}", "watch_version": "${{ env.MIN_WATCH_PLATFORM_VERSION }}"} ] MATRIX_JSON echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT @@ -234,10 +240,14 @@ jobs: ACTION: ${{ matrix.config.action }} DEST: ${{ matrix.config.dest }} TARGET: lib - SDK: sim + SDK: ${{ matrix.config.sdk }} IPHONE_MODEL: ${{ matrix.config.iphone_model }} IPAD_MODEL: ${{ matrix.config.ipad_model }} IOS_VERSION: ${{ matrix.config.ios_version }} + TV_MODEL: ${{ matrix.config.tv_model }} + TV_VERSION: ${{ matrix.config.tv_version }} + WATCH_MODEL: ${{ matrix.config.watch_model }} + WATCH_VERSION: ${{ matrix.config.watch_version }} SKIP_TESTING: ${{ startsWith(matrix.config.ios_version, '27.') && matrix.config.action == 'int_test_3' && 'IntegrationTests_3/FBForceTouchTests/testForceTap' || '' }} FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 600 FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c79b20de1..ae2fed9271 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,51 @@ +## [16.8.0](https://github.com/appium/WebDriverAgent/compare/v16.7.3...v16.8.0) (2026-08-24) + +### Features + +* bound accessibility snapshot requests to avoid indefinite hangs ([#1214](https://github.com/appium/WebDriverAgent/issues/1214)) ([cd829eb](https://github.com/appium/WebDriverAgent/commit/cd829eb9725f57efbfb9538a073de059020ddd38)) + +## [16.7.3](https://github.com/appium/WebDriverAgent/compare/v16.7.2...v16.7.3) (2026-08-24) + +### Bug Fixes + +* let /status, /screenshot, and DELETE /session API methods to bypass the dispatch queue ([#1222](https://github.com/appium/WebDriverAgent/issues/1222)) ([f99b011](https://github.com/appium/WebDriverAgent/commit/f99b0111bba6f5cceabbddf1f9f0144d69d8f168)) + +## [16.7.2](https://github.com/appium/WebDriverAgent/compare/v16.7.1...v16.7.2) (2026-08-24) + +### Bug Fixes + +* harden FBHTTPServer/FBTCPSocket against races and protocol gaps ([#1224](https://github.com/appium/WebDriverAgent/issues/1224)) ([cf4bb2b](https://github.com/appium/WebDriverAgent/commit/cf4bb2b57b4d3a55ee4bbe8f860b00b0e7f5a326)) + +## [16.7.1](https://github.com/appium/WebDriverAgent/compare/v16.7.0...v16.7.1) (2026-08-23) + +### Bug Fixes + +* return W3C-compliant JSON error for unmatched routes ([#1223](https://github.com/appium/WebDriverAgent/issues/1223)) ([c951a91](https://github.com/appium/WebDriverAgent/commit/c951a91c8202d3c875db04dd633d37b409c80e43)) + +## [16.7.0](https://github.com/appium/WebDriverAgent/compare/v16.6.0...v16.7.0) (2026-08-22) + +### Features + +* unify HTTP server across iOS/tvOS/watchOS on Network.framework ([#1221](https://github.com/appium/WebDriverAgent/issues/1221)) ([cd741c5](https://github.com/appium/WebDriverAgent/commit/cd741c5cafe47d03635a88cc9296bbd5a841e773)) + +## [16.6.0](https://github.com/appium/WebDriverAgent/compare/v16.5.1...v16.6.0) (2026-08-22) + +### Features + +* Add MJPEG screenshot streaming support to watchOS ([#1220](https://github.com/appium/WebDriverAgent/issues/1220)) ([c6dcf03](https://github.com/appium/WebDriverAgent/commit/c6dcf03096dd5faab83398f16fb772724fcc5220)) + +## [16.5.1](https://github.com/appium/WebDriverAgent/compare/v16.5.0...v16.5.1) (2026-08-20) + +### Bug Fixes + +* add watchOS assets to GitHub release artifacts ([#1219](https://github.com/appium/WebDriverAgent/issues/1219)) ([ce5a9e8](https://github.com/appium/WebDriverAgent/commit/ce5a9e8af36747a72669c34b38bd39e495b57f5b)) + +## [16.5.0](https://github.com/appium/WebDriverAgent/compare/v16.4.0...v16.5.0) (2026-08-20) + +### Features + +* Add watchOS support to the TS driver, functional tests, and release pipeline ([#1217](https://github.com/appium/WebDriverAgent/issues/1217)) ([b53bb8f](https://github.com/appium/WebDriverAgent/commit/b53bb8fbd03ea37fd3bfc4e3eca4d2c6765b9c9c)) + ## [16.4.0](https://github.com/appium/WebDriverAgent/compare/v16.3.0...v16.4.0) (2026-08-19) ### Features diff --git a/README.md b/README.md index 3c1a4cd7ee..4d8be1961e 100644 --- a/README.md +++ b/README.md @@ -51,15 +51,5 @@ Then, you find `WebDriverAgentRunner-Runner-sim-.zip` for iOS and `Web [`WebDriverAgent` is BSD-licensed](LICENSE). -## Third Party Sources - -WebDriverAgent depends on the following third-party frameworks: -- [CocoaHTTPServer](https://github.com/robbiehanson/CocoaHTTPServer) -- [RoutingHTTPServer](https://github.com/mattstevens/RoutingHTTPServer) - -These projects haven't been maintained in a while. That's why the source code of these -projects has been integrated directly in the WebDriverAgent source tree. - -You can find the source files and their licenses in the `WebDriverAgentLib/Vendor` directory. Have fun! diff --git a/Scripts/build-webdriveragent.mjs b/Scripts/build-webdriveragent.mjs index f841025ddd..a23ca73d65 100644 --- a/Scripts/build-webdriveragent.mjs +++ b/Scripts/build-webdriveragent.mjs @@ -12,14 +12,15 @@ const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === __file const LOG = new logger.getLogger('WDABuild'); const ROOT_DIR = path.resolve(__dirname, '..'); const DERIVED_DATA_PATH = `${ROOT_DIR}/wdaBuild`; -const WDA_BUNDLE = 'WebDriverAgentRunner-Runner.app'; -const WDA_BUNDLE_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'Debug-iphonesimulator'); -const WDA_BUNDLE_TV = 'WebDriverAgentRunner_tvOS-Runner.app'; -const WDA_BUNDLE_TV_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'Debug-appletvsimulator'); +const BUNDLE_INFO = { + runner: {bundle: 'WebDriverAgentRunner-Runner.app', productDir: 'Debug-iphonesimulator'}, + tv_runner: {bundle: 'WebDriverAgentRunner_tvOS-Runner.app', productDir: 'Debug-appletvsimulator'}, + watch_runner: {bundle: 'WebDriverAgentRunner_watchOS-Runner.app', productDir: 'Debug-watchsimulator'}, +}; -const TARGETS = ['runner', 'tv_runner']; -const SDKS = ['sim', 'tv_sim']; +const TARGETS = ['runner', 'tv_runner', 'watch_runner']; +const SDKS = ['sim', 'tv_sim', 'watch_sim']; /** * Build WebDriverAgent and pack the app bundle into a zip archive. @@ -61,9 +62,8 @@ async function buildWebDriverAgent(xcodeVersion) { throw e; } - const isTv = target === 'tv_runner'; - const bundle = isTv ? WDA_BUNDLE_TV : WDA_BUNDLE; - const bundle_path = isTv ? WDA_BUNDLE_TV_PATH : WDA_BUNDLE_PATH; + const {bundle, productDir} = BUNDLE_INFO[target]; + const bundle_path = path.join(DERIVED_DATA_PATH, 'Build', 'Products', productDir); const zipName = `WebDriverAgentRunner-Runner-${sdk}-${xcodeVersion}.zip`; LOG.info(`Creating ${zipName} which includes ${bundle}`); diff --git a/Scripts/build.sh b/Scripts/build.sh index dc75956e26..281ba3fd59 100755 --- a/Scripts/build.sh +++ b/Scripts/build.sh @@ -106,8 +106,11 @@ function fastlane_test() { "tv" ) FASTLANE_DEVICE="$(echo $TV_MODEL | tr -d "'") ($TV_VERSION)" ;; + "watch" ) + FASTLANE_DEVICE="$(echo $WATCH_MODEL | tr -d "'") ($WATCH_VERSION)" + ;; * ) - echo "Error: Unknown DEST value '${DEST:-}'. DEST must be one of: iphone, ipad, tv" + echo "Error: Unknown DEST value '${DEST:-}'. DEST must be one of: iphone, ipad, tv, watch" exit 1 ;; esac @@ -126,5 +129,9 @@ case "$ACTION" in "int_test_1" ) fastlane_test IntegrationTests_1 ;; "int_test_2" ) fastlane_test IntegrationTests_2 ;; "int_test_3" ) fastlane_test IntegrationTests_3 ;; + "tv_int_test" ) fastlane_test IntegrationTests_tvOS ;; + # Like the iOS/tvOS integration tests, this launches the app under test in-process via + # XCUIApplication rather than driving a separately running WDA server over HTTP. + "watch_int_test" ) fastlane_test IntegrationTests_watchOS ;; *) xcbuild ;; esac diff --git a/Scripts/ci/wait-for-simulator-idle.mjs b/Scripts/ci/wait-for-simulator-idle.mjs new file mode 100755 index 0000000000..1242c5b970 --- /dev/null +++ b/Scripts/ci/wait-for-simulator-idle.mjs @@ -0,0 +1,91 @@ +/* eslint-disable no-console */ +import {exec} from 'teen_process'; + +// `simctl bootstatus` only waits for SpringBoard to become reachable; the simulator's launchd then +// spends tens of seconds to over a minute spawning ~150-250 background daemons, and CPU contention +// from that burst has been observed to turn a single native tap into a 100+ second operation on CI. +// Since `simctl boot`/`bootstatus` give no signal for when the burst ends, this polls the aggregate +// CPU usage of the simulator's process tree (children of its `launchd_sim`) and waits for it to stay +// low for several consecutive samples - a direct measurement of busyness, rather than a guessed sleep. +const CPU_THRESHOLD_PERCENT = Number(process.env.WAIT_SIM_IDLE_CPU_THRESHOLD ?? 20); +const CONSECUTIVE_SAMPLES_NEEDED = Number(process.env.WAIT_SIM_IDLE_CONSECUTIVE ?? 3); +const POLL_INTERVAL_MS = Number(process.env.WAIT_SIM_IDLE_INTERVAL_MS ?? 2000); +const MAX_WAIT_MS = Number(process.env.WAIT_SIM_IDLE_MAX_WAIT_MS ?? 150_000); + +const UDID = process.argv[2]; +if (!UDID) { + console.error('Usage: wait-for-simulator-idle.mjs '); + process.exitCode = 1; +} else { + await main(UDID); +} + +/** + * @param {string} udid + */ +async function main(udid) { + console.log(`Waiting for simulator '${udid}' background services to settle...`); + + const start = Date.now(); + let consecutive = 0; + for (;;) { + const cpuPercent = await sumChildProcessCpu(udid); + const elapsedSec = Math.round((Date.now() - start) / 1000); + + if (cpuPercent === null) { + console.warn(`::warning::Simulator '${udid}' process tree disappeared while waiting; skipping idle check`); + return; + } + + if (cpuPercent < CPU_THRESHOLD_PERCENT) { + consecutive++; + if (consecutive >= CONSECUTIVE_SAMPLES_NEEDED) { + console.log(`Simulator settled after ${elapsedSec}s (cpu=${cpuPercent}%)`); + return; + } + } else { + consecutive = 0; + } + + if (Date.now() - start >= MAX_WAIT_MS) { + console.warn( + `::warning::Simulator '${udid}' did not settle within ${Math.round(MAX_WAIT_MS / 1000)}s (last cpu=${cpuPercent}%); proceeding anyway`, + ); + return; + } + + console.log( + `t+${elapsedSec}s cpu=${cpuPercent}% (need ${CONSECUTIVE_SAMPLES_NEEDED} consecutive samples under ${CPU_THRESHOLD_PERCENT}%, have ${consecutive})`, + ); + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); + } +} + +/** + * Sums the %CPU of every direct child of the given simulator's `launchd_sim` - i.e. every process + * running inside that simulator - or `null` if the simulator's `launchd_sim` can no longer be found. + * @param {string} udid + * @returns {Promise} + */ +async function sumChildProcessCpu(udid) { + const {stdout: psOutput} = await exec('ps', ['-Aww', '-o', 'pid=,ppid=,pcpu=,command=']); + let launchdSimPid = null; + for (const line of psOutput.split('\n')) { + if (line.includes('launchd_sim') && line.includes(udid)) { + launchdSimPid = line.trim().split(/\s+/)[0]; + break; + } + } + if (!launchdSimPid) { + return null; + } + + let total = 0; + for (const line of psOutput.split('\n')) { + const match = line.trim().match(/^(\d+)\s+(\d+)\s+([\d.]+)\s/); + if (match && match[2] === launchdSimPid) { + total += Number(match[3]); + } + } + return Math.round(total); +} diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj index f7bba3b890..b0f1f1a558 100644 --- a/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 34AB13EFF1F673084C910195 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C72587443600661B83 /* GCDAsyncSocket.h */; }; + 718226CC2587443700661B83 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C72587443600661B83 /* GCDAsyncSocket.h */; }; + 718226CD2587443700661B83 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C72587443600661B83 /* GCDAsyncSocket.h */; }; + 718226CE2587443700661B83 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C82587443600661B83 /* GCDAsyncSocket.m */; }; + 718226CF2587443700661B83 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C82587443600661B83 /* GCDAsyncSocket.m */; }; FBCAFE000000000000002002 /* FBImageUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000002001 /* FBImageUtilsTests.m */; }; FBCAFE000000000000001002 /* FBVideoStreamSession.h in Headers */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000001001 /* FBVideoStreamSession.h */; }; FBCAFE000000000000001003 /* FBVideoStreamSession.h in Headers */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000001001 /* FBVideoStreamSession.h */; }; @@ -82,6 +87,7 @@ FBCAFE000000000000006051 /* FBAudioStreamTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FBCAFE000000000000006050 /* FBAudioStreamTests.m */; }; 005B327C702EF47820887884 /* FBErrorBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = EE3A18601CDE618F00DE4205 /* FBErrorBuilder.h */; }; 00ABDDC324B060006EA182F7 /* FBScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC01FFA29180053896D /* FBScreen.m */; }; + 0161F45997A981E47729DB25 /* RouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 01E7CFEBAD717FCF4BCDD383 /* RouteResponse.h */; }; 01A6F9758F11F6EF5B495D97 /* XCTPromise.h in Headers */ = {isa = PBXBuildFile; fileRef = 2CA02992F03AE1E134F2CAF5 /* XCTPromise.h */; }; 01AC2C121519821EC6D2ED6B /* XCTMacCatalystStatusProviding-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 42D2B5A0C490D9698C2A87A9 /* XCTMacCatalystStatusProviding-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 02032DB6E25DC5D0DEDD6184 /* XCUIElement+FBUID.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */; }; @@ -98,7 +104,6 @@ 062A682A90296DB8AA3B959E /* XCUIDevice+FBVoiceOver.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0002 /* XCUIDevice+FBVoiceOver.m */; }; 06E45CFFA4E41D77902DA67A /* XCTMeasureOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = B3FDA51EB36F03592BF48762 /* XCTMeasureOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 06FF73DCB07047C42DBFB16A /* AXSettings.h in Headers */ = {isa = PBXBuildFile; fileRef = 6496A5D8230D6EB30087F8CB /* AXSettings.h */; }; - 07CD6433C98FC4A86E2BC7E3 /* WDAAppLifecycleIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1B9A793C9EFB7853C1AA13 /* WDAAppLifecycleIntegrationTests.swift */; }; 07D0D478F000C48B33956E9A /* XCTRunnerDaemonSessionUIAutomationDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = B8A163261EFA440E42CA6AC1 /* XCTRunnerDaemonSessionUIAutomationDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 080C5B8955F9D1A3DB7C5B81 /* XCUIApplicationOpenRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AEAD1CF473F6AD60333E9FF /* XCUIApplicationOpenRequest.h */; }; 0936AE3F017DB42A03BF7751 /* XCTTestIdentifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 831F292661C60B68B557F14D /* XCTTestIdentifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -106,6 +111,7 @@ 09C9A6DCF11987546931BE57 /* XCUIResetAuthorizationStatusOfProtectedResourcesInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 9A0B17F41E4461BBD09A2962 /* XCUIResetAuthorizationStatusOfProtectedResourcesInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 09D97BD46764FD8149747723 /* XCUIElement+FBResolve.h in Headers */ = {isa = PBXBuildFile; fileRef = 71D3B3D3267FC7260076473D /* XCUIElement+FBResolve.h */; }; 09E27E0455701764DDB8C747 /* XCUIAccessibilityInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D47DD8BF27FE639742EA2E3E /* XCUIAccessibilityInterface-Protocol.h */; }; + 0A4413521ECE45EA182E8403 /* FBHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = AADFEA2ED9E61A8C1A99B2D7 /* FBHTTPServer.m */; }; 0B2D769FCCF7C5EEB3BA2404 /* XCUIApplicationPlatformServicesProviderDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 63FB001C1E84011C0096E5D8 /* XCUIApplicationPlatformServicesProviderDelegate-Protocol.h */; }; 0B51B2F12BB356C47986D6F9 /* XCTTagSelection.h in Headers */ = {isa = PBXBuildFile; fileRef = 8E2092D85A7C691F157B1971 /* XCTTagSelection.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0C120FA673A94DB66CB0E6B0 /* XCUIElement+FBCustomActions.m in Sources */ = {isa = PBXBuildFile; fileRef = F59CD6D32EF16E5E00F91287 /* XCUIElement+FBCustomActions.m */; }; @@ -120,7 +126,6 @@ 0E04133C2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */; }; 0E7CAA2FA6570D0C1EADFE4A /* XCTMessagingRole_MemoryTesting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 13D863EAE7F6F8B7E42D99B9 /* XCTMessagingRole_MemoryTesting-Protocol.h */; }; 0EFF39CC188B42E85D258F5A /* XCUIIssueDiagnosticsProviding-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 55B3452AB887CADB7AD757A4 /* XCUIIssueDiagnosticsProviding-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 0F282CAB2A025ECA9EAB18B3 /* HTTPResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8F249131D40060D7EB /* HTTPResponse.h */; }; 10878F7DCE410B73C3F8CCF1 /* FBRouteRequest-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7861CAEDF0C008C271F /* FBRouteRequest-Private.h */; }; 109F8F2F96E674EB1464EF3B /* FBTVNavigationTracker.h in Headers */ = {isa = PBXBuildFile; fileRef = 641EE70A2240CE2D00173FCB /* FBTVNavigationTracker.h */; }; 10EC41A2ECCD2E865EEE7AB0 /* XCUIElement+FBClassChain.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A7EAF31E20516B001DA4F2 /* XCUIElement+FBClassChain.h */; }; @@ -187,11 +192,12 @@ 1EBB250E6F79EBCBA11FCE9E /* FBW3CActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7140974A1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m */; }; 1F545FFB67878CBAF4D6E6A0 /* FBLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A31CF7A43900275851 /* FBLogger.h */; }; 1F9DD4891B8B19D8E13066E9 /* FBXCElementSnapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */; }; - 1FA2AA058DF5AEAC2212EAF8 /* WDAFindIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCED63DFD03326F6351165FE /* WDAFindIntegrationTests.swift */; }; 203561B6C2B8456509B60AD7 /* XCTScreenCapturePolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = FDB15E393EA0850C004D26B2 /* XCTScreenCapturePolicy.h */; settings = {ATTRIBUTES = (Public, ); }; }; 205E75731851D363B53A61DE /* UITestingUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */; }; 208F3FB46E41B0A1C87B8800 /* FBScreenshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 71C9EAAB25E8415A00470CD8 /* FBScreenshot.m */; }; + 2112EC67BDFFA4A0B2CF24EB /* RouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = D585660F7A04651223F29B07 /* RouteRequest.h */; }; 2238B4FC5C7DCD4B4215444D /* XCTMessagingRole_ProtectedResourceAuthorization-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CCCBEAE654102DBB1C8C22CD /* XCTMessagingRole_ProtectedResourceAuthorization-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 227E3F36FAA2C3881F89FD81 /* WDAClickIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C878996F07A9B26E66FC4EAC /* WDAClickIntegrationTests.swift */; }; 229F9F5B85C1255447495847 /* XCUIApplicationAutomationSessionProviding-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 603D97F0F9A5B9D4E6442BF2 /* XCUIApplicationAutomationSessionProviding-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 22A6AD29DB4A497BC03C2B67 /* XCTIssue+FBPatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 71A5C67129A4F39600421C37 /* XCTIssue+FBPatcher.h */; }; 22D9C8C0CDA32D1F307A345E /* XCTMessagingRole_DebugLogging-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 471DF7EE3C63C3069FB9D40D /* XCTMessagingRole_DebugLogging-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -211,15 +217,12 @@ 28104BC8093DBFAB035820FA /* XCTMetricDiagnosticHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = E46239748EC4A6BFBC13F28B /* XCTMetricDiagnosticHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2892ACD1F2CC2B0AF4FC22EA /* XCUIIssueDiagnosticsProviding-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 55B3452AB887CADB7AD757A4 /* XCUIIssueDiagnosticsProviding-Protocol.h */; }; 28B292C9A3654A81B765EB37 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1ABD1093739852B52C472B /* Foundation.framework */; }; - 293BD2162964EEC2A3BA6B57 /* HTTPResponseProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA224913C210060D7EB /* HTTPResponseProxy.h */; }; 2B39D11DD29FD3816F8AAC7C /* XCUIKnobControl.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C78201A2212BEA77979F4FF /* XCUIKnobControl.h */; }; 2B9D1292546083ACA2E34DB4 /* XCTCapabilitiesProviding-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B069904A4086E12556F1FAB /* XCTCapabilitiesProviding-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2BBEC31A5B1401C28C14899E /* XCTExpectedFailureContextManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 9AF0584AD9B6D0A57012C978 /* XCTExpectedFailureContextManager.h */; }; 2C0D6BA4BD20262B836CEDA3 /* XCTRunnerAutomationSession-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 01AF8E73DD47455B4854E470 /* XCTRunnerAutomationSession-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2C3893641ACA0AAFCC8DEB98 /* XCTPromise.h in Headers */ = {isa = PBXBuildFile; fileRef = 2CA02992F03AE1E134F2CAF5 /* XCTPromise.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2C6A45AFEC6B04782EE8E0B0 /* XCUIElement+FBForceTouch.m in Sources */ = {isa = PBXBuildFile; fileRef = EE8DDD7C20C5733B004D4925 /* XCUIElement+FBForceTouch.m */; }; - 2C6A876B29ECA17D6A5BCEED /* HTTPConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC89249131D30060D7EB /* HTTPConnection.h */; }; - 2CDFA27001529A43B560F3A1 /* WDAScreenshotAndSourceIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F677B5B7737E7E1F321C20 /* WDAScreenshotAndSourceIntegrationTests.swift */; }; 2D6B818DC921A4631A496432 /* FBCommandStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7761CAEDF0C008C271F /* FBCommandStatus.h */; }; 2EB7B0D78CEB3388711C7A8B /* FBErrorBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = EE3A18611CDE618F00DE4205 /* FBErrorBuilder.m */; }; 2F56E0614B3A533F388B6B51 /* XCTRemoteSignpostListenerProxy-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 8E47AA1A44A4A3C6EDCB6804 /* XCTRemoteSignpostListenerProxy-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -237,11 +240,10 @@ 328BB10FEA4029E1464BA7B6 /* XCTMessagingRole_TelemetrySending-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 2BA6647ADE7B44F13513065A /* XCTMessagingRole_TelemetrySending-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 33114F7BD00543F50F3BA4C8 /* XCTMessagingRole_SystemConfiguration-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EA24F4BE52B2520874E09BEF /* XCTMessagingRole_SystemConfiguration-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3400BE6CBA163DC9A58D60E6 /* XCUIApplicationProcessTracker-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3238E68F292452D8234153F1 /* XCUIApplicationProcessTracker-Protocol.h */; }; - 3414F451472B235F637F46BC /* DDNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7D249131B00060D7EB /* DDNumber.h */; }; 348B7FFB742C26599E44DB67 /* XCTMessagingRole_ProcessMonitoring-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = B43D656732067585371ADD31 /* XCTMessagingRole_ProcessMonitoring-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34A32D91F0C8B6A31A9E8F9A /* FBSettingsHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F3E7D725417FF400E0C22C /* FBSettingsHandler.m */; }; - 34AB13EFF1F673084C910195 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C72587443600661B83 /* GCDAsyncSocket.h */; }; 34E3403E0FE0E93C74E8FB2D /* XCTScreenCapturePolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = FDB15E393EA0850C004D26B2 /* XCTScreenCapturePolicy.h */; }; + 35924251B4B5D0A486A6A0BB /* FBHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = AADFEA2ED9E61A8C1A99B2D7 /* FBHTTPServer.m */; }; 35C087E005C82F9642F8A76C /* XCUIDeviceDelayedAttachmentTransferSupportInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C19CEEEC9858A106061D1F8 /* XCUIDeviceDelayedAttachmentTransferSupportInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 36635A59A5AEDDEFC2FC01E3 /* XCUIXcodeApplicationManaging-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 30ABCB5051B826025F77E360 /* XCUIXcodeApplicationManaging-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3663B9177361DA1B4255D8F1 /* NSString+FBXMLSafeString.m in Sources */ = {isa = PBXBuildFile; fileRef = 716E0BCD1E917E810087A825 /* NSString+FBXMLSafeString.m */; }; @@ -261,7 +263,6 @@ 3F213A7FAB64730F66D3F0A6 /* XCTReportingSession.h in Headers */ = {isa = PBXBuildFile; fileRef = CB69A2606052D5979C5A436F /* XCTReportingSession.h */; }; 3FB977871FD8786CA90EB3E2 /* FBSessionCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7611CAEDF0C008C271F /* FBSessionCommands.m */; }; 3FBA39A4D511311DB7C779F4 /* FBProtocolHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DE23080CA600646AFB /* FBProtocolHelpers.m */; }; - 3FEF512914A962C5E30579FC /* RouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = ED271671803AAF7188E828CF /* RouteResponse.m */; }; 40955058B413422AABBB21E7 /* XCUIAlertMonitoring-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = DB5797DE5A0B4E7EE3D166F7 /* XCUIAlertMonitoring-Protocol.h */; }; 409BDFE1D64246E9248CD392 /* XCTMessagingChannel_RunnerToDaemon-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = A2793FE9DE63C687BB6E2D37 /* XCTMessagingChannel_RunnerToDaemon-Protocol.h */; }; 40CF9CA5CE21FC96F1AFF761 /* XCUIApplicationOpenRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 4AEAD1CF473F6AD60333E9FF /* XCUIApplicationOpenRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -272,9 +273,11 @@ 431476F175B158C8E009AF69 /* XCTMessagingRole_AttachmentFutureResultStatusUpdating-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 10FBCE8B3A419B970DB7A5CE /* XCTMessagingRole_AttachmentFutureResultStatusUpdating-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 43AFED121DDF428411F72F83 /* FBXMLGenerationOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */; }; 43BC9FE4BF5C19F6F6569177 /* FBRouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7871CAEDF0C008C271F /* FBRouteRequest.h */; }; + 43DE58587952717F4DEE228E /* FBHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = AADFEA2ED9E61A8C1A99B2D7 /* FBHTTPServer.m */; }; 43EBA0D0D54EB99815CE63B6 /* XCUIElement+FBWebDriverAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE376471D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h */; }; 444F211126EAF77FB2B1DB42 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; }; 445523B6024EAE485C37C931 /* XCTRuntimeIssueDetectionPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = B98A9F937EF98D6359FCCC7A /* XCTRuntimeIssueDetectionPolicy.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 44A6F8DBFDCDD735D000458D /* RouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 01E7CFEBAD717FCF4BCDD383 /* RouteResponse.h */; }; 44A9308980F99E72907DD327 /* NSPredicate+FBFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A224E41DE2F56600844D55 /* NSPredicate+FBFormat.m */; }; 44D4667EC1743A6368395EF2 /* XCTestCaseDiscoveryUIAutomationDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = E29888B75A756D1CCD21604C /* XCTestCaseDiscoveryUIAutomationDelegate-Protocol.h */; }; 45033CF6BD35C16E34BF76CC /* FBScreenRecordingPromise.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */; }; @@ -321,13 +324,13 @@ 583AD004FF71CAA305E6DEAF /* XCUIDevice+FBHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26971CF2481700F8B5FF /* XCUIDevice+FBHelpers.m */; }; 587AA46196BC2C46555C8E44 /* XCUIButtonConsole.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E09842154A44874C4E9CA01 /* XCUIButtonConsole.h */; settings = {ATTRIBUTES = (Public, ); }; }; 588F51093FD2A5597366C282 /* XCUIElementEventTarget-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC62BF635704E9C72AF533E /* XCUIElementEventTarget-Protocol.h */; }; - 5917EF5F1372B2098168EA0D /* GCDAsyncUdpSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */; }; 5967912A40E807CDBE87E4B2 /* XCTReportingSessionIssueReporter-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 525D62A58488A52AA1BFE94A /* XCTReportingSessionIssueReporter-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 59689A4C5FC03AF28D15FAA2 /* XCUIAccessibilityInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D47DD8BF27FE639742EA2E3E /* XCUIAccessibilityInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 59892BBAB84DFD927C94593F /* _XCTestObservationPrivate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C840A8703A7C8D48897E158A /* _XCTestObservationPrivate-Protocol.h */; }; 5A1B8098AE35B82BD379B421 /* XCUIElement+FBForceTouch.h in Headers */ = {isa = PBXBuildFile; fileRef = EE8DDD7D20C5733C004D4925 /* XCUIElement+FBForceTouch.h */; }; 5AF49CE43E731B433375C5B7 /* ViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F87CD156E93338642EEFD54 /* ViewController.h */; }; 5B736BB83DEA69D3C5EEC7E1 /* XCTElementSetTransformer-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1BA7DD8C206D694B007C7C26 /* XCTElementSetTransformer-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5B9C00B488A31A95F1460727 /* RouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B52E4A09ADEA252A1B8190F /* RouteResponse.m */; }; 5C1E821041979B5FA859B6D0 /* XCUIElement+FBPickerWheel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */; }; 5D1959FC0B6EBB249DD86062 /* XCApplicationQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB71E3B77D600A02D78 /* XCApplicationQuery.h */; }; 5D2A81B261FEC67033F41101 /* XCUIElementQuery+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 71E75E6B254824230099FC87 /* XCUIElementQuery+FBHelpers.h */; }; @@ -554,6 +557,7 @@ 66030D36126676CD334588B3 /* XCUIApplicationProcessTracker-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3238E68F292452D8234153F1 /* XCUIApplicationProcessTracker-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6627D7B40D3D915B115D7B1A /* FBSession-Private.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7891CAEDF0C008C271F /* FBSession-Private.h */; }; 664471C47B921A09884F5C1F /* XCTMessagingRole_EventSynthesis-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 2E2F8B9A21359AC98424DDE0 /* XCTMessagingRole_EventSynthesis-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 667705A8195BF7B0D48180B3 /* RouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 374BB11AE4E4243BCA444CF8 /* RouteRequest.m */; }; 668B1D9AFF85F0538E738675 /* XCUIElementAttributesPrivate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 8EF5BE504321321FB9320C43 /* XCUIElementAttributesPrivate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 67402F46AAB3DCAB3E40ED27 /* FBUnknownCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7641CAEDF0C008C271F /* FBUnknownCommands.h */; }; 6786D4B257269918E591AC66 /* FBElementUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 713C6DCD1DDC772A00285B92 /* FBElementUtils.h */; }; @@ -561,6 +565,7 @@ 67AFD75FBCD4BFC5424A0956 /* XCTMessagingRole_ActivityReporting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EED93C03F27978174C7333C1 /* XCTMessagingRole_ActivityReporting-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 68BAD7FD955D2732151C355D /* XCUIRemoteAccessibilityInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DD2F42015E89D253EA57F63 /* XCUIRemoteAccessibilityInterface-Protocol.h */; }; 68CF77BE3F4B659BAFC99AF1 /* XCUIDeviceDelayedAttachmentTransferSupportInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C19CEEEC9858A106061D1F8 /* XCUIDeviceDelayedAttachmentTransferSupportInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 68EB0B8D53270CB68C53D028 /* WDAAlertIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F75AEC6442F32A3C4394AD /* WDAAlertIntegrationTests.swift */; }; 698A247C297C36C7DEBFCD28 /* XCTMessagingRole_TestReporting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 13AAB3B43FB533B0E9C3F40D /* XCTMessagingRole_TestReporting-Protocol.h */; }; 69D17671BE6F04D5FE909BD1 /* XCTMessagingRole_BundleRequesting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C928E94CBC13E262D818C28E /* XCTMessagingRole_BundleRequesting-Protocol.h */; }; 6A093FDB29E19E1F1F51A6A8 /* FBImageUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 7150348521A6DAD600A0F4BA /* FBImageUtils.h */; }; @@ -568,6 +573,7 @@ 6A4E39AED8C9C8BEBD960710 /* FBImageProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 63CCF91021ECE4C700E94ABD /* FBImageProcessor.h */; }; 6A594CAA5D54658403A031DC /* XCTRunnerAutomationSession-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 01AF8E73DD47455B4854E470 /* XCTRunnerAutomationSession-Protocol.h */; }; 6ACEDEB2C16EFA502ED27FC4 /* XCTMeasureOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = B3FDA51EB36F03592BF48762 /* XCTMeasureOptions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6AF8892AEF5C6BF01896B6DA /* WDAAppLifecycleIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EB1B9A793C9EFB7853C1AA13 /* WDAAppLifecycleIntegrationTests.swift */; }; 6B3AB44BFFDDF042E4B712C1 /* FBPasteboard.h in Headers */ = {isa = PBXBuildFile; fileRef = 71930C4020662E1F00D3AFEC /* FBPasteboard.h */; }; 6B4E076881F3C7F369044682 /* FBXCDeviceEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A47287C4005003243C6 /* FBXCDeviceEvent.h */; }; 6B68382176E9918AF3110710 /* XCTMacCatalystStatusProviding-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 42D2B5A0C490D9698C2A87A9 /* XCTMacCatalystStatusProviding-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -580,7 +586,9 @@ 6E9672EC1A999619AED1EF4D /* XCTSourceCodeContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 530D925D1492C4DF826B46BB /* XCTSourceCodeContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6EA187930FA95A0FB2D0338F /* XCUIElement+FBUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376401D59F81400ED88DD /* XCUIElement+FBUtilities.m */; }; 6EC82BD561787BE315AD369A /* XCTMessagingRole_SiriAutomation-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = FCD1815F2BF21CA0936B04E1 /* XCTMessagingRole_SiriAutomation-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6EDF83DA0A6C7A7E67C30AF6 /* WDADeviceIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646D714BC17FE9258CFBDF55 /* WDADeviceIntegrationTests.swift */; }; 6FC8E41707F65D5A432206CC /* FBXCAccessibilityElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A41287C2A8D003243C6 /* FBXCAccessibilityElement.h */; }; + 7072174F17BA109C6AB2859F /* RouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 374BB11AE4E4243BCA444CF8 /* RouteRequest.m */; }; 70E21F76098198E19A6E5A5E /* FBXCAXClientProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 7157B28F221DADD2001C348C /* FBXCAXClientProxy.h */; }; 711084441DA3AA7500F913D6 /* FBXPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 711084421DA3AA7500F913D6 /* FBXPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; }; @@ -675,29 +683,6 @@ 716F0DA12A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */; }; 716F0DA32A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 716F0DA02A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m */; }; 716F0DA62A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 716F0DA52A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m */; }; - 718226CA2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */; }; - 718226CB2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */; }; - 718226CC2587443700661B83 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C72587443600661B83 /* GCDAsyncSocket.h */; }; - 718226CD2587443700661B83 /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 718226C72587443600661B83 /* GCDAsyncSocket.h */; }; - 718226CE2587443700661B83 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C82587443600661B83 /* GCDAsyncSocket.m */; }; - 718226CF2587443700661B83 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C82587443600661B83 /* GCDAsyncSocket.m */; }; - 718226D02587443700661B83 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */; }; - 718226D12587443700661B83 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */; }; - 71822702258744A400661B83 /* HTTPResponseProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA224913C210060D7EB /* HTTPResponseProxy.h */; }; - 7182270B258744A700661B83 /* Route.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA324913C210060D7EB /* Route.h */; }; - 71822714258744A900661B83 /* RouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCAA24913C220060D7EB /* RouteRequest.h */; }; - 7182271D258744AB00661B83 /* RouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA124913C210060D7EB /* RouteResponse.h */; }; - 71822726258744AE00661B83 /* RoutingConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA524913C210060D7EB /* RoutingConnection.h */; }; - 7182272F258744B000661B83 /* RoutingHTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA724913C210060D7EB /* RoutingHTTPServer.h */; }; - 71822738258744B800661B83 /* HTTPConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC89249131D30060D7EB /* HTTPConnection.h */; }; - 71822741258744BB00661B83 /* HTTPLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8D249131D30060D7EB /* HTTPLogging.h */; }; - 7182274A258744BE00661B83 /* HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC87249131D30060D7EB /* HTTPMessage.h */; }; - 71822753258744C100661B83 /* HTTPResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8F249131D40060D7EB /* HTTPResponse.h */; }; - 7182275C258744C300661B83 /* HTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8B249131D30060D7EB /* HTTPServer.h */; }; - 71822765258744C700661B83 /* HTTPDataResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC60249131890060D7EB /* HTTPDataResponse.h */; }; - 7182276E258744C900661B83 /* HTTPErrorResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC59249131880060D7EB /* HTTPErrorResponse.h */; }; - 71822777258744CE00661B83 /* DDNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7D249131B00060D7EB /* DDNumber.h */; }; - 71822780258744D000661B83 /* DDRange.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7B249131B00060D7EB /* DDRange.h */; }; 7182A87F3CAA27F71B624AD2 /* XCTRunnerAutomationSession-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 01AF8E73DD47455B4854E470 /* XCTRunnerAutomationSession-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 718F49C8230844330045FE8B /* FBProtocolHelpersTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 718F49C7230844330045FE8B /* FBProtocolHelpersTests.m */; }; 718F49C923087ACF0045FE8B /* FBProtocolHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */; }; @@ -808,6 +793,7 @@ 71F5BE51252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; 71F5BE52252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; 727A1A132D139DB3B82808F4 /* XCTFuture.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F044AD574A9837C04F833CB /* XCTFuture.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 72F5BAE6CA919434EA4DA0F3 /* WDAFindIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCED63DFD03326F6351165FE /* WDAFindIntegrationTests.swift */; }; 72FE28D00A69D335B463C03A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 721E280BFFEFFD40C89277AF /* AppDelegate.m */; }; 732D07CE799D8B584B4033E8 /* XCTMessagingRole_DebugLogging-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 471DF7EE3C63C3069FB9D40D /* XCTMessagingRole_DebugLogging-Protocol.h */; }; 734735CAB6A95282CCF098E5 /* XCUIElement+FBFind.m in Sources */ = {isa = PBXBuildFile; fileRef = EEBBD48A1D47746D00656A81 /* XCUIElement+FBFind.m */; }; @@ -839,7 +825,6 @@ 7C29155F72C16DF727A0692C /* XCUIElement+FBClassChain.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAF41E20516B001DA4F2 /* XCUIElement+FBClassChain.m */; }; 7C9C3C651FB37EC3ED8BC4C1 /* XCTReportingSessionConfiguration-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = E859E58C7C717CB6CD2A80BE /* XCTReportingSessionConfiguration-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7CB113A4CAF4C22E962E2097 /* XCTSourceCodeContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 530D925D1492C4DF826B46BB /* XCTSourceCodeContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 7D099F5AD14B24B9FC3FEBF0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1ABD1093739852B52C472B /* Foundation.framework */; }; 7D2D0CCEF44A91BDB34F5943 /* XCTRuntimeDiagnosticsPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = AD0DDEAB427BA845375C7384 /* XCTRuntimeDiagnosticsPolicy.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7D39CABC61A3F585DEC1E6B1 /* XCUIKnobControl.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C78201A2212BEA77979F4FF /* XCUIKnobControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7DFFC02E95770F89F5A63A8F /* FBSettingsHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F3E7D625417FF400E0C22C /* FBSettingsHandler.h */; }; @@ -878,7 +863,6 @@ 88EE4275BFBD9207CBD84959 /* XCTIssue.h in Headers */ = {isa = PBXBuildFile; fileRef = ACB055BCA9CCEAB3DECD1A74 /* XCTIssue.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8934D96BE720E53106DCFA6C /* XCUIElement+FBResolve.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D3B3D4267FC7260076473D /* XCUIElement+FBResolve.m */; }; 8935209ECEC079F126FBDFAC /* XCTSkippedTestContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C32C75CC17179B347DF6F78 /* XCTSkippedTestContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 894AE4397B5992EF738248AE /* RouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCAA24913C220060D7EB /* RouteRequest.h */; }; 8A8D9B0342BC43BEB65749B7 /* XCUILocation.h in Headers */ = {isa = PBXBuildFile; fileRef = A8230FDD93639CE2E9EFE311 /* XCUILocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8AAA33A5B1943B667B0DB05E /* FBMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A51CF7A43900275851 /* FBMacros.h */; }; 8CB293E6451EB1A6D1240BAA /* XCTElementSetTransformer-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1BA7DD8C206D694B007C7C26 /* XCTElementSetTransformer-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -889,16 +873,14 @@ 8EA51935E09A89896FB1D463 /* FBW3CActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097491FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h */; }; 8F36546FCCCBE918C9088111 /* XCTestCaseDiscoveryUIAutomationDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = E29888B75A756D1CCD21604C /* XCTestCaseDiscoveryUIAutomationDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8FC4331C2C06A3E8B8F490E2 /* FBDebugCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7551CAEDF0C008C271F /* FBDebugCommands.m */; }; - 90107B3BBFBF3B073807D51B /* HTTPLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8D249131D30060D7EB /* HTTPLogging.h */; }; 908767A5CCB6552156CDAFB3 /* XCTAttachmentManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D80202DC5D679EF37896431 /* XCTAttachmentManager.h */; }; + 90C6C1E34B9B850CC79BABCB /* RouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B52E4A09ADEA252A1B8190F /* RouteResponse.m */; }; 914D7116A5BF672ACC7F1CE6 /* XCUIInterruptionMonitoring-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 44019CB45CF491B5FDAB8213 /* XCUIInterruptionMonitoring-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; 918A48693FF49E932FDFE3AE /* FBElementHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */; }; - 91FF05D51401D1F1A88FB0B0 /* FBWatchHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = C1CFF432CCF46627AB7315F9 /* FBWatchHTTPServer.m */; }; 923B2C779F413A9EB7023AC5 /* XCUIElement+FBVisibleFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */; }; 92A632860C8133ADDD5DE5F4 /* LRUCacheNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 71414ED12670A1ED003A8C5D /* LRUCacheNode.h */; }; 92DC81D2F6E58386AC592482 /* FBXCTestDaemonsProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = EE35AD7A1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m */; }; 932D338E506BD3EDDABBDEA9 /* XCTNSPredicateExpectationObject-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEC1E3B77D600A02D78 /* XCTNSPredicateExpectationObject-Protocol.h */; }; - 93B85BB5737C85F650FF772F /* WDATypingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 912F32C353FE7C3E7C841405 /* WDATypingIntegrationTests.swift */; }; 93D5C4C8442EFE91D70780CC /* FBScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 715AFABF1FFA29180053896D /* FBScreen.h */; }; 9452D57FE97CB2ECED093B9F /* FBAccessibilityTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */; }; 9472D6847DFCF79A29E201A1 /* FBFailureProofTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89391D0B38640083E92B /* FBFailureProofTestCase.m */; }; @@ -939,7 +921,6 @@ A0A7AB48F1A3AEEC70AF92D7 /* XCTMessagingRole_ForcePressureSupportQuerying-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = AA2351C66A4616534FB81AE4 /* XCTMessagingRole_ForcePressureSupportQuerying-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; A14687C1A2FC70A2356B7839 /* XCUIApplicationImplReporter-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 221BC403F42F61DDB1F11DD0 /* XCUIApplicationImplReporter-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; A14CB090646BCB0B50F213CF /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D7DD32943CAC7838F942ED5 /* ViewController.m */; }; - A15C224F98CDB418E5727697 /* WDAAlertIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28F75AEC6442F32A3C4394AD /* WDAAlertIntegrationTests.swift */; }; A1B2C3D41F001A00A1B0004 /* XCUIDevice+FBVoiceOver.h in Headers */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0001 /* XCUIDevice+FBVoiceOver.h */; settings = {ATTRIBUTES = (Public, ); }; }; A1B2C3D41F001A00A1B0005 /* XCUIDevice+FBVoiceOver.m in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0002 /* XCUIDevice+FBVoiceOver.m */; }; A1B2C3D41F001A00A1B0006 /* XCUIDevice+FBVoiceOver.h in Headers */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0001 /* XCUIDevice+FBVoiceOver.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -958,21 +939,22 @@ A5005275246FC51C1332B7E5 /* XCUIApplication+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 71C8E54F25399A6B008572C1 /* XCUIApplication+FBQuiescence.h */; }; A5353D849D394BC630839E59 /* XCUIElement+FBAccessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7461CAEDF0C008C271F /* XCUIElement+FBAccessibility.m */; }; A5B1AF65212923D21E54560E /* XCUIButtonConsole.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E09842154A44874C4E9CA01 /* XCUIButtonConsole.h */; settings = {ATTRIBUTES = (Public, ); }; }; - A648F711A5BECD8BE680AB79 /* WDAElementAttributeIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848B7B14F95EFE16EEC46045 /* WDAElementAttributeIntegrationTests.swift */; }; A72A2DF58BC103C624F84907 /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 716C9342224D53A1004B8542 /* XCTest.framework */; }; A72A8752161D64BDB5290703 /* XCUISystem.h in Headers */ = {isa = PBXBuildFile; fileRef = DE39D4384E531299FAA143F7 /* XCUISystem.h */; settings = {ATTRIBUTES = (Public, ); }; }; A781B56952349EB47FBF0DAD /* XCTMessagingRole_SystemConfiguration-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EA24F4BE52B2520874E09BEF /* XCTMessagingRole_SystemConfiguration-Protocol.h */; }; A7F0DE38C7CB24D31857C7AE /* XCTSourceCodeContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 530D925D1492C4DF826B46BB /* XCTSourceCodeContext.h */; }; - A8635BD557F97E6C29A0790E /* RoutingHTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA724913C210060D7EB /* RoutingHTTPServer.h */; }; A893F1BC22A6066353FCD515 /* XCUIApplicationProcess+FBQuiescence.m in Sources */ = {isa = PBXBuildFile; fileRef = 71D475C12538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m */; }; A8CEAEFC8CC63F94DA0176A9 /* XCUIDevice+FBVoiceOver.h in Headers */ = {isa = PBXBuildFile; fileRef = A1B2C3D41F001A00A1B0001 /* XCUIDevice+FBVoiceOver.h */; }; A95AE6DC8C54B7E2C3CB2570 /* XCTMessagingRole_SelfDiagnosisIssueReporting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 74AA57E9A81B1B0515CB587F /* XCTMessagingRole_SelfDiagnosisIssueReporting-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; A967F44C38E10C0295AD08FF /* XCTMessagingRole_CapabilityExchange-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 62A682C55077710D1D90ABC3 /* XCTMessagingRole_CapabilityExchange-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - A970B1FBC690CBE536636228 /* WebDriverAgentLib_watchOS.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = D1171249FE5D91AC5D797E5C /* WebDriverAgentLib_watchOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + A970B1FBC690CBE536636228 /* WebDriverAgentLib_watchOS.framework in Copy frameworks */ = {isa = PBXBuildFile; fileRef = D1171249FE5D91AC5D797E5C /* WebDriverAgentLib_watchOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; A9BCCC677A46E48D4D2B05A9 /* FBActiveAppDetectionPoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 13815F6E2328D20400CDAB61 /* FBActiveAppDetectionPoint.m */; }; + AA11BB22CC33DD44EE55FF02 /* FBMjpegServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7155D702211DCEF400166C20 /* FBMjpegServer.m */; }; + AA11BB22CC33DD44EE55FF04 /* WDAMjpegStreamingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD44EE55FF03 /* WDAMjpegStreamingIntegrationTests.swift */; }; AAA213E600F40AB7F43D1015 /* WebDriverAgentLib_tvOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 641EE6F82240C5CA00173FCB /* WebDriverAgentLib_tvOS.framework */; }; AABBCCDDEEFF001122334457 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AABBCCDDEEFF001122334456 /* SceneDelegate.m */; }; AB126E96C642B235EE02B4F1 /* _XCTestObservationPrivate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C840A8703A7C8D48897E158A /* _XCTestObservationPrivate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AB531917FF460EB87E4AD5A2 /* RouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B52E4A09ADEA252A1B8190F /* RouteResponse.m */; }; AB8D3BCC12F3A47869D31341 /* XCTMessagingRole_TestExecution-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 209C65575782ECAA09892D2B /* XCTMessagingRole_TestExecution-Protocol.h */; }; AC3529AFA966E7202CB3B3B1 /* FBRoute.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7841CAEDF0C008C271F /* FBRoute.h */; }; AC8CA9AA80C6ECF4D1405F25 /* XCTMemoryCheckerDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 8EEB78C32D7E6DD6E4EBCD26 /* XCTMemoryCheckerDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -997,7 +979,6 @@ ADEF63AF1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADEF63AE1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m */; }; AEA08D6963D0F969992A56C3 /* XCTMemoryChecker.h in Headers */ = {isa = PBXBuildFile; fileRef = AFD7929A5F0B45397F0D64CB /* XCTMemoryChecker.h */; settings = {ATTRIBUTES = (Public, ); }; }; AF13E087AA2FF233A495CD0A /* XCUIResetAuthorizationStatusOfProtectedResourcesInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 9A0B17F41E4461BBD09A2962 /* XCUIResetAuthorizationStatusOfProtectedResourcesInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - AF4223DDBCC9EC79D4F9DC0D /* DDRange.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7B249131B00060D7EB /* DDRange.h */; }; AF4B652A13BCF1C08AD6ECA0 /* XCTSignpostListener-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 6F9351F7A43851CA5DFF47CD /* XCTSignpostListener-Protocol.h */; }; AF718FF9F916429C65A2FD11 /* XCTRuntimeIssueDetectionPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = B98A9F937EF98D6359FCCC7A /* XCTRuntimeIssueDetectionPolicy.h */; settings = {ATTRIBUTES = (Public, ); }; }; B1A18407C6600D9E8ABABD5A /* XCUIApplicationProcessDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 43FCB89739438814F43BCA24 /* XCUIApplicationProcessDelegate-Protocol.h */; }; @@ -1006,6 +987,7 @@ B316351D2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */ = {isa = PBXBuildFile; fileRef = B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */; }; B316351F2DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */; }; B31635202DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */; }; + B36D8136E49BF48ED389D1DB /* WDAWatchInProcessTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C280328DE3379F9EF701A20 /* WDAWatchInProcessTestCase.swift */; }; B63FBAF7421417011D724B6F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 722E62C63348A451A351524B /* Foundation.framework */; }; B6AB97EF303CCF15017EB07A /* XCUIApplicationProcessDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 43FCB89739438814F43BCA24 /* XCUIApplicationProcessDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; B6B73CE709728984EF03361D /* FBFailureProofTestCase.h in Headers */ = {isa = PBXBuildFile; fileRef = EE6A89381D0B38640083E92B /* FBFailureProofTestCase.h */; }; @@ -1020,7 +1002,6 @@ BC91C1F00C54FE2115DE983F /* FBRunLoopSpinner.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE9B4711CD02B88009D2030 /* FBRunLoopSpinner.m */; }; BC91E1DAF79A56CFC4A77D6E /* XCTReportingSessionTestReporter.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D6B33F48F089C945641DB3F /* XCTReportingSessionTestReporter.h */; settings = {ATTRIBUTES = (Public, ); }; }; BCC63D234BBBA7F8BF8E953D /* XCTRepetitionPolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = A1291FF806FE75EDE239FE44 /* XCTRepetitionPolicy.h */; settings = {ATTRIBUTES = (Public, ); }; }; - BCC9E02491560712221045CC /* HTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8B249131D30060D7EB /* HTTPServer.h */; }; BD2A5D43881B449E533D5800 /* UIKeyboardImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 648C10AA22AAAD9C00B81B9A /* UIKeyboardImpl.h */; }; BD8002B8812AF2CDD76BDF4D /* FBImageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 7150348621A6DAD600A0F4BA /* FBImageUtils.m */; }; BE63FC311D9DD0A98874568B /* XCUIApplicationManaging-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 00834B3220005AD5A5ABEF7C /* XCUIApplicationManaging-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1032,11 +1013,9 @@ C07F140AF96143A0A5CAA2B0 /* XCTestCaseDiscoveryUIAutomationDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = E29888B75A756D1CCD21604C /* XCTestCaseDiscoveryUIAutomationDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; C1414C29C836902466C4D6DB /* XCTestCastMethodNamesUIAutomationDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = A80A1C12FA9899367D316E8C /* XCTestCastMethodNamesUIAutomationDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; C14FD6933C5E978E7E54F44D /* XCTMessagingRole_BundleRequesting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C928E94CBC13E262D818C28E /* XCTMessagingRole_BundleRequesting-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C154B8CD167DBE068EA74719 /* HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC87249131D30060D7EB /* HTTPMessage.h */; }; C158CE0AD6EEBEA854454AA2 /* XCUIApplicationProcessManaging-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C710FE3AB9E9C22BD2A9E84 /* XCUIApplicationProcessManaging-Protocol.h */; }; C1ACCF2EAF1C402FDF4FEFFD /* XCTMessagingRole_PerformanceMeasurementReporting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 2B650ADFEEA2369A10F6C1F1 /* XCTMessagingRole_PerformanceMeasurementReporting-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; C22CA4AEE1C7395AE82B3BD3 /* XCTMessagingRole_PerformanceMeasurementReporting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 2B650ADFEEA2369A10F6C1F1 /* XCTMessagingRole_PerformanceMeasurementReporting-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C2D22426DB9FCE6AD84FA16A /* RouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 97FFAEF000CE312DEBEA9385 /* RouteRequest.m */; }; C2F6BB3D8A49F762E5172768 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7155B419224D5B460042A993 /* libxml2.tbd */; }; C309FAE89D050C90496FB87B /* XCTRemoteSignpostListenerProxy-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 8E47AA1A44A4A3C6EDCB6804 /* XCTRemoteSignpostListenerProxy-Protocol.h */; }; C3A3578B56BF260A77EB1ECF /* XCUIAlertMonitoring-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = DB5797DE5A0B4E7EE3D166F7 /* XCUIAlertMonitoring-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1062,6 +1041,7 @@ C8FB547922D4C1FC00B69954 /* FBUnattachedAppLauncher.h in Headers */ = {isa = PBXBuildFile; fileRef = C8FB547722D4C1FC00B69954 /* FBUnattachedAppLauncher.h */; }; C8FB547A22D4C1FC00B69954 /* FBUnattachedAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = C8FB547822D4C1FC00B69954 /* FBUnattachedAppLauncher.m */; }; C931666B44D9F20B3A1026B0 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */ = {isa = PBXBuildFile; fileRef = 714E14B629805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h */; }; + CA077D3ED0D0BA2590CB85DE /* RouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = D585660F7A04651223F29B07 /* RouteRequest.h */; }; CA1E428789B0D9CB9C874133 /* XCUIKnobControl.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C78201A2212BEA77979F4FF /* XCUIKnobControl.h */; settings = {ATTRIBUTES = (Public, ); }; }; CAA29EF94D29713540C57528 /* XCUIInterruptionMonitoring-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 44019CB45CF491B5FDAB8213 /* XCUIInterruptionMonitoring-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; CB1790B793BB813AF80F65DF /* XCUIElement+FBTVFocuse.h in Headers */ = {isa = PBXBuildFile; fileRef = 641EE7042240CDCF00173FCB /* XCUIElement+FBTVFocuse.h */; }; @@ -1073,13 +1053,13 @@ CE3D815D09AD8E137CC18AA5 /* XCUIElement+FBScrolling.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7491CAEDF0C008C271F /* XCUIElement+FBScrolling.h */; }; CE4ACC757D04A9662BC1D2D0 /* XCTMessagingRole_AttachmentFutureResultStatusUpdating-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 10FBCE8B3A419B970DB7A5CE /* XCTMessagingRole_AttachmentFutureResultStatusUpdating-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; CE57FBF0D8F391F113DAAB91 /* WebDriverAgentLib_watchOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1171249FE5D91AC5D797E5C /* WebDriverAgentLib_watchOS.framework */; }; - CEA3AF02E2BFF31ABB97F6F0 /* WDADeviceIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646D714BC17FE9258CFBDF55 /* WDADeviceIntegrationTests.swift */; }; CF063E3902F2FF73D65CAAA9 /* XCUIAXNotificationHandling-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EA96C2FEDE73CB5148BA4949 /* XCUIAXNotificationHandling-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; CFB0AFBC9F429B279C95F5BC /* FBAlertViewCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7501CAEDF0C008C271F /* FBAlertViewCommands.h */; }; CFDBE0DF5D99CB310C7AFB01 /* XCUIApplicationProcess.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFB1E3B77D600A02D78 /* XCUIApplicationProcess.h */; }; D02E19924A93582C7F4F80AF /* _TtC10XCTestCore19XCTReportingContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DFEF3D0F3F006AC53F9E525 /* _TtC10XCTestCore19XCTReportingContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; D062CA5914608761FE002799 /* FBXCAXClientProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */; }; D101B194EE49F32110C394FD /* NSFastEnumeration-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 58156929D12ADE7DFE10116F /* NSFastEnumeration-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D1172A7F53F89B78D8324A13 /* RouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 374BB11AE4E4243BCA444CF8 /* RouteRequest.m */; }; D1212325FDE77754EE503755 /* FBRouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7881CAEDF0C008C271F /* FBRouteRequest.m */; }; D142183578036F5FD5B9880B /* XCUIDevice+FBHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26961CF2481700F8B5FF /* XCUIDevice+FBHelpers.h */; }; D14718959F7D46949859EEE6 /* FBDebugLogDelegateDecorator.m in Sources */ = {isa = PBXBuildFile; fileRef = EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */; }; @@ -1088,6 +1068,7 @@ D209CAD44BCD87BDF8BE30B1 /* libAccessibility.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 7155B41A224D5B480042A993 /* libAccessibility.tbd */; }; D227087A470351BB2BD58376 /* XCTWaiterWait.h in Headers */ = {isa = PBXBuildFile; fileRef = 8566F4674B2CF640A678C952 /* XCTWaiterWait.h */; settings = {ATTRIBUTES = (Public, ); }; }; D22F20BC746C639DCDD2F69C /* XCTMessagingRole_ProcessMonitoring-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = B43D656732067585371ADD31 /* XCTMessagingRole_ProcessMonitoring-Protocol.h */; }; + D22FED0B3856919DA4BCFA67 /* WDATypingIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 912F32C353FE7C3E7C841405 /* WDATypingIntegrationTests.swift */; }; D234E390A449065FA22F28EF /* XCUIAccessibilityInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D47DD8BF27FE639742EA2E3E /* XCUIAccessibilityInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; D27A70EE9F480A3D1CDDB389 /* XCTMessagingRole_PerformanceMeasurementReporting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 2B650ADFEEA2369A10F6C1F1 /* XCTMessagingRole_PerformanceMeasurementReporting-Protocol.h */; }; D2A463EE081CED34AA988123 /* XCTTestIdentifierSet.h in Headers */ = {isa = PBXBuildFile; fileRef = 7E079E00FE148476F94BC42F /* XCTTestIdentifierSet.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1105,78 +1086,37 @@ D869B58002B298F70EE06B98 /* XCUIElement+FBMinMax.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */; }; D87509BF0784FDFFD57A2EF5 /* FBNotificationsHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 719DCF142601EAFB000E765F /* FBNotificationsHelper.m */; }; D9D6F0D054AF1BDAAFD0E667 /* XCUIXcodeApplicationManaging-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 30ABCB5051B826025F77E360 /* XCUIXcodeApplicationManaging-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - D9FF15E0009D95996128C5E1 /* WDASessionIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27D14C409E982099568175D1 /* WDASessionIntegrationTests.swift */; }; DA26A56D4FE01CA1FAF6F1E7 /* FBOrientationCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB75D1CAEDF0C008C271F /* FBOrientationCommands.m */; }; + DAB4F2FB4C0AB107E461BBA4 /* RouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = 01E7CFEBAD717FCF4BCDD383 /* RouteResponse.h */; }; DADC5E6FD3E80449EE5B5841 /* NSString+FBVisualLength.h in Headers */ = {isa = PBXBuildFile; fileRef = EE0D1F5F1EBCDCF7006A3123 /* NSString+FBVisualLength.h */; }; DAFD9ED6842CE53DBAF9F8EB /* XCTMessagingRole_DebugLogging-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 471DF7EE3C63C3069FB9D40D /* XCTMessagingRole_DebugLogging-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; DBD3777FD8EAD997F04F52A2 /* XCTMessagingRole_ProcessMonitoring-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = B43D656732067585371ADD31 /* XCTMessagingRole_ProcessMonitoring-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD44723878134A0A42473299 /* XCUIApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACF91E3B77D600A02D78 /* XCUIApplication.h */; }; DE2D708340ED70EBF9244D0F /* NSDictionary+FBUtf8SafeDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h */; }; DE75F01A64FBC6A58012E6B4 /* FBXCElementSnapshotDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = F46F78706C5157469122F730 /* FBXCElementSnapshotDouble.m */; }; - DEFE56CC1A4EC88BCCD9BD6D /* WDAWatchHTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F28D3CD9DDC2E54DB115316 /* WDAWatchHTTPClient.swift */; }; DF0FF8388E4C4CFDD1746BF7 /* XCTRunnerIDESessionDelegate-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A5EC96F8F1C3888B9E24FAA /* XCTRunnerIDESessionDelegate-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; DFB9638AF3480053CA39AB3C /* XCUIElement+FBVisibleFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */; }; DFC01347FC6A5308D5C6765D /* XCTMessagingRole_MemoryTesting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 13D863EAE7F6F8B7E42D99B9 /* XCTMessagingRole_MemoryTesting-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; E046D4602540FA7272DCBCB4 /* XCUIElement+FBWebDriverAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */; }; - E101F1B19A8079EAD8457C0C /* WDAWatchIntegrationTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24BD02A29E3B2EB1D57FDD29 /* WDAWatchIntegrationTestCase.swift */; }; + E1653072AC5040C7325DAAED /* WebDriverAgentLib_watchOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D1171249FE5D91AC5D797E5C /* WebDriverAgentLib_watchOS.framework */; }; E19946F12AD25E3EFFD89A36 /* XCTAggregateSuiteRunStatistics.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AA21CEBA6E92AAC73FB6A48 /* XCTAggregateSuiteRunStatistics.h */; settings = {ATTRIBUTES = (Public, ); }; }; - E1C6D95706F0F87BE535E332 /* WDAClickIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C878996F07A9B26E66FC4EAC /* WDAClickIntegrationTests.swift */; }; E1E21D96BC2347D9AC01269D /* XCUIApplicationProcessTracker-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3238E68F292452D8234153F1 /* XCUIApplicationProcessTracker-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E1F20E47472FED558822C1E5 /* WDAElementAttributeIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848B7B14F95EFE16EEC46045 /* WDAElementAttributeIntegrationTests.swift */; }; E1F9DBCDED4574CBF53450BE /* XCUIApplicationAutomationSessionProviding-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 603D97F0F9A5B9D4E6442BF2 /* XCUIApplicationAutomationSessionProviding-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; E29A0688606C24973775C64D /* XCTTestIdentifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 831F292661C60B68B557F14D /* XCTTestIdentifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; E2D4264AE37BE5A1EDEA72AF /* XCTReportingSessionTestContainer-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D5D58975822CD58C8FEF7FEB /* XCTReportingSessionTestContainer-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; E2EEFB9B3D049FB31AF1CFF4 /* XCUISiriService.h in Headers */ = {isa = PBXBuildFile; fileRef = 7076779B6AB29D5AAB5E4D33 /* XCUISiriService.h */; settings = {ATTRIBUTES = (Public, ); }; }; E30D52357A950271BD8EF346 /* XCTReportingSessionTestContainer-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D5D58975822CD58C8FEF7FEB /* XCTReportingSessionTestContainer-Protocol.h */; }; E3653A8F329E8AB4061B0310 /* FBConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9B76A11CF7A43900275851 /* FBConfiguration.h */; }; - E444DC65249131890060D7EB /* HTTPErrorResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC59249131880060D7EB /* HTTPErrorResponse.h */; }; - E444DC67249131890060D7EB /* HTTPDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC5B249131880060D7EB /* HTTPDataResponse.m */; }; - E444DC6C249131890060D7EB /* HTTPDataResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC60249131890060D7EB /* HTTPDataResponse.h */; }; - E444DC6D249131890060D7EB /* HTTPErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC61249131890060D7EB /* HTTPErrorResponse.m */; }; - E444DC81249131B10060D7EB /* DDRange.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7B249131B00060D7EB /* DDRange.h */; }; - E444DC83249131B10060D7EB /* DDNumber.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC7D249131B00060D7EB /* DDNumber.h */; }; - E444DC84249131B10060D7EB /* DDRange.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC7E249131B00060D7EB /* DDRange.m */; }; - E444DC85249131B10060D7EB /* DDNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC7F249131B00060D7EB /* DDNumber.m */; }; - E444DC93249131D40060D7EB /* HTTPMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC87249131D30060D7EB /* HTTPMessage.h */; }; - E444DC95249131D40060D7EB /* HTTPConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC89249131D30060D7EB /* HTTPConnection.h */; }; - E444DC97249131D40060D7EB /* HTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8B249131D30060D7EB /* HTTPServer.h */; }; - E444DC98249131D40060D7EB /* HTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC8C249131D30060D7EB /* HTTPConnection.m */; }; - E444DC99249131D40060D7EB /* HTTPLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8D249131D30060D7EB /* HTTPLogging.h */; }; - E444DC9B249131D40060D7EB /* HTTPResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC8F249131D40060D7EB /* HTTPResponse.h */; }; - E444DC9C249131D40060D7EB /* HTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC90249131D40060D7EB /* HTTPServer.m */; }; - E444DC9D249131D40060D7EB /* HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC91249131D40060D7EB /* HTTPMessage.m */; }; - E444DCAB24913C220060D7EB /* HTTPResponseProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC9F24913C210060D7EB /* HTTPResponseProxy.m */; }; - E444DCAC24913C220060D7EB /* Route.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA024913C210060D7EB /* Route.m */; }; - E444DCAD24913C220060D7EB /* RouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA124913C210060D7EB /* RouteResponse.h */; }; - E444DCAE24913C220060D7EB /* HTTPResponseProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA224913C210060D7EB /* HTTPResponseProxy.h */; }; - E444DCAF24913C220060D7EB /* Route.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA324913C210060D7EB /* Route.h */; }; - E444DCB024913C220060D7EB /* RouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA424913C210060D7EB /* RouteResponse.m */; }; - E444DCB124913C220060D7EB /* RoutingConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA524913C210060D7EB /* RoutingConnection.h */; }; - E444DCB224913C220060D7EB /* RoutingConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA624913C210060D7EB /* RoutingConnection.m */; }; - E444DCB324913C220060D7EB /* RoutingHTTPServer.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA724913C210060D7EB /* RoutingHTTPServer.h */; }; - E444DCB424913C220060D7EB /* RoutingHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA824913C220060D7EB /* RoutingHTTPServer.m */; }; - E444DCB524913C220060D7EB /* RouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA924913C220060D7EB /* RouteRequest.m */; }; - E444DCB624913C220060D7EB /* RouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCAA24913C220060D7EB /* RouteRequest.h */; }; - E444DCBC24917A5E0060D7EB /* HTTPResponseProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC9F24913C210060D7EB /* HTTPResponseProxy.m */; }; - E444DCBE24917A5E0060D7EB /* Route.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA024913C210060D7EB /* Route.m */; }; - E444DCC024917A5E0060D7EB /* RouteRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA924913C220060D7EB /* RouteRequest.m */; }; - E444DCC224917A5E0060D7EB /* RouteResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA424913C210060D7EB /* RouteResponse.m */; }; - E444DCC424917A5E0060D7EB /* RoutingConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA624913C210060D7EB /* RoutingConnection.m */; }; - E444DCC624917A5E0060D7EB /* RoutingHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DCA824913C220060D7EB /* RoutingHTTPServer.m */; }; - E444DCC824917A5E0060D7EB /* HTTPConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC8C249131D30060D7EB /* HTTPConnection.m */; }; - E444DCCB24917A5E0060D7EB /* HTTPMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC91249131D40060D7EB /* HTTPMessage.m */; }; - E444DCCE24917A5E0060D7EB /* HTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC90249131D40060D7EB /* HTTPServer.m */; }; - E444DCD024917A5E0060D7EB /* HTTPDataResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC5B249131880060D7EB /* HTTPDataResponse.m */; }; - E444DCD224917A5E0060D7EB /* HTTPErrorResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC61249131890060D7EB /* HTTPErrorResponse.m */; }; - E444DCD424917A5E0060D7EB /* DDNumber.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC7F249131B00060D7EB /* DDNumber.m */; }; - E444DCD624917A5E0060D7EB /* DDRange.m in Sources */ = {isa = PBXBuildFile; fileRef = E444DC7E249131B00060D7EB /* DDRange.m */; }; E47C0E7ED1200567EC7DA4FC /* XCUIApplicationProcess+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 71D475C02538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.h */; }; E4F38E2031A5FB938468C536 /* XCTIssueHandling-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = CD4DA0D55FD20614EDA82368 /* XCTIssueHandling-Protocol.h */; }; + E4FA72A388B36E0B6C41C421 /* RouteRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = D585660F7A04651223F29B07 /* RouteRequest.h */; }; E571286CBEA891946EED7C70 /* XCTSourceCodeLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = 81BB54ECBFB5B7B680BB5D4F /* XCTSourceCodeLocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; E608A983E2E6A6A1A46A6E91 /* XCUIDeviceAutomationModeInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CF2F53B94CC13C628FC7760 /* XCUIDeviceAutomationModeInterface-Protocol.h */; }; E62AED48CAE64088C1E7498B /* XCUIElement+FBPickerWheel.h in Headers */ = {isa = PBXBuildFile; fileRef = 7136A4771E8918E60024FC3D /* XCUIElement+FBPickerWheel.h */; }; + E66195839119DC5A8BB7A5D9 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD1ABD1093739852B52C472B /* Foundation.framework */; }; E6B214145FE46BBFD824FAE7 /* FBMathUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EE1888381DA661C400307AA8 /* FBMathUtils.h */; }; E78F581F8E7530B24AC31A8B /* XCTMessagingRole_UIAutomation-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 92182E4007B37665AB8CD88E /* XCTMessagingRole_UIAutomation-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - E7DF976BD298CC4815946781 /* WDAUnknownCommandIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C90E6C9835DC2FDB3CB6501 /* WDAUnknownCommandIntegrationTests.swift */; }; E8E894CD81C41AC4467A4F4C /* XCTCapabilities.h in Headers */ = {isa = PBXBuildFile; fileRef = DB5400B170F08C71779CCD0E /* XCTCapabilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; E8E917CF8B3ADB4F24CD0B96 /* _TtC10XCTestCore19XCTReportingContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DFEF3D0F3F006AC53F9E525 /* _TtC10XCTestCore19XCTReportingContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; E8FE8448E0118775E7C789CC /* FBXMLGenerationOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */; }; @@ -1193,7 +1133,6 @@ ECC4ABB5477FBA3D442559B1 /* AppDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 242FCA9DD0E30D748E0A1969 /* AppDelegate.h */; }; ED055540E72DDD419F88EEA4 /* XCUIApplicationProcessDelay.m in Sources */ = {isa = PBXBuildFile; fileRef = 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */; }; ED342D194B20A206864675B2 /* XCUIApplication+FBUIInterruptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 716C9DFE27315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h */; }; - EDB13FDB5D8220A65560629B /* HTTPErrorResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC59249131880060D7EB /* HTTPErrorResponse.h */; }; EDC993E0858F89F73D8E9DF8 /* FBCustomCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7531CAEDF0C008C271F /* FBCustomCommands.m */; }; EDF7C92DC3B9860FC08BE3DC /* XCTFuture.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F044AD574A9837C04F833CB /* XCTFuture.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE006EAD1EB99B15006900A4 /* FBElementVisibilityTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE006EAC1EB99B15006900A4 /* FBElementVisibilityTests.m */; }; @@ -1261,7 +1200,6 @@ EE2202131ECC612200A29571 /* FBIntegrationTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1E06D91D1808C2007CF043 /* FBIntegrationTestCase.m */; }; EE2202171ECC612200A29571 /* WebDriverAgentLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; }; EE22021E1ECC618900A29571 /* FBTapTest.m in Sources */ = {isa = PBXBuildFile; fileRef = EE26409A1D0EB5E8009BE6B0 /* FBTapTest.m */; }; - EE22974A8F641FA94DCBCB3E /* HTTPDataResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DC60249131890060D7EB /* HTTPDataResponse.h */; }; EE26409D1D0EBA25009BE6B0 /* FBElementAttributeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE26409C1D0EBA25009BE6B0 /* FBElementAttributeTests.m */; }; EE35AD151E3B77D600A02D78 /* CDStructures.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACA41E3B77D600A02D78 /* CDStructures.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE35AD281E3B77D600A02D78 /* XCApplicationQuery.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACB71E3B77D600A02D78 /* XCApplicationQuery.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -1364,7 +1302,6 @@ F036DAC96136D88F0427B9CB /* XCUIElementTypeQueryProvider_Private-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = AF208CBAC82CDFF8B6F88867 /* XCUIElementTypeQueryProvider_Private-Protocol.h */; }; F03F1D8CFEA1D26423192BF0 /* XCTExpectedFailureContextManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 9AF0584AD9B6D0A57012C978 /* XCTExpectedFailureContextManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; F044875736461509BDFC72B9 /* XCTMessagingRole_HIDEventRecording-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 94D7F0C1E30FBDB5D2583908 /* XCTMessagingRole_HIDEventRecording-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - F0F7B8DFB14C9DFC86DFFBFD /* RouteResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA124913C210060D7EB /* RouteResponse.h */; }; F12C29FCEE471095C1FA8A7A /* XCUIDeviceEventAndStateInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 003AAAB9CB5FB38E45E05F6F /* XCUIDeviceEventAndStateInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; F12D854E1692CE11249A9762 /* XCTReportingSession.h in Headers */ = {isa = PBXBuildFile; fileRef = CB69A2606052D5979C5A436F /* XCTReportingSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; F13426E5482242AFB787BE4A /* FBActiveAppDetectionPoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 13815F6D2328D20400CDAB61 /* FBActiveAppDetectionPoint.h */; }; @@ -1395,8 +1332,8 @@ FAC85D261CAD26DB5F9D804C /* XCUIPlatformApplicationServicesProviding-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 3BA71BCF1FDA482419CA8596 /* XCUIPlatformApplicationServicesProviding-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; FBAFC553152CE132D6CB98E5 /* XCUIDeviceDelayedAttachmentTransferSupportInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C19CEEEC9858A106061D1F8 /* XCUIDeviceDelayedAttachmentTransferSupportInterface-Protocol.h */; }; FBB82323D9D57F069440BF94 /* XCTMessagingRole_SystemConfiguration-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EA24F4BE52B2520874E09BEF /* XCTMessagingRole_SystemConfiguration-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - FBDF04E5916B91FCEC0190CB /* Route.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA324913C210060D7EB /* Route.h */; }; FBF064E4D96CFCD08E1F4EF7 /* XCUIDevice.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFD1E3B77D600A02D78 /* XCUIDevice.h */; }; + FBFEC05ED2C01D1EAE34BE9A /* WDAScreenshotAndSourceIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75F677B5B7737E7E1F321C20 /* WDAScreenshotAndSourceIntegrationTests.swift */; }; FC607DB5132FEF425237267B /* XCTMessagingRole_MemoryTesting-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 13D863EAE7F6F8B7E42D99B9 /* XCTMessagingRole_MemoryTesting-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; FC73EE781D24A598B44C3337 /* XCUIElement+FBScrolling.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB74A1CAEDF0C008C271F /* XCUIElement+FBScrolling.m */; }; FD89236D119E129E8CEBDBCD /* XCTMessagingRole_HIDEventRecording-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 94D7F0C1E30FBDB5D2583908 /* XCTMessagingRole_HIDEventRecording-Protocol.h */; }; @@ -1405,12 +1342,18 @@ FDEB571007C83EC56F365EB3 /* XCTMessagingRole_EventSynthesis-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 2E2F8B9A21359AC98424DDE0 /* XCTMessagingRole_EventSynthesis-Protocol.h */; }; FEB143DBE613795D4F8693B4 /* FBSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB78A1CAEDF0C008C271F /* FBSession.h */; }; FEC3A97A4929115A192F947A /* XCUIDeviceEventAndStateInterface-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 003AAAB9CB5FB38E45E05F6F /* XCUIDeviceEventAndStateInterface-Protocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; - FEC52AE4F35AD38D3AC2659F /* RoutingConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = E444DCA524913C210060D7EB /* RoutingConnection.h */; }; FFA7672B731B57AB9283DD40 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; }; FFD70914E6D8CE5D4FED4B69 /* XCUIAXNotificationHandling-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = EA96C2FEDE73CB5148BA4949 /* XCUIAXNotificationHandling-Protocol.h */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 61F47E68EDECE16439DC2F57 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9D6B02D8FC050BAF7159284F; + remoteInfo = IntegrationApp_watchOS; + }; 641EE6FA2240C5F400173FCB /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; @@ -1425,6 +1368,13 @@ remoteGlobalIDString = 641EE5D52240C5CA00173FCB; remoteInfo = WebDriverAgentLib_tvOS; }; + 79A7BAB354E176E532A7A562 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 95186DE2383671DFA81D7FCB; + remoteInfo = WebDriverAgentLib_watchOS; + }; 844B4A2ED5B8EE2FDD98DAD6 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; @@ -1453,13 +1403,6 @@ remoteGlobalIDString = 641EE5D52240C5CA00173FCB; remoteInfo = WebDriverAgentLib_tvOS; }; - D564026BFC4C47F0187BD64C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 9D6B02D8FC050BAF7159284F; - remoteInfo = IntegrationApp_watchOS; - }; EE158B5B1CBD462500A3E3F0 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 91F9DAE11B99DBC2001349B2 /* Project object */; @@ -1525,7 +1468,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - A970B1FBC690CBE536636228 /* WebDriverAgentLib_watchOS.framework in CopyFiles */, + A970B1FBC690CBE536636228 /* WebDriverAgentLib_watchOS.framework in Copy frameworks */, ); name = "Copy frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -1555,6 +1498,10 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GCDAsyncUdpSocket.h; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h; sourceTree = SOURCE_ROOT; }; + 718226C72587443600661B83 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GCDAsyncSocket.h; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h; sourceTree = SOURCE_ROOT; }; + 718226C82587443600661B83 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GCDAsyncSocket.m; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m; sourceTree = SOURCE_ROOT; }; + 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GCDAsyncUdpSocket.m; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m; sourceTree = SOURCE_ROOT; }; FBCAFE000000000000002001 /* FBImageUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBImageUtilsTests.m; sourceTree = ""; }; FBCAFE000000000000001001 /* FBVideoStreamSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBVideoStreamSession.h; sourceTree = ""; }; FBCAFE000000000000001004 /* FBVideoStreamSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBVideoStreamSession.m; sourceTree = ""; }; @@ -1603,14 +1550,15 @@ FBCAFE000000000000005331 /* FBBroadcastPickerHost.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBBroadcastPickerHost.m; sourceTree = ""; }; 003AAAB9CB5FB38E45E05F6F /* XCUIDeviceEventAndStateInterface-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIDeviceEventAndStateInterface-Protocol.h"; sourceTree = ""; }; 00834B3220005AD5A5ABEF7C /* XCUIApplicationManaging-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIApplicationManaging-Protocol.h"; sourceTree = ""; }; + 00B1F89716AFE04C8509B916 /* FBHTTPServer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = FBHTTPServer.h; sourceTree = ""; }; 01AF8E73DD47455B4854E470 /* XCTRunnerAutomationSession-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTRunnerAutomationSession-Protocol.h"; sourceTree = ""; }; + 01E7CFEBAD717FCF4BCDD383 /* RouteResponse.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = RouteResponse.h; sourceTree = ""; }; 036CD60C5DFA1644E3D51289 /* XCTMessagingRole_UserPresence-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_UserPresence-Protocol.h"; sourceTree = ""; }; 059F291E7D13B17580B4AD43 /* _XCTMessaging_VoidProtocol-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "_XCTMessaging_VoidProtocol-Protocol.h"; sourceTree = ""; }; 0DC62BF635704E9C72AF533E /* XCUIElementEventTarget-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIElementEventTarget-Protocol.h"; sourceTree = ""; }; 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBMinMax.m"; sourceTree = ""; }; 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBMinMax.h"; sourceTree = ""; }; 0F044AD574A9837C04F833CB /* XCTFuture.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTFuture.h; sourceTree = ""; }; - 0F28D3CD9DDC2E54DB115316 /* WDAWatchHTTPClient.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAWatchHTTPClient.swift; sourceTree = ""; }; 0F719C2804F6473A9C4D3090 /* SceneDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; 0F8E4FAAC62765A3405F90D6 /* _XCTestObservationInternal-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "_XCTestObservationInternal-Protocol.h"; sourceTree = ""; }; 100F37231BF17CA9BB91DF16 /* XCTMessagingChannel_RunnerToIDE-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingChannel_RunnerToIDE-Protocol.h"; sourceTree = ""; }; @@ -1634,15 +1582,13 @@ 13FFF2F0287DBEE600E561E4 /* XCElementSnapshotDouble.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XCElementSnapshotDouble.h; sourceTree = ""; }; 13FFF2F1287DBEE600E561E4 /* XCElementSnapshotDouble.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = XCElementSnapshotDouble.m; sourceTree = ""; }; 16223B3E144418CC980D160E /* FBXCAccessibilityElementDouble.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCAccessibilityElementDouble.m; sourceTree = ""; }; + 167CC387B5F9D62618B48F80 /* IntegrationTests_watchOS-Bridging-Header.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "IntegrationTests_watchOS-Bridging-Header.h"; sourceTree = ""; }; 1BA7DD8C206D694B007C7C26 /* XCTElementSetTransformer-Protocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCTElementSetTransformer-Protocol.h"; sourceTree = ""; }; 1E09842154A44874C4E9CA01 /* XCUIButtonConsole.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCUIButtonConsole.h; sourceTree = ""; }; 209C65575782ECAA09892D2B /* XCTMessagingRole_TestExecution-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_TestExecution-Protocol.h"; sourceTree = ""; }; 221BC403F42F61DDB1F11DD0 /* XCUIApplicationImplReporter-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIApplicationImplReporter-Protocol.h"; sourceTree = ""; }; 242FCA9DD0E30D748E0A1969 /* AppDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 24BD02A29E3B2EB1D57FDD29 /* WDAWatchIntegrationTestCase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAWatchIntegrationTestCase.swift; sourceTree = ""; }; - 27D14C409E982099568175D1 /* WDASessionIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDASessionIntegrationTests.swift; sourceTree = ""; }; 28F75AEC6442F32A3C4394AD /* WDAAlertIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAAlertIntegrationTests.swift; sourceTree = ""; }; - 2B2328F1BEDB7C3DFBC22E9A /* IntegrationTests_watchOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_watchOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2B650ADFEEA2369A10F6C1F1 /* XCTMessagingRole_PerformanceMeasurementReporting-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_PerformanceMeasurementReporting-Protocol.h"; sourceTree = ""; }; 2BA6647ADE7B44F13513065A /* XCTMessagingRole_TelemetrySending-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_TelemetrySending-Protocol.h"; sourceTree = ""; }; 2CA02992F03AE1E134F2CAF5 /* XCTPromise.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTPromise.h; sourceTree = ""; }; @@ -1658,13 +1604,12 @@ 315A15082518D6F400A3A064 /* TouchViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchViewController.h; sourceTree = ""; }; 315A15092518D6F400A3A064 /* TouchViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TouchViewController.m; sourceTree = ""; }; 3238E68F292452D8234153F1 /* XCUIApplicationProcessTracker-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIApplicationProcessTracker-Protocol.h"; sourceTree = ""; }; - 341B5F150DED296FF38FB5F7 /* FBWatchHTTPServer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = FBWatchHTTPServer.h; sourceTree = ""; }; + 374BB11AE4E4243BCA444CF8 /* RouteRequest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = RouteRequest.m; sourceTree = ""; }; 38E3FC945B8BF6F6AFD73EE7 /* XCTElementSnapshotAttributeDataSource-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTElementSnapshotAttributeDataSource-Protocol.h"; sourceTree = ""; }; 3A53731B41356D9B4E723A4D /* FBTVFocusIntegrationTests.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = FBTVFocusIntegrationTests.m; sourceTree = ""; }; 3B3F8E1F3F489A3A94A61ADB /* XCTMessagingChannel_IDEToRunner-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingChannel_IDEToRunner-Protocol.h"; sourceTree = ""; }; 3BA71BCF1FDA482419CA8596 /* XCUIPlatformApplicationServicesProviding-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIPlatformApplicationServicesProviding-Protocol.h"; sourceTree = ""; }; 3C710FE3AB9E9C22BD2A9E84 /* XCUIApplicationProcessManaging-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIApplicationProcessManaging-Protocol.h"; sourceTree = ""; }; - 3C90E6C9835DC2FDB3CB6501 /* WDAUnknownCommandIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAUnknownCommandIntegrationTests.swift; sourceTree = ""; }; 3DD2F42015E89D253EA57F63 /* XCUIRemoteAccessibilityInterface-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIRemoteAccessibilityInterface-Protocol.h"; sourceTree = ""; }; 42D2B5A0C490D9698C2A87A9 /* XCTMacCatalystStatusProviding-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMacCatalystStatusProviding-Protocol.h"; sourceTree = ""; }; 42F7AB68482BA168011C52EA /* XCTTestSelection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTTestSelection.h; sourceTree = ""; }; @@ -1675,6 +1620,7 @@ 4951F55904679A17CDFEC186 /* XCTMessagingRole_UIAutomationRunnerEventReporting-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_UIAutomationRunnerEventReporting-Protocol.h"; sourceTree = ""; }; 49D8AC825D239A4A1D834F62 /* XCTestCaseUIAutomationDelegate-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTestCaseUIAutomationDelegate-Protocol.h"; sourceTree = ""; }; 4AEAD1CF473F6AD60333E9FF /* XCUIApplicationOpenRequest.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCUIApplicationOpenRequest.h; sourceTree = ""; }; + 4B52E4A09ADEA252A1B8190F /* RouteResponse.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = RouteResponse.m; sourceTree = ""; }; 4C19CEEEC9858A106061D1F8 /* XCUIDeviceDelayedAttachmentTransferSupportInterface-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIDeviceDelayedAttachmentTransferSupportInterface-Protocol.h"; sourceTree = ""; }; 4E2F683D8A6C6EAAEC8B080A /* XCTMessagingRole_SignpostRequesting-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_SignpostRequesting-Protocol.h"; sourceTree = ""; }; 525D62A58488A52AA1BFE94A /* XCTReportingSessionIssueReporter-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTReportingSessionIssueReporter-Protocol.h"; sourceTree = ""; }; @@ -1789,10 +1735,6 @@ 716F0DA52A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NSDictionaryFBUtf8SafeTests.m; sourceTree = ""; }; 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TVOSSettings.xcconfig; sourceTree = ""; }; 717C0D862518ED7000CAA6EC /* TVOSTestSettings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TVOSTestSettings.xcconfig; sourceTree = ""; }; - 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GCDAsyncUdpSocket.h; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.h; sourceTree = SOURCE_ROOT; }; - 718226C72587443600661B83 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GCDAsyncSocket.h; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.h; sourceTree = SOURCE_ROOT; }; - 718226C82587443600661B83 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GCDAsyncSocket.m; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m; sourceTree = SOURCE_ROOT; }; - 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GCDAsyncUdpSocket.m; path = WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m; sourceTree = SOURCE_ROOT; }; 7183E8C2B556594311CB8898 /* XCUIRemoteSiriInterface-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIRemoteSiriInterface-Protocol.h"; sourceTree = ""; }; 718F49C7230844330045FE8B /* FBProtocolHelpersTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBProtocolHelpersTests.m; sourceTree = ""; }; 71930C4020662E1F00D3AFEC /* FBPasteboard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBPasteboard.h; sourceTree = ""; }; @@ -1887,9 +1829,9 @@ 912F32C353FE7C3E7C841405 /* WDATypingIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDATypingIntegrationTests.swift; sourceTree = ""; }; 92182E4007B37665AB8CD88E /* XCTMessagingRole_UIAutomation-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_UIAutomation-Protocol.h"; sourceTree = ""; }; 94D7F0C1E30FBDB5D2583908 /* XCTMessagingRole_HIDEventRecording-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_HIDEventRecording-Protocol.h"; sourceTree = ""; }; - 97FFAEF000CE312DEBEA9385 /* RouteRequest.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = RouteRequest.m; sourceTree = ""; }; 9A0B17F41E4461BBD09A2962 /* XCUIResetAuthorizationStatusOfProtectedResourcesInterface-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIResetAuthorizationStatusOfProtectedResourcesInterface-Protocol.h"; sourceTree = ""; }; 9AF0584AD9B6D0A57012C978 /* XCTExpectedFailureContextManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTExpectedFailureContextManager.h; sourceTree = ""; }; + 9C280328DE3379F9EF701A20 /* WDAWatchInProcessTestCase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAWatchInProcessTestCase.swift; sourceTree = ""; }; 9DD3770A67ED561EBE5E443A /* main.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; A1291FF806FE75EDE239FE44 /* XCTRepetitionPolicy.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTRepetitionPolicy.h; sourceTree = ""; }; A1B2C3D41F001A00A1B0001 /* XCUIDevice+FBVoiceOver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIDevice+FBVoiceOver.h"; sourceTree = ""; }; @@ -1900,9 +1842,11 @@ A80A1C12FA9899367D316E8C /* XCTestCastMethodNamesUIAutomationDelegate-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTestCastMethodNamesUIAutomationDelegate-Protocol.h"; sourceTree = ""; }; A8230FDD93639CE2E9EFE311 /* XCUILocation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCUILocation.h; sourceTree = ""; }; A87AE5544E9A5CA7C9168DDF /* SceneDelegate.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; + AA11BB22CC33DD44EE55FF03 /* WDAMjpegStreamingIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAMjpegStreamingIntegrationTests.swift; sourceTree = ""; }; AA2351C66A4616534FB81AE4 /* XCTMessagingRole_ForcePressureSupportQuerying-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_ForcePressureSupportQuerying-Protocol.h"; sourceTree = ""; }; AABBCCDDEEFF001122334455 /* SceneDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; AABBCCDDEEFF001122334456 /* SceneDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; + AADFEA2ED9E61A8C1A99B2D7 /* FBHTTPServer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = FBHTTPServer.m; sourceTree = ""; }; AAE921136A147FD01D630869 /* WatchSpikeApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WatchSpikeApp.swift; sourceTree = ""; }; ACA330765D30E21E3EEB163D /* IntegrationTests_tvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_tvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; ACB055BCA9CCEAB3DECD1A74 /* XCTIssue.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTIssue.h; sourceTree = ""; }; @@ -1930,12 +1874,9 @@ B38AC76FAF9275974F272DE9 /* XCUIEventRecording-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIEventRecording-Protocol.h"; sourceTree = ""; }; B3FDA51EB36F03592BF48762 /* XCTMeasureOptions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTMeasureOptions.h; sourceTree = ""; }; B43D656732067585371ADD31 /* XCTMessagingRole_ProcessMonitoring-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_ProcessMonitoring-Protocol.h"; sourceTree = ""; }; - B6F479A809ED48C9D0604659 /* RouteRequest.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = RouteRequest.h; sourceTree = ""; }; B8A163261EFA440E42CA6AC1 /* XCTRunnerDaemonSessionUIAutomationDelegate-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTRunnerDaemonSessionUIAutomationDelegate-Protocol.h"; sourceTree = ""; }; B98A9F937EF98D6359FCCC7A /* XCTRuntimeIssueDetectionPolicy.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTRuntimeIssueDetectionPolicy.h; sourceTree = ""; }; BCED63DFD03326F6351165FE /* WDAFindIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAFindIntegrationTests.swift; sourceTree = ""; }; - C1CFF432CCF46627AB7315F9 /* FBWatchHTTPServer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = FBWatchHTTPServer.m; sourceTree = ""; }; - C31A91BD7553670CBF19500F /* RouteResponse.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = RouteResponse.h; sourceTree = ""; }; C840A8703A7C8D48897E158A /* _XCTestObservationPrivate-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "_XCTestObservationPrivate-Protocol.h"; sourceTree = ""; }; C878996F07A9B26E66FC4EAC /* WDAClickIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAClickIntegrationTests.swift; sourceTree = ""; }; C8FB547322D3949C00B69954 /* LSApplicationWorkspace.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSApplicationWorkspace.h; sourceTree = ""; }; @@ -1949,6 +1890,7 @@ CFE8F33794194FA3EB790C46 /* WebDriverAgentRunner_watchOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WebDriverAgentRunner_watchOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; D1171249FE5D91AC5D797E5C /* WebDriverAgentLib_watchOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WebDriverAgentLib_watchOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D47DD8BF27FE639742EA2E3E /* XCUIAccessibilityInterface-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIAccessibilityInterface-Protocol.h"; sourceTree = ""; }; + D585660F7A04651223F29B07 /* RouteRequest.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = RouteRequest.h; sourceTree = ""; }; D5D58975822CD58C8FEF7FEB /* XCTReportingSessionTestContainer-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTReportingSessionTestContainer-Protocol.h"; sourceTree = ""; }; DB5400B170F08C71779CCD0E /* XCTCapabilities.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTCapabilities.h; sourceTree = ""; }; DB5797DE5A0B4E7EE3D166F7 /* XCUIAlertMonitoring-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIAlertMonitoring-Protocol.h"; sourceTree = ""; }; @@ -1957,41 +1899,12 @@ E005FEDAF49DCFA9FB77BEF3 /* IntegrationApp_tvOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationApp_tvOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; E29888B75A756D1CCD21604C /* XCTestCaseDiscoveryUIAutomationDelegate-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTestCaseDiscoveryUIAutomationDelegate-Protocol.h"; sourceTree = ""; }; E2F99C1A19D7B6D5B872D084 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E444DC59249131880060D7EB /* HTTPErrorResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPErrorResponse.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h; sourceTree = SOURCE_ROOT; }; - E444DC5B249131880060D7EB /* HTTPDataResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPDataResponse.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m; sourceTree = SOURCE_ROOT; }; - E444DC60249131890060D7EB /* HTTPDataResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPDataResponse.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h; sourceTree = SOURCE_ROOT; }; - E444DC61249131890060D7EB /* HTTPErrorResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPErrorResponse.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m; sourceTree = SOURCE_ROOT; }; - E444DC7B249131B00060D7EB /* DDRange.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DDRange.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h; sourceTree = SOURCE_ROOT; }; - E444DC7D249131B00060D7EB /* DDNumber.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DDNumber.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h; sourceTree = SOURCE_ROOT; }; - E444DC7E249131B00060D7EB /* DDRange.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DDRange.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m; sourceTree = SOURCE_ROOT; }; - E444DC7F249131B00060D7EB /* DDNumber.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DDNumber.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m; sourceTree = SOURCE_ROOT; }; - E444DC87249131D30060D7EB /* HTTPMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPMessage.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h; sourceTree = SOURCE_ROOT; }; - E444DC89249131D30060D7EB /* HTTPConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPConnection.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h; sourceTree = SOURCE_ROOT; }; - E444DC8B249131D30060D7EB /* HTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPServer.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h; sourceTree = SOURCE_ROOT; }; - E444DC8C249131D30060D7EB /* HTTPConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPConnection.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m; sourceTree = SOURCE_ROOT; }; - E444DC8D249131D30060D7EB /* HTTPLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPLogging.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h; sourceTree = SOURCE_ROOT; }; - E444DC8F249131D40060D7EB /* HTTPResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPResponse.h; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h; sourceTree = SOURCE_ROOT; }; - E444DC90249131D40060D7EB /* HTTPServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPServer.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m; sourceTree = SOURCE_ROOT; }; - E444DC91249131D40060D7EB /* HTTPMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPMessage.m; path = WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m; sourceTree = SOURCE_ROOT; }; - E444DC9F24913C210060D7EB /* HTTPResponseProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HTTPResponseProxy.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m; sourceTree = SOURCE_ROOT; }; - E444DCA024913C210060D7EB /* Route.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Route.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m; sourceTree = SOURCE_ROOT; }; - E444DCA124913C210060D7EB /* RouteResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RouteResponse.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h; sourceTree = SOURCE_ROOT; }; - E444DCA224913C210060D7EB /* HTTPResponseProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HTTPResponseProxy.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h; sourceTree = SOURCE_ROOT; }; - E444DCA324913C210060D7EB /* Route.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Route.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h; sourceTree = SOURCE_ROOT; }; - E444DCA424913C210060D7EB /* RouteResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RouteResponse.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m; sourceTree = SOURCE_ROOT; }; - E444DCA524913C210060D7EB /* RoutingConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RoutingConnection.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h; sourceTree = SOURCE_ROOT; }; - E444DCA624913C210060D7EB /* RoutingConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RoutingConnection.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m; sourceTree = SOURCE_ROOT; }; - E444DCA724913C210060D7EB /* RoutingHTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RoutingHTTPServer.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h; sourceTree = SOURCE_ROOT; }; - E444DCA824913C220060D7EB /* RoutingHTTPServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RoutingHTTPServer.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m; sourceTree = SOURCE_ROOT; }; - E444DCA924913C220060D7EB /* RouteRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RouteRequest.m; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m; sourceTree = SOURCE_ROOT; }; - E444DCAA24913C220060D7EB /* RouteRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RouteRequest.h; path = WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h; sourceTree = SOURCE_ROOT; }; E46239748EC4A6BFBC13F28B /* XCTMetricDiagnosticHelper.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = XCTMetricDiagnosticHelper.h; sourceTree = ""; }; E859E58C7C717CB6CD2A80BE /* XCTReportingSessionConfiguration-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTReportingSessionConfiguration-Protocol.h"; sourceTree = ""; }; EA24F4BE52B2520874E09BEF /* XCTMessagingRole_SystemConfiguration-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_SystemConfiguration-Protocol.h"; sourceTree = ""; }; EA96C2FEDE73CB5148BA4949 /* XCUIAXNotificationHandling-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIAXNotificationHandling-Protocol.h"; sourceTree = ""; }; EB1B9A793C9EFB7853C1AA13 /* WDAAppLifecycleIntegrationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WDAAppLifecycleIntegrationTests.swift; sourceTree = ""; }; EC8B17452E38AA1AE03AE251 /* XCTElementSnapshotProvider-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTElementSnapshotProvider-Protocol.h"; sourceTree = ""; }; - ED271671803AAF7188E828CF /* RouteResponse.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = RouteResponse.m; sourceTree = ""; }; EE006EAC1EB99B15006900A4 /* FBElementVisibilityTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementVisibilityTests.m; sourceTree = ""; }; EE006EB21EBA1C7B006900A4 /* XCElementSnapshotHitPointTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCElementSnapshotHitPointTests.m; sourceTree = ""; }; EE05BAF91D13003C00A3EB00 /* FBKeyboardTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBKeyboardTests.m; sourceTree = ""; }; @@ -2166,6 +2079,7 @@ EEE9B4701CD02B88009D2030 /* FBRunLoopSpinner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBRunLoopSpinner.h; sourceTree = ""; }; EEE9B4711CD02B88009D2030 /* FBRunLoopSpinner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRunLoopSpinner.m; sourceTree = ""; }; EEF9882A1C486603005CA669 /* WebDriverAgentRunner.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WebDriverAgentRunner.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F089699F492CFC82E55D84D7 /* IntegrationTests_watchOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_watchOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F0D85EFE3ACAC48EEE0F8348 /* XCTMessagingRole_ScreenRecording-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCTMessagingRole_ScreenRecording-Protocol.h"; sourceTree = ""; }; F2034DFA5DEC7D26F32590EF /* XCUIRemoteDeviceRunner-Protocol.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "XCUIRemoteDeviceRunner-Protocol.h"; sourceTree = ""; }; F46F78706C5157469122F730 /* FBXCElementSnapshotDouble.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCElementSnapshotDouble.m; sourceTree = ""; }; @@ -2202,6 +2116,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 30DBE8D4E4AB99F4C518DD6C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E1653072AC5040C7325DAAED /* WebDriverAgentLib_watchOS.framework in Frameworks */, + E66195839119DC5A8BB7A5D9 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 57D58DEF6D8CF9A5E59668A1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2246,14 +2169,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - ECA2D9FDA872B089E60C5289 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 7D099F5AD14B24B9FC3FEBF0 /* Foundation.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; EE158A951CBD452B00A3E3F0 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2326,19 +2241,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 12A83FB6068CC1266BBDBE2C /* WatchOS */ = { - isa = PBXGroup; - children = ( - 97FFAEF000CE312DEBEA9385 /* RouteRequest.m */, - ED271671803AAF7188E828CF /* RouteResponse.m */, - C1CFF432CCF46627AB7315F9 /* FBWatchHTTPServer.m */, - B6F479A809ED48C9D0604659 /* RouteRequest.h */, - C31A91BD7553670CBF19500F /* RouteResponse.h */, - 341B5F150DED296FF38FB5F7 /* FBWatchHTTPServer.h */, - ); - path = WatchOS; - sourceTree = ""; - }; 498495C81BB2E6FA009CC848 /* Resources */ = { isa = PBXGroup; children = ( @@ -2436,17 +2338,6 @@ name = iOS; sourceTree = ""; }; - 7182268F2587432E00661B83 /* CocoaAsyncSocket */ = { - isa = PBXGroup; - children = ( - 718226C72587443600661B83 /* GCDAsyncSocket.h */, - 718226C82587443600661B83 /* GCDAsyncSocket.m */, - 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */, - 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */, - ); - name = CocoaAsyncSocket; - sourceTree = ""; - }; 89DE54DD1FA69115F5538E11 /* IntegrationTests_tvOS */ = { isa = PBXGroup; children = ( @@ -2493,11 +2384,11 @@ E005FEDAF49DCFA9FB77BEF3 /* IntegrationApp_tvOS.app */, ACA330765D30E21E3EEB163D /* IntegrationTests_tvOS.xctest */, 758FC0D745C185A2C28BEDA1 /* IntegrationApp_watchOS.app */, - 2B2328F1BEDB7C3DFBC22E9A /* IntegrationTests_watchOS.xctest */, 5B56DD7542C71ECB1C6E2439 /* WebDriverAgentLib_watchOS.framework */, CFE8F33794194FA3EB790C46 /* WebDriverAgentRunner_watchOS.xctest */, D1171249FE5D91AC5D797E5C /* WebDriverAgentLib_watchOS.framework */, 8B03F8C09D809131DE7792A7 /* WebDriverAgentRunner_watchOS.xctest */, + F089699F492CFC82E55D84D7 /* IntegrationTests_watchOS.xctest */, ); name = Products; sourceTree = ""; @@ -2562,89 +2453,20 @@ path = IntegrationApp_watchOS; sourceTree = ""; }; - E444DC4A24912EC40060D7EB /* Vendor */ = { - isa = PBXGroup; - children = ( - 7182268F2587432E00661B83 /* CocoaAsyncSocket */, - E444DC9E24913C080060D7EB /* RoutingHTTPServer */, - E444DC52249131050060D7EB /* CocoaHTTPServer */, - ); - name = Vendor; - sourceTree = ""; - }; - E444DC52249131050060D7EB /* CocoaHTTPServer */ = { - isa = PBXGroup; - children = ( - E444DC89249131D30060D7EB /* HTTPConnection.h */, - E444DC8C249131D30060D7EB /* HTTPConnection.m */, - E444DC8D249131D30060D7EB /* HTTPLogging.h */, - E444DC87249131D30060D7EB /* HTTPMessage.h */, - E444DC91249131D40060D7EB /* HTTPMessage.m */, - E444DC8F249131D40060D7EB /* HTTPResponse.h */, - E444DC8B249131D30060D7EB /* HTTPServer.h */, - E444DC90249131D40060D7EB /* HTTPServer.m */, - E444DC55249131740060D7EB /* Responses */, - E444DC53249131640060D7EB /* Categories */, - ); - name = CocoaHTTPServer; - sourceTree = ""; - }; - E444DC53249131640060D7EB /* Categories */ = { - isa = PBXGroup; - children = ( - E444DC7D249131B00060D7EB /* DDNumber.h */, - E444DC7F249131B00060D7EB /* DDNumber.m */, - E444DC7B249131B00060D7EB /* DDRange.h */, - E444DC7E249131B00060D7EB /* DDRange.m */, - ); - name = Categories; - sourceTree = ""; - }; - E444DC55249131740060D7EB /* Responses */ = { - isa = PBXGroup; - children = ( - E444DC60249131890060D7EB /* HTTPDataResponse.h */, - E444DC5B249131880060D7EB /* HTTPDataResponse.m */, - E444DC59249131880060D7EB /* HTTPErrorResponse.h */, - E444DC61249131890060D7EB /* HTTPErrorResponse.m */, - ); - name = Responses; - sourceTree = ""; - }; - E444DC9E24913C080060D7EB /* RoutingHTTPServer */ = { - isa = PBXGroup; - children = ( - E444DCA224913C210060D7EB /* HTTPResponseProxy.h */, - E444DC9F24913C210060D7EB /* HTTPResponseProxy.m */, - E444DCA324913C210060D7EB /* Route.h */, - E444DCA024913C210060D7EB /* Route.m */, - E444DCAA24913C220060D7EB /* RouteRequest.h */, - E444DCA924913C220060D7EB /* RouteRequest.m */, - E444DCA124913C210060D7EB /* RouteResponse.h */, - E444DCA424913C210060D7EB /* RouteResponse.m */, - E444DCA524913C210060D7EB /* RoutingConnection.h */, - E444DCA624913C210060D7EB /* RoutingConnection.m */, - E444DCA724913C210060D7EB /* RoutingHTTPServer.h */, - E444DCA824913C220060D7EB /* RoutingHTTPServer.m */, - ); - name = RoutingHTTPServer; - sourceTree = ""; - }; E94EE979A7D7C50FDC1EFE7D /* IntegrationTests_watchOS */ = { isa = PBXGroup; children = ( - 0F28D3CD9DDC2E54DB115316 /* WDAWatchHTTPClient.swift */, - 24BD02A29E3B2EB1D57FDD29 /* WDAWatchIntegrationTestCase.swift */, - 27D14C409E982099568175D1 /* WDASessionIntegrationTests.swift */, BCED63DFD03326F6351165FE /* WDAFindIntegrationTests.swift */, 848B7B14F95EFE16EEC46045 /* WDAElementAttributeIntegrationTests.swift */, C878996F07A9B26E66FC4EAC /* WDAClickIntegrationTests.swift */, 912F32C353FE7C3E7C841405 /* WDATypingIntegrationTests.swift */, 75F677B5B7737E7E1F321C20 /* WDAScreenshotAndSourceIntegrationTests.swift */, + AA11BB22CC33DD44EE55FF03 /* WDAMjpegStreamingIntegrationTests.swift */, EB1B9A793C9EFB7853C1AA13 /* WDAAppLifecycleIntegrationTests.swift */, 646D714BC17FE9258CFBDF55 /* WDADeviceIntegrationTests.swift */, 28F75AEC6442F32A3C4394AD /* WDAAlertIntegrationTests.swift */, - 3C90E6C9835DC2FDB3CB6501 /* WDAUnknownCommandIntegrationTests.swift */, + 167CC387B5F9D62618B48F80 /* IntegrationTests_watchOS-Bridging-Header.h */, + 9C280328DE3379F9EF701A20 /* WDAWatchInProcessTestCase.swift */, ); path = IntegrationTests_watchOS; sourceTree = ""; @@ -2826,7 +2648,12 @@ 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */, 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */, 13DE7A54287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m */, - 12A83FB6068CC1266BBDBE2C /* WatchOS */, + 00B1F89716AFE04C8509B916 /* FBHTTPServer.h */, + AADFEA2ED9E61A8C1A99B2D7 /* FBHTTPServer.m */, + D585660F7A04651223F29B07 /* RouteRequest.h */, + 374BB11AE4E4243BCA444CF8 /* RouteRequest.m */, + 01E7CFEBAD717FCF4BCDD383 /* RouteResponse.h */, + 4B52E4A09ADEA252A1B8190F /* RouteResponse.m */, ); name = Routing; path = WebDriverAgentLib/Routing; @@ -3313,6 +3140,25 @@ path = IntegrationApp_tvOS; sourceTree = ""; }; + 7182268F2587432E00661B83 /* CocoaAsyncSocket */ = { + isa = PBXGroup; + children = ( + 718226C72587443600661B83 /* GCDAsyncSocket.h */, + 718226C82587443600661B83 /* GCDAsyncSocket.m */, + 718226C62587443600661B83 /* GCDAsyncUdpSocket.h */, + 718226C92587443600661B83 /* GCDAsyncUdpSocket.m */, + ); + name = CocoaAsyncSocket; + sourceTree = ""; + }; + E444DC4A24912EC40060D7EB /* Vendor */ = { + isa = PBXGroup; + children = ( + 7182268F2587432E00661B83 /* CocoaAsyncSocket */, + ); + name = Vendor; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -3348,12 +3194,11 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 718226CD2587443700661B83 /* GCDAsyncSocket.h in Headers */, 641EE6312240C5CA00173FCB /* XCUIElement+FBWebDriverAttributes.h in Headers */, - 7182274A258744BE00661B83 /* HTTPMessage.h in Headers */, 641EE6322240C5CA00173FCB /* FBScreen.h in Headers */, 641EE6332240C5CA00173FCB /* XCTestPrivateSymbols.h in Headers */, 641EE6342240C5CA00173FCB /* XCUIElement+FBTyping.h in Headers */, - 7182270B258744A700661B83 /* Route.h in Headers */, 641EE6352240C5CA00173FCB /* XCUIElement+FBUtilities.h in Headers */, 641EE6362240C5CA00173FCB /* XCUIElement+FBScrolling.h in Headers */, 1357E297233D05240054BDB8 /* XCUIHitPointResult.h in Headers */, @@ -3361,7 +3206,6 @@ 641EE6382240C5CA00173FCB /* XCPointerEventPath.h in Headers */, 641EE6392240C5CA00173FCB /* FBRouteRequest.h in Headers */, 648C10AC22AAAD9C00B81B9A /* UIKeyboardImpl.h in Headers */, - 718226CD2587443700661B83 /* GCDAsyncSocket.h in Headers */, 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */, 71F3E7D525417FF400E0C22B /* FBSettings.h in Headers */, @@ -3398,7 +3242,6 @@ 71BB58E22B9631F100CB9BFE /* FBScreenRecordingPromise.h in Headers */, 641EE6632240C5CA00173FCB /* FBUnknownCommands.h in Headers */, 641EE7062240CDCF00173FCB /* XCUIElement+FBTVFocuse.h in Headers */, - 71822738258744B800661B83 /* HTTPConnection.h in Headers */, 641EE6642240C5CA00173FCB /* NSPredicate+FBFormat.h in Headers */, 641EE6662240C5CA00173FCB /* XCTestCase.h in Headers */, 641EE6682240C5CA00173FCB /* XCUIApplicationImpl.h in Headers */, @@ -3407,7 +3250,6 @@ 641EE66A2240C5CA00173FCB /* NSExpression+FBFormat.h in Headers */, 641EE66E2240C5CA00173FCB /* XCUIApplication+FBAlert.h in Headers */, 716C9E0127315EFF005AD475 /* XCUIApplication+FBUIInterruptions.h in Headers */, - 7182275C258744C300661B83 /* HTTPServer.h in Headers */, 641EE6702240C5CA00173FCB /* FBMathUtils.h in Headers */, 641EE6722240C5CA00173FCB /* FBElementUtils.h in Headers */, 641EE6732240C5CA00173FCB /* FBDebugCommands.h in Headers */, @@ -3424,18 +3266,14 @@ 641EE6802240C5CA00173FCB /* FBElementTypeTransformer.h in Headers */, 641EE6812240C5CA00173FCB /* FBXCAXClientProxy.h in Headers */, 641EE6822240C5CA00173FCB /* FBElementCache.h in Headers */, - 7182271D258744AB00661B83 /* RouteResponse.h in Headers */, 641EE6852240C5CA00173FCB /* XCUIElement+FBClassChain.h in Headers */, 13DE7A44287C2A8D003243C6 /* FBXCAccessibilityElement.h in Headers */, 641EE6862240C5CA00173FCB /* FBResponseJSONPayload.h in Headers */, - 71822714258744A900661B83 /* RouteRequest.h in Headers */, 641EE6882240C5CA00173FCB /* FBElement.h in Headers */, 641EE68B2240C5CA00173FCB /* FBExceptionHandler.h in Headers */, - 71822726258744AE00661B83 /* RoutingConnection.h in Headers */, 641EE68C2240C5CA00173FCB /* FBRoute.h in Headers */, 641EE68D2240C5CA00173FCB /* XCTestDriver.h in Headers */, 641EE68F2240C5CA00173FCB /* XCSynthesizedEventRecord.h in Headers */, - 71822753258744C100661B83 /* HTTPResponse.h in Headers */, 641EE6942240C5CA00173FCB /* FBXPath.h in Headers */, 641EE6972240C5CA00173FCB /* XCUIElement+FBForceTouch.h in Headers */, 641EE6982240C5CA00173FCB /* FBRuntimeUtils.h in Headers */, @@ -3445,7 +3283,6 @@ 641EE69F2240C5CA00173FCB /* FBTCPSocket.h in Headers */, 641EE6A02240C5CA00173FCB /* XCUIElement+FBUID.h in Headers */, 641EE6A22240C5CA00173FCB /* XCUIDevice.h in Headers */, - 7182272F258744B000661B83 /* RoutingHTTPServer.h in Headers */, 641EE6A32240C5CA00173FCB /* XCUIApplication+FBTouchAction.h in Headers */, 641EE6A42240C5CA00173FCB /* FBCommandHandler.h in Headers */, 641EE6A52240C5CA00173FCB /* FBSessionCommands.h in Headers */, @@ -3457,8 +3294,6 @@ B316351F2DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */, 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */, 641EE6A92240C5CA00173FCB /* FBCommandStatus.h in Headers */, - 71822702258744A400661B83 /* HTTPResponseProxy.h in Headers */, - 71822741258744BB00661B83 /* HTTPLogging.h in Headers */, 641EE6AB2240C5CA00173FCB /* FBAlertViewCommands.h in Headers */, 641EE6AC2240C5CA00173FCB /* XCTWaiter.h in Headers */, 641EE6AD2240C5CA00173FCB /* XCTWaiterManagement-Protocol.h in Headers */, @@ -3467,7 +3302,6 @@ 648C10B022AAAE4000B81B9A /* TIPreferencesController.h in Headers */, 71F5BE24252E576C00EE9EBA /* XCUIElement+FBSwiping.h in Headers */, 641EE6B72240C5CA00173FCB /* FBBaseActionsSynthesizer.h in Headers */, - 7182276E258744C900661B83 /* HTTPErrorResponse.h in Headers */, 641EE6B82240C5CA00173FCB /* FBAlert.h in Headers */, 641EE6B92240C5CA00173FCB /* XCUIElementQuery.h in Headers */, 71BB58F02B96511800CB9BFE /* FBVideoCommands.h in Headers */, @@ -3485,7 +3319,6 @@ 641EE6C32240C5CA00173FCB /* FBClassChainQueryParser.h in Headers */, 641EE6C42240C5CA00173FCB /* FBMacros.h in Headers */, 641EE6C52240C5CA00173FCB /* XCTestExpectationDelegate-Protocol.h in Headers */, - 71822777258744CE00661B83 /* DDNumber.h in Headers */, 641EE6C92240C5CA00173FCB /* XCUIDevice+FBRotation.h in Headers */, A1B2C3D41F001A00A1B0004 /* XCUIDevice+FBVoiceOver.h in Headers */, 719DCF162601EAFB000E765F /* FBNotificationsHelper.h in Headers */, @@ -3497,7 +3330,6 @@ 641EE6D62240C5CA00173FCB /* FBLogger.h in Headers */, 71BB58F72B96531900CB9BFE /* FBScreenRecordingContainer.h in Headers */, 641EE6D82240C5CA00173FCB /* XCUIElement.h in Headers */, - 718226CB2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */, 641EE6DB2240C5CA00173FCB /* FBPasteboard.h in Headers */, 711CD03525ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */, 641EE6DD2240C5CA00173FCB /* FBDebugLogDelegateDecorator.h in Headers */, @@ -3506,9 +3338,7 @@ 641EE6E12240C5CA00173FCB /* XCUIApplicationProcess.h in Headers */, 641EE6E22240C5CA00173FCB /* FBW3CActionsSynthesizer.h in Headers */, 641EE6E32240C5CA00173FCB /* CDStructures.h in Headers */, - 71822780258744D000661B83 /* DDRange.h in Headers */, F59CD6D62EF16E5E00F91287 /* XCUIElement+FBCustomActions.h in Headers */, - 71822765258744C700661B83 /* HTTPDataResponse.h in Headers */, 641EE6E72240C5CA00173FCB /* XCUIElement+FBFind.h in Headers */, 641EE6E92240C5CA00173FCB /* FBFailureProofTestCase.h in Headers */, 641EE6ED2240C5CA00173FCB /* FBXPath-Private.h in Headers */, @@ -3637,6 +3467,8 @@ 1DFC6477940D11BBAD68D59F /* _XCTestObservationInternal-Protocol.h in Headers */, BF9B191D841681A571BAFED1 /* _XCTestObservationPrivate-Protocol.h in Headers */, 5B736BB83DEA69D3C5EEC7E1 /* XCTElementSetTransformer-Protocol.h in Headers */, + 2112EC67BDFFA4A0B2CF24EB /* RouteRequest.h in Headers */, + 0161F45997A981E47729DB25 /* RouteResponse.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3689,12 +3521,11 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 34AB13EFF1F673084C910195 /* GCDAsyncSocket.h in Headers */, 43EBA0D0D54EB99815CE63B6 /* XCUIElement+FBWebDriverAttributes.h in Headers */, - C154B8CD167DBE068EA74719 /* HTTPMessage.h in Headers */, 93D5C4C8442EFE91D70780CC /* FBScreen.h in Headers */, B9FAD6E3F3AB201B59D6F8BD /* XCTestPrivateSymbols.h in Headers */, CDDEBD9F5F6A380C8D32D671 /* XCUIElement+FBTyping.h in Headers */, - FBDF04E5916B91FCEC0190CB /* Route.h in Headers */, 488C4CBD86EBFA2AB5396917 /* XCUIElement+FBUtilities.h in Headers */, CE3D815D09AD8E137CC18AA5 /* XCUIElement+FBScrolling.h in Headers */, 767CB761C99AA6275E08FDB9 /* XCUIHitPointResult.h in Headers */, @@ -3702,7 +3533,6 @@ 852D69D1623D81697D700B3C /* XCPointerEventPath.h in Headers */, 43BC9FE4BF5C19F6F6569177 /* FBRouteRequest.h in Headers */, BD2A5D43881B449E533D5800 /* UIKeyboardImpl.h in Headers */, - 34AB13EFF1F673084C910195 /* GCDAsyncSocket.h in Headers */, 444F211126EAF77FB2B1DB42 /* FBXCElementSnapshot.h in Headers */, FFA7672B731B57AB9283DD40 /* FBXCElementSnapshotWrapper.h in Headers */, D43AF1E150975634F3AFF329 /* FBSettings.h in Headers */, @@ -3739,7 +3569,6 @@ F876324AFD3617FFCFC479C3 /* FBScreenRecordingPromise.h in Headers */, 67402F46AAB3DCAB3E40ED27 /* FBUnknownCommands.h in Headers */, CB1790B793BB813AF80F65DF /* XCUIElement+FBTVFocuse.h in Headers */, - 2C6A876B29ECA17D6A5BCEED /* HTTPConnection.h in Headers */, 617AE9B41FB23CCE66769441 /* NSPredicate+FBFormat.h in Headers */, 60CF6CA8AC7961DCCE402764 /* XCTestCase.h in Headers */, 995DF7C1928C60D7AE02840D /* XCUIApplicationImpl.h in Headers */, @@ -3748,7 +3577,6 @@ 4D80F932D0CD80C99950DA4F /* NSExpression+FBFormat.h in Headers */, 95A597B8CC694006DDE49999 /* XCUIApplication+FBAlert.h in Headers */, ED342D194B20A206864675B2 /* XCUIApplication+FBUIInterruptions.h in Headers */, - BCC9E02491560712221045CC /* HTTPServer.h in Headers */, E6B214145FE46BBFD824FAE7 /* FBMathUtils.h in Headers */, 6786D4B257269918E591AC66 /* FBElementUtils.h in Headers */, 51FFE79AF3A5FADFFAB287BC /* FBDebugCommands.h in Headers */, @@ -3765,18 +3593,14 @@ D40D635A4ABBC79829A0D590 /* FBElementTypeTransformer.h in Headers */, 70E21F76098198E19A6E5A5E /* FBXCAXClientProxy.h in Headers */, 97AB752A8043A962100DB4EB /* FBElementCache.h in Headers */, - F0F7B8DFB14C9DFC86DFFBFD /* RouteResponse.h in Headers */, 10EC41A2ECCD2E865EEE7AB0 /* XCUIElement+FBClassChain.h in Headers */, 6FC8E41707F65D5A432206CC /* FBXCAccessibilityElement.h in Headers */, 4E8E83C42F2E2A2A5A62F3E3 /* FBResponseJSONPayload.h in Headers */, - 894AE4397B5992EF738248AE /* RouteRequest.h in Headers */, D2B83B27DAB80FE235285D2F /* FBElement.h in Headers */, 8597F35CEB1617767B63D770 /* FBExceptionHandler.h in Headers */, - FEC52AE4F35AD38D3AC2659F /* RoutingConnection.h in Headers */, AC3529AFA966E7202CB3B3B1 /* FBRoute.h in Headers */, A47F8777C5C8D021B2AC910C /* XCTestDriver.h in Headers */, 1D0C64A6600E2D1BBAFC2D62 /* XCSynthesizedEventRecord.h in Headers */, - 0F282CAB2A025ECA9EAB18B3 /* HTTPResponse.h in Headers */, 4C472F2027329ACA65C7D721 /* FBXPath.h in Headers */, 5A1B8098AE35B82BD379B421 /* XCUIElement+FBForceTouch.h in Headers */, 9DFDA1423A4651A4DB9FE341 /* FBRuntimeUtils.h in Headers */, @@ -3786,7 +3610,6 @@ BC1470BF3E54F478DAEDF05D /* FBTCPSocket.h in Headers */, 476E44716A4A3A22F95EC51A /* XCUIElement+FBUID.h in Headers */, FBF064E4D96CFCD08E1F4EF7 /* XCUIDevice.h in Headers */, - A8635BD557F97E6C29A0790E /* RoutingHTTPServer.h in Headers */, 46AEB32485B508AF1D9B8CE2 /* XCUIApplication+FBTouchAction.h in Headers */, 3EC5404A04E5A61B8144E834 /* FBCommandHandler.h in Headers */, 3E199AC580C5DA4B070DD01A /* FBSessionCommands.h in Headers */, @@ -3798,8 +3621,6 @@ 9452D57FE97CB2ECED093B9F /* FBAccessibilityTraits.h in Headers */, DE2D708340ED70EBF9244D0F /* NSDictionary+FBUtf8SafeDictionary.h in Headers */, 2D6B818DC921A4631A496432 /* FBCommandStatus.h in Headers */, - 293BD2162964EEC2A3BA6B57 /* HTTPResponseProxy.h in Headers */, - 90107B3BBFBF3B073807D51B /* HTTPLogging.h in Headers */, CFB0AFBC9F429B279C95F5BC /* FBAlertViewCommands.h in Headers */, 6A1D9B851D58D0A36D6822B7 /* XCTWaiter.h in Headers */, F7010B261A861C5C63D59273 /* XCTWaiterManagement-Protocol.h in Headers */, @@ -3808,7 +3629,6 @@ 16E809B6BFDF7CBD862A7B48 /* TIPreferencesController.h in Headers */, 42A92793513CA29B39B72721 /* XCUIElement+FBSwiping.h in Headers */, 268CBDE31376AF96DDDD4BD0 /* FBBaseActionsSynthesizer.h in Headers */, - EDB13FDB5D8220A65560629B /* HTTPErrorResponse.h in Headers */, 462DA317CCA9DBE4F249516B /* FBAlert.h in Headers */, 79FD4FC269A91F525A4E1413 /* XCUIElementQuery.h in Headers */, 71BB64FB648CB9BBBD25F3E3 /* FBVideoCommands.h in Headers */, @@ -3826,7 +3646,6 @@ 6BF3F57247F59B81B8BCEE7B /* FBClassChainQueryParser.h in Headers */, 8AAA33A5B1943B667B0DB05E /* FBMacros.h in Headers */, 1CA8A47F9D6B967D99FC0896 /* XCTestExpectationDelegate-Protocol.h in Headers */, - 3414F451472B235F637F46BC /* DDNumber.h in Headers */, 74CF72DC7209A5B04763D0D2 /* XCUIDevice+FBRotation.h in Headers */, A8CEAEFC8CC63F94DA0176A9 /* XCUIDevice+FBVoiceOver.h in Headers */, 150276DDADD9963F29465D57 /* FBNotificationsHelper.h in Headers */, @@ -3838,7 +3657,6 @@ 1F545FFB67878CBAF4D6E6A0 /* FBLogger.h in Headers */, 05F8A3B33BCFAFFA75CD81CA /* FBScreenRecordingContainer.h in Headers */, 5787FD65011B3AF632694EEA /* XCUIElement.h in Headers */, - 5917EF5F1372B2098168EA0D /* GCDAsyncUdpSocket.h in Headers */, 6B3AB44BFFDDF042E4B712C1 /* FBPasteboard.h in Headers */, 36C5DA6C013EAAFC575B4A2B /* XCUIScreenDataSource-Protocol.h in Headers */, D1DE06DF54156F744E364B5F /* FBDebugLogDelegateDecorator.h in Headers */, @@ -3847,9 +3665,7 @@ CFDBE0DF5D99CB310C7AFB01 /* XCUIApplicationProcess.h in Headers */, 8EA51935E09A89896FB1D463 /* FBW3CActionsSynthesizer.h in Headers */, 79A47D7F50179802052ED774 /* CDStructures.h in Headers */, - AF4223DDBCC9EC79D4F9DC0D /* DDRange.h in Headers */, B70EB4BDC07CE9EC58505E85 /* XCUIElement+FBCustomActions.h in Headers */, - EE22974A8F641FA94DCBCB3E /* HTTPDataResponse.h in Headers */, 760885DAFDB4A126DBD66649 /* XCUIElement+FBFind.h in Headers */, B6B73CE709728984EF03361D /* FBFailureProofTestCase.h in Headers */, 1C159E8E35E0823278141DC2 /* FBXPath-Private.h in Headers */, @@ -3963,6 +3779,8 @@ F4E8FB5A2EB57854EB6A00E9 /* _XCTestObservationInternal-Protocol.h in Headers */, 59892BBAB84DFD927C94593F /* _XCTestObservationPrivate-Protocol.h in Headers */, A47A7F9E098C88328DF38A4C /* XCTElementSetTransformer-Protocol.h in Headers */, + E4FA72A388B36E0B6C41C421 /* RouteRequest.h in Headers */, + DAB4F2FB4C0AB107E461BBA4 /* RouteResponse.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3970,6 +3788,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + 718226CC2587443700661B83 /* GCDAsyncSocket.h in Headers */, EEE376491D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h in Headers */, 715AFAC11FFA29180053896D /* FBScreen.h in Headers */, EE6B64FD1D0F86EF00E85F5D /* XCTestPrivateSymbols.h in Headers */, @@ -3997,7 +3816,6 @@ EE35AD611E3B77D600A02D78 /* XCTRunnerIDESession.h in Headers */, EE158AE01CBD456F00A3E3F0 /* FBRouteRequest-Private.h in Headers */, EE35AD281E3B77D600A02D78 /* XCApplicationQuery.h in Headers */, - E444DCB124913C220060D7EB /* RoutingConnection.h in Headers */, EE35AD601E3B77D600A02D78 /* XCTRunnerDaemonSession.h in Headers */, 71414ED62670A1EE003A8C5D /* LRUCacheNode.h in Headers */, 64B2650A228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */, @@ -4036,15 +3854,12 @@ 714EAA0D2673FDFE005C5B47 /* FBCapabilities.h in Headers */, EE35AD521E3B77D600A02D78 /* XCTestObservationCenter.h in Headers */, 71AE3CF92D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */, - E444DC97249131D40060D7EB /* HTTPServer.h in Headers */, - E444DCAE24913C220060D7EB /* HTTPResponseProxy.h in Headers */, 1357E296233D05240054BDB8 /* XCUIHitPointResult.h in Headers */, 711CD03425ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */, EE158AAE1CBD456F00A3E3F0 /* XCUIElement+FBAccessibility.h in Headers */, EE35AD421E3B77D600A02D78 /* XCTestCaseRun.h in Headers */, EE35AD441E3B77D600A02D78 /* XCTestConfiguration.h in Headers */, 715A84D02DD92AD3007134CC /* FBElementHelpers.h in Headers */, - 718226CA2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */, EE35AD491E3B77D600A02D78 /* XCTestExpectation.h in Headers */, EE158AE81CBD456F00A3E3F0 /* FBElementTypeTransformer.h in Headers */, 7157B291221DADD2001C348C /* FBXCAXClientProxy.h in Headers */, @@ -4055,20 +3870,15 @@ EE158AD01CBD456F00A3E3F0 /* FBElement.h in Headers */, EE158AD41CBD456F00A3E3F0 /* FBExceptionHandler.h in Headers */, EE158ADE1CBD456F00A3E3F0 /* FBRoute.h in Headers */, - E444DC81249131B10060D7EB /* DDRange.h in Headers */, EE35AD471E3B77D600A02D78 /* XCTestDriver.h in Headers */, - E444DC93249131D40060D7EB /* HTTPMessage.h in Headers */, EE35AD3A1E3B77D600A02D78 /* XCSynthesizedEventRecord.h in Headers */, - E444DCAD24913C220060D7EB /* RouteResponse.h in Headers */, 13DE7A5B287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h in Headers */, 711084441DA3AA7500F913D6 /* FBXPath.h in Headers */, - E444DC83249131B10060D7EB /* DDNumber.h in Headers */, EE8DDD7F20C5733C004D4925 /* XCUIElement+FBForceTouch.h in Headers */, 71A5C67329A4F39600421C37 /* XCTIssue+FBPatcher.h in Headers */, 716F0DA12A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.h in Headers */, EE158AEA1CBD456F00A3E3F0 /* FBRuntimeUtils.h in Headers */, 7136A4791E8918E60024FC3D /* XCUIElement+FBPickerWheel.h in Headers */, - E444DCB324913C220060D7EB /* RoutingHTTPServer.h in Headers */, EE158ABE1CBD456F00A3E3F0 /* FBElementCommands.h in Headers */, 715557D3211DBCE700613B26 /* FBTCPSocket.h in Headers */, 71B49EC71ED1A58100D51AD6 /* XCUIElement+FBUID.h in Headers */, @@ -4087,16 +3897,12 @@ EE35AD681E3B77D600A02D78 /* XCTWaiterManagement-Protocol.h in Headers */, EE35AD291E3B77D600A02D78 /* XCAXClient_iOS.h in Headers */, 648C10AF22AAAE4000B81B9A /* TIPreferencesController.h in Headers */, - E444DC6C249131890060D7EB /* HTTPDataResponse.h in Headers */, - E444DC65249131890060D7EB /* HTTPErrorResponse.h in Headers */, 714097431FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h in Headers */, AD6C26941CF2379700F8B5FF /* FBAlert.h in Headers */, EE35AD731E3B77D600A02D78 /* XCUIElementQuery.h in Headers */, EE35AD331E3B77D600A02D78 /* XCPointerEvent.h in Headers */, 71D04DC825356C43008A052C /* XCUIElement+FBCaching.h in Headers */, 71BB58E12B9631F100CB9BFE /* FBScreenRecordingPromise.h in Headers */, - E444DC99249131D40060D7EB /* HTTPLogging.h in Headers */, - E444DC9B249131D40060D7EB /* HTTPResponse.h in Headers */, EEE9B4721CD02B88009D2030 /* FBRunLoopSpinner.h in Headers */, EE3A18621CDE618F00DE4205 /* FBErrorBuilder.h in Headers */, 0E04133B2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */, @@ -4120,7 +3926,6 @@ EE35AD2A1E3B77D600A02D78 /* XCDebugLogDelegate-Protocol.h in Headers */, 7150348721A6DAD600A0F4BA /* FBImageUtils.h in Headers */, C8FB547422D3949C00B69954 /* LSApplicationWorkspace.h in Headers */, - E444DCAF24913C220060D7EB /* Route.h in Headers */, EE9B76A81CF7A43900275851 /* FBLogger.h in Headers */, EE35AD6F1E3B77D600A02D78 /* XCUIElement.h in Headers */, 71930C4220662E1F00D3AFEC /* FBPasteboard.h in Headers */, @@ -4132,12 +3937,9 @@ EE35AD151E3B77D600A02D78 /* CDStructures.h in Headers */, 71E75E6D254824230099FC87 /* XCUIElementQuery+FBHelpers.h in Headers */, 716C9DFA27315D21005AD475 /* FBReflectionUtils.h in Headers */, - E444DCB624913C220060D7EB /* RouteRequest.h in Headers */, 71F5BE23252E576C00EE9EBA /* XCUIElement+FBSwiping.h in Headers */, - 718226CC2587443700661B83 /* GCDAsyncSocket.h in Headers */, EEBBD48B1D47746D00656A81 /* XCUIElement+FBFind.h in Headers */, EE6A893A1D0B38640083E92B /* FBFailureProofTestCase.h in Headers */, - E444DC95249131D40060D7EB /* HTTPConnection.h in Headers */, 712A0C871DA3E55D007D02E5 /* FBXPath-Private.h in Headers */, FBCAFE000000000000000002 /* FBScreenCaptureCommands.h in Headers */, FBCAFE000000000000003002 /* FBMobilerunA11yCommands.h in Headers */, @@ -4262,6 +4064,8 @@ 604A7EDA9D8A9BFCF3B62E5C /* _XCTestObservationInternal-Protocol.h in Headers */, AB126E96C642B235EE02B4F1 /* _XCTestObservationPrivate-Protocol.h in Headers */, 8CB293E6451EB1A6D1240BAA /* XCTElementSetTransformer-Protocol.h in Headers */, + CA077D3ED0D0BA2590CB85DE /* RouteRequest.h in Headers */, + 44A6F8DBFDCDD735D000458D /* RouteResponse.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4275,22 +4079,23 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ - 4632809915F58C9095914E85 /* IntegrationTests_watchOS */ = { + 5D9539D7055B5E6EED56D100 /* IntegrationTests_watchOS */ = { isa = PBXNativeTarget; - buildConfigurationList = 9DA322A56A2DFF03F3E5CDEA /* Build configuration list for PBXNativeTarget "IntegrationTests_watchOS" */; + buildConfigurationList = 3C27414B1DEE3FFF4AEE3112 /* Build configuration list for PBXNativeTarget "IntegrationTests_watchOS" */; buildPhases = ( - A17BAB1FBE5B3A5B0AB97661 /* Sources */, - ECA2D9FDA872B089E60C5289 /* Frameworks */, - 7A0971F542104119F22CD36E /* Resources */, + A3A83215DA904C3B5BD86EC8 /* Sources */, + 30DBE8D4E4AB99F4C518DD6C /* Frameworks */, + 38F16BEEEEDF88C74D4F570F /* Resources */, ); buildRules = ( ); dependencies = ( - DC55141E0512D9D0ADE54987 /* PBXTargetDependency */, + B5DEACD7B6109622C39DEFD1 /* PBXTargetDependency */, + 07E03EEE96B99EBB8DDAC7F5 /* PBXTargetDependency */, ); name = IntegrationTests_watchOS; productName = IntegrationTests_watchOS; - productReference = 2B2328F1BEDB7C3DFBC22E9A /* IntegrationTests_watchOS.xctest */; + productReference = F089699F492CFC82E55D84D7 /* IntegrationTests_watchOS.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; 641EE2D92240BBE300173FCB /* WebDriverAgentRunner_tvOS */ = { @@ -4673,7 +4478,7 @@ 7C3BE5A1B1A6022529590D15 /* IntegrationApp_tvOS */, DE52ACA4B43F527189E374EE /* IntegrationTests_tvOS */, 9D6B02D8FC050BAF7159284F /* IntegrationApp_watchOS */, - 4632809915F58C9095914E85 /* IntegrationTests_watchOS */, + 5D9539D7055B5E6EED56D100 /* IntegrationTests_watchOS */, ); }; /* End PBXProject section */ @@ -4693,28 +4498,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 641EE2D82240BBE300173FCB /* Resources */ = { + 38F16BEEEEDF88C74D4F570F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 641EE6EF2240C5CA00173FCB /* Resources */ = { + 641EE2D82240BBE300173FCB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 64B264F7228C50E0002A5025 /* Resources */ = { + 641EE6EF2240C5CA00173FCB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - 7A0971F542104119F22CD36E /* Resources */ = { + 64B264F7228C50E0002A5025 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -4831,25 +4636,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 718226CF2587443700661B83 /* GCDAsyncSocket.m in Sources */, F59CD6D72EF16E5E00F91287 /* XCUIElement+FBCustomActions.m in Sources */, 64E3502E2AC0B6EB005F3ACB /* NSDictionary+FBUtf8SafeDictionary.m in Sources */, - 718226CF2587443700661B83 /* GCDAsyncSocket.m in Sources */, - E444DCBC24917A5E0060D7EB /* HTTPResponseProxy.m in Sources */, 71D3B3D8267FC7260076473D /* XCUIElement+FBResolve.m in Sources */, - E444DCBE24917A5E0060D7EB /* Route.m in Sources */, - E444DCC024917A5E0060D7EB /* RouteRequest.m in Sources */, 13DE7A52287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */, - E444DCC224917A5E0060D7EB /* RouteResponse.m in Sources */, - E444DCC424917A5E0060D7EB /* RoutingConnection.m in Sources */, - E444DCC624917A5E0060D7EB /* RoutingHTTPServer.m in Sources */, - E444DCC824917A5E0060D7EB /* HTTPConnection.m in Sources */, - E444DCCB24917A5E0060D7EB /* HTTPMessage.m in Sources */, - E444DCCE24917A5E0060D7EB /* HTTPServer.m in Sources */, - E444DCD024917A5E0060D7EB /* HTTPDataResponse.m in Sources */, - E444DCD224917A5E0060D7EB /* HTTPErrorResponse.m in Sources */, 71414ED92670A1EE003A8C5D /* LRUCache.m in Sources */, - E444DCD424917A5E0060D7EB /* DDNumber.m in Sources */, - E444DCD624917A5E0060D7EB /* DDRange.m in Sources */, 641EE5D72240C5CA00173FCB /* FBScreenshotCommands.m in Sources */, 71F3E7D725417FF400E0C22B /* FBSettings.m in Sources */, 71F3E7DA25417FF400E0C22C /* FBSettingsHandler.m in Sources */, @@ -4895,7 +4687,6 @@ 641EE5F52240C5CA00173FCB /* XCUIElement+FBUID.m in Sources */, 641EE5F62240C5CA00173FCB /* FBRouteRequest.m in Sources */, 641EE5F72240C5CA00173FCB /* FBResponseJSONPayload.m in Sources */, - 718226D12587443700661B83 /* GCDAsyncUdpSocket.m in Sources */, 641EE5F92240C5CA00173FCB /* FBMjpegServer.m in Sources */, 641EE5FA2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.m in Sources */, 641EE5FD2240C5CA00173FCB /* FBBaseActionsSynthesizer.m in Sources */, @@ -4971,6 +4762,9 @@ FBCAFE000000000000006015 /* FBAudioStreamSession.m in Sources */, FBCAFE000000000000006025 /* FBAudioStreamManager.m in Sources */, FBCAFE000000000000006035 /* FBAudioCaptureCommands.m in Sources */, + 43DE58587952717F4DEE228E /* FBHTTPServer.m in Sources */, + 667705A8195BF7B0D48180B3 /* RouteRequest.m in Sources */, + 90C6C1E34B9B850CC79BABCB /* RouteResponse.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4994,22 +4788,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A17BAB1FBE5B3A5B0AB97661 /* Sources */ = { + A3A83215DA904C3B5BD86EC8 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DEFE56CC1A4EC88BCCD9BD6D /* WDAWatchHTTPClient.swift in Sources */, - E101F1B19A8079EAD8457C0C /* WDAWatchIntegrationTestCase.swift in Sources */, - D9FF15E0009D95996128C5E1 /* WDASessionIntegrationTests.swift in Sources */, - 1FA2AA058DF5AEAC2212EAF8 /* WDAFindIntegrationTests.swift in Sources */, - A648F711A5BECD8BE680AB79 /* WDAElementAttributeIntegrationTests.swift in Sources */, - E1C6D95706F0F87BE535E332 /* WDAClickIntegrationTests.swift in Sources */, - 93B85BB5737C85F650FF772F /* WDATypingIntegrationTests.swift in Sources */, - 2CDFA27001529A43B560F3A1 /* WDAScreenshotAndSourceIntegrationTests.swift in Sources */, - 07CD6433C98FC4A86E2BC7E3 /* WDAAppLifecycleIntegrationTests.swift in Sources */, - CEA3AF02E2BFF31ABB97F6F0 /* WDADeviceIntegrationTests.swift in Sources */, - A15C224F98CDB418E5727697 /* WDAAlertIntegrationTests.swift in Sources */, - E7DF976BD298CC4815946781 /* WDAUnknownCommandIntegrationTests.swift in Sources */, + B36D8136E49BF48ED389D1DB /* WDAWatchInProcessTestCase.swift in Sources */, + 72F5BAE6CA919434EA4DA0F3 /* WDAFindIntegrationTests.swift in Sources */, + E1F20E47472FED558822C1E5 /* WDAElementAttributeIntegrationTests.swift in Sources */, + 227E3F36FAA2C3881F89FD81 /* WDAClickIntegrationTests.swift in Sources */, + 6AF8892AEF5C6BF01896B6DA /* WDAAppLifecycleIntegrationTests.swift in Sources */, + 6EDF83DA0A6C7A7E67C30AF6 /* WDADeviceIntegrationTests.swift in Sources */, + FBFEC05ED2C01D1EAE34BE9A /* WDAScreenshotAndSourceIntegrationTests.swift in Sources */, + AA11BB22CC33DD44EE55FF04 /* WDAMjpegStreamingIntegrationTests.swift in Sources */, + D22FED0B3856919DA4BCFA67 /* WDATypingIntegrationTests.swift in Sources */, + 68EB0B8D53270CB68C53D028 /* WDAAlertIntegrationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5127,9 +4919,10 @@ C4E500AC8CD81B4FE3A75EAE /* FBMathUtils.m in Sources */, D062CA5914608761FE002799 /* FBXCAXClientProxy.m in Sources */, 46C54343FB83AD3AFC6CFCD4 /* FBTCPSocket.m in Sources */, - C2D22426DB9FCE6AD84FA16A /* RouteRequest.m in Sources */, - 3FEF512914A962C5E30579FC /* RouteResponse.m in Sources */, - 91FF05D51401D1F1A88FB0B0 /* FBWatchHTTPServer.m in Sources */, + AA11BB22CC33DD44EE55FF02 /* FBMjpegServer.m in Sources */, + 0A4413521ECE45EA182E8403 /* FBHTTPServer.m in Sources */, + D1172A7F53F89B78D8324A13 /* RouteRequest.m in Sources */, + AB531917FF460EB87E4AD5A2 /* RouteResponse.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5137,10 +4930,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 718226CE2587443700661B83 /* GCDAsyncSocket.m in Sources */, EE158AC71CBD456F00A3E3F0 /* FBScreenshotCommands.m in Sources */, - E444DC98249131D40060D7EB /* HTTPConnection.m in Sources */, 7136A47A1E8918E60024FC3D /* XCUIElement+FBPickerWheel.m in Sources */, - E444DC84249131B10060D7EB /* DDRange.m in Sources */, 6385F4A7220A40760095BBDB /* XCUIApplicationProcessDelay.m in Sources */, 71A5C67529A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */, 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */, @@ -5158,7 +4950,6 @@ AD6C269D1CF2494200F8B5FF /* XCUIApplication+FBHelpers.m in Sources */, EE3A18671CDE734B00DE4205 /* FBKeyboard.m in Sources */, 719DCF172601EAFB000E765F /* FBNotificationsHelper.m in Sources */, - E444DCAC24913C220060D7EB /* Route.m in Sources */, 713C6DD01DDC772A00285B92 /* FBElementUtils.m in Sources */, 71BB58E32B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */, 7140974C1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m in Sources */, @@ -5170,17 +4961,14 @@ EEBBD48C1D47746D00656A81 /* XCUIElement+FBFind.m in Sources */, EE158ADD1CBD456F00A3E3F0 /* FBResponsePayload.m in Sources */, B316351C2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */, - E444DCB524913C220060D7EB /* RouteRequest.m in Sources */, C8FB547A22D4C1FC00B69954 /* FBUnattachedAppLauncher.m in Sources */, EE158ADF1CBD456F00A3E3F0 /* FBRoute.m in Sources */, EE0D1F621EBCDCF7006A3123 /* NSString+FBVisualLength.m in Sources */, EEE9B4731CD02B88009D2030 /* FBRunLoopSpinner.m in Sources */, 719CD8F92126C78F00C7D0C2 /* FBAlertsMonitor.m in Sources */, 71A7EAFA1E224648001DA4F2 /* FBClassChainQueryParser.m in Sources */, - 718226D02587443700661B83 /* GCDAsyncUdpSocket.m in Sources */, 13DE7A51287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */, 71A224E61DE2F56600844D55 /* NSPredicate+FBFormat.m in Sources */, - E444DC85249131B10060D7EB /* DDNumber.m in Sources */, EEE376441D59F81400ED88DD /* XCUIDevice+FBRotation.m in Sources */, A1B2C3D41F001A00A1B0007 /* XCUIDevice+FBVoiceOver.m in Sources */, 13815F712328D20400CDAB61 /* FBActiveAppDetectionPoint.m in Sources */, @@ -5191,7 +4979,6 @@ 7155D704211DCEF400166C20 /* FBMjpegServer.m in Sources */, EEDFE1221D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.m in Sources */, 714D88CE2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */, - E444DCB424913C220060D7EB /* RoutingHTTPServer.m in Sources */, 7140974E1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m in Sources */, EEE3764A1D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m in Sources */, EE8DDD7E20C5733C004D4925 /* XCUIElement+FBForceTouch.m in Sources */, @@ -5221,12 +5008,9 @@ EE158AAF1CBD456F00A3E3F0 /* XCUIElement+FBAccessibility.m in Sources */, 714E14BA29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */, 7150348821A6DAD600A0F4BA /* FBImageUtils.m in Sources */, - E444DCAB24913C220060D7EB /* HTTPResponseProxy.m in Sources */, - E444DC6D249131890060D7EB /* HTTPErrorResponse.m in Sources */, 71F5BE25252E576C00EE9EBA /* XCUIElement+FBSwiping.m in Sources */, EE158AE51CBD456F00A3E3F0 /* FBSession.m in Sources */, 71C9EAAE25E8415A00470CD8 /* FBScreenshot.m in Sources */, - E444DCB224913C220060D7EB /* RoutingConnection.m in Sources */, EE158AC11CBD456F00A3E3F0 /* FBFindElementCommands.m in Sources */, EE7E271D1D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m in Sources */, 716C9DFC27315D21005AD475 /* FBReflectionUtils.m in Sources */, @@ -5239,13 +5023,10 @@ 13DE7A57287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */, 71BB58F82B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */, EE158AB31CBD456F00A3E3F0 /* XCUIElement+FBScrolling.m in Sources */, - 718226CE2587443700661B83 /* GCDAsyncSocket.m in Sources */, EE158AC91CBD456F00A3E3F0 /* FBSessionCommands.m in Sources */, 715A84CF2DD92AD3007134CC /* FBElementHelpers.m in Sources */, EE9B76A71CF7A43900275851 /* FBConfiguration.m in Sources */, - E444DC9C249131D40060D7EB /* HTTPServer.m in Sources */, 71414ED82670A1EE003A8C5D /* LRUCache.m in Sources */, - E444DC67249131890060D7EB /* HTTPDataResponse.m in Sources */, EE158AD31CBD456F00A3E3F0 /* FBElementCache.m in Sources */, 71930C4320662E1F00D3AFEC /* FBPasteboard.m in Sources */, AD6C26951CF2379700F8B5FF /* FBAlert.m in Sources */, @@ -5255,8 +5036,6 @@ EE158AD51CBD456F00A3E3F0 /* FBExceptionHandler.m in Sources */, EE5A24421F136D360078B1D9 /* FBXCodeCompatibility.m in Sources */, EE158AE91CBD456F00A3E3F0 /* FBElementTypeTransformer.m in Sources */, - E444DC9D249131D40060D7EB /* HTTPMessage.m in Sources */, - E444DCB024913C220060D7EB /* RouteResponse.m in Sources */, 71D3B3D7267FC7260076473D /* XCUIElement+FBResolve.m in Sources */, 715AFAC21FFA29180053896D /* FBScreen.m in Sources */, 71B155DC230711E900646AFB /* FBCommandStatus.m in Sources */, @@ -5278,6 +5057,9 @@ FBCAFE000000000000006014 /* FBAudioStreamSession.m in Sources */, FBCAFE000000000000006024 /* FBAudioStreamManager.m in Sources */, FBCAFE000000000000006034 /* FBAudioCaptureCommands.m in Sources */, + 35924251B4B5D0A486A6A0BB /* FBHTTPServer.m in Sources */, + 7072174F17BA109C6AB2859F /* RouteRequest.m in Sources */, + 5B9C00B488A31A95F1460727 /* RouteResponse.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5436,6 +5218,12 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 07E03EEE96B99EBB8DDAC7F5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = WebDriverAgentLib_watchOS; + target = 95186DE2383671DFA81D7FCB /* WebDriverAgentLib_watchOS */; + targetProxy = 79A7BAB354E176E532A7A562 /* PBXContainerItemProxy */; + }; 641EE6FB2240C5F400173FCB /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 641EE5D52240C5CA00173FCB /* WebDriverAgentLib_tvOS */; @@ -5469,11 +5257,11 @@ target = EE158A981CBD452B00A3E3F0 /* WebDriverAgentLib */; targetProxy = AD8D96F01D3C12960061268E /* PBXContainerItemProxy */; }; - DC55141E0512D9D0ADE54987 /* PBXTargetDependency */ = { + B5DEACD7B6109622C39DEFD1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = IntegrationApp_watchOS; target = 9D6B02D8FC050BAF7159284F /* IntegrationApp_watchOS */; - targetProxy = D564026BFC4C47F0187BD64C /* PBXContainerItemProxy */; + targetProxy = 61F47E68EDECE16439DC2F57 /* PBXContainerItemProxy */; }; EE158B5C1CBD462500A3E3F0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -5935,6 +5723,26 @@ }; name = Release; }; + 6C5CE831509454D3ACD726D0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.wda.IntegrationTests.watchOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SWIFT_OBJC_BRIDGING_HEADER = "WebDriverAgentTests/IntegrationTests_watchOS/IntegrationTests_watchOS-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + TEST_TARGET_NAME = IntegrationApp_watchOS; + VALIDATE_PRODUCT = YES; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Release; + }; 91F9DB0A1B99DBC2001349B2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -6067,22 +5875,6 @@ }; name = Release; }; - 98904BB9FB61E1406D4E2754 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.facebook.wda.IntegrationTests.watchOS; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - TEST_TARGET_NAME = IntegrationApp_watchOS; - VALIDATE_PRODUCT = YES; - WATCHOS_DEPLOYMENT_TARGET = 10.0; - }; - name = Release; - }; 9AD9FD0D35FBBD5C87BC6F5B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -6160,9 +5952,11 @@ ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_WKWatchOnly = YES; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.wda.IntegrationApp.watchOS; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; @@ -6202,9 +5996,11 @@ ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_WKWatchOnly = YES; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.facebook.wda.IntegrationApp.watchOS; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = watchos; @@ -6296,6 +6092,25 @@ }; name = Release; }; + CF2088FE066E2E9E31EB2544 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = "$(inherited)"; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.wda.IntegrationTests.watchOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = watchos; + SWIFT_OBJC_BRIDGING_HEADER = "WebDriverAgentTests/IntegrationTests_watchOS/IntegrationTests_watchOS-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 4; + TEST_TARGET_NAME = IntegrationApp_watchOS; + WATCHOS_DEPLOYMENT_TARGET = 10.0; + }; + name = Debug; + }; D086EA231A06418467927B2C /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 717C0D862518ED7000CAA6EC /* TVOSTestSettings.xcconfig */; @@ -6315,21 +6130,6 @@ }; name = Debug; }; - ED63FDC4EBEFE82B7D106218 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - GENERATE_INFOPLIST_FILE = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.facebook.wda.IntegrationTests.watchOS; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = watchos; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 4; - TEST_TARGET_NAME = IntegrationApp_watchOS; - WATCHOS_DEPLOYMENT_TARGET = 10.0; - }; - name = Debug; - }; EE158A9E1CBD452B00A3E3F0 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; @@ -6849,6 +6649,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 3C27414B1DEE3FFF4AEE3112 /* Build configuration list for PBXNativeTarget "IntegrationTests_watchOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6C5CE831509454D3ACD726D0 /* Release */, + CF2088FE066E2E9E31EB2544 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 52337A6D84BBC564B5D2090B /* Build configuration list for PBXNativeTarget "IntegrationApp_tvOS" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -6921,15 +6730,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 9DA322A56A2DFF03F3E5CDEA /* Build configuration list for PBXNativeTarget "IntegrationTests_watchOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 98904BB9FB61E1406D4E2754 /* Release */, - ED63FDC4EBEFE82B7D106218 /* Debug */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; EE158AA01CBD452B00A3E3F0 /* Build configuration list for PBXNativeTarget "WebDriverAgentLib" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_watchOS.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_watchOS.xcscheme index d6c728fd07..8f6affd4d5 100644 --- a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_watchOS.xcscheme +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationTests_watchOS.xcscheme @@ -14,7 +14,7 @@ buildForAnalyzing = "NO"> @@ -32,7 +32,7 @@ skipped = "NO"> @@ -53,7 +53,7 @@ @@ -69,7 +69,7 @@ diff --git a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m index 8698eb734d..20d6baa31e 100644 --- a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +++ b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m @@ -10,6 +10,13 @@ #import +#import "FBConfiguration.h" +#import "FBErrorBuilder.h" +#import "FBLogger.h" +#import "FBXCAccessibilityElement.h" +#import "FBXCAXClientProxy.h" +#import "XCUIApplication.h" + /** Available parameters with their default values for XCTest: @"maxChildren" : (int)2147483647 @@ -60,6 +67,82 @@ static id swizzledSnapshotParameters(id self, SEL _cmd) return result; } +static id (*original_requestSnapshotForElement)(id, SEL, id, id, id, NSError **); + +// pid -> last-unresponsive-at. XCTest retries a failed snapshot request several +// times in a row; this lets retries fail fast within `timeout` of the last check +// instead of each re-running the full wait. +static NSMutableDictionary *unresponsiveApplicationPids; +static NSObject *unresponsiveApplicationPidsLock; + +static NSError *FBBuildUnresponsiveApplicationError(int pid, NSTimeInterval timeout) +{ + // https://github.com/appium/WebDriverAgent/issues/1210 + NSString *description = [NSString stringWithFormat: + @"The application with process identifier %d did not confirm its main run loop is " + @"responsive within %.1f second(s) and is likely in an unresponsive state. " + @"Aborting the accessibility snapshot request instead of risking an indefinite " + @"hang.", + pid, timeout]; + [FBLogger logFmt:@"%@", description]; + NSError *error; + [[[FBErrorBuilder builder] withDescription:description] buildError:&error]; + return error; +} + +// Guards -[XCAXClient_iOS requestSnapshotForElement:...] against hanging forever +// on an unresponsive app (#1210). If accessibilityDeadline > 0, checks run loop +// responsiveness first and aborts with an error instead of risking an unbounded +// wait; otherwise falls through to the original, unbounded behavior. +static id swizzledRequestSnapshotForElement(id self, SEL _cmd, id element, id attributes, id parameters, NSError **error) +{ + NSTimeInterval timeout = FBConfiguration.sharedInstance.accessibilityDeadline; + if (timeout < DBL_EPSILON) { + return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error); + } + + int pid = [(id)element processIdentifier]; + XCUIApplication *application = [FBXCAXClientProxy.sharedClient monitoredApplicationWithProcessIdentifier:pid]; + if (nil == application) { + // Nothing to confirm responsiveness for (e.g. the system element) - fall + // through to the original behavior. + return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error); + } + + NSNumber *pidKey = @(pid); + @synchronized (unresponsiveApplicationPidsLock) { + NSDate *markedUnresponsiveAt = unresponsiveApplicationPids[pidKey]; + if (nil != markedUnresponsiveAt && -markedUnresponsiveAt.timeIntervalSinceNow < timeout) { + if (nil != error) { + *error = FBBuildUnresponsiveApplicationError(pid, timeout); + } + return nil; + } + } + + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + __block BOOL isResponsive = NO; + [FBXCAXClientProxy.sharedClient notifyWhenEventLoopIsIdleForApplication:application + reply:^(id result, NSError *idleError) { + isResponsive = (nil == idleError); + dispatch_semaphore_signal(sem); + }]; + BOOL didReplyInTime = 0 == dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC))); + if (didReplyInTime && isResponsive) { + @synchronized (unresponsiveApplicationPidsLock) { + [unresponsiveApplicationPids removeObjectForKey:pidKey]; + } + return original_requestSnapshotForElement(self, _cmd, element, attributes, parameters, error); + } + + @synchronized (unresponsiveApplicationPidsLock) { + unresponsiveApplicationPids[pidKey] = [NSDate date]; + } + if (nil != error) { + *error = FBBuildUnresponsiveApplicationError(pid, timeout); + } + return nil; +} @implementation XCAXClient_iOS (FBSnapshotReqParams) @@ -69,6 +152,9 @@ @implementation XCAXClient_iOS (FBSnapshotReqParams) + (void)load { + unresponsiveApplicationPids = [NSMutableDictionary new]; + unresponsiveApplicationPidsLock = [NSObject new]; + Method original_defaultParametersMethod = class_getInstanceMethod(self.class, @selector(defaultParameters)); IMP swizzledDefaultParametersImp = (IMP)swizzledDefaultParameters; original_defaultParameters = (id (*)(id, SEL)) method_setImplementation(original_defaultParametersMethod, swizzledDefaultParametersImp); @@ -76,6 +162,10 @@ + (void)load Method original_snapshotParametersMethod = class_getInstanceMethod(NSClassFromString(@"XCTElementQuery"), NSSelectorFromString(@"snapshotParameters")); IMP swizzledSnapshotParametersImp = (IMP)swizzledSnapshotParameters; original_snapshotParameters = (id (*)(id, SEL)) method_setImplementation(original_snapshotParametersMethod, swizzledSnapshotParametersImp); + + Method original_requestSnapshotForElementMethod = class_getInstanceMethod(self.class, @selector(requestSnapshotForElement:attributes:parameters:error:)); + IMP swizzledRequestSnapshotForElementImp = (IMP)swizzledRequestSnapshotForElement; + original_requestSnapshotForElement = (id (*)(id, SEL, id, id, id, NSError **)) method_setImplementation(original_requestSnapshotForElementMethod, swizzledRequestSnapshotForElementImp); } #pragma clang diagnostic pop diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.m b/WebDriverAgentLib/Commands/FBCustomCommands.m index 192c2e994d..91a58fc2ab 100644 --- a/WebDriverAgentLib/Commands/FBCustomCommands.m +++ b/WebDriverAgentLib/Commands/FBCustomCommands.m @@ -205,10 +205,13 @@ + (NSArray *)routes + (id)handleActiveAppInfo:(FBRouteRequest *)request { XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + // .identifier can be nil if the app stopped answering accessibility requests + // and accessibilityDeadline aborted the underlying snapshot fetch (#1210). + NSString *name = app.identifier ?: @"unknown"; return FBResponseWithObject(@{ @"pid": @(app.processID), @"bundleId": app.bundleID, - @"name": app.identifier, + @"name": name, @"processArguments": [self processArguments:app], }); } diff --git a/WebDriverAgentLib/Commands/FBScreenCaptureCommands.m b/WebDriverAgentLib/Commands/FBScreenCaptureCommands.m index e7ddabdcd4..81fdf676ac 100644 --- a/WebDriverAgentLib/Commands/FBScreenCaptureCommands.m +++ b/WebDriverAgentLib/Commands/FBScreenCaptureCommands.m @@ -26,17 +26,17 @@ + (NSArray *)routes { return @[ - // The broadcast routes must be registered before the '/:id' routes: RoutingHTTPServer + // The broadcast routes must be registered before the '/:id' routes: FBHTTPServer // matches routes in registration order, so 'GET /mobilerun/screencapture/broadcast' would // otherwise be swallowed by 'GET /mobilerun/screencapture/:id'. [[FBRoute POST:@"/mobilerun/screencapture/broadcast/start"] respondWithTarget:self action:@selector(handleStartBroadcast:)], [[FBRoute POST:@"/mobilerun/screencapture/broadcast/stop"] respondWithTarget:self action:@selector(handleStopBroadcast:)], - // Not marked onControlQueue: decorating a session-required route reads FBSession's static - // active-session state, which the automation queue writes without synchronization. + // Not marked standalone: decorating a session-required route reads FBSession's static + // active-session state, so it stays serialized behind the automation funnel. [[FBRoute GET:@"/mobilerun/screencapture/broadcast"] respondWithTarget:self action:@selector(handleGetBroadcastStatus:)], [[FBRoute POST:@"/mobilerun/screencapture/broadcast/start"].withoutSession respondWithTarget:self action:@selector(handleStartBroadcast:)], [[FBRoute POST:@"/mobilerun/screencapture/broadcast/stop"].withoutSession respondWithTarget:self action:@selector(handleStopBroadcast:)], - [[[FBRoute GET:@"/mobilerun/screencapture/broadcast"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleGetBroadcastStatus:)], + [[FBRoute GET:@"/mobilerun/screencapture/broadcast"].withoutSession.standalone respondWithTarget:self action:@selector(handleGetBroadcastStatus:)], [[FBRoute POST:@"/mobilerun/screencapture/start"] respondWithTarget:self action:@selector(handleStartScreenCapture:)], [[FBRoute POST:@"/mobilerun/screencapture/stop"] respondWithTarget:self action:@selector(handleStopAllScreenCapture:)], @@ -46,11 +46,11 @@ + (NSArray *)routes [[FBRoute POST:@"/mobilerun/screencapture/:id/keyframe"] respondWithTarget:self action:@selector(handleRequestKeyFrame:)], [[FBRoute POST:@"/mobilerun/screencapture/start"].withoutSession respondWithTarget:self action:@selector(handleStartScreenCapture:)], - [[[FBRoute POST:@"/mobilerun/screencapture/stop"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleStopAllScreenCapture:)], - [[[FBRoute GET:@"/mobilerun/screencapture"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleListScreenCapture:)], - [[[FBRoute GET:@"/mobilerun/screencapture/:id"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleGetScreenCapture:)], - [[[FBRoute POST:@"/mobilerun/screencapture/:id/stop"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleStopScreenCapture:)], - [[[FBRoute POST:@"/mobilerun/screencapture/:id/keyframe"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleRequestKeyFrame:)], + [[FBRoute POST:@"/mobilerun/screencapture/stop"].withoutSession.standalone respondWithTarget:self action:@selector(handleStopAllScreenCapture:)], + [[FBRoute GET:@"/mobilerun/screencapture"].withoutSession.standalone respondWithTarget:self action:@selector(handleListScreenCapture:)], + [[FBRoute GET:@"/mobilerun/screencapture/:id"].withoutSession.standalone respondWithTarget:self action:@selector(handleGetScreenCapture:)], + [[FBRoute POST:@"/mobilerun/screencapture/:id/stop"].withoutSession.standalone respondWithTarget:self action:@selector(handleStopScreenCapture:)], + [[FBRoute POST:@"/mobilerun/screencapture/:id/keyframe"].withoutSession.standalone respondWithTarget:self action:@selector(handleRequestKeyFrame:)], ]; } diff --git a/WebDriverAgentLib/Commands/FBScreenshotCommands.m b/WebDriverAgentLib/Commands/FBScreenshotCommands.m index e2b0907223..9586a197fb 100644 --- a/WebDriverAgentLib/Commands/FBScreenshotCommands.m +++ b/WebDriverAgentLib/Commands/FBScreenshotCommands.m @@ -18,8 +18,8 @@ + (NSArray *)routes { return @[ - [[FBRoute GET:@"/screenshot"].withoutSession respondWithTarget:self action:@selector(handleGetScreenshot:)], - [[FBRoute GET:@"/screenshot"] respondWithTarget:self action:@selector(handleGetScreenshot:)], + [[FBRoute GET:@"/screenshot"].withoutSession.standalone respondWithTarget:self action:@selector(handleGetScreenshot:)], + [[FBRoute GET:@"/screenshot"].standalone respondWithTarget:self action:@selector(handleGetScreenshot:)], ]; } diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgentLib/Commands/FBSessionCommands.m index 800c86ff40..9f5a601ea8 100644 --- a/WebDriverAgentLib/Commands/FBSessionCommands.m +++ b/WebDriverAgentLib/Commands/FBSessionCommands.m @@ -47,8 +47,8 @@ + (NSArray *)routes [[FBRoute POST:@"/wda/apps/state"] respondWithTarget:self action:@selector(handleSessionAppState:)], [[FBRoute GET:@"/wda/apps/list"] respondWithTarget:self action:@selector(handleGetActiveAppsList:)], [[FBRoute GET:@""] respondWithTarget:self action:@selector(handleGetActiveSession:)], - [[FBRoute DELETE:@""] respondWithTarget:self action:@selector(handleDeleteSession:)], - [[[FBRoute GET:@"/status"].withoutSession onControlQueue] respondWithTarget:self action:@selector(handleGetStatus:)], + [[FBRoute DELETE:@""].standalone respondWithTarget:self action:@selector(handleDeleteSession:)], + [[FBRoute GET:@"/status"].withoutSession.standalone respondWithTarget:self action:@selector(handleGetStatus:)], // Health check might modify simulator state so it should only be called in-between testing sessions [[FBRoute GET:@"/wda/healthcheck"].withoutSession respondWithTarget:self action:@selector(handleGetHealthCheck:)], @@ -89,9 +89,7 @@ + (NSArray *)routes + (id)handleCreateSession:(FBRouteRequest *)request { - if (nil != FBSession.activeSession) { - [FBSession.activeSession kill]; - } + [FBSession killActiveSessionAndWaitForTeardown]; NSDictionary *capabilities; id errorResponse = [self capabilitiesFromCreateSessionRequest:request @@ -306,6 +304,9 @@ + (void)applyConfigurationFromCapabilities:(NSDictionary *)capab if (nil != capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) { FBConfiguration.sharedInstance.waitForIdleTimeout = [capabilities[FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]; } + if (nil != capabilities[FB_SETTING_ACCESSIBILITY_DEADLINE]) { + FBConfiguration.sharedInstance.accessibilityDeadline = [capabilities[FB_SETTING_ACCESSIBILITY_DEADLINE] doubleValue]; + } if (nil == capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] || [capabilities[FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE] boolValue]) { [FBConfiguration.sharedInstance forceSimulatorSoftwareKeyboardPresence]; diff --git a/WebDriverAgentLib/Commands/FBUnknownCommands.m b/WebDriverAgentLib/Commands/FBUnknownCommands.m index 4355953854..f3017616aa 100644 --- a/WebDriverAgentLib/Commands/FBUnknownCommands.m +++ b/WebDriverAgentLib/Commands/FBUnknownCommands.m @@ -23,10 +23,10 @@ + (NSArray *)routes { return @[ - [[[FBRoute GET:@"/*"].withoutSession onControlQueue] respondWithTarget:self action:@selector(unhandledHandler:)], - [[[FBRoute POST:@"/*"].withoutSession onControlQueue] respondWithTarget:self action:@selector(unhandledHandler:)], - [[[FBRoute PUT:@"/*"].withoutSession onControlQueue] respondWithTarget:self action:@selector(unhandledHandler:)], - [[[FBRoute DELETE:@"/*"].withoutSession onControlQueue] respondWithTarget:self action:@selector(unhandledHandler:)] + [[FBRoute GET:@"/*"].withoutSession.standalone respondWithTarget:self action:@selector(unhandledHandler:)], + [[FBRoute POST:@"/*"].withoutSession.standalone respondWithTarget:self action:@selector(unhandledHandler:)], + [[FBRoute PUT:@"/*"].withoutSession.standalone respondWithTarget:self action:@selector(unhandledHandler:)], + [[FBRoute DELETE:@"/*"].withoutSession.standalone respondWithTarget:self action:@selector(unhandledHandler:)] ]; } diff --git a/WebDriverAgentLib/FBAlert.m b/WebDriverAgentLib/FBAlert.m index 93e014bcdd..9324ea4b74 100644 --- a/WebDriverAgentLib/FBAlert.m +++ b/WebDriverAgentLib/FBAlert.m @@ -236,17 +236,18 @@ - (void)accept } XCUIElement *acceptButton = nil; - if (FBConfiguration.sharedInstance.acceptAlertButtonSelector.length) { + NSString *acceptAlertButtonSelector = FBConfiguration.sharedInstance.acceptAlertButtonSelector ?: @""; + if (acceptAlertButtonSelector.length) { NSString *errorReason = nil; @try { - acceptButton = [[alertElement fb_descendantsMatchingClassChain:FBConfiguration.sharedInstance.acceptAlertButtonSelector + acceptButton = [[alertElement fb_descendantsMatchingClassChain:acceptAlertButtonSelector shouldReturnAfterFirstMatch:YES] firstObject]; } @catch (NSException *ex) { errorReason = ex.reason; } if (nil == acceptButton) { [FBLogger logFmt:@"Cannot find any match for Accept alert button using the class chain selector '%@'", - FBConfiguration.sharedInstance.acceptAlertButtonSelector]; + acceptAlertButtonSelector]; if (nil != errorReason) { [FBLogger logFmt:@"Original error: %@", errorReason]; } @@ -293,17 +294,18 @@ - (void)dismiss } XCUIElement *dismissButton = nil; - if (FBConfiguration.sharedInstance.dismissAlertButtonSelector.length) { + NSString *dismissAlertButtonSelector = FBConfiguration.sharedInstance.dismissAlertButtonSelector ?: @""; + if (dismissAlertButtonSelector.length) { NSString *errorReason = nil; @try { - dismissButton = [[alertElement fb_descendantsMatchingClassChain:FBConfiguration.sharedInstance.dismissAlertButtonSelector + dismissButton = [[alertElement fb_descendantsMatchingClassChain:dismissAlertButtonSelector shouldReturnAfterFirstMatch:YES] firstObject]; } @catch (NSException *ex) { errorReason = ex.reason; } if (nil == dismissButton) { [FBLogger logFmt:@"Cannot find any match for Dismiss alert button using the class chain selector '%@'", - FBConfiguration.sharedInstance.dismissAlertButtonSelector]; + dismissAlertButtonSelector]; if (nil != errorReason) { [FBLogger logFmt:@"Original error: %@", errorReason]; } diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 31dc42d20d..185ec8aa5b 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 16.4.0 + 16.8.0 CFBundleSignature ???? CFBundleVersion - 16.4.0 + 16.8.0 NSPrincipalClass diff --git a/WebDriverAgentLib/Routing/FBHTTPServer.h b/WebDriverAgentLib/Routing/FBHTTPServer.h new file mode 100644 index 0000000000..75ea80ccd4 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBHTTPServer.h @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +// A minimal HTTP/1.1 server on top of FBTCPSocket (Network.framework-backed on every platform, +// since watchOS forbids BSD sockets outright - see FBTCPSocket.h/.m). +// +// No range requests or request pipelining - just request line + headers + Content-Length body, +// and ":param" path matching. Any Transfer-Encoding is rejected outright (501) rather than +// silently mishandled, since no decoder is implemented. + +@import Foundation; + +#import "RouteRequest.h" +#import "RouteResponse.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FBHTTPServer : NSObject + +/*! The port the server is (or will be) listening on */ +@property (nonatomic) uint16_t port; + +/*! Whether the server is currently listening for connections */ +@property (nonatomic, readonly) BOOL isRunning; + +/** + Sets the dispatch queue on which route blocks are invoked. Pass NULL to invoke them + synchronously on the socket's own queue. + */ +- (void)setRouteQueue:(nullable dispatch_queue_t)queue; + +/** + Sets a header which is added to every response, unless overridden by the route itself. + */ +- (void)setDefaultHeader:(NSString *)field value:(NSString *)value; + +/** + Sets the local IP address to bind the listener to. Must be called before -start:. Pass nil (the + default) to listen on all interfaces. + */ +- (void)setInterface:(nullable NSString *)interface; + +/** + Registers a route handler for the given HTTP method and path pattern (":param" segments are + captured into the request's `params`). Equivalent to -handleMethod:withPath:standalone:block: + with standalone:NO. + */ +- (void)handleMethod:(NSString *)method + withPath:(NSString *)path + block:(void (^)(RouteRequest *request, RouteResponse *response))block; + +/** + Registers a route handler that, when `standalone` is YES, bypasses -routeQueue entirely so a + handler stuck on that queue can never block it. Concurrent requests to the same method+path are + coalesced into a single in-flight execution, whose response is delivered to all of them; anything + else runs on its own queue, so distinct standalone endpoints always execute in parallel with each + other and with whatever is stuck on -routeQueue. + */ +- (void)handleMethod:(NSString *)method + withPath:(NSString *)path + standalone:(BOOL)standalone + block:(void (^)(RouteRequest *request, RouteResponse *response))block; + +/** + Convenience for -handleMethod:@"GET" withPath:path block:block. + */ +- (void)get:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block; + +/** + Immediately sends `response` to every non-standalone request currently pending for the given + "sessionID" path param - whether still queued on -routeQueue or already executing - instead of + leaving their HTTP clients waiting on a session that no longer exists. A request that has already + started executing keeps running to completion in the background regardless (GCD gives no way to + abort a block once it starts), but its eventual result is discarded rather than ever reaching a + client. `response` is written as-is to every pending client, so the caller is expected to supply + a fully-populated, protocol-correct error response (e.g. a W3C-shaped JSON body). + */ +- (void)abandonPendingRequestsForSessionID:(NSString *)sessionID withResponse:(RouteResponse *)response; + +/** + Starts listening on `port`. + */ +- (BOOL)start:(NSError **)error; + +/** + Stops listening and disconnects all clients. + */ +- (void)stop:(BOOL)immediately; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBHTTPServer.m b/WebDriverAgentLib/Routing/FBHTTPServer.m new file mode 100644 index 0000000000..914e564c2a --- /dev/null +++ b/WebDriverAgentLib/Routing/FBHTTPServer.m @@ -0,0 +1,886 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "FBHTTPServer.h" + +#import "FBCommandStatus.h" +#import "FBConfiguration.h" +#import "FBLogger.h" +#import "FBResponsePayload.h" +#import "FBTCPSocket.h" + +static NSData *FBCRLFCRLFData(void) +{ + static NSData *data; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + data = [@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]; + }); + return data; +} + +// -dataUsingEncoding:NSUTF8StringEncoding never actually returns nil; this just keeps the cast +// out of every call site below. +static NSData * _Nonnull FBUTF8Data(NSString *string) +{ + return (NSData * _Nonnull)[string dataUsingEncoding:NSUTF8StringEncoding]; +} + +// Caps a request's header block, so a connection that never completes one cannot grow its buffer +// without limit. Far above anything a real request needs. +static const NSUInteger FBMaxRequestHeaderSize = 64 * 1024; + +// ASCII decimal digits only. -integerValue must not be used here: it maps garbage silently +// ("bogus" -> 0, "12abc" -> 12), desyncing the framing of every later request on the connection. +static BOOL FBParseContentLength(NSString *value, NSUInteger *outLength) +{ + // Bounds the digit count so the accumulation below cannot overflow unsigned long long. + if (value.length < 1 || value.length > 15) { + return NO; + } + unsigned long long result = 0; + for (NSUInteger i = 0; i < value.length; i++) { + unichar c = [value characterAtIndex:i]; + if (c < '0' || c > '9') { + return NO; + } + result = result * 10 + (c - '0'); + } + // NSUInteger is 32-bit on watchOS (arm64_32), so the digit bound alone would still truncate. + if (result > (unsigned long long)NSUIntegerMax) { + return NO; + } + *outLength = (NSUInteger)result; + return YES; +} + +@interface FBHTTPRoute : NSObject +@property (nonatomic, copy) NSString *verb; +@property (nonatomic, strong) NSRegularExpression *regex; +@property (nonatomic, copy, nullable) NSArray *keys; +@property (nonatomic, copy) void (^block)(RouteRequest *request, RouteResponse *response); +@property (nonatomic, assign) BOOL isStandalone; +@end + +@implementation FBHTTPRoute +@end + + +// Cached result of parsing a connection's request line + headers, kept around while its body is +// still streaming in so a slow body doesn't cause the header block to be re-found and re-parsed +// on every single incoming TCP segment. +@interface FBPendingHTTPRequestHeader : NSObject +@property (nonatomic, copy) NSString *method; +@property (nonatomic, copy) NSString *pathAndQuery; +@property (nonatomic) NSUInteger bodyStart; +@property (nonatomic) NSUInteger contentLength; +@end + +@implementation FBPendingHTTPRequestHeader +@end + + +// One dispatched-but-not-yet-answered request. Default (pointer) identity, so two pipelined +// requests sharing a connection are never conflated into a single tracked entry. +@interface FBPendingRequest : NSObject +@property (nonatomic, strong, readonly) nw_connection_t client; +@end + +@implementation FBPendingRequest + +- (instancetype)initWithClient:(nw_connection_t)client +{ + if ((self = [super init])) { + _client = client; + } + return self; +} + +@end + + +@interface FBHTTPServer () + +@property (nonatomic, nullable, strong) FBTCPSocket *socket; +@property (nonatomic, strong) NSMutableArray *routes; +@property (nonatomic, strong) NSMutableDictionary *defaultHeaders; +@property (nonatomic, nullable) dispatch_queue_t routeQueue; +@property (nonatomic, copy, nullable) NSString *interface; +// nw_connection_t isn't NSCopying, so it can't be an NSDictionary key - use NSMapTable instead. +@property (nonatomic, strong) NSMapTable *connectionBuffers; +// Per-client cache of the already-parsed request line + headers while its body is still +// arriving; nil while a client's next unread bytes start with an unparsed header block. +@property (nonatomic, strong) NSMapTable *pendingRequestHeaders; +// All buffer access - appending new data and -processBufferForClient:'s unlocked parse - is +// funneled through this one serial queue, so appends can never race a parse. +@property (nonatomic, strong) dispatch_queue_t bufferProcessingQueue; +// Connections with a request parsed off the buffer but not yet answered. Blocks +// -processBufferForClient: from starting the next pipelined request, so responses on one +// connection can't be written out of order. Guarded by @synchronized(self.connectionBuffers). +@property (nonatomic, strong) NSMutableSet *connectionsAwaitingResponse; +// Keyed by "METHOD path" - requests waiting on an already in-flight standalone request for that +// endpoint. Guarded by @synchronized(self.standaloneWaiters). +@property (nonatomic, strong) NSMutableDictionary *> *standaloneWaiters; +// Keyed by the "sessionID" path param - requests currently queued or executing for that session, +// standalone or not (except DELETE /session itself - see -dispatchMethod:). See +// -abandonPendingRequestsForSessionID:. Guarded by @synchronized(self.pendingSessionRequests). +@property (nonatomic, strong) NSMutableDictionary *> *pendingSessionRequests; +// Already-abandoned sessions mapped to the response they were abandoned with, so a request +// parsed after that point is answered at once instead of queueing for a session that is gone. +// Session ids are UUIDs, so an entry can never reject a live session. Insertion-ordered by +// `abandonedSessionOrder`. Guarded by @synchronized(self.pendingSessionRequests). +@property (nonatomic, strong) NSMutableDictionary *abandonedSessionResponses; +@property (nonatomic, strong) NSMutableArray *abandonedSessionOrder; +// When each connection started waiting for its current request. The reaper closes connections +// whose entry outlives FBIncompleteRequestTimeout; idle keep-alive connections have no entry and +// are exempt. Guarded by @synchronized(self.connectionBuffers). +@property (nonatomic, strong) NSMapTable *incompleteRequestStarts; +@property (nonatomic, nullable) dispatch_source_t staleConnectionReaper; + +@end + +// How long a connection may take to deliver a complete request, matching the header read timeout +// the previous CocoaHTTPServer stack enforced. +static const NSTimeInterval FBIncompleteRequestTimeout = 30.0; +static const int64_t FBStaleConnectionSweepIntervalSec = 10; + +// Only has to outlive the in-flight requests of the previous few sessions; older ones are +// answered "no such driver" by the route itself anyway. +static const NSUInteger FBMaxRecordedAbandonedSessions = 8; + +@implementation FBHTTPServer + +- (instancetype)init +{ + if ((self = [super init])) { + _routes = [NSMutableArray array]; + _defaultHeaders = [NSMutableDictionary dictionary]; + _connectionBuffers = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory) + valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory]; + _pendingRequestHeaders = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory) + valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory]; + _bufferProcessingQueue = dispatch_queue_create("com.facebook.wda.http.bufferProcessing", DISPATCH_QUEUE_SERIAL); + _connectionsAwaitingResponse = [NSMutableSet set]; + _standaloneWaiters = [NSMutableDictionary dictionary]; + _pendingSessionRequests = [NSMutableDictionary dictionary]; + _abandonedSessionResponses = [NSMutableDictionary dictionary]; + _abandonedSessionOrder = [NSMutableArray array]; + _incompleteRequestStarts = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory) + valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory]; + } + return self; +} + +- (void)setRouteQueue:(nullable dispatch_queue_t)queue +{ + _routeQueue = queue; +} + +- (void)setDefaultHeader:(NSString *)field value:(NSString *)value +{ + self.defaultHeaders[field] = value; +} + +- (void)setInterface:(nullable NSString *)interface +{ + _interface = interface.copy; +} + +#pragma mark - Route registration + +- (FBHTTPRoute *)compiledRouteWithPath:(NSString *)path +{ + FBHTTPRoute *route = [FBHTTPRoute new]; + NSMutableArray *keys = [NSMutableArray array]; + + // Escape regex-significant characters before substituting :param placeholders. + NSRegularExpression *escapeRegex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" + options:(NSRegularExpressionOptions)0 + error:nil]; + NSString *escapedPath = [escapeRegex stringByReplacingMatchesInString:path + options:(NSMatchingOptions)0 + range:NSMakeRange(0, path.length) + withTemplate:@"\\\\$0"]; + + NSRegularExpression *paramRegex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)" + options:(NSRegularExpressionOptions)0 + error:nil]; + NSMutableString *regexPath = [NSMutableString stringWithString:escapedPath]; + __block NSInteger diff = 0; + __block NSUInteger wildcardIndex = 0; + [paramRegex enumerateMatchesInString:escapedPath + options:(NSMatchingOptions)0 + range:NSMakeRange(0, escapedPath.length) + usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) { + NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length); + NSString *capturedString = [escapedPath substringWithRange:result.range]; + NSString *replacementString; + if ([capturedString isEqualToString:@"*"]) { + // Only the first wildcard keeps the plain "wildcards" name - later ones get an index + // suffix so multiple "*" segments in one path don't overwrite each other's capture. + NSString *wildcardKey = 0 == wildcardIndex ? @"wildcards" : [NSString stringWithFormat:@"wildcards%lu", (unsigned long)wildcardIndex]; + wildcardIndex++; + [keys addObject:wildcardKey]; + replacementString = @"(.*?)"; + } else { + NSString *keyString = [escapedPath substringWithRange:[result rangeAtIndex:2]]; + [keys addObject:keyString]; + replacementString = @"([^/]+)"; + } + [regexPath replaceCharactersInRange:replacementRange withString:replacementString]; + diff += replacementString.length - result.range.length; + }]; + + NSString *anchoredPattern = [NSString stringWithFormat:@"^%@$", regexPath]; + route.regex = [NSRegularExpression regularExpressionWithPattern:anchoredPattern + options:NSRegularExpressionCaseInsensitive + error:nil]; + route.keys = keys.count > 0 ? keys.copy : nil; + return route; +} + +- (void)handleMethod:(NSString *)method + withPath:(NSString *)path + block:(void (^)(RouteRequest *request, RouteResponse *response))block +{ + [self handleMethod:method withPath:path standalone:NO block:block]; +} + +- (void)handleMethod:(NSString *)method + withPath:(NSString *)path + standalone:(BOOL)standalone + block:(void (^)(RouteRequest *request, RouteResponse *response))block +{ + FBHTTPRoute *route = [self compiledRouteWithPath:path]; + route.verb = method.uppercaseString; + route.block = block; + route.isStandalone = standalone; + [self.routes addObject:route]; +} + +- (void)get:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block +{ + [self handleMethod:@"GET" withPath:path block:block]; +} + +#pragma mark - Lifecycle + +- (BOOL)start:(NSError **)error +{ + FBTCPSocket *socket = [[FBTCPSocket alloc] initWithPort:self.port]; + socket.interface = self.interface; + socket.delegate = self; + if (![socket startWithError:error]) { + return NO; + } + self.socket = socket; + dispatch_source_t reaper = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.bufferProcessingQueue); + dispatch_source_set_timer(reaper, + dispatch_time(DISPATCH_TIME_NOW, FBStaleConnectionSweepIntervalSec * NSEC_PER_SEC), + (uint64_t)FBStaleConnectionSweepIntervalSec * NSEC_PER_SEC, + NSEC_PER_SEC); + __weak typeof(self) weakSelf = self; + dispatch_source_set_event_handler(reaper, ^{ + [weakSelf reapStaleConnections]; + }); + dispatch_resume(reaper); + self.staleConnectionReaper = reaper; + _isRunning = YES; + return YES; +} + +- (void)reapStaleConnections +{ + NSMutableArray *staleConnections = [NSMutableArray array]; + @synchronized (self.connectionBuffers) { + for (id connection in self.incompleteRequestStarts) { + // Waiting on the handler, not the peer - never reap, however long the handler takes. + if ([self.connectionsAwaitingResponse containsObject:connection]) { + continue; + } + NSDate *start = [self.incompleteRequestStarts objectForKey:connection]; + if (nil != start && -start.timeIntervalSinceNow > FBIncompleteRequestTimeout) { + [staleConnections addObject:connection]; + } + } + } + for (id connection in staleConnections) { + [FBLogger logFmt:@"Closing a connection that did not deliver a complete request within %@ seconds", @(FBIncompleteRequestTimeout)]; + [self closeClient:(nw_connection_t)connection]; + } +} + +- (void)stop:(BOOL)immediately +{ + dispatch_source_t reaper = self.staleConnectionReaper; + if (nil != reaper) { + dispatch_source_cancel(reaper); + self.staleConnectionReaper = nil; + } + [self.socket stop]; + self.socket = nil; + @synchronized (self.connectionBuffers) { + [self.connectionBuffers removeAllObjects]; + [self.pendingRequestHeaders removeAllObjects]; + [self.connectionsAwaitingResponse removeAllObjects]; + [self.incompleteRequestStarts removeAllObjects]; + } + _isRunning = NO; +} + +#pragma mark - FBTCPSocketDelegate + +- (void)didClientConnect:(nw_connection_t)newClient +{ + @synchronized (self.connectionBuffers) { + [self.connectionBuffers setObject:[NSMutableData data] forKey:newClient]; + // Starts at connect, so a peer that connects and then sends nothing is reaped too. + [self.incompleteRequestStarts setObject:[NSDate date] forKey:newClient]; + } +} + +- (void)didClientDisconnect:(nw_connection_t)client +{ + @synchronized (self.connectionBuffers) { + [self.connectionBuffers removeObjectForKey:client]; + [self.pendingRequestHeaders removeObjectForKey:client]; + [self.connectionsAwaitingResponse removeObject:client]; + [self.incompleteRequestStarts removeObjectForKey:client]; + } +} + +- (void)client:(nw_connection_t)client didReceiveData:(NSData *)data +{ + // The append itself, not just the parse, must run on bufferProcessingQueue: otherwise a receive + // callback here could still mutate the buffer while -processBufferForClient: is reading it + // unlocked on that queue. + __weak typeof(self) weakSelf = self; + dispatch_async(self.bufferProcessingQueue, ^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } + BOOL isOverBufferCap = NO; + @synchronized (strongSelf.connectionBuffers) { + NSMutableData *buffer = [strongSelf.connectionBuffers objectForKey:client]; + if (nil == buffer) { + return; + } + [buffer appendData:data]; + // One maximal header block plus one maximal body, plus headroom for a pipelined follow-up. + // The per-request checks don't run while a request is executing, so without this cap a + // client could pump data unboundedly for as long as its previous request takes. + uint64_t bufferCap = FBConfiguration.sharedInstance.httpRequestBodySizeLimit + 2 * (uint64_t)FBMaxRequestHeaderSize; + if (bufferCap < FBConfiguration.sharedInstance.httpRequestBodySizeLimit) { + bufferCap = UINT64_MAX; + } + isOverBufferCap = buffer.length > bufferCap; + // In the body phase the timeout is an idle bound, refreshed on progress: a declared body + // may legitimately be slow and its size is already capped by Content-Length. In the header + // phase the clock is only started, never refreshed, so drip-fed headers cannot outlive it. + BOOL isBodyPhase = nil != [strongSelf.pendingRequestHeaders objectForKey:client]; + if (isBodyPhase || nil == [strongSelf.incompleteRequestStarts objectForKey:client]) { + [strongSelf.incompleteRequestStarts setObject:[NSDate date] forKey:client]; + } + } + if (isOverBufferCap) { + // No response owed: a peer this far past any legitimate size is not reading anyway. + [FBLogger log:@"Closing a connection that overflowed its request buffer"]; + [strongSelf closeClient:client]; + return; + } + [strongSelf processBufferForClient:client]; + }); +} + +#pragma mark - HTTP parsing + +// Parses and dispatches at most one request per call; a connection with one already in flight is +// left alone (see -connectionsAwaitingResponse) until its response is written. +- (void)processBufferForClient:(nw_connection_t)client +{ + NSMutableData *buffer; + FBPendingHTTPRequestHeader *pending; + @synchronized (self.connectionBuffers) { + if ([self.connectionsAwaitingResponse containsObject:client]) { + return; + } + buffer = [self.connectionBuffers objectForKey:client]; + if (nil == buffer) { + return; + } + pending = [self.pendingRequestHeaders objectForKey:client]; + } + + if (nil == pending) { + NSRange headerEndRange = [buffer rangeOfData:FBCRLFCRLFData() options:(NSDataSearchOptions)0 range:NSMakeRange(0, buffer.length)]; + if (NSNotFound == headerEndRange.location) { + if (buffer.length > FBMaxRequestHeaderSize) { + // Past any legitimate header block and still unterminated - stop buffering. + [self respondBadRequestToClient:client]; + return; + } + // Wait for the rest of the header block to arrive. + return; + } + if (headerEndRange.location > FBMaxRequestHeaderSize) { + // The check above only fires while the terminator is missing; one large receive can deliver + // an oversized block with it, so bound the completed block too before parsing it. + [self respondBadRequestToClient:client]; + return; + } + + NSData *headerData = [buffer subdataWithRange:NSMakeRange(0, headerEndRange.location)]; + NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding]; + NSArray *lines = [headerString componentsSeparatedByString:@"\r\n"]; + if (lines.count < 1) { + [self respondBadRequestToClient:client]; + return; + } + + NSArray *requestLineParts = [lines.firstObject componentsSeparatedByString:@" "]; + if (requestLineParts.count < 2) { + [self respondBadRequestToClient:client]; + return; + } + + NSMutableDictionary *requestHeaders = [NSMutableDictionary dictionary]; + for (NSUInteger i = 1; i < lines.count; i++) { + NSString *line = lines[i]; + NSRange colonRange = [line rangeOfString:@":"]; + if (0 == line.length) { + continue; + } + if (NSNotFound == colonRange.location) { + // Malformed. Skipping it would drop what it meant to say: "Content-Length 5" would + // dispatch with an empty body, leaving its bytes to be parsed as another request. + [self respondBadRequestToClient:client]; + return; + } + NSString *name = [line substringToIndex:colonRange.location]; + // RFC 7230 (3.2.4): whitespace before the colon MUST be rejected. Storing "content-length " + // as its own key would drop the real header and desync the framing. + if (0 == name.length + || NSNotFound != [name rangeOfCharacterFromSet:NSCharacterSet.whitespaceAndNewlineCharacterSet].location) { + [self respondBadRequestToClient:client]; + return; + } + NSString *value = [[line substringFromIndex:colonRange.location + 1] + stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; + NSString *normalizedName = name.lowercaseString; + // RFC 7230 (3.3.3): repeated framing fields are unrecoverable. Last-wins would let an empty + // "Transfer-Encoding:" mask an earlier "chunked", and the last Content-Length drive parsing. + if (([normalizedName isEqualToString:@"content-length"] || [normalizedName isEqualToString:@"transfer-encoding"]) + && nil != requestHeaders[normalizedName]) { + [self respondBadRequestToClient:client]; + return; + } + requestHeaders[normalizedName] = value; + } + + NSString *transferEncoding = requestHeaders[@"transfer-encoding"]; + if (nil != transferEncoding) { + // No transfer decoder exists, so mere presence is rejected - including an empty value, + // which is not a valid encoding list and would let the body be misread as empty. + RouteResponse *notImplemented = [RouteResponse new]; + id notImplementedPayload = FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Transfer-Encoding is not supported" + traceback:nil]); + [notImplementedPayload dispatchWithResponse:notImplemented]; + [self failClient:client withResponse:notImplemented]; + return; + } + + NSString *contentLengthValue = requestHeaders[@"content-length"]; + NSUInteger contentLength = 0; + if (nil != contentLengthValue && !FBParseContentLength(contentLengthValue, &contentLength)) { + // The body's extent is unknowable, so the connection cannot be resynced - reject and close. + [self respondBadRequestToClient:client]; + return; + } + if (contentLength > FBConfiguration.sharedInstance.httpRequestBodySizeLimit) { + // Closes the connection after responding, since the rest of the oversized body is still incoming. + RouteResponse *tooLarge = [RouteResponse new]; + id tooLargePayload = FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"The request body exceeds the configured size limit" + traceback:nil]); + [tooLargePayload dispatchWithResponse:tooLarge]; + [self failClient:client withResponse:tooLarge]; + return; + } + + pending = [FBPendingHTTPRequestHeader new]; + pending.method = requestLineParts[0].uppercaseString; + pending.pathAndQuery = requestLineParts[1]; + pending.bodyStart = headerEndRange.location + headerEndRange.length; + pending.contentLength = contentLength; + @synchronized (self.connectionBuffers) { + [self.pendingRequestHeaders setObject:pending forKey:client]; + } + } + + NSUInteger totalRequestLength = pending.bodyStart + pending.contentLength; + if (buffer.length < totalRequestLength) { + // Wait for the rest of the body to arrive - the parsed header stays cached above, so this + // doesn't re-scan/re-parse the header block on every subsequently arriving chunk. + @synchronized (self.connectionBuffers) { + // The request is now in its body phase, which is idle-bounded rather than hard-bounded. + // -client:didReceiveData: samples that phase before this parse runs, so the receive that + // completed a slowly-delivered header (and carried the first body bytes) would otherwise + // leave the connection on its header-phase timestamp and let the sweep close it despite + // the body having just made progress. + [self.incompleteRequestStarts setObject:[NSDate date] forKey:client]; + } + return; + } + + NSData *body = pending.contentLength > 0 ? [buffer subdataWithRange:NSMakeRange(pending.bodyStart, pending.contentLength)] : [NSData data]; + + @synchronized (self.connectionBuffers) { + [buffer replaceBytesInRange:NSMakeRange(0, totalRequestLength) withBytes:NULL length:0]; + [self.pendingRequestHeaders removeObjectForKey:client]; + [self.connectionsAwaitingResponse addObject:client]; + if (0 == buffer.length) { + // A complete request was delivered and nothing further is buffered: the connection is a + // healthy keep-alive and must not be reaped while idle. + [self.incompleteRequestStarts removeObjectForKey:client]; + } else { + // Pipelined bytes of the next request are already buffered - restart its clock. + [self.incompleteRequestStarts setObject:[NSDate date] forKey:client]; + } + } + + [self dispatchMethod:pending.method pathAndQuery:pending.pathAndQuery body:body client:client]; +} + +// Removes the client's buffered state and responds with a closing error response. Removing the +// buffer synchronously ensures any request bytes still streaming in for this connection are +// dropped rather than being re-parsed and re-triggering this same response. +- (void)failClient:(nw_connection_t)client withResponse:(RouteResponse *)response +{ + @synchronized (self.connectionBuffers) { + [self.connectionBuffers removeObjectForKey:client]; + [self.pendingRequestHeaders removeObjectForKey:client]; + } + [self applyDefaultHeadersToResponse:response]; + [self writeResponse:response toClient:client thenCloseConnection:YES]; +} + +- (void)respondBadRequestToClient:(nw_connection_t)client +{ + RouteResponse *badRequest = [RouteResponse new]; + id payload = FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"The request could not be parsed as valid HTTP" + traceback:nil]); + [payload dispatchWithResponse:badRequest]; + [self failClient:client withResponse:badRequest]; +} + +- (void)applyDefaultHeadersToResponse:(RouteResponse *)response +{ + [self.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { + [response setHeader:field value:value]; + }]; +} + +- (void)dispatchMethod:(NSString *)method pathAndQuery:(NSString *)pathAndQuery body:(NSData *)body client:(nw_connection_t)client +{ + NSURLComponents *requestTarget = [NSURLComponents componentsWithString:pathAndQuery]; + NSString *path = requestTarget.path ?: pathAndQuery; + + for (FBHTTPRoute *route in self.routes) { + if (![route.verb isEqualToString:method]) { + continue; + } + NSTextCheckingResult *result = [route.regex firstMatchInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length)]; + if (nil == result) { + continue; + } + + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + for (NSURLQueryItem *queryItem in requestTarget.queryItems) { + params[queryItem.name] = queryItem.value ?: @""; + } + if (route.keys.count > 0 && result.numberOfRanges == route.keys.count + 1) { + NSUInteger index = 1; + for (NSString *key in route.keys) { + params[key] = [path substringWithRange:[result rangeAtIndex:index]]; + index++; + } + } + + NSURL *url = [NSURL URLWithString:path] ?: [NSURL URLWithString:@"/"]; + RouteRequest *request = [[RouteRequest alloc] initWithURL:url params:params.copy body:body]; + RouteResponse *response = [RouteResponse new]; + [self applyDefaultHeadersToResponse:response]; + + NSString *sessionID = params[@"sessionID"]; + if (route.isStandalone) { + // DELETE triggers -abandonPendingRequestsForSessionID: itself; tracking its own request + // would make it abandon itself and write a response twice. + NSString *trackedSessionID = [route.verb isEqualToString:@"DELETE"] ? nil : sessionID; + [self dispatchStandaloneRoute:route request:request response:response client:client method:method pathAndQuery:pathAndQuery sessionID:trackedSessionID]; + return; + } + + FBPendingRequest *pendingRequest = nil; + if (nil != sessionID) { + pendingRequest = [[FBPendingRequest alloc] initWithClient:client]; + RouteResponse *abandonedResponse = [self trackPendingRequest:pendingRequest forSessionID:sessionID]; + if (nil != abandonedResponse) { + [self writeResponse:abandonedResponse toClient:client]; + return; + } + } + + void (^invoke)(void) = ^{ + route.block(request, response); + // Whoever untracks `pendingRequest` first "wins" and gets to respond - either this normal + // completion, or -abandonPendingRequestsForSessionID: on another thread. + BOOL shouldRespond = (nil == pendingRequest) || [self untrackPendingRequest:pendingRequest forSessionID:sessionID]; + if (shouldRespond) { + [self writeResponse:response toClient:client]; + } + }; + dispatch_queue_t routeQueue = self.routeQueue; + if (routeQueue) { + dispatch_async((dispatch_queue_t _Nonnull)routeQueue, invoke); + } else { + invoke(); + } + return; + } + + RouteResponse *notFound = [RouteResponse new]; + FBCommandStatus *status = [FBCommandStatus unknownCommandErrorWithMessage:nil + traceback:nil]; + [FBResponseWithStatus(status) dispatchWithResponse:notFound]; + [self applyDefaultHeadersToResponse:notFound]; + [self writeResponse:notFound toClient:client]; +} + +#pragma mark - Session-scoped request cancellation + +// Returns nil once `pendingRequest` is tracked, or - if the session was already abandoned - the +// response to deliver instead of dispatching, since no abandonment notification would ever reach +// this request. Shares a lock with -abandonPendingRequestsForSessionID: so nothing slips between +// the abandonment and the record of it. +- (nullable RouteResponse *)trackPendingRequest:(FBPendingRequest *)pendingRequest forSessionID:(NSString *)sessionID +{ + @synchronized (self.pendingSessionRequests) { + RouteResponse *abandonedResponse = self.abandonedSessionResponses[sessionID]; + if (nil != abandonedResponse) { + return abandonedResponse; + } + NSMutableSet *pendingRequests = self.pendingSessionRequests[sessionID]; + if (nil == pendingRequests) { + pendingRequests = [NSMutableSet set]; + self.pendingSessionRequests[sessionID] = pendingRequests; + } + [pendingRequests addObject:pendingRequest]; + return nil; + } +} + +// Returns YES if this caller won the race to respond, vs. -abandonPendingRequestsForSessionID: +// already having claimed `pendingRequest` on another thread. +- (BOOL)untrackPendingRequest:(FBPendingRequest *)pendingRequest forSessionID:(NSString *)sessionID +{ + @synchronized (self.pendingSessionRequests) { + NSMutableSet *pendingRequests = self.pendingSessionRequests[sessionID]; + BOOL wasPending = [pendingRequests containsObject:pendingRequest]; + if (wasPending) { + [pendingRequests removeObject:pendingRequest]; + if (0 == pendingRequests.count) { + [self.pendingSessionRequests removeObjectForKey:sessionID]; + } + } + return wasPending; + } +} + +- (void)abandonPendingRequestsForSessionID:(NSString *)sessionID withResponse:(RouteResponse *)response +{ + NSSet *pendingRequests; + @synchronized (self.pendingSessionRequests) { + pendingRequests = [self.pendingSessionRequests[sessionID] copy]; + [self.pendingSessionRequests removeObjectForKey:sessionID]; + // Recorded before the lock is dropped, so requests admitted from here on are rejected. + if (nil == self.abandonedSessionResponses[sessionID]) { + [self.abandonedSessionOrder addObject:sessionID]; + self.abandonedSessionResponses[sessionID] = response; + while (self.abandonedSessionOrder.count > FBMaxRecordedAbandonedSessions) { + [self.abandonedSessionResponses removeObjectForKey:self.abandonedSessionOrder.firstObject]; + [self.abandonedSessionOrder removeObjectAtIndex:0]; + } + } + } + for (FBPendingRequest *pendingRequest in pendingRequests) { + [self writeResponse:response toClient:pendingRequest.client]; + } +} + +#pragma mark - Standalone route dispatch + +- (void)dispatchStandaloneRoute:(FBHTTPRoute *)route + request:(RouteRequest *)request + response:(RouteResponse *)response + client:(nw_connection_t)client + method:(NSString *)method + pathAndQuery:(NSString *)pathAndQuery + sessionID:(nullable NSString *)sessionID +{ + // Includes the query string so requests with different params are never coalesced together. + NSString *key = [NSString stringWithFormat:@"%@ %@", method, pathAndQuery]; + FBPendingRequest *waiter = [[FBPendingRequest alloc] initWithClient:client]; + if (nil != sessionID) { + RouteResponse *abandonedResponse = [self trackPendingRequest:waiter forSessionID:sessionID]; + if (nil != abandonedResponse) { + [self writeResponse:abandonedResponse toClient:client]; + return; + } + } + + BOOL isInFlight = NO; + @synchronized (self.standaloneWaiters) { + NSMutableArray *waiters = self.standaloneWaiters[key]; + if (nil != waiters) { + [waiters addObject:waiter]; + isInFlight = YES; + } else { + self.standaloneWaiters[key] = [NSMutableArray array]; + } + } + if (isInFlight) { + // An identical request is already executing; it will deliver this connection's response too. + return; + } + + dispatch_queue_t queue = dispatch_queue_create(key.UTF8String, DISPATCH_QUEUE_SERIAL); + __weak typeof(self) weakSelf = self; + dispatch_async(queue, ^{ + route.block(request, response); + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } + NSArray *joinedWaiters; + @synchronized (strongSelf.standaloneWaiters) { + joinedWaiters = [strongSelf.standaloneWaiters[key] copy]; + [strongSelf.standaloneWaiters removeObjectForKey:key]; + } + for (FBPendingRequest *joinedWaiter in [@[waiter] arrayByAddingObjectsFromArray:joinedWaiters]) { + BOOL shouldRespond = (nil == sessionID) || [strongSelf untrackPendingRequest:joinedWaiter forSessionID:sessionID]; + if (shouldRespond) { + [strongSelf writeResponse:response toClient:joinedWaiter.client]; + } + } + }); +} + +- (void)writeResponse:(RouteResponse *)response toClient:(nw_connection_t)client +{ + [self writeResponse:response toClient:client thenCloseConnection:NO]; +} + +- (void)writeResponse:(RouteResponse *)response toClient:(nw_connection_t)client thenCloseConnection:(BOOL)shouldClose +{ + NSMutableData *payload = [NSMutableData data]; + NSString *statusLine = [NSString stringWithFormat:@"HTTP/1.1 %ld %@\r\n", + (long)response.statusCode, [self reasonPhraseForStatusCode:response.statusCode]]; + [payload appendData:FBUTF8Data(statusLine)]; + + NSData *body = response.responseData ?: [NSData data]; + NSMutableDictionary *headers = response.headers.mutableCopy; + if (nil == headers[@"Content-Length"]) { + headers[@"Content-Length"] = [NSString stringWithFormat:@"%lu", (unsigned long)body.length]; + } + [headers enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { + NSString *headerLine = [NSString stringWithFormat:@"%@: %@\r\n", field, value]; + [payload appendData:FBUTF8Data(headerLine)]; + }]; + [payload appendData:FBUTF8Data(@"\r\n")]; + [payload appendData:body]; + + if (shouldClose) { + __weak typeof(self) weakSelf = self; + [self.socket writeData:payload toClient:client completion:^(BOOL didSucceed) { + [weakSelf closeClient:client]; + }]; + } else { + // Unblocked from the send's completion, not before it: ordering is preserved either way + // (nw_connection_send is FIFO per connection), but unblocking early lets a client that + // pipelines without reading responses pile up rendered responses inside Network.framework. + __weak typeof(self) weakSelf = self; + [self.socket writeData:payload toClient:client completion:^(BOOL didSucceed) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } + if (!didSucceed) { + // The response never reached the peer, so running its next pipelined request - possibly + // a mutating one - would change device state for a client that can no longer be answered. + [FBLogger log:@"Failed to write a response; dropping the connection and its pending requests"]; + [strongSelf closeClient:client]; + return; + } + // Lifting the exemption and resuming parsing happen in one step on bufferProcessingQueue, + // the queue the reaper also runs on: doing it out here exposes the connection to a sweep + // queued ahead of the parse, which would judge a buffered request by the previous one's + // timestamp. + dispatch_async(strongSelf.bufferProcessingQueue, ^{ + __strong typeof(weakSelf) queuedSelf = weakSelf; + if (nil == queuedSelf) { + return; + } + @synchronized (queuedSelf.connectionBuffers) { + [queuedSelf.connectionsAwaitingResponse removeObject:client]; + // Mid-request connections get their window from when parsing could resume, not from the + // previous request. Absent entries stay absent, so idle keep-alives remain exempt. + if (nil != [queuedSelf.incompleteRequestStarts objectForKey:client]) { + [queuedSelf.incompleteRequestStarts setObject:[NSDate date] forKey:client]; + } + } + [queuedSelf processBufferForClient:client]; + }); + }]; + } +} + +- (void)closeClient:(nw_connection_t)client +{ + @synchronized (self.connectionBuffers) { + [self.connectionBuffers removeObjectForKey:client]; + [self.pendingRequestHeaders removeObjectForKey:client]; + [self.connectionsAwaitingResponse removeObject:client]; + [self.incompleteRequestStarts removeObjectForKey:client]; + } + nw_connection_cancel(client); +} + +- (NSString *)reasonPhraseForStatusCode:(HTTPStatusCode)statusCode +{ + // if/else, not switch, to avoid having to list all ~90 HTTPStatusCode cases for -Wswitch-enum. + if (kHTTPStatusCodeOK == statusCode) { + return @"OK"; + } else if (kHTTPStatusCodeBadRequest == statusCode) { + return @"Bad Request"; + } else if (kHTTPStatusCodeNotFound == statusCode) { + return @"Not Found"; + } else if (kHTTPStatusCodeMethodNotAllowed == statusCode) { + return @"Method Not Allowed"; + } else if (kHTTPStatusCodeRequestTimeout == statusCode) { + return @"Request Timeout"; + } else if (kHTTPStatusCodeRequestEntityTooLarge == statusCode) { + return @"Request Entity Too Large"; + } else if (kHTTPStatusCodeNotImplemented == statusCode) { + return @"Not Implemented"; + } else if (kHTTPStatusCodeInternalServerError == statusCode) { + return @"Internal Server Error"; + } + return @"Status"; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBRoute.h b/WebDriverAgentLib/Routing/FBRoute.h index 7982b7e707..35d55add21 100644 --- a/WebDriverAgentLib/Routing/FBRoute.h +++ b/WebDriverAgentLib/Routing/FBRoute.h @@ -27,9 +27,8 @@ typedef __nonnull id (^FBRouteSyncHandler)(FBRouteRequest *re /*! Route's path */ @property (nonatomic, copy, readonly) NSString *path; -/*! YES when the route is served directly on the HTTP connection's queue instead of the - automation (main) queue */ -@property (nonatomic, assign, readonly) BOOL usesControlQueue; +/*! Whether this route bypasses the shared route queue - see -standalone */ +@property (nonatomic, assign, readonly) BOOL isStandalone; /** Convenience constructor for GET route with given pathPattern @@ -72,12 +71,10 @@ typedef __nonnull id (^FBRouteSyncHandler)(FBRouteRequest *re - (instancetype)withoutSession; /** - Chain-able modifier that marks the route to be served on the HTTP connection's own queue, - bypassing the automation (main) queue. Only routes whose handlers never call XCUI or - testmanagerd APIs and only touch thread-safe state may opt in — such routes stay responsive - even while an automation request is blocked. + Chain-able constructor for a route that bypasses the shared route queue - see FBHTTPServer.h's + -handleMethod:withPath:standalone:block: for what that changes about how/when the handler runs. */ -- (instancetype)onControlQueue; +- (instancetype)standalone; /** Dispatches response for request diff --git a/WebDriverAgentLib/Routing/FBRoute.m b/WebDriverAgentLib/Routing/FBRoute.m index b401666496..47964014df 100644 --- a/WebDriverAgentLib/Routing/FBRoute.m +++ b/WebDriverAgentLib/Routing/FBRoute.m @@ -18,7 +18,7 @@ @interface FBRoute () @property (nonatomic, assign, readwrite) BOOL requiresSession; -@property (nonatomic, assign, readwrite) BOOL usesControlQueue; +@property (nonatomic, assign, readwrite) BOOL isStandalone; @property (nonatomic, copy, readwrite) NSString *verb; @property (nonatomic, copy, readwrite) NSString *path; @@ -127,9 +127,9 @@ - (instancetype)withoutSession return self; } -- (instancetype)onControlQueue +- (instancetype)standalone { - self.usesControlQueue = YES; + self.isStandalone = YES; return self; } @@ -137,7 +137,7 @@ - (instancetype)respondWithBlock:(FBRouteSyncHandler)handler { FBRoute_Sync *route = [FBRoute_Sync withVerb:self.verb path:self.path requiresSession:self.requiresSession]; route.handler = handler; - route.usesControlQueue = self.usesControlQueue; + route.isStandalone = self.isStandalone; return route; } @@ -146,7 +146,7 @@ - (instancetype)respondWithTarget:(id)target action:(SEL)action FBRoute_TargetAction *route = [FBRoute_TargetAction withVerb:self.verb path:self.path requiresSession:self.requiresSession]; route.target = target; route.action = action; - route.usesControlQueue = self.usesControlQueue; + route.isStandalone = self.isStandalone; return route; } diff --git a/WebDriverAgentLib/Routing/FBSession.h b/WebDriverAgentLib/Routing/FBSession.h index 61b1f87429..821891ea36 100644 --- a/WebDriverAgentLib/Routing/FBSession.h +++ b/WebDriverAgentLib/Routing/FBSession.h @@ -16,6 +16,12 @@ NS_ASSUME_NONNULL_BEGIN /** Bundle identifier of Mobile Safari browser */ extern NSString* const FB_SAFARI_BUNDLE_ID; +/** + Posted (synchronously, on whatever thread calls -kill) once a session has been torn down. The + notification's object is the FBSession instance that was killed - see -identifier. + */ +extern NSString* const FBSessionWasKilledNotification; + /** Class that represents testing session */ @@ -44,6 +50,13 @@ extern NSString* const FB_SAFARI_BUNDLE_ID; + (nullable instancetype)activeSession; +/** + Kills the active session, if any, and blocks until its teardown - including one already started + by a concurrent caller - is fully finished. Call this before preparing/launching a replacement + application, so it can't race a still-in-progress termination of the outgoing one. + */ ++ (void)killActiveSessionAndWaitForTeardown; + /** Fetches session for given identifier. If identifier doesn't match activeSession identifier, will return nil. diff --git a/WebDriverAgentLib/Routing/FBSession.m b/WebDriverAgentLib/Routing/FBSession.m index a57b1c0877..f7aa926ac6 100644 --- a/WebDriverAgentLib/Routing/FBSession.m +++ b/WebDriverAgentLib/Routing/FBSession.m @@ -34,12 +34,23 @@ NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari"; +// FBXCAXClientProxy's shared accessibility channel can be stuck servicing another request. +static const NSTimeInterval FB_IS_SYSTEM_APP_CHECK_TIMEOUT_SEC = 5.; +// -terminate hard-asserts off the main thread, which may itself be busy - see -fb_terminate...:. +static const NSTimeInterval FB_APP_TERMINATE_TIMEOUT_SEC = 5.; +// How long a -kill caller that lost the race below waits for the winner's teardown to finish. +static const NSTimeInterval FB_KILL_WAIT_TIMEOUT_SEC = 35.; +NSString *const FBSessionWasKilledNotification = @"FBSessionWasKilledNotification"; + @interface FBSession () @property (nullable, nonatomic) XCUIApplication *testedApplication; @property (nonatomic) BOOL isTestedApplicationExpectedToRun; @property (nonatomic) BOOL shouldAppsWaitForQuiescence; @property (nonatomic, nullable) FBAlertsMonitor *alertsMonitor; @property (nonatomic, readwrite) NSMutableDictionary *> *elementsVisibilityCache; + +- (BOOL)fb_isTestedApplicationSameAsSystemAppWithTimeout:(NSTimeInterval)timeout; +- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout generation:(NSUInteger)generation; @end @interface FBSession (FBAlertsMonitorDelegate) @@ -89,11 +100,44 @@ - (void)didDetectAlert:(FBAlert *)alert @implementation FBSession -// Control routes (e.g. /status) are served on their own connection queue and read this -// static concurrently with main-queue writes in markSessionActive:/kill. All reads and -// writes of _activeSession must go through the @synchronized (FBSession.class) accessors -// below. +// Standalone routes (e.g. /status) are served on their own queues and read this static +// concurrently with writes in markSessionActive:/kill. All reads and writes of +// _activeSession must go through @synchronized (FBSession.class). static FBSession *_activeSession = nil; +// Class-level, not per-instance: a caller that finds _activeSession already nil (a concurrent +// -kill beat it there) still needs to know whether that -kill's teardown is done, since it cleared +// the pointer before running it. See +waitForActiveTeardownWithTimeout:. +// A count, not a flag: the wait below is bounded, so teardowns can overlap. A shared flag would +// let the first to finish wake waiters while the other still ran, and the next session creation +// would then bump the generation and make that teardown skip its cleanup. +static NSUInteger _activeTeardownCount = 0; +// Bumped by +killActiveSessionAndWaitForTeardown, i.e. when a caller takes ownership of the +// device - before it launches anything. Since the wait there is bounded, a slow teardown can +// still be running by then; its remaining steps mutate process-wide state (the tested app, whose +// bundle ID the replacement usually shares, and the recording container), so each re-checks the +// generation it started with. Guarded by @synchronized (FBSession.class). +static NSUInteger _sessionGeneration = 0; + ++ (NSCondition *)teardownCondition +{ + static NSCondition *condition; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + condition = [NSCondition new]; + }); + return condition; +} + +// Waits (bounded) for every -kill teardown currently in progress to finish. ++ (void)waitForActiveTeardownWithTimeout:(NSTimeInterval)timeout +{ + NSCondition *condition = self.teardownCondition; + [condition lock]; + NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:timeout]; + while (_activeTeardownCount > 0 && [condition waitUntilDate:deadline]) { + } + [condition unlock]; +} + (instancetype)activeSession { @@ -102,20 +146,47 @@ + (instancetype)activeSession } } -+ (void)markSessionActive:(FBSession *)session ++ (void)killActiveSessionAndWaitForTeardown { - FBSession *previousSession; + FBSession *session; @synchronized (FBSession.class) { - previousSession = _activeSession; + session = _activeSession; } - if (previousSession) { - [previousSession kill]; + if (nil != session) { + // Runs the real teardown synchronously if this call wins the race in -kill, or waits for + // whoever did to finish if it lost - either way, blocks until torn down. + [session kill]; + } else { + // _activeSession is already nil, but a concurrent -kill (e.g. from DELETE /session) may still + // be mid-teardown - wait for it, so we don't launch a replacement app too early. + [self waitForActiveTeardownWithTimeout:FB_KILL_WAIT_TIMEOUT_SEC]; } + // Claimed here, not in +markSessionActive:, because the caller starts launching its application + // as soon as this returns. If the bounded wait expired with a teardown still running, that + // teardown has to be stale before the replacement app exists or it could terminate it. A + // teardown this call ran to completion is already finished, so invalidating it is a no-op. + @synchronized (FBSession.class) { + _sessionGeneration++; + } +} + ++ (void)markSessionActive:(FBSession *)session +{ + [self killActiveSessionAndWaitForTeardown]; @synchronized (FBSession.class) { _activeSession = session; } } +// NO once a newer session has been marked active, meaning the caller's teardown is stale and must +// not touch process-wide state that the newer session now owns. ++ (BOOL)isSessionGenerationCurrent:(NSUInteger)generation +{ + @synchronized (FBSession.class) { + return generation == _sessionGeneration; + } +} + + (instancetype)sessionWithIdentifier:(NSString *)identifier { if (!identifier) { @@ -182,38 +253,70 @@ - (BOOL)disableAlertsMonitor - (void)kill { + // DELETE /session and session creation can now run concurrently, so a session already + // superseded by a newer one can still reach here via a stale reference. Check-and-clear must be + // atomic, else a belated -kill could null out the new session's pointer instead of its own. + // _activeTeardownCount is published in the same critical section (with the condition lock + // held): were it set only after clearing the pointer, a concurrent + // +killActiveSessionAndWaitForTeardown could observe a nil session with no teardown to wait + // for and start a replacement whose app the still-running teardown then terminates. + NSCondition *teardownCondition = self.class.teardownCondition; BOOL wasActive; - @synchronized (FBSession.class) { - wasActive = (nil != _activeSession); + NSUInteger generation; + [teardownCondition lock]; + @synchronized (self.class) { + wasActive = (self == _activeSession); + // Captured here so the teardown steps below can tell whether a replacement session has been + // created in the meantime - see +isSessionGenerationCurrent:. + generation = _sessionGeneration; + if (wasActive) { + _activeSession = nil; + _activeTeardownCount++; + } } + [teardownCondition unlock]; if (!wasActive) { + // Someone else is already tearing this session down - wait for that to finish (bounded), so + // we don't act as if it's gone (e.g. launch a new app) while its -terminate is still in flight. + [self.class waitForActiveTeardownWithTimeout:FB_KILL_WAIT_TIMEOUT_SEC]; return; } - [self disableAlertsMonitor]; + @try { + // Posted before teardown so pending HTTP requests for this session can stop waiting sooner. + [NSNotificationCenter.defaultCenter postNotificationName:FBSessionWasKilledNotification object:self]; - FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise; - if (nil != activeScreenRecording) { - NSError *error; - if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:activeScreenRecording.identifier error:&error]) { - [FBLogger logFmt:@"%@", error]; - } - [FBScreenRecordingContainer.sharedInstance reset]; - } + [self disableAlertsMonitor]; - if (nil != self.testedApplication - && FBConfiguration.sharedInstance.shouldTerminateApp - && self.testedApplication.running - && ![self.testedApplication fb_isSameAppAs:XCUIApplication.fb_systemApplication]) { - @try { - [self.testedApplication terminate]; - } @catch (NSException *e) { - [FBLogger logFmt:@"%@", e.description]; + FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise; + if (nil != activeScreenRecording) { + NSError *error; + if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:activeScreenRecording.identifier error:&error]) { + [FBLogger logFmt:@"%@", error]; + } + // The stop above is by UUID and safe either way, but the container is process-wide: + // resetting it would drop a replacement session's promise instead. + if ([self.class isSessionGenerationCurrent:generation]) { + [FBScreenRecordingContainer.sharedInstance reset]; + } } - } - @synchronized (FBSession.class) { - _activeSession = nil; + if (nil != self.testedApplication + && FBConfiguration.sharedInstance.shouldTerminateApp + && self.testedApplication.running + && ![self fb_isTestedApplicationSameAsSystemAppWithTimeout:FB_IS_SYSTEM_APP_CHECK_TIMEOUT_SEC]) { + // Blocks until the app is either actually terminated or durably given up on (never left + // pending) - see -fb_terminateTestedApplicationWithTimeout:generation: - so it's safe to + // report this teardown as finished as soon as this returns. + [self fb_terminateTestedApplicationWithTimeout:FB_APP_TERMINATE_TIMEOUT_SEC generation:generation]; + } + } @finally { + [teardownCondition lock]; + _activeTeardownCount--; + // Broadcast unconditionally: waiters re-check the count themselves, so a wake-up while + // another teardown is still running simply puts them back to waiting. + [teardownCondition broadcast]; + [teardownCondition unlock]; } } @@ -309,4 +412,80 @@ - (XCUIApplication *)makeApplicationWithBundleId:(NSString *)bundleIdentifier : [[XCUIApplication alloc] initWithBundleIdentifier:bundleIdentifier]; } +// Has no async variant and can block on the shared accessibility channel. Run off-thread and give +// up after `timeout`, assuming the tested app IS the system app - safer, since it means skipping +// termination rather than risking terminating springboard. +- (BOOL)fb_isTestedApplicationSameAsSystemAppWithTimeout:(NSTimeInterval)timeout +{ + __block XCUIApplication *systemApp = nil; + __block NSException *caughtException = nil; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + // Undocumented private API; guard in case it hard-asserts off-main like -terminate does on + // some Xcode/iOS version - uncaught, that would crash the whole process. + @try { + systemApp = XCUIApplication.fb_systemApplication; + } @catch (NSException *e) { + caughtException = e; + } + dispatch_semaphore_signal(sem); + }); + int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC); + if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs)) || nil != caughtException) { + [FBLogger logFmt:@"Could not determine the system application within %@ seconds%@; assuming '%@' might be it and skipping its termination", @(timeout), nil == caughtException ? @"" : [NSString stringWithFormat:@" (%@)", caughtException.description], self.testedApplication.bundleID]; + return YES; + } + return [self.testedApplication fb_isSameAppAs:systemApp]; +} + +// -terminate hard-asserts off-main, but -kill can now run on a background queue. Dispatching to +// main and waiting indefinitely could hang just as long as main is stuck, so give up after +// `timeout` - but a "given up on" call must never still terminate whatever's running by the time +// main gets to it (e.g. a replacement session's app), so cancellation and the actual terminate +// call share a lock: whichever gets there first - the dispatched block, or the timeout - wins. +- (void)fb_terminateTestedApplicationWithTimeout:(NSTimeInterval)timeout generation:(NSUInteger)generation +{ + XCUIApplication *application = self.testedApplication; + if (NSThread.isMainThread) { + // Already on main (e.g. a session replacement via POST /session, whose handler runs on the + // main queue): dispatching to main and blocking on the semaphore below would deadlock until + // the timeout and then skip the termination entirely. Terminate inline instead. + if (![self.class isSessionGenerationCurrent:generation]) { + [FBLogger logFmt:@"Skipping termination of '%@': a newer session is already active", application.bundleID]; + return; + } + @try { + [application terminate]; + } @catch (NSException *e) { + [FBLogger logFmt:@"%@", e.description]; + } + return; + } + NSObject *lock = [NSObject new]; + __block BOOL isAllowedToTerminate = YES; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + dispatch_async(dispatch_get_main_queue(), ^{ + @synchronized (lock) { + // Re-checked here rather than before dispatching: this block can sit on a busy main queue + // longer than the teardown wait allows, and a replacement created in that window usually + // runs the same bundle ID - terminating "the old app" would kill the new one. + if (isAllowedToTerminate && [self.class isSessionGenerationCurrent:generation]) { + @try { + [application terminate]; + } @catch (NSException *e) { + [FBLogger logFmt:@"%@", e.description]; + } + } + } + dispatch_semaphore_signal(sem); + }); + int64_t timeoutNs = (int64_t)(timeout * NSEC_PER_SEC); + if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) { + @synchronized (lock) { + isAllowedToTerminate = NO; + } + [FBLogger logFmt:@"Could not terminate '%@' within %@ seconds; giving up on it rather than risk terminating a possible replacement session's app later", application.bundleID, @(timeout)]; + } +} + @end diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.h b/WebDriverAgentLib/Routing/FBTCPSocket.h index 37603ba9dc..f6bb1a8d38 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.h +++ b/WebDriverAgentLib/Routing/FBTCPSocket.h @@ -6,28 +6,18 @@ * LICENSE file in the root directory of this source tree. */ -// TARGET_OS_WATCH must be defined before the #if below runs, or (on some older Xcode/SDK -// toolchains) it silently evaluates as undefined/false here despite being true for the rest of -// the translation unit - which desyncs this file's declarations from FBTCPSocket.m's own -// #if TARGET_OS_WATCH branch. -#import - -#if TARGET_OS_WATCH @import Foundation; // A textual import, not `@import Network;` - older Xcode/watchOS SDK combinations (verified: // Xcode 15.4/watchOS 10.5) fail to expose nw_listener_t/nw_connection_t and friends through the // Network module map on watchOS, even though the underlying API has existed since watchOS 5.0. +// Kept unconditional (rather than gated to watchOS) since it also builds cleanly on iOS/tvOS. #import -#else -#import "GCDAsyncSocket.h" -#endif NS_ASSUME_NONNULL_BEGIN -#if TARGET_OS_WATCH - -// watchOS forbids BSD sockets, so this is backed by Network.framework instead of GCDAsyncSocket - -// hence a differently-shaped, push-style delegate protocol. +// Backed by Network.framework rather than BSD sockets on every platform, since watchOS forbids +// BSD sockets outright and there is no reason to keep a second, socket-based implementation +// around just for iOS/tvOS. @protocol FBTCPSocketDelegate /** @@ -54,43 +44,35 @@ NS_ASSUME_NONNULL_BEGIN @end -#else -@protocol FBTCPSocketDelegate +@interface FBTCPSocket : NSObject -/** - The callback which is fired on new TCP client connection +#if __has_feature(objc_arc_weak) +@property (nullable, nonatomic, weak) id delegate; +#else +@property (nullable, nonatomic, assign) id delegate; +#endif - @param newClient The newly connected socket +/** + The port this socket is listening on. Equal to the port passed to -initWithPort: unless that was + 0 ("let the system assign a port"), in which case this reflects the actually assigned port once + -startWithError: has returned successfully. */ -- (void)didClientConnect:(GCDAsyncSocket *)newClient; +@property (nonatomic, readonly) uint16_t port; /** - The callback which is fired when the TCP server receives a data from a connected client - - @param client The client, which sent the data -*/ -- (void)didClientSendData:(GCDAsyncSocket *)client; + The local IP address to bind the listener to, or nil to listen on all interfaces. Must be set + before -startWithError: is called. + */ +@property (nonatomic, copy, nullable) NSString *interface; /** - The callback which is fired when TCP client disconnects - - @param client The actual diconnected client + Whether to disable Nagle's algorithm (TCP_NODELAY) on accepted connections. Must be set before + -startWithError: is called, since Network.framework takes it from the listener's parameters + rather than per accepted connection. Defaults to NO; enable it for latency-sensitive streams + that push many small payloads, where Nagle would otherwise coalesce them. */ -- (void)didClientDisconnect:(GCDAsyncSocket *)client; - -@end - -#endif - - -@interface FBTCPSocket : NSObject - -#if __has_feature(objc_arc_weak) -@property (nullable, nonatomic, weak) id delegate; -#else -@property (nullable, nonatomic, assign) id delegate; -#endif +@property (nonatomic) BOOL noDelay; /** Creates TCP socket isntance which is going to be started on the specified port @@ -113,7 +95,6 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)stop; -#if TARGET_OS_WATCH /** Writes data to the given connected client @@ -129,10 +110,11 @@ NS_ASSUME_NONNULL_BEGIN @param data The data to send @param client The destination client - @param completion Called once the send attempt finishes + @param completion Called once the send attempt finishes. `didSucceed` is NO if the send failed + (e.g. the peer went away mid-write), in which case nothing was delivered and the caller + must not treat the connection as usable. */ -- (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(void))completion; -#endif +- (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(BOOL didSucceed))completion; @end diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.m b/WebDriverAgentLib/Routing/FBTCPSocket.m index ba4cdcc98a..9fbe72cd36 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.m +++ b/WebDriverAgentLib/Routing/FBTCPSocket.m @@ -8,13 +8,10 @@ #import "FBTCPSocket.h" -#if TARGET_OS_WATCH - @interface FBTCPSocket() @property (readonly, nonatomic) dispatch_queue_t socketQueue; @property (nullable, nonatomic) nw_listener_t listener; @property (readonly, nonatomic) NSMutableArray *connectedClients; -@property (readonly, nonatomic) uint16_t port; @end @@ -33,12 +30,31 @@ - (instancetype)initWithPort:(uint16_t)port - (BOOL)startWithError:(NSError **)error { + // TCP_NODELAY has to be requested here: Network.framework configures it through the listener's + // parameters, and an accepted nw_connection_t exposes no socket descriptor to set it on later. + nw_parameters_configure_protocol_block_t configureTCP = NW_PARAMETERS_DEFAULT_CONFIGURATION; + if (self.noDelay) { + configureTCP = ^(nw_protocol_options_t options) { + nw_tcp_options_set_no_delay(options, true); + }; + } nw_parameters_t parameters = nw_parameters_create_secure_tcp(NW_PARAMETERS_DISABLE_PROTOCOL, - NW_PARAMETERS_DEFAULT_CONFIGURATION); + configureTCP); NSString *portString = [NSString stringWithFormat:@"%u", (unsigned int)self.port]; // portString is always valid UTF8; -UTF8String is just declared nullable in general. const char * _Nonnull portCString = (const char * _Nonnull)portString.UTF8String; - nw_listener_t listener = nw_listener_create_with_port(portCString, parameters); + + nw_listener_t listener; + if (nil != self.interface) { + const char * _Nonnull interfaceCString = (const char * _Nonnull)((NSString * _Nonnull)self.interface).UTF8String; + nw_endpoint_t localEndpoint = nw_endpoint_create_host(interfaceCString, portCString); + nw_parameters_set_local_endpoint(parameters, localEndpoint); + // The port is already encoded in localEndpoint above - do not also pass it to + // nw_listener_create_with_port, which would be ambiguous. + listener = nw_listener_create(parameters); + } else { + listener = nw_listener_create_with_port(portCString, parameters); + } if (nil == listener) { if (error) { *error = [NSError errorWithDomain:@"FBTCPSocket" @@ -61,17 +77,27 @@ - (BOOL)startWithError:(NSError **)error // if/else, not switch: -Wswitch-enum, -Wswitch-default, and -Wcovered-switch-default can't // all be satisfied by one switch statement at once. if (nw_listener_state_ready == state) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf) { + strongSelf->_port = nw_listener_get_port(listener); + } dispatch_semaphore_signal(startupSemaphore); } else if (nw_listener_state_failed == state || nw_listener_state_cancelled == state) { // NSLocalizedDescriptionKey must be a string, not the underlying NSError itself, or // -[NSError localizedDescription] crashes trying to treat it as one. NSError *underlyingError = nwError ? (NSError *)CFBridgingRelease(nw_error_copy_cf_error(nwError)) : nil; - NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; - userInfo[NSLocalizedDescriptionKey] = underlyingError.localizedDescription ?: @"The TCP listener failed to start"; - if (underlyingError) { - userInfo[NSUnderlyingErrorKey] = underlyingError; + if ([underlyingError.domain isEqualToString:NSPOSIXErrorDomain]) { + // Surface POSIX errors (e.g. EADDRINUSE) directly, since callers like FBWebServer check + // for them by domain/code on the top-level error to decide whether to retry another port. + startupError = underlyingError; + } else { + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + userInfo[NSLocalizedDescriptionKey] = underlyingError.localizedDescription ?: @"The TCP listener failed to start"; + if (underlyingError) { + userInfo[NSUnderlyingErrorKey] = underlyingError; + } + startupError = [NSError errorWithDomain:@"FBTCPSocket" code:2 userInfo:userInfo]; } - startupError = [NSError errorWithDomain:@"FBTCPSocket" code:2 userInfo:userInfo]; dispatch_semaphore_signal(startupSemaphore); } }); @@ -86,6 +112,10 @@ - (BOOL)startWithError:(NSError **)error if (error) { *error = startupError; } + // Cancel rather than just dropping our reference - otherwise a late ready/failed callback + // can still fire and the port stays bound at the OS level even though the caller was told + // startup failed. + nw_listener_cancel(listener); self.listener = nil; return NO; } @@ -113,7 +143,8 @@ - (void)acceptConnection:(nw_connection_t)connection } [strongSelf scheduleReceiveForConnection:connection]; } else if (nw_connection_state_failed == state || nw_connection_state_cancelled == state) { - [weakSelf handleDisconnectForConnection:connection]; + __strong typeof(weakSelf) strongSelf = weakSelf; + [strongSelf handleDisconnectForConnection:connection]; } }); nw_connection_start(connection); @@ -132,11 +163,9 @@ - (void)scheduleReceiveForConnection:(nw_connection_t)connection } if (nil != content) { dispatch_data_t nonnullContent = (dispatch_data_t _Nonnull)content; - __block NSData *data = nil; + NSMutableData *data = [NSMutableData data]; dispatch_data_apply(nonnullContent, ^bool(dispatch_data_t _Nonnull region, size_t offset, const void * _Nonnull buffer, size_t size) { - NSMutableData *accumulated = [(data ?: [NSData data]) mutableCopy]; - [accumulated appendBytes:buffer length:size]; - data = accumulated.copy; + [data appendBytes:buffer length:size]; return true; }); if (data.length > 0) { @@ -174,25 +203,34 @@ - (void)writeData:(NSData *)data toClient:(nw_connection_t)client [self writeData:data toClient:client completion:nil]; } -- (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(void))completion +- (void)writeData:(NSData *)data toClient:(nw_connection_t)client completion:(nullable void (^)(BOOL didSucceed))completion { dispatch_data_t dispatchData = dispatch_data_create(data.bytes, data.length, self.socketQueue, DISPATCH_DATA_DESTRUCTOR_DEFAULT); nw_connection_send(client, dispatchData, NW_CONNECTION_DEFAULT_STREAM_CONTEXT, false, ^(nw_error_t _Nullable sendError) { if (completion) { - completion(); + // The send error must reach the caller: a failed write means the response never reached + // the peer, and treating that as success would e.g. let the next pipelined request run + // against a connection that can no longer answer it. + completion(nil == sendError); } }); } - (void)stop { + NSArray *clients; @synchronized (self.connectedClients) { - NSArray *clients = self.connectedClients.copy; + clients = self.connectedClients.copy; [self.connectedClients removeAllObjects]; + } + // Cancel on socketQueue, the same queue every connection's send/receive is bound to (see + // -acceptConnection:), so a write already issued just before -stop (e.g. a shutdown route's + // response) is processed before the cancellation rather than racing it. + dispatch_async(self.socketQueue, ^{ for (nw_connection_t client in clients) { nw_connection_cancel(client); } - } + }); self.delegate = nil; nw_listener_t listener = self.listener; @@ -203,94 +241,3 @@ - (void)stop } @end - -#else - -@interface FBTCPSocket() -@property (readonly, nonatomic) dispatch_queue_t socketQueue; -@property (readonly, nonatomic) GCDAsyncSocket *listeningSocket; -@property (readonly, nonatomic) NSMutableArray *connectedClients; -@property (readonly, nonatomic) uint16_t port; -@end - - -@interface FBTCPSocket(AsyncSocket) - -@end - - -@implementation FBTCPSocket - -- (instancetype)initWithPort:(uint16_t)port -{ - if ((self = [super init])) { - _socketQueue = dispatch_queue_create("socketQueue", NULL); - _listeningSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:_socketQueue]; - _connectedClients = [[NSMutableArray alloc] initWithCapacity:1]; - _port = port; - _delegate = nil; - } - return self; -} - -- (BOOL)startWithError:(NSError **)error -{ - if (![self.listeningSocket acceptOnPort:self.port error:error]) { - return NO; - } - - return YES; -} - -- (void)stop -{ - @synchronized(self.connectedClients) { - NSArray *clients = self.connectedClients.copy; - [self.connectedClients removeAllObjects]; - for (GCDAsyncSocket *client in clients) { - [client disconnect]; - } - } - - self.delegate = nil; - [self.listeningSocket disconnect]; -} - -@end - - -@implementation FBTCPSocket(AsyncSocket) - -- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket -{ - @synchronized(self.connectedClients) { - [self.connectedClients addObject:newSocket]; - } - id delegate = self.delegate; - if (nil != delegate) { - [delegate didClientConnect:newSocket]; - } -} - -- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag -{ - id delegate = self.delegate; - if (nil != delegate) { - [delegate didClientSendData:sock]; - } -} - -- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err -{ - @synchronized(self.connectedClients) { - [self.connectedClients removeObject:sock]; - } - id delegate = self.delegate; - if (nil != delegate) { - [delegate didClientDisconnect:sock]; - } -} - -@end - -#endif diff --git a/WebDriverAgentLib/Routing/FBWebServer.h b/WebDriverAgentLib/Routing/FBWebServer.h index 7ab0b8809d..ed590fd203 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.h +++ b/WebDriverAgentLib/Routing/FBWebServer.h @@ -8,7 +8,7 @@ #import -@class RouteResponse, RoutingHTTPServer, FBExceptionHandler; +@class RouteResponse, FBExceptionHandler; @protocol FBWebServerDelegate; NS_ASSUME_NONNULL_BEGIN diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m index 738637f18c..cf6cf6c1ea 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -8,20 +8,19 @@ #import "FBWebServer.h" -#if TARGET_OS_WATCH -#import "FBWatchHTTPServer.h" -#else -#import "RoutingConnection.h" -#import "RoutingHTTPServer.h" -#import "FBBroadcastManager.h" +#import "FBHTTPServer.h" #import "FBMjpegServer.h" #import "FBTCPSocket.h" +#if !TARGET_OS_WATCH +#import "FBBroadcastManager.h" #import "FBVideoStreamManager.h" #endif #import "FBCommandHandler.h" +#import "FBCommandStatus.h" #import "FBErrorBuilder.h" #import "FBExceptionHandler.h" +#import "FBResponsePayload.h" #import "FBRouteRequest.h" #import "FBRuntimeUtils.h" #import "FBSession.h" @@ -36,36 +35,11 @@ static NSString *const FBServerURLBeginMarker = @"ServerURLHere->"; static NSString *const FBServerURLEndMarker = @"<-ServerURLHere"; -#if !TARGET_OS_WATCH -@interface FBHTTPConnection : RoutingConnection -@end - -@implementation FBHTTPConnection - -- (void)handleResourceNotFound -{ - [FBLogger logFmt:@"Received request for %@ which we do not handle", self.requestURI]; - [super handleResourceNotFound]; -} - -- (UInt64)maxRequestBodySize -{ - return FBConfiguration.sharedInstance.httpRequestBodySizeLimit; -} - -@end -#endif - - @interface FBWebServer () @property (nonatomic, strong) FBExceptionHandler *exceptionHandler; -#if TARGET_OS_WATCH -@property (nonatomic, strong) FBWatchHTTPServer *server; -#else -@property (nonatomic, strong) RoutingHTTPServer *server; +@property (nonatomic, strong) FBHTTPServer *server; @property (nonatomic, nullable) FBTCPSocket *screenshotsBroadcaster; @property (nonatomic, nullable, strong) FBMjpegServer *mjpegServer; -#endif @property (atomic, assign) BOOL keepAlive; // Serializes automation requests onto a single funnel so at most one is ever in flight on // the main queue. See registerRouteHandlers: for why this is necessary. @@ -76,9 +50,7 @@ @implementation FBWebServer - (void)dealloc { -#if !TARGET_OS_WATCH [self stopScreenshotsBroadcaster]; -#endif } + (NSArray> *)collectCommandHandlerClasses @@ -108,14 +80,14 @@ - (void)startServing if (![self startHTTPServer]) { return; } -#if !TARGET_OS_WATCH [self initScreenshotsBroadcaster]; +#if !TARGET_OS_WATCH // Listen permanently so broadcasts started from Control Center attach as well. [FBBroadcastManager.sharedInstance startListening]; #endif self.keepAlive = YES; - // /status is served off the main queue (it uses onControlQueue), but FBSDKVersion() and + // /status is served off the main queue (it is a standalone route), but FBSDKVersion() and // FBTestmanagerdVersion() cache their result behind a dispatch_once. Burn both once-tokens // here, on the main thread, warmed only after the server has bound: FBTestmanagerdVersion()'s // legacy branch waits (with a bounded timeout) on the daemon, and a degraded daemon must not @@ -148,34 +120,29 @@ - (void)startServing - (BOOL)startHTTPServer { -#if TARGET_OS_WATCH - self.server = [[FBWatchHTTPServer alloc] init]; -#else - self.server = [[RoutingHTTPServer alloc] init]; -#endif -#if TARGET_OS_WATCH - [self.server setRouteQueue:dispatch_get_main_queue()]; -#endif + self.server = [[FBHTTPServer alloc] init]; + // Serializes automation requests so at most one is ever in flight on the main queue; handlers + // are invoked here and hop to main via dispatch_sync. See registerRouteHandlers:. + self.automationQueue = dispatch_queue_create("com.facebook.WebDriverAgent.automation-funnel", DISPATCH_QUEUE_SERIAL); + [self.server setRouteQueue:self.automationQueue]; [self.server setDefaultHeader:@"Server" value:@"WebDriverAgent/1.0"]; [self.server setDefaultHeader:@"Access-Control-Allow-Origin" value:@"*"]; [self.server setDefaultHeader:@"Access-Control-Allow-Headers" value:@"Content-Type, X-Requested-With"]; -#if !TARGET_OS_WATCH - [self.server setConnectionClass:[FBHTTPConnection self]]; -#endif - self.automationQueue = dispatch_queue_create("com.facebook.WebDriverAgent.automation-funnel", DISPATCH_QUEUE_SERIAL); + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(sessionWasKilled:) + name:FBSessionWasKilledNotification + object:nil]; [self registerRouteHandlers:[self.class collectCommandHandlerClasses]]; [self registerServerKeyRouteHandlers]; NSRange serverPortRange = FBConfiguration.sharedInstance.bindingPortRange; NSString *bindingIP = FBConfiguration.sharedInstance.bindingIPAddress; -#if !TARGET_OS_WATCH if (bindingIP != nil) { [self.server setInterface:bindingIP]; [FBLogger logFmt:@"Using custom binding IP address: %@", bindingIP]; } -#endif NSError *error; BOOL serverStarted = NO; @@ -207,13 +174,13 @@ - (BOOL)startHTTPServer return YES; } -#if !TARGET_OS_WATCH - (void)initScreenshotsBroadcaster { [self readMjpegSettingsFromEnv]; self.mjpegServer = [[FBMjpegServer alloc] init]; self.screenshotsBroadcaster = [[FBTCPSocket alloc] initWithPort:(uint16_t)FBConfiguration.sharedInstance.mjpegServerPort]; + self.mjpegServer.socket = self.screenshotsBroadcaster; self.screenshotsBroadcaster.delegate = self.mjpegServer; NSError *error; if (![self.screenshotsBroadcaster startWithError:&error]) { @@ -253,16 +220,33 @@ - (void)readMjpegSettingsFromEnv FBConfiguration.sharedInstance.mjpegServerScreenshotQuality = [screenshotQuality integerValue]; } } -#endif + +- (void)sessionWasKilled:(NSNotification *)notification +{ + FBSession *session = notification.object; + if (![session isKindOfClass:FBSession.class]) { + return; + } + // Same "invalid session id" shape a still-queued request would eventually get anyway, once + // -routeQueue drains and FBRoute.decorateRequest: finds the session gone - just delivered now + // instead of after however long the request would otherwise have been stuck waiting. + NSString *message = [NSString stringWithFormat:@"Session %@ was deleted while this request was still pending", session.identifier]; + id payload = FBResponseWithStatus([FBCommandStatus noSuchDriverErrorWithMessage:message + traceback:nil]); + RouteResponse *response = [RouteResponse new]; + [payload dispatchWithResponse:response]; + [self.server abandonPendingRequestsForSessionID:session.identifier withResponse:response]; +} - (void)stopServing { + [NSNotificationCenter.defaultCenter removeObserver:self name:FBSessionWasKilledNotification object:nil]; [FBSession.activeSession kill]; #if !TARGET_OS_WATCH [FBVideoStreamManager.sharedInstance stopAllSessions]; [FBBroadcastManager.sharedInstance stopListening]; - [self stopScreenshotsBroadcaster]; #endif + [self stopScreenshotsBroadcaster]; if (self.server.isRunning) { [self.server stop:NO]; } @@ -271,11 +255,7 @@ - (void)stopServing self.keepAlive = NO; } -#if TARGET_OS_WATCH -- (BOOL)attemptToStartServer:(FBWatchHTTPServer *)server onPort:(NSInteger)port withError:(NSError **)error -#else -- (BOOL)attemptToStartServer:(RoutingHTTPServer *)server onPort:(NSInteger)port withError:(NSError **)error -#endif +- (BOOL)attemptToStartServer:(FBHTTPServer *)server onPort:(NSInteger)port withError:(NSError **)error { server.port = (UInt16)port; NSError *innerError = nil; @@ -304,7 +284,7 @@ - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses for (Class commandHandler in commandHandlerClasses) { NSArray *routes = [commandHandler routes]; for (FBRoute *route in routes) { - [self.server handleMethod:route.verb withPath:route.path block:^(RouteRequest *request, RouteResponse *response) { + [self.server handleMethod:route.verb withPath:route.path standalone:route.isStandalone block:^(RouteRequest *request, RouteResponse *response) { __strong typeof(weakSelf) strongSelf = weakSelf; if (nil == strongSelf) { return; @@ -318,26 +298,24 @@ - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses [FBLogger verboseLog:routeParams.description]; -#if TARGET_OS_WATCH - [strongSelf mountRoute:route request:routeParams intoResponse:response]; -#else - if (route.usesControlQueue) { - // Served on this connection's own queue so it stays responsive while the automation - // queue is busy or blocked. Only routes that never touch XCUI state opt in. + if (route.isStandalone) { + // Standalone handlers are invoked by FBHTTPServer on their own queues so they stay + // responsive while the main queue is busy or blocked. Only routes that never touch + // XCUI state opt in. [strongSelf mountRoute:route request:routeParams intoResponse:response]; } else { - // Serialize automation requests: while one is on the main queue (possibly spinning the - // run loop), the next waits here instead of being enqueued to main, where a nested run - // loop drain would otherwise execute it reentrantly inside the first handler. - dispatch_sync(strongSelf.automationQueue, ^{ - dispatch_sync(dispatch_get_main_queue(), ^{ - @autoreleasepool { - [strongSelf mountRoute:route request:routeParams intoResponse:response]; - } - }); + // Invoked on the automation funnel (the server's routeQueue). Hopping to main from + // there - instead of using the main queue as the routeQueue directly - serializes + // automation requests: while one is on the main queue (possibly spinning the run + // loop), the next waits on the funnel instead of being enqueued to main, where a + // nested run loop drain would otherwise execute it reentrantly inside the first + // handler. + dispatch_sync(dispatch_get_main_queue(), ^{ + @autoreleasepool { + [strongSelf mountRoute:route request:routeParams intoResponse:response]; + } }); } -#endif }]; } } @@ -360,29 +338,36 @@ - (void)handleException:(NSException *)exception forResponse:(RouteResponse *)re - (void)registerServerKeyRouteHandlers { - [self.server get:@"/health" withBlock:^(RouteRequest *request, RouteResponse *response) { + // Standalone, i.e. off -routeQueue: these must stay answerable while the funnel is wedged - + // /health as a liveness signal, /wda/shutdown as the way out. (/mobilerun/state is deliberately + // the opposite; see docs/request-dispatch.md.) + [self.server handleMethod:@"GET" withPath:@"/health" standalone:YES block:^(RouteRequest *request, RouteResponse *response) { [response respondWithString:@"Health Check

I-AM-ALIVE

"]; }]; + // Deprecated: no longer needed since appium-xcuitest-driver handles calibration + // itself (https://github.com/appium/appium-xcuitest-driver/pull/2948). Kept for + // backward compatibility; will be removed in a future major release. NSString *calibrationPage = @"" "{\"x\":null,\"y\":null}" "
" "" "
" ""; - [self.server get:@"/calibrate" withBlock:^(RouteRequest *request, RouteResponse *response) { + [self.server handleMethod:@"GET" withPath:@"/calibrate" standalone:YES block:^(RouteRequest *request, RouteResponse *response) { + [FBLogger logFmt:@"The /calibrate endpoint is deprecated and will be removed in a future release"]; [response respondWithString:calibrationPage]; }]; __weak typeof(self) weakSelf = self; - [self.server get:@"/wda/shutdown" withBlock:^(RouteRequest *request, RouteResponse *response) { + [self.server handleMethod:@"GET" withPath:@"/wda/shutdown" standalone:YES block:^(RouteRequest *request, RouteResponse *response) { __strong typeof(weakSelf) strongSelf = weakSelf; if (nil == strongSelf) { return; } [response respondWithString:@"Shutting down"]; - // The delegate tears down automation state; run it on the main queue without blocking - // this connection's queue. + // Deferred so the "Shutting down" response is written to the client before + // webServerDidRequestShutdown: tears down the server's socket out from under it. dispatch_async(dispatch_get_main_queue(), ^{ [strongSelf.delegate webServerDidRequestShutdown:strongSelf]; }); diff --git a/WebDriverAgentLib/Routing/WatchOS/RouteRequest.h b/WebDriverAgentLib/Routing/RouteRequest.h similarity index 68% rename from WebDriverAgentLib/Routing/WatchOS/RouteRequest.h rename to WebDriverAgentLib/Routing/RouteRequest.h index d81830eda8..1a5c1b458c 100644 --- a/WebDriverAgentLib/Routing/WatchOS/RouteRequest.h +++ b/WebDriverAgentLib/Routing/RouteRequest.h @@ -6,10 +6,8 @@ * LICENSE file in the root directory of this source tree. */ -// Minimal, watchOS-only stand-in for Vendor/RoutingHTTPServer/RouteRequest.h, which is not -// available on watchOS because RoutingHTTPServer/CocoaHTTPServer/CocoaAsyncSocket cannot be -// built there (see FBWatchHTTPServer.h). Exposes just the surface FBWebServer's route blocks -// and FBRoute.decorateRequest: read. +// A minimal request value type, exposing just the surface FBWebServer's route blocks and +// FBRoute.decorateRequest: read. @import Foundation; diff --git a/WebDriverAgentLib/Routing/WatchOS/RouteRequest.m b/WebDriverAgentLib/Routing/RouteRequest.m similarity index 100% rename from WebDriverAgentLib/Routing/WatchOS/RouteRequest.m rename to WebDriverAgentLib/Routing/RouteRequest.m diff --git a/WebDriverAgentLib/Routing/WatchOS/RouteResponse.h b/WebDriverAgentLib/Routing/RouteResponse.h similarity index 82% rename from WebDriverAgentLib/Routing/WatchOS/RouteResponse.h rename to WebDriverAgentLib/Routing/RouteResponse.h index c0af255aba..da71e57d7a 100644 --- a/WebDriverAgentLib/Routing/WatchOS/RouteResponse.h +++ b/WebDriverAgentLib/Routing/RouteResponse.h @@ -6,8 +6,8 @@ * LICENSE file in the root directory of this source tree. */ -// Minimal, watchOS-only stand-in for Vendor/RoutingHTTPServer/RouteResponse.h, reproducing just -// what FBRoute.m/FBResponseJSONPayload.m call on it. See FBWatchHTTPServer.h. +// A minimal response value type, exposing just the surface FBRoute.m/FBResponseJSONPayload.m +// call on it. @import Foundation; #import diff --git a/WebDriverAgentLib/Routing/WatchOS/RouteResponse.m b/WebDriverAgentLib/Routing/RouteResponse.m similarity index 92% rename from WebDriverAgentLib/Routing/WatchOS/RouteResponse.m rename to WebDriverAgentLib/Routing/RouteResponse.m index 393ccab05c..eb383724bb 100644 --- a/WebDriverAgentLib/Routing/WatchOS/RouteResponse.m +++ b/WebDriverAgentLib/Routing/RouteResponse.m @@ -9,7 +9,7 @@ #import "RouteResponse.h" @interface RouteResponse () -@property (nonatomic, copy) NSMutableDictionary *mutableHeaders; +@property (nonatomic, strong) NSMutableDictionary *mutableHeaders; @end @implementation RouteResponse diff --git a/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.h b/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.h deleted file mode 100644 index a570642c2f..0000000000 --- a/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.h +++ /dev/null @@ -1,68 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -// RoutingHTTPServer/CocoaHTTPServer need BSD sockets (via GCDAsyncSocket), which watchOS -// forbids - see FBTCPSocket.h/.m. This is a minimal HTTP/1.1 server on top of the watchOS -// FBTCPSocket, mirroring just enough of RoutingHTTPServer's API for FBWebServer.m to swap -// servers with a single #if TARGET_OS_WATCH. -// -// No chunked encoding, range requests, or pipelining - just request line + headers + -// Content-Length body, and ":param" path matching like RoutingHTTPServer.m. - -@import Foundation; - -#import "RouteRequest.h" -#import "RouteResponse.h" - -NS_ASSUME_NONNULL_BEGIN - -@interface FBWatchHTTPServer : NSObject - -/*! The port the server is (or will be) listening on */ -@property (nonatomic) uint16_t port; - -/*! Whether the server is currently listening for connections */ -@property (nonatomic, readonly) BOOL isRunning; - -/** - Sets the dispatch queue on which route blocks are invoked. Pass NULL to invoke them - synchronously on the socket's own queue. - */ -- (void)setRouteQueue:(nullable dispatch_queue_t)queue; - -/** - Sets a header which is added to every response, unless overridden by the route itself. - */ -- (void)setDefaultHeader:(NSString *)field value:(NSString *)value; - -/** - Registers a route handler for the given HTTP method and path pattern (":param" segments are - captured into the request's `params`, matching RoutingHTTPServer's convention). - */ -- (void)handleMethod:(NSString *)method - withPath:(NSString *)path - block:(void (^)(RouteRequest *request, RouteResponse *response))block; - -/** - Convenience for -handleMethod:@"GET" withPath:path block:block. - */ -- (void)get:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block; - -/** - Starts listening on `port`. - */ -- (BOOL)start:(NSError **)error; - -/** - Stops listening and disconnects all clients. - */ -- (void)stop:(BOOL)immediately; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.m b/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.m deleted file mode 100644 index b5b9eed85c..0000000000 --- a/WebDriverAgentLib/Routing/WatchOS/FBWatchHTTPServer.m +++ /dev/null @@ -1,376 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -#import "FBWatchHTTPServer.h" - -#import "FBConfiguration.h" -#import "FBTCPSocket.h" - -static NSData *FBCRLFCRLFData(void) -{ - static NSData *data; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - data = [@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]; - }); - return data; -} - -// -dataUsingEncoding:NSUTF8StringEncoding never actually returns nil; this just keeps the cast -// out of every call site below. -static NSData * _Nonnull FBUTF8Data(NSString *string) -{ - return (NSData * _Nonnull)[string dataUsingEncoding:NSUTF8StringEncoding]; -} - -@interface FBWatchHTTPRoute : NSObject -@property (nonatomic, copy) NSString *verb; -@property (nonatomic, strong) NSRegularExpression *regex; -@property (nonatomic, copy, nullable) NSArray *keys; -@property (nonatomic, copy) void (^block)(RouteRequest *request, RouteResponse *response); -@end - -@implementation FBWatchHTTPRoute -@end - - -@interface FBWatchHTTPServer () - -@property (nonatomic, nullable, strong) FBTCPSocket *socket; -@property (nonatomic, strong) NSMutableArray *routes; -@property (nonatomic, strong) NSMutableDictionary *defaultHeaders; -@property (nonatomic, nullable) dispatch_queue_t routeQueue; -// nw_connection_t isn't NSCopying, so it can't be an NSDictionary key - use NSMapTable instead. -@property (nonatomic, strong) NSMapTable *connectionBuffers; - -@end - -@implementation FBWatchHTTPServer - -- (instancetype)init -{ - if ((self = [super init])) { - _routes = [NSMutableArray array]; - _defaultHeaders = [NSMutableDictionary dictionary]; - _connectionBuffers = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory) - valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory]; - } - return self; -} - -- (void)setRouteQueue:(nullable dispatch_queue_t)queue -{ - _routeQueue = queue; -} - -- (void)setDefaultHeader:(NSString *)field value:(NSString *)value -{ - self.defaultHeaders[field] = value; -} - -#pragma mark - Route registration - -- (FBWatchHTTPRoute *)compiledRouteWithPath:(NSString *)path -{ - FBWatchHTTPRoute *route = [FBWatchHTTPRoute new]; - NSMutableArray *keys = [NSMutableArray array]; - - // Escape regex-significant characters before substituting :param placeholders, like - // RoutingHTTPServer.m does. - NSRegularExpression *escapeRegex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" - options:(NSRegularExpressionOptions)0 - error:nil]; - NSString *escapedPath = [escapeRegex stringByReplacingMatchesInString:path - options:(NSMatchingOptions)0 - range:NSMakeRange(0, path.length) - withTemplate:@"\\\\$0"]; - - NSRegularExpression *paramRegex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)" - options:(NSRegularExpressionOptions)0 - error:nil]; - NSMutableString *regexPath = [NSMutableString stringWithString:escapedPath]; - __block NSInteger diff = 0; - [paramRegex enumerateMatchesInString:escapedPath - options:(NSMatchingOptions)0 - range:NSMakeRange(0, escapedPath.length) - usingBlock:^(NSTextCheckingResult * _Nullable result, NSMatchingFlags flags, BOOL * _Nonnull stop) { - NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length); - NSString *capturedString = [escapedPath substringWithRange:result.range]; - NSString *replacementString; - if ([capturedString isEqualToString:@"*"]) { - [keys addObject:@"wildcards"]; - replacementString = @"(.*?)"; - } else { - NSString *keyString = [escapedPath substringWithRange:[result rangeAtIndex:2]]; - [keys addObject:keyString]; - replacementString = @"([^/]+)"; - } - [regexPath replaceCharactersInRange:replacementRange withString:replacementString]; - diff += replacementString.length - result.range.length; - }]; - - NSString *anchoredPattern = [NSString stringWithFormat:@"^%@$", regexPath]; - route.regex = [NSRegularExpression regularExpressionWithPattern:anchoredPattern - options:NSRegularExpressionCaseInsensitive - error:nil]; - route.keys = keys.count > 0 ? keys.copy : nil; - return route; -} - -- (void)handleMethod:(NSString *)method - withPath:(NSString *)path - block:(void (^)(RouteRequest *request, RouteResponse *response))block -{ - FBWatchHTTPRoute *route = [self compiledRouteWithPath:path]; - route.verb = method.uppercaseString; - route.block = block; - [self.routes addObject:route]; -} - -- (void)get:(NSString *)path withBlock:(void (^)(RouteRequest *request, RouteResponse *response))block -{ - [self handleMethod:@"GET" withPath:path block:block]; -} - -#pragma mark - Lifecycle - -- (BOOL)start:(NSError **)error -{ - FBTCPSocket *socket = [[FBTCPSocket alloc] initWithPort:self.port]; - socket.delegate = self; - if (![socket startWithError:error]) { - return NO; - } - self.socket = socket; - _isRunning = YES; - return YES; -} - -- (void)stop:(BOOL)immediately -{ - [self.socket stop]; - self.socket = nil; - @synchronized (self.connectionBuffers) { - [self.connectionBuffers removeAllObjects]; - } - _isRunning = NO; -} - -#pragma mark - FBTCPSocketDelegate - -- (void)didClientConnect:(nw_connection_t)newClient -{ - @synchronized (self.connectionBuffers) { - [self.connectionBuffers setObject:[NSMutableData data] forKey:newClient]; - } -} - -- (void)didClientDisconnect:(nw_connection_t)client -{ - @synchronized (self.connectionBuffers) { - [self.connectionBuffers removeObjectForKey:client]; - } -} - -- (void)client:(nw_connection_t)client didReceiveData:(NSData *)data -{ - NSMutableData *buffer; - @synchronized (self.connectionBuffers) { - buffer = [self.connectionBuffers objectForKey:client]; - if (nil == buffer) { - return; - } - [buffer appendData:data]; - } - [self processBufferForClient:client]; -} - -#pragma mark - HTTP parsing - -- (void)processBufferForClient:(nw_connection_t)client -{ - while (YES) { - NSMutableData *buffer; - @synchronized (self.connectionBuffers) { - buffer = [self.connectionBuffers objectForKey:client]; - } - if (nil == buffer) { - return; - } - - NSRange headerEndRange = [buffer rangeOfData:FBCRLFCRLFData() options:(NSDataSearchOptions)0 range:NSMakeRange(0, buffer.length)]; - if (NSNotFound == headerEndRange.location) { - return; - } - - NSData *headerData = [buffer subdataWithRange:NSMakeRange(0, headerEndRange.location)]; - NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSUTF8StringEncoding]; - NSArray *lines = [headerString componentsSeparatedByString:@"\r\n"]; - if (lines.count < 1) { - [self closeClient:client]; - return; - } - - NSArray *requestLineParts = [lines.firstObject componentsSeparatedByString:@" "]; - if (requestLineParts.count < 2) { - [self closeClient:client]; - return; - } - NSString *method = requestLineParts[0].uppercaseString; - NSString *pathAndQuery = requestLineParts[1]; - - NSMutableDictionary *requestHeaders = [NSMutableDictionary dictionary]; - for (NSUInteger i = 1; i < lines.count; i++) { - NSString *line = lines[i]; - NSRange colonRange = [line rangeOfString:@":"]; - if (NSNotFound == colonRange.location) { - continue; - } - NSString *name = [line substringToIndex:colonRange.location]; - NSString *value = [[line substringFromIndex:colonRange.location + 1] - stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; - requestHeaders[name.lowercaseString] = value; - } - - NSUInteger contentLength = (NSUInteger)requestHeaders[@"content-length"].integerValue; - if (contentLength > FBConfiguration.sharedInstance.httpRequestBodySizeLimit) { - // Mirrors FBHTTPConnection's maxRequestBodySize enforcement on iOS/tvOS. Closes the - // connection after responding, since the rest of the oversized body is still incoming. - RouteResponse *tooLarge = [RouteResponse new]; - tooLarge.statusCode = kHTTPStatusCodeRequestEntityTooLarge; - [tooLarge respondWithString:@"Request Entity Too Large"]; - [self writeResponse:tooLarge toClient:client thenCloseConnection:YES]; - return; - } - NSUInteger bodyStart = headerEndRange.location + headerEndRange.length; - NSUInteger totalRequestLength = bodyStart + contentLength; - if (buffer.length < totalRequestLength) { - // Wait for the rest of the body to arrive. - return; - } - - NSData *body = contentLength > 0 ? [buffer subdataWithRange:NSMakeRange(bodyStart, contentLength)] : [NSData data]; - - @synchronized (self.connectionBuffers) { - [buffer replaceBytesInRange:NSMakeRange(0, totalRequestLength) withBytes:NULL length:0]; - } - - [self dispatchMethod:method pathAndQuery:pathAndQuery body:body client:client]; - } -} - -- (void)dispatchMethod:(NSString *)method pathAndQuery:(NSString *)pathAndQuery body:(NSData *)body client:(nw_connection_t)client -{ - NSURLComponents *requestTarget = [NSURLComponents componentsWithString:pathAndQuery]; - NSString *path = requestTarget.path ?: pathAndQuery; - - for (FBWatchHTTPRoute *route in self.routes) { - if (![route.verb isEqualToString:method]) { - continue; - } - NSTextCheckingResult *result = [route.regex firstMatchInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length)]; - if (nil == result) { - continue; - } - - NSMutableDictionary *params = [NSMutableDictionary dictionary]; - for (NSURLQueryItem *queryItem in requestTarget.queryItems) { - params[queryItem.name] = queryItem.value ?: @""; - } - if (route.keys.count > 0 && result.numberOfRanges == route.keys.count + 1) { - NSUInteger index = 1; - for (NSString *key in route.keys) { - params[key] = [path substringWithRange:[result rangeAtIndex:index]]; - index++; - } - } - - NSURL *url = [NSURL URLWithString:path] ?: [NSURL URLWithString:@"/"]; - RouteRequest *request = [[RouteRequest alloc] initWithURL:url params:params.copy body:body]; - RouteResponse *response = [RouteResponse new]; - [self.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { - [response setHeader:field value:value]; - }]; - - void (^invoke)(void) = ^{ - route.block(request, response); - [self writeResponse:response toClient:client]; - }; - dispatch_queue_t routeQueue = self.routeQueue; - if (routeQueue) { - dispatch_async((dispatch_queue_t _Nonnull)routeQueue, invoke); - } else { - invoke(); - } - return; - } - - RouteResponse *notFound = [RouteResponse new]; - notFound.statusCode = kHTTPStatusCodeNotFound; - [notFound respondWithString:@"Not Found"]; - [self writeResponse:notFound toClient:client]; -} - -- (void)writeResponse:(RouteResponse *)response toClient:(nw_connection_t)client -{ - [self writeResponse:response toClient:client thenCloseConnection:NO]; -} - -- (void)writeResponse:(RouteResponse *)response toClient:(nw_connection_t)client thenCloseConnection:(BOOL)shouldClose -{ - NSMutableData *payload = [NSMutableData data]; - NSString *statusLine = [NSString stringWithFormat:@"HTTP/1.1 %ld %@\r\n", - (long)response.statusCode, [self reasonPhraseForStatusCode:response.statusCode]]; - [payload appendData:FBUTF8Data(statusLine)]; - - NSData *body = response.responseData ?: [NSData data]; - NSMutableDictionary *headers = response.headers.mutableCopy; - if (nil == headers[@"Content-Length"]) { - headers[@"Content-Length"] = [NSString stringWithFormat:@"%lu", (unsigned long)body.length]; - } - [headers enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { - NSString *headerLine = [NSString stringWithFormat:@"%@: %@\r\n", field, value]; - [payload appendData:FBUTF8Data(headerLine)]; - }]; - [payload appendData:FBUTF8Data(@"\r\n")]; - [payload appendData:body]; - - if (shouldClose) { - __weak typeof(self) weakSelf = self; - [self.socket writeData:payload toClient:client completion:^{ - [weakSelf closeClient:client]; - }]; - } else { - [self.socket writeData:payload toClient:client]; - } -} - -- (void)closeClient:(nw_connection_t)client -{ - @synchronized (self.connectionBuffers) { - [self.connectionBuffers removeObjectForKey:client]; - } - nw_connection_cancel(client); -} - -- (NSString *)reasonPhraseForStatusCode:(HTTPStatusCode)statusCode -{ - // if/else, not switch, to avoid having to list all ~90 HTTPStatusCode cases for -Wswitch-enum. - if (kHTTPStatusCodeOK == statusCode) { - return @"OK"; - } else if (kHTTPStatusCodeBadRequest == statusCode) { - return @"Bad Request"; - } else if (kHTTPStatusCodeNotFound == statusCode) { - return @"Not Found"; - } else if (kHTTPStatusCodeInternalServerError == statusCode) { - return @"Internal Server Error"; - } - return @"Status"; -} - -@end diff --git a/WebDriverAgentLib/Utilities/FBAudioStreamSession.m b/WebDriverAgentLib/Utilities/FBAudioStreamSession.m index 35f5aedeb4..39da888f9c 100644 --- a/WebDriverAgentLib/Utilities/FBAudioStreamSession.m +++ b/WebDriverAgentLib/Utilities/FBAudioStreamSession.m @@ -8,20 +8,16 @@ #import "FBAudioStreamSession.h" -#import #import -#import -#import -#import -#import "GCDAsyncSocket.h" #import "FBBroadcastProtocol.h" #import "FBLogger.h" #import "FBScrcpyPacket.h" #import "FBTCPSocket.h" -static const NSTimeInterval PACKET_TIMEOUT = 1.0; static const NSUInteger FBAudioStreamSampleRate = 48000; +// Smallest per-client send backlog tolerated regardless of the configured bitrate. +static const NSUInteger FBAudioMinPendingBytes = 64 * 1024; @implementation FBAudioCaptureConfiguration @end @@ -29,7 +25,15 @@ @implementation FBAudioCaptureConfiguration @interface FBAudioStreamSession () -@property (nonatomic) NSMutableArray *listeningClients; +@property (nonatomic) NSMutableArray *listeningClients; +/** + Bytes submitted to the socket but not sent yet, per client. nw_connection_send has no + backpressure signal, so a client that stops draining is disconnected once its backlog exceeds + roughly a second of stream. Opus packets are not independently decodable, so dropping them + would corrupt the stream rather than degrade it. + Guarded by @synchronized (self.listeningClients). + */ +@property (nonatomic) NSMapTable *pendingBytesByClient; @property (nonatomic, nullable) FBTCPSocket *broadcaster; @property (atomic, getter=isActive) BOOL active; /** The OpusHead describing the stream; the extension's real one replaces the synthesized fallback. */ @@ -53,6 +57,8 @@ - (instancetype)initWithIdentifier:(NSUInteger)identifier _identifier = identifier; _configuration = configuration; _listeningClients = [NSMutableArray array]; + _pendingBytesByClient = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory) + valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory]; _active = NO; _streaming = NO; // A synthesized header (pre-skip 0) so scrcpy-framing clients always receive a config packet @@ -68,6 +74,8 @@ - (BOOL)startWithError:(NSError **)error { self.broadcaster = [[FBTCPSocket alloc] initWithPort:self.configuration.port]; self.broadcaster.delegate = self; + // Send small Opus packets immediately instead of letting Nagle coalesce them. + self.broadcaster.noDelay = YES; if (![self.broadcaster startWithError:error]) { self.broadcaster = nil; return NO; @@ -88,6 +96,7 @@ - (void)stop } @synchronized (self.listeningClients) { [self.listeningClients removeAllObjects]; + [self.pendingBytesByClient removeAllObjects]; } } } @@ -146,55 +155,87 @@ - (void)markBroadcastError:(NSString *)message self.lastError = message; } +// Roughly one second of stream, mirroring the write timeout that used to disconnect slow clients. +- (NSUInteger)maxPendingBytesPerClient +{ + return MAX(self.configuration.bitrate / 8, FBAudioMinPendingBytes); +} + +// Caller must hold @synchronized (self.listeningClients). +- (void)sendData:(NSData *)data toClient:(nw_connection_t)client +{ + NSUInteger pending = [self.pendingBytesByClient objectForKey:client].unsignedIntegerValue; + if (pending > self.maxPendingBytesPerClient) { + [FBLogger logFmt:@"Audio capture session %@: dropping a client that is not draining its socket (%@ bytes pending)", + @(self.identifier), @(pending)]; + [self.listeningClients removeObject:client]; + [self.pendingBytesByClient removeObjectForKey:client]; + // -didClientDisconnect: follows from the cancellation and cleans up anything left. + nw_connection_cancel(client); + return; + } + [self.pendingBytesByClient setObject:@(pending + data.length) forKey:client]; + NSUInteger sentLength = data.length; + __weak typeof(self) weakSelf = self; + [self.broadcaster writeData:data toClient:client completion:^(BOOL didSucceed) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } + @synchronized (strongSelf.listeningClients) { + NSUInteger stillPending = [strongSelf.pendingBytesByClient objectForKey:client].unsignedIntegerValue; + if (stillPending > sentLength) { + [strongSelf.pendingBytesByClient setObject:@(stillPending - sentLength) forKey:client]; + } else { + [strongSelf.pendingBytesByClient removeObjectForKey:client]; + } + } + }]; +} + - (void)broadcastData:(NSData *)data { if (data.length == 0) { return; } @synchronized (self.listeningClients) { - for (GCDAsyncSocket *client in self.listeningClients) { - // Slow clients should fail/close instead of buffering indefinitely. - [client writeData:data withTimeout:PACKET_TIMEOUT tag:0]; + // Copied because -sendData:toClient: can remove a stalled client from the array. + for (nw_connection_t client in self.listeningClients.copy) { + [self sendData:data toClient:client]; } } } #pragma mark - -- (void)didClientConnect:(GCDAsyncSocket *)newClient +- (void)didClientConnect:(nw_connection_t)newClient { - [FBLogger logFmt:@"Audio capture session %@: client connected at %@:%d", - @(self.identifier), newClient.connectedHost, newClient.connectedPort]; - // Disable Nagle's algorithm so small Opus packets are sent immediately, keeping latency low. - [self.class enableNoDelayForClient:newClient]; + [FBLogger logFmt:@"Audio capture session %@: client connected", @(self.identifier)]; @synchronized (self.listeningClients) { if (![self.listeningClients containsObject:newClient]) { [self.listeningClients addObject:newClient]; } + // Hand the codec configuration to the new client so it can start decoding immediately. + // lastSentOpusHead is deliberately not updated: it tracks what was broadcast to the whole + // client set, and marking it sent here would skip the changed-config broadcast that earlier + // clients still need (the new client just receives the same config twice, which is harmless). + if (self.configuration.framing == FBAudioFramingScrcpy) { + [self sendData:FBScrcpyPacketCreate(self.currentOpusHead, FBScrcpyFlagConfig, 0) toClient:newClient]; + } } - // Hand the codec configuration to the new client so it can start decoding immediately. - // lastSentOpusHead is deliberately not updated: it tracks what was broadcast to the whole - // client set, and marking it sent here would skip the changed-config broadcast that earlier - // clients still need (the new client just receives the same config twice, which is harmless). - if (self.configuration.framing == FBAudioFramingScrcpy) { - [newClient writeData:FBScrcpyPacketCreate(self.currentOpusHead, FBScrcpyFlagConfig, 0) - withTimeout:PACKET_TIMEOUT - tag:0]; - } - // Keep reading (and discarding) any client bytes so disconnects are detected promptly. - [newClient readDataWithTimeout:-1 tag:0]; } -- (void)didClientSendData:(GCDAsyncSocket *)client +- (void)client:(nw_connection_t)client didReceiveData:(NSData *)data { - // The stream is push-only; client payloads are ignored. Keep the read loop alive. - [client readDataWithTimeout:-1 tag:0]; + // The stream is push-only; client payloads are ignored. FBTCPSocket keeps the receive loop + // running on its own, which is what surfaces disconnects. } -- (void)didClientDisconnect:(GCDAsyncSocket *)client +- (void)didClientDisconnect:(nw_connection_t)client { @synchronized (self.listeningClients) { [self.listeningClients removeObject:client]; + [self.pendingBytesByClient removeObjectForKey:client]; } [FBLogger logFmt:@"Audio capture session %@: client disconnected", @(self.identifier)]; } @@ -226,18 +267,4 @@ - (NSDictionary *)toDictionary }; } -+ (void)enableNoDelayForClient:(GCDAsyncSocket *)client -{ - [client performBlock:^{ - int fd = client.socketFD; - if (fd < 0) { - return; - } - int flag = 1; - if (0 != setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag))) { - [FBLogger logFmt:@"Cannot enable TCP_NODELAY on the audio capture client socket (errno %d)", errno]; - } - }]; -} - @end diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.h b/WebDriverAgentLib/Utilities/FBConfiguration.h index 74004ed8df..421043d8d0 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.h +++ b/WebDriverAgentLib/Utilities/FBConfiguration.h @@ -270,6 +270,17 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { */ @property (atomic, assign) NSTimeInterval animationCoolOffTimeout; +/** + * Maximum time to wait for the frontmost application to confirm its main run loop + * is responsive before an accessibility snapshot request (element attribute + * lookups, active app detection, etc). XCTest has no bounded timeout of its own + * here, so a frozen app could otherwise block WDA forever (#1210); past this + * timeout the request is aborted with an error instead. + * Set to zero or negative to disable, restoring unbounded behavior. Disabled (0) + * by default. + */ +@property (atomic, assign) NSTimeInterval accessibilityDeadline; + /** Custom class chain locator for accept alert button location. This might be useful if the default buttons detection algorithm fails to determine alert buttons properly diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.m b/WebDriverAgentLib/Utilities/FBConfiguration.m index 4c321d2c6f..b0df977fb0 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.m +++ b/WebDriverAgentLib/Utilities/FBConfiguration.m @@ -128,17 +128,14 @@ - (NSRange)bindingPortRange { // 'WebDriverAgent --port 8080' can be passed via the arguments to the process NSRange rangeFromArguments = [self.class bindingPortRangeFromArguments]; - if (rangeFromArguments.location != NSNotFound) { - return rangeFromArguments; + if (rangeFromArguments.location == NSNotFound) { + // Existence of USE_PORT in the environment implies the port range is managed by the launching process. + NSString *usePort = NSProcessInfo.processInfo.environment[@"USE_PORT"]; + rangeFromArguments = usePort.length > 0 + ? NSMakeRange((NSUInteger)usePort.integerValue, 1) + : NSMakeRange(DefaultStartingPort, DefaultPortRange); } - - // Existence of USE_PORT in the environment implies the port range is managed by the launching process. - if (NSProcessInfo.processInfo.environment[@"USE_PORT"] && - [NSProcessInfo.processInfo.environment[@"USE_PORT"] length] > 0) { - return NSMakeRange([NSProcessInfo.processInfo.environment[@"USE_PORT"] integerValue] , 1); - } - - return NSMakeRange(DefaultStartingPort, DefaultPortRange); + return rangeFromArguments; } - (NSString *)bindingIPAddress @@ -412,6 +409,7 @@ - (void)resetSessionSettings // these per session via the settings API. self.waitForIdleTimeout = 0.; self.animationCoolOffTimeout = 0.; + self.accessibilityDeadline = 0.; // 50 should be enough for the majority of the cases. The performance is acceptable for values up to 100. FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @50); FBSetCustomParameterForElementSnapshot(FBSnapshotMaxChildrenKey, @INT_MAX); diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.h b/WebDriverAgentLib/Utilities/FBMjpegServer.h index a9b47cadab..03cd717541 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.h +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.h @@ -19,6 +19,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)init; +/** + The socket that owns this instance as its delegate. Clients are bare nw_connection_t values + with no write method of their own, so frame writes are routed through + -[FBTCPSocket writeData:toClient:]. Must be set before streaming starts. + */ +@property (nonatomic, weak, nullable) FBTCPSocket *socket; + /** Stops screenshot broadcasting and prevents future scheduling. */ diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m index f9005878ce..a73510188d 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -11,7 +11,8 @@ #import @import UniformTypeIdentifiers; -#import "GCDAsyncSocket.h" +// Textual import, not `@import Network;` - see the comment in FBTCPSocket.h. +#import #import "FBConfiguration.h" #import "FBLogger.h" #import "FBScreenshot.h" @@ -21,6 +22,9 @@ static const NSUInteger MAX_FPS = 60; static const NSTimeInterval FRAME_TIMEOUT = 1.; +// nw_connection_send buffers without backpressure, so a client that stops reading would retain +// every generated frame. Frames past this cap are dropped instead of queued. +static const NSUInteger MAX_PENDING_FRAMES_PER_CLIENT = 4; static const NSTimeInterval FAILURE_BACKOFF_MIN = 1.0; static const NSTimeInterval FAILURE_BACKOFF_MAX = 10.0; @@ -36,13 +40,16 @@ static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate) @interface FBMjpegServer() @property (nonatomic, readonly) dispatch_queue_t backgroundQueue; -@property (nonatomic, readonly) NSMutableArray *listeningClients; +@property (nonatomic, readonly) NSMutableArray *listeningClients; @property (nonatomic, readonly) FBImageProcessor *imageProcessor; @property (nonatomic, readonly) long long mainScreenID; @property (nonatomic, assign) NSUInteger consecutiveScreenshotFailures; @property (atomic, assign) BOOL isStreaming; @property (nonatomic, assign) NSUInteger sentFramesCount; @property (nonatomic, assign) NSUInteger sentBytesCount; +@property (nonatomic, assign) NSUInteger droppedFramesCount; +// Frames submitted but not sent yet, per client. Guarded by @synchronized (self.listeningClients). +@property (nonatomic, readonly) NSMapTable *pendingFrameCounts; @end @@ -57,6 +64,8 @@ - (instancetype)init _sentFramesCount = 0; _sentBytesCount = 0; _listeningClients = [NSMutableArray array]; + _pendingFrameCounts = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory) + valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory]; _imageProcessor = [[FBImageProcessor alloc] init]; _mainScreenID = [XCUIScreen.mainScreen displayID]; dispatch_queue_attr_t queueAttributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, 0); @@ -148,30 +157,51 @@ - (void)sendScreenshot:(NSData *)screenshotData { return; } NSUInteger clientCount = self.listeningClients.count; - for (GCDAsyncSocket *client in self.listeningClients) { - // Slow clients should fail/close instead of buffering indefinitely. - [client writeData:chunk withTimeout:FRAME_TIMEOUT tag:0]; + __weak typeof(self) weakSelf = self; + for (nw_connection_t client in self.listeningClients) { + NSUInteger pendingFrames = [self.pendingFrameCounts objectForKey:client].unsignedIntegerValue; + if (pendingFrames >= MAX_PENDING_FRAMES_PER_CLIENT) { + self.droppedFramesCount++; + continue; + } + [self.pendingFrameCounts setObject:@(pendingFrames + 1) forKey:client]; + [self.socket writeData:chunk toClient:client completion:^(BOOL didSucceed) { + // Success or failure, this frame is no longer outstanding - a failed send means the + // connection is going away and -didClientDisconnect: will drop its state entirely. + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } + @synchronized (strongSelf.listeningClients) { + NSUInteger stillPending = [strongSelf.pendingFrameCounts objectForKey:client].unsignedIntegerValue; + if (stillPending > 1) { + [strongSelf.pendingFrameCounts setObject:@(stillPending - 1) forKey:client]; + } else { + [strongSelf.pendingFrameCounts removeObjectForKey:client]; + } + } + }]; } self.sentFramesCount++; self.sentBytesCount += chunk.length * clientCount; NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.sharedInstance.mjpegServerFramerate); if (0 == self.sentFramesCount % framerate) { - [FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@", + [FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@ droppedFrames=%@", @(clientCount), @(self.sentFramesCount), - @(self.sentBytesCount)]]; + @(self.sentBytesCount), + @(self.droppedFramesCount)]]; } } } -- (void)didClientConnect:(GCDAsyncSocket *)newClient +- (void)didClientConnect:(nw_connection_t)newClient { - [FBLogger logFmt:@"Got screenshots broadcast client connection at %@:%d", newClient.connectedHost, newClient.connectedPort]; - // Start broadcast only after there is any data from the client - [newClient readDataWithTimeout:-1 tag:0]; + [FBLogger log:@"Got screenshots broadcast client connection"]; + // FBTCPSocket already schedules the receive that -client:didReceiveData: relies on below. } -- (void)didClientSendData:(GCDAsyncSocket *)client +- (void)client:(nw_connection_t)client didReceiveData:(NSData *)data { @synchronized (self.listeningClients) { if ([self.listeningClients containsObject:client]) { @@ -179,18 +209,19 @@ - (void)didClientSendData:(GCDAsyncSocket *)client } } - [FBLogger logFmt:@"Starting screenshots broadcast for the client at %@:%d", client.connectedHost, client.connectedPort]; + [FBLogger log:@"Starting screenshots broadcast for the client"]; NSString *streamHeader = [NSString stringWithFormat:@"HTTP/1.0 200 OK\r\nServer: %@\r\nConnection: close\r\nMax-Age: 0\r\nExpires: 0\r\nCache-Control: no-cache, private\r\nPragma: no-cache\r\nContent-Type: multipart/x-mixed-replace; boundary=--BoundaryString\r\n\r\n", SERVER_NAME]; - [client writeData:(id)[streamHeader dataUsingEncoding:NSUTF8StringEncoding] withTimeout:-1 tag:0]; + [self.socket writeData:(id)[streamHeader dataUsingEncoding:NSUTF8StringEncoding] toClient:client]; @synchronized (self.listeningClients) { [self.listeningClients addObject:client]; } } -- (void)didClientDisconnect:(GCDAsyncSocket *)client +- (void)didClientDisconnect:(nw_connection_t)client { @synchronized (self.listeningClients) { [self.listeningClients removeObject:client]; + [self.pendingFrameCounts removeObjectForKey:client]; } [FBLogger log:@"Disconnected a client from screenshots broadcast"]; } @@ -199,10 +230,11 @@ - (void)stopStreaming { self.isStreaming = NO; @synchronized (self.listeningClients) { - NSArray *clients = self.listeningClients.copy; + NSArray *clients = self.listeningClients.copy; [self.listeningClients removeAllObjects]; - for (GCDAsyncSocket *client in clients) { - [client disconnect]; + [self.pendingFrameCounts removeAllObjects]; + for (nw_connection_t client in clients) { + nw_connection_cancel(client); } } } diff --git a/WebDriverAgentLib/Utilities/FBSettings.h b/WebDriverAgentLib/Utilities/FBSettings.h index c3f1523e27..92a436db79 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.h +++ b/WebDriverAgentLib/Utilities/FBSettings.h @@ -23,6 +23,7 @@ extern NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION; extern NSString* const FB_SETTING_KEYBOARD_PREDICTION; extern NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH; extern NSString* const FB_SETTING_SNAPSHOT_MAX_CHILDREN; +extern NSString* const FB_SETTING_ACCESSIBILITY_DEADLINE; extern NSString* const FB_SETTING_USE_FIRST_MATCH; extern NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX; extern NSString* const FB_SETTING_REDUCE_MOTION; diff --git a/WebDriverAgentLib/Utilities/FBSettings.m b/WebDriverAgentLib/Utilities/FBSettings.m index b2b219d852..826a226517 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.m +++ b/WebDriverAgentLib/Utilities/FBSettings.m @@ -19,6 +19,7 @@ NSString* const FB_SETTING_KEYBOARD_PREDICTION = @"keyboardPrediction"; NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH = @"snapshotMaxDepth"; NSString* const FB_SETTING_SNAPSHOT_MAX_CHILDREN = @"snapshotMaxChildren"; +NSString* const FB_SETTING_ACCESSIBILITY_DEADLINE = @"accessibilityDeadline"; NSString* const FB_SETTING_USE_FIRST_MATCH = @"useFirstMatch"; NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX = @"boundElementsByIndex"; NSString* const FB_SETTING_REDUCE_MOTION = @"reduceMotion"; diff --git a/WebDriverAgentLib/Utilities/FBSettingsHandler.m b/WebDriverAgentLib/Utilities/FBSettingsHandler.m index c5110cc64d..6184cab5b9 100644 --- a/WebDriverAgentLib/Utilities/FBSettingsHandler.m +++ b/WebDriverAgentLib/Utilities/FBSettingsHandler.m @@ -142,6 +142,10 @@ @implementation FBSettingsHandler FBConfiguration.sharedInstance.animationCoolOffTimeout = [value doubleValue]; return nil; }; + map[FB_SETTING_ACCESSIBILITY_DEADLINE] = ^FBCommandStatus *(FBSession *session, id value) { + FBConfiguration.sharedInstance.accessibilityDeadline = [value doubleValue]; + return nil; + }; map[FB_SETTING_DEFAULT_ALERT_ACTION] = ^FBCommandStatus *(FBSession *session, id value) { if (nil == value) { session.defaultAlertAction = nil; @@ -249,6 +253,9 @@ @implementation FBSettingsHandler map[FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT] = ^id(FBSession *session) { return @(FBConfiguration.sharedInstance.animationCoolOffTimeout); }; + map[FB_SETTING_ACCESSIBILITY_DEADLINE] = ^id(FBSession *session) { + return @(FBConfiguration.sharedInstance.accessibilityDeadline); + }; map[FB_SETTING_BOUND_ELEMENTS_BY_INDEX] = ^id(FBSession *session) { return @(FBConfiguration.sharedInstance.boundElementsByIndex); }; diff --git a/WebDriverAgentLib/Utilities/FBVideoStreamSession.m b/WebDriverAgentLib/Utilities/FBVideoStreamSession.m index 8a9387dba5..a9fe7f8cf9 100644 --- a/WebDriverAgentLib/Utilities/FBVideoStreamSession.m +++ b/WebDriverAgentLib/Utilities/FBVideoStreamSession.m @@ -15,13 +15,13 @@ #import #import -#import "GCDAsyncSocket.h" #import "FBLogger.h" #import "FBPixelBufferConverter.h" #import "FBScrcpyPacket.h" #import "FBTCPSocket.h" -static const NSTimeInterval FRAME_TIMEOUT = 1.0; +// Smallest per-client send backlog tolerated regardless of the configured bitrate. +static const NSUInteger FBVideoMinPendingBytes = 512 * 1024; static const CGFloat FBDefaultScreenCaptureQuality = 0.8; @@ -131,7 +131,15 @@ + (BOOL)fb_pixelBudget:(NSUInteger *)outBudget fromArgument:(nullable id)maxPixe @interface FBVideoStreamSession () -@property (nonatomic) NSMutableArray *listeningClients; +@property (nonatomic) NSMutableArray *listeningClients; +/** + Bytes submitted to the socket but not sent yet, per client. nw_connection_send has no + backpressure signal, so a client that stops draining is disconnected once its backlog exceeds + roughly a second of stream. Dropping frames (as the MJPEG server does) is not an option: H.264 + is inter-frame coded, so a dropped NAL unit corrupts decoding until the next key frame. + Guarded by @synchronized (self.listeningClients). + */ +@property (nonatomic) NSMapTable *pendingBytesByClient; @property (nonatomic, nullable) FBVideoEncoder *encoder; @property (nonatomic, nullable) FBPixelBufferConverter *converter; @property (nonatomic, nullable) FBTCPSocket *broadcaster; @@ -156,6 +164,8 @@ - (instancetype)initWithIdentifier:(NSUInteger)identifier _identifier = identifier; _configuration = configuration; _listeningClients = [NSMutableArray array]; + _pendingBytesByClient = [NSMapTable mapTableWithKeyOptions:(NSPointerFunctionsOptions)(NSMapTableObjectPointerPersonality | NSMapTableStrongMemory) + valueOptions:(NSPointerFunctionsOptions)NSMapTableStrongMemory]; _active = NO; _activeSource = FBVideoStreamSourceScreenshot; } @@ -167,6 +177,8 @@ - (BOOL)startWithError:(NSError **)error // Bind the broadcast socket first so that a port conflict fails cheaply. self.broadcaster = [[FBTCPSocket alloc] initWithPort:self.configuration.port]; self.broadcaster.delegate = self; + // Send small NAL units immediately instead of letting Nagle coalesce them. + self.broadcaster.noDelay = YES; if (![self.broadcaster startWithError:error]) { self.broadcaster = nil; return NO; @@ -201,6 +213,7 @@ - (void)stop } @synchronized (self.listeningClients) { [self.listeningClients removeAllObjects]; + [self.pendingBytesByClient removeAllObjects]; } if (nil != self.encoder) { self.encoder.delegate = nil; @@ -394,56 +407,90 @@ - (void)emitEncodedPicture:(NSData *)annexBPictureData [self broadcastData:annexBPictureData]; } +// Roughly one second of stream, mirroring the write timeout that used to disconnect slow clients. +- (NSUInteger)maxPendingBytesPerClient +{ + return MAX(self.configuration.bitrate / 8, FBVideoMinPendingBytes); +} + +// Caller must hold @synchronized (self.listeningClients). +- (void)sendData:(NSData *)data toClient:(nw_connection_t)client +{ + NSUInteger pending = [self.pendingBytesByClient objectForKey:client].unsignedIntegerValue; + if (pending > self.maxPendingBytesPerClient) { + [FBLogger logFmt:@"Screen capture session %@: dropping a client that is not draining its socket (%@ bytes pending)", + @(self.identifier), @(pending)]; + [self.listeningClients removeObject:client]; + [self.pendingBytesByClient removeObjectForKey:client]; + // -didClientDisconnect: follows from the cancellation and cleans up anything left. + nw_connection_cancel(client); + return; + } + [self.pendingBytesByClient setObject:@(pending + data.length) forKey:client]; + NSUInteger sentLength = data.length; + __weak typeof(self) weakSelf = self; + [self.broadcaster writeData:data toClient:client completion:^(BOOL didSucceed) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } + @synchronized (strongSelf.listeningClients) { + NSUInteger stillPending = [strongSelf.pendingBytesByClient objectForKey:client].unsignedIntegerValue; + if (stillPending > sentLength) { + [strongSelf.pendingBytesByClient setObject:@(stillPending - sentLength) forKey:client]; + } else { + [strongSelf.pendingBytesByClient removeObjectForKey:client]; + } + } + }]; +} + - (void)broadcastData:(NSData *)data { if (data.length == 0) { return; } @synchronized (self.listeningClients) { - for (GCDAsyncSocket *client in self.listeningClients) { - // Slow clients should fail/close instead of buffering indefinitely. - [client writeData:data withTimeout:FRAME_TIMEOUT tag:0]; + // Copied because -sendData:toClient: can remove a stalled client from the array. + for (nw_connection_t client in self.listeningClients.copy) { + [self sendData:data toClient:client]; } } } #pragma mark - -- (void)didClientConnect:(GCDAsyncSocket *)newClient +- (void)didClientConnect:(nw_connection_t)newClient { - [FBLogger logFmt:@"Screen capture session %@: client connected at %@:%d", - @(self.identifier), newClient.connectedHost, newClient.connectedPort]; - // Disable Nagle's algorithm so small NAL units are sent immediately, keeping latency low. - [self.class enableNoDelayForClient:newClient]; + [FBLogger logFmt:@"Screen capture session %@: client connected", @(self.identifier)]; + // Hand the latest parameter sets to the new client and force a key frame so it can start + // decoding immediately. In scrcpy mode the parameter sets are wrapped as a config packet. + NSData *parameterSets = [self currentParameterSets]; @synchronized (self.listeningClients) { if (![self.listeningClients containsObject:newClient]) { [self.listeningClients addObject:newClient]; } - } - // Hand the latest parameter sets to the new client and force a key frame so it can start - // decoding immediately. In scrcpy mode the parameter sets are wrapped as a config packet. - NSData *parameterSets = [self currentParameterSets]; - if (parameterSets.length > 0) { - NSData *payload = self.configuration.framing == FBVideoFramingScrcpy - ? FBScrcpyPacketCreate(parameterSets, FBScrcpyFlagConfig, 0) - : parameterSets; - [newClient writeData:payload withTimeout:FRAME_TIMEOUT tag:0]; + if (parameterSets.length > 0) { + NSData *payload = self.configuration.framing == FBVideoFramingScrcpy + ? FBScrcpyPacketCreate(parameterSets, FBScrcpyFlagConfig, 0) + : parameterSets; + [self sendData:payload toClient:newClient]; + } } [self requestKeyFrame]; - // Keep reading (and discarding) any client bytes so disconnects are detected promptly. - [newClient readDataWithTimeout:-1 tag:0]; } -- (void)didClientSendData:(GCDAsyncSocket *)client +- (void)client:(nw_connection_t)client didReceiveData:(NSData *)data { - // The stream is push-only; client payloads are ignored. Keep the read loop alive. - [client readDataWithTimeout:-1 tag:0]; + // The stream is push-only; client payloads are ignored. FBTCPSocket keeps the receive loop + // running on its own, which is what surfaces disconnects. } -- (void)didClientDisconnect:(GCDAsyncSocket *)client +- (void)didClientDisconnect:(nw_connection_t)client { @synchronized (self.listeningClients) { [self.listeningClients removeObject:client]; + [self.pendingBytesByClient removeObjectForKey:client]; } [FBLogger logFmt:@"Screen capture session %@: client disconnected", @(self.identifier)]; } @@ -471,18 +518,5 @@ - (NSDictionary *)toDictionary }; } -+ (void)enableNoDelayForClient:(GCDAsyncSocket *)client -{ - [client performBlock:^{ - int fd = client.socketFD; - if (fd < 0) { - return; - } - int flag = 1; - if (0 != setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag))) { - [FBLogger logFmt:@"Cannot enable TCP_NODELAY on the screen capture client socket (errno %d)", errno]; - } - }]; -} @end diff --git a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h index b216e303f5..208672af75 100644 --- a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h +++ b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h @@ -38,6 +38,15 @@ NS_ASSUME_NONNULL_BEGIN - (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)application reply:(void (^)(void))reply; +/** + Wraps the private -[XCAXClient_iOS notifyWhenEventLoopIsIdleForApplication:reply:], + used to check run loop responsiveness before a snapshot request (#1210). + `reply` may fire more than once per call; `error` is non-nil only if monitoring + itself could not be started. + */ +- (void)notifyWhenEventLoopIsIdleForApplication:(XCUIApplication *)application + reply:(void (^)(id _Nullable result, NSError * _Nullable error))reply; + - (nullable NSDictionary *)attributesForElement:(id)element attributes:(NSArray *)attributes error:(NSError**)error; diff --git a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m index f84f803fb1..fc90d43f92 100644 --- a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m +++ b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m @@ -81,6 +81,12 @@ - (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)applica [FBAXClient notifyWhenNoAnimationsAreActiveForApplication:application reply:reply]; } +- (void)notifyWhenEventLoopIsIdleForApplication:(XCUIApplication *)application + reply:(void (^)(id _Nullable result, NSError * _Nullable error))reply +{ + [FBAXClient notifyWhenEventLoopIsIdleForApplication:application reply:reply]; +} + - (NSDictionary *)attributesForElement:(id)element attributes:(NSArray *)attributes error:(NSError**)error; @@ -92,28 +98,30 @@ - (NSDictionary *)attributesForElement:(id)element - (XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid { - NSMutableSet *terminatedAppIds = [NSMutableSet set]; - for (NSNumber *appPid in self.appsCache) { - if (![self.appsCache[appPid] running]) { - [terminatedAppIds addObject:appPid]; + @synchronized (self) { + NSMutableSet *terminatedAppIds = [NSMutableSet set]; + for (NSNumber *appPid in self.appsCache) { + if (![self.appsCache[appPid] running]) { + [terminatedAppIds addObject:appPid]; + } + } + for (NSNumber *appPid in terminatedAppIds) { + [self.appsCache removeObjectForKey:appPid]; } - } - for (NSNumber *appPid in terminatedAppIds) { - [self.appsCache removeObjectForKey:appPid]; - } - XCUIApplication *result = [self.appsCache objectForKey:@(pid)]; - if (nil != result) { - return result; - } + XCUIApplication *result = [self.appsCache objectForKey:@(pid)]; + if (nil != result) { + return result; + } - XCUIApplication *app = [[FBAXClient applicationProcessTracker] - monitoredApplicationWithProcessIdentifier:pid]; - if (nil == app) { - return nil; + XCUIApplication *app = [[FBAXClient applicationProcessTracker] + monitoredApplicationWithProcessIdentifier:pid]; + if (nil == app) { + return nil; + } + [self.appsCache setObject:app forKey:@(pid)]; + return app; } - [self.appsCache setObject:app forKey:@(pid)]; - return app; } @end diff --git a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m index dde0e26ee9..16d01c4e79 100644 --- a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m +++ b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m @@ -24,6 +24,7 @@ #import "XCSynthesizedEventRecord.h" #define LAUNCH_APP_TIMEOUT_SEC 300 +#define STOP_SCREEN_RECORDING_TIMEOUT_SEC 20 static void (*originalLaunchAppMethod)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *)); @@ -362,14 +363,16 @@ + (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid error:(NSError *__autoreleasi } __block NSError *innerError = nil; - [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ - [session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) { - if (nil != invokeError) { - innerError = invokeError; - } - completion(); - }]; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + [session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) { + innerError = invokeError; + dispatch_semaphore_signal(sem); }]; + int64_t timeoutNs = (int64_t)(STOP_SCREEN_RECORDING_TIMEOUT_SEC * NSEC_PER_SEC); + if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs)) && nil == innerError) { + NSString *message = [NSString stringWithFormat:@"Did not receive a reply to stop screen recording within %d seconds", STOP_SCREEN_RECORDING_TIMEOUT_SEC]; + innerError = [[[FBErrorBuilder builder] withDescription:message] build]; + } if (nil != innerError && error) { *error = innerError; } diff --git a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m index 161be878cd..2a9c5a8454 100644 --- a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m +++ b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m @@ -63,11 +63,6 @@ - (XCUIElementQuery *)fb_query @end -// Bounds the legacy testmanagerd protocol-version exchange below. A daemon that never replies -// (observed on some legacy configurations) must not be able to hang startup indefinitely; the -// value is diagnostic-only, so timing out and moving on is safe. -static const NSTimeInterval FBProtocolVersionExchangeTimeout = 30.0; - @implementation XCPointerEvent (FBXcodeCompatibility) + (BOOL)fb_areKeyEventsSupported @@ -82,42 +77,53 @@ + (BOOL)fb_areKeyEventsSupported @end +#define TESTMANAGERD_VERSION_TIMEOUT_SEC 20 + NSInteger FBTestmanagerdVersion(void) { - static dispatch_once_t getTestmanagerdVersion; - static NSInteger testmanagerdVersion; - dispatch_once(&getTestmanagerdVersion, ^{ + // -1 means "not yet determined". The timeout fallback is cached like any other outcome: the + // value is diagnostic-only, and retrying would stall every later /status for the full timeout + // against a daemon that never answers. + static NSInteger cachedVersion = -1; + static dispatch_queue_t syncQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + syncQueue = dispatch_queue_create("com.facebook.wda.testmanagerdVersion", DISPATCH_QUEUE_SERIAL); + }); + + __block NSInteger result; + dispatch_sync(syncQueue, ^{ + if (cachedVersion >= 0) { + result = cachedVersion; + return; + } + id proxy = [FBXCTestDaemonsProxy testRunnerProxy]; if ([(NSObject *)proxy respondsToSelector:@selector(_XCT_exchangeProtocolVersion:reply:)]) { id legacyProxy = (id)proxy; - // The reply lands in a block-local so a late response (after the bounded wait below has - // given up and dispatch_once has completed) never writes the shared static while - // concurrent readers may be using it; late replies are simply ignored. - __block NSInteger exchangedVersion = 0; - BOOL exchanged = [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ - [legacyProxy _XCT_exchangeProtocolVersion:testmanagerdVersion reply:^(unsigned long long code) { - exchangedVersion = (NSInteger) code; - completion(); - }]; - } timeout:FBProtocolVersionExchangeTimeout]; - if (exchanged) { - testmanagerdVersion = exchangedVersion; + __block NSInteger receivedVersion = -1; + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + [legacyProxy _XCT_exchangeProtocolVersion:0 reply:^(unsigned long long code) { + receivedVersion = (NSInteger) code; + dispatch_semaphore_signal(sem); + }]; + int64_t timeoutNs = (int64_t)(TESTMANAGERD_VERSION_TIMEOUT_SEC * NSEC_PER_SEC); + if (0 != dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, timeoutNs))) { + [FBLogger logFmt:@"Did not receive a testmanagerd protocol version reply within %d seconds; assuming the newest/full-featured protocol", TESTMANAGERD_VERSION_TIMEOUT_SEC]; + result = 0xFFFF; } else { - [FBLogger log:@"Timed out waiting for the testmanagerd protocol version exchange"]; - // testmanagerdVersion stays at its default (diagnostic-only). + result = receivedVersion; } } else { - // Modern testmanagerd (Xcode 15+) has already negotiated named XCTCapabilities by the time - // a daemon session exists, instead of a single scalar protocol version. There is no direct - // integer equivalent to report here (this value is diagnostic-only, surfaced via the - // 'testmanagerdVersion' session capability), so keep reporting the existing "assume - // newest/full-featured" sentinel, while confirming capabilities did negotiate successfully. + // Modern testmanagerd (Xcode 15+) negotiates named XCTCapabilities instead of a scalar + // version; there's no direct integer equivalent, so just confirm capabilities negotiated. XCTCapabilities *capabilities = [XCTRunnerDaemonSession sharedSession].remoteInterfaceCapabilities; if (nil == capabilities) { [FBLogger log:@"Could not retrieve testmanagerd capabilities"]; } - testmanagerdVersion = 0xFFFF; + result = 0xFFFF; } + cachedVersion = result; }); - return testmanagerdVersion; + return result; } diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h deleted file mode 100644 index 26436103ea..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.h +++ /dev/null @@ -1,12 +0,0 @@ -#import - - -@interface NSNumber (DDNumber) - -+ (BOOL)parseString:(NSString *)str intoSInt64:(SInt64 *)pNum; -+ (BOOL)parseString:(NSString *)str intoUInt64:(UInt64 *)pNum; - -+ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum; -+ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum; - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m deleted file mode 100644 index 2a9f207555..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDNumber.m +++ /dev/null @@ -1,88 +0,0 @@ -#import "DDNumber.h" - - -@implementation NSNumber (DDNumber) - -+ (BOOL)parseString:(NSString *)str intoSInt64:(SInt64 *)pNum -{ - if(str == nil) - { - *pNum = 0; - return NO; - } - - errno = 0; - - // On both 32-bit and 64-bit machines, long long = 64 bit - - *pNum = strtoll([str UTF8String], NULL, 10); - - if(errno != 0) - return NO; - else - return YES; -} - -+ (BOOL)parseString:(NSString *)str intoUInt64:(UInt64 *)pNum -{ - if(str == nil) - { - *pNum = 0; - return NO; - } - - errno = 0; - - // On both 32-bit and 64-bit machines, unsigned long long = 64 bit - - *pNum = strtoull([str UTF8String], NULL, 10); - - if(errno != 0) - return NO; - else - return YES; -} - -+ (BOOL)parseString:(NSString *)str intoNSInteger:(NSInteger *)pNum -{ - if(str == nil) - { - *pNum = 0; - return NO; - } - - errno = 0; - - // On LP64, NSInteger = long = 64 bit - // Otherwise, NSInteger = int = long = 32 bit - - *pNum = strtol([str UTF8String], NULL, 10); - - if(errno != 0) - return NO; - else - return YES; -} - -+ (BOOL)parseString:(NSString *)str intoNSUInteger:(NSUInteger *)pNum -{ - if(str == nil) - { - *pNum = 0; - return NO; - } - - errno = 0; - - // On LP64, NSUInteger = unsigned long = 64 bit - // Otherwise, NSUInteger = unsigned int = unsigned long = 32 bit - - *pNum = strtoul([str UTF8String], NULL, 10); - - if(errno != 0) - return NO; - else - return YES; -} - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h deleted file mode 100644 index e01db03f75..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.h +++ /dev/null @@ -1,56 +0,0 @@ -/** - * DDRange is the functional equivalent of a 64 bit NSRange. - * The HTTP Server is designed to support very large files. - * On 32 bit architectures (ppc, i386) NSRange uses unsigned 32 bit integers. - * This only supports a range of up to 4 gigabytes. - * By defining our own variant, we can support a range up to 16 exabytes. - * - * All effort is given such that DDRange functions EXACTLY the same as NSRange. - **/ - -#import -#import - -@class NSString; - -typedef struct _DDRange { - UInt64 location; - UInt64 length; -} DDRange; - -typedef DDRange *DDRangePointer; - -NS_INLINE DDRange DDMakeRange(UInt64 loc, UInt64 len) { - DDRange r; - r.location = loc; - r.length = len; - return r; -} - -NS_INLINE UInt64 DDMaxRange(DDRange range) { - return (range.location + range.length); -} - -NS_INLINE BOOL DDLocationInRange(UInt64 loc, DDRange range) { - return (loc - range.location < range.length); -} - -NS_INLINE BOOL DDEqualRanges(DDRange range1, DDRange range2) { - return ((range1.location == range2.location) && (range1.length == range2.length)); -} - -FOUNDATION_EXPORT DDRange DDUnionRange(DDRange range1, DDRange range2); -FOUNDATION_EXPORT DDRange DDIntersectionRange(DDRange range1, DDRange range2); -FOUNDATION_EXPORT NSString *DDStringFromRange(DDRange range); -FOUNDATION_EXPORT DDRange DDRangeFromString(NSString *aString); - -NSInteger DDRangeCompare(DDRangePointer pDDRange1, DDRangePointer pDDRange2); - -@interface NSValue (NSValueDDRangeExtensions) - -+ (NSValue *)valueWithDDRange:(DDRange)range; -- (DDRange)ddrangeValue; - -- (NSInteger)ddrangeCompare:(NSValue *)ddrangeValue; - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m deleted file mode 100644 index d8c8c70ca9..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m +++ /dev/null @@ -1,100 +0,0 @@ -#import "DDRange.h" -#import "DDNumber.h" - -#pragma clang diagnostic ignored "-Wformat-non-iso" - -DDRange DDUnionRange(DDRange range1, DDRange range2) -{ - UInt64 location = MIN(range1.location, range2.location); - UInt64 length = MAX(DDMaxRange(range1), DDMaxRange(range2)) - location; - - return DDMakeRange(location, length); -} - -DDRange DDIntersectionRange(DDRange range1, DDRange range2) -{ - if((DDMaxRange(range1) < range2.location) || (DDMaxRange(range2) < range1.location)) - { - return DDMakeRange(0, 0); - } - - return DDMakeRange(MAX(range1.location, range2.location), - MIN(DDMaxRange(range1), DDMaxRange(range2)) - MAX(range1.location, range2.location)); -} - -NSString *DDStringFromRange(DDRange range) -{ - return [NSString stringWithFormat:@"{%qu, %qu}", range.location, range.length]; -} - -DDRange DDRangeFromString(NSString *aString) -{ - DDRange result = DDMakeRange(0, 0); - - // NSRange will ignore '-' characters, but not '+' characters - NSCharacterSet *cset = [NSCharacterSet characterSetWithCharactersInString:@"+0123456789"]; - - NSScanner *scanner = [NSScanner scannerWithString:aString]; - [scanner setCharactersToBeSkipped:[cset invertedSet]]; - - NSString *str1 = nil; - NSString *str2 = nil; - - BOOL found1 = [scanner scanCharactersFromSet:cset intoString:&str1]; - BOOL found2 = [scanner scanCharactersFromSet:cset intoString:&str2]; - - if(found1) [NSNumber parseString:str1 intoUInt64:&result.location]; - if(found2) [NSNumber parseString:str2 intoUInt64:&result.length]; - - return result; -} - -NSInteger DDRangeCompare(DDRangePointer pDDRange1, DDRangePointer pDDRange2) -{ - // Comparison basis: - // Which range would you encouter first if you started at zero, and began walking towards infinity. - // If you encouter both ranges at the same time, which range would end first. - - if(pDDRange1->location < pDDRange2->location) - { - return NSOrderedAscending; - } - if(pDDRange1->location > pDDRange2->location) - { - return NSOrderedDescending; - } - if(pDDRange1->length < pDDRange2->length) - { - return NSOrderedAscending; - } - if(pDDRange1->length > pDDRange2->length) - { - return NSOrderedDescending; - } - - return NSOrderedSame; -} - -@implementation NSValue (NSValueDDRangeExtensions) - -+ (NSValue *)valueWithDDRange:(DDRange)range -{ - return [NSValue valueWithBytes:&range objCType:@encode(DDRange)]; -} - -- (DDRange)ddrangeValue -{ - DDRange result; - [self getValue:&result]; - return result; -} - -- (NSInteger)ddrangeCompare:(NSValue *)other -{ - DDRange r1 = [self ddrangeValue]; - DDRange r2 = [other ddrangeValue]; - - return DDRangeCompare(&r1, &r2); -} - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h deleted file mode 100644 index a6e605cfb5..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.h +++ /dev/null @@ -1,112 +0,0 @@ -#import - -@class GCDAsyncSocket; -@class HTTPMessage; -@class HTTPServer; -@class WebSocket; -@protocol HTTPResponse; - - -#define HTTPConnectionDidDieNotification @"HTTPConnectionDidDie" - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@interface HTTPConfig : NSObject -{ - // Strong on purpose: connections reply on their own queues, so an unretained server is a - // use-after-free once the server is torn down (e.g. via /wda/shutdown) while replies are in - // flight. The server->connections->config->server cycle is broken when connections die. - HTTPServer __strong *server; - NSString __strong *documentRoot; - dispatch_queue_t queue; -} - -- (id)initWithServer:(HTTPServer *)server documentRoot:(NSString *)documentRoot; -- (id)initWithServer:(HTTPServer *)server documentRoot:(NSString *)documentRoot queue:(dispatch_queue_t)q; - -@property (nonatomic, strong, readonly) HTTPServer *server; -@property (nonatomic, strong, readonly) NSString *documentRoot; -@property (nonatomic, readonly) dispatch_queue_t queue; - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@interface HTTPConnection : NSObject -{ - dispatch_queue_t connectionQueue; - GCDAsyncSocket *asyncSocket; - HTTPConfig *config; - - BOOL started; - - HTTPMessage *request; - unsigned int numHeaderLines; - - BOOL sentResponseHeaders; - - NSObject *httpResponse; - - NSMutableArray *ranges; - NSMutableArray *ranges_headers; - NSString *ranges_boundry; - int rangeIndex; - - UInt64 requestContentLength; - UInt64 requestContentLengthReceived; - UInt64 requestChunkSize; - UInt64 requestChunkSizeReceived; - - NSMutableArray *responseDataSizes; -} - -- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig; - -- (void)start; -- (void)stop; - -- (void)startConnection; - -- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path; -- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path; - -- (NSDictionary *)parseParams:(NSString *)query; -- (NSDictionary *)parseGetParams; - -- (NSString *)requestURI; - -- (NSArray *)directoryIndexFileNames; -- (NSString *)filePathForURI:(NSString *)path; -- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory; -- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path; -- (WebSocket *)webSocketForURI:(NSString *)path; - -- (void)prepareForBodyWithSize:(UInt64)contentLength; -- (void)processBodyData:(NSData *)postDataChunk; -- (void)finishBody; -- (UInt64)maxRequestBodySize; - -- (void)handleVersionNotSupported:(NSString *)version; -- (void)handleRequestBodyTooLarge; -- (void)handleResourceNotFound; -- (void)handleInvalidRequest:(NSData *)data; -- (void)handleUnknownMethod:(NSString *)method; - -- (NSData *)preprocessResponse:(HTTPMessage *)response; -- (NSData *)preprocessErrorResponse:(HTTPMessage *)response; - -- (void)finishResponse; - -- (BOOL)shouldDie; -- (void)die; - -@end - -@interface HTTPConnection (AsynchronousHTTPResponse) -- (void)responseHasAvailableData:(NSObject *)sender; -- (void)responseDidAbort:(NSObject *)sender; -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m deleted file mode 100644 index 3e2a11893e..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPConnection.m +++ /dev/null @@ -1,2281 +0,0 @@ -#import "HTTPServer.h" -#import "HTTPConnection.h" -#import "HTTPMessage.h" -#import "HTTPResponse.h" -#import "DDNumber.h" -#import "DDRange.h" -#import "HTTPLogging.h" - -#import "GCDAsyncSocket.h" - -#if ! __has_feature(objc_arc) -#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -#pragma clang diagnostic ignored "-Wunknown-warning-option" -#pragma clang diagnostic ignored "-Wdirect-ivar-access" -#pragma clang diagnostic ignored "-Wimplicit-retain-self" -#pragma clang diagnostic ignored "-Wformat-non-iso" -#pragma clang diagnostic ignored "-Wunused-variable" -#pragma clang diagnostic ignored "-Wsign-compare" -#pragma clang diagnostic ignored "-Wformat-nonliteral" -#pragma clang diagnostic ignored "-Wunreachable-code" -#pragma clang diagnostic ignored "-Wfloat-conversion" - -// Log levels: off, error, warn, info, verbose -// Other flags: trace -static const int httpLogLevel = HTTP_LOG_LEVEL_WARN; // | HTTP_LOG_FLAG_TRACE; - -// Define chunk size used to read in data for responses -// This is how much data will be read from disk into RAM at a time -#if TARGET_OS_IPHONE -#define READ_CHUNKSIZE (1024 * 256) -#else -#define READ_CHUNKSIZE (1024 * 512) -#endif - -// Define chunk size used to read in POST upload data -#if TARGET_OS_IPHONE -#define POST_CHUNKSIZE (1024 * 256) -#else -#define POST_CHUNKSIZE (1024 * 512) -#endif - -// Define the various timeouts (in seconds) for various parts of the HTTP process -#define TIMEOUT_READ_FIRST_HEADER_LINE 30 -#define TIMEOUT_READ_SUBSEQUENT_HEADER_LINE 30 -#define TIMEOUT_READ_BODY -1 -#define TIMEOUT_WRITE_HEAD 30 -#define TIMEOUT_WRITE_BODY -1 -#define TIMEOUT_WRITE_ERROR 30 -#define TIMEOUT_NONCE 300 - -// Define the various limits -// MAX_HEADER_LINE_LENGTH: Max length (in bytes) of any single line in a header (including \r\n) -// MAX_HEADER_LINES : Max number of lines in a single header (including first GET line) -#define MAX_HEADER_LINE_LENGTH 8190 -#define MAX_HEADER_LINES 100 -// MAX_CHUNK_LINE_LENGTH : For accepting chunked transfer uploads, max length of chunk size line (including \r\n) -#define MAX_CHUNK_LINE_LENGTH 200 - -// Define the various tags we'll use to differentiate what it is we're currently doing -#define HTTP_REQUEST_HEADER 10 -#define HTTP_REQUEST_BODY 11 -#define HTTP_REQUEST_CHUNK_SIZE 12 -#define HTTP_REQUEST_CHUNK_DATA 13 -#define HTTP_REQUEST_CHUNK_TRAILER 14 -#define HTTP_REQUEST_CHUNK_FOOTER 15 -#define HTTP_PARTIAL_RESPONSE 20 -#define HTTP_PARTIAL_RESPONSE_HEADER 21 -#define HTTP_PARTIAL_RESPONSE_BODY 22 -#define HTTP_CHUNKED_RESPONSE_HEADER 30 -#define HTTP_CHUNKED_RESPONSE_BODY 31 -#define HTTP_CHUNKED_RESPONSE_FOOTER 32 -#define HTTP_PARTIAL_RANGE_RESPONSE_BODY 40 -#define HTTP_PARTIAL_RANGES_RESPONSE_BODY 50 -#define HTTP_RESPONSE 90 -#define HTTP_FINAL_RESPONSE 91 - -// A quick note about the tags: -// -// The HTTP_RESPONSE and HTTP_FINAL_RESPONSE are designated tags signalling that the response is completely sent. -// That is, in the onSocket:didWriteDataWithTag: method, if the tag is HTTP_RESPONSE or HTTP_FINAL_RESPONSE, -// it is assumed that the response is now completely sent. -// Use HTTP_RESPONSE if it's the end of a response, and you want to start reading more requests afterwards. -// Use HTTP_FINAL_RESPONSE if you wish to terminate the connection after sending the response. -// -// If you are sending multiple data segments in a custom response, make sure that only the last segment has -// the HTTP_RESPONSE tag. For all other segments prior to the last segment use HTTP_PARTIAL_RESPONSE, or some other -// tag of your own invention. - -@interface HTTPConnection (PrivateAPI) -- (void)startReadingRequest; -- (void)sendResponseHeadersAndBody; -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@implementation HTTPConnection - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Init, Dealloc: -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * Sole Constructor. - * Associates this new HTTP connection with the given AsyncSocket. - * This HTTP connection object will become the socket's delegate and take over responsibility for the socket. - **/ -- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig -{ - if ((self = [super init])) - { - HTTPLogTrace(); - - if (aConfig.queue) - { - connectionQueue = aConfig.queue; -#if !OS_OBJECT_USE_OBJC - dispatch_retain(connectionQueue); -#endif - } - else - { - connectionQueue = dispatch_queue_create("HTTPConnection", NULL); - } - - // Take over ownership of the socket - asyncSocket = newSocket; - [asyncSocket setDelegate:(id)self delegateQueue:connectionQueue]; - - - // Store configuration - config = aConfig; - - // Create a new HTTP message - request = [[HTTPMessage alloc] initEmptyRequest]; - - numHeaderLines = 0; - - responseDataSizes = [[NSMutableArray alloc] initWithCapacity:5]; - } - return self; -} - -/** - * Standard Deconstructor. - **/ -- (void)dealloc -{ - HTTPLogTrace(); - -#if !OS_OBJECT_USE_OBJC - dispatch_release(connectionQueue); -#endif - - [asyncSocket setDelegate:nil delegateQueue:NULL]; - [asyncSocket disconnect]; - - if ([httpResponse respondsToSelector:@selector(connectionDidClose)]) - { - [httpResponse connectionDidClose]; - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Method Support -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * Returns whether or not the server will accept messages of a given method - * at a particular URI. - **/ -- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path -{ - HTTPLogTrace(); - - // Override me to support methods such as POST. - // - // Things you may want to consider: - // - Does the given path represent a resource that is designed to accept this method? - // - If accepting an upload, is the size of the data being uploaded too big? - // To do this you can check the requestContentLength variable. - // - // For more information, you can always access the HTTPMessage request variable. - // - // You should fall through with a call to [super supportsMethod:method atPath:path] - // - // See also: expectsRequestBodyFromMethod:atPath: - - if ([method isEqualToString:@"GET"]) - return YES; - - if ([method isEqualToString:@"HEAD"]) - return YES; - - return NO; -} - -/** - * Returns whether or not the server expects a body from the given method. - * - * In other words, should the server expect a content-length header and associated body from this method. - * This would be true in the case of a POST, where the client is sending data, - * or for something like PUT where the client is supposed to be uploading a file. - **/ -- (BOOL)expectsRequestBodyFromMethod:(NSString *)method atPath:(NSString *)path -{ - HTTPLogTrace(); - - // Override me to add support for other methods that expect the client - // to send a body along with the request header. - // - // You should fall through with a call to [super expectsRequestBodyFromMethod:method atPath:path] - // - // See also: supportsMethod:atPath: - - if ([method isEqualToString:@"POST"]) - return YES; - - if ([method isEqualToString:@"PUT"]) - return YES; - - return NO; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Core -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * Starting point for the HTTP connection after it has been fully initialized (including subclasses). - * This method is called by the HTTP server. - **/ -- (void)start -{ - dispatch_async(connectionQueue, ^{ @autoreleasepool { - - if (!started) - { - started = YES; - [self startConnection]; - } - }}); -} - -/** - * This method is called by the HTTPServer if it is asked to stop. - * The server, in turn, invokes stop on each HTTPConnection instance. - **/ -- (void)stop -{ - dispatch_async(connectionQueue, ^{ @autoreleasepool { - - // Disconnect the socket. - // The socketDidDisconnect delegate method will handle everything else. - [asyncSocket disconnect]; - }}); -} - -/** - * Starting point for the HTTP connection. - **/ -- (void)startConnection -{ - // Override me to do any custom work before the connection starts. - // - // Be sure to invoke [super startConnection] when you're done. - - HTTPLogTrace(); - - [self startReadingRequest]; -} - -/** - * Starts reading an HTTP request. - **/ -- (void)startReadingRequest -{ - HTTPLogTrace(); - - [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] - withTimeout:TIMEOUT_READ_FIRST_HEADER_LINE - maxLength:MAX_HEADER_LINE_LENGTH - tag:HTTP_REQUEST_HEADER]; -} - -/** - * Parses the given query string. - * - * For example, if the query is "q=John%20Mayer%20Trio&num=50" - * then this method would return the following dictionary: - * { - * q = "John Mayer Trio" - * num = "50" - * } - **/ -- (NSDictionary *)parseParams:(NSString *)query -{ - NSArray *components = [query componentsSeparatedByString:@"&"]; - NSMutableDictionary *result = [NSMutableDictionary dictionaryWithCapacity:[components count]]; - - NSUInteger i; - for (i = 0; i < [components count]; i++) - { - NSString *component = [components objectAtIndex:i]; - if ([component length] > 0) - { - NSRange range = [component rangeOfString:@"="]; - if (range.location != NSNotFound) - { - NSString *escapedKey = [component substringToIndex:(range.location + 0)]; - NSString *escapedValue = [component substringFromIndex:(range.location + 1)]; - - if ([escapedKey length] > 0) - { - CFStringRef k, v; - - k = CFURLCreateStringByReplacingPercentEscapes(NULL, (__bridge CFStringRef)escapedKey, CFSTR("")); - v = CFURLCreateStringByReplacingPercentEscapes(NULL, (__bridge CFStringRef)escapedValue, CFSTR("")); - - NSString *key, *value; - - key = (__bridge_transfer NSString *)k; - value = (__bridge_transfer NSString *)v; - - if (key) - { - if (value) - [result setObject:value forKey:key]; - else - [result setObject:[NSNull null] forKey:key]; - } - } - } - } - } - - return result; -} - -/** - * Parses the query variables in the request URI. - * - * For example, if the request URI was "/search.html?q=John%20Mayer%20Trio&num=50" - * then this method would return the following dictionary: - * { - * q = "John Mayer Trio" - * num = "50" - * } - **/ -- (NSDictionary *)parseGetParams -{ - if(![request isHeaderComplete]) return nil; - - NSDictionary *result = nil; - - NSURL *url = [request url]; - if(url) - { - NSString *query = [url query]; - if (query) - { - result = [self parseParams:query]; - } - } - - return result; -} - -/** - * Attempts to parse the given range header into a series of sequential non-overlapping ranges. - * If successfull, the variables 'ranges' and 'rangeIndex' will be updated, and YES will be returned. - * Otherwise, NO is returned, and the range request should be ignored. - **/ -- (BOOL)parseRangeRequest:(NSString *)rangeHeader withContentLength:(UInt64)contentLength -{ - HTTPLogTrace(); - - // Examples of byte-ranges-specifier values (assuming an entity-body of length 10000): - // - // - The first 500 bytes (byte offsets 0-499, inclusive): bytes=0-499 - // - // - The second 500 bytes (byte offsets 500-999, inclusive): bytes=500-999 - // - // - The final 500 bytes (byte offsets 9500-9999, inclusive): bytes=-500 - // - // - Or bytes=9500- - // - // - The first and last bytes only (bytes 0 and 9999): bytes=0-0,-1 - // - // - Several legal but not canonical specifications of the second 500 bytes (byte offsets 500-999, inclusive): - // bytes=500-600,601-999 - // bytes=500-700,601-999 - // - - NSRange eqsignRange = [rangeHeader rangeOfString:@"="]; - - if(eqsignRange.location == NSNotFound) return NO; - - NSUInteger tIndex = eqsignRange.location; - NSUInteger fIndex = eqsignRange.location + eqsignRange.length; - - NSMutableString *rangeType = [[rangeHeader substringToIndex:tIndex] mutableCopy]; - NSMutableString *rangeValue = [[rangeHeader substringFromIndex:fIndex] mutableCopy]; - - CFStringTrimWhitespace((__bridge CFMutableStringRef)rangeType); - CFStringTrimWhitespace((__bridge CFMutableStringRef)rangeValue); - - if([rangeType caseInsensitiveCompare:@"bytes"] != NSOrderedSame) return NO; - - NSArray *rangeComponents = [rangeValue componentsSeparatedByString:@","]; - - if([rangeComponents count] == 0) return NO; - - ranges = [[NSMutableArray alloc] initWithCapacity:[rangeComponents count]]; - - rangeIndex = 0; - - // Note: We store all range values in the form of DDRange structs, wrapped in NSValue objects. - // Since DDRange consists of UInt64 values, the range extends up to 16 exabytes. - - NSUInteger i; - for (i = 0; i < [rangeComponents count]; i++) - { - NSString *rangeComponent = [rangeComponents objectAtIndex:i]; - - NSRange dashRange = [rangeComponent rangeOfString:@"-"]; - - if (dashRange.location == NSNotFound) - { - // We're dealing with an individual byte number - - UInt64 byteIndex; - if(![NSNumber parseString:rangeComponent intoUInt64:&byteIndex]) return NO; - - if(byteIndex >= contentLength) return NO; - - [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(byteIndex, 1)]]; - } - else - { - // We're dealing with a range of bytes - - tIndex = dashRange.location; - fIndex = dashRange.location + dashRange.length; - - NSString *r1str = [rangeComponent substringToIndex:tIndex]; - NSString *r2str = [rangeComponent substringFromIndex:fIndex]; - - UInt64 r1, r2; - - BOOL hasR1 = [NSNumber parseString:r1str intoUInt64:&r1]; - BOOL hasR2 = [NSNumber parseString:r2str intoUInt64:&r2]; - - if (!hasR1) - { - // We're dealing with a "-[#]" range - // - // r2 is the number of ending bytes to include in the range - - if(!hasR2) return NO; - if(r2 > contentLength) return NO; - - UInt64 startIndex = contentLength - r2; - - [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(startIndex, r2)]]; - } - else if (!hasR2) - { - // We're dealing with a "[#]-" range - // - // r1 is the starting index of the range, which goes all the way to the end - - if(r1 >= contentLength) return NO; - - [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(r1, contentLength - r1)]]; - } - else - { - // We're dealing with a normal "[#]-[#]" range - // - // Note: The range is inclusive. So 0-1 has a length of 2 bytes. - - if(r1 > r2) return NO; - if(r2 >= contentLength) return NO; - - [ranges addObject:[NSValue valueWithDDRange:DDMakeRange(r1, r2 - r1 + 1)]]; - } - } - } - - if([ranges count] == 0) return NO; - - // Now make sure none of the ranges overlap - - for (i = 0; i < [ranges count] - 1; i++) - { - DDRange range1 = [[ranges objectAtIndex:i] ddrangeValue]; - - NSUInteger j; - for (j = i+1; j < [ranges count]; j++) - { - DDRange range2 = [[ranges objectAtIndex:j] ddrangeValue]; - - DDRange iRange = DDIntersectionRange(range1, range2); - - if(iRange.length != 0) - { - return NO; - } - } - } - - // Sort the ranges - - [ranges sortUsingSelector:@selector(ddrangeCompare:)]; - - return YES; -} - -- (NSString *)requestURI -{ - if(request == nil) return nil; - - return [[request url] relativeString]; -} - -/** - * This method is called after a full HTTP request has been received. - * The current request is in the HTTPMessage request variable. - **/ -- (void)replyToHTTPRequest -{ - HTTPLogTrace(); - - if (HTTP_LOG_VERBOSE) - { - NSData *tempData = [request messageData]; - - NSString *tempStr = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding]; - HTTPLogVerbose(@"%@[%p]: Received HTTP request:\n%@", THIS_FILE, self, tempStr); - } - - // Check the HTTP version - // We only support version 1.0 and 1.1 - - NSString *version = [request version]; - if (![version isEqualToString:HTTPVersion1_1] && ![version isEqualToString:HTTPVersion1_0]) - { - [self handleVersionNotSupported:version]; - return; - } - - // Extract requested URI - NSString *uri = [self requestURI]; - - // Extract the method - NSString *method = [request method]; - - // Note: We already checked to ensure the method was supported in onSocket:didReadData:withTag: - - // Respond properly to HTTP 'GET' and 'HEAD' commands - httpResponse = [self httpResponseForMethod:method URI:uri]; - - if (httpResponse == nil) - { - [self handleResourceNotFound]; - return; - } - - [self sendResponseHeadersAndBody]; -} - -/** - * Prepares a single-range response. - * - * Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it. - **/ -- (HTTPMessage *)newUniRangeResponse:(UInt64)contentLength -{ - HTTPLogTrace(); - - // Status Code 206 - Partial Content - HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1]; - - DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; - - NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", range.length]; - [response setHeaderField:@"Content-Length" value:contentLengthStr]; - - NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1]; - NSString *contentRangeStr = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength]; - [response setHeaderField:@"Content-Range" value:contentRangeStr]; - - return response; -} - -/** - * Prepares a multi-range response. - * - * Note: The returned HTTPMessage is owned by the sender, who is responsible for releasing it. - **/ -- (HTTPMessage *)newMultiRangeResponse:(UInt64)contentLength -{ - HTTPLogTrace(); - - // Status Code 206 - Partial Content - HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:206 description:nil version:HTTPVersion1_1]; - - // We have to send each range using multipart/byteranges - // So each byterange has to be prefix'd and suffix'd with the boundry - // Example: - // - // HTTP/1.1 206 Partial Content - // Content-Length: 220 - // Content-Type: multipart/byteranges; boundary=4554d24e986f76dd6 - // - // - // --4554d24e986f76dd6 - // Content-Range: bytes 0-25/4025 - // - // [...] - // --4554d24e986f76dd6 - // Content-Range: bytes 3975-4024/4025 - // - // [...] - // --4554d24e986f76dd6-- - - ranges_headers = [[NSMutableArray alloc] initWithCapacity:[ranges count]]; - - CFUUIDRef theUUID = CFUUIDCreate(NULL); - ranges_boundry = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, theUUID); - CFRelease(theUUID); - - NSString *startingBoundryStr = [NSString stringWithFormat:@"\r\n--%@\r\n", ranges_boundry]; - NSString *endingBoundryStr = [NSString stringWithFormat:@"\r\n--%@--\r\n", ranges_boundry]; - - UInt64 actualContentLength = 0; - - NSUInteger i; - for (i = 0; i < [ranges count]; i++) - { - DDRange range = [[ranges objectAtIndex:i] ddrangeValue]; - - NSString *rangeStr = [NSString stringWithFormat:@"%qu-%qu", range.location, DDMaxRange(range) - 1]; - NSString *contentRangeVal = [NSString stringWithFormat:@"bytes %@/%qu", rangeStr, contentLength]; - NSString *contentRangeStr = [NSString stringWithFormat:@"Content-Range: %@\r\n\r\n", contentRangeVal]; - - NSString *fullHeader = [startingBoundryStr stringByAppendingString:contentRangeStr]; - NSData *fullHeaderData = [fullHeader dataUsingEncoding:NSUTF8StringEncoding]; - - [ranges_headers addObject:fullHeaderData]; - - actualContentLength += [fullHeaderData length]; - actualContentLength += range.length; - } - - NSData *endingBoundryData = [endingBoundryStr dataUsingEncoding:NSUTF8StringEncoding]; - - actualContentLength += [endingBoundryData length]; - - NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", actualContentLength]; - [response setHeaderField:@"Content-Length" value:contentLengthStr]; - - NSString *contentTypeStr = [NSString stringWithFormat:@"multipart/byteranges; boundary=%@", ranges_boundry]; - [response setHeaderField:@"Content-Type" value:contentTypeStr]; - - return response; -} - -/** - * Returns the chunk size line that must precede each chunk of data when using chunked transfer encoding. - * This consists of the size of the data, in hexadecimal, followed by a CRLF. - **/ -- (NSData *)chunkedTransferSizeLineForLength:(NSUInteger)length -{ - return [[NSString stringWithFormat:@"%lx\r\n", (unsigned long)length] dataUsingEncoding:NSUTF8StringEncoding]; -} - -/** - * Returns the data that signals the end of a chunked transfer. - **/ -- (NSData *)chunkedTransferFooter -{ - // Each data chunk is preceded by a size line (in hex and including a CRLF), - // followed by the data itself, followed by another CRLF. - // After every data chunk has been sent, a zero size line is sent, - // followed by optional footer (which are just more headers), - // and followed by a CRLF on a line by itself. - - return [@"\r\n0\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]; -} - -- (void)sendResponseHeadersAndBody -{ - if ([httpResponse respondsToSelector:@selector(delayResponseHeaders)]) - { - if ([httpResponse delayResponseHeaders]) - { - return; - } - } - - BOOL isChunked = NO; - - if ([httpResponse respondsToSelector:@selector(isChunked)]) - { - isChunked = [httpResponse isChunked]; - } - - // If a response is "chunked", this simply means the HTTPResponse object - // doesn't know the content-length in advance. - - UInt64 contentLength = 0; - - if (!isChunked) - { - contentLength = [httpResponse contentLength]; - } - - // Check for specific range request - NSString *rangeHeader = [request headerField:@"Range"]; - - BOOL isRangeRequest = NO; - - // If the response is "chunked" then we don't know the exact content-length. - // This means we'll be unable to process any range requests. - // This is because range requests might include a range like "give me the last 100 bytes" - - if (!isChunked && rangeHeader) - { - if ([self parseRangeRequest:rangeHeader withContentLength:contentLength]) - { - isRangeRequest = YES; - } - } - - HTTPMessage *response; - - if (!isRangeRequest) - { - // Create response - // Default status code: 200 - OK - NSInteger status = 200; - - if ([httpResponse respondsToSelector:@selector(status)]) - { - status = [httpResponse status]; - } - response = [[HTTPMessage alloc] initResponseWithStatusCode:status description:nil version:HTTPVersion1_1]; - - if (isChunked) - { - [response setHeaderField:@"Transfer-Encoding" value:@"chunked"]; - } - else - { - NSString *contentLengthStr = [NSString stringWithFormat:@"%qu", contentLength]; - [response setHeaderField:@"Content-Length" value:contentLengthStr]; - } - } - else - { - if ([ranges count] == 1) - { - response = [self newUniRangeResponse:contentLength]; - } - else - { - response = [self newMultiRangeResponse:contentLength]; - } - } - - BOOL isZeroLengthResponse = !isChunked && (contentLength == 0); - - // If they issue a 'HEAD' command, we don't have to include the file - // If they issue a 'GET' command, we need to include the file - - if ([[request method] isEqualToString:@"HEAD"] || isZeroLengthResponse) - { - NSData *responseData = [self preprocessResponse:response]; - [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; - - sentResponseHeaders = YES; - } - else - { - // Write the header response - NSData *responseData = [self preprocessResponse:response]; - [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER]; - - sentResponseHeaders = YES; - - // Now we need to send the body of the response - if (!isRangeRequest) - { - // Regular request - NSData *data = [httpResponse readDataOfLength:READ_CHUNKSIZE]; - - if ([data length] > 0) - { - [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; - - if (isChunked) - { - NSData *chunkSize = [self chunkedTransferSizeLineForLength:[data length]]; - [asyncSocket writeData:chunkSize withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_HEADER]; - - [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_CHUNKED_RESPONSE_BODY]; - - if ([httpResponse isDone]) - { - NSData *footer = [self chunkedTransferFooter]; - [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; - } - else - { - NSData *footer = [GCDAsyncSocket CRLFData]; - [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_FOOTER]; - } - } - else - { - long tag = [httpResponse isDone] ? HTTP_RESPONSE : HTTP_PARTIAL_RESPONSE_BODY; - [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; - } - } - } - else - { - // Client specified a byte range in request - - if ([ranges count] == 1) - { - // Client is requesting a single range - DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; - - [httpResponse setOffset:range.location]; - - NSUInteger bytesToRead = range.length < READ_CHUNKSIZE ? (NSUInteger)range.length : READ_CHUNKSIZE; - - NSData *data = [httpResponse readDataOfLength:bytesToRead]; - - if ([data length] > 0) - { - [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; - - long tag = [data length] == range.length ? HTTP_RESPONSE : HTTP_PARTIAL_RANGE_RESPONSE_BODY; - [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; - } - } - else - { - // Client is requesting multiple ranges - // We have to send each range using multipart/byteranges - - // Write range header - NSData *rangeHeaderData = [ranges_headers objectAtIndex:0]; - [asyncSocket writeData:rangeHeaderData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER]; - - // Start writing range body - DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; - - [httpResponse setOffset:range.location]; - - NSUInteger bytesToRead = range.length < READ_CHUNKSIZE ? (NSUInteger)range.length : READ_CHUNKSIZE; - - NSData *data = [httpResponse readDataOfLength:bytesToRead]; - - if ([data length] > 0) - { - [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; - - [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_PARTIAL_RANGES_RESPONSE_BODY]; - } - } - } - } - -} - -/** - * Returns the number of bytes of the http response body that are sitting in asyncSocket's write queue. - * - * We keep track of this information in order to keep our memory footprint low while - * working with asynchronous HTTPResponse objects. - **/ -- (NSUInteger)writeQueueSize -{ - NSUInteger result = 0; - - NSUInteger i; - for(i = 0; i < [responseDataSizes count]; i++) - { - result += [[responseDataSizes objectAtIndex:i] unsignedIntegerValue]; - } - - return result; -} - -/** - * Sends more data, if needed, without growing the write queue over its approximate size limit. - * The last chunk of the response body will be sent with a tag of HTTP_RESPONSE. - * - * This method should only be called for standard (non-range) responses. - **/ -- (void)continueSendingStandardResponseBody -{ - HTTPLogTrace(); - - // This method is called when either asyncSocket has finished writing one of the response data chunks, - // or when an asynchronous HTTPResponse object informs us that it has more available data for us to send. - // In the case of the asynchronous HTTPResponse, we don't want to blindly grab the new data, - // and shove it onto asyncSocket's write queue. - // Doing so could negatively affect the memory footprint of the application. - // Instead, we always ensure that we place no more than READ_CHUNKSIZE bytes onto the write queue. - // - // Note that this does not affect the rate at which the HTTPResponse object may generate data. - // The HTTPResponse is free to do as it pleases, and this is up to the application's developer. - // If the memory footprint is a concern, the developer creating the custom HTTPResponse object may freely - // use the calls to readDataOfLength as an indication to start generating more data. - // This provides an easy way for the HTTPResponse object to throttle its data allocation in step with the rate - // at which the socket is able to send it. - - NSUInteger writeQueueSize = [self writeQueueSize]; - - if(writeQueueSize >= READ_CHUNKSIZE) return; - - NSUInteger available = READ_CHUNKSIZE - writeQueueSize; - NSData *data = [httpResponse readDataOfLength:available]; - - if ([data length] > 0) - { - [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; - - BOOL isChunked = NO; - - if ([httpResponse respondsToSelector:@selector(isChunked)]) - { - isChunked = [httpResponse isChunked]; - } - - if (isChunked) - { - NSData *chunkSize = [self chunkedTransferSizeLineForLength:[data length]]; - [asyncSocket writeData:chunkSize withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_HEADER]; - - [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_CHUNKED_RESPONSE_BODY]; - - if([httpResponse isDone]) - { - NSData *footer = [self chunkedTransferFooter]; - [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; - } - else - { - NSData *footer = [GCDAsyncSocket CRLFData]; - [asyncSocket writeData:footer withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_CHUNKED_RESPONSE_FOOTER]; - } - } - else - { - long tag = [httpResponse isDone] ? HTTP_RESPONSE : HTTP_PARTIAL_RESPONSE_BODY; - [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; - } - } -} - -/** - * Sends more data, if needed, without growing the write queue over its approximate size limit. - * The last chunk of the response body will be sent with a tag of HTTP_RESPONSE. - * - * This method should only be called for single-range responses. - **/ -- (void)continueSendingSingleRangeResponseBody -{ - HTTPLogTrace(); - - // This method is called when either asyncSocket has finished writing one of the response data chunks, - // or when an asynchronous response informs us that is has more available data for us to send. - // In the case of the asynchronous response, we don't want to blindly grab the new data, - // and shove it onto asyncSocket's write queue. - // Doing so could negatively affect the memory footprint of the application. - // Instead, we always ensure that we place no more than READ_CHUNKSIZE bytes onto the write queue. - // - // Note that this does not affect the rate at which the HTTPResponse object may generate data. - // The HTTPResponse is free to do as it pleases, and this is up to the application's developer. - // If the memory footprint is a concern, the developer creating the custom HTTPResponse object may freely - // use the calls to readDataOfLength as an indication to start generating more data. - // This provides an easy way for the HTTPResponse object to throttle its data allocation in step with the rate - // at which the socket is able to send it. - - NSUInteger writeQueueSize = [self writeQueueSize]; - - if(writeQueueSize >= READ_CHUNKSIZE) return; - - DDRange range = [[ranges objectAtIndex:0] ddrangeValue]; - - UInt64 offset = [httpResponse offset]; - UInt64 bytesRead = offset - range.location; - UInt64 bytesLeft = range.length - bytesRead; - - if (bytesLeft > 0) - { - NSUInteger available = READ_CHUNKSIZE - writeQueueSize; - NSUInteger bytesToRead = bytesLeft < available ? (NSUInteger)bytesLeft : available; - - NSData *data = [httpResponse readDataOfLength:bytesToRead]; - - if ([data length] > 0) - { - [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; - - long tag = [data length] == bytesLeft ? HTTP_RESPONSE : HTTP_PARTIAL_RANGE_RESPONSE_BODY; - [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:tag]; - } - } -} - -/** - * Sends more data, if needed, without growing the write queue over its approximate size limit. - * The last chunk of the response body will be sent with a tag of HTTP_RESPONSE. - * - * This method should only be called for multi-range responses. - **/ -- (void)continueSendingMultiRangeResponseBody -{ - HTTPLogTrace(); - - // This method is called when either asyncSocket has finished writing one of the response data chunks, - // or when an asynchronous HTTPResponse object informs us that is has more available data for us to send. - // In the case of the asynchronous HTTPResponse, we don't want to blindly grab the new data, - // and shove it onto asyncSocket's write queue. - // Doing so could negatively affect the memory footprint of the application. - // Instead, we always ensure that we place no more than READ_CHUNKSIZE bytes onto the write queue. - // - // Note that this does not affect the rate at which the HTTPResponse object may generate data. - // The HTTPResponse is free to do as it pleases, and this is up to the application's developer. - // If the memory footprint is a concern, the developer creating the custom HTTPResponse object may freely - // use the calls to readDataOfLength as an indication to start generating more data. - // This provides an easy way for the HTTPResponse object to throttle its data allocation in step with the rate - // at which the socket is able to send it. - - NSUInteger writeQueueSize = [self writeQueueSize]; - - if(writeQueueSize >= READ_CHUNKSIZE) return; - - DDRange range = [[ranges objectAtIndex:rangeIndex] ddrangeValue]; - - UInt64 offset = [httpResponse offset]; - UInt64 bytesRead = offset - range.location; - UInt64 bytesLeft = range.length - bytesRead; - - if (bytesLeft > 0) - { - NSUInteger available = READ_CHUNKSIZE - writeQueueSize; - NSUInteger bytesToRead = bytesLeft < available ? (NSUInteger)bytesLeft : available; - - NSData *data = [httpResponse readDataOfLength:bytesToRead]; - - if ([data length] > 0) - { - [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; - - [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_PARTIAL_RANGES_RESPONSE_BODY]; - } - } - else - { - if (++rangeIndex < [ranges count]) - { - // Write range header - NSData *rangeHeader = [ranges_headers objectAtIndex:rangeIndex]; - [asyncSocket writeData:rangeHeader withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_PARTIAL_RESPONSE_HEADER]; - - // Start writing range body - range = [[ranges objectAtIndex:rangeIndex] ddrangeValue]; - - [httpResponse setOffset:range.location]; - - NSUInteger available = READ_CHUNKSIZE - writeQueueSize; - NSUInteger bytesToRead = range.length < available ? (NSUInteger)range.length : available; - - NSData *data = [httpResponse readDataOfLength:bytesToRead]; - - if ([data length] > 0) - { - [responseDataSizes addObject:[NSNumber numberWithUnsignedInteger:[data length]]]; - - [asyncSocket writeData:data withTimeout:TIMEOUT_WRITE_BODY tag:HTTP_PARTIAL_RANGES_RESPONSE_BODY]; - } - } - else - { - // We're not done yet - we still have to send the closing boundry tag - NSString *endingBoundryStr = [NSString stringWithFormat:@"\r\n--%@--\r\n", ranges_boundry]; - NSData *endingBoundryData = [endingBoundryStr dataUsingEncoding:NSUTF8StringEncoding]; - - [asyncSocket writeData:endingBoundryData withTimeout:TIMEOUT_WRITE_HEAD tag:HTTP_RESPONSE]; - } - } -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Responses -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * Returns an array of possible index pages. - * For example: {"index.html", "index.htm"} - **/ -- (NSArray *)directoryIndexFileNames -{ - HTTPLogTrace(); - - // Override me to support other index pages. - - return [NSArray arrayWithObjects:@"index.html", @"index.htm", nil]; -} - -- (NSString *)filePathForURI:(NSString *)path -{ - return [self filePathForURI:path allowDirectory:NO]; -} - -/** - * Converts relative URI path into full file-system path. - **/ -- (NSString *)filePathForURI:(NSString *)path allowDirectory:(BOOL)allowDirectory -{ - HTTPLogTrace(); - - // Override me to perform custom path mapping. - // For example you may want to use a default file other than index.html, or perhaps support multiple types. - - NSString *documentRoot = [config documentRoot]; - - // Part 0: Validate document root setting. - // - // If there is no configured documentRoot, - // then it makes no sense to try to return anything. - - if (documentRoot == nil) - { - HTTPLogWarn(@"%@[%p]: No configured document root", THIS_FILE, self); - return nil; - } - - // Part 1: Strip parameters from the url - // - // E.g.: /page.html?q=22&var=abc -> /page.html - - NSURL *docRoot = [NSURL fileURLWithPath:documentRoot isDirectory:YES]; - if (docRoot == nil) - { - HTTPLogWarn(@"%@[%p]: Document root is invalid file path", THIS_FILE, self); - return nil; - } - - NSString *relativePath = [[NSURL URLWithString:path relativeToURL:docRoot] relativePath]; - - // Part 2: Append relative path to document root (base path) - // - // E.g.: relativePath="/images/icon.png" - // documentRoot="/Users/robbie/Sites" - // fullPath="/Users/robbie/Sites/images/icon.png" - // - // We also standardize the path. - // - // E.g.: "Users/robbie/Sites/images/../index.html" -> "/Users/robbie/Sites/index.html" - - NSString *fullPath = [[documentRoot stringByAppendingPathComponent:relativePath] stringByStandardizingPath]; - - if ([relativePath isEqualToString:@"/"]) - { - fullPath = [fullPath stringByAppendingString:@"/"]; - } - - // Part 3: Prevent serving files outside the document root. - // - // Sneaky requests may include ".." in the path. - // - // E.g.: relativePath="../Documents/TopSecret.doc" - // documentRoot="/Users/robbie/Sites" - // fullPath="/Users/robbie/Documents/TopSecret.doc" - // - // E.g.: relativePath="../Sites_Secret/TopSecret.doc" - // documentRoot="/Users/robbie/Sites" - // fullPath="/Users/robbie/Sites_Secret/TopSecret" - - if (![documentRoot hasSuffix:@"/"]) - { - documentRoot = [documentRoot stringByAppendingString:@"/"]; - } - - if (![fullPath hasPrefix:documentRoot]) - { - HTTPLogWarn(@"%@[%p]: Request for file outside document root", THIS_FILE, self); - return nil; - } - - // Part 4: Search for index page if path is pointing to a directory - if (!allowDirectory) - { - BOOL isDir = NO; - if ([[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDir] && isDir) - { - NSArray *indexFileNames = [self directoryIndexFileNames]; - - for (NSString *indexFileName in indexFileNames) - { - NSString *indexFilePath = [fullPath stringByAppendingPathComponent:indexFileName]; - - if ([[NSFileManager defaultManager] fileExistsAtPath:indexFilePath isDirectory:&isDir] && !isDir) - { - return indexFilePath; - } - } - - // No matching index files found in directory - return nil; - } - } - - return fullPath; -} - -/** - * This method is called to get a response for a request. - * You may return any object that adopts the HTTPResponse protocol. - * The HTTPServer comes with two such classes: HTTPFileResponse and HTTPDataResponse. - * HTTPFileResponse is a wrapper for an NSFileHandle object, and is the preferred way to send a file response. - * HTTPDataResponse is a wrapper for an NSData object, and may be used to send a custom response. - **/ -- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path -{ - HTTPLogTrace(); - - // Override me to provide custom responses. - - return nil; -} - -- (WebSocket *)webSocketForURI:(NSString *)path -{ - HTTPLogTrace(); - - // Override me to provide custom WebSocket responses. - // To do so, simply override the base WebSocket implementation, and add your custom functionality. - // Then return an instance of your custom WebSocket here. - // - // For example: - // - // if ([path isEqualToString:@"/myAwesomeWebSocketStream"]) - // { - // return [[[MyWebSocket alloc] initWithRequest:request socket:asyncSocket] autorelease]; - // } - // - // return [super webSocketForURI:path]; - - return nil; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Uploads -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This method is called after receiving all HTTP headers, but before reading any of the request body. - **/ -- (void)prepareForBodyWithSize:(UInt64)contentLength -{ - // Override me to allocate buffers, file handles, etc. -} - -/** - * This method is called to handle data read from a POST / PUT. - * The given data is part of the request body. - **/ -- (void)processBodyData:(NSData *)postDataChunk -{ - // Override me to do something useful with a POST / PUT. - // If the post is small, such as a simple form, you may want to simply append the data to the request. - // If the post is big, such as a file upload, you may want to store the file to disk. - // - // Remember: In order to support LARGE POST uploads, the data is read in chunks. - // This prevents a 50 MB upload from being stored in RAM. - // The size of the chunks are limited by the POST_CHUNKSIZE definition. - // Therefore, this method may be called multiple times for the same POST request. -} - -/** - * This method is called after the request body has been fully read but before the HTTP request is processed. - **/ -- (void)finishBody -{ - // Override me to perform any final operations on an upload. - // For example, if you were saving the upload to disk this would be - // the hook to flush any pending data to disk and maybe close the file. -} - -/** - * Returns the maximum request body size this connection accepts. - **/ -- (UInt64)maxRequestBodySize -{ - return (UInt64)-1; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Errors -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * Called if the HTML version is other than what is supported - **/ -- (void)handleVersionNotSupported:(NSString *)version -{ - // Override me for custom error handling of unsupported http version responses - // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. - // You can also use preprocessErrorResponse: to add an optional HTML body. - - HTTPLogWarn(@"HTTP Server: Error 505 - Version Not Supported: %@ (%@)", version, [self requestURI]); - - HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:505 description:nil version:HTTPVersion1_1]; - [response setHeaderField:@"Content-Length" value:@"0"]; - - NSData *responseData = [self preprocessErrorResponse:response]; - [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_RESPONSE]; - -} - -/** - * Called if the HTTP request body is larger than the configured limit. - **/ -- (void)handleRequestBodyTooLarge -{ - HTTPLogWarn(@"HTTP Server: Error 413 - Request Entity Too Large (%@)", [self requestURI]); - - HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:413 description:nil version:HTTPVersion1_1]; - [response setHeaderField:@"Content-Length" value:@"0"]; - [response setHeaderField:@"Connection" value:@"close"]; - - NSData *responseData = [self preprocessErrorResponse:response]; - [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_FINAL_RESPONSE]; -} - -/** - * Called if we receive some sort of malformed HTTP request. - * The data parameter is the invalid HTTP header line, including CRLF, as read from GCDAsyncSocket. - * The data parameter may also be nil if the request as a whole was invalid, such as a POST with no Content-Length. - **/ -- (void)handleInvalidRequest:(NSData *)data -{ - // Override me for custom error handling of invalid HTTP requests - // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. - // You can also use preprocessErrorResponse: to add an optional HTML body. - - HTTPLogWarn(@"HTTP Server: Error 400 - Bad Request (%@)", [self requestURI]); - - // Status Code 400 - Bad Request - HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:400 description:nil version:HTTPVersion1_1]; - [response setHeaderField:@"Content-Length" value:@"0"]; - [response setHeaderField:@"Connection" value:@"close"]; - - NSData *responseData = [self preprocessErrorResponse:response]; - [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_FINAL_RESPONSE]; - - - // Note: We used the HTTP_FINAL_RESPONSE tag to disconnect after the response is sent. - // We do this because we couldn't parse the request, - // so we won't be able to recover and move on to another request afterwards. - // In other words, we wouldn't know where the first request ends and the second request begins. -} - -/** - * Called if we receive a HTTP request with a method other than GET or HEAD. - **/ -- (void)handleUnknownMethod:(NSString *)method -{ - // Override me for custom error handling of 405 method not allowed responses. - // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. - // You can also use preprocessErrorResponse: to add an optional HTML body. - // - // See also: supportsMethod:atPath: - - HTTPLogWarn(@"HTTP Server: Error 405 - Method Not Allowed: %@ (%@)", method, [self requestURI]); - - // Status code 405 - Method Not Allowed - HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:405 description:nil version:HTTPVersion1_1]; - [response setHeaderField:@"Content-Length" value:@"0"]; - [response setHeaderField:@"Connection" value:@"close"]; - - NSData *responseData = [self preprocessErrorResponse:response]; - [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_FINAL_RESPONSE]; - - - // Note: We used the HTTP_FINAL_RESPONSE tag to disconnect after the response is sent. - // We do this because the method may include an http body. - // Since we can't be sure, we should close the connection. -} - -/** - * Called if we're unable to find the requested resource. - **/ -- (void)handleResourceNotFound -{ - // Override me for custom error handling of 404 not found responses - // If you simply want to add a few extra header fields, see the preprocessErrorResponse: method. - // You can also use preprocessErrorResponse: to add an optional HTML body. - - HTTPLogInfo(@"HTTP Server: Error 404 - Not Found (%@)", [self requestURI]); - - // Status Code 404 - Not Found - HTTPMessage *response = [[HTTPMessage alloc] initResponseWithStatusCode:404 description:nil version:HTTPVersion1_1]; - [response setHeaderField:@"Content-Length" value:@"0"]; - - NSData *responseData = [self preprocessErrorResponse:response]; - [asyncSocket writeData:responseData withTimeout:TIMEOUT_WRITE_ERROR tag:HTTP_RESPONSE]; - -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Headers -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * Gets the current date and time, formatted properly (according to RFC) for insertion into an HTTP header. - **/ -- (NSString *)dateAsString:(NSDate *)date -{ - // From Apple's Documentation (Data Formatting Guide -> Date Formatters -> Cache Formatters for Efficiency): - // - // "Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, - // it is typically more efficient to cache a single instance than to create and dispose of multiple instances. - // One approach is to use a static variable." - // - // This was discovered to be true in massive form via issue #46: - // - // "Was doing some performance benchmarking using instruments and httperf. Using this single optimization - // I got a 26% speed improvement - from 1000req/sec to 3800req/sec. Not insignificant. - // The culprit? Why, NSDateFormatter, of course!" - // - // Thus, we are using a static NSDateFormatter here. - - static NSDateFormatter *df; - - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - - // Example: Sun, 06 Nov 1994 08:49:37 GMT - - df = [[NSDateFormatter alloc] init]; - [df setFormatterBehavior:NSDateFormatterBehavior10_4]; - [df setTimeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"]]; - [df setDateFormat:@"EEE, dd MMM y HH:mm:ss 'GMT'"]; - [df setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]]; - - // For some reason, using zzz in the format string produces GMT+00:00 - }); - - return [df stringFromDate:date]; -} - -/** - * This method is called immediately prior to sending the response headers. - * This method adds standard header fields, and then converts the response to an NSData object. - **/ -- (NSData *)preprocessResponse:(HTTPMessage *)response -{ - HTTPLogTrace(); - - // Override me to customize the response headers - // You'll likely want to add your own custom headers, and then return [super preprocessResponse:response] - - // Add standard headers - NSString *now = [self dateAsString:[NSDate date]]; - [response setHeaderField:@"Date" value:now]; - - // Add server capability headers - [response setHeaderField:@"Accept-Ranges" value:@"bytes"]; - - // Add optional response headers - if ([httpResponse respondsToSelector:@selector(httpHeaders)]) - { - NSDictionary *responseHeaders = [httpResponse httpHeaders]; - - NSEnumerator *keyEnumerator = [responseHeaders keyEnumerator]; - NSString *key; - - while ((key = [keyEnumerator nextObject])) - { - NSString *value = [responseHeaders objectForKey:key]; - - [response setHeaderField:key value:value]; - } - } - - return [response messageData]; -} - -/** - * This method is called immediately prior to sending the response headers (for an error). - * This method adds standard header fields, and then converts the response to an NSData object. - **/ -- (NSData *)preprocessErrorResponse:(HTTPMessage *)response -{ - HTTPLogTrace(); - - // Override me to customize the error response headers - // You'll likely want to add your own custom headers, and then return [super preprocessErrorResponse:response] - // - // Notes: - // You can use [response statusCode] to get the type of error. - // You can use [response setBody:data] to add an optional HTML body. - // If you add a body, don't forget to update the Content-Length. - // - // if ([response statusCode] == 404) - // { - // NSString *msg = @"Error 404 - Not Found"; - // NSData *msgData = [msg dataUsingEncoding:NSUTF8StringEncoding]; - // - // [response setBody:msgData]; - // - // NSString *contentLengthStr = [NSString stringWithFormat:@"%lu", (unsigned long)[msgData length]]; - // [response setHeaderField:@"Content-Length" value:contentLengthStr]; - // } - - // Add standard headers - NSString *now = [self dateAsString:[NSDate date]]; - [response setHeaderField:@"Date" value:now]; - - // Add server capability headers - [response setHeaderField:@"Accept-Ranges" value:@"bytes"]; - - // Add optional response headers - if ([httpResponse respondsToSelector:@selector(httpHeaders)]) - { - NSDictionary *responseHeaders = [httpResponse httpHeaders]; - - NSEnumerator *keyEnumerator = [responseHeaders keyEnumerator]; - NSString *key; - - while((key = [keyEnumerator nextObject])) - { - NSString *value = [responseHeaders objectForKey:key]; - - [response setHeaderField:key value:value]; - } - } - - return [response messageData]; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark GCDAsyncSocket Delegate -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This method is called after the socket has successfully read data from the stream. - * Remember that this method will only be called after the socket reaches a CRLF, or after it's read the proper length. - **/ -- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData*)data withTag:(long)tag -{ - if (tag == HTTP_REQUEST_HEADER) - { - // Append the header line to the http message - BOOL result = [request appendData:data]; - if (!result) - { - HTTPLogWarn(@"%@[%p]: Malformed request", THIS_FILE, self); - - [self handleInvalidRequest:data]; - } - else if (![request isHeaderComplete]) - { - // We don't have a complete header yet - // That is, we haven't yet received a CRLF on a line by itself, indicating the end of the header - if (++numHeaderLines > MAX_HEADER_LINES) - { - // Reached the maximum amount of header lines in a single HTTP request - // This could be an attempted DOS attack - [asyncSocket disconnect]; - - // Explictly return to ensure we don't do anything after the socket disconnect - return; - } - else - { - [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] - withTimeout:TIMEOUT_READ_SUBSEQUENT_HEADER_LINE - maxLength:MAX_HEADER_LINE_LENGTH - tag:HTTP_REQUEST_HEADER]; - } - } - else - { - // We have an entire HTTP request header from the client - - // Extract the method (such as GET, HEAD, POST, etc) - NSString *method = [request method]; - - // Extract the uri (such as "/index.html") - NSString *uri = [self requestURI]; - - // Check for a Transfer-Encoding field - NSString *transferEncoding = [request headerField:@"Transfer-Encoding"]; - - // Check for a Content-Length field - NSString *contentLength = [request headerField:@"Content-Length"]; - - // Content-Length MUST be present for upload methods (such as POST or PUT) - // and MUST NOT be present for other methods. - BOOL expectsUpload = [self expectsRequestBodyFromMethod:method atPath:uri]; - - if (expectsUpload) - { - if (transferEncoding && ![transferEncoding caseInsensitiveCompare:@"Chunked"]) - { - requestContentLength = -1; - } - else - { - if (contentLength == nil) - { - HTTPLogWarn(@"%@[%p]: Method expects request body, but had no specified Content-Length", - THIS_FILE, self); - - [self handleInvalidRequest:nil]; - return; - } - - if (![NSNumber parseString:(NSString *)contentLength intoUInt64:&requestContentLength]) - { - HTTPLogWarn(@"%@[%p]: Unable to parse Content-Length header into a valid number", - THIS_FILE, self); - - [self handleInvalidRequest:nil]; - return; - } - - if (requestContentLength > [self maxRequestBodySize]) - { - HTTPLogWarn(@"%@[%p]: Request body size %llu exceeds the configured limit %llu", - THIS_FILE, self, requestContentLength, [self maxRequestBodySize]); - - [self handleRequestBodyTooLarge]; - return; - } - } - } - else - { - if (contentLength != nil) - { - // Received Content-Length header for method not expecting an upload. - // This better be zero... - - if (![NSNumber parseString:(NSString *)contentLength intoUInt64:&requestContentLength]) - { - HTTPLogWarn(@"%@[%p]: Unable to parse Content-Length header into a valid number", - THIS_FILE, self); - - [self handleInvalidRequest:nil]; - return; - } - - if (requestContentLength > 0) - { - HTTPLogWarn(@"%@[%p]: Method not expecting request body had non-zero Content-Length", - THIS_FILE, self); - - [self handleInvalidRequest:nil]; - return; - } - } - - requestContentLength = 0; - requestContentLengthReceived = 0; - } - - // Check to make sure the given method is supported - if (![self supportsMethod:method atPath:uri]) - { - // The method is unsupported - either in general, or for this specific request - // Send a 405 - Method not allowed response - [self handleUnknownMethod:method]; - return; - } - - if (expectsUpload) - { - // Reset the total amount of data received for the upload - requestContentLengthReceived = 0; - - // Prepare for the upload - [self prepareForBodyWithSize:requestContentLength]; - - if (requestContentLength > 0) - { - // Start reading the request body - if (requestContentLength == -1) - { - // Chunked transfer - - [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] - withTimeout:TIMEOUT_READ_BODY - maxLength:MAX_CHUNK_LINE_LENGTH - tag:HTTP_REQUEST_CHUNK_SIZE]; - } - else - { - NSUInteger bytesToRead; - if (requestContentLength < POST_CHUNKSIZE) - bytesToRead = (NSUInteger)requestContentLength; - else - bytesToRead = POST_CHUNKSIZE; - - [asyncSocket readDataToLength:bytesToRead - withTimeout:TIMEOUT_READ_BODY - tag:HTTP_REQUEST_BODY]; - } - } - else - { - // Empty upload - [self finishBody]; - [self replyToHTTPRequest]; - } - } - else - { - // Now we need to reply to the request - [self replyToHTTPRequest]; - } - } - } - else - { - BOOL doneReadingRequest = NO; - - // A chunked message body contains a series of chunks, - // followed by a line with "0" (zero), - // followed by optional footers (just like headers), - // and a blank line. - // - // Each chunk consists of two parts: - // - // 1. A line with the size of the chunk data, in hex, - // possibly followed by a semicolon and extra parameters you can ignore (none are currently standard), - // and ending with CRLF. - // 2. The data itself, followed by CRLF. - // - // Part 1 is represented by HTTP_REQUEST_CHUNK_SIZE - // Part 2 is represented by HTTP_REQUEST_CHUNK_DATA and HTTP_REQUEST_CHUNK_TRAILER - // where the trailer is the CRLF that follows the data. - // - // The optional footers and blank line are represented by HTTP_REQUEST_CHUNK_FOOTER. - - if (tag == HTTP_REQUEST_CHUNK_SIZE) - { - // We have just read in a line with the size of the chunk data, in hex, - // possibly followed by a semicolon and extra parameters that can be ignored, - // and ending with CRLF. - - NSString *sizeLine = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - - errno = 0; // Reset errno before calling strtoull() to ensure it is always zero on success - requestChunkSize = (UInt64)strtoull([sizeLine UTF8String], NULL, 16); - requestChunkSizeReceived = 0; - - if (errno != 0) - { - HTTPLogWarn(@"%@[%p]: Method expects chunk size, but received something else", THIS_FILE, self); - - [self handleInvalidRequest:nil]; - return; - } - - if (requestChunkSize > 0) - { - UInt64 maxRequestBodySize = [self maxRequestBodySize]; - if (requestChunkSize > maxRequestBodySize || - requestContentLengthReceived > maxRequestBodySize - requestChunkSize) - { - HTTPLogWarn(@"%@[%p]: Chunked request body exceeds the configured limit %llu", - THIS_FILE, self, maxRequestBodySize); - - [self handleRequestBodyTooLarge]; - return; - } - - NSUInteger bytesToRead; - bytesToRead = (requestChunkSize < POST_CHUNKSIZE) ? (NSUInteger)requestChunkSize : POST_CHUNKSIZE; - - [asyncSocket readDataToLength:bytesToRead - withTimeout:TIMEOUT_READ_BODY - tag:HTTP_REQUEST_CHUNK_DATA]; - } - else - { - // This is the "0" (zero) line, - // which is to be followed by optional footers (just like headers) and finally a blank line. - - [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] - withTimeout:TIMEOUT_READ_BODY - maxLength:MAX_HEADER_LINE_LENGTH - tag:HTTP_REQUEST_CHUNK_FOOTER]; - } - - return; - } - else if (tag == HTTP_REQUEST_CHUNK_DATA) - { - // We just read part of the actual data. - - requestContentLengthReceived += [data length]; - requestChunkSizeReceived += [data length]; - - [self processBodyData:data]; - - UInt64 bytesLeft = requestChunkSize - requestChunkSizeReceived; - if (bytesLeft > 0) - { - NSUInteger bytesToRead = (bytesLeft < POST_CHUNKSIZE) ? (NSUInteger)bytesLeft : POST_CHUNKSIZE; - - [asyncSocket readDataToLength:bytesToRead - withTimeout:TIMEOUT_READ_BODY - tag:HTTP_REQUEST_CHUNK_DATA]; - } - else - { - // We've read in all the data for this chunk. - // The data is followed by a CRLF, which we need to read (and basically ignore) - - [asyncSocket readDataToLength:2 - withTimeout:TIMEOUT_READ_BODY - tag:HTTP_REQUEST_CHUNK_TRAILER]; - } - - return; - } - else if (tag == HTTP_REQUEST_CHUNK_TRAILER) - { - // This should be the CRLF following the data. - // Just ensure it's a CRLF. - - if (![data isEqualToData:[GCDAsyncSocket CRLFData]]) - { - HTTPLogWarn(@"%@[%p]: Method expects chunk trailer, but is missing", THIS_FILE, self); - - [self handleInvalidRequest:nil]; - return; - } - - // Now continue with the next chunk - - [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] - withTimeout:TIMEOUT_READ_BODY - maxLength:MAX_CHUNK_LINE_LENGTH - tag:HTTP_REQUEST_CHUNK_SIZE]; - - } - else if (tag == HTTP_REQUEST_CHUNK_FOOTER) - { - if (++numHeaderLines > MAX_HEADER_LINES) - { - // Reached the maximum amount of header lines in a single HTTP request - // This could be an attempted DOS attack - [asyncSocket disconnect]; - - // Explictly return to ensure we don't do anything after the socket disconnect - return; - } - - if ([data length] > 2) - { - // We read in a footer. - // In the future we may want to append these to the request. - // For now we ignore, and continue reading the footers, waiting for the final blank line. - - [asyncSocket readDataToData:[GCDAsyncSocket CRLFData] - withTimeout:TIMEOUT_READ_BODY - maxLength:MAX_HEADER_LINE_LENGTH - tag:HTTP_REQUEST_CHUNK_FOOTER]; - } - else - { - doneReadingRequest = YES; - } - } - else // HTTP_REQUEST_BODY - { - // Handle a chunk of data from the POST body - - requestContentLengthReceived += [data length]; - [self processBodyData:data]; - - if (requestContentLengthReceived < requestContentLength) - { - // We're not done reading the post body yet... - - UInt64 bytesLeft = requestContentLength - requestContentLengthReceived; - - NSUInteger bytesToRead = bytesLeft < POST_CHUNKSIZE ? (NSUInteger)bytesLeft : POST_CHUNKSIZE; - - [asyncSocket readDataToLength:bytesToRead - withTimeout:TIMEOUT_READ_BODY - tag:HTTP_REQUEST_BODY]; - } - else - { - doneReadingRequest = YES; - } - } - - // Now that the entire body has been received, we need to reply to the request - - if (doneReadingRequest) - { - [self finishBody]; - [self replyToHTTPRequest]; - } - } -} - -/** - * This method is called after the socket has successfully written data to the stream. - **/ -- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag -{ - BOOL doneSendingResponse = NO; - - if (tag == HTTP_PARTIAL_RESPONSE_BODY) - { - // Update the amount of data we have in asyncSocket's write queue - if ([responseDataSizes count] > 0) { - [responseDataSizes removeObjectAtIndex:0]; - } - - // We only wrote a part of the response - there may be more - [self continueSendingStandardResponseBody]; - } - else if (tag == HTTP_CHUNKED_RESPONSE_BODY) - { - // Update the amount of data we have in asyncSocket's write queue. - // This will allow asynchronous responses to continue sending more data. - if ([responseDataSizes count] > 0) { - [responseDataSizes removeObjectAtIndex:0]; - } - // Don't continue sending the response yet. - // The chunked footer that was sent after the body will tell us if we have more data to send. - } - else if (tag == HTTP_CHUNKED_RESPONSE_FOOTER) - { - // Normal chunked footer indicating we have more data to send (non final footer). - [self continueSendingStandardResponseBody]; - } - else if (tag == HTTP_PARTIAL_RANGE_RESPONSE_BODY) - { - // Update the amount of data we have in asyncSocket's write queue - if ([responseDataSizes count] > 0) { - [responseDataSizes removeObjectAtIndex:0]; - } - // We only wrote a part of the range - there may be more - [self continueSendingSingleRangeResponseBody]; - } - else if (tag == HTTP_PARTIAL_RANGES_RESPONSE_BODY) - { - // Update the amount of data we have in asyncSocket's write queue - if ([responseDataSizes count] > 0) { - [responseDataSizes removeObjectAtIndex:0]; - } - // We only wrote part of the range - there may be more, or there may be more ranges - [self continueSendingMultiRangeResponseBody]; - } - else if (tag == HTTP_RESPONSE || tag == HTTP_FINAL_RESPONSE) - { - // Update the amount of data we have in asyncSocket's write queue - if ([responseDataSizes count] > 0) - { - [responseDataSizes removeObjectAtIndex:0]; - } - - doneSendingResponse = YES; - } - - if (doneSendingResponse) - { - // Inform the http response that we're done - if ([httpResponse respondsToSelector:@selector(connectionDidClose)]) - { - [httpResponse connectionDidClose]; - } - - - if (tag == HTTP_FINAL_RESPONSE) - { - // Cleanup after the last request - [self finishResponse]; - - // Terminate the connection - [asyncSocket disconnect]; - - // Explictly return to ensure we don't do anything after the socket disconnects - return; - } - else - { - if ([self shouldDie]) - { - // Cleanup after the last request - // Note: Don't do this before calling shouldDie, as it needs the request object still. - [self finishResponse]; - - // The only time we should invoke [self die] is from socketDidDisconnect, - // or if the socket gets taken over by someone else like a WebSocket. - - [asyncSocket disconnect]; - } - else - { - // Cleanup after the last request - [self finishResponse]; - - // Prepare for the next request - - // If this assertion fails, it likely means you overrode the - // finishBody method and forgot to call [super finishBody]. - NSAssert(request == nil, @"Request not properly released in finishBody"); - - request = [[HTTPMessage alloc] initEmptyRequest]; - - numHeaderLines = 0; - sentResponseHeaders = NO; - - // And start listening for more requests - [self startReadingRequest]; - } - } - } -} - -/** - * Sent after the socket has been disconnected. - **/ -- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err -{ - HTTPLogTrace(); - - asyncSocket = nil; - - [self die]; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark HTTPResponse Notifications -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This method may be called by asynchronous HTTPResponse objects. - * That is, HTTPResponse objects that return YES in their "- (BOOL)isAsynchronous" method. - * - * This informs us that the response object has generated more data that we may be able to send. - **/ -- (void)responseHasAvailableData:(NSObject *)sender -{ - HTTPLogTrace(); - - // We always dispatch this asynchronously onto our connectionQueue, - // even if the connectionQueue is the current queue. - // - // We do this to give the HTTPResponse classes the flexibility to call - // this method whenever they want, even from within a readDataOfLength method. - - dispatch_async(connectionQueue, ^{ @autoreleasepool { - - if (sender != httpResponse) - { - HTTPLogWarn(@"%@[%p]: %@ - Sender is not current httpResponse", THIS_FILE, self, THIS_METHOD); - return; - } - - if (!sentResponseHeaders) - { - [self sendResponseHeadersAndBody]; - } - else - { - if (ranges == nil) - { - [self continueSendingStandardResponseBody]; - } - else - { - if ([ranges count] == 1) - [self continueSendingSingleRangeResponseBody]; - else - [self continueSendingMultiRangeResponseBody]; - } - } - }}); -} - -/** - * This method is called if the response encounters some critical error, - * and it will be unable to fullfill the request. - **/ -- (void)responseDidAbort:(NSObject *)sender -{ - HTTPLogTrace(); - - // We always dispatch this asynchronously onto our connectionQueue, - // even if the connectionQueue is the current queue. - // - // We do this to give the HTTPResponse classes the flexibility to call - // this method whenever they want, even from within a readDataOfLength method. - - dispatch_async(connectionQueue, ^{ @autoreleasepool { - - if (sender != httpResponse) - { - HTTPLogWarn(@"%@[%p]: %@ - Sender is not current httpResponse", THIS_FILE, self, THIS_METHOD); - return; - } - - [asyncSocket disconnectAfterWriting]; - }}); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Post Request -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This method is called after each response has been fully sent. - * Since a single connection may handle multiple request/responses, this method may be called multiple times. - * That is, it will be called after completion of each response. - **/ -- (void)finishResponse -{ - HTTPLogTrace(); - - // Override me if you want to perform any custom actions after a response has been fully sent. - // This is the place to release memory or resources associated with the last request. - // - // If you override this method, you should take care to invoke [super finishResponse] at some point. - - request = nil; - - httpResponse = nil; - - ranges = nil; - ranges_headers = nil; - ranges_boundry = nil; -} - -/** - * This method is called after each successful response has been fully sent. - * It determines whether the connection should stay open and handle another request. - **/ -- (BOOL)shouldDie -{ - HTTPLogTrace(); - - // Override me if you have any need to force close the connection. - // You may do so by simply returning YES. - // - // If you override this method, you should take care to fall through with [super shouldDie] - // instead of returning NO. - - - BOOL shouldDie = NO; - - NSString *version = [request version]; - if ([version isEqualToString:HTTPVersion1_1]) - { - // HTTP version 1.1 - // Connection should only be closed if request included "Connection: close" header - - NSString *connection = [request headerField:@"Connection"]; - - shouldDie = (connection && ([connection caseInsensitiveCompare:@"close"] == NSOrderedSame)); - } - else if ([version isEqualToString:HTTPVersion1_0]) - { - // HTTP version 1.0 - // Connection should be closed unless request included "Connection: Keep-Alive" header - - NSString *connection = [request headerField:@"Connection"]; - - if (connection == nil) - shouldDie = YES; - else - shouldDie = [connection caseInsensitiveCompare:@"Keep-Alive"] != NSOrderedSame; - } - - return shouldDie; -} - -- (void)die -{ - HTTPLogTrace(); - - // Override me if you want to perform any custom actions when a connection is closed. - // Then call [super die] when you're done. - // - // See also the finishResponse method. - // - // Important: There is a rare timing condition where this method might get invoked twice. - // If you override this method, you should be prepared for this situation. - - // Inform the http response that we're done - if ([httpResponse respondsToSelector:@selector(connectionDidClose)]) - { - [httpResponse connectionDidClose]; - } - - // Release the http response so we don't call it's connectionDidClose method again in our dealloc method - httpResponse = nil; - - // Post notification of dead connection - // This will allow our server to release us from its array of connections - [[NSNotificationCenter defaultCenter] postNotificationName:HTTPConnectionDidDieNotification object:self]; -} - -@end - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@implementation HTTPConfig - -@synthesize server; -@synthesize documentRoot; -@synthesize queue; - -- (id)initWithServer:(HTTPServer *)aServer documentRoot:(NSString *)aDocumentRoot -{ - if ((self = [super init])) - { - server = aServer; - documentRoot = aDocumentRoot; - } - return self; -} - -- (id)initWithServer:(HTTPServer *)aServer documentRoot:(NSString *)aDocumentRoot queue:(dispatch_queue_t)q -{ - if ((self = [super init])) - { - server = aServer; - - documentRoot = [aDocumentRoot stringByStandardizingPath]; - if ([documentRoot hasSuffix:@"/"]) - { - documentRoot = [documentRoot stringByAppendingString:@"/"]; - } - - if (q) - { - queue = q; -#if !OS_OBJECT_USE_OBJC - dispatch_retain(queue); -#endif - } - } - return self; -} - -- (void)dealloc -{ -#if !OS_OBJECT_USE_OBJC - if (queue) dispatch_release(queue); -#endif -} - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h deleted file mode 100644 index 4c277f1db4..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h +++ /dev/null @@ -1,122 +0,0 @@ -/** - * In order to provide fast and flexible logging, this project uses Cocoa Lumberjack. - * - * The Google Code page has a wealth of documentation if you have any questions. - * https://github.com/robbiehanson/CocoaLumberjack - * - * Here's what you need to know concerning how logging is setup for CocoaHTTPServer: - * - * There are 4 log levels: - * - Error - * - Warning - * - Info - * - Verbose - * - * In addition to this, there is a Trace flag that can be enabled. - * When tracing is enabled, it spits out the methods that are being called. - * - * Please note that tracing is separate from the log levels. - * For example, one could set the log level to warning, and enable tracing. - * - * All logging is asynchronous, except errors. - * To use logging within your own custom files, follow the steps below. - * - * Step 1: - * Import this header in your implementation file: - * - * #import "HTTPLogging.h" - * - * Step 2: - * Define your logging level in your implementation file: - * - * // Log levels: off, error, warn, info, verbose - * static const int httpLogLevel = HTTP_LOG_LEVEL_VERBOSE; - * - * If you wish to enable tracing, you could do something like this: - * - * // Debug levels: off, error, warn, info, verbose - * static const int httpLogLevel = HTTP_LOG_LEVEL_INFO | HTTP_LOG_FLAG_TRACE; - * - * Step 3: - * Replace your NSLog statements with HTTPLog statements according to the severity of the message. - * - * NSLog(@"Fatal error, no dohickey found!"); -> HTTPLogError(@"Fatal error, no dohickey found!"); - * - * HTTPLog works exactly the same as NSLog. - * This means you can pass it multiple variables just like NSLog. - **/ - -// Define logging context for every log message coming from the HTTP server. -// The logging context can be extracted from the DDLogMessage from within the logging framework, -// which gives loggers, formatters, and filters the ability to optionally process them differently. - -#define HTTP_LOG_CONTEXT 80 - -// Configure log levels. - -#define HTTP_LOG_FLAG_ERROR (1 << 0) // 0...00001 -#define HTTP_LOG_FLAG_WARN (1 << 1) // 0...00010 -#define HTTP_LOG_FLAG_INFO (1 << 2) // 0...00100 -#define HTTP_LOG_FLAG_VERBOSE (1 << 3) // 0...01000 - -#define HTTP_LOG_LEVEL_OFF 0 // 0...00000 -#define HTTP_LOG_LEVEL_ERROR (HTTP_LOG_LEVEL_OFF | HTTP_LOG_FLAG_ERROR) // 0...00001 -#define HTTP_LOG_LEVEL_WARN (HTTP_LOG_LEVEL_ERROR | HTTP_LOG_FLAG_WARN) // 0...00011 -#define HTTP_LOG_LEVEL_INFO (HTTP_LOG_LEVEL_WARN | HTTP_LOG_FLAG_INFO) // 0...00111 -#define HTTP_LOG_LEVEL_VERBOSE (HTTP_LOG_LEVEL_INFO | HTTP_LOG_FLAG_VERBOSE) // 0...01111 - -// Setup fine grained logging. -// The first 4 bits are being used by the standard log levels (0 - 3) -// -// We're going to add tracing, but NOT as a log level. -// Tracing can be turned on and off independently of log level. - -#define HTTP_LOG_FLAG_TRACE (1 << 4) // 0...10000 - -// Setup the usual boolean macros. - -#define HTTP_LOG_ERROR (httpLogLevel & HTTP_LOG_FLAG_ERROR) -#define HTTP_LOG_WARN (httpLogLevel & HTTP_LOG_FLAG_WARN) -#define HTTP_LOG_INFO (httpLogLevel & HTTP_LOG_FLAG_INFO) -#define HTTP_LOG_VERBOSE (httpLogLevel & HTTP_LOG_FLAG_VERBOSE) -#define HTTP_LOG_TRACE (httpLogLevel & HTTP_LOG_FLAG_TRACE) - -// Configure asynchronous logging. -// We follow the default configuration, -// but we reserve a special macro to easily disable asynchronous logging for debugging purposes. - -#define HTTP_LOG_ASYNC_ENABLED YES - -#define HTTP_LOG_ASYNC_ERROR ( NO && HTTP_LOG_ASYNC_ENABLED) -#define HTTP_LOG_ASYNC_WARN (YES && HTTP_LOG_ASYNC_ENABLED) -#define HTTP_LOG_ASYNC_INFO (YES && HTTP_LOG_ASYNC_ENABLED) -#define HTTP_LOG_ASYNC_VERBOSE (YES && HTTP_LOG_ASYNC_ENABLED) -#define HTTP_LOG_ASYNC_TRACE (YES && HTTP_LOG_ASYNC_ENABLED) - -// Define logging primitives. - -#define HTTPLogError(...) do {} while (0) - -#define HTTPLogWarn(...) do {} while (0) - -#define HTTPLogInfo(...) do {} while (0) - -#define HTTPLogVerbose(...) do {} while (0) - -#define HTTPLogTrace() do {} while (0) - -#define HTTPLogTrace2(...) do {} while (0) - - -#define HTTPLogCError(...) do {} while (0) - -#define HTTPLogCWarn(...) do {} while (0) - -#define HTTPLogCInfo(...) do {} while (0) - -#define HTTPLogCVerbose(...) do {} while (0) - -#define HTTPLogCTrace() do {} while (0) - -#define HTTPLogCTrace2(...) do {} while (0) - diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h deleted file mode 100644 index 401830e56f..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h +++ /dev/null @@ -1,53 +0,0 @@ -/** - * The HTTPMessage class is a simple Objective-C wrapper for HTTP message parsing. - * Migrated from CFHTTPMessage to use Foundation and Network framework. - **/ - -#import - -#define HTTPVersion1_0 @"HTTP/1.0" -#define HTTPVersion1_1 @"HTTP/1.1" - - -@interface HTTPMessage : NSObject -{ - NSMutableDictionary *_headers; - NSMutableData *_body; - NSString *_version; - NSString *_method; - NSURL *_url; - NSInteger _statusCode; - NSString *_statusDescription; - BOOL _isRequest; - BOOL _headerComplete; - NSMutableData *_rawData; -} - -- (id)initEmptyRequest; - -- (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version; - -- (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version; - -- (BOOL)appendData:(NSData *)data; - -- (BOOL)isHeaderComplete; - -- (NSString *)version; - -- (NSString *)method; -- (NSURL *)url; - -- (NSInteger)statusCode; - -- (NSDictionary *)allHeaderFields; -- (NSString *)headerField:(NSString *)headerField; - -- (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue; - -- (NSData *)messageData; - -- (NSData *)body; -- (void)setBody:(NSData *)body; - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m deleted file mode 100644 index 44eaee1800..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m +++ /dev/null @@ -1,357 +0,0 @@ -#import "HTTPMessage.h" - -#if ! __has_feature(objc_arc) -#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -#pragma clang diagnostic ignored "-Wdirect-ivar-access" - -@implementation HTTPMessage - -- (id)init -{ - if ((self = [super init])) - { - _headers = [[NSMutableDictionary alloc] init]; - _body = [[NSMutableData alloc] init]; - _rawData = [[NSMutableData alloc] init]; - _version = HTTPVersion1_1; - _headerComplete = NO; - _isRequest = YES; - } - return self; -} - -- (id)initEmptyRequest -{ - if ((self = [self init])) - { - _isRequest = YES; - } - return self; -} - -- (id)initRequestWithMethod:(NSString *)method URL:(NSURL *)url version:(NSString *)version -{ - if ((self = [self init])) - { - _isRequest = YES; - _method = [method copy]; - _url = [url copy]; - _version = version ? [version copy] : HTTPVersion1_1; - } - return self; -} - -- (id)initResponseWithStatusCode:(NSInteger)code description:(NSString *)description version:(NSString *)version -{ - if ((self = [self init])) - { - _isRequest = NO; - _statusCode = code; - _statusDescription = [description copy]; - _version = version ? [version copy] : HTTPVersion1_1; - } - return self; -} - -- (BOOL)appendData:(NSData *)data -{ - if (!data || [data length] == 0) - { - return NO; - } - - [_rawData appendData:data]; - - if (!_headerComplete) - { - // Look for the end of headers (CRLF CRLF or LF LF) - NSData *headerEndMarker = [@"\r\n\r\n" dataUsingEncoding:NSASCIIStringEncoding]; - NSRange headerEndRange = [_rawData rangeOfData:headerEndMarker options:(NSDataSearchOptions)0 range:NSMakeRange(0, [_rawData length])]; - - if (headerEndRange.location == NSNotFound) - { - // Also check for LF LF (some clients use this) - NSData *lfMarker = [@"\n\n" dataUsingEncoding:NSASCIIStringEncoding]; - headerEndRange = [_rawData rangeOfData:lfMarker options:(NSDataSearchOptions)0 range:NSMakeRange(0, [_rawData length])]; - } - - if (headerEndRange.location != NSNotFound) - { - _headerComplete = YES; - - // Parse the header data - NSData *headerData = [_rawData subdataWithRange:NSMakeRange(0, headerEndRange.location + headerEndRange.length)]; - NSString *headerString = [[NSString alloc] initWithData:headerData encoding:NSASCIIStringEncoding]; - - if (headerString) - { - [self parseHeaders:headerString]; - } - - // Extract body data if any - NSUInteger bodyStart = headerEndRange.location + headerEndRange.length; - if ([_rawData length] > bodyStart) - { - NSData *bodyData = [_rawData subdataWithRange:NSMakeRange(bodyStart, [_rawData length] - bodyStart)]; - [_body appendData:bodyData]; - } - - [_rawData setLength:0]; - } - } - else - { - // Headers are complete, append to body - [_body appendData:data]; - } - - return YES; -} - -- (void)parseHeaders:(NSString *)headerString -{ - NSArray *lines; - - // Try splitting by "\r\n" first (standard HTTP line ending) - // Check if the string actually contains "\r\n" delimiter - if ([headerString rangeOfString:@"\r\n"].location != NSNotFound) - { - // Found "\r\n" delimiter, use this split - lines = [headerString componentsSeparatedByString:@"\r\n"]; - } - else - { - // No "\r\n" found, try "\n" (some clients use just LF) - lines = [headerString componentsSeparatedByString:@"\n"]; - } - - // componentsSeparatedByString: always returns at least one element, - // so check if we have meaningful content (non-empty first line) - if ([lines count] == 0 || [[lines objectAtIndex:0] length] == 0) - { - return; - } - - // Parse first line (request line or status line) - NSString *firstLine = [lines objectAtIndex:0]; - NSArray *firstLineParts = [firstLine componentsSeparatedByString:@" "]; - - if (_isRequest && [firstLineParts count] >= 3) - { - // Request line: METHOD URL VERSION - _method = [[firstLineParts objectAtIndex:0] copy]; - NSString *urlString = [firstLineParts objectAtIndex:1]; - - // Handle both absolute URLs and relative paths - // Try absolute URL first - NSURL *parsedURL = [NSURL URLWithString:urlString]; - - // If that fails (nil), it's likely a relative path like "/endpoint" - // Create a URL with a base URL to handle relative paths - if (!parsedURL) - { - // Use a dummy base URL to allow relative path parsing - NSURL *baseURL = [NSURL URLWithString:@"http://localhost"]; - parsedURL = [NSURL URLWithString:urlString relativeToURL:baseURL]; - } - - _url = [parsedURL copy]; - if ([firstLineParts count] >= 3) - { - _version = [[firstLineParts objectAtIndex:2] copy]; - } - } - else if (!_isRequest && [firstLineParts count] >= 3) - { - // Status line: VERSION CODE DESCRIPTION - _version = [[firstLineParts objectAtIndex:0] copy]; - _statusCode = [[firstLineParts objectAtIndex:1] integerValue]; - NSMutableArray *descParts = [NSMutableArray arrayWithArray:firstLineParts]; - [descParts removeObjectAtIndex:0]; - [descParts removeObjectAtIndex:0]; - _statusDescription = [[descParts componentsJoinedByString:@" "] copy]; - } - - // Parse header fields - for (NSUInteger i = 1; i < [lines count]; i++) - { - NSString *line = [lines objectAtIndex:i]; - if ([line length] == 0) - { - continue; - } - - NSRange colonRange = [line rangeOfString:@":"]; - if (colonRange.location != NSNotFound) - { - NSString *headerName = [[line substringToIndex:colonRange.location] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - NSString *headerValue = [[line substringFromIndex:colonRange.location + 1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; - - if ([headerName length] > 0) - { - // HTTP headers are case-insensitive, but we'll store them with their original case - // For lookup, we'll use case-insensitive comparison - [_headers setObject:headerValue forKey:headerName]; - } - } - } -} - -- (BOOL)isHeaderComplete -{ - return _headerComplete; -} - -- (NSString *)version -{ - return _version; -} - -- (NSString *)method -{ - return _method; -} - -- (NSURL *)url -{ - return _url; -} - -- (NSInteger)statusCode -{ - return _statusCode; -} - -- (NSDictionary *)allHeaderFields -{ - return [_headers copy]; -} - -- (NSString *)headerField:(NSString *)headerField -{ - // Case-insensitive lookup - for (NSString *key in [_headers allKeys]) - { - if ([key caseInsensitiveCompare:headerField] == NSOrderedSame) - { - return [_headers objectForKey:key]; - } - } - return nil; -} - -- (void)setHeaderField:(NSString *)headerField value:(NSString *)headerFieldValue -{ - if (headerField && headerFieldValue) - { - // Remove existing header with same name (case-insensitive) - NSMutableArray *keysToRemove = [NSMutableArray array]; - for (NSString *key in [_headers allKeys]) - { - if ([key caseInsensitiveCompare:headerField] == NSOrderedSame) - { - [keysToRemove addObject:key]; - } - } - [_headers removeObjectsForKeys:keysToRemove]; - - // Add new header - [_headers setObject:headerFieldValue forKey:headerField]; - } -} - -- (NSData *)messageData -{ - NSMutableString *messageString = [NSMutableString string]; - - if (_isRequest) - { - // Request line - // For relative URLs, use the path component; for absolute URLs, use absoluteString - NSString *urlString = nil; - if (_url) - { - // If it's a relative URL (has a base), use the relative path - // Otherwise use absoluteString or path - if ([_url baseURL]) - { - // Relative URL - use the relative portion - urlString = [_url relativeString]; - } - else - { - // Absolute URL - urlString = [_url absoluteString]; - if (!urlString) - { - urlString = [_url path]; - } - } - } - [messageString appendFormat:@"%@ %@ %@\r\n", _method ?: @"GET", urlString ?: @"/", _version ?: HTTPVersion1_1]; - } - else - { - // Status line - [messageString appendFormat:@"%@ %ld %@\r\n", _version ?: HTTPVersion1_1, (long)_statusCode, _statusDescription ?: @""]; - } - - // Headers - for (NSString *key in [_headers allKeys]) - { - NSString *value = [_headers objectForKey:key]; - [messageString appendFormat:@"%@: %@\r\n", key, value]; - } - - // Empty line to separate headers from body - [messageString appendString:@"\r\n"]; - - NSMutableData *data = [NSMutableData dataWithData:(id)[messageString dataUsingEncoding:NSASCIIStringEncoding]]; - - // Append body if present - if ([_body length] > 0) - { - [data appendData:_body]; - } - - return data; -} - -- (NSData *)body -{ - return [_body copy]; -} - -- (void)setBody:(NSData *)body -{ - if (body) - { - _body = [body mutableCopy]; - } - else - { - _body = [[NSMutableData alloc] init]; - } -} - -- (void)dealloc -{ - // ARC automatically releases all instance variables, but we include this - // for clarity and to match the pattern of the original CFNetwork implementation. - // All Objective-C objects (_headers, _body, _rawData, _version, _method, _url, _statusDescription) - // will be automatically released by ARC when this object is deallocated. -#if ! __has_feature(objc_arc) - [_headers release]; - [_body release]; - [_rawData release]; - [_version release]; - [_method release]; - [_url release]; - [_statusDescription release]; - [super dealloc]; -#endif -} - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h deleted file mode 100644 index 726ca5d4df..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPResponse.h +++ /dev/null @@ -1,149 +0,0 @@ -#import - - -@protocol HTTPResponse - -/** - * Returns the length of the data in bytes. - * If you don't know the length in advance, implement the isChunked method and have it return YES. - **/ -- (UInt64)contentLength; - -/** - * The HTTP server supports range requests in order to allow things like - * file download resumption and optimized streaming on mobile devices. - **/ -- (UInt64)offset; -- (void)setOffset:(UInt64)offset; - -/** - * Returns the data for the response. - * You do not have to return data of the exact length that is given. - * You may optionally return data of a lesser length. - * However, you must never return data of a greater length than requested. - * Doing so could disrupt proper support for range requests. - * - * To support asynchronous responses, read the discussion at the bottom of this header. - **/ -- (NSData *)readDataOfLength:(NSUInteger)length; - -/** - * Should only return YES after the HTTPConnection has read all available data. - * That is, all data for the response has been returned to the HTTPConnection via the readDataOfLength method. - **/ -- (BOOL)isDone; - -@optional - -/** - * If you need time to calculate any part of the HTTP response headers (status code or header fields), - * this method allows you to delay sending the headers so that you may asynchronously execute the calculations. - * Simply implement this method and return YES until you have everything you need concerning the headers. - * - * This method ties into the asynchronous response architecture of the HTTPConnection. - * You should read the full discussion at the bottom of this header. - * - * If you return YES from this method, - * the HTTPConnection will wait for you to invoke the responseHasAvailableData method. - * After you do, the HTTPConnection will again invoke this method to see if the response is ready to send the headers. - * - * You should only delay sending the headers until you have everything you need concerning just the headers. - * Asynchronously generating the body of the response is not an excuse to delay sending the headers. - * Instead you should tie into the asynchronous response architecture, and use techniques such as the isChunked method. - * - * Important: You should read the discussion at the bottom of this header. - **/ -- (BOOL)delayResponseHeaders; - -/** - * Status code for response. - * Allows for responses such as redirect (301), etc. - **/ -- (NSInteger)status; - -/** - * If you want to add any extra HTTP headers to the response, - * simply return them in a dictionary in this method. - **/ -- (NSDictionary *)httpHeaders; - -/** - * If you don't know the content-length in advance, - * implement this method in your custom response class and return YES. - * - * Important: You should read the discussion at the bottom of this header. - **/ -- (BOOL)isChunked; - -/** - * This method is called from the HTTPConnection class when the connection is closed, - * or when the connection is finished with the response. - * If your response is asynchronous, you should implement this method so you know not to - * invoke any methods on the HTTPConnection after this method is called (as the connection may be deallocated). - **/ -- (void)connectionDidClose; - -@end - - -/** - * Important notice to those implementing custom asynchronous and/or chunked responses: - * - * HTTPConnection supports asynchronous responses. All you have to do in your custom response class is - * asynchronously generate the response, and invoke HTTPConnection's responseHasAvailableData method. - * You don't have to wait until you have all of the response ready to invoke this method. For example, if you - * generate the response in incremental chunks, you could call responseHasAvailableData after generating - * each chunk. Please see the HTTPAsyncFileResponse class for an example of how to do this. - * - * The normal flow of events for an HTTPConnection while responding to a request is like this: - * - Send http resopnse headers - * - Get data from response via readDataOfLength method. - * - Add data to asyncSocket's write queue. - * - Wait for asyncSocket to notify it that the data has been sent. - * - Get more data from response via readDataOfLength method. - * - ... continue this cycle until the entire response has been sent. - * - * With an asynchronous response, the flow is a little different. - * - * First the HTTPResponse is given the opportunity to postpone sending the HTTP response headers. - * This allows the response to asynchronously execute any code needed to calculate a part of the header. - * An example might be the response needs to generate some custom header fields, - * or perhaps the response needs to look for a resource on network-attached storage. - * Since the network-attached storage may be slow, the response doesn't know whether to send a 200 or 404 yet. - * In situations such as this, the HTTPResponse simply implements the delayResponseHeaders method and returns YES. - * After returning YES from this method, the HTTPConnection will wait until the response invokes its - * responseHasAvailableData method. After this occurs, the HTTPConnection will again query the delayResponseHeaders - * method to see if the response is ready to send the headers. - * This cycle will continue until the delayResponseHeaders method returns NO. - * - * You should only delay sending the response headers until you have everything you need concerning just the headers. - * Asynchronously generating the body of the response is not an excuse to delay sending the headers. - * - * After the response headers have been sent, the HTTPConnection calls your readDataOfLength method. - * You may or may not have any available data at this point. If you don't, then simply return nil. - * You should later invoke HTTPConnection's responseHasAvailableData when you have data to send. - * - * You don't have to keep track of when you return nil in the readDataOfLength method, or how many times you've invoked - * responseHasAvailableData. Just simply call responseHasAvailableData whenever you've generated new data, and - * return nil in your readDataOfLength whenever you don't have any available data in the requested range. - * HTTPConnection will automatically detect when it should be requesting new data and will act appropriately. - * - * It's important that you also keep in mind that the HTTP server supports range requests. - * The setOffset method is mandatory, and should not be ignored. - * Make sure you take into account the offset within the readDataOfLength method. - * You should also be aware that the HTTPConnection automatically sorts any range requests. - * So if your setOffset method is called with a value of 100, then you can safely release bytes 0-99. - * - * HTTPConnection can also help you keep your memory footprint small. - * Imagine you're dynamically generating a 10 MB response. You probably don't want to load all this data into - * RAM, and sit around waiting for HTTPConnection to slowly send it out over the network. All you need to do - * is pay attention to when HTTPConnection requests more data via readDataOfLength. This is because HTTPConnection - * will never allow asyncSocket's write queue to get much bigger than READ_CHUNKSIZE bytes. You should - * consider how you might be able to take advantage of this fact to generate your asynchronous response on demand, - * while at the same time keeping your memory footprint small, and your application lightning fast. - * - * If you don't know the content-length in advanced, you should also implement the isChunked method. - * This means the response will not include a Content-Length header, and will instead use "Transfer-Encoding: chunked". - * There's a good chance that if your response is asynchronous and dynamic, it's also chunked. - * If your response is chunked, you don't need to worry about range requests. - **/ diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h deleted file mode 100644 index 6934321f18..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.h +++ /dev/null @@ -1,126 +0,0 @@ -#import - -@class GCDAsyncSocket; -@class WebSocket; - -#if TARGET_OS_IPHONE -#define IMPLEMENTED_PROTOCOLS -#else -#define IMPLEMENTED_PROTOCOLS -#endif - - -@interface HTTPServer : NSObject IMPLEMENTED_PROTOCOLS -{ - // Underlying asynchronous TCP/IP socket - GCDAsyncSocket *asyncSocket; - - // Dispatch queues - dispatch_queue_t serverQueue; - dispatch_queue_t connectionQueue; - void *IsOnServerQueueKey; - void *IsOnConnectionQueueKey; - - // HTTP server configuration - NSString *documentRoot; - Class connectionClass; - NSString *interface; - UInt16 port; - - // Connection management - NSMutableArray *connections; - NSLock *connectionsLock; - - BOOL isRunning; -} - -/** - * Specifies the document root to serve files from. - * For example, if you set this to "/Users//Sites", - * then it will serve files out of the local Sites directory (including subdirectories). - * - * The default value is nil. - * The default server configuration will not serve any files until this is set. - * - * If you change the documentRoot while the server is running, - * the change will affect future incoming http connections. - **/ -- (NSString *)documentRoot; -- (void)setDocumentRoot:(NSString *)value; - -/** - * The connection class is the class used to handle incoming HTTP connections. - * - * The default value is [HTTPConnection class]. - * You can override HTTPConnection, and then set this to [MyHTTPConnection class]. - * - * If you change the connectionClass while the server is running, - * the change will affect future incoming http connections. - **/ -- (Class)connectionClass; -- (void)setConnectionClass:(Class)value; - -/** - * Set what interface you'd like the server to listen on. - * By default this is nil, which causes the server to listen on all available interfaces like en1, wifi etc. - * - * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). - * You may also use the special strings "localhost" or "loopback" to specify that - * the socket only accept connections from the local machine. - **/ -- (NSString *)interface; -- (void)setInterface:(NSString *)value; - -/** - * The port number to run the HTTP server on. - * - * The default port number is zero, meaning the server will automatically use any available port. - * This is the recommended port value, as it avoids possible port conflicts with other applications. - * Technologies such as Bonjour can be used to allow other applications to automatically discover the port number. - * - * Note: As is common on most OS's, you need root privledges to bind to port numbers below 1024. - * - * You can change the port property while the server is running, but it won't affect the running server. - * To actually change the port the server is listening for connections on you'll need to restart the server. - * - * The listeningPort method will always return the port number the running server is listening for connections on. - * If the server is not running this method returns 0. - **/ -- (UInt16)port; -- (UInt16)listeningPort; -- (void)setPort:(UInt16)value; - -/** - * Attempts to starts the server on the configured port, interface, etc. - * - * If an error occurs, this method returns NO and sets the errPtr (if given). - * Otherwise returns YES on success. - * - * Some examples of errors that might occur: - * - You specified the server listen on a port which is already in use by another application. - * - You specified the server listen on a port number below 1024, which requires root priviledges. - * - * Code Example: - * - * NSError *err = nil; - * if (![httpServer start:&err]) - * { - * NSLog(@"Error starting http server: %@", err); - * } - **/ -- (BOOL)start:(NSError **)errPtr; - -/** - * Stops the server, preventing it from accepting any new connections. - * You may specify whether or not you want to close the existing client connections. - * - * The default stop method (with no arguments) will close any existing connections. (It invokes [self stop:NO]) - **/ -- (void)stop; -- (void)stop:(BOOL)keepExistingConnections; - -- (BOOL)isRunning; - -- (NSUInteger)numberOfHTTPConnections; - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m deleted file mode 100644 index 26731df089..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPServer.m +++ /dev/null @@ -1,375 +0,0 @@ -#import "HTTPServer.h" -#import "HTTPConnection.h" -#import "HTTPLogging.h" - -#import "GCDAsyncSocket.h" - -#if ! __has_feature(objc_arc) -#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -#pragma clang diagnostic ignored "-Wdirect-ivar-access" -#pragma clang diagnostic ignored "-Wimplicit-retain-self" -#pragma clang diagnostic ignored "-Wnullable-to-nonnull-conversion" -#pragma clang diagnostic ignored "-Wunused" - -// Log levels: off, error, warn, info, verbose -// Other flags: trace -static const int httpLogLevel = HTTP_LOG_LEVEL_INFO; // | HTTP_LOG_FLAG_TRACE; - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -@implementation HTTPServer - -/** - * Standard Constructor. - * Instantiates an HTTP server, but does not start it. - **/ -- (id)init -{ - if ((self = [super init])) - { - HTTPLogTrace(); - - // Setup underlying dispatch queues - serverQueue = dispatch_queue_create("HTTPServer", NULL); - connectionQueue = dispatch_queue_create("HTTPConnection", NULL); - - IsOnServerQueueKey = &IsOnServerQueueKey; - IsOnConnectionQueueKey = &IsOnConnectionQueueKey; - - void *nonNullUnusedPointer = (__bridge void *)self; // Whatever, just not null - - dispatch_queue_set_specific(serverQueue, IsOnServerQueueKey, nonNullUnusedPointer, NULL); - dispatch_queue_set_specific(connectionQueue, IsOnConnectionQueueKey, nonNullUnusedPointer, NULL); - - // Initialize underlying GCD based tcp socket - asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:(id)self delegateQueue:serverQueue]; - - // Use default connection class of HTTPConnection - connectionClass = [HTTPConnection self]; - - // By default bind on all available interfaces, en1, wifi etc - interface = nil; - - // Use a default port of 0 - // This will allow the kernel to automatically pick an open port for us - port = 0; - - // Initialize arrays to hold all the HTTP connections - connections = [[NSMutableArray alloc] init]; - - connectionsLock = [[NSLock alloc] init]; - - // Register for notifications of closed connections - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(connectionDidDie:) - name:HTTPConnectionDidDieNotification - object:nil]; - - isRunning = NO; - } - return self; -} - -/** - * Standard Deconstructor. - * Stops the server, and clients, and releases any resources connected with this instance. - **/ -- (void)dealloc -{ - HTTPLogTrace(); - - // Remove notification observer - [[NSNotificationCenter defaultCenter] removeObserver:self]; - - // Stop the server if it's running - [self stop]; - - // Release all instance variables - -#if !OS_OBJECT_USE_OBJC - dispatch_release(serverQueue); - dispatch_release(connectionQueue); -#endif - - [asyncSocket setDelegate:nil delegateQueue:NULL]; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Server Configuration -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * The document root is filesystem root for the webserver. - * Thus requests for /index.html will be referencing the index.html file within the document root directory. - * All file requests are relative to this document root. - **/ -- (NSString *)documentRoot -{ - __block NSString *result; - - dispatch_sync(serverQueue, ^{ - result = documentRoot; - }); - - return result; -} - -- (void)setDocumentRoot:(NSString *)value -{ - HTTPLogTrace(); - - // Document root used to be of type NSURL. - // Add type checking for early warning to developers upgrading from older versions. - - if (value && ![value isKindOfClass:[NSString class]]) - { - HTTPLogWarn(@"%@: %@ - Expecting NSString parameter, received %@ parameter", - THIS_FILE, THIS_METHOD, NSStringFromClass([value class])); - return; - } - - NSString *valueCopy = [value copy]; - - dispatch_async(serverQueue, ^{ - documentRoot = valueCopy; - }); - -} - -/** - * The connection class is the class that will be used to handle connections. - * That is, when a new connection is created, an instance of this class will be intialized. - * The default connection class is HTTPConnection. - * If you use a different connection class, it is assumed that the class extends HTTPConnection - **/ -- (Class)connectionClass -{ - __block Class result; - - dispatch_sync(serverQueue, ^{ - result = connectionClass; - }); - - return result; -} - -- (void)setConnectionClass:(Class)value -{ - HTTPLogTrace(); - - dispatch_async(serverQueue, ^{ - connectionClass = value; - }); -} - -/** - * What interface to bind the listening socket to. - **/ -- (NSString *)interface -{ - __block NSString *result; - - dispatch_sync(serverQueue, ^{ - result = interface; - }); - - return result; -} - -- (void)setInterface:(NSString *)value -{ - NSString *valueCopy = [value copy]; - - dispatch_async(serverQueue, ^{ - interface = valueCopy; - }); - -} - -/** - * The port to listen for connections on. - * By default this port is initially set to zero, which allows the kernel to pick an available port for us. - * After the HTTP server has started, the port being used may be obtained by this method. - **/ -- (UInt16)port -{ - __block UInt16 result; - - dispatch_sync(serverQueue, ^{ - result = port; - }); - - return result; -} - -- (UInt16)listeningPort -{ - __block UInt16 result; - - dispatch_sync(serverQueue, ^{ - if (isRunning) - result = [asyncSocket localPort]; - else - result = 0; - }); - - return result; -} - -- (void)setPort:(UInt16)value -{ - HTTPLogTrace(); - - dispatch_async(serverQueue, ^{ - port = value; - }); -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Server Control -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (BOOL)start:(NSError **)errPtr -{ - HTTPLogTrace(); - - __block BOOL success = YES; - __block NSError *err = nil; - - dispatch_sync(serverQueue, ^{ @autoreleasepool { - - success = [asyncSocket acceptOnInterface:interface port:port error:&err]; - if (success) - { - HTTPLogInfo(@"%@: Started HTTP server on port %hu", THIS_FILE, [asyncSocket localPort]); - - isRunning = YES; - } - else - { - HTTPLogError(@"%@: Failed to start HTTP Server: %@", THIS_FILE, err); - } - }}); - - if (errPtr) - *errPtr = err; - - return success; -} - -- (void)stop -{ - [self stop:NO]; -} - -- (void)stop:(BOOL)keepExistingConnections -{ - HTTPLogTrace(); - - dispatch_sync(serverQueue, ^{ @autoreleasepool { - // Stop listening / accepting incoming connections - [asyncSocket disconnect]; - isRunning = NO; - - if (!keepExistingConnections) - { - // Stop all HTTP connections the server owns - [connectionsLock lock]; - for (HTTPConnection *connection in connections) - { - [connection stop]; - } - [connections removeAllObjects]; - [connectionsLock unlock]; - } - }}); -} - -- (BOOL)isRunning -{ - __block BOOL result; - - dispatch_sync(serverQueue, ^{ - result = isRunning; - }); - - return result; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Server Status -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * Returns the number of http client connections that are currently connected to the server. - **/ -- (NSUInteger)numberOfHTTPConnections -{ - NSUInteger result = 0; - - [connectionsLock lock]; - result = [connections count]; - [connectionsLock unlock]; - - return result; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Incoming Connections -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -- (HTTPConfig *)config -{ - // Override me if you want to provide a custom config to the new connection. - // - // Generally this involves overriding the HTTPConfig class to include any custom settings, - // and then having this method return an instance of 'MyHTTPConfig'. - - // Note: Think you can make the server faster by putting each connection on its own queue? - // Then benchmark it before and after and discover for yourself the shocking truth! - // - // Try the apache benchmark tool (already installed on your Mac): - // $ ab -n 1000 -c 1 http://localhost:/some_path.html - - // Each connection gets its own dispatch queue (HTTPConnection creates one when the config - // carries none), so a request blocked on the automation queue cannot stall request parsing - // and responses for every other connection. - return [[HTTPConfig alloc] initWithServer:self documentRoot:documentRoot queue:NULL]; -} - -- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket -{ - HTTPConnection *newConnection = (HTTPConnection *)[[connectionClass alloc] initWithAsyncSocket:newSocket - configuration:[self config]]; - [connectionsLock lock]; - [connections addObject:newConnection]; - [connectionsLock unlock]; - - [newConnection start]; -} - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Notifications -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** - * This method is automatically called when a notification of type HTTPConnectionDidDieNotification is posted. - * It allows us to remove the connection from our array. - **/ -- (void)connectionDidDie:(NSNotification *)notification -{ - // Note: This method is called on the connection queue that posted the notification - - [connectionsLock lock]; - - HTTPLogTrace(); - [connections removeObject:[notification object]]; - - [connectionsLock unlock]; -} - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/LICENSE b/WebDriverAgentLib/Vendor/CocoaHTTPServer/LICENSE deleted file mode 100644 index 64c3c902bf..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/LICENSE +++ /dev/null @@ -1,18 +0,0 @@ -Software License Agreement (BSD License) - -Copyright (c) 2011, Deusty, LLC -All rights reserved. - -Redistribution and use of this software in source and binary forms, -with or without modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - -* Neither the name of Deusty nor the names of its - contributors may be used to endorse or promote products - derived from this software without specific prior - written permission of Deusty, LLC. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h deleted file mode 100644 index 309a6d9e73..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.h +++ /dev/null @@ -1,13 +0,0 @@ -#import -#import "HTTPResponse.h" - - -@interface HTTPDataResponse : NSObject -{ - NSUInteger offset; - NSData *data; -} - -- (id)initWithData:(NSData *)data; - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m deleted file mode 100644 index 79c5bca57b..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPDataResponse.m +++ /dev/null @@ -1,83 +0,0 @@ -#import "HTTPDataResponse.h" -#import "HTTPLogging.h" - -#if ! __has_feature(objc_arc) -#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). -#endif - -#pragma clang diagnostic ignored "-Wdirect-ivar-access" -#pragma clang diagnostic ignored "-Wcast-qual" -#pragma clang diagnostic ignored "-Wunused-variable" - -// Log levels : off, error, warn, info, verbose -// Other flags: trace -static const int httpLogLevel = HTTP_LOG_LEVEL_OFF; // | HTTP_LOG_FLAG_TRACE; - - -@implementation HTTPDataResponse - -- (id)initWithData:(NSData *)dataParam -{ - if((self = [super init])) - { - HTTPLogTrace(); - - offset = 0; - data = dataParam; - } - return self; -} - -- (void)dealloc -{ - HTTPLogTrace(); - -} - -- (UInt64)contentLength -{ - UInt64 result = (UInt64)[data length]; - - HTTPLogTrace2(@"%@[%p]: contentLength - %llu", THIS_FILE, self, result); - - return result; -} - -- (UInt64)offset -{ - HTTPLogTrace(); - - return offset; -} - -- (void)setOffset:(UInt64)offsetParam -{ - HTTPLogTrace2(@"%@[%p]: setOffset:%lu", THIS_FILE, self, (unsigned long)offset); - - offset = (NSUInteger)offsetParam; -} - -- (NSData *)readDataOfLength:(NSUInteger)lengthParameter -{ - HTTPLogTrace2(@"%@[%p]: readDataOfLength:%lu", THIS_FILE, self, (unsigned long)lengthParameter); - - NSUInteger remaining = [data length] - offset; - NSUInteger length = lengthParameter < remaining ? lengthParameter : remaining; - - void *bytes = (void *)(((char*)[data bytes]) + offset); - - offset += length; - - return [NSData dataWithBytesNoCopy:bytes length:length freeWhenDone:NO]; -} - -- (BOOL)isDone -{ - BOOL result = (offset == [data length]); - - HTTPLogTrace2(@"%@[%p]: isDone - %@", THIS_FILE, self, (result ? @"YES" : @"NO")); - - return result; -} - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h deleted file mode 100644 index 0b4fed96a8..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.h +++ /dev/null @@ -1,9 +0,0 @@ -#import "HTTPResponse.h" - -@interface HTTPErrorResponse : NSObject { - NSInteger _status; -} - -- (id)initWithErrorCode:(int)httpErrorCode; - -@end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m deleted file mode 100644 index a11552008c..0000000000 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m +++ /dev/null @@ -1,38 +0,0 @@ -#import "HTTPErrorResponse.h" - -#pragma clang diagnostic ignored "-Wdirect-ivar-access" - -@implementation HTTPErrorResponse - --(id)initWithErrorCode:(int)httpErrorCode -{ - if ((self = [super init])) - { - _status = httpErrorCode; - } - - return self; -} - -- (UInt64) contentLength { - return 0; -} - -- (UInt64) offset { - return 0; -} - -- (void)setOffset:(UInt64)offset {} - -- (NSData*) readDataOfLength:(NSUInteger)length { - return nil; -} - -- (BOOL) isDone { - return YES; -} - -- (NSInteger) status { - return _status; -} -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h deleted file mode 100644 index e3930fcc34..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.h +++ /dev/null @@ -1,13 +0,0 @@ -#import -#import "HTTPResponse.h" - -// Wraps an HTTPResponse object to allow setting a custom status code -// without needing to create subclasses of every response. -@interface HTTPResponseProxy : NSObject - -@property (nonatomic) NSObject *response; -@property (nonatomic) NSInteger status; - -- (NSInteger)customStatus; - -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m deleted file mode 100644 index f74f3ad1f2..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/HTTPResponseProxy.m +++ /dev/null @@ -1,84 +0,0 @@ -#import "HTTPResponseProxy.h" - -#pragma clang diagnostic ignored "-Wdirect-ivar-access" - -@implementation HTTPResponseProxy - -@synthesize response; -@synthesize status; - -- (NSInteger)status { - if (status != 0) { - return status; - } else if ([response respondsToSelector:@selector(status)]) { - return [response status]; - } - - return 200; -} - -- (void)setStatus:(NSInteger)statusCode { - status = statusCode; -} - -- (NSInteger)customStatus { - return status; -} - -// Implement the required HTTPResponse methods -- (UInt64)contentLength { - if (response) { - return [response contentLength]; - } else { - return 0; - } -} - -- (UInt64)offset { - if (response) { - return [response offset]; - } else { - return 0; - } -} - -- (void)setOffset:(UInt64)offset { - if (response) { - [response setOffset:offset]; - } -} - -- (NSData *)readDataOfLength:(NSUInteger)length { - if (response) { - return [response readDataOfLength:length]; - } else { - return nil; - } -} - -- (BOOL)isDone { - if (response) { - return [response isDone]; - } else { - return YES; - } -} - -// Forward all other invocations to the actual response object -- (void)forwardInvocation:(NSInvocation *)invocation { - if ([response respondsToSelector:[invocation selector]]) { - [invocation invokeWithTarget:response]; - } else { - [super forwardInvocation:invocation]; - } -} - -- (BOOL)respondsToSelector:(SEL)selector { - if ([super respondsToSelector:selector]) - return YES; - - return [response respondsToSelector:selector]; -} - -@end - diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/LICENSE b/WebDriverAgentLib/Vendor/RoutingHTTPServer/LICENSE deleted file mode 100644 index 717caf79b6..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2011 Matt Stevens - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h deleted file mode 100644 index 185a2b7e63..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.h +++ /dev/null @@ -1,18 +0,0 @@ -#import -#import "RoutingHTTPServer.h" - -@interface Route : NSObject - -@property (nonatomic) NSRegularExpression *regex; -@property (nonatomic, copy) RequestHandler handler; - -#if __has_feature(objc_arc_weak) -@property (nonatomic, weak) id target; -#else -@property (nonatomic, assign) id target; -#endif - -@property (nonatomic, assign) SEL selector; -@property (nonatomic) NSArray *keys; - -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m deleted file mode 100644 index 8c9e7e56b4..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/Route.m +++ /dev/null @@ -1,11 +0,0 @@ -#import "Route.h" - -@implementation Route - -@synthesize regex; -@synthesize handler; -@synthesize target; -@synthesize selector; -@synthesize keys; - -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h deleted file mode 100644 index 0219addee9..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.h +++ /dev/null @@ -1,16 +0,0 @@ -#import -@class HTTPMessage; - -@interface RouteRequest : NSObject - -@property (nonatomic, readonly) NSDictionary *headers; -@property (nonatomic, readonly) NSDictionary *params; - -- (id)initWithHTTPMessage:(HTTPMessage *)msg parameters:(NSDictionary *)params; -- (NSString *)header:(NSString *)field; -- (id)param:(NSString *)name; -- (NSString *)method; -- (NSURL *)url; -- (NSData *)body; - -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m deleted file mode 100644 index 50046d03e8..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteRequest.m +++ /dev/null @@ -1,50 +0,0 @@ -#import "RouteRequest.h" -#import "HTTPMessage.h" - -#pragma clang diagnostic ignored "-Wdirect-ivar-access" -#pragma clang diagnostic ignored "-Widiomatic-parentheses" - -@implementation RouteRequest { - HTTPMessage *message; -} - -@synthesize params; - -- (id)initWithHTTPMessage:(HTTPMessage *)msg parameters:(NSDictionary *)parameters { - if (self = [super init]) { - params = parameters; - message = msg; - } - return self; -} - -- (NSDictionary *)headers { - return [message allHeaderFields]; -} - -- (NSString *)header:(NSString *)field { - return [message headerField:field]; -} - -- (id)param:(NSString *)name { - return [params objectForKey:name]; -} - -- (NSString *)method { - return [message method]; -} - -- (NSURL *)url { - return [message url]; -} - -- (NSData *)body { - return [message body]; -} - -- (NSString *)description { - NSData *data = [message messageData]; - return [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding]; -} - -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h deleted file mode 100644 index 688002f859..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.h +++ /dev/null @@ -1,20 +0,0 @@ -#import -#import "HTTPResponse.h" -@class HTTPConnection; -@class HTTPResponseProxy; - -@interface RouteResponse : NSObject - -@property (nonatomic, unsafe_unretained, readonly) HTTPConnection *connection; -@property (nonatomic, readonly) NSDictionary *headers; -@property (nonatomic, strong) NSObject *response; -@property (nonatomic, readonly) NSObject *proxiedResponse; -@property (nonatomic) NSInteger statusCode; - -- (id)initWithConnection:(HTTPConnection *)theConnection; -- (void)setHeader:(NSString *)field value:(NSString *)value; -- (void)respondWithString:(NSString *)string; -- (void)respondWithString:(NSString *)string encoding:(NSStringEncoding)encoding; -- (void)respondWithData:(NSData *)data; - -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m deleted file mode 100644 index 47db7b6fad..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RouteResponse.m +++ /dev/null @@ -1,66 +0,0 @@ -#import "RouteResponse.h" -#import "HTTPConnection.h" -#import "HTTPDataResponse.h" -#import "HTTPResponseProxy.h" - -#pragma clang diagnostic ignored "-Wdirect-ivar-access" -#pragma clang diagnostic ignored "-Widiomatic-parentheses" - -@implementation RouteResponse { - NSMutableDictionary *headers; - HTTPResponseProxy *proxy; -} - -@synthesize connection; -@synthesize headers; - -- (id)initWithConnection:(HTTPConnection *)theConnection { - if (self = [super init]) { - connection = theConnection; - headers = [[NSMutableDictionary alloc] init]; - proxy = [[HTTPResponseProxy alloc] init]; - } - return self; -} - -- (NSObject *)response { - return proxy.response; -} - -- (void)setResponse:(NSObject *)response { - proxy.response = response; -} - -- (NSObject *)proxiedResponse { - if (proxy.response != nil || proxy.customStatus != 0 || [headers count] > 0) { - return proxy; - } - - return nil; -} - -- (NSInteger)statusCode { - return proxy.status; -} - -- (void)setStatusCode:(NSInteger)status { - proxy.status = status; -} - -- (void)setHeader:(NSString *)field value:(NSString *)value { - [headers setObject:value forKey:field]; -} - -- (void)respondWithString:(NSString *)string { - [self respondWithString:string encoding:NSUTF8StringEncoding]; -} - -- (void)respondWithString:(NSString *)string encoding:(NSStringEncoding)encoding { - [self respondWithData:[string dataUsingEncoding:encoding]]; -} - -- (void)respondWithData:(NSData *)data { - self.response = [[HTTPDataResponse alloc] initWithData:data]; -} - -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h deleted file mode 100644 index 1f6cd27d00..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.h +++ /dev/null @@ -1,5 +0,0 @@ -#import -#import "HTTPConnection.h" - -@interface RoutingConnection : HTTPConnection -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m deleted file mode 100644 index 99769cd614..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingConnection.m +++ /dev/null @@ -1,145 +0,0 @@ -#import "RoutingConnection.h" -#import "RoutingHTTPServer.h" -#import "HTTPMessage.h" -#import "HTTPResponseProxy.h" - -#pragma clang diagnostic ignored "-Wdirect-ivar-access" -#pragma clang diagnostic ignored "-Widiomatic-parentheses" -#pragma clang diagnostic ignored "-Wundeclared-selector" - -@implementation RoutingConnection { - // Weak (was unsafe_unretained): replies run on connection queues and must not dereference a - // torn-down server; messaging nil is harmless, a dangling pointer crashes in objc_msgSend. - __weak RoutingHTTPServer *http; - NSDictionary *headers; -} - -- (id)initWithAsyncSocket:(GCDAsyncSocket *)newSocket configuration:(HTTPConfig *)aConfig { - if (self = [super initWithAsyncSocket:newSocket configuration:aConfig]) { - NSAssert([config.server isKindOfClass:[RoutingHTTPServer class]], - @"A RoutingConnection is being used with a server that is not a %@", - NSStringFromClass([RoutingHTTPServer class])); - - http = (RoutingHTTPServer *)config.server; - } - return self; -} - -- (BOOL)supportsMethod:(NSString *)method atPath:(NSString *)path { - - if ([http supportsMethod:method]) - return YES; - - return [super supportsMethod:method atPath:path]; -} - -- (BOOL)shouldHandleRequestForMethod:(NSString *)method atPath:(NSString *)path { - // The default implementation is strict about the use of Content-Length. Either - // a given method + path combination must *always* include data or *never* - // include data. The routing connection is lenient, a POST that sometimes does - // not include data or a GET that sometimes does is fine. It is up to the route - // implementations to decide how to handle these situations. - return YES; -} - -- (void)processBodyData:(NSData *)postDataChunk { - BOOL result = [request appendData:postDataChunk]; - if (!result) { - // TODO: Log - } -} - -- (NSObject *)httpResponseForMethod:(NSString *)method URI:(NSString *)path { - NSURL *url = [request url]; - NSString *query = nil; - NSDictionary *params = [NSDictionary dictionary]; - headers = nil; - - if (url) { - path = [url path]; // Strip the query string from the path - query = [url query]; - if (query) { - params = [self parseParams:query]; - } - } - - RouteResponse *response = [http routeMethod:method withPath:path parameters:params request:request connection:self]; - if (response != nil) { - // Snapshot instead of aliasing the route response's live (mutable) dictionary. - headers = [response.headers copy]; - return response.proxiedResponse; - } - - // Set a MIME type for static files if possible - NSObject *staticResponse = [super httpResponseForMethod:method URI:path]; - if (staticResponse && [staticResponse respondsToSelector:@selector(filePath)]) { - NSString *mimeType = [http mimeTypeForPath:[staticResponse performSelector:@selector(filePath)]]; - if (mimeType) { - headers = [NSDictionary dictionaryWithObject:mimeType forKey:@"Content-Type"]; - } - } - return staticResponse; -} - -- (void)responseHasAvailableData:(NSObject *)sender { - HTTPResponseProxy *proxy = (HTTPResponseProxy *)httpResponse; - if (proxy.response == sender) { - [super responseHasAvailableData:httpResponse]; - } -} - -- (void)responseDidAbort:(NSObject *)sender { - HTTPResponseProxy *proxy = (HTTPResponseProxy *)httpResponse; - if (proxy.response == sender) { - [super responseDidAbort:httpResponse]; - } -} - -- (void)setHeadersForResponse:(HTTPMessage *)response isError:(BOOL)isError { - [http.defaultHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) { - [response setHeaderField:field value:value]; - }]; - - if (headers && !isError) { - [headers enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) { - [response setHeaderField:field value:value]; - }]; - } - - // Set the connection header if not already specified - NSString *connection = [response headerField:@"Connection"]; - if (!connection) { - connection = [self shouldDie] ? @"close" : @"keep-alive"; - [response setHeaderField:@"Connection" value:connection]; - } -} - -- (NSData *)preprocessResponse:(HTTPMessage *)response { - [self setHeadersForResponse:response isError:NO]; - return [super preprocessResponse:response]; -} - -- (NSData *)preprocessErrorResponse:(HTTPMessage *)response { - [self setHeadersForResponse:response isError:YES]; - return [super preprocessErrorResponse:response]; -} - -- (BOOL)shouldDie { - __block BOOL shouldDie = [super shouldDie]; - - // Allow custom headers to determine if the connection should be closed - if (!shouldDie && headers) { - [headers enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL *stop) { - if ([field caseInsensitiveCompare:@"connection"] == NSOrderedSame) { - if ([value caseInsensitiveCompare:@"close"] == NSOrderedSame) { - shouldDie = YES; - } - *stop = YES; - } - }]; - } - - return shouldDie; -} - -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h deleted file mode 100644 index 91c7768c5b..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.h +++ /dev/null @@ -1,55 +0,0 @@ -#import - -//! Project version number for Peertalk. -FOUNDATION_EXPORT double RoutingHTTPServerVersionNumber; - -//! Project version string for Peertalk. -FOUNDATION_EXPORT const unsigned char RoutingHTTPServerVersionString[]; - -#import "HTTPServer.h" -#import "HTTPConnection.h" -#import "HTTPResponse.h" -#import "RouteResponse.h" -#import "RouteRequest.h" -#import "RoutingConnection.h" - -#import "GCDAsyncSocket.h" - -typedef void (^RequestHandler)(RouteRequest *request, RouteResponse *response); - -@interface RoutingHTTPServer : HTTPServer - -@property (nonatomic, readonly) NSDictionary *defaultHeaders; - -// Specifies headers that will be set on every response. -// These headers can be overridden by RouteResponses. -- (void)setDefaultHeaders:(NSDictionary *)headers; -- (void)setDefaultHeader:(NSString *)field value:(NSString *)value; - -// Returns the dispatch queue on which routes are processed. -// By default this is NULL and routes are processed on CocoaHTTPServer's -// connection queue. You can specify a queue to process routes on, such as -// dispatch_get_main_queue() to process all routes on the main thread. -- (dispatch_queue_t)routeQueue; -- (void)setRouteQueue:(dispatch_queue_t)queue; - -- (NSDictionary *)mimeTypes; -- (void)setMIMETypes:(NSDictionary *)types; -- (void)setMIMEType:(NSString *)type forExtension:(NSString *)ext; -- (NSString *)mimeTypeForPath:(NSString *)path; - -// Convenience methods. Yes I know, this is Cocoa and we don't use convenience -// methods because typing lengthy primitives over and over and over again is -// elegant with the beauty and the poetry. These are just, you know, here. -- (void)get:(NSString *)path withBlock:(RequestHandler)block; -- (void)post:(NSString *)path withBlock:(RequestHandler)block; -- (void)put:(NSString *)path withBlock:(RequestHandler)block; -- (void)delete:(NSString *)path withBlock:(RequestHandler)block; - -- (void)handleMethod:(NSString *)method withPath:(NSString *)path block:(RequestHandler)block; -- (void)handleMethod:(NSString *)method withPath:(NSString *)path target:(id)target selector:(SEL)selector; - -- (BOOL)supportsMethod:(NSString *)method; -- (RouteResponse *)routeMethod:(NSString *)method withPath:(NSString *)path parameters:(NSDictionary *)params request:(HTTPMessage *)request connection:(HTTPConnection *)connection; - -@end diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m deleted file mode 100644 index 68e6a274aa..0000000000 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m +++ /dev/null @@ -1,303 +0,0 @@ -#import "RoutingHTTPServer.h" -#import "RoutingConnection.h" -#import "Route.h" - -#pragma clang diagnostic ignored "-Wdirect-ivar-access" -#pragma clang diagnostic ignored "-Widiomatic-parentheses" - -@implementation RoutingHTTPServer { - NSMutableDictionary *routes; - NSMutableDictionary *defaultHeaders; - NSMutableDictionary *mimeTypes; - dispatch_queue_t routeQueue; -} - -@synthesize defaultHeaders; - -- (id)init { - if (self = [super init]) { - connectionClass = [RoutingConnection self]; - routes = [[NSMutableDictionary alloc] init]; - defaultHeaders = [[NSMutableDictionary alloc] init]; - [self setupMIMETypes]; - } - return self; -} - -#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE -- (void)dealloc { - if (routeQueue) - dispatch_release(routeQueue); -} -#endif - -- (void)setDefaultHeaders:(NSDictionary *)headers { - if (headers) { - defaultHeaders = [headers mutableCopy]; - } else { - defaultHeaders = [[NSMutableDictionary alloc] init]; - } -} - -- (void)setDefaultHeader:(NSString *)field value:(NSString *)value { - [defaultHeaders setObject:value forKey:field]; -} - -- (dispatch_queue_t)routeQueue { - return routeQueue; -} - -- (void)setRouteQueue:(dispatch_queue_t)queue { -#if !OS_OBJECT_USE_OBJC_RETAIN_RELEASE - if (queue) - dispatch_retain(queue); - - if (routeQueue) - dispatch_release(routeQueue); -#endif - - routeQueue = queue; -} - -- (NSDictionary *)mimeTypes { - return mimeTypes; -} - -- (void)setMIMETypes:(NSDictionary *)types { - NSMutableDictionary *newTypes; - if (types) { - newTypes = [types mutableCopy]; - } else { - newTypes = [[NSMutableDictionary alloc] init]; - } - - mimeTypes = newTypes; -} - -- (void)setMIMEType:(NSString *)theType forExtension:(NSString *)ext { - [mimeTypes setObject:theType forKey:ext]; -} - -- (NSString *)mimeTypeForPath:(NSString *)path { - NSString *ext = [[path pathExtension] lowercaseString]; - if (!ext || [ext length] < 1) - return nil; - - return [mimeTypes objectForKey:ext]; -} - -- (void)get:(NSString *)path withBlock:(RequestHandler)block { - [self handleMethod:@"GET" withPath:path block:block]; -} - -- (void)post:(NSString *)path withBlock:(RequestHandler)block { - [self handleMethod:@"POST" withPath:path block:block]; -} - -- (void)put:(NSString *)path withBlock:(RequestHandler)block { - [self handleMethod:@"PUT" withPath:path block:block]; -} - -- (void)delete:(NSString *)path withBlock:(RequestHandler)block { - [self handleMethod:@"DELETE" withPath:path block:block]; -} - -- (void)handleMethod:(NSString *)method - withPath:(NSString *)path - block:(RequestHandler)block { - Route *route = [self routeWithPath:path]; - route.handler = block; - - [self addRoute:route forMethod:method]; -} - -- (void)handleMethod:(NSString *)method - withPath:(NSString *)path - target:(id)target - selector:(SEL)selector { - Route *route = [self routeWithPath:path]; - route.target = target; - route.selector = selector; - - [self addRoute:route forMethod:method]; -} - -- (void)addRoute:(Route *)route forMethod:(NSString *)method { - method = [method uppercaseString]; - NSMutableArray *methodRoutes = [routes objectForKey:method]; - if (methodRoutes == nil) { - methodRoutes = [NSMutableArray array]; - [routes setObject:methodRoutes forKey:method]; - } - - [methodRoutes addObject:route]; - - // Define a HEAD route for all GET routes - if ([method isEqualToString:@"GET"]) { - [self addRoute:route forMethod:@"HEAD"]; - } -} - -- (Route *)routeWithPath:(NSString *)path { - Route *route = [[Route alloc] init]; - NSMutableArray *keys = [NSMutableArray array]; - - if ([path length] > 2 && [path characterAtIndex:0] == '{') { - // This is a custom regular expression, just remove the {} - path = [path substringWithRange:NSMakeRange(1, [path length] - 2)]; - } else { - NSRegularExpression *regex = nil; - - // Escape regex characters - regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:(NSRegularExpressionOptions)0 error:nil]; - path = [regex stringByReplacingMatchesInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"]; - - // Parse any :parameters and * in the path - regex = [NSRegularExpression regularExpressionWithPattern:@"(:(\\w+)|\\*)" - options:(NSRegularExpressionOptions)0 - error:nil]; - NSMutableString *regexPath = [NSMutableString stringWithString:path]; - __block NSInteger diff = 0; - [regex enumerateMatchesInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length) - usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { - NSRange replacementRange = NSMakeRange(diff + result.range.location, result.range.length); - NSString *replacementString; - - NSString *capturedString = [path substringWithRange:result.range]; - if ([capturedString isEqualToString:@"*"]) { - [keys addObject:@"wildcards"]; - replacementString = @"(.*?)"; - } else { - NSString *keyString = [path substringWithRange:[result rangeAtIndex:2]]; - [keys addObject:keyString]; - replacementString = @"([^/]+)"; - } - - [regexPath replaceCharactersInRange:replacementRange withString:replacementString]; - diff += replacementString.length - result.range.length; - }]; - - path = [NSString stringWithFormat:@"^%@$", regexPath]; - } - - route.regex = [NSRegularExpression regularExpressionWithPattern:path options:NSRegularExpressionCaseInsensitive error:nil]; - if ([keys count] > 0) { - route.keys = keys; - } - - return route; -} - -- (BOOL)supportsMethod:(NSString *)method { - return ([routes objectForKey:method] != nil); -} - -- (void)handleRoute:(Route *)route - withRequest:(RouteRequest *)request - response:(RouteResponse *)response { - if (route.handler) { - route.handler(request, response); - } else { - id target = route.target; - SEL selector = route.selector; - NSMethodSignature *signature = [target methodSignatureForSelector:selector]; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; - [invocation setSelector:selector]; - [invocation setArgument:&request atIndex:2]; - [invocation setArgument:&response atIndex:3]; - [invocation invokeWithTarget:target]; - } -} - -- (RouteResponse *)routeMethod:(NSString *)method - withPath:(NSString *)path - parameters:(NSDictionary *)params - request:(HTTPMessage *)httpMessage - connection:(HTTPConnection *)connection { - NSMutableArray *methodRoutes = [routes objectForKey:method]; - if (methodRoutes == nil) - return nil; - - for (Route *route in methodRoutes) { - NSTextCheckingResult *result = [route.regex firstMatchInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length)]; - if (!result) - continue; - - // The first range is all of the text matched by the regex. - NSUInteger captureCount = [result numberOfRanges]; - - if (route.keys) { - // Add the route's parameters to the parameter dictionary, accounting for - // the first range containing the matched text. - if (captureCount == [route.keys count] + 1) { - NSMutableDictionary *newParams = [params mutableCopy]; - NSUInteger index = 1; - BOOL firstWildcard = YES; - for (NSString *key in route.keys) { - NSString *capture = [path substringWithRange:[result rangeAtIndex:index]]; - if ([key isEqualToString:@"wildcards"]) { - NSMutableArray *wildcards = [newParams objectForKey:key]; - if (firstWildcard) { - // Create a new array and replace any existing object with the same key - wildcards = [NSMutableArray array]; - [newParams setObject:wildcards forKey:key]; - firstWildcard = NO; - } - [wildcards addObject:capture]; - } else { - [newParams setObject:capture forKey:key]; - } - index++; - } - params = newParams; - } - } else if (captureCount > 1) { - // For custom regular expressions place the anonymous captures in the captures parameter - NSMutableDictionary *newParams = [params mutableCopy]; - NSMutableArray *captures = [NSMutableArray array]; - for (NSUInteger i = 1; i < captureCount; i++) { - [captures addObject:[path substringWithRange:[result rangeAtIndex:i]]]; - } - [newParams setObject:captures forKey:@"captures"]; - params = newParams; - } - - RouteRequest *request = [[RouteRequest alloc] initWithHTTPMessage:httpMessage parameters:params]; - RouteResponse *response = [[RouteResponse alloc] initWithConnection:connection]; - if (!routeQueue) { - [self handleRoute:route withRequest:request response:response]; - } else { - // Process the route on the specified queue - dispatch_sync(routeQueue, ^{ - @autoreleasepool { - [self handleRoute:route withRequest:request response:response]; - } - }); - } - return response; - } - - return nil; -} - -- (void)setupMIMETypes { - mimeTypes = [[NSMutableDictionary alloc] initWithObjectsAndKeys: - @"application/x-javascript", @"js", - @"image/gif", @"gif", - @"image/jpeg", @"jpg", - @"image/jpeg", @"jpeg", - @"image/png", @"png", - @"image/svg+xml", @"svg", - @"image/tiff", @"tif", - @"image/tiff", @"tiff", - @"image/x-icon", @"ico", - @"image/x-ms-bmp", @"bmp", - @"text/css", @"css", - @"text/html", @"html", - @"text/html", @"htm", - @"text/plain", @"txt", - @"text/xml", @"xml", - nil]; -} - -@end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m index 3bd1884abf..f63a91561d 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m @@ -38,9 +38,10 @@ - (BOOL)handleCustomAction:(UIAccessibilityCustomAction *)action - (IBAction)deadlockApp:(id)sender { - dispatch_sync(dispatch_get_main_queue(), ^{ - // This will never execute - }); + // A self dispatch_sync would trip the OS watchdog and get the process + // killed outright. Sleeping instead simulates an app that stops answering + // accessibility requests while staying alive, per #1210. + [NSThread sleepForTimeInterval:20.0]; } - (IBAction)didTapButton:(UIButton *)button diff --git a/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard b/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard index ceeb4fb5be..7a9156769a 100644 --- a/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard +++ b/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard @@ -38,7 +38,6 @@ -