diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml deleted file mode 100644 index a324a9b7d..000000000 --- a/.azure-pipelines.yml +++ /dev/null @@ -1,334 +0,0 @@ -# https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/hosted?view=azure-devops&tabs=yaml -variables: - MIN_VM_IMAGE: macOS-12 - MIN_XCODE_VERSION: 13.1 - MIN_PLATFORM_VERSION: 15.0 - MIN_TV_PLATFORM_VERSION: 15.0 - MIN_TV_DEVICE_NAME: Apple TV 4K (2nd generation) - MIN_IPHONE_DEVICE_NAME: iPhone 11 - MIN_IPAD_DEVICE_NAME: iPad Pro (11-inch) (3rd generation) - MAX_VM_IMAGE: macOS-12 - MAX_XCODE_VERSION: 14.2 - MAX_PLATFORM_VERSION: 16.2 - MAX_PLATFORM_VERSION_TV: 16.1 - MAX_IPHONE_DEVICE_NAME: iPhone 13 - MAX_TV_DEVICE_NAME: Apple TV 4K (2nd generation) - MAX_IPAD_DEVICE_NAME: iPad Pro (11-inch) (3rd generation) - DEFAULT_NODE_VERSION: "18.x" - -trigger: - batch: true - branches: - include: [master] - -pr: - autoCancel: true - branches: - include: [master] - -pool: - vmImage: "$(MAX_VM_IMAGE)" - - -parameters: -- name: integrationJobs - type: object - default: - - action: int_test_1 - dest: iphone - - action: int_test_2 - dest: iphone - - action: int_test_3 - dest: iphone - - action: int_test_1 - dest: ipad - - action: int_test_2 - dest: ipad - - action: int_test_3 - dest: ipad - - -stages: -- stage: Unit_Tests_And_Linters - jobs: - # region Build - - template: ./azure-templates/base_job.yml - parameters: - name: Generic_iOS_Build_Max_Xcode - action: build - target: runner - sdk: sim - dest: generic - codeSign: no - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: Generic_tvOS_Build_Max_Xcode - action: build - target: tv_runner - sdk: tv_sim - dest: tv_generic - codeSign: no - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: iOS_Build_Max_Xcode - action: build - target: runner - sdk: sim - iphoneModel: $(MAX_IPHONE_DEVICE_NAME) - ipadModel: $(MAX_IPAD_DEVICE_NAME) - iosVersion: $(MAX_PLATFORM_VERSION) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: tvOS_Build_Max_Xcode - action: build - target: tv_runner - sdk: tv_sim - tvModel: $(MAX_TV_DEVICE_NAME) - tvVersion: $(MAX_PLATFORM_VERSION_TV) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: Generic_iOS_Build_Min_Xcode - action: build - target: runner - sdk: sim - dest: generic - codeSign: no - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - extraXcArgs: IPHONEOS_DEPLOYMENT_TARGET=$(MIN_PLATFORM_VERSION) - - template: ./azure-templates/base_job.yml - parameters: - name: Generic_tvOS_Build_Min_Xcode - action: build - target: tv_runner - dest: tv_generic - sdk: tv_sim - codeSign: no - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: iOS_Build_Min_Xcode - action: build - target: runner - sdk: sim - iphoneModel: $(MIN_IPHONE_DEVICE_NAME) - ipadModel: $(MIN_IPAD_DEVICE_NAME) - iosVersion: $(MIN_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: tvOS_Build_Min_Xcode - action: build - target: tv_runner - sdk: tv_sim - dest: tv - tvModel: $(MIN_TV_DEVICE_NAME) - tvVersion: $(MIN_TV_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - # endregion - - # region Analyze - - template: ./azure-templates/base_job.yml - parameters: - name: iOS_Lib_Analyze_Max_Xcode - action: analyze - sdk: sim - target: lib - iphoneModel: $(MAX_IPHONE_DEVICE_NAME) - ipadModel: $(MAX_IPAD_DEVICE_NAME) - iosVersion: $(MAX_PLATFORM_VERSION) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: iOS_Runner_Analyze_Max_Xcode - action: analyze - sdk: sim - target: runner - iphoneModel: $(MAX_IPHONE_DEVICE_NAME) - ipadModel: $(MAX_IPAD_DEVICE_NAME) - iosVersion: $(MAX_PLATFORM_VERSION) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: tvOS_Lib_Analyze_Max_Xcode - action: analyze - target: tv_lib - sdk: tv_sim - tvModel: $(MAX_TV_DEVICE_NAME) - tvVersion: $(MAX_PLATFORM_VERSION_TV) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: tvOS_Runner_Analyze_Max_Xcode - action: analyze - target: tv_runner - sdk: tv_sim - tvModel: $(MAX_TV_DEVICE_NAME) - tvVersion: $(MAX_PLATFORM_VERSION_TV) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: iOS_Lib_Analyze_Min_Xcode - action: analyze - target: lib - sdk: sim - iphoneModel: $(MIN_IPHONE_DEVICE_NAME) - ipadModel: $(MIN_IPAD_DEVICE_NAME) - iosVersion: $(MIN_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: iOS_Runner_Analyze_Min_Xcode - action: analyze - target: runner - sdk: sim - iphoneModel: $(MIN_IPHONE_DEVICE_NAME) - ipadModel: $(MIN_IPAD_DEVICE_NAME) - iosVersion: $(MIN_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: tvOS_Lib_Analyze_Min_Xcode - action: analyze - target: tv_lib - sdk: tv_sim - tvModel: $(MIN_TV_DEVICE_NAME) - tvVersion: $(MIN_TV_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: tvOS_Runner_Analyze_Min_Xcode - action: analyze - target: tv_runner - sdk: tv_sim - tvModel: $(MIN_TV_DEVICE_NAME) - tvVersion: $(MIN_TV_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - # endregion - - # region Unit Tests - - template: ./azure-templates/base_job.yml - parameters: - name: iPhone_Unit_Test_Max_Xcode - action: unit_test - dest: iphone - target: lib - sdk: sim - iphoneModel: $(MAX_IPHONE_DEVICE_NAME) - ipadModel: $(MAX_IPAD_DEVICE_NAME) - iosVersion: $(MAX_PLATFORM_VERSION) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: iPad_Unit_Test_Max_Xcode - action: unit_test - dest: ipad - target: lib - sdk: sim - iphoneModel: $(MAX_IPHONE_DEVICE_NAME) - ipadModel: $(MAX_IPAD_DEVICE_NAME) - iosVersion: $(MAX_PLATFORM_VERSION) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: tvOS_Unit_Test_Max_Xcode - action: tv_unit_test - dest: tv - target: tv_lib - sdk: tv_sim - tvModel: $(MAX_TV_DEVICE_NAME) - tvVersion: $(MAX_PLATFORM_VERSION_TV) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: iPhone_Unit_Test_Min_Xcode - action: unit_test - dest: iphone - target: lib - sdk: sim - iphoneModel: $(MIN_IPHONE_DEVICE_NAME) - ipadModel: $(MIN_IPAD_DEVICE_NAME) - iosVersion: $(MIN_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: iPad_Unit_Test_Min_Xcode - action: unit_test - dest: ipad - target: lib - sdk: sim - iphoneModel: $(MIN_IPHONE_DEVICE_NAME) - ipadModel: $(MIN_IPAD_DEVICE_NAME) - iosVersion: $(MIN_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - - template: ./azure-templates/base_job.yml - parameters: - name: tvOS_Unit_Test_Min_Xcode - action: tv_unit_test - dest: tv - target: tv_lib - sdk: tv_sim - tvModel: $(MIN_TV_DEVICE_NAME) - tvVersion: $(MIN_TV_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - # endregion - -- stage: Integration_Tests - jobs: - - # region Integration Tests Max Xcode - - ${{ each job in parameters.integrationJobs }}: - - template: ./azure-templates/base_job.yml - parameters: - name: ${{ job.dest }}_${{ job.action }}_Max_Xcode - action: ${{ job.action }} - dest: ${{ job.dest }} - target: lib - sdk: sim - iphoneModel: $(MAX_IPHONE_DEVICE_NAME) - ipadModel: $(MAX_IPAD_DEVICE_NAME) - iosVersion: $(MAX_PLATFORM_VERSION) - xcodeVersion: $(MAX_XCODE_VERSION) - vmImage: $(MAX_VM_IMAGE) - # endregion - - # region Integration Tests Min Xcode - - ${{ each job in parameters.integrationJobs }}: - - template: ./azure-templates/base_job.yml - parameters: - name: ${{ job.dest }}_${{ job.action }}_Min_Xcode - action: ${{ job.action }} - dest: ${{ job.dest }} - target: lib - sdk: sim - iphoneModel: $(MIN_IPHONE_DEVICE_NAME) - ipadModel: $(MIN_IPAD_DEVICE_NAME) - iosVersion: $(MIN_PLATFORM_VERSION) - xcodeVersion: $(MIN_XCODE_VERSION) - vmImage: $(MIN_VM_IMAGE) - # endregion diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 84930aead..000000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -Resources -coverage -build diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3d7db0846..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "extends": ["@appium/eslint-config-appium-ts"], - "overrides": [ - { - "files": "test/**/*.js", - "rules": { - "func-names": "off", - "@typescript-eslint/no-var-requires": "off" - } - }, - { - "files": "Scripts/**/*.js", - "parserOptions": {"sourceType": "script"}, - "rules": { - "@typescript-eslint/no-var-requires": "off" - } - }, - { - "files": "ci-jobs/scripts/*.js", - "parserOptions": {"sourceType": "script"}, - "rules": { - "@typescript-eslint/no-var-requires": "off" - } - } - ], - "rules": { - "require-await": "error" - } -} diff --git a/.github/actions/xcode-setup/action.yml b/.github/actions/xcode-setup/action.yml new file mode 100644 index 000000000..63d4ec90c --- /dev/null +++ b/.github/actions/xcode-setup/action.yml @@ -0,0 +1,32 @@ +name: 'Setup Xcode Test Environment' +description: 'Common setup steps for Xcode tests' + +inputs: + xcode_version: + description: 'Xcode version to use' + required: true + ruby_version: + description: 'Ruby version to use' + required: false + default: '3.3' + +runs: + using: 'composite' + steps: + - uses: actions/setup-node@v6 + with: + node-version: lts/* + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: ${{ inputs.xcode_version }} + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ inputs.ruby_version }} + bundler-cache: true + - name: Install dependencies + run: | + bundle install + mkdir -p ./Resources/WebDriverAgent.bundle + shell: bash + diff --git a/.github/dependabot.yml b/.github/dependabot.yml index efc8b6d5b..e06673298 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -9,3 +9,14 @@ updates: commit-message: prefix: "chore" include: "scope" + +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + time: "11:00" + day: "tuesday" + open-pull-requests-limit: 5 + commit-message: + prefix: "ci" + include: "scope" diff --git a/.github/workflows/functional-test.yml b/.github/workflows/functional-test.yml index e5c1ebad9..4291078fd 100644 --- a/.github/workflows/functional-test.yml +++ b/.github/workflows/functional-test.yml @@ -2,35 +2,62 @@ name: Functional Tests on: [pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: test: - env: - CI: true - _FORCE_LOGS: 1 - XCODE_VERSION: 13.4 - DEVICE_NAME: iPhone 11 - PLATFORM_VERSION: 15.5 - # https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md - runs-on: macos-12 + strategy: + fail-fast: false + matrix: + test_targets: + - HOST_OS: 'macos-26' + XCODE_VERSION: '26.4' + IOS_VERSION: '26.4' + IOS_MODEL: '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' + + # https://github.com/actions/runner-images/blob/main/images/macos/macos-14-Readme.md + runs-on: ${{matrix.test_targets.HOST_OS}} steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version: lts/* + check-latest: true - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: "${{ env.XCODE_VERSION }}" + xcode-version: ${{matrix.test_targets.XCODE_VERSION}} - run: | npm install mkdir -p ./Resources/WebDriverAgent.bundle name: Install dev dependencies - - run: | - target_sim_id=$(xcrun simctl list devices available | grep "$DEVICE_NAME (" | cut -d "(" -f2 | cut -d ")" -f1) + - name: Prepare iOS simulator + env: + DEVICE_NAME: ${{matrix.test_targets.IOS_MODEL}} + PLATFORM_VERSION: ${{matrix.test_targets.IOS_VERSION}} + run: | + xcrun simctl list devices available open -Fn "$(xcode-select -p)/Applications/Simulator.app" - xcrun simctl bootstatus $target_sim_id -b - name: Preboot Simulator + 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 - run: npm run e2e-test name: Run functional tests + env: + CI: true + _FORCE_LOGS: 1 + _LOG_TIMESTAMP: 1 + DEVICE_NAME: ${{matrix.test_targets.IOS_MODEL}} + PLATFORM_VERSION: ${{matrix.test_targets.IOS_VERSION}} diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 9a192aec5..929df1336 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -1,15 +1,10 @@ name: Conventional Commits on: pull_request: - + types: [opened, edited, synchronize, reopened] jobs: lint: - name: https://www.conventionalcommits.org - runs-on: ubuntu-latest - steps: - - uses: beemojs/conventional-pr-action@v2 - with: - config-preset: angular - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: appium/appium-workflows/.github/workflows/pr-title.yml@main + with: + config-preset: angular diff --git a/.github/workflows/publish.js.yml b/.github/workflows/publish.js.yml index 92299e7a8..250a42e3e 100644 --- a/.github/workflows/publish.js.yml +++ b/.github/workflows/publish.js.yml @@ -8,21 +8,81 @@ on: push: branches: [ master ] +permissions: + contents: write + pull-requests: write + issues: write + id-token: write # to enable use of OIDC for trusted publishing and npm provenance + +env: + # DO NOT USE 26.4+ for a while since it could drop lower iOS versions forcefully + # while the project config allows such lower versions. + # (at least WDA failed to start on iOS 15) + # Xcode 26.3 looks like it's working as expected. + XCODE_VERSION: '16.4' + # 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) + jobs: - build: - runs-on: macos-13 + build_matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - id: set-matrix + run: | + cat < matrix.json + [ + {"name": "iOS Real Device", "build_script": "build-real.sh", "scheme": "WebDriverAgentRunner", "destination": "generic/platform=iOS", "derived_data_path": "appium_wda_ios", "wd": "appium_wda_ios/Build/Products/Debug-iphoneos", "zip_name": "WebDriverAgentRunner-Runner.zip", "artifact_name": "WebDriverAgentRunner-Runner"}, + {"name": "tvOS Real Device", "build_script": "build-real.sh", "scheme": "WebDriverAgentRunner_tvOS", "destination": "generic/platform=tvOS", "derived_data_path": "appium_wda_tvos", "wd": "appium_wda_tvos/Build/Products/Debug-appletvos", "zip_name": "WebDriverAgentRunner_tvOS-Runner.zip", "artifact_name": "WebDriverAgentRunner_tvOS-Runner"}, + {"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"} + ] + MATRIX_JSON + echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT - env: - XCODE_VERSION: 14.3.1 - ZIP_PKG_NAME_IOS: "WebDriverAgentRunner-Runner.zip" - PKG_PATH_IOS: "appium_wda_ios" - ZIP_PKG_NAME_TVOS: "WebDriverAgentRunner_tvOS-Runner.zip" - PKG_PATH_TVOS: "appium_wda_tvos" + build-wda: + needs: build_matrix + name: ${{ matrix.config.name }} + runs-on: macos-15 + strategy: + fail-fast: false + matrix: + config: ${{ fromJSON(needs.build_matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v6 + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: "${{ env.XCODE_VERSION }}" + - name: ${{ matrix.config.name }} + uses: nick-fields/retry@v4 + with: + timeout_minutes: 10 + max_attempts: 3 + command: | + export DERIVED_DATA_PATH="${{ matrix.config.derived_data_path }}" + export SCHEME="${{ matrix.config.scheme }}" + export DESTINATION="${{ matrix.config.destination }}" + export WD="${{ matrix.config.wd }}" + export ZIP_PKG_NAME="${{ matrix.config.zip_name }}" + export ARCHS="${{ matrix.config.archs || '' }}" + sh $GITHUB_WORKSPACE/Scripts/ci/${{ matrix.config.build_script }} + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.config.artifact_name }} + path: ${{ matrix.config.zip_name }} + release: + needs: build-wda + runs-on: macos-15 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v6 with: node-version: lts/* - uses: maxim-lobanov/setup-xcode@v1 @@ -34,43 +94,16 @@ jobs: name: Run build - run: npm run test name: Run test - - # building WDA packages - - name: Build iOS - run: | - xcodebuild clean build-for-testing \ - -project WebDriverAgent.xcodeproj \ - -derivedDataPath $PKG_PATH_IOS \ - -scheme WebDriverAgentRunner \ - -destination generic/platform=iOS \ - CODE_SIGNING_ALLOWED=NO ARCHS=arm64 - - name: Creating a zip of WebDriverAgentRunner-Runner.app for iOS after removing test frameworks - run: | - pushd appium_wda_ios/Build/Products/Debug-iphoneos - rm -rf WebDriverAgentRunner-Runner.app/Frameworks/XC*.framework - zip -r $ZIP_PKG_NAME_IOS WebDriverAgentRunner-Runner.app - popd - mv $PKG_PATH_IOS/Build/Products/Debug-iphoneos/$ZIP_PKG_NAME_IOS ./ - - name: Build tvOS - run: | - xcodebuild clean build-for-testing \ - -project WebDriverAgent.xcodeproj \ - -derivedDataPath $PKG_PATH_TVOS \ - -scheme WebDriverAgentRunner_tvOS \ - -destination generic/platform=tvOS \ - CODE_SIGNING_ALLOWED=NO ARCHS=arm64 - - name: Creating a zip of WebDriverAgentRunner-Runner.app for tvOS after removing test frameworks + - name: Download all artifacts + uses: actions/download-artifact@v8 + with: + path: ${{ runner.temp }}/artifacts-temp + - name: Move artifacts to root for release run: | - pushd appium_wda_tvos/Build/Products/Debug-appletvos - rm -rf WebDriverAgentRunner_tvOS-Runner.app/Frameworks/XC*.framework - zip -r $ZIP_PKG_NAME_TVOS WebDriverAgentRunner_tvOS-Runner.app - popd - mv $PKG_PATH_TVOS/Build/Products/Debug-appletvos/$ZIP_PKG_NAME_TVOS ./ - - # release tasks + find "${{ runner.temp }}/artifacts-temp" -name "*.zip" -type f -exec mv {} . \; + rm -rf "${{ runner.temp }}/artifacts-temp" - run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} name: Release diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index ef98e4ca6..bd57c7beb 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -4,30 +4,29 @@ on: [pull_request, push] jobs: - prepare_matrix: - runs-on: ubuntu-latest - outputs: - versions: ${{ steps.generate-matrix.outputs.versions }} - steps: - - name: Select 3 most recent LTS versions of Node.js - id: generate-matrix - run: echo "versions=$(curl -s https://endoflife.date/api/nodejs.json | jq -c '[[.[] | select(.lts != false)][:3] | .[].cycle | tonumber]')" >> "$GITHUB_OUTPUT" + node_matrix: + uses: appium/appium-workflows/.github/workflows/node-lts-matrix.yml@main - test: + node_test: needs: - - prepare_matrix + - node_matrix strategy: matrix: - node-version: ${{ fromJSON(needs.prepare_matrix.outputs.versions) }} - runs-on: macos-latest + node-version: ${{ fromJSON(needs.node_matrix.outputs.versions) }} + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - - run: npm install --no-package-lock + - uses: SocketDev/action@v1 + with: + mode: firewall-free + - run: sfw npm install --no-package-lock name: Install dev dependencies - run: npm run lint name: Run linter + - run: npm run format:check + name: Run Prettier check - run: npm run test name: Run unit tests diff --git a/.github/workflows/wda-package.yml b/.github/workflows/wda-package.yml index b4b4223f2..c841cec86 100644 --- a/.github/workflows/wda-package.yml +++ b/.github/workflows/wda-package.yml @@ -2,16 +2,12 @@ name: Building WebDriverAgent on: workflow_dispatch: - workflow_run: - workflows: ["Release"] - types: - - completed env: - HOST: macos-13 - XCODE_VERSION: 14.3.1 - DESTINATION_SIM: platform=iOS Simulator,name=iPhone 14 Pro - DESTINATION_SIM_tvOS: platform=tvOS Simulator,name=Apple TV + HOST: macos-15 + XCODE_VERSION: 16.4 + DESTINATION_SIM: platform=iOS Simulator,name=iPhone 17 + DESTINATION_SIM_tvOS: platform=tvOS Simulator,name=Apple TV 4K (3rd generation) jobs: host_machine: @@ -29,12 +25,14 @@ jobs: runs-on: ${{ needs.host_machine.outputs.host }} env: + PKG_NAME_IOS: "WebDriverAgentRunner-Runner" ZIP_PKG_NAME_IOS: "WebDriverAgentRunner-Runner.zip" + PKG_NAME_TVOS: "WebDriverAgentRunner_tvOS-Runner" ZIP_PKG_NAME_TVOS: "WebDriverAgentRunner_tvOS-Runner.zip" steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: "${{ env.XCODE_VERSION }}" @@ -57,12 +55,14 @@ jobs: ZIP_PKG_NAME: "${{ env.ZIP_PKG_NAME_TVOS }}" - name: Upload the built generic app package for iOS - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@master with: + name: "${{ env.PKG_NAME_IOS }}" path: "${{ env.ZIP_PKG_NAME_IOS }}" - name: Upload the built generic app package for tvOS - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@master with: + name: "${{ env.PKG_NAME_TVOS }}" path: "${{ env.ZIP_PKG_NAME_TVOS }}" for_simulator_devices: @@ -72,12 +72,22 @@ jobs: strategy: matrix: - # '' is for iOS - target: ['', '_tvOS'] - arch: [x86_64, arm64] + include: + - target: '' + arch: x86_64 + simulator_name: Debug-iphonesimulator + - target: '' + arch: arm64 + simulator_name: Debug-iphonesimulator + - target: '_tvOS' + arch: x86_64 + simulator_name: Debug-appletvsimulator + - target: '_tvOS' + arch: arm64 + simulator_name: Debug-appletvsimulator steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: "${{ env.XCODE_VERSION }}" @@ -85,11 +95,13 @@ jobs: run: | DESTINATION=$DESTINATION_SIM${{ matrix.target }} sh $GITHUB_WORKSPACE/Scripts/ci/build-sim.sh env: - TARGET: ${{ matrix.target }} SCHEME: WebDriverAgentRunner${{ matrix.target }} ARCHS: ${{ matrix.arch }} ZIP_PKG_NAME: "WebDriverAgentRunner${{ matrix.target }}-Build-Sim-${{ matrix.arch }}.zip" + DERIVED_DATA_PATH: wda_build + WD: wda_build/Build/Products/${{ matrix.simulator_name }} - name: Upload the built generic app package for WebDriverAgentRunner${{ matrix.target }} with ${{ matrix.arch }} - uses: actions/upload-artifact@v3.1.0 + uses: actions/upload-artifact@master with: + name: "WebDriverAgentRunner${{ matrix.target }}-Build-Sim-${{ matrix.arch }}" path: "WebDriverAgentRunner${{ matrix.target }}-Build-Sim-${{ matrix.arch }}.zip" diff --git a/.github/workflows/wda-tests.yml b/.github/workflows/wda-tests.yml new file mode 100644 index 000000000..5c1530a8e --- /dev/null +++ b/.github/workflows/wda-tests.yml @@ -0,0 +1,221 @@ +name: WDA Tests + +on: [pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + 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_TV_DEVICE_NAME: "Apple TV 4K (3rd generation)" + MIN_IPHONE_DEVICE_NAME: "iPhone 15 Plus" + MIN_IPAD_DEVICE_NAME: "iPad Air 13-inch (M2)" + MAX_VM_IMAGE: macos-15 + MAX_XCODE_VERSION: "26.1" + MAX_PLATFORM_VERSION: "26.1" + MAX_TV_PLATFORM_VERSION: "26.1" + MAX_IPHONE_DEVICE_NAME: "iPhone 17" + MAX_TV_DEVICE_NAME: "Apple TV 4K (3rd generation)" + MAX_IPAD_DEVICE_NAME: "iPad Air 11-inch (M2)" + +jobs: + build_matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - id: set-matrix + run: | + cat <<'MATRIX_JSON' | jq -c . > matrix.json + [ + {"name": "Generic_iOS_Build_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "build", "target": "runner", "sdk": "sim", "dest": "generic", "code_sign": "no"}, + {"name": "Generic_iOS_Build_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "build", "target": "runner", "sdk": "sim", "dest": "generic", "code_sign": "no"}, + {"name": "Generic_tvOS_Build_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "build", "target": "tv_runner", "sdk": "tv_sim", "dest": "tv_generic", "code_sign": "no"}, + {"name": "Generic_tvOS_Build_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "build", "target": "tv_runner", "sdk": "tv_sim", "dest": "tv_generic", "code_sign": "no"}, + {"name": "iOS_Build_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "build", "target": "runner", "sdk": "sim", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "iOS_Build_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "build", "target": "runner", "sdk": "sim", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, + {"name": "tvOS_Build_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "action": "build", "target": "tv_runner", "sdk": "tv_sim", "tv_model": "${{ env.MAX_TV_DEVICE_NAME }}", "tv_version": "${{ env.MAX_TV_PLATFORM_VERSION }}"}, + {"name": "tvOS_Build_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "action": "build", "target": "tv_runner", "sdk": "tv_sim", "dest": "tv", "tv_model": "${{ env.MIN_TV_DEVICE_NAME }}", "tv_version": "${{ env.MIN_TV_PLATFORM_VERSION }}"} + ] + MATRIX_JSON + echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT + + analyze_matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - id: set-matrix + run: | + cat <<'MATRIX_JSON' | jq -c . > matrix.json + [ + {"name": "iOS_Lib_Analyze_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "target": "lib", "sdk": "sim", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "iOS_Lib_Analyze_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "target": "lib", "sdk": "sim", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, + {"name": "iOS_Runner_Analyze_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "target": "runner", "sdk": "sim", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "iOS_Runner_Analyze_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "target": "runner", "sdk": "sim", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"}, + {"name": "tvOS_Lib_Analyze_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "target": "tv_lib", "sdk": "tv_sim", "tv_model": "${{ env.MAX_TV_DEVICE_NAME }}", "tv_version": "${{ env.MAX_TV_PLATFORM_VERSION }}"}, + {"name": "tvOS_Lib_Analyze_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "target": "tv_lib", "sdk": "tv_sim", "tv_model": "${{ env.MIN_TV_DEVICE_NAME }}", "tv_version": "${{ env.MIN_TV_PLATFORM_VERSION }}"}, + {"name": "tvOS_Runner_Analyze_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "target": "tv_runner", "sdk": "tv_sim", "tv_model": "${{ env.MAX_TV_DEVICE_NAME }}", "tv_version": "${{ env.MAX_TV_PLATFORM_VERSION }}"}, + {"name": "tvOS_Runner_Analyze_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "target": "tv_runner", "sdk": "tv_sim", "tv_model": "${{ env.MIN_TV_DEVICE_NAME }}", "tv_version": "${{ env.MIN_TV_PLATFORM_VERSION }}"} + ] + MATRIX_JSON + echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT + + unit_test_matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - id: set-matrix + run: | + cat <<'MATRIX_JSON' | jq -c . > matrix.json + [ + {"name": "iPhone_Unit_Test_Max_Xcode", "vm_image": "${{ env.MAX_VM_IMAGE }}", "xcode_version": "${{ env.MAX_XCODE_VERSION }}", "iphone_model": "${{ env.MAX_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MAX_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MAX_PLATFORM_VERSION }}"}, + {"name": "iPhone_Unit_Test_Min_Xcode", "vm_image": "${{ env.MIN_VM_IMAGE }}", "xcode_version": "${{ env.MIN_XCODE_VERSION }}", "iphone_model": "${{ env.MIN_IPHONE_DEVICE_NAME }}", "ipad_model": "${{ env.MIN_IPAD_DEVICE_NAME }}", "ios_version": "${{ env.MIN_PLATFORM_VERSION }}"} + ] + MATRIX_JSON + echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT + + integration_test_matrix: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - id: set-matrix + 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 }}"} + ] + MATRIX_JSON + echo "matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT + + build: + needs: build_matrix + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.vm_image }} + strategy: + fail-fast: false + matrix: + config: ${{ fromJSON(needs.build_matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/xcode-setup + with: + xcode_version: ${{ matrix.config.xcode_version }} + - name: Build + run: ./Scripts/build.sh + env: + ACTION: ${{ matrix.config.action }} + TARGET: ${{ matrix.config.target }} + SDK: ${{ matrix.config.sdk }} + DEST: ${{ matrix.config.dest }} + CODE_SIGN: ${{ matrix.config.code_sign }} + 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 }} + + analyze: + needs: analyze_matrix + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.vm_image }} + strategy: + fail-fast: false + matrix: + config: ${{ fromJSON(needs.analyze_matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/xcode-setup + with: + xcode_version: ${{ matrix.config.xcode_version }} + - name: Analyze + run: ./Scripts/build.sh + env: + ACTION: analyze + TARGET: ${{ matrix.config.target }} + 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 }} + + unit_test: + needs: unit_test_matrix + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.vm_image }} + strategy: + fail-fast: false + matrix: + config: ${{ fromJSON(needs.unit_test_matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/xcode-setup + with: + xcode_version: ${{ matrix.config.xcode_version }} + - name: Run unit tests + run: ./Scripts/build.sh + env: + ACTION: unit_test + DEST: iphone + TARGET: lib + SDK: sim + IPHONE_MODEL: ${{ matrix.config.iphone_model }} + IPAD_MODEL: ${{ matrix.config.ipad_model }} + IOS_VERSION: ${{ matrix.config.ios_version }} + + integration_test: + needs: integration_test_matrix + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.vm_image }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + config: ${{ fromJSON(needs.integration_test_matrix.outputs.matrix) }} + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/xcode-setup + with: + xcode_version: ${{ matrix.config.xcode_version }} + # - name: Start iOS Simulator UI + # run: open -Fn "$(xcode-select --print-path)/Applications/Simulator.app" + # - name: Prepare iOS simulator + # id: prepareSimulator + # uses: futureware-tech/simulator-action@v4 + # with: + # model: "${{ matrix.config.dest == 'iphone' && matrix.config.iphone_model || matrix.config.ipad_model }}" + # os_version: "${{ matrix.config.ios_version }}" + # shutdown_after_job: false + # wait_for_boot: true + # - name: Finalize iOS simulator boot + # run: xcrun --sdk iphonesimulator --show-sdk-version + - name: Run integration test + run: ./Scripts/build.sh + env: + ACTION: ${{ matrix.config.action }} + DEST: ${{ matrix.config.dest }} + TARGET: lib + SDK: sim + IPHONE_MODEL: ${{ matrix.config.iphone_model }} + IPAD_MODEL: ${{ matrix.config.ipad_model }} + IOS_VERSION: ${{ matrix.config.ios_version }} + FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 600 + FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 2 diff --git a/.gitignore b/.gitignore index bc5a943fe..75359860f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build/ clang/ DerivedData +wdaBuild/ ## Various settings *.pbxuser diff --git a/.releaserc b/.releaserc index 0cb17f220..3987c1e97 100644 --- a/.releaserc +++ b/.releaserc @@ -35,7 +35,11 @@ ["@semantic-release/github", { "assets": [ "WebDriverAgentRunner-Runner.zip", - "WebDriverAgentRunner_tvOS-Runner.zip" + "WebDriverAgentRunner_tvOS-Runner.zip", + "WebDriverAgentRunner-Build-Sim-arm64.zip", + "WebDriverAgentRunner-Build-Sim-x86_64.zip", + "WebDriverAgentRunner_tvOS-Build-Sim-arm64.zip", + "WebDriverAgentRunner_tvOS-Build-Sim-x86_64.zip" ]}] ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index e4eb6d86a..abb4f7859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,1090 @@ +## [13.2.0](https://github.com/appium/WebDriverAgent/compare/v13.1.3...v13.2.0) (2026-05-26) + +### Features + +* Add XPath extensions ([#1144](https://github.com/appium/WebDriverAgent/issues/1144)) ([a975b89](https://github.com/appium/WebDriverAgent/commit/a975b89ac998d31a72bf3723b843d85af8867cf0)) + +## [13.1.3](https://github.com/appium/WebDriverAgent/compare/v13.1.2...v13.1.3) (2026-05-24) + +### Bug Fixes + +* Scheme for derived data path retrieval ([#1142](https://github.com/appium/WebDriverAgent/issues/1142)) ([9ada5f6](https://github.com/appium/WebDriverAgent/commit/9ada5f6fe2af29278c488e845f8714f22fabfeee)) + +## [13.1.2](https://github.com/appium/WebDriverAgent/compare/v13.1.1...v13.1.2) (2026-05-23) + +### Bug Fixes + +* Address compilation warnings ([#1143](https://github.com/appium/WebDriverAgent/issues/1143)) ([f1f9976](https://github.com/appium/WebDriverAgent/commit/f1f9976f4a0a0fb8a8aa3ee1f2483b25275600e6)) + +## [13.1.1](https://github.com/appium/WebDriverAgent/compare/v13.1.0...v13.1.1) (2026-05-22) + +### Bug Fixes + +* ship Scripts/embed-runner-icon.sh in the npm package ([#1141](https://github.com/appium/WebDriverAgent/issues/1141)) ([17ac1c1](https://github.com/appium/WebDriverAgent/commit/17ac1c16a0890ee0fbfe73504a3ff570dfe1a7bf)), closes [#1138](https://github.com/appium/WebDriverAgent/issues/1138) + +## [13.1.0](https://github.com/appium/WebDriverAgent/compare/v13.0.0...v13.1.0) (2026-05-21) + +### Features + +* add app icon to WebDriverAgentRunner ([#1138](https://github.com/appium/WebDriverAgent/issues/1138)) ([fe8adc8](https://github.com/appium/WebDriverAgent/commit/fe8adc89923994428783397170de850e11ebb3c6)) +* Add helper method to fetch build settings ([#1139](https://github.com/appium/WebDriverAgent/issues/1139)) ([56b5f38](https://github.com/appium/WebDriverAgent/commit/56b5f384ed9ba1a014d4b642ddf26b8573ceaafe)) + +## [13.0.0](https://github.com/appium/WebDriverAgent/compare/v12.2.2...v13.0.0) (2026-05-17) + +### ⚠ BREAKING CHANGES + +* quitAndUninstall() removed — use quit() only. App uninstall is out of scope for this module. +* uninstall() removed — WDA must not be uninstalled from this package; callers (e.g. xcuitest-driver) should own that if needed. +* setupCaching() no longer uninstalls WDA — on bundle-id or version mismatch it logs and skips caching instead of removing apps from the device. Also, it now returns the cached url on success. +* appium-ios-device dependency removed — preinstalled WDA on real devices always launches via devicectl (no iOS < 17 Xctest fallback). + +### Features + +* Drop legacy APIs ([#1137](https://github.com/appium/WebDriverAgent/issues/1137)) ([8995d24](https://github.com/appium/WebDriverAgent/commit/8995d24e16634a4624918319996839993502c7b4)) + +## [12.2.2](https://github.com/appium/WebDriverAgent/compare/v12.2.1...v12.2.2) (2026-05-08) + +### Bug Fixes + +* linter ([#1134](https://github.com/appium/WebDriverAgent/issues/1134)) ([2bd1816](https://github.com/appium/WebDriverAgent/commit/2bd181628a1d4525a8f1c459ea295ac0541b514c)) + +## [12.2.1](https://github.com/appium/WebDriverAgent/compare/v12.2.0...v12.2.1) (2026-05-06) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 21.1.2 to 22.0.0 ([#1133](https://github.com/appium/WebDriverAgent/issues/1133)) ([11c579b](https://github.com/appium/WebDriverAgent/commit/11c579b7ed3a9995715d65590a2959763871aa6d)) + +## [12.2.0](https://github.com/appium/WebDriverAgent/compare/v12.1.1...v12.2.0) (2026-04-29) + +### Features + +* Ditch bluebird and lodash ([#1130](https://github.com/appium/WebDriverAgent/issues/1130)) ([8899895](https://github.com/appium/WebDriverAgent/commit/88998951f004daed1d22ce2c06eec89a08129e4f)) + +## [12.1.1](https://github.com/appium/WebDriverAgent/compare/v12.1.0...v12.1.1) (2026-04-27) + +### Miscellaneous Chores + +* **compile:** fix compilation ([#1129](https://github.com/appium/WebDriverAgent/issues/1129)) ([76d59e8](https://github.com/appium/WebDriverAgent/commit/76d59e85c75680c97abe9e67fdf4a70cacd46418)) + +## [12.1.0](https://github.com/appium/WebDriverAgent/compare/v12.0.0...v12.1.0) (2026-04-24) + +### Features + +* **client:** add ability to set headers on requests ([#1127](https://github.com/appium/WebDriverAgent/issues/1127)) ([a8889cd](https://github.com/appium/WebDriverAgent/commit/a8889cd7cb85c1b58faee306295fb3c5c2a9d0e3)) + +## [12.0.0](https://github.com/appium/WebDriverAgent/compare/v11.4.3...v12.0.0) (2026-04-14) + +### ⚠ BREAKING CHANGES + +* remove idb from AppleDevice; clients must stop passing device.idb. +* remove includeNonModalElements WDA setting; clients must stop sending this setting in /settings. +* remove shouldUseTestManagerForVisibilityDetection capability; clients must stop sending this desired capability. + +### Code Refactoring + +* remove deprecated WDA settings/capabilities and idb typing ([#1124](https://github.com/appium/WebDriverAgent/issues/1124)) ([5072e25](https://github.com/appium/WebDriverAgent/commit/5072e255faa3538f5ff4c8769bf16fd290ee8af9)) + +## [11.4.3](https://github.com/appium/WebDriverAgent/compare/v11.4.2...v11.4.3) (2026-04-12) + +### Miscellaneous Chores + +* **deps-dev:** bump typescript from 5.9.3 to 6.0.2 ([#1121](https://github.com/appium/WebDriverAgent/issues/1121)) ([046b080](https://github.com/appium/WebDriverAgent/commit/046b08042df33f507466f55b68b444c91684931a)) + +## [11.4.2](https://github.com/appium/WebDriverAgent/compare/v11.4.1...v11.4.2) (2026-04-12) + +### Bug Fixes + +* Avoid keeping strong reference to self instance in delegates ([#1123](https://github.com/appium/WebDriverAgent/issues/1123)) ([dd15f48](https://github.com/appium/WebDriverAgent/commit/dd15f48d33edabef6aea8ac951cf539946b492f2)) + +## [11.4.1](https://github.com/appium/WebDriverAgent/compare/v11.4.0...v11.4.1) (2026-03-15) + +### Bug Fixes + +* Add exponential backoff interval on consecutive screenshot failures ([#1119](https://github.com/appium/WebDriverAgent/issues/1119)) ([8aa1c5b](https://github.com/appium/WebDriverAgent/commit/8aa1c5b81363f45a4046161099ad624febdc0802)) + +## [11.4.0](https://github.com/appium/WebDriverAgent/compare/v11.3.0...v11.4.0) (2026-03-08) + +### Features + +* make maxChildren configuable ([#1117](https://github.com/appium/WebDriverAgent/issues/1117)) ([57dd6dc](https://github.com/appium/WebDriverAgent/commit/57dd6dce1677e84740c13aec2742872d11d64526)) + +## [11.3.0](https://github.com/appium/WebDriverAgent/compare/v11.2.0...v11.3.0) (2026-03-05) + +### Features + +* add 6 tvOS button values for `mobile: pressButton` ([#1116](https://github.com/appium/WebDriverAgent/issues/1116)) ([efd64ed](https://github.com/appium/WebDriverAgent/commit/efd64ede7212b322f412cd6b25eab9c8097c286d)) + +## [11.2.0](https://github.com/appium/WebDriverAgent/compare/v11.1.7...v11.2.0) (2026-03-04) + +### Features + +* add `action` and `camera` values for `mobile: pressButton` ([#1115](https://github.com/appium/WebDriverAgent/issues/1115)) ([3df0284](https://github.com/appium/WebDriverAgent/commit/3df0284d741f8b4a36a9e12e130ecf0711b60366)) + +## [11.1.7](https://github.com/appium/WebDriverAgent/compare/v11.1.6...v11.1.7) (2026-03-03) + +### Miscellaneous Chores + +* **deps:** bump actions/download-artifact from 7 to 8 ([#1114](https://github.com/appium/WebDriverAgent/issues/1114)) ([b3de174](https://github.com/appium/WebDriverAgent/commit/b3de174fd5793f12d9976c6e1567be7bda89ef79)) +* **deps:** bump actions/upload-artifact from 6 to 7 ([#1113](https://github.com/appium/WebDriverAgent/issues/1113)) ([981757c](https://github.com/appium/WebDriverAgent/commit/981757c958217a0a3a9f9070c8bc72bfa9fd6076)) + +## [11.1.6](https://github.com/appium/WebDriverAgent/compare/v11.1.5...v11.1.6) (2026-02-23) + +### Bug Fixes + +* bump the minimum deployment target and apply recommend settings with xcode 26 ([#1112](https://github.com/appium/WebDriverAgent/issues/1112)) ([681ffcc](https://github.com/appium/WebDriverAgent/commit/681ffccd04fe5b23295d72f23cb94df9e385af48)) + +## [11.1.5](https://github.com/appium/WebDriverAgent/compare/v11.1.4...v11.1.5) (2026-02-16) + +### Bug Fixes + +* format ([#1111](https://github.com/appium/WebDriverAgent/issues/1111)) ([f099df1](https://github.com/appium/WebDriverAgent/commit/f099df154676c71d4a696c5cdfad1ae336a4f750)) + +## [11.1.4](https://github.com/appium/WebDriverAgent/compare/v11.1.3...v11.1.4) (2026-02-01) + +### Miscellaneous Chores + +* **deps:** bump asyncbox from 4.1.1 to 6.1.0 ([#1109](https://github.com/appium/WebDriverAgent/issues/1109)) ([eb0ef79](https://github.com/appium/WebDriverAgent/commit/eb0ef7919fb80e42db287ef8fc476b921167bfd8)) + +## [11.1.3](https://github.com/appium/WebDriverAgent/compare/v11.1.2...v11.1.3) (2026-01-28) + +### Miscellaneous Chores + +* **deps-dev:** bump @appium/eslint-config-appium-ts from 2.0.5 to 3.0.0 ([#1107](https://github.com/appium/WebDriverAgent/issues/1107)) ([41f3eae](https://github.com/appium/WebDriverAgent/commit/41f3eaeaf1380d5d4ab60ef66d125dd745354d5d)) + +## [11.1.2](https://github.com/appium/WebDriverAgent/compare/v11.1.1...v11.1.2) (2026-01-23) + +### Miscellaneous Chores + +* Exclude tests from published dist ([cc29734](https://github.com/appium/WebDriverAgent/commit/cc2973486d4c09835ef6317a10554d8fb2e094be)) + +## [11.1.1](https://github.com/appium/WebDriverAgent/compare/v11.1.0...v11.1.1) (2026-01-21) + +### Bug Fixes + +* Avoid modifying WDA sources ([#1101](https://github.com/appium/WebDriverAgent/issues/1101)) ([5f2a00f](https://github.com/appium/WebDriverAgent/commit/5f2a00faa38fac524a8eb229df500cdf6140bc2d)) + +## [11.1.0](https://github.com/appium/WebDriverAgent/compare/v11.0.2...v11.1.0) (2025-12-28) + +### Features + +* expose customActions on the element ([#1095](https://github.com/appium/WebDriverAgent/issues/1095)) ([3028512](https://github.com/appium/WebDriverAgent/commit/302851237297ee31965c55a75436ad0aea2a8c8f)) + +## [11.0.2](https://github.com/appium/WebDriverAgent/compare/v11.0.1...v11.0.2) (2025-12-25) + +### Miscellaneous Chores + +* **deps:** bump actions/download-artifact from 4 to 7 ([#1100](https://github.com/appium/WebDriverAgent/issues/1100)) ([6098b67](https://github.com/appium/WebDriverAgent/commit/6098b67796a83448ce0223b70055bff718cb80fe)) +* **deps:** bump actions/upload-artifact from 4 to 6 ([#1099](https://github.com/appium/WebDriverAgent/issues/1099)) ([e7b69ec](https://github.com/appium/WebDriverAgent/commit/e7b69ec3518600f44d6ab86f1547e4ccfac5207f)) + +## [11.0.1](https://github.com/appium/WebDriverAgent/compare/v11.0.0...v11.0.1) (2025-12-23) + +### Miscellaneous Chores + +* update teen-process ([#1098](https://github.com/appium/WebDriverAgent/issues/1098)) ([a97034c](https://github.com/appium/WebDriverAgent/commit/a97034ce92007964ac9f149f97ebfc8d8f9c2d21)) + +## [11.0.0](https://github.com/appium/WebDriverAgent/compare/v10.5.4...v11.0.0) (2025-12-21) + +### ⚠ BREAKING CHANGES + +* Removed the deprecated xcodeVersion argument from WebDriverAgent constructor +* Removed the deprecated xcodeVersion argument from XcodeBuild constructor +* Removed the deprecated idb property from WebDriverAgent class +* Removed the noop checkForDependencies export from index + +- All .js modules were migrated to TypeScript +- Module scripts were migrated to ESM/.mjs +- All private WebDriverAgent and XcodeBuild methods and properties were marked as such + +### Features + +* Migrate the module to typescript ([#1096](https://github.com/appium/WebDriverAgent/issues/1096)) ([7d0a022](https://github.com/appium/WebDriverAgent/commit/7d0a022585ef70205dafe22d20736978d9e1cba3)) + +## [10.5.4](https://github.com/appium/WebDriverAgent/compare/v10.5.3...v10.5.4) (2025-12-19) + +### Miscellaneous Chores + +* Apply format to socket helpers ([#1092](https://github.com/appium/WebDriverAgent/issues/1092)) ([ddc6313](https://github.com/appium/WebDriverAgent/commit/ddc631371350ef04ab8f49a93f1f674cab9aefde)) + +## [10.5.3](https://github.com/appium/WebDriverAgent/compare/v10.5.2...v10.5.3) (2025-12-19) + +### Miscellaneous Chores + +* **ci:** Fix collection of release artifacts ([#1094](https://github.com/appium/WebDriverAgent/issues/1094)) ([d1bf689](https://github.com/appium/WebDriverAgent/commit/d1bf689d390c399d6f80c6a2e786e14f73157f93)) + +## [10.5.2](https://github.com/appium/WebDriverAgent/compare/v10.5.1...v10.5.2) (2025-12-18) + +### Bug Fixes + +* Address possible NULL binding compiler warning ([#1091](https://github.com/appium/WebDriverAgent/issues/1091)) ([263e646](https://github.com/appium/WebDriverAgent/commit/263e646dcbfe95a16c30821f2f8fe1cf7e1a0cce)) + +## [10.5.1](https://github.com/appium/WebDriverAgent/compare/v10.5.0...v10.5.1) (2025-12-18) + +### Miscellaneous Chores + +* **deps:** bump asyncbox from 3.0.0 to 4.0.1 ([#1090](https://github.com/appium/WebDriverAgent/issues/1090)) ([92228ba](https://github.com/appium/WebDriverAgent/commit/92228ba2f1a75e4d7f7bba67e5dac1c625a27efe)) + +## [10.5.0](https://github.com/appium/WebDriverAgent/compare/v10.4.5...v10.5.0) (2025-12-18) + +### Features + +* Add `enforceCustomSnapshots` setting ([#1087](https://github.com/appium/WebDriverAgent/issues/1087)) ([33d780a](https://github.com/appium/WebDriverAgent/commit/33d780a4b7b47f0587b25983d4e0eb3738975904)) + +## [10.4.5](https://github.com/appium/WebDriverAgent/compare/v10.4.4...v10.4.5) (2025-12-13) + +### Miscellaneous Chores + +* **deps:** remove source-map-support ([#1084](https://github.com/appium/WebDriverAgent/issues/1084)) ([a803089](https://github.com/appium/WebDriverAgent/commit/a8030894f38eed327c90e10c92ac0fbf0dab8239)) + +## [10.4.4](https://github.com/appium/WebDriverAgent/compare/v10.4.3...v10.4.4) (2025-12-13) + +### Miscellaneous Chores + +* Migrate tests to typescript ([#1083](https://github.com/appium/WebDriverAgent/issues/1083)) ([6ac993e](https://github.com/appium/WebDriverAgent/commit/6ac993e29ab02a71f5aac5354bc8408110f3fe6d)) + +## [10.4.3](https://github.com/appium/WebDriverAgent/compare/v10.4.2...v10.4.3) (2025-12-12) + +### Miscellaneous Chores + +* Bump Xcode platform versions for Azure tests ([#1082](https://github.com/appium/WebDriverAgent/issues/1082)) ([c698bd5](https://github.com/appium/WebDriverAgent/commit/c698bd5bef3009a83e0c48a39d53e84ac336fb72)) + +## [10.4.2](https://github.com/appium/WebDriverAgent/compare/v10.4.1...v10.4.2) (2025-12-11) + +### Miscellaneous Chores + +* **deps-dev:** bump @types/node from 24.10.3 to 25.0.0 ([#1081](https://github.com/appium/WebDriverAgent/issues/1081)) ([6157d8e](https://github.com/appium/WebDriverAgent/commit/6157d8edd12f0e40938fd38fa8473d84999ce360)) + +## [10.4.1](https://github.com/appium/WebDriverAgent/compare/v10.4.0...v10.4.1) (2025-12-07) + +### Miscellaneous Chores + +* Ditch usage of @appium/test-support ([#1080](https://github.com/appium/WebDriverAgent/issues/1080)) ([5c7cd2e](https://github.com/appium/WebDriverAgent/commit/5c7cd2ef2dec11b00dddd7a357bdaafd6df62e73)) + +## [10.4.0](https://github.com/appium/WebDriverAgent/compare/v10.3.0...v10.4.0) (2025-12-06) + +### Features + +* Migrate IntegrationApp to use UIScene lifecycle ([#1079](https://github.com/appium/WebDriverAgent/issues/1079)) ([dfba786](https://github.com/appium/WebDriverAgent/commit/dfba7863195651535a498c246e7b6461eaa24f8b)) + +## [10.3.0](https://github.com/appium/WebDriverAgent/compare/v10.2.7...v10.3.0) (2025-12-03) + +### Features + +* Deprecate CFNetwork usage ([#1078](https://github.com/appium/WebDriverAgent/issues/1078)) ([6df0c5f](https://github.com/appium/WebDriverAgent/commit/6df0c5fb442879a1af115f02ac79055a7ed76719)) + +## [10.2.7](https://github.com/appium/WebDriverAgent/compare/v10.2.6...v10.2.7) (2025-12-03) + +### Miscellaneous Chores + +* use any iphone/tvos simulator devices to build WDA for sim ([#1077](https://github.com/appium/WebDriverAgent/issues/1077)) ([9600d83](https://github.com/appium/WebDriverAgent/commit/9600d837bf35f420f71d00772fc682e3db9f257d)) + +## [10.2.6](https://github.com/appium/WebDriverAgent/compare/v10.2.5...v10.2.6) (2025-12-02) + +### Miscellaneous Chores + +* **deps:** bump actions/setup-node from 4 to 6 ([#1075](https://github.com/appium/WebDriverAgent/issues/1075)) ([5042063](https://github.com/appium/WebDriverAgent/commit/504206338a1d7038f1d95ef241006997c5512757)) + +## [10.2.5](https://github.com/appium/WebDriverAgent/compare/v10.2.4...v10.2.5) (2025-11-29) + +### Miscellaneous Chores + +* **deps:** bump actions/checkout from 4 to 6 ([#1076](https://github.com/appium/WebDriverAgent/issues/1076)) ([276be79](https://github.com/appium/WebDriverAgent/commit/276be795ad36ea4d3beddff47b0ed3aa2c1f9461)) + +## [10.2.4](https://github.com/appium/WebDriverAgent/compare/v10.2.3...v10.2.4) (2025-11-29) + +### Miscellaneous Chores + +* Deprecate idb ([#1073](https://github.com/appium/WebDriverAgent/issues/1073)) ([260bc31](https://github.com/appium/WebDriverAgent/commit/260bc319795aff26468eca261d5f286b31216270)) +* **deps:** bump appium-ios-simulator from 7.0.3 to 8.0.0 ([#1070](https://github.com/appium/WebDriverAgent/issues/1070)) ([7cb4b6e](https://github.com/appium/WebDriverAgent/commit/7cb4b6ecd37717c7aad214f8e387c404d4937534)) + +## [10.2.3](https://github.com/appium/WebDriverAgent/compare/v10.2.2...v10.2.3) (2025-11-29) + +### Bug Fixes + +* fix type and mark deprecated as no usage for unused xcodeVersion and deprecated idb ([#1072](https://github.com/appium/WebDriverAgent/issues/1072)) ([4499fb2](https://github.com/appium/WebDriverAgent/commit/4499fb22ae4884df84e5e2d2bd0570c90ee4848b)) + +## [10.2.2](https://github.com/appium/WebDriverAgent/compare/v10.2.1...v10.2.2) (2025-11-15) + +### Miscellaneous Chores + +* publish via trusted publisher ([#1068](https://github.com/appium/WebDriverAgent/issues/1068)) ([6321379](https://github.com/appium/WebDriverAgent/commit/6321379d3e97dfa846bf0ff6f0b8e8b9ef85ba1c)) + +## [10.2.1](https://github.com/appium/WebDriverAgent/compare/v10.2.0...v10.2.1) (2025-11-01) + +### Miscellaneous Chores + +* update xcodebuild commands for the new binding ip capability ([#1067](https://github.com/appium/WebDriverAgent/issues/1067)) ([d12f421](https://github.com/appium/WebDriverAgent/commit/d12f4214b958855022c21b4d700b6726740294c1)) + +## [10.2.0](https://github.com/appium/WebDriverAgent/compare/v10.1.4...v10.2.0) (2025-10-31) + +### Features + +* Let binding IP address to be configurable via USE_IP environment variable ([#1066](https://github.com/appium/WebDriverAgent/issues/1066)) ([70ed7cf](https://github.com/appium/WebDriverAgent/commit/70ed7cf0a74e0df3763b99f6155a7923dde17c9d)) + +## [10.1.4](https://github.com/appium/WebDriverAgent/compare/v10.1.3...v10.1.4) (2025-10-31) + +### Miscellaneous Chores + +* Improve type declarations ([#1065](https://github.com/appium/WebDriverAgent/issues/1065)) ([5aadcb8](https://github.com/appium/WebDriverAgent/commit/5aadcb8fa99459e7f8852f75d2549a76f3e55b07)) + +## [10.1.3](https://github.com/appium/WebDriverAgent/compare/v10.1.2...v10.1.3) (2025-10-17) + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 24.2.9 to 25.0.0 ([#1064](https://github.com/appium/WebDriverAgent/issues/1064)) ([6c2cffa](https://github.com/appium/WebDriverAgent/commit/6c2cffa4ee6fcd0c86ae7aa171f25cb800908932)) + +## [10.1.2](https://github.com/appium/WebDriverAgent/compare/v10.1.1...v10.1.2) (2025-10-08) + +### Miscellaneous Chores + +* Skip staleness checks for subelement lookups ([#1063](https://github.com/appium/WebDriverAgent/issues/1063)) ([ada7760](https://github.com/appium/WebDriverAgent/commit/ada77604f9fa9bfc85c61cabbd2a9f4de00aceb9)) + +## [10.1.1](https://github.com/appium/WebDriverAgent/compare/v10.1.0...v10.1.1) (2025-09-12) + +### Miscellaneous Chores + +* remove patents file ([#1061](https://github.com/appium/WebDriverAgent/issues/1061)) ([b001c4e](https://github.com/appium/WebDriverAgent/commit/b001c4e39ef71cb8b91ef7391b418f32a7ebe21c)) + +## [10.1.0](https://github.com/appium/WebDriverAgent/compare/v10.0.1...v10.1.0) (2025-09-03) + +### Features + +* Add process and bundle identifiers to the application node in the XML source ([#1055](https://github.com/appium/WebDriverAgent/issues/1055)) ([088cff2](https://github.com/appium/WebDriverAgent/commit/088cff2b2bc19ddde698ec06f1db37c6989cf392)) + +## [10.0.1](https://github.com/appium/WebDriverAgent/compare/v10.0.0...v10.0.1) (2025-08-23) + +### Miscellaneous Chores + +* **deps-dev:** bump chai from 5.3.2 to 6.0.0 ([#1053](https://github.com/appium/WebDriverAgent/issues/1053)) ([9e9ec38](https://github.com/appium/WebDriverAgent/commit/9e9ec381bd6695e1c8b89f2a9c304b12385c0134)) + +## [10.0.0](https://github.com/appium/WebDriverAgent/compare/v9.15.3...v10.0.0) (2025-08-17) + +### ⚠ BREAKING CHANGES + +* Required Node.js version has been bumped to ^20.19.0 || ^22.12.0 || >=24.0.0 +* Required npm version has been bumped to >=10 +* Required base driver version has been bumped to >=10.0.0-rc.1 + +### Features + +* Update server compatibility ([#1051](https://github.com/appium/WebDriverAgent/issues/1051)) ([f9ea1e5](https://github.com/appium/WebDriverAgent/commit/f9ea1e5e2f5306030387d5293f073b2a6fe658e7)) + +## [9.15.3](https://github.com/appium/WebDriverAgent/compare/v9.15.2...v9.15.3) (2025-08-12) + +### Miscellaneous Chores + +* Cache application instances for their PIDs ([#1049](https://github.com/appium/WebDriverAgent/issues/1049)) ([e9cbf64](https://github.com/appium/WebDriverAgent/commit/e9cbf640c21243c304b476a497f33802e0501a7d)) + +## [9.15.2](https://github.com/appium/WebDriverAgent/compare/v9.15.1...v9.15.2) (2025-08-04) + +### Miscellaneous Chores + +* bump appium-ios-device to 2.9.0 ([#1047](https://github.com/appium/WebDriverAgent/issues/1047)) ([305019d](https://github.com/appium/WebDriverAgent/commit/305019d4dde89853e44c58170e17ec23c89de2f3)) + +## [9.15.1](https://github.com/appium/WebDriverAgent/compare/v9.15.0...v9.15.1) (2025-07-17) + +### Miscellaneous Chores + +* Remove the redundant check after activating the system app ([#1043](https://github.com/appium/WebDriverAgent/issues/1043)) ([33ccba1](https://github.com/appium/WebDriverAgent/commit/33ccba1ab3bc2980349f8553fd30aa5b08141b6b)) + +## [9.15.0](https://github.com/appium/WebDriverAgent/compare/v9.14.6...v9.15.0) (2025-07-10) + +### Features + +* HTTPS support for wda-client if webDriverAgentUrl is set ([#1042](https://github.com/appium/WebDriverAgent/issues/1042)) ([f7c4193](https://github.com/appium/WebDriverAgent/commit/f7c41939c793cdbc62e9c14d8eb91e06957bb566)) + +## [9.14.6](https://github.com/appium/WebDriverAgent/compare/v9.14.5...v9.14.6) (2025-06-24) + +### Miscellaneous Chores + +* add missing arch ([#1039](https://github.com/appium/WebDriverAgent/issues/1039)) ([a8dd958](https://github.com/appium/WebDriverAgent/commit/a8dd958bd92ef685bc1798ec04e92080b798d7d2)) + +## [9.14.5](https://github.com/appium/WebDriverAgent/compare/v9.14.4...v9.14.5) (2025-06-24) + +### Miscellaneous Chores + +* keep entire app for simulators ([d2bbcc6](https://github.com/appium/WebDriverAgent/commit/d2bbcc6d7af6b8eea076e24cd18429b74eeaffd6)) + +## [9.14.4](https://github.com/appium/WebDriverAgent/compare/v9.14.3...v9.14.4) (2025-06-23) + +### Miscellaneous Chores + +* include wda sim prebuilt for gh release ([#1038](https://github.com/appium/WebDriverAgent/issues/1038)) ([4423ecb](https://github.com/appium/WebDriverAgent/commit/4423ecb4f23c50343d8ffbf56a7753b985cbab81)) + +## [9.14.3](https://github.com/appium/WebDriverAgent/compare/v9.14.2...v9.14.3) (2025-06-13) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 20.0.0 to 21.0.0 ([#1034](https://github.com/appium/WebDriverAgent/issues/1034)) ([5b205f4](https://github.com/appium/WebDriverAgent/commit/5b205f493f35cd1744cf9e33bce21e0f9e7c3bea)) + +## [9.14.2](https://github.com/appium/WebDriverAgent/compare/v9.14.1...v9.14.2) (2025-06-10) + +### Miscellaneous Chores + +* **deps-dev:** bump @types/node from 22.15.31 to 24.0.0 ([#1033](https://github.com/appium/WebDriverAgent/issues/1033)) ([e9705d9](https://github.com/appium/WebDriverAgent/commit/e9705d964e63222daaf0710bd3b860ca2ba6850f)) + +## [9.14.1](https://github.com/appium/WebDriverAgent/compare/v9.14.0...v9.14.1) (2025-06-09) + +### Miscellaneous Chores + +* add -Wno-reserved-identifier option ([#1032](https://github.com/appium/WebDriverAgent/issues/1032)) ([005dc21](https://github.com/appium/WebDriverAgent/commit/005dc216d9f41757763fe5b1714b68697fa8ee30)) + +## [9.14.0](https://github.com/appium/WebDriverAgent/compare/v9.13.0...v9.14.0) (2025-06-09) + +### Features + +* add minimum and maximum value attributes to page source ([#1031](https://github.com/appium/WebDriverAgent/issues/1031)) ([0e4e7e7](https://github.com/appium/WebDriverAgent/commit/0e4e7e7c483b9196edae576481f4e37f99fc8705)) + +## [9.13.0](https://github.com/appium/WebDriverAgent/compare/v9.12.0...v9.13.0) (2025-06-05) + +### Features + +* expose nativeFrame attribute in XML page source ([#1029](https://github.com/appium/WebDriverAgent/issues/1029)) ([5b56a45](https://github.com/appium/WebDriverAgent/commit/5b56a453f836cbc4358ce24ae43032658467c35c)) + +## [9.12.0](https://github.com/appium/WebDriverAgent/compare/v9.11.0...v9.12.0) (2025-06-04) + +### Features + +* add accessibility traits to XML page source ([#1028](https://github.com/appium/WebDriverAgent/issues/1028)) ([2df6649](https://github.com/appium/WebDriverAgent/commit/2df6649cb532d65a8c14633591b76c90185644cb)) + +## [9.11.0](https://github.com/appium/WebDriverAgent/compare/v9.10.1...v9.11.0) (2025-06-03) + +### Features + +* Add includeHittableInSource setting for including real hittable attribute in XML source ([#1026](https://github.com/appium/WebDriverAgent/issues/1026)) ([0fa4e74](https://github.com/appium/WebDriverAgent/commit/0fa4e7417404b5975445d381d111753fe681edd4)) + +## [9.10.1](https://github.com/appium/WebDriverAgent/compare/v9.10.0...v9.10.1) (2025-05-30) + +### Miscellaneous Chores + +* Make sure the same import style is used everywhere ([#1024](https://github.com/appium/WebDriverAgent/issues/1024)) ([1c50072](https://github.com/appium/WebDriverAgent/commit/1c50072457a8b82eec3684029386ccfa9432eccc)) + +## [9.10.0](https://github.com/appium/WebDriverAgent/compare/v9.9.0...v9.10.0) (2025-05-27) + +### Features + +* Add accessibility traits of the element ([#1020](https://github.com/appium/WebDriverAgent/issues/1020)) ([9465aaf](https://github.com/appium/WebDriverAgent/commit/9465aafd5e81ef57be7f78e9f2e188d3c1ba1bee)) + +### Bug Fixes + +* Use native snapshots if hittable attribute is requested in xPath ([#1023](https://github.com/appium/WebDriverAgent/issues/1023)) ([49d26cb](https://github.com/appium/WebDriverAgent/commit/49d26cb02a8515d1a1b52b65b7cb65512dfd749b)) + +## [9.9.0](https://github.com/appium/WebDriverAgent/compare/v9.8.0...v9.9.0) (2025-05-26) + +### Features + +* Use another snapshotting mechanism for the hittable attribute calculation ([#1022](https://github.com/appium/WebDriverAgent/issues/1022)) ([13c9f45](https://github.com/appium/WebDriverAgent/commit/13c9f453d890ad9b78fa7c47728ebae33880966a)) + +## [9.8.0](https://github.com/appium/WebDriverAgent/compare/v9.7.1...v9.8.0) (2025-05-21) + +### Features + +* Add a native frame property of the element ([#1017](https://github.com/appium/WebDriverAgent/issues/1017)) ([09214c4](https://github.com/appium/WebDriverAgent/commit/09214c4228ed5a49c02adead452cb0bb8dd83b6d)) + +## [9.7.1](https://github.com/appium/WebDriverAgent/compare/v9.7.0...v9.7.1) (2025-05-21) + +### Miscellaneous Chores + +* **deps-dev:** bump conventional-changelog-conventionalcommits ([#1019](https://github.com/appium/WebDriverAgent/issues/1019)) ([7108f7f](https://github.com/appium/WebDriverAgent/commit/7108f7f79575a1758bc7f05bd4ef790fd7694784)) + +## [9.7.0](https://github.com/appium/WebDriverAgent/compare/v9.6.3...v9.7.0) (2025-05-20) + +### Features + +* add placeholderValue to page source tree ([#1016](https://github.com/appium/WebDriverAgent/issues/1016)) ([509c207](https://github.com/appium/WebDriverAgent/commit/509c207b1366dd8582ba273edcdf77bfb30f53c9)) + +## [9.6.3](https://github.com/appium/WebDriverAgent/compare/v9.6.2...v9.6.3) (2025-05-18) + +### Miscellaneous Chores + +* Move the FBDoesElementSupportInnerText helper to a separate utility file ([#1018](https://github.com/appium/WebDriverAgent/issues/1018)) ([f17b07d](https://github.com/appium/WebDriverAgent/commit/f17b07d03abb6c2100405fda04326b7c35bfb48b)) + +## [9.6.2](https://github.com/appium/WebDriverAgent/compare/v9.6.1...v9.6.2) (2025-05-01) + +### Bug Fixes + +* release element screenshot data ([#1013](https://github.com/appium/WebDriverAgent/issues/1013)) ([a85f327](https://github.com/appium/WebDriverAgent/commit/a85f3271991556941234fbc888528051b1569db1)) + +## [9.6.1](https://github.com/appium/WebDriverAgent/compare/v9.6.0...v9.6.1) (2025-04-22) + +### Bug Fixes + +* allow setting precise resolution for the MJPEG stream ([#1009](https://github.com/appium/WebDriverAgent/issues/1009)) ([3f86eda](https://github.com/appium/WebDriverAgent/commit/3f86edafda42d955929f7cca870e2b8da54ae930)) + +## [9.6.0](https://github.com/appium/WebDriverAgent/compare/v9.5.2...v9.6.0) (2025-04-20) + +### Features + +* Split custom and standard snapshotting methods ([#1008](https://github.com/appium/WebDriverAgent/issues/1008)) ([8358856](https://github.com/appium/WebDriverAgent/commit/8358856f5968977b13d5cbdafac97f3053dae56e)) + +## [9.5.2](https://github.com/appium/WebDriverAgent/compare/v9.5.1...v9.5.2) (2025-04-19) + +### Bug Fixes + +* Missing text in long text for get text/value ([#1007](https://github.com/appium/WebDriverAgent/issues/1007)) ([6603a0b](https://github.com/appium/WebDriverAgent/commit/6603a0ba384917d39389509958ccac03ad174610)) + +## [9.5.1](https://github.com/appium/WebDriverAgent/compare/v9.5.0...v9.5.1) (2025-04-10) + +### Bug Fixes + +* Make sure we don't store element snapshot in the cache ([#1001](https://github.com/appium/WebDriverAgent/issues/1001)) ([cfe052b](https://github.com/appium/WebDriverAgent/commit/cfe052bb3adb3f3b24d0a34f386c60cf1516b308)) + +## [9.5.0](https://github.com/appium/WebDriverAgent/compare/v9.4.1...v9.5.0) (2025-04-10) + +### Features + +* Add support for the autoClickAlertSelector setting ([#1002](https://github.com/appium/WebDriverAgent/issues/1002)) ([fd31b95](https://github.com/appium/WebDriverAgent/commit/fd31b9589199d0a7bc76919f6aa7c7c74c498b90)) + +## [9.4.1](https://github.com/appium/WebDriverAgent/compare/v9.4.0...v9.4.1) (2025-04-05) + +### Miscellaneous Chores + +* bump appium-ios-simulator ([445741d](https://github.com/appium/WebDriverAgent/commit/445741d03313019016d4232f49e656d50f673f16)) + +## [9.4.0](https://github.com/appium/WebDriverAgent/compare/v9.3.3...v9.4.0) (2025-04-02) + +### Features + +* Always apply the native snapshotting strategy for XCUIApplication instances ([#998](https://github.com/appium/WebDriverAgent/issues/998)) ([60f5aef](https://github.com/appium/WebDriverAgent/commit/60f5aeffdda85faffd60aba416dc9d92987f19ac)) + +## [9.3.3](https://github.com/appium/WebDriverAgent/compare/v9.3.2...v9.3.3) (2025-03-27) + +### Bug Fixes + +* Properly set snapshot lookup scope if limitXpathContextScope is disabled ([#996](https://github.com/appium/WebDriverAgent/issues/996)) ([03ca7cd](https://github.com/appium/WebDriverAgent/commit/03ca7cd27b7cd92a45b344eb661db973c5dde809)) + +## [9.3.2](https://github.com/appium/WebDriverAgent/compare/v9.3.1...v9.3.2) (2025-03-26) + +### Bug Fixes + +* Adjust limitXPathContextScope setting name ([#995](https://github.com/appium/WebDriverAgent/issues/995)) ([9789e39](https://github.com/appium/WebDriverAgent/commit/9789e393b55bc682a9a8ef5a65fba5e4dbf752ce)) + +## [9.3.1](https://github.com/appium/WebDriverAgent/compare/v9.3.0...v9.3.1) (2025-03-25) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 19.0.5 to 20.0.0 ([#994](https://github.com/appium/WebDriverAgent/issues/994)) ([f55462f](https://github.com/appium/WebDriverAgent/commit/f55462f4fa63314dfea48670d17ee54dc5fe2d96)) + +## [9.3.0](https://github.com/appium/WebDriverAgent/compare/v9.2.0...v9.3.0) (2025-03-21) + +### Features + +* Add /window/rect W3C endpoint ([#991](https://github.com/appium/WebDriverAgent/issues/991)) ([34f9510](https://github.com/appium/WebDriverAgent/commit/34f95107997bdec63219a2fd917de899de3e198c)) + +## [9.2.0](https://github.com/appium/WebDriverAgent/compare/v9.1.0...v9.2.0) (2025-03-13) + +### Features + +* Add 'limitXpathContextScope' setting ([#988](https://github.com/appium/WebDriverAgent/issues/988)) ([9c9d8af](https://github.com/appium/WebDriverAgent/commit/9c9d8af9c98ba7b2843a42f54354b78e126d2d27)) + +## [9.1.0](https://github.com/appium/WebDriverAgent/compare/v9.0.6...v9.1.0) (2025-03-09) + +### Features + +* add placeholderValue ([#987](https://github.com/appium/WebDriverAgent/issues/987)) ([8c3a1cb](https://github.com/appium/WebDriverAgent/commit/8c3a1cb30655ed8d1a77d25bbeca71ee48c2ec3e)) + +## [9.0.6](https://github.com/appium/WebDriverAgent/compare/v9.0.5...v9.0.6) (2025-02-28) + +### Bug Fixes + +* optimize LRU cache ([#985](https://github.com/appium/WebDriverAgent/issues/985)) ([46dc417](https://github.com/appium/WebDriverAgent/commit/46dc417da9f4a843838b414c0b154d6f478dbc0b)) + +## [9.0.5](https://github.com/appium/WebDriverAgent/compare/v9.0.4...v9.0.5) (2025-02-26) + +### Bug Fixes + +* add autorelease pool to drain temporary objects ([#983](https://github.com/appium/WebDriverAgent/issues/983)) ([f92f1cd](https://github.com/appium/WebDriverAgent/commit/f92f1cde0fe914086103a110844bbe3bc0e3c4a6)) + +## [9.0.4](https://github.com/appium/WebDriverAgent/compare/v9.0.3...v9.0.4) (2025-02-21) + +### Bug Fixes + +* Accept reqBasePath proxy option ([#982](https://github.com/appium/WebDriverAgent/issues/982)) ([19efbdd](https://github.com/appium/WebDriverAgent/commit/19efbdd69ff9edff20c0c318bd39c29963d4d51d)) + +## [9.0.3](https://github.com/appium/WebDriverAgent/compare/v9.0.2...v9.0.3) (2025-02-05) + +### Bug Fixes + +* add nullable signature ([#979](https://github.com/appium/WebDriverAgent/issues/979)) ([34b303c](https://github.com/appium/WebDriverAgent/commit/34b303c4e226d6a75a45a14eee7ca5e253e67737)) + +## [9.0.2](https://github.com/appium/WebDriverAgent/compare/v9.0.1...v9.0.2) (2025-02-03) + +### Bug Fixes + +* update docs link in xcodebuild error message ([#978](https://github.com/appium/WebDriverAgent/issues/978)) ([ea3863a](https://github.com/appium/WebDriverAgent/commit/ea3863a67d5cfa8bc2e48a1dc2c59052acd47937)) + +## [9.0.1](https://github.com/appium/WebDriverAgent/compare/v9.0.0...v9.0.1) (2025-01-17) + +### Miscellaneous Chores + +* Optimize stable instance retrieval ([#973](https://github.com/appium/WebDriverAgent/issues/973)) ([f2c752d](https://github.com/appium/WebDriverAgent/commit/f2c752db4707b3864efb62b95b64abb487d28e4b)) + +## [9.0.0](https://github.com/appium/WebDriverAgent/compare/v8.12.2...v9.0.0) (2025-01-16) + +### ⚠ BREAKING CHANGES + +* snapshotTimeout and customSnapshotTimeout settings have been removed as a result of the custom snapshotting logic removal + +### Features + +* Refactor snapshotting mechanism ([#970](https://github.com/appium/WebDriverAgent/issues/970)) ([08f1306](https://github.com/appium/WebDriverAgent/commit/08f13060119c710f53b34a98c95683287c0365a0)) + +## [8.12.2](https://github.com/appium/WebDriverAgent/compare/v8.12.1...v8.12.2) (2025-01-13) + +### Miscellaneous Chores + +* Exclude element visibility and accessibility info from the accessibility audit details ([#968](https://github.com/appium/WebDriverAgent/issues/968)) ([f62afc3](https://github.com/appium/WebDriverAgent/commit/f62afc372c123bdd8dd7bb493f653bb128144d24)) + +## [8.12.1](https://github.com/appium/WebDriverAgent/compare/v8.12.0...v8.12.1) (2025-01-03) + +### Miscellaneous Chores + +* Bump eslint ([#965](https://github.com/appium/WebDriverAgent/issues/965)) ([17f49ec](https://github.com/appium/WebDriverAgent/commit/17f49ec5a54e97b0ef0d20a3e39fc96b32575e43)) + +## [8.12.0](https://github.com/appium/WebDriverAgent/compare/v8.11.3...v8.12.0) (2024-12-13) + +### Features + +* look for critical notification in respectSystemAlerts ([#962](https://github.com/appium/WebDriverAgent/issues/962)) ([916c8c5](https://github.com/appium/WebDriverAgent/commit/916c8c557a9366608df211f33b5b7fbb0354dad3)) + +## [8.11.3](https://github.com/appium/WebDriverAgent/compare/v8.11.2...v8.11.3) (2024-12-06) + +### Miscellaneous Chores + +* **deps:** bump @appium/support from 5.1.8 to 6.0.0 ([#960](https://github.com/appium/WebDriverAgent/issues/960)) ([dbeb09c](https://github.com/appium/WebDriverAgent/commit/dbeb09c89f8c02e00a7bdffe7899650d435f3575)) + +## [8.11.2](https://github.com/appium/WebDriverAgent/compare/v8.11.1...v8.11.2) (2024-12-03) + +### Miscellaneous Chores + +* **deps-dev:** bump mocha from 10.8.2 to 11.0.1 ([#959](https://github.com/appium/WebDriverAgent/issues/959)) ([55b49c8](https://github.com/appium/WebDriverAgent/commit/55b49c83581c9e88f70806d98015238de3104f19)) + +## [8.11.1](https://github.com/appium/WebDriverAgent/compare/v8.11.0...v8.11.1) (2024-11-11) + +### Miscellaneous Chores + +* bump appium-ios-device ([#955](https://github.com/appium/WebDriverAgent/issues/955)) ([021f349](https://github.com/appium/WebDriverAgent/commit/021f34901866f4a7870914c00781b83bd0cbddc4)) + +## [8.11.0](https://github.com/appium/WebDriverAgent/compare/v8.10.1...v8.11.0) (2024-11-11) + +### Features + +* Add support for excluded_attributes in JSON source hierarchy ([#953](https://github.com/appium/WebDriverAgent/issues/953)) ([6112223](https://github.com/appium/WebDriverAgent/commit/6112223b21026fae5545fe1b1433a09c67ff524b)) + +## [8.10.1](https://github.com/appium/WebDriverAgent/compare/v8.10.0...v8.10.1) (2024-11-10) + +### Miscellaneous Chores + +* remove unnecessary lines ([#954](https://github.com/appium/WebDriverAgent/issues/954)) ([940df80](https://github.com/appium/WebDriverAgent/commit/940df80937381b481a2762fbf86b6249804591bd)) + +## [8.10.0](https://github.com/appium/WebDriverAgent/compare/v8.9.4...v8.10.0) (2024-11-07) + +### Features + +* add useClearTextShortcut setting ([#952](https://github.com/appium/WebDriverAgent/issues/952)) ([61bc051](https://github.com/appium/WebDriverAgent/commit/61bc051180d691d26233c66a5a76ed20b7fa09d2)) + +## [8.9.4](https://github.com/appium/WebDriverAgent/compare/v8.9.3...v8.9.4) (2024-10-17) + +### Bug Fixes + +* Consider transient overlay windows when respectSystemAlerts is enabled ([#946](https://github.com/appium/WebDriverAgent/issues/946)) ([f0bdce7](https://github.com/appium/WebDriverAgent/commit/f0bdce7eb8fdb13d2309d28e936950c77f006b20)) + +## [8.9.3](https://github.com/appium/WebDriverAgent/compare/v8.9.2...v8.9.3) (2024-10-07) + +### Miscellaneous Chores + +* remove unused FBBaseActionsParser and cleanup imports in FBConfiguration ([#943](https://github.com/appium/WebDriverAgent/issues/943)) ([a2173d0](https://github.com/appium/WebDriverAgent/commit/a2173d05df8ef831310e805a8e6a8a8d17725201)) + +## [8.9.2](https://github.com/appium/WebDriverAgent/compare/v8.9.1...v8.9.2) (2024-09-13) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 18.0.1 to 19.0.1 ([#938](https://github.com/appium/WebDriverAgent/issues/938)) ([3ef0093](https://github.com/appium/WebDriverAgent/commit/3ef009317801dca47efe34bd048d3cab2e644ee2)) + +## [8.9.1](https://github.com/appium/WebDriverAgent/compare/v8.9.0...v8.9.1) (2024-08-09) + +### Bug Fixes + +* Update swizzling of waitForQuiescenceIncludingAnimationsIdle: API for Xcode16-beta5 ([#935](https://github.com/appium/WebDriverAgent/issues/935)) ([2ccc436](https://github.com/appium/WebDriverAgent/commit/2ccc436991ca880a1dfdec688dc8167008fe382d)) + +## [8.9.0](https://github.com/appium/WebDriverAgent/compare/v8.8.0...v8.9.0) (2024-08-07) + +### Features + +* Add idleTimeoutMs param to the openUrl call ([#933](https://github.com/appium/WebDriverAgent/issues/933)) ([5e98841](https://github.com/appium/WebDriverAgent/commit/5e98841f56eda6454d67d813b921bfcf98f1ff78)) + +### Bug Fixes + +* Revert the logic to open the default URL in Safari via deeplink ([#932](https://github.com/appium/WebDriverAgent/issues/932)) ([7c51145](https://github.com/appium/WebDriverAgent/commit/7c5114518509c9a399845283eca7708248fb838f)) + +## [8.8.0](https://github.com/appium/WebDriverAgent/compare/v8.7.12...v8.8.0) (2024-08-06) + +### Features + +* Open the default URL in Safari upon session startup ([#929](https://github.com/appium/WebDriverAgent/issues/929)) ([97cf91d](https://github.com/appium/WebDriverAgent/commit/97cf91de34dc53e5f75f91829dc43224101c1b45)) + +## [8.7.12](https://github.com/appium/WebDriverAgent/compare/v8.7.11...v8.7.12) (2024-08-02) + +### Miscellaneous Chores + +* Replace fancy-log dependency with appium logger ([#928](https://github.com/appium/WebDriverAgent/issues/928)) ([5d2ec24](https://github.com/appium/WebDriverAgent/commit/5d2ec249488655451e2d46384e560fee7e08e840)) + +## [8.7.11](https://github.com/appium/WebDriverAgent/compare/v8.7.10...v8.7.11) (2024-07-29) + +### Bug Fixes + +* Respond to /health with a proper HTML ([#925](https://github.com/appium/WebDriverAgent/issues/925)) ([42c519f](https://github.com/appium/WebDriverAgent/commit/42c519f9df7beec81175fd38af388975d6f6b800)) + +## [8.7.10](https://github.com/appium/WebDriverAgent/compare/v8.7.9...v8.7.10) (2024-07-29) + +### Miscellaneous Chores + +* **deps-dev:** bump @types/node from 20.14.13 to 22.0.0 ([#926](https://github.com/appium/WebDriverAgent/issues/926)) ([1699023](https://github.com/appium/WebDriverAgent/commit/1699023086a243c3d86ddae4da8342c6beda3f48)) + +## [8.7.9](https://github.com/appium/WebDriverAgent/compare/v8.7.8...v8.7.9) (2024-07-21) + +### Miscellaneous Chores + +* keep error handling for the future possible usage ([#921](https://github.com/appium/WebDriverAgent/issues/921)) ([2f90739](https://github.com/appium/WebDriverAgent/commit/2f90739340d70073b48c703b36b9a313d3618972)) + +## [8.7.8](https://github.com/appium/WebDriverAgent/compare/v8.7.7...v8.7.8) (2024-07-18) + +### Bug Fixes + +* do nothing for an empty array in w3c actions ([#919](https://github.com/appium/WebDriverAgent/issues/919)) ([9e70ec1](https://github.com/appium/WebDriverAgent/commit/9e70ec1dbec1d1844278a58297a5b956ebaeb7fc)) + +## [8.7.7](https://github.com/appium/WebDriverAgent/compare/v8.7.6...v8.7.7) (2024-07-18) + +### Bug Fixes + +* Pass-through modifier keys ([#918](https://github.com/appium/WebDriverAgent/issues/918)) ([29d0e5c](https://github.com/appium/WebDriverAgent/commit/29d0e5cb2a19809e1babb06e5adaa49b43c754a5)) + +## [8.7.6](https://github.com/appium/WebDriverAgent/compare/v8.7.5...v8.7.6) (2024-07-02) + +### Miscellaneous Chores + +* Simplify xcodebuild lines monitoring ([#916](https://github.com/appium/WebDriverAgent/issues/916)) ([87678f2](https://github.com/appium/WebDriverAgent/commit/87678f260c98b3a3bc3d37017e9ef39098ccb3c4)) + +## [8.7.5](https://github.com/appium/WebDriverAgent/compare/v8.7.4...v8.7.5) (2024-06-26) + +### Bug Fixes + +* Respect wdaRemotePort capability for real devices ([#915](https://github.com/appium/WebDriverAgent/issues/915)) ([03ea143](https://github.com/appium/WebDriverAgent/commit/03ea1439a9cc5b6495be60707bc474e3ae9bdb06)) + +## [8.7.4](https://github.com/appium/WebDriverAgent/compare/v8.7.3...v8.7.4) (2024-06-20) + +### Miscellaneous Chores + +* Bump chai and chai-as-promised ([#913](https://github.com/appium/WebDriverAgent/issues/913)) ([9086783](https://github.com/appium/WebDriverAgent/commit/90867832ec3077f0036938aa68a168a5702fc90a)) + +## [8.7.3](https://github.com/appium/WebDriverAgent/compare/v8.7.2...v8.7.3) (2024-06-12) + +### Miscellaneous Chores + +* **deps:** bump @appium/support from 4.5.0 to 5.0.3 ([#910](https://github.com/appium/WebDriverAgent/issues/910)) ([936005b](https://github.com/appium/WebDriverAgent/commit/936005b458e7b5b64b60d9bda37d45bb5a90e615)) + +## [8.7.2](https://github.com/appium/WebDriverAgent/compare/v8.7.1...v8.7.2) (2024-06-04) + +### Miscellaneous Chores + +* **deps-dev:** bump sinon from 17.0.2 to 18.0.0 ([#903](https://github.com/appium/WebDriverAgent/issues/903)) ([87e4ba5](https://github.com/appium/WebDriverAgent/commit/87e4ba5ce3868d99ac889795039936be119ef87a)) + +## [8.7.1](https://github.com/appium/WebDriverAgent/compare/v8.7.0...v8.7.1) (2024-06-04) + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 23.1.1 to 24.0.0 and conventional-changelog-conventionalcommits to 8.0.0 ([#908](https://github.com/appium/WebDriverAgent/issues/908)) ([26019ec](https://github.com/appium/WebDriverAgent/commit/26019eca9b7331353e26a1014bc4afcecc0450f3)) + +## [8.7.0](https://github.com/appium/WebDriverAgent/compare/v8.6.0...v8.7.0) (2024-06-01) + + +### Features + +* Add a setting to respect system alerts while detecting active apps ([#907](https://github.com/appium/WebDriverAgent/issues/907)) ([5c82d66](https://github.com/appium/WebDriverAgent/commit/5c82d66890b1a74f9b6f698c87590b2154a6c1bd)) + +## [8.6.0](https://github.com/appium/WebDriverAgent/compare/v8.5.7...v8.6.0) (2024-05-17) + + +### Features + +* support maxTypingFrequency in settings api ([#904](https://github.com/appium/WebDriverAgent/issues/904)) ([fa4776a](https://github.com/appium/WebDriverAgent/commit/fa4776a2bfa15cbec8bba35d8ed11318d9629934)) + +## [8.5.7](https://github.com/appium/WebDriverAgent/compare/v8.5.6...v8.5.7) (2024-05-16) + + +### Miscellaneous Chores + +* Update dev dependencies ([e49dcf2](https://github.com/appium/WebDriverAgent/commit/e49dcf2afb0a10edc7085ac56d297234c00d57b0)) + +## [8.5.6](https://github.com/appium/WebDriverAgent/compare/v8.5.5...v8.5.6) (2024-04-20) + + +### Bug Fixes + +* unit test for linux ([#894](https://github.com/appium/WebDriverAgent/issues/894)) ([3a90158](https://github.com/appium/WebDriverAgent/commit/3a9015898d70b177cb6cbfcaf412dfa3c4ec3865)) + +## [8.5.5](https://github.com/appium/WebDriverAgent/compare/v8.5.4...v8.5.5) (2024-04-20) + + +### Bug Fixes + +* xcode warning about com.facebook.wda.lib ([#892](https://github.com/appium/WebDriverAgent/issues/892)) ([6398079](https://github.com/appium/WebDriverAgent/commit/63980796d8f40bd68ffb5af4b085a2348e544a13)) + +## [8.5.4](https://github.com/appium/WebDriverAgent/compare/v8.5.3...v8.5.4) (2024-04-20) + + +### Miscellaneous Chores + +* remove old iOS/Xcode related test code and errors ([#890](https://github.com/appium/WebDriverAgent/issues/890)) ([2fd0dea](https://github.com/appium/WebDriverAgent/commit/2fd0dead0c86d6be08e040360dec9ea085ba0392)) + +## [8.5.3](https://github.com/appium/WebDriverAgent/compare/v8.5.2...v8.5.3) (2024-04-19) + + +### Miscellaneous Chores + +* update integerationapp for newer OS env ([#891](https://github.com/appium/WebDriverAgent/issues/891)) ([2c78348](https://github.com/appium/WebDriverAgent/commit/2c7834842afeb1aec77e953ce11ac3c43c839431)) + +## [8.5.2](https://github.com/appium/WebDriverAgent/compare/v8.5.1...v8.5.2) (2024-04-09) + + +### Miscellaneous Chores + +* **deps-dev:** bump @typescript-eslint/parser from 6.21.0 to 7.6.0 ([#888](https://github.com/appium/WebDriverAgent/issues/888)) ([ead75eb](https://github.com/appium/WebDriverAgent/commit/ead75eb87a5c8e94088bace8f372ab137dcf57ad)) +* Remove extra imports ([fb25742](https://github.com/appium/WebDriverAgent/commit/fb25742a07a2fbcb0365a48d54117267c7c916df)) + +## [8.5.1](https://github.com/appium/WebDriverAgent/compare/v8.5.0...v8.5.1) (2024-04-08) + + +### Miscellaneous Chores + +* Add more type declarations ([#886](https://github.com/appium/WebDriverAgent/issues/886)) ([9ca7632](https://github.com/appium/WebDriverAgent/commit/9ca7632faf999931e7f5edf47267fcce6d6392b2)) + +## [8.5.0](https://github.com/appium/WebDriverAgent/compare/v8.4.0...v8.5.0) (2024-04-07) + + +### Features + +* Add types for WDA caps and settings ([#885](https://github.com/appium/WebDriverAgent/issues/885)) ([4b3c220](https://github.com/appium/WebDriverAgent/commit/4b3c220c0c609802924b7b6ff9a4dfa7a98eb5f4)) + +## [8.4.0](https://github.com/appium/WebDriverAgent/compare/v8.3.1...v8.4.0) (2024-04-01) + + +### Features + +* add system screen size/width in the system info endpoint ([#881](https://github.com/appium/WebDriverAgent/issues/881)) ([5ebc71c](https://github.com/appium/WebDriverAgent/commit/5ebc71c6ca2b364d44a44716e794885f8d3b6d9c)) + +## [8.3.1](https://github.com/appium/WebDriverAgent/compare/v8.3.0...v8.3.1) (2024-03-31) + + +### Miscellaneous Chores + +* do not cleanup with this.usePrebuiltWDA ([#882](https://github.com/appium/WebDriverAgent/issues/882)) ([0436e95](https://github.com/appium/WebDriverAgent/commit/0436e95752826bee7786577ac1bc0d056af11bc8)) + +## [8.3.0](https://github.com/appium/WebDriverAgent/compare/v8.2.1...v8.3.0) (2024-03-29) + + +### Features + +* Add module version to the /status output ([#878](https://github.com/appium/WebDriverAgent/issues/878)) ([a9603f8](https://github.com/appium/WebDriverAgent/commit/a9603f82acbdacdeb7a55b857512ba35353a4bc3)) + +## [8.2.1](https://github.com/appium/WebDriverAgent/compare/v8.2.0...v8.2.1) (2024-03-28) + + +### Miscellaneous Chores + +* wait for wda start in sim as well for preinstalled wda start ([#876](https://github.com/appium/WebDriverAgent/issues/876)) ([6c8920a](https://github.com/appium/WebDriverAgent/commit/6c8920adddb373b463259c3e6c14cb3c49ecbf2b)) + +## [8.2.0](https://github.com/appium/WebDriverAgent/compare/v8.1.0...v8.2.0) (2024-03-28) + + +### Features + +* Add a capability to customize the default state change timeout on app startup ([#877](https://github.com/appium/WebDriverAgent/issues/877)) ([98351c3](https://github.com/appium/WebDriverAgent/commit/98351c358367e67e63701612fd3702d53437e12e)) + +## [8.1.0](https://github.com/appium/WebDriverAgent/compare/v8.0.2...v8.1.0) (2024-03-26) + + +### Features + +* add updatedWDABundleIdSuffix to handle bundle id for updatedWDABundleId with usePreinstalledWDA ([#871](https://github.com/appium/WebDriverAgent/issues/871)) ([d79b624](https://github.com/appium/WebDriverAgent/commit/d79b6245966baaa57f7a1f785d7f9b4ea5a7f104)) + +## [8.0.2](https://github.com/appium/WebDriverAgent/compare/v8.0.1...v8.0.2) (2024-03-26) + + +### Miscellaneous Chores + +* **deps:** bump appium-ios-simulator from 5.5.3 to 6.0.0 ([#874](https://github.com/appium/WebDriverAgent/issues/874)) ([72f2a97](https://github.com/appium/WebDriverAgent/commit/72f2a97ec31dbb3c66e5f459e0d7fd417c197d5d)) + +## [8.0.1](https://github.com/appium/WebDriverAgent/compare/v8.0.0...v8.0.1) (2024-03-26) + + +### Miscellaneous Chores + +* use bundle id outside opts for this.device.devicectl.launchApp ([#872](https://github.com/appium/WebDriverAgent/issues/872)) ([e2aeda2](https://github.com/appium/WebDriverAgent/commit/e2aeda2f2020f4014cba478b459e47954175f597)) + +## [8.0.0](https://github.com/appium/WebDriverAgent/compare/v7.3.1...v8.0.0) (2024-03-25) + + +### ⚠ BREAKING CHANGES + +* calls launch app process command with devicectl via this.device.devicectl + +### Features + +* launch WDA via devicectl object ([#870](https://github.com/appium/WebDriverAgent/issues/870)) ([090b815](https://github.com/appium/WebDriverAgent/commit/090b815ae47e1ef0e0a9842fac6828346bc38fe6)) + +## [7.3.1](https://github.com/appium/WebDriverAgent/compare/v7.3.0...v7.3.1) (2024-03-24) + + +### Miscellaneous Chores + +* move node-simctl to dev deps ([#869](https://github.com/appium/WebDriverAgent/issues/869)) ([9033759](https://github.com/appium/WebDriverAgent/commit/90337597e6c480c790cf299e160bc53731c0a87d)) + +## [7.3.0](https://github.com/appium/WebDriverAgent/compare/v7.2.0...v7.3.0) (2024-03-23) + + +### Features + +* Support prebuiltWDAPath for iOS 17 ([#868](https://github.com/appium/WebDriverAgent/issues/868)) ([39194d4](https://github.com/appium/WebDriverAgent/commit/39194d4ac6d0072c1214088ff5c15c986969914c)) + +## [7.2.0](https://github.com/appium/WebDriverAgent/compare/v7.1.2...v7.2.0) (2024-03-21) + + +### Features + +* Enable usePreinstalledWDA feature for simulators ([#866](https://github.com/appium/WebDriverAgent/issues/866)) ([7c684e2](https://github.com/appium/WebDriverAgent/commit/7c684e2def9dd968de1cf89e4ec26403a52ba805)) + +## [7.1.2](https://github.com/appium/WebDriverAgent/compare/v7.1.1...v7.1.2) (2024-03-14) + + +### Bug Fixes + +* Always assume en0 is the WiFi interface ([#864](https://github.com/appium/WebDriverAgent/issues/864)) ([6dbfb3f](https://github.com/appium/WebDriverAgent/commit/6dbfb3f2ec8e0bfa5a42c6f8ab882893bfe3f534)) + +## [7.1.1](https://github.com/appium/WebDriverAgent/compare/v7.1.0...v7.1.1) (2024-03-13) + + +### Bug Fixes + +* respect defaultActiveApplication in activeApplication selection ([#862](https://github.com/appium/WebDriverAgent/issues/862)) ([b1ddae2](https://github.com/appium/WebDriverAgent/commit/b1ddae2be3fd3f7c87de79e804d82cf7c13dc56e)) + +## [7.1.0](https://github.com/appium/WebDriverAgent/compare/v7.0.6...v7.1.0) (2024-03-07) + + +### Features + +* Add wrappers for native XCTest video recorder ([#858](https://github.com/appium/WebDriverAgent/issues/858)) ([9728548](https://github.com/appium/WebDriverAgent/commit/9728548676c8de67c30d127ee8b0374f58286e74)) + + +### Miscellaneous Chores + +* bump typescript ([89880f5](https://github.com/appium/WebDriverAgent/commit/89880f509f930f16f6469bcda613569040c337b6)) + +## [7.0.6](https://github.com/appium/WebDriverAgent/compare/v7.0.5...v7.0.6) (2024-03-03) + + +### Miscellaneous Chores + +* Handle app startup errors as session creation exceptions ([#855](https://github.com/appium/WebDriverAgent/issues/855)) ([0ec5398](https://github.com/appium/WebDriverAgent/commit/0ec5398e9cb4b0e5ab133cc0c330b85b3d37766e)) + +## [7.0.5](https://github.com/appium/WebDriverAgent/compare/v7.0.4...v7.0.5) (2024-03-03) + + +### Reverts + +* Revert "chore: tune release packages (#856)" (#857) ([dc72015](https://github.com/appium/WebDriverAgent/commit/dc720157a60925451e6d5935abcd168082d44785)), closes [#856](https://github.com/appium/WebDriverAgent/issues/856) [#857](https://github.com/appium/WebDriverAgent/issues/857) + +## [7.0.4](https://github.com/appium/WebDriverAgent/compare/v7.0.3...v7.0.4) (2024-03-03) + + +### Miscellaneous Chores + +* dummy commit to trigger a release ([0cb66c5](https://github.com/appium/WebDriverAgent/commit/0cb66c5edc91c191d5ec412ba0a479e07cb4214b)) + +## [7.0.3](https://github.com/appium/WebDriverAgent/compare/v7.0.2...v7.0.3) (2024-03-03) + + +### Miscellaneous Chores + +* tune release packages ([#856](https://github.com/appium/WebDriverAgent/issues/856)) ([aa0765e](https://github.com/appium/WebDriverAgent/commit/aa0765e425faba6c035a9933320e91679b167b80)) + +## [7.0.2](https://github.com/appium/WebDriverAgent/compare/v7.0.1...v7.0.2) (2024-02-28) + + +### Miscellaneous Chores + +* Tune alert detection if system app is active ([#854](https://github.com/appium/WebDriverAgent/issues/854)) ([857d3de](https://github.com/appium/WebDriverAgent/commit/857d3decf497935098ba6acb61654be1da173b11)) + +## [7.0.1](https://github.com/appium/WebDriverAgent/compare/v7.0.0...v7.0.1) (2024-02-21) + + +### Miscellaneous Chores + +* Simplify the logic of alert element detection ([#851](https://github.com/appium/WebDriverAgent/issues/851)) ([54f91f1](https://github.com/appium/WebDriverAgent/commit/54f91f198e45535ea9d86b7eee40b21f43f84294)) + +## [7.0.0](https://github.com/appium/WebDriverAgent/compare/v6.1.1...v7.0.0) (2024-02-12) + + +### ⚠ BREAKING CHANGES + +* The following REST endpoints have been removed, use W3C actions instead: +- /wda/touch/perform +- /wda/touch/multi/perform + +### Features + +* Remove obsolete MJSONWP touch actions ([#847](https://github.com/appium/WebDriverAgent/issues/847)) ([d77f640](https://github.com/appium/WebDriverAgent/commit/d77f640867155fddbbbc9575f0a77802602865e7)) + +## [6.1.1](https://github.com/appium/WebDriverAgent/compare/v6.1.0...v6.1.1) (2024-02-11) + + +### Miscellaneous Chores + +* Make sure the app under test is restarted if opened from a deep link ([#846](https://github.com/appium/WebDriverAgent/issues/846)) ([88b0a5b](https://github.com/appium/WebDriverAgent/commit/88b0a5b0f8aefa05a7dc28d17faf62c229e0706f)) + +## [6.1.0](https://github.com/appium/WebDriverAgent/compare/v6.0.0...v6.1.0) (2024-02-10) + + +### Features + +* Add a possibility of starting a test with a deep link ([#845](https://github.com/appium/WebDriverAgent/issues/845)) ([aa25e49](https://github.com/appium/WebDriverAgent/commit/aa25e49fa9821960b08e9f4f3ea5891ebdf7d48d)) + +## [6.0.0](https://github.com/appium/WebDriverAgent/compare/v5.15.8...v6.0.0) (2024-01-31) + + +### ⚠ BREAKING CHANGES + +* The /wda/tap/:uuid endpoint has been replaced by /wda/element/:uuid/tap and /wda/tap ones + +### Features + +* Add coordinate-based APIs for gesture calls ([#843](https://github.com/appium/WebDriverAgent/issues/843)) ([feda373](https://github.com/appium/WebDriverAgent/commit/feda373b6147d3e87b29dceb871887c77febe76b)) + +## [5.15.8](https://github.com/appium/WebDriverAgent/compare/v5.15.7...v5.15.8) (2024-01-24) + + +### Bug Fixes + +* use arm64 naming for xctestrun ([#840](https://github.com/appium/WebDriverAgent/issues/840)) ([429e154](https://github.com/appium/WebDriverAgent/commit/429e154c28ab2f17685723b02c941efce03984d4)) + +## [5.15.7](https://github.com/appium/WebDriverAgent/compare/v5.15.6...v5.15.7) (2024-01-16) + + +### Miscellaneous Chores + +* **deps-dev:** bump semantic-release from 22.0.12 to 23.0.0 ([#836](https://github.com/appium/WebDriverAgent/issues/836)) ([a3ac2c5](https://github.com/appium/WebDriverAgent/commit/a3ac2c58786955507a34d0adcc4a53cd30f55014)) + + +### Code Refactoring + +* Ditch FBApplication in favour of XCUIApplication extensions ([#834](https://github.com/appium/WebDriverAgent/issues/834)) ([70a8d98](https://github.com/appium/WebDriverAgent/commit/70a8d98bc15d8fc615455be07fad9c37ff8d430b)) + +## [5.15.6](https://github.com/appium/WebDriverAgent/compare/v5.15.5...v5.15.6) (2024-01-06) + + +### Miscellaneous Chores + +* Update keyboard typing implementation ([#832](https://github.com/appium/WebDriverAgent/issues/832)) ([06cfb3b](https://github.com/appium/WebDriverAgent/commit/06cfb3b2b895a0bec681218fce658bdfcb4d13e9)) + ## [5.15.5](https://github.com/appium/WebDriverAgent/compare/v5.15.4...v5.15.5) (2023-12-13) diff --git a/Configurations/IOSSettings.xcconfig b/Configurations/IOSSettings.xcconfig index 70ae5ec68..3bde5a21f 100644 --- a/Configurations/IOSSettings.xcconfig +++ b/Configurations/IOSSettings.xcconfig @@ -28,4 +28,4 @@ RUN_CLANG_STATIC_ANALYZER = YES GCC_PREPROCESSOR_DEFINITIONS = $(inherited) -WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability +WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability -Wno-declaration-after-statement -Wno-objc-messaging-id -Wno-direct-ivar-access -Wno-cast-qual -Wno-deprecated-declarations -Wno-reserved-identifier diff --git a/Configurations/TVOSSettings.xcconfig b/Configurations/TVOSSettings.xcconfig index 70ae5ec68..3bde5a21f 100644 --- a/Configurations/TVOSSettings.xcconfig +++ b/Configurations/TVOSSettings.xcconfig @@ -28,4 +28,4 @@ RUN_CLANG_STATIC_ANALYZER = YES GCC_PREPROCESSOR_DEFINITIONS = $(inherited) -WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability +WARNING_CFLAGS = $(inherited) -Weverything -Wno-objc-missing-property-synthesis -Wno-unused-macros -Wno-disabled-macro-expansion -Wno-gnu-statement-expression -Wno-language-extension-token -Wno-overriding-method-mismatch -Wno-missing-variable-declarations -Rno-module-build -Wno-auto-import -Wno-objc-interface-ivars -Wno-documentation-unknown-command -Wno-reserved-id-macro -Wno-unused-parameter -Wno-gnu-conditional-omitted-operand -Wno-explicit-ownership-type -Wno-date-time -Wno-cast-align -Wno-cstring-format-directive -Wno-double-promotion -Wno-partial-availability -Wno-declaration-after-statement -Wno-objc-messaging-id -Wno-direct-ivar-access -Wno-cast-qual -Wno-deprecated-declarations -Wno-reserved-identifier diff --git a/Configurations/TVOSTestSettings.xcconfig b/Configurations/TVOSTestSettings.xcconfig index 08679e421..17824cc36 100644 --- a/Configurations/TVOSTestSettings.xcconfig +++ b/Configurations/TVOSTestSettings.xcconfig @@ -1,2 +1 @@ EXCLUDED_ARCHS = i386 - diff --git a/Fastlane/Fastfile b/Fastlane/Fastfile index 41fd63669..d2fb186ac 100644 --- a/Fastlane/Fastfile +++ b/Fastlane/Fastfile @@ -7,6 +7,7 @@ lane :test do fail_build: true, scheme: ENV['SCHEME'], sdk: ENV['SDK'], - destination: ENV['DEST'], + device: ENV['DEVICE'], + number_of_retries: 3 ) end diff --git a/Fastlane/Pluginfile b/Fastlane/Pluginfile deleted file mode 100644 index 273a6b6f4..000000000 --- a/Fastlane/Pluginfile +++ /dev/null @@ -1,3 +0,0 @@ -# Autogenerated by fastlane -# -# Ensure this file is checked in to source control! diff --git a/Gemfile b/Gemfile index 90c93561d..ed0a22fa4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" -gem "fastlane", '2.217.0' +gem "fastlane", '~> 2.229' diff --git a/PATENTS b/PATENTS deleted file mode 100644 index f5c989c54..000000000 --- a/PATENTS +++ /dev/null @@ -1,33 +0,0 @@ -Additional Grant of Patent Rights Version 2 - -"Software" means the WebDriverAgent software distributed by Facebook, Inc. - -Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software -("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable -(subject to the termination provision below) license under any Necessary -Claims, to make, have made, use, sell, offer to sell, import, and otherwise -transfer the Software. For avoidance of doubt, no license is granted under -Facebook’s rights in any patent claims that are infringed by (i) modifications -to the Software made by you or any third party or (ii) the Software in -combination with any software or other technology. - -The license granted hereunder will terminate, automatically and without notice, -if you (or any of your subsidiaries, corporate affiliates or agents) initiate -directly or indirectly, or take a direct financial interest in, any Patent -Assertion: (i) against Facebook or any of its subsidiaries or corporate -affiliates, (ii) against any party if such Patent Assertion arises in whole or -in part from any software, technology, product or service of Facebook or any of -its subsidiaries or corporate affiliates, or (iii) against any party relating -to the Software. Notwithstanding the foregoing, if Facebook or any of its -subsidiaries or corporate affiliates files a lawsuit alleging patent -infringement against you in the first instance, and you respond by filing a -patent infringement counterclaim in that lawsuit against that party that is -unrelated to the Software, the license granted hereunder will not terminate -under section (i) of this paragraph due to such counterclaim. - -A "Necessary Claim" is a claim of a patent owned by Facebook that is -necessarily infringed by the Software standing alone. - -A "Patent Assertion" is any lawsuit or other action alleging direct, indirect, -or contributory infringement or inducement to infringe any patent, including a -cross-claim or counterclaim. diff --git a/PrivateHeaders/XCTest/CDStructures.h b/PrivateHeaders/XCTest/CDStructures.h index eaf3f46fa..56078e9aa 100644 --- a/PrivateHeaders/XCTest/CDStructures.h +++ b/PrivateHeaders/XCTest/CDStructures.h @@ -24,5 +24,8 @@ typedef struct { unsigned short _field3[1]; } CDStruct_27a325c0; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-identifier" int _XCTSetApplicationStateTimeout(double timeout); double _XCTApplicationStateTimeout(void); +#pragma clang diagnostic pop diff --git a/PrivateHeaders/XCTest/XCSynthesizedEventRecord.h b/PrivateHeaders/XCTest/XCSynthesizedEventRecord.h index 279eee3f0..d630eeb15 100644 --- a/PrivateHeaders/XCTest/XCSynthesizedEventRecord.h +++ b/PrivateHeaders/XCTest/XCSynthesizedEventRecord.h @@ -25,6 +25,7 @@ #if !TARGET_OS_TV - (id)initWithName:(NSString *)arg1 interfaceOrientation:(UIInterfaceOrientation)arg2; #endif +- (id)initWithName:(id)arg1; - (id)init; - (BOOL)synthesizeWithError:(NSError **)arg1; diff --git a/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h b/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h index 7473b2e0a..7ca530371 100644 --- a/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h +++ b/PrivateHeaders/XCTest/XCTRunnerDaemonSession.h @@ -80,6 +80,14 @@ @property(readonly) _Bool supportsLocationSimulation; #endif +// Since Xcode 15.0-beta1 +- (void)stopScreenRecordingWithUUID:(NSUUID *)arg1 + withReply:(void (^)(NSError *))arg2; +- (void)startScreenRecordingWithRequest:(id/* XCTScreenRecordingRequest */)arg1 + withReply:(void (^)(id/* XCTAttachmentFutureMetadata */, NSError *))arg2; +- (_Bool)supportsScreenRecording; +- (_Bool)preferScreenshotsOverScreenRecordings; + // Since Xcode 10.2 - (void)launchApplicationWithPath:(NSString *)arg1 bundleID:(NSString *)arg2 diff --git a/PrivateHeaders/XCTest/XCTestCase.h b/PrivateHeaders/XCTest/XCTestCase.h index d68d949c5..8e4a52161 100644 --- a/PrivateHeaders/XCTest/XCTestCase.h +++ b/PrivateHeaders/XCTest/XCTestCase.h @@ -8,7 +8,10 @@ #import +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-identifier" @class NSInvocation, XCTestCaseRun, XCTestContext, _XCTestCaseImplementation; +#pragma clang diagnostic pop @interface XCTestCase() { diff --git a/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h b/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h index 8ed03ebf7..d68f25c17 100644 --- a/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h +++ b/PrivateHeaders/XCTest/XCTestManager_ManagerInterface-Protocol.h @@ -27,6 +27,7 @@ - (void)_XCT_requestElementAtPoint:(CGPoint)arg1 reply:(void (^)(id/*XCAccessibilityElement*/, NSError *))arg2; - (void)_XCT_fetchParameterizedAttributeForElement:(id/*XCAccessibilityElement*/)arg1 attributes:(NSNumber *)arg2 parameter:(id)arg3 reply:(void (^)(id, NSError *))arg4; - (void)_XCT_setAttribute:(NSNumber *)arg1 value:(id)arg2 element:(id/*XCAccessibilityElement*/)arg3 reply:(void (^)(BOOL, NSError *))arg4; +- (void)_XCT_fetchAttributes:(id)attributes forElement:(id)element reply:(void (^)(NSDictionary *, NSError *))reply; - (void)_XCT_fetchAttributesForElement:(id/*XCAccessibilityElement*/)arg1 attributes:(NSArray *)arg2 reply:(void (^)(NSDictionary *, NSError *))arg3; - (void)_XCT_terminateApplicationWithBundleID:(NSString *)arg1 completion:(void (^)(NSError *))arg2; - (void)_XCT_performAccessibilityAction:(int)arg1 onElement:(id/*XCAccessibilityElement*/)arg2 withValue:(id)arg3 reply:(void (^)(NSError *))arg4; diff --git a/PrivateHeaders/XCTest/XCUIApplication.h b/PrivateHeaders/XCTest/XCUIApplication.h index 34bf69032..e092f26db 100644 --- a/PrivateHeaders/XCTest/XCUIApplication.h +++ b/PrivateHeaders/XCTest/XCUIApplication.h @@ -39,7 +39,6 @@ @property(readonly) id/*XCAccessibilityElement*/ accessibilityElement; + (instancetype)applicationWithPID:(pid_t)processID; -/*! DO NOT USE DIRECTLY! Please use fb_activate instead */ - (void)activate; - (void)dismissKeyboard; diff --git a/PrivateHeaders/XCTest/XCUIApplicationProcess.h b/PrivateHeaders/XCTest/XCUIApplicationProcess.h index f0b4d358c..64770393e 100644 --- a/PrivateHeaders/XCTest/XCUIApplicationProcess.h +++ b/PrivateHeaders/XCTest/XCUIApplicationProcess.h @@ -65,7 +65,11 @@ - (void)terminate; - (void)waitForViewControllerViewDidDisappearWithTimeout:(double)arg1; - (void)waitForAutomationSession; +// Before Xcode16-beta5 - (void)waitForQuiescenceIncludingAnimationsIdle:(BOOL)arg1; +// Since Xcode16-beta5 +- (void)waitForQuiescenceIncludingAnimationsIdle:(BOOL)arg1 isPreEvent:(BOOL)arg2; + - (id)shortDescription; - (id)_queue_description; diff --git a/README.md b/README.md index 547559ebc..a367763f7 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Then, you find `WebDriverAgentRunner-Runner-sim-.zip` for iOS and `Web ## License -[`WebDriverAgent` is BSD-licensed](LICENSE). We also provide an additional [patent grant](PATENTS). +[`WebDriverAgent` is BSD-licensed](LICENSE). ## Third Party Sources diff --git a/Scripts/build-webdriveragent.js b/Scripts/build-webdriveragent.mjs similarity index 77% rename from Scripts/build-webdriveragent.js rename to Scripts/build-webdriveragent.mjs index 1958b876d..cd82cd622 100644 --- a/Scripts/build-webdriveragent.js +++ b/Scripts/build-webdriveragent.mjs @@ -1,8 +1,12 @@ -const path = require('path'); -const { asyncify } = require('asyncbox'); -const { logger, fs } = require('@appium/support'); -const { exec } = require('teen_process'); -const xcode = require('appium-xcode'); +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { logger, fs } from '@appium/support'; +import { exec } from 'teen_process'; +import * as xcode from 'appium-xcode'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === __filename; const LOG = new logger.getLogger('WDABuild'); const ROOT_DIR = path.resolve(__dirname, '..'); @@ -16,6 +20,11 @@ const WDA_BUNDLE_TV_PATH = path.join(DERIVED_DATA_PATH, 'Build', 'Products', 'De const TARGETS = ['runner', 'tv_runner']; const SDKS = ['sim', 'tv_sim']; +/** + * Build WebDriverAgent and pack the app bundle into a zip archive. + * + * @param {string} [xcodeVersion] Xcode version to include in archive name. + */ async function buildWebDriverAgent (xcodeVersion) { const target = process.env.TARGET; const sdk = process.env.SDK; @@ -34,7 +43,7 @@ async function buildWebDriverAgent (xcodeVersion) { await exec('xcodebuild', ['clean', '-derivedDataPath', DERIVED_DATA_PATH, '-scheme', 'WebDriverAgentRunner'], { cwd: ROOT_DIR }); - } catch (ign) {} + } catch {} // Get Xcode version xcodeVersion = xcodeVersion || await xcode.getVersion(); @@ -72,8 +81,14 @@ async function buildWebDriverAgent (xcodeVersion) { LOG.info(`Zip bundled at "${appBundleZipPath}"`); } -if (require.main === module) { - asyncify(buildWebDriverAgent); +if (isMainModule) { + try { + await buildWebDriverAgent(); + } catch (e) { + LOG.error(e); + process.exit(1); + } } -module.exports = buildWebDriverAgent; +export default buildWebDriverAgent; + diff --git a/Scripts/build.sh b/Scripts/build.sh index 64195368c..63985e8c2 100755 --- a/Scripts/build.sh +++ b/Scripts/build.sh @@ -4,8 +4,7 @@ # 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. An additional grant -# of patent rights can be found in the PATENTS file in the same directory. +# LICENSE file in the root directory of this source tree. # set -ex @@ -22,9 +21,9 @@ function define_xc_macros() { esac case "${DEST:-}" in - "iphone" ) XC_DESTINATION="name=`echo $IPHONE_MODEL | tr -d "'"`,OS=$IOS_VERSION";; - "ipad" ) XC_DESTINATION="name=`echo $IPAD_MODEL | tr -d "'"`,OS=$IOS_VERSION";; - "tv" ) XC_DESTINATION="name=`echo $TV_MODEL | tr -d "'"`,OS=$TV_VERSION";; + "iphone" ) XC_DESTINATION="platform=iOS Simulator,name=`echo $IPHONE_MODEL | tr -d "'"`,OS=$IOS_VERSION";; + "ipad" ) XC_DESTINATION="platform=iOS Simulator,name=`echo $IPAD_MODEL | tr -d "'"`,OS=$IOS_VERSION";; + "tv" ) XC_DESTINATION="platform=tvOS Simulator,name=`echo $TV_MODEL | tr -d "'"`,OS=$TV_VERSION";; "generic" ) XC_DESTINATION="generic/platform=iOS";; "tv_generic" ) XC_DESTINATION="generic/platform=tvOS" XC_MACROS="${XC_MACROS} ARCHS=arm64";; # tvOS only supports arm64 esac @@ -86,13 +85,33 @@ function xcbuild() { } function fastlane_test() { - bundle install - - if [[ -n "$XC_DESTINATION" ]]; then - SDK="$XC_SDK" DEST="$XC_DESTINATION" SCHEME="$1" bundle exec fastlane test - else - SDK="$XC_SDK" SCHEME="$1" bundle exec fastlane test + # Skip bundle install if already installed (CI already does this) + if ! bundle check &>/dev/null; then + bundle install fi + + case "${DEST:-}" in + "iphone" ) + FASTLANE_DEVICE="$(echo $IPHONE_MODEL | tr -d "'") ($IOS_VERSION)" + ;; + "ipad" ) + FASTLANE_DEVICE="$(echo $IPAD_MODEL | tr -d "'") ($IOS_VERSION)" + ;; + "tv" ) + FASTLANE_DEVICE="$(echo $TV_MODEL | tr -d "'") ($TV_VERSION)" + ;; + * ) + echo "Error: Unknown DEST value '${DEST:-}'. DEST must be one of: iphone, ipad, tv" + exit 1 + ;; + esac + + echo "Fastlane environment variables:" + echo " DEVICE=$FASTLANE_DEVICE" + echo " SCHEME=$1" + echo " SDK=$XC_SDK" + + SDK="$XC_SDK" DEVICE="$FASTLANE_DEVICE" SCHEME="$1" bundle exec fastlane test } define_xc_macros diff --git a/Scripts/ci/build-real.sh b/Scripts/ci/build-real.sh index 93eec1145..bb41b0b6f 100755 --- a/Scripts/ci/build-real.sh +++ b/Scripts/ci/build-real.sh @@ -1,7 +1,5 @@ #!/bin/bash -# To run build script for CI - xcodebuild clean build-for-testing \ -project WebDriverAgent.xcodeproj \ -derivedDataPath $DERIVED_DATA_PATH \ @@ -9,15 +7,18 @@ xcodebuild clean build-for-testing \ -destination "$DESTINATION" \ CODE_SIGNING_ALLOWED=NO ARCHS=arm64 -# Only .app is needed. - pushd $WD -# to remove test packages to refer to the device local instead of embedded ones -# XCTAutomationSupport.framework, XCTest.framewor, XCTestCore.framework, -# XCUIAutomation.framework, XCUnit.framework -rm -rf $SCHEME-Runner.app/Frameworks/XC*.framework - -zip -r $ZIP_PKG_NAME $SCHEME-Runner.app +# The reason why here excludes several frameworks are: +# - to remove test packages to refer to the device local instead of embedded ones +# XCTAutomationSupport.framework, XCTest.framewor, XCTestCore.framework, +# XCUIAutomation.framework, XCUnit.framework. +# This can be excluded only for real devices. +# - Xcode 16 started generating 5.9MB of 'Testing.framework', but it might not be necessary for WDA. +# - libXCTestSwiftSupport is used for Swift testing. WDA doesn't include Swift stuff, thus this is not needed. +zip -r $ZIP_PKG_NAME $SCHEME-Runner.app \ + -x "$SCHEME-Runner.app/Frameworks/XC*.framework*" \ + "$SCHEME-Runner.app/Frameworks/Testing.framework*" \ + "$SCHEME-Runner.app/Frameworks/libXCTestSwiftSupport.dylib" popd mv $WD/$ZIP_PKG_NAME ./ diff --git a/Scripts/ci/build-sim.sh b/Scripts/ci/build-sim.sh index de52abccf..b04cb48b6 100755 --- a/Scripts/ci/build-sim.sh +++ b/Scripts/ci/build-sim.sh @@ -1,19 +1,15 @@ #!/bin/bash -# To run build script for CI - xcodebuild clean build-for-testing \ -project WebDriverAgent.xcodeproj \ - -derivedDataPath wda_build \ + -derivedDataPath $DERIVED_DATA_PATH \ -scheme $SCHEME \ -destination "$DESTINATION" \ CODE_SIGNING_ALLOWED=NO ARCHS=$ARCHS -# simulator needs to build entire build files +pushd $WD -pushd wda_build -# to remove unnecessary space consuming files -rm -rf Build/Intermediates.noindex -zip -r $ZIP_PKG_NAME Build +# Simulators might have an issue to lauch if we drop frameworks even we don't use them. +zip -r $ZIP_PKG_NAME $SCHEME-Runner.app popd -mv wda_build/$ZIP_PKG_NAME ./ +mv $WD/$ZIP_PKG_NAME ./ diff --git a/Scripts/embed-runner-icon.sh b/Scripts/embed-runner-icon.sh new file mode 100755 index 000000000..b71ba0ab5 --- /dev/null +++ b/Scripts/embed-runner-icon.sh @@ -0,0 +1,87 @@ +#!/bin/bash +# Embed the WDA app icon into the wrapping XCTRunner host app so the +# installed WebDriverAgent shows the Appium logo on the iOS home screen +# instead of a blank icon. +# +# Apple's USES_XCTRUNNER auto-generates a Runner.app around UI-testing +# .xctest bundles but does not inherit icons from the test bundle's +# asset catalog. actool produces AppIcon*.png + Assets.car inside +# PlugIns/.xctest/ where iOS never looks. This script lifts +# them up to the Runner.app root and patches Info.plist accordingly. +# +# Limitations: +# - Touches XCTRunner internals; may need updates across Xcode versions. +# - iOS only; tvOS uses different "Brand Assets" and is not handled. +# - Cloud device farms that re-sign WDA must preserve these changes. + +set -euo pipefail + +RUNNER_APP="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}-Runner.app" +XCTEST="${RUNNER_APP}/PlugIns/${PRODUCT_NAME}.xctest" + +if [ ! -d "$RUNNER_APP" ]; then + echo "warning: ${PRODUCT_NAME}-Runner.app not found at $RUNNER_APP; skipping icon embed" + exit 0 +fi + +if [ ! -d "$XCTEST" ]; then + echo "warning: ${PRODUCT_NAME}.xctest not found inside Runner.app; skipping icon embed" + exit 0 +fi + +shopt -s nullglob +ICONS=("$XCTEST"/AppIcon*.png) +if [ ${#ICONS[@]} -eq 0 ]; then + echo "warning: no compiled AppIcon*.png found inside $XCTEST; skipping icon embed" + exit 0 +fi + +cp -f "${ICONS[@]}" "$RUNNER_APP/" +if [ -f "$XCTEST/Assets.car" ]; then + cp -f "$XCTEST/Assets.car" "$RUNNER_APP/" +fi + +PLIST="$RUNNER_APP/Info.plist" +/usr/libexec/PlistBuddy -c "Delete :CFBundleIcons" "$PLIST" 2>/dev/null || true +/usr/libexec/PlistBuddy -c "Delete :CFBundleIcons~ipad" "$PLIST" 2>/dev/null || true + +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons dict" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon dict" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconName string AppIcon" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles array" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles:0 string AppIcon60x60" "$PLIST" + +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad dict" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon dict" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon:CFBundleIconName string AppIcon" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon:CFBundleIconFiles array" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon:CFBundleIconFiles:0 string AppIcon60x60" "$PLIST" +/usr/libexec/PlistBuddy -c "Add :CFBundleIcons~ipad:CFBundlePrimaryIcon:CFBundleIconFiles:1 string AppIcon76x76" "$PLIST" + +# Re-codesign since we modified the bundle after Xcode signed it. +# In a scheme post-action context Xcode's CODE_SIGN_* env vars are not exposed, +# so discover the existing signing identity from the already-signed bundle. +if [ -d "$RUNNER_APP/_CodeSignature" ]; then + # Capture the signature info once. Piping codesign straight into + # `awk ... exit` makes awk close the pipe early, killing codesign with + # SIGPIPE -- which `set -o pipefail` turns into a fatal error. That trips + # only when an Authority line exists, i.e. on every real-device build. + SIGN_INFO=$(codesign -dvv "$RUNNER_APP" 2>&1 || true) + EXISTING_IDENT="${EXPANDED_CODE_SIGN_IDENTITY:-}" + if [ -z "$EXISTING_IDENT" ]; then + EXISTING_IDENT=$(awk -F'=' '/^Authority/ {print $2; exit}' <<< "$SIGN_INFO") + fi + # Simulator builds are ad-hoc signed: there is no Authority line, but the + # bundle can still be re-signed ad-hoc with an identity of "-". + if [ -z "$EXISTING_IDENT" ] && grep -q '^Signature=adhoc' <<< "$SIGN_INFO"; then + EXISTING_IDENT="-" + fi + if [ -n "$EXISTING_IDENT" ]; then + codesign --force --sign "$EXISTING_IDENT" \ + --preserve-metadata=identifier,entitlements "$RUNNER_APP" + else + echo "warning: bundle is signed but no identity discovered; signature will be invalid" + fi +fi + +echo "embedded icon into $RUNNER_APP" diff --git a/Scripts/fetch-prebuilt-wda.js b/Scripts/fetch-prebuilt-wda.mjs similarity index 57% rename from Scripts/fetch-prebuilt-wda.js rename to Scripts/fetch-prebuilt-wda.mjs index 8bedd9a3d..2171e85c7 100644 --- a/Scripts/fetch-prebuilt-wda.js +++ b/Scripts/fetch-prebuilt-wda.mjs @@ -1,14 +1,21 @@ -const path = require('path'); -const axios = require('axios'); -const { asyncify } = require('asyncbox'); -const { logger, fs, mkdirp, net } = require('@appium/support'); -const _ = require('lodash'); -const B = require('bluebird'); +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { readFileSync } from 'node:fs'; +import axios from 'axios'; +import { logger, fs, mkdirp, net } from '@appium/support'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const isMainModule = process.argv[1] && path.resolve(process.argv[1]) === __filename; const log = logger.getLogger('WDA'); +/** + * Download all prebuilt WebDriverAgent archives for the current package version. + */ async function fetchPrebuiltWebDriverAgentAssets () { - const tag = require('../package.json').version; + const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')); + const tag = packageJson.version; log.info(`Getting links to webdriveragent release ${tag}`); const downloadUrl = `https://api.github.com/repos/appium/webdriveragent/releases/tags/v${tag}`; log.info(`Getting WDA release ${downloadUrl}`); @@ -22,7 +29,7 @@ async function fetchPrebuiltWebDriverAgentAssets () { }, })).data; } catch (e) { - throw new Error(`Could not fetch endpoint ${downloadUrl}. Reason: ${e.message}`); + throw new Error(`Could not fetch endpoint ${downloadUrl}. Reason: ${e.message}`, {cause: e}); } const webdriveragentsDir = path.resolve(__dirname, '..', 'prebuilt-agents'); @@ -35,7 +42,9 @@ async function fetchPrebuiltWebDriverAgentAssets () { try { await net.downloadFile(url, targetPath); } catch (err) { - throw new Error(`Problem downloading webdriveragent from url ${url}: ${err.message}`); + throw new Error(`Problem downloading webdriveragent from url ${url}: ${err.message}`, { + cause: err, + }); } } @@ -45,17 +54,26 @@ async function fetchPrebuiltWebDriverAgentAssets () { const url = asset.browser_download_url; log.info(`Downloading: ${url}`); try { - const nameOfAgent = _.last(url.split('/')); + const nameOfAgent = url.split('/').at(-1); + if (!nameOfAgent) { + continue; + } agentsDownloading.push(downloadAgent(url, path.join(webdriveragentsDir, nameOfAgent))); - } catch (ign) { } + } catch { } } // Wait for them all to finish - return await B.all(agentsDownloading); + return await Promise.all(agentsDownloading); } -if (require.main === module) { - asyncify(fetchPrebuiltWebDriverAgentAssets); +if (isMainModule) { + try { + await fetchPrebuiltWebDriverAgentAssets(); + } catch (e) { + log.error(e); + process.exit(1); + } } -module.exports = fetchPrebuiltWebDriverAgentAssets; +export default fetchPrebuiltWebDriverAgentAssets; + diff --git a/Scripts/update-wda-version.mjs b/Scripts/update-wda-version.mjs new file mode 100644 index 000000000..cc74c7fa3 --- /dev/null +++ b/Scripts/update-wda-version.mjs @@ -0,0 +1,42 @@ +import {plist, logger} from '@appium/support'; +import path from 'node:path'; +import semver from 'semver'; + +const log = logger.getLogger('Versioner'); + +/** + * @param {string} argName + * @returns {string|null} + */ +function parseArgValue (argName) { + const argNamePattern = new RegExp(`^--${argName}\\b`); + for (let i = 1; i < process.argv.length; ++i) { + const arg = process.argv[i]; + if (argNamePattern.test(arg)) { + return arg.includes('=') ? arg.split('=')[1] : process.argv[i + 1]; + } + } + return null; +} + +async function updateWdaVersion() { + const newVersion = parseArgValue('package-version'); + if (!newVersion) { + throw new Error('No package version argument (use `--package-version=xxx`)'); + } + if (!semver.valid(newVersion)) { + throw new Error( + `Invalid version specified '${newVersion}'. Version should be in the form '1.2.3'` + ); + } + + const libManifest = path.resolve('WebDriverAgentLib', 'Info.plist'); + log.info(`Updating the WebDriverAgent manifest at '${libManifest}' to version '${newVersion}'`); + await plist.updatePlistFile(libManifest, { + CFBundleShortVersionString: newVersion, + CFBundleVersion: newVersion, + }, false); +} + +(async () => await updateWdaVersion())(); + diff --git a/WebDriverAgent.xcodeproj/project.pbxproj b/WebDriverAgent.xcodeproj/project.pbxproj index 836267cce..904461564 100644 --- a/WebDriverAgent.xcodeproj/project.pbxproj +++ b/WebDriverAgent.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 0E0413382DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */; }; + 0E0413392DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */ = {isa = PBXBuildFile; fileRef = 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */; }; + 0E04133B2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */; }; + 0E04133C2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */; }; 1357E296233D05240054BDB8 /* XCUIHitPointResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */; }; 1357E297233D05240054BDB8 /* XCUIHitPointResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */; }; 13815F6F2328D20400CDAB61 /* FBActiveAppDetectionPoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 13815F6D2328D20400CDAB61 /* FBActiveAppDetectionPoint.h */; }; @@ -21,12 +25,12 @@ 13DE7A4A287C4005003243C6 /* FBXCDeviceEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A47287C4005003243C6 /* FBXCDeviceEvent.h */; }; 13DE7A4B287C4005003243C6 /* FBXCDeviceEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A48287C4005003243C6 /* FBXCDeviceEvent.m */; }; 13DE7A4C287C4005003243C6 /* FBXCDeviceEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A48287C4005003243C6 /* FBXCDeviceEvent.m */; }; - 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; }; - 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; }; + 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A4D287C46BB003243C6 /* FBXCElementSnapshot.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13DE7A51287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */; }; 13DE7A52287C46BB003243C6 /* FBXCElementSnapshot.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A4E287C46BB003243C6 /* FBXCElementSnapshot.m */; }; - 13DE7A55287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; }; - 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; }; + 13DE7A55287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A53287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13DE7A57287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A54287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m */; }; 13DE7A58287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 13DE7A54287CA1EC003243C6 /* FBXCElementSnapshotWrapper.m */; }; 13DE7A5B287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 13DE7A59287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h */; }; @@ -48,8 +52,8 @@ 641EE5D92240C5CA00173FCB /* XCUIElement+FBPickerWheel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */; }; 641EE5DA2240C5CA00173FCB /* XCUIApplicationProcessDelay.m in Sources */ = {isa = PBXBuildFile; fileRef = 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */; }; 641EE5DB2240C5CA00173FCB /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 711084431DA3AA7500F913D6 /* FBXPath.m */; }; + 71B2E0042733FB970074B004 /* FBXPathExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B2E0022733FB970074B002 /* FBXPathExtensions.m */; }; 641EE5DC2240C5CA00173FCB /* XCUIApplication+FBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 719CD8FB2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m */; }; - 641EE5DD2240C5CA00173FCB /* FBAppiumActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 71241D771FAE31F100B9559F /* FBAppiumActionsSynthesizer.m */; }; 641EE5DE2240C5CA00173FCB /* XCUIApplication+FBTouchAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BD20721F86116100B36EC2 /* XCUIApplication+FBTouchAction.m */; }; 641EE5DF2240C5CA00173FCB /* FBWebServer.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB78D1CAEDF0C008C271F /* FBWebServer.m */; }; 641EE5E02240C5CA00173FCB /* FBTCPSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 715557D2211DBCE700613B26 /* FBTCPSocket.m */; }; @@ -109,7 +113,6 @@ 641EE61E2240C5CA00173FCB /* FBExceptionHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = EEC088E71CB56DA400B65968 /* FBExceptionHandler.m */; }; 641EE61F2240C5CA00173FCB /* FBXCodeCompatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = EE5A24411F136C8D0078B1D9 /* FBXCodeCompatibility.m */; }; 641EE6212240C5CA00173FCB /* FBElementTypeTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7901CAEDF0C008C271F /* FBElementTypeTransformer.m */; }; - 641EE6222240C5CA00173FCB /* FBApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7671CAEDF0C008C271F /* FBApplication.m */; }; 641EE6232240C5CA00173FCB /* FBScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC01FFA29180053896D /* FBScreen.m */; }; 641EE6242240C5CA00173FCB /* FBXCTestDaemonsProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = EE35AD7A1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m */; }; 641EE6262240C5CA00173FCB /* FBMathUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1888391DA661C400307AA8 /* FBMathUtils.m */; }; @@ -142,7 +145,6 @@ 641EE64B2240C5CA00173FCB /* XCTAsyncActivity.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACCB1E3B77D600A02D78 /* XCTAsyncActivity.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE64C2240C5CA00173FCB /* XCTestMisuseObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDF1E3B77D600A02D78 /* XCTestMisuseObserver.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE64D2240C5CA00173FCB /* XCTRunnerDaemonSession.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACEF1E3B77D600A02D78 /* XCTRunnerDaemonSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 641EE64E2240C5CA00173FCB /* FBApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7661CAEDF0C008C271F /* FBApplication.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE64F2240C5CA00173FCB /* XCTestExpectationWaiter.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACDA1E3B77D600A02D78 /* XCTestExpectationWaiter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6502240C5CA00173FCB /* UIGestureRecognizer-RecordingAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACAD1E3B77D600A02D78 /* UIGestureRecognizer-RecordingAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6512240C5CA00173FCB /* XCKeyboardKeyMap.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACBF1E3B77D600A02D78 /* XCKeyboardKeyMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -271,7 +273,6 @@ 641EE6D82240C5CA00173FCB /* XCUIElement.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACFE1E3B77D600A02D78 /* XCUIElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6D92240C5CA00173FCB /* XCKeyboardInputSolver.h in Headers */ = {isa = PBXBuildFile; fileRef = EE35ACBE1E3B77D600A02D78 /* XCKeyboardInputSolver.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6DB2240C5CA00173FCB /* FBPasteboard.h in Headers */ = {isa = PBXBuildFile; fileRef = 71930C4020662E1F00D3AFEC /* FBPasteboard.h */; }; - 641EE6DC2240C5CA00173FCB /* FBAppiumActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097451FAE1B32008FB2C5 /* FBAppiumActionsSynthesizer.h */; }; 641EE6DD2240C5CA00173FCB /* FBDebugLogDelegateDecorator.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6DE2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.h in Headers */ = {isa = PBXBuildFile; fileRef = EEDFE11F1D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h */; settings = {ATTRIBUTES = (Public, ); }; }; 641EE6DF2240C5CA00173FCB /* FBMjpegServer.h in Headers */ = {isa = PBXBuildFile; fileRef = 7155D701211DCEF400166C20 /* FBMjpegServer.h */; }; @@ -316,11 +317,11 @@ 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 716F0D9F2A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.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 */; }; + 71B2E0062733FB970074B006 /* FBXPathExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B2E0022733FB970074B002 /* FBXPathExtensions.m */; }; 7119097C2152580600BA3C7E /* XCUIScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 7119097B2152580600BA3C7E /* XCUIScreen.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7119E1EC1E891F8600D0B125 /* FBPickerWheelSelectTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */; }; 711CD03425ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */; }; 711CD03525ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */; }; - 71241D781FAE31F100B9559F /* FBAppiumActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 71241D771FAE31F100B9559F /* FBAppiumActionsSynthesizer.m */; }; 71241D7B1FAE3D2500B9559F /* FBTouchActionCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = 71241D791FAE3D2500B9559F /* FBTouchActionCommands.h */; }; 71241D7C1FAE3D2500B9559F /* FBTouchActionCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71241D7A1FAE3D2500B9559F /* FBTouchActionCommands.m */; }; 71241D7E1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71241D7D1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m */; }; @@ -338,7 +339,6 @@ 713C6DCF1DDC772A00285B92 /* FBElementUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 713C6DCD1DDC772A00285B92 /* FBElementUtils.h */; }; 713C6DD01DDC772A00285B92 /* FBElementUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 713C6DCE1DDC772A00285B92 /* FBElementUtils.m */; }; 714097431FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097411FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h */; }; - 714097471FAE1B32008FB2C5 /* FBAppiumActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097451FAE1B32008FB2C5 /* FBAppiumActionsSynthesizer.h */; }; 7140974B1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h in Headers */ = {isa = PBXBuildFile; fileRef = 714097491FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h */; }; 7140974C1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7140974A1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m */; }; 7140974E1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7140974D1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m */; }; @@ -380,6 +380,10 @@ 7155D704211DCEF400166C20 /* FBMjpegServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7155D702211DCEF400166C20 /* FBMjpegServer.m */; }; 7157B291221DADD2001C348C /* FBXCAXClientProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 7157B28F221DADD2001C348C /* FBXCAXClientProxy.h */; }; 7157B292221DADD2001C348C /* FBXCAXClientProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */; }; + 715A84CF2DD92AD3007134CC /* FBElementHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */; }; + 715A84D02DD92AD3007134CC /* FBElementHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */; }; + 715A84D12DD92AD3007134CC /* FBElementHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */; }; + 715A84D22DD92AD3007134CC /* FBElementHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */; }; 715AFAC11FFA29180053896D /* FBScreen.h in Headers */ = {isa = PBXBuildFile; fileRef = 715AFABF1FFA29180053896D /* FBScreen.h */; }; 715AFAC21FFA29180053896D /* FBScreen.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC01FFA29180053896D /* FBScreen.m */; }; 715AFAC41FFA2AAF0053896D /* FBScreenTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 715AFAC31FFA2AAF0053896D /* FBScreenTests.m */; }; @@ -434,7 +438,6 @@ 71930C4220662E1F00D3AFEC /* FBPasteboard.h in Headers */ = {isa = PBXBuildFile; fileRef = 71930C4020662E1F00D3AFEC /* FBPasteboard.h */; }; 71930C4320662E1F00D3AFEC /* FBPasteboard.m in Sources */ = {isa = PBXBuildFile; fileRef = 71930C4120662E1F00D3AFEC /* FBPasteboard.m */; }; 71930C472066434000D3AFEC /* FBPasteboardTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71930C462066434000D3AFEC /* FBPasteboardTests.m */; }; - 719A97AC1F88E7370063B4BD /* FBAppiumMultiTouchActionsIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 719A97AB1F88E7370063B4BD /* FBAppiumMultiTouchActionsIntegrationTests.m */; }; 719CD8F82126C78F00C7D0C2 /* FBAlertsMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 719CD8F62126C78F00C7D0C2 /* FBAlertsMonitor.h */; }; 719CD8F92126C78F00C7D0C2 /* FBAlertsMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 719CD8F72126C78F00C7D0C2 /* FBAlertsMonitor.m */; }; 719CD8FC2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = 719CD8FA2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h */; }; @@ -458,15 +461,39 @@ 71A7EAFA1E224648001DA4F2 /* FBClassChainQueryParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */; }; 71A7EAFC1E229302001DA4F2 /* FBClassChainTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71A7EAFB1E229302001DA4F2 /* FBClassChainTests.m */; }; 71ACF5B8242F2FDC00F0AAD4 /* FBSafariAlertTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */; }; + 71AE3CF72D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */; }; + 71AE3CF82D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */; }; + 71AE3CF92D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */; }; + 71AE3CFA2D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */; }; 71B155DA23070ECF00646AFB /* FBHTTPStatusCodes.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155D923070ECF00646AFB /* FBHTTPStatusCodes.h */; settings = {ATTRIBUTES = (Public, ); }; }; 71B155DC230711E900646AFB /* FBCommandStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DB230711E900646AFB /* FBCommandStatus.m */; }; 71B155DF23080CA600646AFB /* FBProtocolHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */; }; 71B155E123080CA600646AFB /* FBProtocolHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B155DE23080CA600646AFB /* FBProtocolHelpers.m */; }; 71B49EC71ED1A58100D51AD6 /* XCUIElement+FBUID.h in Headers */ = {isa = PBXBuildFile; fileRef = 71B49EC51ED1A58100D51AD6 /* XCUIElement+FBUID.h */; }; 71B49EC81ED1A58100D51AD6 /* XCUIElement+FBUID.m in Sources */ = {isa = PBXBuildFile; fileRef = 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */; }; + 71BB58DE2B9631B700CB9BFE /* FBVideoRecordingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58DD2B9631B700CB9BFE /* FBVideoRecordingTests.m */; }; + 71BB58E12B9631F100CB9BFE /* FBScreenRecordingPromise.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58DF2B9631F100CB9BFE /* FBScreenRecordingPromise.h */; }; + 71BB58E22B9631F100CB9BFE /* FBScreenRecordingPromise.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58DF2B9631F100CB9BFE /* FBScreenRecordingPromise.h */; }; + 71BB58E32B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */; }; + 71BB58E42B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */; }; + 71BB58E52B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */; }; + 71BB58E82B96328700CB9BFE /* FBScreenRecordingRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58E62B96328700CB9BFE /* FBScreenRecordingRequest.h */; }; + 71BB58E92B96328700CB9BFE /* FBScreenRecordingRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58E62B96328700CB9BFE /* FBScreenRecordingRequest.h */; }; + 71BB58EA2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */; }; + 71BB58EB2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */; }; + 71BB58EC2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */; }; + 71BB58EF2B96511800CB9BFE /* FBVideoCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58ED2B96511800CB9BFE /* FBVideoCommands.h */; }; + 71BB58F02B96511800CB9BFE /* FBVideoCommands.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58ED2B96511800CB9BFE /* FBVideoCommands.h */; }; + 71BB58F12B96511800CB9BFE /* FBVideoCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */; }; + 71BB58F22B96511800CB9BFE /* FBVideoCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */; }; + 71BB58F32B96511800CB9BFE /* FBVideoCommands.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */; }; + 71BB58F62B96531900CB9BFE /* FBScreenRecordingContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58F42B96531900CB9BFE /* FBScreenRecordingContainer.h */; }; + 71BB58F72B96531900CB9BFE /* FBScreenRecordingContainer.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BB58F42B96531900CB9BFE /* FBScreenRecordingContainer.h */; }; + 71BB58F82B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */; }; + 71BB58F92B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */; }; + 71BB58FA2B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */; }; 71BD20731F86116100B36EC2 /* XCUIApplication+FBTouchAction.h in Headers */ = {isa = PBXBuildFile; fileRef = 71BD20711F86116100B36EC2 /* XCUIApplication+FBTouchAction.h */; }; 71BD20741F86116100B36EC2 /* XCUIApplication+FBTouchAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BD20721F86116100B36EC2 /* XCUIApplication+FBTouchAction.m */; }; - 71BD20781F869E0F00B36EC2 /* FBAppiumTouchActionsIntegrationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 71BD20771F869E0F00B36EC2 /* FBAppiumTouchActionsIntegrationTests.m */; }; 71C8E55125399A6B008572C1 /* XCUIApplication+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 71C8E54F25399A6B008572C1 /* XCUIApplication+FBQuiescence.h */; }; 71C8E55225399A6B008572C1 /* XCUIApplication+FBQuiescence.h in Headers */ = {isa = PBXBuildFile; fileRef = 71C8E54F25399A6B008572C1 /* XCUIApplication+FBQuiescence.h */; }; 71C8E55325399A6B008572C1 /* XCUIApplication+FBQuiescence.m in Sources */ = {isa = PBXBuildFile; fileRef = 71C8E55025399A6B008572C1 /* XCUIApplication+FBQuiescence.m */; }; @@ -504,6 +531,7 @@ 71F5BE50252F14EB00EE9EBA /* FBExceptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */; }; 71F5BE51252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; 71F5BE52252F14EB00EE9EBA /* FBExceptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */; }; + AABBCCDDEEFF001122334457 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AABBCCDDEEFF001122334456 /* SceneDelegate.m */; }; AD35D06C1CF1C35500870A75 /* WebDriverAgentLib.framework in Copy frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; AD6C26941CF2379700F8B5FF /* FBAlert.h in Headers */ = {isa = PBXBuildFile; fileRef = AD6C26921CF2379700F8B5FF /* FBAlert.h */; settings = {ATTRIBUTES = (Public, ); }; }; AD6C26951CF2379700F8B5FF /* FBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = AD6C26931CF2379700F8B5FF /* FBAlert.m */; }; @@ -518,6 +546,10 @@ ADBC39981D07842800327304 /* XCUIElementDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = ADBC39971D07842800327304 /* XCUIElementDouble.m */; }; ADDA07241D6BB2BF001700AC /* FBScrollViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ADDA07231D6BB2BF001700AC /* FBScrollViewController.m */; }; ADEF63AF1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ADEF63AE1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m */; }; + B316351C2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */ = {isa = PBXBuildFile; fileRef = B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */; }; + 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 */; }; C845206222D5E79400EA68CB /* FBUnattachedAppLauncher.h in Headers */ = {isa = PBXBuildFile; fileRef = C8FB547722D4C1FC00B69954 /* FBUnattachedAppLauncher.h */; }; C845206322D5E79700EA68CB /* FBUnattachedAppLauncher.m in Sources */ = {isa = PBXBuildFile; fileRef = C8FB547822D4C1FC00B69954 /* FBUnattachedAppLauncher.m */; }; C8FB547422D3949C00B69954 /* LSApplicationWorkspace.h in Headers */ = {isa = PBXBuildFile; fileRef = C8FB547322D3949C00B69954 /* LSApplicationWorkspace.h */; }; @@ -619,8 +651,6 @@ EE158AE91CBD456F00A3E3F0 /* FBElementTypeTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7901CAEDF0C008C271F /* FBElementTypeTransformer.m */; }; EE158AEA1CBD456F00A3E3F0 /* FBRuntimeUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7911CAEDF0C008C271F /* FBRuntimeUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE158AEB1CBD456F00A3E3F0 /* FBRuntimeUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */; }; - EE158AF51CBD456F00A3E3F0 /* FBApplication.h in Headers */ = {isa = PBXBuildFile; fileRef = EE9AB7661CAEDF0C008C271F /* FBApplication.h */; settings = {ATTRIBUTES = (Public, ); }; }; - EE158AF61CBD456F00A3E3F0 /* FBApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = EE9AB7671CAEDF0C008C271F /* FBApplication.m */; }; EE158B5A1CBD462100A3E3F0 /* WebDriverAgentLib.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EE158A991CBD452B00A3E3F0 /* WebDriverAgentLib.framework */; }; EE158B5F1CBD47A000A3E3F0 /* WebDriverAgentLib.h in Headers */ = {isa = PBXBuildFile; fileRef = EE158B5E1CBD47A000A3E3F0 /* WebDriverAgentLib.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE18883A1DA661C400307AA8 /* FBMathUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EE1888381DA661C400307AA8 /* FBMathUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -756,7 +786,7 @@ EE55B3271D1D54CF003AAAEC /* FBScrollingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE55B3261D1D54CF003AAAEC /* FBScrollingTests.m */; }; EE5A24421F136D360078B1D9 /* FBXCodeCompatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = EE5A24411F136C8D0078B1D9 /* FBXCodeCompatibility.m */; }; EE6A89261D0B19E60083E92B /* FBSessionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89251D0B19E60083E92B /* FBSessionTests.m */; }; - EE6A892B1D0B25820083E92B /* FBApplicationDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89281D0B257B0083E92B /* FBApplicationDouble.m */; }; + EE6A892B1D0B25820083E92B /* XCUIApplicationDouble.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89281D0B257B0083E92B /* XCUIApplicationDouble.m */; }; EE6A892D1D0B2AF40083E92B /* FBErrorBuilderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A892C1D0B2AF40083E92B /* FBErrorBuilderTests.m */; }; EE6A89371D0B35920083E92B /* FBFailureProofTestCaseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = EE6A89361D0B35920083E92B /* FBFailureProofTestCaseTests.m */; }; EE6A893A1D0B38640083E92B /* FBFailureProofTestCase.h in Headers */ = {isa = PBXBuildFile; fileRef = EE6A89381D0B38640083E92B /* FBFailureProofTestCase.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -794,6 +824,11 @@ EEE3764A1D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */; }; EEE9B4721CD02B88009D2030 /* FBRunLoopSpinner.h in Headers */ = {isa = PBXBuildFile; fileRef = EEE9B4701CD02B88009D2030 /* FBRunLoopSpinner.h */; settings = {ATTRIBUTES = (Public, ); }; }; EEE9B4731CD02B88009D2030 /* FBRunLoopSpinner.m in Sources */ = {isa = PBXBuildFile; fileRef = EEE9B4711CD02B88009D2030 /* FBRunLoopSpinner.m */; }; + F59CD6D42EF16E5E00F91287 /* XCUIElement+FBCustomActions.h in Headers */ = {isa = PBXBuildFile; fileRef = F59CD6D22EF16E5E00F91287 /* XCUIElement+FBCustomActions.h */; }; + F59CD6D52EF16E5E00F91287 /* XCUIElement+FBCustomActions.m in Sources */ = {isa = PBXBuildFile; fileRef = F59CD6D32EF16E5E00F91287 /* XCUIElement+FBCustomActions.m */; }; + F59CD6D62EF16E5E00F91287 /* XCUIElement+FBCustomActions.h in Headers */ = {isa = PBXBuildFile; fileRef = F59CD6D22EF16E5E00F91287 /* XCUIElement+FBCustomActions.h */; }; + F59CD6D72EF16E5E00F91287 /* XCUIElement+FBCustomActions.m in Sources */ = {isa = PBXBuildFile; fileRef = F59CD6D32EF16E5E00F91287 /* XCUIElement+FBCustomActions.m */; }; + A1B2C3D4E5F600000000001B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F600000000001A /* Assets.xcassets */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -895,6 +930,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 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 = ""; }; 1357E295233D05240054BDB8 /* XCUIHitPointResult.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = XCUIHitPointResult.h; sourceTree = ""; }; 13815F6D2328D20400CDAB61 /* FBActiveAppDetectionPoint.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBActiveAppDetectionPoint.h; sourceTree = ""; }; 13815F6E2328D20400CDAB61 /* FBActiveAppDetectionPoint.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBActiveAppDetectionPoint.m; sourceTree = ""; }; @@ -941,10 +978,11 @@ 64B26509228CE4FF002A5025 /* FBTVNavigationTracker-Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FBTVNavigationTracker-Private.h"; sourceTree = ""; }; 711084421DA3AA7500F913D6 /* FBXPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBXPath.h; sourceTree = ""; }; 711084431DA3AA7500F913D6 /* FBXPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBXPath.m; sourceTree = ""; }; + 71B2E0012733FB970074B001 /* FBXPathExtensions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXPathExtensions.h; sourceTree = ""; }; + 71B2E0022733FB970074B002 /* FBXPathExtensions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXPathExtensions.m; sourceTree = ""; }; 7119097B2152580600BA3C7E /* XCUIScreen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIScreen.h; sourceTree = ""; }; 7119E1EB1E891F8600D0B125 /* FBPickerWheelSelectTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBPickerWheelSelectTests.m; sourceTree = ""; }; 711CD03325ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIScreenDataSource-Protocol.h"; sourceTree = ""; }; - 71241D771FAE31F100B9559F /* FBAppiumActionsSynthesizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBAppiumActionsSynthesizer.m; sourceTree = ""; }; 71241D791FAE3D2500B9559F /* FBTouchActionCommands.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBTouchActionCommands.h; sourceTree = ""; }; 71241D7A1FAE3D2500B9559F /* FBTouchActionCommands.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBTouchActionCommands.m; sourceTree = ""; }; 71241D7D1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBW3CTouchActionsIntegrationTests.m; sourceTree = ""; }; @@ -962,7 +1000,6 @@ 713C6DCD1DDC772A00285B92 /* FBElementUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBElementUtils.h; sourceTree = ""; }; 713C6DCE1DDC772A00285B92 /* FBElementUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBElementUtils.m; sourceTree = ""; }; 714097411FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBBaseActionsSynthesizer.h; sourceTree = ""; }; - 714097451FAE1B32008FB2C5 /* FBAppiumActionsSynthesizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBAppiumActionsSynthesizer.h; sourceTree = ""; }; 714097491FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBW3CActionsSynthesizer.h; sourceTree = ""; }; 7140974A1FAE1B51008FB2C5 /* FBW3CActionsSynthesizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBW3CActionsSynthesizer.m; sourceTree = ""; }; 7140974D1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBBaseActionsSynthesizer.m; sourceTree = ""; }; @@ -993,6 +1030,8 @@ 7155D702211DCEF400166C20 /* FBMjpegServer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBMjpegServer.m; sourceTree = ""; }; 7157B28F221DADD2001C348C /* FBXCAXClientProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXCAXClientProxy.h; sourceTree = ""; }; 7157B290221DADD2001C348C /* FBXCAXClientProxy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCAXClientProxy.m; sourceTree = ""; }; + 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBElementHelpers.h; sourceTree = ""; }; + 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementHelpers.m; sourceTree = ""; }; 715AFABF1FFA29180053896D /* FBScreen.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreen.h; sourceTree = ""; }; 715AFAC01FFA29180053896D /* FBScreen.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreen.m; sourceTree = ""; }; 715AFAC31FFA2AAF0053896D /* FBScreenTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenTests.m; sourceTree = ""; }; @@ -1022,7 +1061,6 @@ 71930C4020662E1F00D3AFEC /* FBPasteboard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBPasteboard.h; sourceTree = ""; }; 71930C4120662E1F00D3AFEC /* FBPasteboard.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBPasteboard.m; sourceTree = ""; }; 71930C462066434000D3AFEC /* FBPasteboardTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBPasteboardTests.m; sourceTree = ""; }; - 719A97AB1F88E7370063B4BD /* FBAppiumMultiTouchActionsIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBAppiumMultiTouchActionsIntegrationTests.m; sourceTree = ""; }; 719CD8F62126C78F00C7D0C2 /* FBAlertsMonitor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBAlertsMonitor.h; sourceTree = ""; }; 719CD8F72126C78F00C7D0C2 /* FBAlertsMonitor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBAlertsMonitor.m; sourceTree = ""; }; 719CD8FA2126C88B00C7D0C2 /* XCUIApplication+FBAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIApplication+FBAlert.h"; sourceTree = ""; }; @@ -1042,15 +1080,25 @@ 71A7EAF81E224648001DA4F2 /* FBClassChainQueryParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBClassChainQueryParser.m; sourceTree = ""; }; 71A7EAFB1E229302001DA4F2 /* FBClassChainTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBClassChainTests.m; sourceTree = ""; }; 71ACF5B7242F2FDC00F0AAD4 /* FBSafariAlertTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBSafariAlertTests.m; sourceTree = ""; }; + 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBVisibleFrame.h"; sourceTree = ""; }; + 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBVisibleFrame.m"; sourceTree = ""; }; 71B155D923070ECF00646AFB /* FBHTTPStatusCodes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBHTTPStatusCodes.h; sourceTree = ""; }; 71B155DB230711E900646AFB /* FBCommandStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBCommandStatus.m; sourceTree = ""; }; 71B155DD23080CA600646AFB /* FBProtocolHelpers.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBProtocolHelpers.h; sourceTree = ""; }; 71B155DE23080CA600646AFB /* FBProtocolHelpers.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBProtocolHelpers.m; sourceTree = ""; }; 71B49EC51ED1A58100D51AD6 /* XCUIElement+FBUID.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBUID.h"; sourceTree = ""; }; 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBUID.m"; sourceTree = ""; }; + 71BB58DD2B9631B700CB9BFE /* FBVideoRecordingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBVideoRecordingTests.m; sourceTree = ""; }; + 71BB58DF2B9631F100CB9BFE /* FBScreenRecordingPromise.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreenRecordingPromise.h; sourceTree = ""; }; + 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenRecordingPromise.m; sourceTree = ""; }; + 71BB58E62B96328700CB9BFE /* FBScreenRecordingRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreenRecordingRequest.h; sourceTree = ""; }; + 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenRecordingRequest.m; sourceTree = ""; }; + 71BB58ED2B96511800CB9BFE /* FBVideoCommands.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBVideoCommands.h; sourceTree = ""; }; + 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBVideoCommands.m; sourceTree = ""; }; + 71BB58F42B96531900CB9BFE /* FBScreenRecordingContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreenRecordingContainer.h; sourceTree = ""; }; + 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBScreenRecordingContainer.m; sourceTree = ""; }; 71BD20711F86116100B36EC2 /* XCUIApplication+FBTouchAction.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIApplication+FBTouchAction.h"; sourceTree = ""; }; 71BD20721F86116100B36EC2 /* XCUIApplication+FBTouchAction.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIApplication+FBTouchAction.m"; sourceTree = ""; }; - 71BD20771F869E0F00B36EC2 /* FBAppiumTouchActionsIntegrationTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBAppiumTouchActionsIntegrationTests.m; sourceTree = ""; }; 71C8E54F25399A6B008572C1 /* XCUIApplication+FBQuiescence.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIApplication+FBQuiescence.h"; sourceTree = ""; }; 71C8E55025399A6B008572C1 /* XCUIApplication+FBQuiescence.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIApplication+FBQuiescence.m"; sourceTree = ""; }; 71C9EAAA25E8415A00470CD8 /* FBScreenshot.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBScreenshot.h; sourceTree = ""; }; @@ -1071,6 +1119,8 @@ 71F5BE33252E5B2200EE9EBA /* FBElementSwipingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBElementSwipingTests.m; sourceTree = ""; }; 71F5BE4D252F14EB00EE9EBA /* FBExceptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBExceptions.h; sourceTree = ""; }; 71F5BE4E252F14EB00EE9EBA /* FBExceptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBExceptions.m; 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 = ""; }; AD42DD2A1CF121E600806E5D /* module.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = module.modulemap; sourceTree = ""; }; AD6C26921CF2379700F8B5FF /* FBAlert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FBAlert.h; path = WebDriverAgentLib/FBAlert.h; sourceTree = SOURCE_ROOT; }; AD6C26931CF2379700F8B5FF /* FBAlert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; name = FBAlert.m; path = WebDriverAgentLib/FBAlert.m; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; @@ -1087,6 +1137,8 @@ ADDA07221D6BB2BF001700AC /* FBScrollViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBScrollViewController.h; sourceTree = ""; }; ADDA07231D6BB2BF001700AC /* FBScrollViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBScrollViewController.m; sourceTree = ""; }; ADEF63AE1D09DEBE0070A7E3 /* FBRuntimeUtilsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRuntimeUtilsTests.m; sourceTree = ""; }; + B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBAccessibilityTraits.m; sourceTree = ""; }; + B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBAccessibilityTraits.h; sourceTree = ""; }; C8FB547322D3949C00B69954 /* LSApplicationWorkspace.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LSApplicationWorkspace.h; sourceTree = ""; }; C8FB547722D4C1FC00B69954 /* FBUnattachedAppLauncher.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBUnattachedAppLauncher.h; sourceTree = ""; }; C8FB547822D4C1FC00B69954 /* FBUnattachedAppLauncher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBUnattachedAppLauncher.m; sourceTree = ""; }; @@ -1254,8 +1306,8 @@ EE5A24401F136C8D0078B1D9 /* FBXCodeCompatibility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FBXCodeCompatibility.h; sourceTree = ""; }; EE5A24411F136C8D0078B1D9 /* FBXCodeCompatibility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FBXCodeCompatibility.m; sourceTree = ""; }; EE6A89251D0B19E60083E92B /* FBSessionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBSessionTests.m; sourceTree = ""; }; - EE6A89271D0B257B0083E92B /* FBApplicationDouble.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBApplicationDouble.h; sourceTree = ""; }; - EE6A89281D0B257B0083E92B /* FBApplicationDouble.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBApplicationDouble.m; sourceTree = ""; }; + EE6A89271D0B257B0083E92B /* XCUIApplicationDouble.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = XCUIApplicationDouble.h; sourceTree = ""; }; + EE6A89281D0B257B0083E92B /* XCUIApplicationDouble.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = XCUIApplicationDouble.m; sourceTree = ""; }; EE6A892C1D0B2AF40083E92B /* FBErrorBuilderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBErrorBuilderTests.m; sourceTree = ""; }; EE6A89361D0B35920083E92B /* FBFailureProofTestCaseTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBFailureProofTestCaseTests.m; sourceTree = ""; }; EE6A89381D0B38640083E92B /* FBFailureProofTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBFailureProofTestCase.h; sourceTree = ""; }; @@ -1296,8 +1348,6 @@ EE9AB7631CAEDF0C008C271F /* FBTouchIDCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBTouchIDCommands.m; sourceTree = ""; }; EE9AB7641CAEDF0C008C271F /* FBUnknownCommands.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBUnknownCommands.h; sourceTree = ""; }; EE9AB7651CAEDF0C008C271F /* FBUnknownCommands.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBUnknownCommands.m; sourceTree = ""; }; - EE9AB7661CAEDF0C008C271F /* FBApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FBApplication.h; path = WebDriverAgentLib/FBApplication.h; sourceTree = SOURCE_ROOT; }; - EE9AB7671CAEDF0C008C271F /* FBApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FBApplication.m; path = WebDriverAgentLib/FBApplication.m; sourceTree = SOURCE_ROOT; }; EE9AB7731CAEDF0C008C271F /* WebDriverAgent.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = WebDriverAgent.bundle; sourceTree = ""; }; EE9AB7751CAEDF0C008C271F /* FBCommandHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBCommandHandler.h; sourceTree = ""; }; EE9AB7761CAEDF0C008C271F /* FBCommandStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBCommandStatus.h; sourceTree = ""; }; @@ -1323,6 +1373,7 @@ EE9AB7921CAEDF0C008C271F /* FBRuntimeUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FBRuntimeUtils.m; sourceTree = ""; }; EE9AB7FC1CAEE048008C271F /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = WebDriverAgentRunner/Info.plist; sourceTree = SOURCE_ROOT; }; EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UITestingUITests.m; path = WebDriverAgentRunner/UITestingUITests.m; sourceTree = SOURCE_ROOT; }; + A1B2C3D4E5F600000000001A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = WebDriverAgentRunner/Assets.xcassets; sourceTree = SOURCE_ROOT; }; EE9AB8031CAEE182008C271F /* build.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = ""; }; EE9B75D41CF7956C00275851 /* IntegrationApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = IntegrationApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; EE9B75EC1CF7956C00275851 /* IntegrationTests_1.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = IntegrationTests_1.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1363,6 +1414,8 @@ 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; }; + F59CD6D22EF16E5E00F91287 /* XCUIElement+FBCustomActions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XCUIElement+FBCustomActions.h"; sourceTree = ""; }; + F59CD6D32EF16E5E00F91287 /* XCUIElement+FBCustomActions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "XCUIElement+FBCustomActions.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1615,8 +1668,8 @@ 13FFF2F1287DBEE600E561E4 /* XCElementSnapshotDouble.m */, ADBC39961D07842800327304 /* XCUIElementDouble.h */, ADBC39971D07842800327304 /* XCUIElementDouble.m */, - EE6A89271D0B257B0083E92B /* FBApplicationDouble.h */, - EE6A89281D0B257B0083E92B /* FBApplicationDouble.m */, + EE6A89271D0B257B0083E92B /* XCUIApplicationDouble.h */, + EE6A89281D0B257B0083E92B /* XCUIApplicationDouble.m */, ); path = Doubles; sourceTree = ""; @@ -1753,6 +1806,8 @@ EE8DDD7C20C5733B004D4925 /* XCUIElement+FBForceTouch.m */, EE9AB7471CAEDF0C008C271F /* XCUIElement+FBIsVisible.h */, EE9AB7481CAEDF0C008C271F /* XCUIElement+FBIsVisible.m */, + 0E04133A2DF1E15900AF007C /* XCUIElement+FBMinMax.h */, + 0E0413372DF1E15100AF007C /* XCUIElement+FBMinMax.m */, 7136A4771E8918E60024FC3D /* XCUIElement+FBPickerWheel.h */, 7136A4781E8918E60024FC3D /* XCUIElement+FBPickerWheel.m */, 71D3B3D3267FC7260076473D /* XCUIElement+FBResolve.h */, @@ -1767,6 +1822,8 @@ 71B49EC61ED1A58100D51AD6 /* XCUIElement+FBUID.m */, EEE3763F1D59F81400ED88DD /* XCUIElement+FBUtilities.h */, EEE376401D59F81400ED88DD /* XCUIElement+FBUtilities.m */, + 71AE3CF52D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h */, + 71AE3CF62D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m */, EEE376471D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.h */, EEE376481D59FAE900ED88DD /* XCUIElement+FBWebDriverAttributes.m */, 641EE7042240CDCF00173FCB /* XCUIElement+FBTVFocuse.h */, @@ -1775,6 +1832,8 @@ 71E75E6C254824230099FC87 /* XCUIElementQuery+FBHelpers.m */, 13DE7A59287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h */, 13DE7A5A287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m */, + F59CD6D22EF16E5E00F91287 /* XCUIElement+FBCustomActions.h */, + F59CD6D32EF16E5E00F91287 /* XCUIElement+FBCustomActions.m */, ); name = Categories; path = WebDriverAgentLib/Categories; @@ -1805,6 +1864,8 @@ EE9AB7631CAEDF0C008C271F /* FBTouchIDCommands.m */, EE9AB7641CAEDF0C008C271F /* FBUnknownCommands.h */, EE9AB7651CAEDF0C008C271F /* FBUnknownCommands.m */, + 71BB58ED2B96511800CB9BFE /* FBVideoCommands.h */, + 71BB58EE2B96511800CB9BFE /* FBVideoCommands.m */, ); name = Commands; path = WebDriverAgentLib/Commands; @@ -1844,6 +1905,12 @@ EE9AB7861CAEDF0C008C271F /* FBRouteRequest-Private.h */, EE9AB7871CAEDF0C008C271F /* FBRouteRequest.h */, EE9AB7881CAEDF0C008C271F /* FBRouteRequest.m */, + 71BB58F42B96531900CB9BFE /* FBScreenRecordingContainer.h */, + 71BB58F52B96531900CB9BFE /* FBScreenRecordingContainer.m */, + 71BB58DF2B9631F100CB9BFE /* FBScreenRecordingPromise.h */, + 71BB58E02B9631F100CB9BFE /* FBScreenRecordingPromise.m */, + 71BB58E62B96328700CB9BFE /* FBScreenRecordingRequest.h */, + 71BB58E72B96328700CB9BFE /* FBScreenRecordingRequest.m */, EE9AB7891CAEDF0C008C271F /* FBSession-Private.h */, EE9AB78A1CAEDF0C008C271F /* FBSession.h */, EE9AB78B1CAEDF0C008C271F /* FBSession.m */, @@ -1872,8 +1939,6 @@ 13815F6E2328D20400CDAB61 /* FBActiveAppDetectionPoint.m */, 719CD8F62126C78F00C7D0C2 /* FBAlertsMonitor.h */, 719CD8F72126C78F00C7D0C2 /* FBAlertsMonitor.m */, - 714097451FAE1B32008FB2C5 /* FBAppiumActionsSynthesizer.h */, - 71241D771FAE31F100B9559F /* FBAppiumActionsSynthesizer.m */, 714097411FAE1B0B008FB2C5 /* FBBaseActionsSynthesizer.h */, 7140974D1FAE20EE008FB2C5 /* FBBaseActionsSynthesizer.m */, 714EAA0B2673FDFE005C5B47 /* FBCapabilities.h */, @@ -1884,6 +1949,8 @@ EE9B76A21CF7A43900275851 /* FBConfiguration.m */, EE7E27181D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h */, EE7E27191D06C69F001BEC7B /* FBDebugLogDelegateDecorator.m */, + 715A84CD2DD92AD3007134CC /* FBElementHelpers.h */, + 715A84CE2DD92AD3007134CC /* FBElementHelpers.m */, EE9AB78F1CAEDF0C008C271F /* FBElementTypeTransformer.h */, EE9AB7901CAEDF0C008C271F /* FBElementTypeTransformer.m */, EE3A18601CDE618F00DE4205 /* FBErrorBuilder.h */, @@ -1937,12 +2004,16 @@ 714D88CA2733FB970074A925 /* FBXMLGenerationOptions.h */, 714D88CB2733FB970074A925 /* FBXMLGenerationOptions.m */, 712A0C861DA3E55D007D02E5 /* FBXPath-Private.h */, - 711084421DA3AA7500F913D6 /* FBXPath.h */, - 711084431DA3AA7500F913D6 /* FBXPath.m */, - EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */, + 711084421DA3AA7500F913D6 /* FBXPath.h */, + 711084431DA3AA7500F913D6 /* FBXPath.m */, + 71B2E0012733FB970074B001 /* FBXPathExtensions.h */, + 71B2E0022733FB970074B002 /* FBXPathExtensions.m */, + EE6B64FB1D0F86EF00E85F5D /* XCTestPrivateSymbols.h */, EE6B64FC1D0F86EF00E85F5D /* XCTestPrivateSymbols.m */, 633E904A220DEE7F007CADF9 /* XCUIApplicationProcessDelay.h */, 6385F4A5220A40760095BBDB /* XCUIApplicationProcessDelay.m */, + B316351B2DDF0CF5007D9317 /* FBAccessibilityTraits.m */, + B316351E2DDF0D0B007D9317 /* FBAccessibilityTraits.h */, ); name = Utilities; path = WebDriverAgentLib/Utilities; @@ -1971,8 +2042,6 @@ isa = PBXGroup; children = ( EE9B76991CF799F400275851 /* FBAlertTests.m */, - 71BD20771F869E0F00B36EC2 /* FBAppiumTouchActionsIntegrationTests.m */, - 719A97AB1F88E7370063B4BD /* FBAppiumMultiTouchActionsIntegrationTests.m */, 719CD8FE2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m */, EE26409C1D0EBA25009BE6B0 /* FBElementAttributeTests.m */, 71F5BE33252E5B2200EE9EBA /* FBElementSwipingTests.m */, @@ -1992,6 +2061,7 @@ EE1E06DC1D1811C4007CF043 /* FBTestMacros.h */, AD76723F1D6B826F00610457 /* FBTypingTest.m */, 714CA3C61DC23186000F12C9 /* FBXPathIntegrationTests.m */, + 71BB58DD2B9631B700CB9BFE /* FBVideoRecordingTests.m */, 71241D7D1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m */, 71241D7F1FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m */, 7136C0F8243A182400921C76 /* FBW3CTypeActionsTests.m */, @@ -2056,6 +2126,8 @@ children = ( EE9B76821CF7997600275851 /* AppDelegate.h */, EE9B76831CF7997600275851 /* AppDelegate.m */, + AABBCCDDEEFF001122334455 /* SceneDelegate.h */, + AABBCCDDEEFF001122334456 /* SceneDelegate.m */, EE1E06E51D182E95007CF043 /* FBAlertViewController.h */, EE1E06E61D182E95007CF043 /* FBAlertViewController.m */, EE8BA9781DCCED9A00A9DEF8 /* FBNavigationController.h */, @@ -2096,8 +2168,6 @@ EE158B5E1CBD47A000A3E3F0 /* WebDriverAgentLib.h */, AD6C26921CF2379700F8B5FF /* FBAlert.h */, AD6C26931CF2379700F8B5FF /* FBAlert.m */, - EE9AB7661CAEDF0C008C271F /* FBApplication.h */, - EE9AB7671CAEDF0C008C271F /* FBApplication.m */, EE3A18641CDE734B00DE4205 /* FBKeyboard.h */, EE3A18651CDE734B00DE4205 /* FBKeyboard.m */, EE158B5D1CBD479000A3E3F0 /* Info.plist */, @@ -2232,6 +2302,7 @@ children = ( EE9AB7FC1CAEE048008C271F /* Info.plist */, EE9AB7FD1CAEE048008C271F /* UITestingUITests.m */, + A1B2C3D4E5F600000000001A /* Assets.xcassets */, ); name = WebDriverAgentRunner; path = XCTUITestRunner; @@ -2259,6 +2330,8 @@ 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 */, 641EE63A2240C5CA00173FCB /* XCTest.h in Headers */, 641EE63B2240C5CA00173FCB /* FBAlertsMonitor.h in Headers */, @@ -2283,10 +2356,10 @@ 641EE64D2240C5CA00173FCB /* XCTRunnerDaemonSession.h in Headers */, 714E14B929805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.h in Headers */, 64B2650B228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */, - 641EE64E2240C5CA00173FCB /* FBApplication.h in Headers */, 641EE64F2240C5CA00173FCB /* XCTestExpectationWaiter.h in Headers */, 13DE7A5C287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.h in Headers */, 641EE6502240C5CA00173FCB /* UIGestureRecognizer-RecordingAdditions.h in Headers */, + 71BB58E92B96328700CB9BFE /* FBScreenRecordingRequest.h in Headers */, 641EE6512240C5CA00173FCB /* XCKeyboardKeyMap.h in Headers */, 641EE6522240C5CA00173FCB /* XCTNSPredicateExpectationObject-Protocol.h in Headers */, 641EE6532240C5CA00173FCB /* WebDriverAgentLib.h in Headers */, @@ -2303,10 +2376,10 @@ 641EE65F2240C5CA00173FCB /* XCSourceCodeTreeNodeEnumerator.h in Headers */, 641EE6602240C5CA00173FCB /* XCUIElement+FBIsVisible.h in Headers */, 641EE6622240C5CA00173FCB /* FBResponsePayload.h in Headers */, + 71BB58E22B9631F100CB9BFE /* FBScreenRecordingPromise.h in Headers */, 641EE6632240C5CA00173FCB /* FBUnknownCommands.h in Headers */, 641EE7062240CDCF00173FCB /* XCUIElement+FBTVFocuse.h in Headers */, 71822738258744B800661B83 /* HTTPConnection.h in Headers */, - 13DE7A56287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */, 641EE6642240C5CA00173FCB /* NSPredicate+FBFormat.h in Headers */, 641EE6652240C5CA00173FCB /* UILongPressGestureRecognizer-RecordingAdditions.h in Headers */, 641EE6662240C5CA00173FCB /* XCTestCase.h in Headers */, @@ -2328,11 +2401,13 @@ 641EE6732240C5CA00173FCB /* FBDebugCommands.h in Headers */, 641EE6742240C5CA00173FCB /* XCTestSuite.h in Headers */, 641EE6752240C5CA00173FCB /* XCUICoordinate.h in Headers */, + 715A84D22DD92AD3007134CC /* FBElementHelpers.h in Headers */, 641EE6762240C5CA00173FCB /* XCTNSPredicateExpectation.h in Headers */, 641EE6772240C5CA00173FCB /* XCTestObservationCenter.h in Headers */, 641EE6782240C5CA00173FCB /* XCTNSNotificationExpectation.h in Headers */, 641EE6792240C5CA00173FCB /* XCUIRecorderNodeFinder.h in Headers */, 641EE67A2240C5CA00173FCB /* XCUIElement+FBAccessibility.h in Headers */, + 0E04133C2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */, 641EE67B2240C5CA00173FCB /* XCUIRecorderUtilities.h in Headers */, 6496A5DA230D6EB30087F8CB /* AXSettings.h in Headers */, 641EE67C2240C5CA00173FCB /* XCTestCaseRun.h in Headers */, @@ -2381,9 +2456,11 @@ 641EE6A42240C5CA00173FCB /* FBCommandHandler.h in Headers */, 641EE6A52240C5CA00173FCB /* FBSessionCommands.h in Headers */, 641EE70C2240CE2D00173FCB /* FBTVNavigationTracker.h in Headers */, + 71AE3CF72D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */, 641EE6A62240C5CA00173FCB /* FBImageProcessor.h in Headers */, 641EE6A72240C5CA00173FCB /* FBSession-Private.h in Headers */, 641EE6A82240C5CA00173FCB /* NSString+FBXMLSafeString.h in Headers */, + B316351F2DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */, 64E3502F2AC0B6FE005F3ACB /* NSDictionary+FBUtf8SafeDictionary.h in Headers */, 641EE6A92240C5CA00173FCB /* FBCommandStatus.h in Headers */, 71822702258744A400661B83 /* HTTPResponseProxy.h in Headers */, @@ -2405,6 +2482,7 @@ 7182276E258744C900661B83 /* HTTPErrorResponse.h in Headers */, 641EE6B82240C5CA00173FCB /* FBAlert.h in Headers */, 641EE6B92240C5CA00173FCB /* XCUIElementQuery.h in Headers */, + 71BB58F02B96511800CB9BFE /* FBVideoCommands.h in Headers */, 641EE6BA2240C5CA00173FCB /* XCPointerEvent.h in Headers */, 718F49C923087ACF0045FE8B /* FBProtocolHelpers.h in Headers */, 641EE6BB2240C5CA00173FCB /* XCSourceCodeRecording.h in Headers */, @@ -2442,13 +2520,13 @@ 641EE6D42240C5CA00173FCB /* NSValue-XCTestAdditions.h in Headers */, 641EE6D52240C5CA00173FCB /* _XCTWaiterImpl.h in Headers */, 641EE6D62240C5CA00173FCB /* FBLogger.h in Headers */, + 71BB58F72B96531900CB9BFE /* FBScreenRecordingContainer.h in Headers */, 641EE6D72240C5CA00173FCB /* XCTestObserver.h in Headers */, 641EE6D82240C5CA00173FCB /* XCUIElement.h in Headers */, 641EE6D92240C5CA00173FCB /* XCKeyboardInputSolver.h in Headers */, 718226CB2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */, 641EE6DB2240C5CA00173FCB /* FBPasteboard.h in Headers */, 711CD03525ED1106001C01D2 /* XCUIScreenDataSource-Protocol.h in Headers */, - 641EE6DC2240C5CA00173FCB /* FBAppiumActionsSynthesizer.h in Headers */, 641EE6DD2240C5CA00173FCB /* FBDebugLogDelegateDecorator.h in Headers */, 641EE6DE2240C5CA00173FCB /* XCUIDevice+FBHealthCheck.h in Headers */, 641EE6DF2240C5CA00173FCB /* FBMjpegServer.h in Headers */, @@ -2460,11 +2538,11 @@ 641EE6E42240C5CA00173FCB /* XCKeyboardLayout.h in Headers */, 641EE6E52240C5CA00173FCB /* XCTAsyncActivity-Protocol.h in Headers */, 641EE6E62240C5CA00173FCB /* XCActivityRecord.h in Headers */, + F59CD6D62EF16E5E00F91287 /* XCUIElement+FBCustomActions.h in Headers */, 71822765258744C700661B83 /* HTTPDataResponse.h in Headers */, 641EE6E72240C5CA00173FCB /* XCUIElement+FBFind.h in Headers */, 641EE6E82240C5CA00173FCB /* XCTestManager_ManagerInterface-Protocol.h in Headers */, 641EE6E92240C5CA00173FCB /* FBFailureProofTestCase.h in Headers */, - 13DE7A50287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, 641EE6EA2240C5CA00173FCB /* XCTTestRunSessionDelegate-Protocol.h in Headers */, 641EE6EB2240C5CA00173FCB /* XCTestCaseSuite.h in Headers */, 641EE6EC2240C5CA00173FCB /* _XCInternalTestRun.h in Headers */, @@ -2496,6 +2574,7 @@ 719CD8F82126C78F00C7D0C2 /* FBAlertsMonitor.h in Headers */, EE158AE41CBD456F00A3E3F0 /* FBSession.h in Headers */, 13DE7A55287CA1EC003243C6 /* FBXCElementSnapshotWrapper.h in Headers */, + F59CD6D42EF16E5E00F91287 /* XCUIElement+FBCustomActions.h in Headers */, EE35AD0F1E3B77D600A02D78 /* _XCTestImplementation.h in Headers */, 71241D7B1FAE3D2500B9559F /* FBTouchActionCommands.h in Headers */, EE158ACA1CBD456F00A3E3F0 /* FBTouchIDCommands.h in Headers */, @@ -2503,6 +2582,7 @@ EE158ABA1CBD456F00A3E3F0 /* FBCustomCommands.h in Headers */, EE35AD0D1E3B77D600A02D78 /* _XCTestCaseInterruptionException.h in Headers */, EE158AC41CBD456F00A3E3F0 /* FBOrientationCommands.h in Headers */, + 71BB58EF2B96511800CB9BFE /* FBVideoCommands.h in Headers */, 7119097C2152580600BA3C7E /* XCUIScreen.h in Headers */, EE35AD611E3B77D600A02D78 /* XCTRunnerIDESession.h in Headers */, EE158AE01CBD456F00A3E3F0 /* FBRouteRequest-Private.h in Headers */, @@ -2515,7 +2595,6 @@ EE35AD601E3B77D600A02D78 /* XCTRunnerDaemonSession.h in Headers */, 71414ED62670A1EE003A8C5D /* LRUCacheNode.h in Headers */, 64B2650A228CE4FF002A5025 /* FBTVNavigationTracker-Private.h in Headers */, - EE158AF51CBD456F00A3E3F0 /* FBApplication.h in Headers */, 71B155DF23080CA600646AFB /* FBProtocolHelpers.h in Headers */, EE35AD4B1E3B77D600A02D78 /* XCTestExpectationWaiter.h in Headers */, EE35AD1E1E3B77D600A02D78 /* UIGestureRecognizer-RecordingAdditions.h in Headers */, @@ -2535,6 +2614,7 @@ EE35AD721E3B77D600A02D78 /* XCUIElementHitPointCoordinate.h in Headers */, EE35AD3F1E3B77D600A02D78 /* XCTDarwinNotificationExpectation.h in Headers */, EE35AD5F1E3B77D600A02D78 /* XCTRunnerAutomationSession.h in Headers */, + 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, 71C9EAAC25E8415A00470CD8 /* FBScreenshot.h in Headers */, EE35AD371E3B77D600A02D78 /* XCSourceCodeTreeNodeEnumerator.h in Headers */, EE158AB01CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.h in Headers */, @@ -2566,6 +2646,7 @@ 714EAA0D2673FDFE005C5B47 /* FBCapabilities.h in Headers */, EE35AD5C1E3B77D600A02D78 /* XCTNSPredicateExpectation.h in Headers */, EE35AD521E3B77D600A02D78 /* XCTestObservationCenter.h in Headers */, + 71AE3CF92D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.h in Headers */, EE35AD5B1E3B77D600A02D78 /* XCTNSNotificationExpectation.h in Headers */, E444DC97249131D40060D7EB /* HTTPServer.h in Headers */, E444DCAE24913C220060D7EB /* HTTPResponseProxy.h in Headers */, @@ -2576,6 +2657,7 @@ EE35AD781E3B77D600A02D78 /* XCUIRecorderUtilities.h in Headers */, EE35AD421E3B77D600A02D78 /* XCTestCaseRun.h in Headers */, EE35AD441E3B77D600A02D78 /* XCTestConfiguration.h in Headers */, + 715A84D02DD92AD3007134CC /* FBElementHelpers.h in Headers */, EE35AD0B1E3B77D600A02D78 /* _XCTDarwinNotificationExpectationImplementation.h in Headers */, 718226CA2587443700661B83 /* GCDAsyncUdpSocket.h in Headers */, EE35AD491E3B77D600A02D78 /* XCTestExpectation.h in Headers */, @@ -2584,6 +2666,7 @@ EE158AD21CBD456F00A3E3F0 /* FBElementCache.h in Headers */, EE35AD5A1E3B77D600A02D78 /* XCTMetric.h in Headers */, EE35AD461E3B77D600A02D78 /* XCTestContextScope.h in Headers */, + 71BB58F62B96531900CB9BFE /* FBScreenRecordingContainer.h in Headers */, 71A7EAF51E20516B001DA4F2 /* XCUIElement+FBClassChain.h in Headers */, EE158ADA1CBD456F00A3E3F0 /* FBResponseJSONPayload.h in Headers */, EE35AD3D1E3B77D600A02D78 /* XCTAutomationTarget-Protocol.h in Headers */, @@ -2629,7 +2712,6 @@ EE158AB81CBD456F00A3E3F0 /* FBAlertViewCommands.h in Headers */, EE35AD651E3B77D600A02D78 /* XCTWaiter.h in Headers */, EE35AD681E3B77D600A02D78 /* XCTWaiterManagement-Protocol.h in Headers */, - 13DE7A4F287C46BB003243C6 /* FBXCElementSnapshot.h in Headers */, EE35AD451E3B77D600A02D78 /* XCTestContext.h in Headers */, EE35AD661E3B77D600A02D78 /* XCTWaiterDelegate-Protocol.h in Headers */, EE35AD0E1E3B77D600A02D78 /* _XCTestExpectationImplementation.h in Headers */, @@ -2646,11 +2728,13 @@ EE35AD331E3B77D600A02D78 /* XCPointerEvent.h in Headers */, EE35AD351E3B77D600A02D78 /* XCSourceCodeRecording.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 */, EE35AD261E3B77D600A02D78 /* XCApplicationMonitor_iOS.h in Headers */, + 0E04133B2DF1E15900AF007C /* XCUIElement+FBMinMax.h in Headers */, EE3A18661CDE734B00DE4205 /* FBKeyboard.h in Headers */, AD6C269C1CF2494200F8B5FF /* XCUIApplication+FBHelpers.h in Headers */, 714D88CC2733FB970074A925 /* FBXMLGenerationOptions.h in Headers */, @@ -2671,6 +2755,8 @@ EE35AD571E3B77D600A02D78 /* XCTestSuiteRun.h in Headers */, EE35AD701E3B77D600A02D78 /* XCUIElementAsynchronousHandlerWrapper.h in Headers */, EE35AD4C1E3B77D600A02D78 /* XCTestLog.h in Headers */, + B31635202DDF0D0B007D9317 /* FBAccessibilityTraits.h in Headers */, + 71BB58E82B96328700CB9BFE /* FBScreenRecordingRequest.h in Headers */, EE35AD231E3B77D600A02D78 /* UITapGestureRecognizer-RecordingAdditions.h in Headers */, EE35AD2A1E3B77D600A02D78 /* XCDebugLogDelegate-Protocol.h in Headers */, EE35AD1C1E3B77D600A02D78 /* NSString-XCTAdditions.h in Headers */, @@ -2685,7 +2771,6 @@ EE35AD6F1E3B77D600A02D78 /* XCUIElement.h in Headers */, EE35AD2F1E3B77D600A02D78 /* XCKeyboardInputSolver.h in Headers */, 71930C4220662E1F00D3AFEC /* FBPasteboard.h in Headers */, - 714097471FAE1B32008FB2C5 /* FBAppiumActionsSynthesizer.h in Headers */, EE7E271C1D06C69F001BEC7B /* FBDebugLogDelegateDecorator.h in Headers */, EEDFE1211D9C06F800E6FFE5 /* XCUIDevice+FBHealthCheck.h in Headers */, 7155D703211DCEF400166C20 /* FBMjpegServer.h in Headers */, @@ -2906,8 +2991,9 @@ 91F9DAE11B99DBC2001349B2 /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1020; - LastUpgradeCheck = 1310; + LastUpgradeCheck = 2600; ORGANIZATIONNAME = Facebook; TargetAttributes = { 641EE2D92240BBE300173FCB = { @@ -3033,6 +3119,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + A1B2C3D4E5F600000000001B /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3051,6 +3138,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F59CD6D72EF16E5E00F91287 /* XCUIElement+FBCustomActions.m in Sources */, 64E3502E2AC0B6EB005F3ACB /* NSDictionary+FBUtf8SafeDictionary.m in Sources */, 718226CF2587443700661B83 /* GCDAsyncSocket.m in Sources */, E444DCBC24917A5E0060D7EB /* HTTPResponseProxy.m in Sources */, @@ -3074,10 +3162,11 @@ 641EE5D92240C5CA00173FCB /* XCUIElement+FBPickerWheel.m in Sources */, 641EE5DA2240C5CA00173FCB /* XCUIApplicationProcessDelay.m in Sources */, 641EE5DB2240C5CA00173FCB /* FBXPath.m in Sources */, + 71B2E0042733FB970074B004 /* FBXPathExtensions.m in Sources */, 71C8E55425399A6B008572C1 /* XCUIApplication+FBQuiescence.m in Sources */, 641EE5DC2240C5CA00173FCB /* XCUIApplication+FBAlert.m in Sources */, - 641EE5DD2240C5CA00173FCB /* FBAppiumActionsSynthesizer.m in Sources */, 641EE70F2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */, + 71BB58EB2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */, 714D88CF2733FB970074A925 /* FBXMLGenerationOptions.m in Sources */, 641EE5DE2240C5CA00173FCB /* XCUIApplication+FBTouchAction.m in Sources */, 714E14BB29805CAE00375DD7 /* XCAXClient_iOS+FBSnapshotReqParams.m in Sources */, @@ -3132,20 +3221,26 @@ 641EE6082240C5CA00173FCB /* FBRuntimeUtils.m in Sources */, 641EE6092240C5CA00173FCB /* XCUIElement+FBUtilities.m in Sources */, 641EE60A2240C5CA00173FCB /* FBLogger.m in Sources */, + B316351D2DDF0CF5007D9317 /* FBAccessibilityTraits.m in Sources */, 641EE60B2240C5CA00173FCB /* FBCustomCommands.m in Sources */, + 71BB58E42B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */, 641EE60C2240C5CA00173FCB /* XCUIDevice+FBHelpers.m in Sources */, 641EE60D2240C5CA00173FCB /* XCTestPrivateSymbols.m in Sources */, 641EE60E2240C5CA00173FCB /* XCUIElement+FBTyping.m in Sources */, 641EE60F2240C5CA00173FCB /* XCUIElement+FBAccessibility.m in Sources */, 641EE6102240C5CA00173FCB /* FBImageUtils.m in Sources */, + 71AE3CF82D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */, + 715A84D12DD92AD3007134CC /* FBElementHelpers.m in Sources */, 641EE6112240C5CA00173FCB /* FBSession.m in Sources */, 641EE6122240C5CA00173FCB /* FBFindElementCommands.m in Sources */, 71A5C67629A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */, 641EE6132240C5CA00173FCB /* FBDebugLogDelegateDecorator.m in Sources */, 641EE6142240C5CA00173FCB /* FBAlertViewCommands.m in Sources */, 71414EDB2670A1EE003A8C5D /* LRUCacheNode.m in Sources */, + 71BB58F92B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */, 641EE6152240C5CA00173FCB /* XCUIElement+FBScrolling.m in Sources */, 641EE6162240C5CA00173FCB /* FBSessionCommands.m in Sources */, + 0E0413392DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */, 641EE6192240C5CA00173FCB /* FBConfiguration.m in Sources */, 641EE61A2240C5CA00173FCB /* FBElementCache.m in Sources */, 71F5BE26252E576C00EE9EBA /* XCUIElement+FBSwiping.m in Sources */, @@ -3155,10 +3250,10 @@ 716C9DFD27315D21005AD475 /* FBReflectionUtils.m in Sources */, 641EE61D2240C5CA00173FCB /* FBElementCommands.m in Sources */, 641EE61E2240C5CA00173FCB /* FBExceptionHandler.m in Sources */, + 71BB58F22B96511800CB9BFE /* FBVideoCommands.m in Sources */, 641EE61F2240C5CA00173FCB /* FBXCodeCompatibility.m in Sources */, 71E75E70254824230099FC87 /* XCUIElementQuery+FBHelpers.m in Sources */, 641EE6212240C5CA00173FCB /* FBElementTypeTransformer.m in Sources */, - 641EE6222240C5CA00173FCB /* FBApplication.m in Sources */, 13DE7A5E287CA444003243C6 /* FBXCElementSnapshotWrapper+Helpers.m in Sources */, 641EE6232240C5CA00173FCB /* FBScreen.m in Sources */, 71D04DCB25356C43008A052C /* XCUIElement+FBCaching.m in Sources */, @@ -3189,11 +3284,12 @@ 6385F4A7220A40760095BBDB /* XCUIApplicationProcessDelay.m in Sources */, 71A5C67529A4F39600421C37 /* XCTIssue+FBPatcher.m in Sources */, 711084451DA3AA7500F913D6 /* FBXPath.m in Sources */, + 71B2E0062733FB970074B006 /* FBXPathExtensions.m in Sources */, 719CD8FD2126C88B00C7D0C2 /* XCUIApplication+FBAlert.m in Sources */, 13DE7A45287C2A8D003243C6 /* FBXCAccessibilityElement.m in Sources */, - 71241D781FAE31F100B9559F /* FBAppiumActionsSynthesizer.m in Sources */, 641EE70E2240CE4800173FCB /* FBTVNavigationTracker.m in Sources */, 71BD20741F86116100B36EC2 /* XCUIApplication+FBTouchAction.m in Sources */, + 0E0413382DF1E15100AF007C /* XCUIElement+FBMinMax.m in Sources */, EE158AE71CBD456F00A3E3F0 /* FBWebServer.m in Sources */, 715557D4211DBCE700613B26 /* FBTCPSocket.m in Sources */, EE3A18631CDE618F00DE4205 /* FBErrorBuilder.m in Sources */, @@ -3204,13 +3300,16 @@ 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 */, EE6A893B1D0B38640083E92B /* FBFailureProofTestCase.m in Sources */, 713AE576243A53BE0000D657 /* FBW3CActionsHelpers.m in Sources */, 71B155E123080CA600646AFB /* FBProtocolHelpers.m in Sources */, EE158AB11CBD456F00A3E3F0 /* XCUIElement+FBIsVisible.m in Sources */, + 71AE3CFA2D38EE8E0039FC36 /* XCUIElement+FBVisibleFrame.m in Sources */, 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 */, @@ -3241,12 +3340,14 @@ 71F5BE51252F14EB00EE9EBA /* FBExceptions.m in Sources */, EE158ABD1CBD456F00A3E3F0 /* FBDebugCommands.m in Sources */, 716E0BCF1E917E810087A825 /* NSString+FBXMLSafeString.m in Sources */, + 71BB58EA2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */, EE158ACD1CBD456F00A3E3F0 /* FBUnknownCommands.m in Sources */, EE158AC51CBD456F00A3E3F0 /* FBOrientationCommands.m in Sources */, 716F0DA32A16CA1000CDD977 /* NSDictionary+FBUtf8SafeDictionary.m in Sources */, 71D475C42538F5A8008D9401 /* XCUIApplicationProcess+FBQuiescence.m in Sources */, 641EE7082240CDEB00173FCB /* XCUIElement+FBTVFocuse.m in Sources */, 71E75E6F254824230099FC87 /* XCUIElementQuery+FBHelpers.m in Sources */, + F59CD6D52EF16E5E00F91287 /* XCUIElement+FBCustomActions.m in Sources */, 71D04DCA25356C43008A052C /* XCUIElement+FBCaching.m in Sources */, EE158AEB1CBD456F00A3E3F0 /* FBRuntimeUtils.m in Sources */, EEE376461D59F81400ED88DD /* XCUIElement+FBUtilities.m in Sources */, @@ -3271,11 +3372,14 @@ 71C8E55325399A6B008572C1 /* XCUIApplication+FBQuiescence.m in Sources */, 71414EDA2670A1EE003A8C5D /* LRUCacheNode.m in Sources */, EE158AB91CBD456F00A3E3F0 /* FBAlertViewCommands.m in Sources */, + 71BB58F12B96511800CB9BFE /* FBVideoCommands.m in Sources */, 71F3E7D625417FF400E0C22B /* FBSettings.m in Sources */, 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 */, @@ -3292,7 +3396,6 @@ E444DC9D249131D40060D7EB /* HTTPMessage.m in Sources */, E444DCB024913C220060D7EB /* RouteResponse.m in Sources */, 71D3B3D7267FC7260076473D /* XCUIElement+FBResolve.m in Sources */, - EE158AF61CBD456F00A3E3F0 /* FBApplication.m in Sources */, 715AFAC21FFA29180053896D /* FBScreen.m in Sources */, 71B155DC230711E900646AFB /* FBCommandStatus.m in Sources */, EE35AD7C1E3B80C000A02D78 /* FBXCTestDaemonsProxy.m in Sources */, @@ -3306,16 +3409,19 @@ buildActionMask = 2147483647; files = ( 71241D801FAF087500B9559F /* FBW3CMultiTouchActionsIntegrationTests.m in Sources */, + 71BB58DE2B9631B700CB9BFE /* FBVideoRecordingTests.m in Sources */, 71241D7E1FAF084E00B9559F /* FBW3CTouchActionsIntegrationTests.m in Sources */, 63FD950221F9D06100A3E356 /* FBImageProcessorTests.m in Sources */, 719CD8FF2126C90200C7D0C2 /* FBAutoAlertsHandlerTests.m in Sources */, EE2202131ECC612200A29571 /* FBIntegrationTestCase.m in Sources */, - 71BD20781F869E0F00B36EC2 /* FBAppiumTouchActionsIntegrationTests.m in Sources */, - 719A97AC1F88E7370063B4BD /* FBAppiumMultiTouchActionsIntegrationTests.m in Sources */, 715AFAC41FFA2AAF0053896D /* FBScreenTests.m in Sources */, + 71BB58EC2B96328700CB9BFE /* FBScreenRecordingRequest.m in Sources */, EE22021E1ECC618900A29571 /* FBTapTest.m in Sources */, 71930C472066434000D3AFEC /* FBPasteboardTests.m in Sources */, + 71BB58E52B9631F100CB9BFE /* FBScreenRecordingPromise.m in Sources */, + 71BB58FA2B96531900CB9BFE /* FBScreenRecordingContainer.m in Sources */, 7150FFF722476B3A00B2EE28 /* FBForceTouchTests.m in Sources */, + 71BB58F32B96511800CB9BFE /* FBVideoCommands.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3360,7 +3466,7 @@ EE9B76591CF7987800275851 /* FBRouteTests.m in Sources */, 7139145C1DF01A12005896C2 /* NSExpressionFBFormatTests.m in Sources */, 71A224E81DE326C500844D55 /* NSPredicateFBFormatTests.m in Sources */, - EE6A892B1D0B25820083E92B /* FBApplicationDouble.m in Sources */, + EE6A892B1D0B25820083E92B /* XCUIApplicationDouble.m in Sources */, 716F0DA62A17323300CDD977 /* NSDictionaryFBUtf8SafeTests.m in Sources */, EE6A892D1D0B2AF40083E92B /* FBErrorBuilderTests.m in Sources */, 712A0C851DA3E459007D02E5 /* FBXPathTests.m in Sources */, @@ -3377,6 +3483,7 @@ buildActionMask = 2147483647; files = ( EE9B768E1CF7997600275851 /* AppDelegate.m in Sources */, + AABBCCDDEEFF001122334457 /* SceneDelegate.m in Sources */, EE1E06E71D182E95007CF043 /* FBAlertViewController.m in Sources */, 315A15072518CC2800A3A064 /* TouchSpotView.m in Sources */, EE9B76911CF7997600275851 /* main.m in Sources */, @@ -3608,6 +3715,7 @@ baseConfigurationReference = 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */; buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; + CODE_SIGN_IDENTITY = ""; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = NO; @@ -3675,6 +3783,7 @@ baseConfigurationReference = 717C0D702518ED2800CAA6EC /* TVOSSettings.xcconfig */; buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; + CODE_SIGN_IDENTITY = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = NO; DYLIB_COMPATIBILITY_VERSION = 1; @@ -3834,6 +3943,7 @@ ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -3854,12 +3964,13 @@ "$(SDKROOT)/usr/include/libxml2", "$(SRCROOT)/Modules", ); - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 12.0; + TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_WORKSPACE = NO; }; name = Debug; @@ -3901,6 +4012,7 @@ ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = "DEBUG=0"; @@ -3915,11 +4027,12 @@ "$(SDKROOT)/usr/include/libxml2", "$(SRCROOT)/Modules", ); - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + STRING_CATALOG_GENERATE_SYMBOLS = YES; TARGETED_DEVICE_FAMILY = "1,2"; - TVOS_DEPLOYMENT_TARGET = 12.0; + TVOS_DEPLOYMENT_TARGET = 13.0; VALIDATE_PRODUCT = YES; VALIDATE_WORKSPACE = NO; }; @@ -3930,6 +4043,7 @@ baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; + CODE_SIGN_IDENTITY = ""; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; DEFINES_MODULE = NO; @@ -3995,6 +4109,7 @@ baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; buildSettings = { CLANG_STATIC_ANALYZER_MODE = deep; + CODE_SIGN_IDENTITY = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = NO; DYLIB_COMPATIBILITY_VERSION = 1; @@ -4173,6 +4288,7 @@ CLANG_ANALYZER_NONNULL = YES; DEBUG_INFORMATION_FORMAT = dwarf; INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4189,6 +4305,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ANALYZER_NONNULL = YES; INFOPLIST_FILE = WebDriverAgentTests/IntegrationApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4241,6 +4358,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_STATIC_ANALYZER_MODE = deep; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_TESTING_SEARCH_PATHS = YES; @@ -4294,6 +4412,7 @@ isa = XCBuildConfiguration; baseConfigurationReference = EEE5CABF1C80361500CBBDD9 /* IOSSettings.xcconfig */; buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_STATIC_ANALYZER_MODE = deep; ENABLE_TESTING_SEARCH_PATHS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; diff --git a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp.xcscheme b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp.xcscheme index 5b24c4ece..d43e4ec1b 100644 --- a/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp.xcscheme +++ b/WebDriverAgent.xcodeproj/xcshareddata/xcschemes/IntegrationApp.xcscheme @@ -1,6 +1,6 @@ + + + + + + + + + + + + + + snapshot, NSArray *types); @@ -65,10 +68,17 @@ - (NSString *)fb_description } - (id)fb_attributeValue:(NSString *)attribute + error:(NSError **)error { + NSDate *start = [NSDate date]; NSDictionary *result = [FBXCAXClientProxy.sharedClient attributesForElement:[self accessibilityElement] - attributes:@[attribute]]; - return result[attribute]; + attributes:@[attribute] + error:error]; + NSTimeInterval elapsed = ABS([start timeIntervalSinceNow]); + if (elapsed > ATTRIBUTE_FETCH_WARN_TIME_LIMIT) { + NSLog(@"! Fetching of %@ value for %@ took %@s", attribute, self.fb_description, @(elapsed)); + } + return [result objectForKey:attribute]; } inline static BOOL areValuesEqual(id value1, id value2); @@ -140,29 +150,6 @@ - (BOOL)fb_framelessFuzzyMatchesElement:(id)snapshot return targetCellSnapshot; } -- (CGRect)fb_visibleFrameWithFallback -{ - CGRect thisVisibleFrame = [self visibleFrame]; - if (!CGRectIsEmpty(thisVisibleFrame)) { - return thisVisibleFrame; - } - - NSDictionary *visibleFrameDict = (NSDictionary*)[self fb_attributeValue:@"XC_kAXXCAttributeVisibleFrame"]; - if (visibleFrameDict == nil) { - return thisVisibleFrame; - } - - id x = [visibleFrameDict objectForKey:@"X"]; - id y = [visibleFrameDict objectForKey:@"Y"]; - id height = [visibleFrameDict objectForKey:@"Height"]; - id width = [visibleFrameDict objectForKey:@"Width"]; - if (x != nil && y != nil && height != nil && width != nil) { - return CGRectMake([x doubleValue], [y doubleValue], [width doubleValue], [height doubleValue]); - } - - return thisVisibleFrame; -} - - (NSValue *)fb_hitPoint { NSError *error; diff --git a/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h index 0f657ab6f..cb2b539a1 100644 --- a/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h +++ b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m index 399769142..0a5905daf 100644 --- a/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m +++ b/WebDriverAgentLib/Categories/NSDictionary+FBUtf8SafeDictionary.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSDictionary+FBUtf8SafeDictionary.h" diff --git a/WebDriverAgentLib/Categories/NSExpression+FBFormat.h b/WebDriverAgentLib/Categories/NSExpression+FBFormat.h index feafbac45..274d42011 100644 --- a/WebDriverAgentLib/Categories/NSExpression+FBFormat.h +++ b/WebDriverAgentLib/Categories/NSExpression+FBFormat.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/NSExpression+FBFormat.m b/WebDriverAgentLib/Categories/NSExpression+FBFormat.m index 81858d65a..d4c32aeba 100644 --- a/WebDriverAgentLib/Categories/NSExpression+FBFormat.m +++ b/WebDriverAgentLib/Categories/NSExpression+FBFormat.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSExpression+FBFormat.h" diff --git a/WebDriverAgentLib/Categories/NSString+FBVisualLength.h b/WebDriverAgentLib/Categories/NSString+FBVisualLength.h index a24eb690a..ec065e28f 100644 --- a/WebDriverAgentLib/Categories/NSString+FBVisualLength.h +++ b/WebDriverAgentLib/Categories/NSString+FBVisualLength.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/NSString+FBVisualLength.m b/WebDriverAgentLib/Categories/NSString+FBVisualLength.m index 9487bbebe..652f1a06a 100644 --- a/WebDriverAgentLib/Categories/NSString+FBVisualLength.m +++ b/WebDriverAgentLib/Categories/NSString+FBVisualLength.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSString+FBVisualLength.h" diff --git a/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h b/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h index e51407b79..7cdceb796 100644 --- a/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h +++ b/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m b/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m index cdc7ab3b5..dbb7573f0 100644 --- a/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m +++ b/WebDriverAgentLib/Categories/NSString+FBXMLSafeString.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSString+FBXMLSafeString.h" diff --git a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h index 8080f2097..d15aeba68 100644 --- a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h +++ b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -13,6 +12,7 @@ NS_ASSUME_NONNULL_BEGIN +extern NSString *const FBSnapshotMaxChildrenKey; extern NSString *const FBSnapshotMaxDepthKey; void FBSetCustomParameterForElementSnapshot (NSString* name, id value); diff --git a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m index 356a1ea05..8698eb734 100644 --- a/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m +++ b/WebDriverAgentLib/Categories/XCAXClient_iOS+FBSnapshotReqParams.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCAXClient_iOS+FBSnapshotReqParams.h" @@ -20,6 +19,7 @@ @"maxDepth" : (int)2147483647 */ NSString *const FBSnapshotMaxDepthKey = @"maxDepth"; +NSString *const FBSnapshotMaxChildrenKey = @"maxChildren"; static id (*original_defaultParameters)(id, SEL); static id (*original_snapshotParameters)(id, SEL); @@ -65,6 +65,7 @@ @implementation XCAXClient_iOS (FBSnapshotReqParams) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-load-method" +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + (void)load { diff --git a/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h index b4cdf1d44..fc81daa04 100644 --- a/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h +++ b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m index dc27e86a6..70472e4a2 100644 --- a/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m +++ b/WebDriverAgentLib/Categories/XCTIssue+FBPatcher.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCTIssue+FBPatcher.h" @@ -20,6 +19,8 @@ @implementation XCTIssue (AMPatcher) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-load-method" +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + + (void)load { SEL originalShouldInterruptTest = NSSelectorFromString(@"shouldInterruptTest"); @@ -28,6 +29,7 @@ + (void)load if (nil == originalShouldInterruptTestMethod) return; method_setImplementation(originalShouldInterruptTestMethod, (IMP)swizzledShouldInterruptTest); } + #pragma clang diagnostic pop @end diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h index 7c057699b..01bea467d 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m index 38b6586ef..36628bc61 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBAlert.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplication+FBAlert.h" @@ -52,7 +51,7 @@ - (nullable XCUIElement *)fb_alertElementFromSafariWithScrollView:(XCUIElement * // and conatins at least one text view __block NSUInteger buttonsCount = 0; __block NSUInteger textViewsCount = 0; - id snapshot = candidate.fb_cachedSnapshot ?: candidate.fb_takeSnapshot; + id snapshot = candidate.fb_cachedSnapshot ?: [candidate fb_customSnapshot]; [snapshot enumerateDescendantsUsingBlock:^(id descendant) { XCUIElementType curType = descendant.elementType; if (curType == XCUIElementTypeButton) { @@ -73,7 +72,7 @@ - (XCUIElement *)fb_alertElement if (nil == alert) { return nil; } - id alertSnapshot = alert.fb_cachedSnapshot ?: alert.fb_takeSnapshot; + id alertSnapshot = alert.fb_cachedSnapshot ?: [alert fb_customSnapshot]; if (alertSnapshot.elementType == XCUIElementTypeAlert) { return alert; diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h b/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h index 6951e760c..4df5025c0 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBFocused.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h index 13fabcf88..f5d358ff9 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -31,6 +30,12 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSDictionary *)fb_tree; +/** + @param excludedAttributes Set of possible attributes to be excluded i.e frame, enabled, visible, accessible, focused. If set to nil or an empty array then no attributes will be excluded from the resulting JSON + @return application elements tree in form of nested dictionaries + */ +- (NSDictionary *)fb_tree:(nullable NSSet *) excludedAttributes; + /** Return application elements accessibility tree in form of nested dictionaries */ @@ -122,6 +127,44 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable NSArray *> *)fb_performAccessibilityAuditWithAuditTypes:(uint64_t)auditTypes error:(NSError **)error; +/** + Constructor used to get current active application + */ ++ (instancetype)fb_activeApplication; + +/** + Constructor used to get current active application + + @param bundleId The bundle identifier of an app, which should be selected as active by default + if it is present in the list of active applications + */ ++ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId; + +/** + Constructor used to get the system application (e.g. Springboard on iOS) + */ ++ (instancetype)fb_systemApplication; + +/** + Retrieves the list of all currently active applications + */ ++ (NSArray *)fb_activeApplications; + +/** + Switch to system app (called Springboard on iOS) + + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ ++ (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error; + +/** + Determines whether the other app is the same as the current one + + @param otherApp Other app instance + @return YES if the other app has the same identifier + */ +- (BOOL)fb_isSameAppAs:(nullable XCUIApplication *)otherApp; @end diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m index 2656de7d6..bda8a8c5f 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBHelpers.m @@ -3,19 +3,19 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplication+FBHelpers.h" +#import "FBActiveAppDetectionPoint.h" #import "FBElementTypeTransformer.h" #import "FBKeyboard.h" #import "FBLogger.h" #import "FBExceptions.h" #import "FBMacros.h" #import "FBMathUtils.h" -#import "FBActiveAppDetectionPoint.h" +#import "FBRunLoopSpinner.h" #import "FBXCodeCompatibility.h" #import "FBXPath.h" #import "FBXCAccessibilityElement.h" @@ -23,17 +23,34 @@ #import "FBXCElementSnapshotWrapper+Helpers.h" #import "FBXCAXClientProxy.h" #import "FBXMLGenerationOptions.h" +#import "XCTestManager_ManagerInterface-Protocol.h" +#import "XCTestPrivateSymbols.h" +#import "XCTRunnerDaemonSession.h" +#import "XCUIApplication.h" +#import "XCUIApplicationImpl.h" +#import "XCUIApplicationProcess.h" #import "XCUIDevice+FBHelpers.h" +#import "XCUIElement.h" #import "XCUIElement+FBCaching.h" #import "XCUIElement+FBIsVisible.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" -#import "XCTestManager_ManagerInterface-Protocol.h" -#import "XCTestPrivateSymbols.h" -#import "XCTRunnerDaemonSession.h" +#import "XCUIElementQuery.h" +#import "FBElementHelpers.h" static NSString* const FBUnknownBundleId = @"unknown"; +static NSString* const FBExclusionAttributeFrame = @"frame"; +static NSString* const FBExclusionAttributeEnabled = @"enabled"; +static NSString* const FBExclusionAttributeVisible = @"visible"; +static NSString* const FBExclusionAttributeAccessible = @"accessible"; +static NSString* const FBExclusionAttributeFocused = @"focused"; +static NSString* const FBExclusionAttributePlaceholderValue = @"placeholderValue"; +static NSString* const FBExclusionAttributeNativeFrame = @"nativeFrame"; +static NSString* const FBExclusionAttributeTraits = @"traits"; +static NSString* const FBExclusionAttributeMinValue = @"minValue"; +static NSString* const FBExclusionAttributeMaxValue = @"maxValue"; + _Nullable id extractIssueProperty(id issue, NSString *propertyName) { SEL selector = NSSelectorFromString(propertyName); NSMethodSignature *methodSignature = [issue methodSignatureForSelector:selector]; @@ -82,6 +99,17 @@ _Nullable id extractIssueProperty(id issue, NSString *propertyName) { return result; } +NSDictionary *customExclusionAttributesMap(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + result = @{ + FBExclusionAttributeVisible: FB_XCAXAIsVisibleAttributeName, + FBExclusionAttributeAccessible: FB_XCAXAIsElementAttributeName, + }; + }); + return result; +} @implementation XCUIApplication (FBHelpers) @@ -141,27 +169,32 @@ - (BOOL)fb_deactivateWithDuration:(NSTimeInterval)duration error:(NSError **)err return NO; } [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:MAX(duration, .0)]]; - [self fb_activate]; + [self activate]; return YES; } - (NSDictionary *)fb_tree { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : [self fb_snapshotWithAllAttributesAndMaxDepth:nil]; - return [self.class dictionaryForElement:snapshot recursive:YES]; + return [self fb_tree:nil]; +} + +- (NSDictionary *)fb_tree:(nullable NSSet *)excludedAttributes +{ + id snapshot = [self fb_standardSnapshot]; + return [self.class dictionaryForElement:snapshot + recursive:YES + excludedAttributes:excludedAttributes]; } - (NSDictionary *)fb_accessibilityTree { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : [self fb_snapshotWithAllAttributesAndMaxDepth:nil]; + id snapshot = [self fb_standardSnapshot]; return [self.class accessibilityInfoForElement:snapshot]; } -+ (NSDictionary *)dictionaryForElement:(id)snapshot recursive:(BOOL)recursive ++ (NSDictionary *)dictionaryForElement:(id)snapshot + recursive:(BOOL)recursive + excludedAttributes:(nullable NSSet *)excludedAttributes { NSMutableDictionary *info = [[NSMutableDictionary alloc] init]; info[@"type"] = [FBElementTypeTransformer shortStringWithElementType:snapshot.elementType]; @@ -171,11 +204,29 @@ + (NSDictionary *)dictionaryForElement:(id)snapshot recursi info[@"value"] = FBValueOrNull(wrappedSnapshot.wdValue); info[@"label"] = FBValueOrNull(wrappedSnapshot.wdLabel); info[@"rect"] = wrappedSnapshot.wdRect; - info[@"frame"] = NSStringFromCGRect(wrappedSnapshot.wdFrame); - info[@"isEnabled"] = [@([wrappedSnapshot isWDEnabled]) stringValue]; - info[@"isVisible"] = [@([wrappedSnapshot isWDVisible]) stringValue]; - info[@"isAccessible"] = [@([wrappedSnapshot isWDAccessible]) stringValue]; - info[@"isFocused"] = [@([wrappedSnapshot isWDFocused]) stringValue]; + info[@"customActions"] = FBValueOrNull(wrappedSnapshot.wdCustomActions); + + NSDictionary *attributeBlocks = [self fb_attributeBlockMapForWrappedSnapshot:wrappedSnapshot]; + + NSSet *nonPrefixedKeys = [NSSet setWithObjects: + FBExclusionAttributeFrame, + FBExclusionAttributePlaceholderValue, + FBExclusionAttributeNativeFrame, + FBExclusionAttributeTraits, + FBExclusionAttributeMinValue, + FBExclusionAttributeMaxValue, + nil]; + + for (NSString *key in attributeBlocks) { + if (excludedAttributes == nil || ![excludedAttributes containsObject:key]) { + NSString *value = ((NSString * (^)(void))attributeBlocks[key])(); + if ([nonPrefixedKeys containsObject:key]) { + info[key] = value; + } else { + info[[NSString stringWithFormat:@"is%@", [key capitalizedString]]] = value; + } + } + } if (!recursive) { return info.copy; @@ -185,12 +236,69 @@ + (NSDictionary *)dictionaryForElement:(id)snapshot recursi if ([childElements count]) { info[@"children"] = [[NSMutableArray alloc] init]; for (id childSnapshot in childElements) { - [info[@"children"] addObject:[self dictionaryForElement:childSnapshot recursive:YES]]; + @autoreleasepool { + [info[@"children"] addObject:[self dictionaryForElement:childSnapshot + recursive:YES + excludedAttributes:excludedAttributes]]; + } } } return info; } +// Helper used by `dictionaryForElement:` to assemble attribute value blocks, +// including both common attributes and conditionally included ones like placeholderValue. ++ (NSDictionary *)fb_attributeBlockMapForWrappedSnapshot:(FBXCElementSnapshotWrapper *)wrappedSnapshot + +{ + // Base attributes common to every element + NSMutableDictionary *blocks = + [@{ + FBExclusionAttributeFrame: ^{ + return NSStringFromCGRect(wrappedSnapshot.wdFrame); + }, + FBExclusionAttributeNativeFrame: ^{ + return NSStringFromCGRect(wrappedSnapshot.wdNativeFrame); + }, + FBExclusionAttributeEnabled: ^{ + return [@([wrappedSnapshot isWDEnabled]) stringValue]; + }, + FBExclusionAttributeVisible: ^{ + return [@([wrappedSnapshot isWDVisible]) stringValue]; + }, + FBExclusionAttributeAccessible: ^{ + return [@([wrappedSnapshot isWDAccessible]) stringValue]; + }, + FBExclusionAttributeFocused: ^{ + return [@([wrappedSnapshot isWDFocused]) stringValue]; + }, + FBExclusionAttributeTraits: ^{ + return wrappedSnapshot.wdTraits; + } + } mutableCopy]; + + XCUIElementType elementType = wrappedSnapshot.elementType; + + // Text-input placeholder (only for elements that support inner text) + if (FBDoesElementSupportInnerText(elementType)) { + blocks[FBExclusionAttributePlaceholderValue] = ^{ + return (NSString *)FBValueOrNull(wrappedSnapshot.wdPlaceholderValue); + }; + } + + // Only for elements that support min/max value + if (FBDoesElementSupportMinMaxValue(elementType)) { + blocks[FBExclusionAttributeMinValue] = ^{ + return wrappedSnapshot.wdMinValue; + }; + blocks[FBExclusionAttributeMaxValue] = ^{ + return wrappedSnapshot.wdMaxValue; + }; + } + + return [blocks copy]; +} + + (NSDictionary *)accessibilityInfoForElement:(id)snapshot { FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; @@ -207,9 +315,11 @@ + (NSDictionary *)accessibilityInfoForElement:(id)snapshot } else { NSMutableArray *children = [[NSMutableArray alloc] init]; for (id childSnapshot in snapshot.children) { - NSDictionary *childInfo = [self accessibilityInfoForElement:childSnapshot]; - if ([childInfo count]) { - [children addObject: childInfo]; + @autoreleasepool { + NSDictionary *childInfo = [self accessibilityInfoForElement:childSnapshot]; + if ([childInfo count]) { + [children addObject: childInfo]; + } } } if ([children count]) { @@ -357,33 +467,41 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames return nil; } + // These custom attributes could take too long to fetch, thus excluded + NSSet *customAttributesToExclude = [NSSet setWithArray:[customExclusionAttributesMap() allKeys]]; NSMutableArray *resultArray = [NSMutableArray array]; NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; [invocation setSelector:selector]; [invocation setArgument:&auditTypes atIndex:2]; BOOL (^issueHandler)(id) = ^BOOL(id issue) { - NSString *auditType = @""; - NSDictionary *valuesToNamesMap = auditTypeValuesToNames(); - NSNumber *auditTypeValue = [issue valueForKey:@"auditType"]; - if (nil != auditTypeValue) { - auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue]; + @autoreleasepool { + NSString *auditType = @""; + NSDictionary *valuesToNamesMap = auditTypeValuesToNames(); + NSNumber *auditTypeValue = [issue valueForKey:@"auditType"]; + if (nil != auditTypeValue) { + auditType = valuesToNamesMap[auditTypeValue] ?: [auditTypeValue stringValue]; + } + + id extractedElement = extractIssueProperty(issue, @"element"); + + id elementSnapshot = [extractedElement fb_cachedSnapshot] ?: [extractedElement fb_standardSnapshot]; + NSDictionary *elementAttributes = elementSnapshot + ? [self.class dictionaryForElement:elementSnapshot + recursive:NO + excludedAttributes:customAttributesToExclude] + : @{}; + + [resultArray addObject:@{ + @"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"", + @"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"", + @"auditType": auditType, + @"element": [extractedElement description] ?: @"", + @"elementDescription": [extractedElement debugDescription] ?: @"", + @"elementAttributes": elementAttributes ?: @{}, + }]; + return YES; } - - id extractedElement = extractIssueProperty(issue, @"element"); - - id elementSnapshot = [extractedElement fb_takeSnapshot]; - NSDictionary *elementAttributes = elementSnapshot ? [self.class dictionaryForElement:elementSnapshot recursive:NO] : @{}; - - [resultArray addObject:@{ - @"detailedDescription": extractIssueProperty(issue, @"detailedDescription") ?: @"", - @"compactDescription": extractIssueProperty(issue, @"compactDescription") ?: @"", - @"auditType": auditType, - @"element": [extractedElement description] ?: @"", - @"elementDescription": [extractedElement debugDescription] ?: @"", - @"elementAttributes": elementAttributes ?: @{}, - }]; - return YES; }; [invocation setArgument:&issueHandler atIndex:3]; [invocation setArgument:&error atIndex:4]; @@ -393,4 +511,135 @@ - (BOOL)fb_dismissKeyboardWithKeyNames:(nullable NSArray *)keyNames return isSuccessful ? resultArray.copy : nil; } ++ (instancetype)fb_activeApplication +{ + return [self fb_activeApplicationWithDefaultBundleId:nil]; +} + ++ (NSArray *)fb_activeApplications +{ + NSArray> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications]; + NSMutableArray *result = [NSMutableArray array]; + if (activeApplicationElements.count > 0) { + for (id applicationElement in activeApplicationElements) { + XCUIApplication *app = [XCUIApplication fb_applicationWithPID:applicationElement.processIdentifier]; + if (nil != app) { + [result addObject:app]; + } + } + } + return result.count > 0 ? result.copy : @[self.class.fb_systemApplication]; +} + ++ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId +{ + NSArray> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications]; + id activeApplicationElement = nil; + id currentElement = nil; + if (nil != bundleId) { + currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement; + if (nil != currentElement) { + NSArray *appInfos = [self fb_appsInfoWithAxElements:@[currentElement]]; + [FBLogger logFmt:@"Detected on-screen application: %@", appInfos.firstObject[@"bundleId"]]; + if ([[appInfos.firstObject objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) { + activeApplicationElement = currentElement; + } + } + } + if (nil == activeApplicationElement && activeApplicationElements.count > 1) { + if (nil != bundleId) { + NSArray *appInfos = [self fb_appsInfoWithAxElements:activeApplicationElements]; + NSMutableArray *bundleIds = [NSMutableArray array]; + for (NSDictionary *appInfo in appInfos) { + [bundleIds addObject:(NSString *)appInfo[@"bundleId"]]; + } + [FBLogger logFmt:@"Detected system active application(s): %@", bundleIds]; + // Try to select the desired application first + for (NSUInteger appIdx = 0; appIdx < appInfos.count; appIdx++) { + if ([[[appInfos objectAtIndex:appIdx] objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) { + activeApplicationElement = [activeApplicationElements objectAtIndex:appIdx]; + break; + } + } + } + // Fall back to the "normal" algorithm if the desired application is either + // not set or is not active + if (nil == activeApplicationElement) { + if (nil == currentElement) { + currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement; + } + if (nil == currentElement) { + [FBLogger log:@"Cannot precisely detect the current application. Will use the system's recently active one"]; + if (nil == bundleId) { + [FBLogger log:@"Consider changing the 'defaultActiveApplication' setting to the bundle identifier of the desired application under test"]; + } + } else { + for (id appElement in activeApplicationElements) { + if (appElement.processIdentifier == currentElement.processIdentifier) { + activeApplicationElement = appElement; + break; + } + } + } + } + } + + if (nil != activeApplicationElement) { + XCUIApplication *application = [XCUIApplication fb_applicationWithPID:activeApplicationElement.processIdentifier]; + if (nil != application) { + return application; + } + [FBLogger log:@"Cannot translate the active process identifier into an application object"]; + } + + if (activeApplicationElements.count > 0) { + [FBLogger logFmt:@"Getting the most recent active application (out of %@ total items)", @(activeApplicationElements.count)]; + for (id appElement in activeApplicationElements) { + XCUIApplication *application = [XCUIApplication fb_applicationWithPID:appElement.processIdentifier]; + if (nil != application) { + return application; + } + } + } + + [FBLogger log:@"Cannot retrieve any active applications. Assuming the system application is the active one"]; + return [self fb_systemApplication]; +} + ++ (instancetype)fb_systemApplication +{ + return [self fb_applicationWithPID: + [[FBXCAXClientProxy.sharedClient systemApplication] processIdentifier]]; +} + ++ (instancetype)fb_applicationWithPID:(pid_t)processID +{ + return [FBXCAXClientProxy.sharedClient monitoredApplicationWithProcessIdentifier:processID]; +} + ++ (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error +{ + XCUIApplication *systemApp = self.fb_systemApplication; + @try { + if (systemApp.running) { + [systemApp activate]; + } else { + [systemApp launch]; + } + } @catch (NSException *e) { + return [[[FBErrorBuilder alloc] + withDescription:nil == e ? @"Cannot open the home screen" : e.reason] + buildError:error]; + } + return YES; +} + +- (BOOL)fb_isSameAppAs:(nullable XCUIApplication *)otherApp +{ + if (nil == otherApp) { + return NO; + } + return self == otherApp || [self.bundleID isEqualToString:(NSString *)otherApp.bundleID]; +} + @end diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h index ef036053e..962a48146 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m index 66f1f118b..72febaa51 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBQuiescence.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplication+FBQuiescence.h" diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h b/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h index 5ab5e357c..79b220c50 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ @@ -15,43 +14,6 @@ NS_ASSUME_NONNULL_BEGIN @interface XCUIApplication (FBTouchAction) -/** - Perform complex touch action in scope of the current application. - Touch actions are represented as lists of dictionaries with predefined sets of values and keys. - Each dictionary must contain 'action' key, which is one of the following: - - 'tap' to perform a single tap - - 'longPress' to perform long tap - - 'press' to perform press - - 'release' to release the finger - - 'moveTo' to move the virtual finger - - 'wait' to modify the duration of the preceeding action - - 'cancel' to cancel the preceeding action in the chain - Each dictionary can also contain 'options' key with additional parameters dictionary related to the appropriate action. - - The following options are mandatory for 'tap', 'longPress', 'press' and 'moveTo' actions: - - 'x' the X coordinate of the action - - 'y' the Y coordinate of the action - - 'element' the corresponding element instance, for which the action is going to be performed - If only 'element' is set then hit point coordinates of this element will be used. - If only 'x' and 'y' are set then these will be considered as absolute coordinates. - If both 'element' and 'x'/'y' are set then these will act as relative element coordinates. - - It is also mandatory, that 'release' and 'wait' actions are preceeded with at least one chain item, which contains absolute coordinates, like 'tap', 'press' or 'longPress'. Empty chains are not allowed. - - The following additional options are available for different actions: - - 'tap': 'count' (defines count of taps to be performed in a row; 1 by default) - - 'longPress': 'duration' (number of milliseconds to hold/move the virtual finger; 500.0 ms by default) - - 'wait': 'ms' (number of milliseconds to wait for the preceeding action; 0.0 ms by default) - - List of lists can be passed there is order to perform multi-finger touch action. Each single actions chain is going to be executed by a separate virtual finger in such case. - - @param actions Either array of dictionaries, whose format is described above to peform single-finger touch action or array of array to perform multi-finger touch action. - @param elementCache Cached elements mapping for the currrent application. The method assumes all elements are already represented by their actual instances if nil value is set - @param error If there is an error, upon return contains an NSError object that describes the problem - @return YES If the touch action has been successfully performed without errors - */ -- (BOOL)fb_performAppiumTouchActions:(NSArray *)actions elementCache:(nullable FBElementCache *)elementCache error:(NSError * _Nullable*)error; - /** Perform complex touch action in scope of the current application. diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m b/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m index 703a0096d..bbd1e5746 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBTouchAction.m @@ -3,14 +3,12 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplication+FBTouchAction.h" -#import "FBAppiumActionsSynthesizer.h" #import "FBBaseActionsSynthesizer.h" #import "FBConfiguration.h" #import "FBExceptions.h" @@ -54,20 +52,6 @@ - (BOOL)fb_performActionsWithSynthesizerType:(Class)synthesizerType return [self fb_synthesizeEvent:eventRecord error:error]; } -- (BOOL)fb_performAppiumTouchActions:(NSArray *)actions - elementCache:(FBElementCache *)elementCache - error:(NSError **)error -{ - if (![self fb_performActionsWithSynthesizerType:FBAppiumActionsSynthesizer.class - actions:actions - elementCache:elementCache - error:error]) { - return NO; - } - [self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; - return YES; -} - - (BOOL)fb_performW3CActions:(NSArray *)actions elementCache:(FBElementCache *)elementCache error:(NSError **)error diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h b/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h index 2078d8c20..e95273216 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m b/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m index 5d48dffa6..b662a1e5a 100644 --- a/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m +++ b/WebDriverAgentLib/Categories/XCUIApplication+FBUIInterruptions.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplication+FBUIInterruptions.h" diff --git a/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h b/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h index 0c78f2403..f911cbfdc 100644 --- a/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h +++ b/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -18,6 +17,11 @@ NS_ASSUME_NONNULL_BEGIN /*! Defines wtether the process should perform quiescence checks. YES by default */ @property (nonatomic) NSNumber* fb_shouldWaitForQuiescence; +/** + @param waitForAnimations Set it to YES if XCTest should also wait for application animations to complete + */ +- (void)fb_waitForQuiescenceIncludingAnimationsIdle:(bool)waitForAnimations; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m b/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m index cc60e429f..1f6272dbc 100644 --- a/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m +++ b/WebDriverAgentLib/Categories/XCUIApplicationProcess+FBQuiescence.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplicationProcess+FBQuiescence.h" @@ -12,10 +11,12 @@ #import #import "FBConfiguration.h" +#import "FBExceptions.h" #import "FBLogger.h" #import "FBSettings.h" static void (*original_waitForQuiescenceIncludingAnimationsIdle)(id, SEL, BOOL); +static void (*original_waitForQuiescenceIncludingAnimationsIdlePreEvent)(id, SEL, BOOL, BOOL); static void swizzledWaitForQuiescenceIncludingAnimationsIdle(id self, SEL _cmd, BOOL includingAnimations) { @@ -38,17 +39,43 @@ static void swizzledWaitForQuiescenceIncludingAnimationsIdle(id self, SEL _cmd, } } +static void swizzledWaitForQuiescenceIncludingAnimationsIdlePreEvent(id self, SEL _cmd, BOOL includingAnimations, BOOL isPreEvent) +{ + NSString *bundleId = [self bundleID]; + if (![[self fb_shouldWaitForQuiescence] boolValue] || FBConfiguration.waitForIdleTimeout < DBL_EPSILON) { + [FBLogger logFmt:@"Quiescence checks are disabled for %@ application. Making it to believe it is idling", + bundleId]; + return; + } + + NSTimeInterval desiredTimeout = FBConfiguration.waitForIdleTimeout; + NSTimeInterval previousTimeout = _XCTApplicationStateTimeout(); + _XCTSetApplicationStateTimeout(desiredTimeout); + [FBLogger logFmt:@"Waiting up to %@s until %@ is in idle state (%@ animations)", + @(desiredTimeout), bundleId, includingAnimations ? @"including" : @"excluding"]; + @try { + original_waitForQuiescenceIncludingAnimationsIdlePreEvent(self, _cmd, includingAnimations, isPreEvent); + } @finally { + _XCTSetApplicationStateTimeout(previousTimeout); + } +} + @implementation XCUIApplicationProcess (FBQuiescence) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-load-method" +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + (void)load { Method waitForQuiescenceIncludingAnimationsIdleMethod = class_getInstanceMethod(self.class, @selector(waitForQuiescenceIncludingAnimationsIdle:)); + Method waitForQuiescenceIncludingAnimationsIdlePreEventMethod = class_getInstanceMethod(self.class, @selector(waitForQuiescenceIncludingAnimationsIdle:isPreEvent:)); if (nil != waitForQuiescenceIncludingAnimationsIdleMethod) { IMP swizzledImp = (IMP)swizzledWaitForQuiescenceIncludingAnimationsIdle; original_waitForQuiescenceIncludingAnimationsIdle = (void (*)(id, SEL, BOOL)) method_setImplementation(waitForQuiescenceIncludingAnimationsIdleMethod, swizzledImp); + } else if (nil != waitForQuiescenceIncludingAnimationsIdlePreEventMethod) { + IMP swizzledImp = (IMP)swizzledWaitForQuiescenceIncludingAnimationsIdlePreEvent; + original_waitForQuiescenceIncludingAnimationsIdlePreEvent = (void (*)(id, SEL, BOOL, BOOL)) method_setImplementation(waitForQuiescenceIncludingAnimationsIdlePreEventMethod, swizzledImp); } else { [FBLogger log:@"Could not find method -[XCUIApplicationProcess waitForQuiescenceIncludingAnimationsIdle:]"]; } @@ -74,4 +101,18 @@ - (void)setFb_shouldWaitForQuiescence:(NSNumber *)value objc_setAssociatedObject(self, &XCUIAPPLICATIONPROCESS_SHOULD_WAIT_FOR_QUIESCENCE, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +- (void)fb_waitForQuiescenceIncludingAnimationsIdle:(bool)waitForAnimations +{ + if ([self respondsToSelector:@selector(waitForQuiescenceIncludingAnimationsIdle:)]) { + [self waitForQuiescenceIncludingAnimationsIdle:waitForAnimations]; + } else if ([self respondsToSelector:@selector(waitForQuiescenceIncludingAnimationsIdle:isPreEvent:)]) { + [self waitForQuiescenceIncludingAnimationsIdle:waitForAnimations isPreEvent:NO]; + } else { + @throw [NSException exceptionWithName:FBIncompatibleWdaException + reason:@"The current WebDriverAgent build is not compatible to your device OS version" + userInfo:@{}]; + } +} + + @end diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h b/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h index 65257ee1f..1c1ab63cd 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m index 315f06744..8f9de6ee2 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHealthCheck.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIDevice+FBHealthCheck.h" diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h index e60efaa48..db03b7a1f 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -70,7 +69,7 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { - (nullable NSData *)fb_screenshotWithError:(NSError*__autoreleasing*)error; /** - Returns device current wifi ip4 address + Returns device's current wifi ip4 address */ - (nullable NSString *)fb_wifiIPAddress; @@ -96,10 +95,19 @@ typedef NS_ENUM(NSUInteger, FBUIInterfaceAppearance) { */ - (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error; +/** + Checks if the device has a specific hardware button available. + + @param buttonName The name of the button to check (e.g., "home", "volumeUp", "volumeDown", "action", "camera") + @return YES if the button is available on the device, otherwise NO + */ +- (BOOL)fb_hasButton:(NSString *)buttonName; + /** Presses the corresponding hardware button on the device with duration. - @param buttonName One of the supported button names: volumeUp (real devices only), volumeDown (real device only), home + @param buttonName One of the supported button names: volumeUp (real devices only), volumeDown (real device only), + camera (supported iOS 16+ real devices only), action (supported iOS 16+ devices only), home @param duration Duration in seconds or nil. This argument works only on tvOS. When this argument is nil on tvOS, https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton will be called. diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m index 959ae2831..bcde01972 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBHelpers.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIDevice+FBHelpers.h" @@ -27,6 +26,66 @@ static const NSTimeInterval FBHomeButtonCoolOffTime = 1.; static const NSTimeInterval FBScreenLockTimeout = 5.; +#if TARGET_OS_TV +NSDictionary *fb_availableButtonNames(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *buttons = [NSMutableDictionary dictionary]; + // https://developer.apple.com/design/human-interface-guidelines/remotes + buttons[@"up"] = @(XCUIRemoteButtonUp); // 0 + buttons[@"down"] = @(XCUIRemoteButtonDown); // 1 + buttons[@"left"] = @(XCUIRemoteButtonLeft); // 2 + buttons[@"right"] = @(XCUIRemoteButtonRight); // 3 + buttons[@"select"] = @(XCUIRemoteButtonSelect); // 4 + buttons[@"menu"] = @(XCUIRemoteButtonMenu); // 5 + buttons[@"playpause"] = @(XCUIRemoteButtonPlayPause); // 6 + buttons[@"home"] = @(XCUIRemoteButtonHome); // 7 +#if __clang_major__ >= 15 // Xcode 15+ + buttons[@"pageup"] = @(XCUIRemoteButtonPageUp); // 9 + buttons[@"pagedown"] = @(XCUIRemoteButtonPageDown); // 10 + buttons[@"guide"] = @(XCUIRemoteButtonGuide); // 11 +#endif +#if __clang_major__ >= 17 // likely Xcode 16.3+ + if (@available(tvOS 18.1, *)) { + buttons[@"fourcolors"] = @(XCUIRemoteButtonFourColors); // 12 + buttons[@"onetwothree"] = @(XCUIRemoteButtonOneTwoThree); // 13 + buttons[@"tvprovider"] = @(XCUIRemoteButtonTVProvider); // 14 + } +#endif + result = [buttons copy]; + }); + return result; +} +#else +NSDictionary *fb_availableButtonNames(void) { + static dispatch_once_t onceToken; + static NSDictionary *result; + dispatch_once(&onceToken, ^{ + NSMutableDictionary *buttons = [NSMutableDictionary dictionary]; + buttons[@"home"] = @(XCUIDeviceButtonHome); // 1 +#if !TARGET_OS_SIMULATOR + buttons[@"volumeup"] = @(XCUIDeviceButtonVolumeUp); // 2 + buttons[@"volumedown"] = @(XCUIDeviceButtonVolumeDown); // 3 +#endif + if (@available(iOS 16.0, *)) { +#if __clang_major__ >= 15 // likely Xcode 15+ + if ([XCUIDevice.sharedDevice hasHardwareButton:XCUIDeviceButtonAction]) { + buttons[@"action"] = @(XCUIDeviceButtonAction); // 4 + } +#endif +#if (!TARGET_OS_SIMULATOR && __clang_major__ >= 16) // likely Xcode 16+ + if ([XCUIDevice.sharedDevice hasHardwareButton:XCUIDeviceButtonCamera]) { + buttons[@"camera"] = @(XCUIDeviceButtonCamera); + } +#endif + } + result = [buttons copy]; + }); + return result; +} +#endif + @implementation XCUIDevice (FBHelpers) static bool fb_isLocked; @@ -56,7 +115,7 @@ + (void)fb_registerAppforDetectLockState - (BOOL)fb_goToHomescreenWithError:(NSError **)error { - return [FBApplication fb_switchToSystemApplicationWithError:error]; + return [XCUIApplication fb_switchToSystemApplicationWithError:error]; } - (BOOL)fb_lockScreen:(NSError **)error @@ -134,7 +193,7 @@ - (NSString *)fb_wifiIPAddress continue; } NSString *interfaceName = [NSString stringWithUTF8String:temp_addr->ifa_name]; - if(![interfaceName containsString:@"en"]) { + if(![interfaceName isEqualToString:@"en0"]) { temp_addr = temp_addr->ifa_next; continue; } @@ -173,7 +232,7 @@ - (BOOL)fb_openUrl:(NSString *)url error:(NSError **)error NSString *description = [NSString stringWithFormat:@"Cannot open '%@' with the default application assigned for it. Consider upgrading to Xcode 14.3+/iOS 16.4+", url]; return [[[FBErrorBuilder builder] withDescriptionFormat:@"%@", description] - buildError:error];; + buildError:error]; } - (BOOL)fb_openUrl:(NSString *)url withApplication:(NSString *)bundleId error:(NSError **)error @@ -211,6 +270,11 @@ - (BOOL)fb_activateSiriVoiceRecognitionWithText:(NSString *)text error:(NSError } } +- (BOOL)fb_hasButton:(NSString *)buttonName +{ + return fb_availableButtonNames()[buttonName.lowercaseString] != nil; +} + - (BOOL)fb_pressButton:(NSString *)buttonName forDuration:(nullable NSNumber *)duration error:(NSError **)error @@ -218,71 +282,23 @@ - (BOOL)fb_pressButton:(NSString *)buttonName #if !TARGET_OS_TV return [self fb_pressButton:buttonName error:error]; #else - NSMutableArray *supportedButtonNames = [NSMutableArray array]; - NSInteger remoteButton = -1; // no remote button - if ([buttonName.lowercaseString isEqualToString:@"home"]) { - // XCUIRemoteButtonHome = 7 - remoteButton = XCUIRemoteButtonHome; - } - [supportedButtonNames addObject:@"home"]; - - // https://developer.apple.com/design/human-interface-guidelines/tvos/remote-and-controllers/remote/ - if ([buttonName.lowercaseString isEqualToString:@"up"]) { - // XCUIRemoteButtonUp = 0, - remoteButton = XCUIRemoteButtonUp; - } - [supportedButtonNames addObject:@"up"]; - - if ([buttonName.lowercaseString isEqualToString:@"down"]) { - // XCUIRemoteButtonDown = 1, - remoteButton = XCUIRemoteButtonDown; - } - [supportedButtonNames addObject:@"down"]; - - if ([buttonName.lowercaseString isEqualToString:@"left"]) { - // XCUIRemoteButtonLeft = 2, - remoteButton = XCUIRemoteButtonLeft; - } - [supportedButtonNames addObject:@"left"]; - - if ([buttonName.lowercaseString isEqualToString:@"right"]) { - // XCUIRemoteButtonRight = 3, - remoteButton = XCUIRemoteButtonRight; - } - [supportedButtonNames addObject:@"right"]; - - if ([buttonName.lowercaseString isEqualToString:@"menu"]) { - // XCUIRemoteButtonMenu = 5, - remoteButton = XCUIRemoteButtonMenu; - } - [supportedButtonNames addObject:@"menu"]; - - if ([buttonName.lowercaseString isEqualToString:@"playpause"]) { - // XCUIRemoteButtonPlayPause = 6, - remoteButton = XCUIRemoteButtonPlayPause; - } - [supportedButtonNames addObject:@"playpause"]; - if ([buttonName.lowercaseString isEqualToString:@"select"]) { - // XCUIRemoteButtonSelect = 4, - remoteButton = XCUIRemoteButtonSelect; - } - [supportedButtonNames addObject:@"select"]; - - if (remoteButton == -1) { + NSDictionary *availableButtons = fb_availableButtonNames(); + NSNumber *buttonValue = availableButtons[buttonName.lowercaseString]; + + if (!buttonValue) { + NSArray *sortedKeys = [availableButtons.allKeys sortedArrayUsingSelector:@selector(compare:)]; return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The button '%@' is unknown. Only the following button names are supported: %@", buttonName, supportedButtonNames] + withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, sortedKeys] buildError:error]; } - if (duration) { - // https://developer.apple.com/documentation/xctest/xcuiremote/1627475-pressbutton - [[XCUIRemote sharedRemote] pressButton:remoteButton forDuration:duration.doubleValue]; + // https://developer.apple.com/documentation/xcuiautomation/xcuiremote/press(_:forduration:) + [[XCUIRemote sharedRemote] pressButton:(XCUIRemoteButton)[buttonValue unsignedIntegerValue] forDuration:duration.doubleValue]; } else { - // https://developer.apple.com/documentation/xctest/xcuiremote/1627476-pressbutton - [[XCUIRemote sharedRemote] pressButton:remoteButton]; + // https://developer.apple.com/documentation/xcuiautomation/xcuiremote/press(_:) + [[XCUIRemote sharedRemote] pressButton:(XCUIRemoteButton)[buttonValue unsignedIntegerValue]]; } - return YES; #endif } @@ -291,29 +307,16 @@ - (BOOL)fb_pressButton:(NSString *)buttonName - (BOOL)fb_pressButton:(NSString *)buttonName error:(NSError **)error { - NSMutableArray *supportedButtonNames = [NSMutableArray array]; - XCUIDeviceButton dstButton = 0; - if ([buttonName.lowercaseString isEqualToString:@"home"]) { - dstButton = XCUIDeviceButtonHome; - } - [supportedButtonNames addObject:@"home"]; -#if !TARGET_OS_SIMULATOR - if ([buttonName.lowercaseString isEqualToString:@"volumeup"]) { - dstButton = XCUIDeviceButtonVolumeUp; - } - if ([buttonName.lowercaseString isEqualToString:@"volumedown"]) { - dstButton = XCUIDeviceButtonVolumeDown; - } - [supportedButtonNames addObject:@"volumeUp"]; - [supportedButtonNames addObject:@"volumeDown"]; -#endif - - if (dstButton == 0) { + NSDictionary *availableButtons = fb_availableButtonNames(); + NSNumber *buttonValue = availableButtons[buttonName.lowercaseString]; + + if (!buttonValue) { + NSArray *sortedKeys = [availableButtons.allKeys sortedArrayUsingSelector:@selector(compare:)]; return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The button '%@' is unknown. Only the following button names are supported: %@", buttonName, supportedButtonNames] + withDescriptionFormat:@"The button '%@' is not supported. The device under test only supports the following buttons: %@", buttonName, sortedKeys] buildError:error]; } - [self pressButton:dstButton]; + [self pressButton:(XCUIDeviceButton)[buttonValue unsignedIntegerValue]]; return YES; } #endif diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h index 79271e628..b947fcdda 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.h @@ -3,12 +3,10 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import -#import NS_ASSUME_NONNULL_BEGIN diff --git a/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m index cff45cf6a..269d77f6c 100644 --- a/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m +++ b/WebDriverAgentLib/Categories/XCUIDevice+FBRotation.m @@ -3,13 +3,14 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIDevice+FBRotation.h" #import "FBConfiguration.h" +#import "XCUIApplication.h" +#import "XCUIApplication+FBHelpers.h" #import "XCUIElement+FBUtilities.h" # if !TARGET_OS_TV @@ -18,7 +19,7 @@ @implementation XCUIDevice (FBRotation) - (BOOL)fb_setDeviceInterfaceOrientation:(UIDeviceOrientation)orientation { - FBApplication *application = FBApplication.fb_activeApplication; + XCUIApplication *application = XCUIApplication.fb_activeApplication; [XCUIDevice sharedDevice].orientation = orientation; return [self waitUntilInterfaceIsAtOrientation:orientation application:application]; } @@ -29,19 +30,38 @@ - (BOOL)fb_setDeviceRotation:(NSDictionary *)rotationObj if (keysForRotationObj.count == 0) { return NO; } - NSInteger orientation = keysForRotationObj.firstObject.integerValue; - FBApplication *application = FBApplication.fb_activeApplication; + UIDeviceOrientation orientation = (UIDeviceOrientation)keysForRotationObj.firstObject.integerValue; + XCUIApplication *application = XCUIApplication.fb_activeApplication; [XCUIDevice sharedDevice].orientation = orientation; return [self waitUntilInterfaceIsAtOrientation:orientation application:application]; } -- (BOOL)waitUntilInterfaceIsAtOrientation:(NSInteger)orientation application:(FBApplication *)application +static UIInterfaceOrientation FBInterfaceOrientationFromDeviceOrientation(UIDeviceOrientation orientation) +{ + switch (orientation) { + case UIDeviceOrientationPortrait: + return UIInterfaceOrientationPortrait; + case UIDeviceOrientationPortraitUpsideDown: + return UIInterfaceOrientationPortraitUpsideDown; + case UIDeviceOrientationLandscapeLeft: + return UIInterfaceOrientationLandscapeRight; + case UIDeviceOrientationLandscapeRight: + return UIInterfaceOrientationLandscapeLeft; + case UIDeviceOrientationUnknown: + case UIDeviceOrientationFaceUp: + case UIDeviceOrientationFaceDown: + default: + return UIInterfaceOrientationUnknown; + } +} + +- (BOOL)waitUntilInterfaceIsAtOrientation:(UIDeviceOrientation)orientation application:(XCUIApplication *)application { // Tapping elements immediately after rotation may fail due to way UIKit is handling touches. // We should wait till UI cools off, before continuing [application fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; - return application.interfaceOrientation == orientation; + return application.interfaceOrientation == FBInterfaceOrientationFromDeviceOrientation(orientation); } - (NSDictionary *)fb_rotationMapping diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h b/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h index 2af37a8d5..baa56565d 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.h @@ -3,12 +3,11 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import -#import "FBXCElementSnapshotWrapper.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m b/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m index 327b2171a..47d62e58f 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBAccessibility.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBAccessibility.h" @@ -18,8 +17,7 @@ @implementation XCUIElement (FBAccessibility) - (BOOL)fb_isAccessibilityElement { - id snapshot = [self fb_snapshotWithAttributes:@[FB_XCAXAIsElementAttributeName] - maxDepth:@1]; + id snapshot = [self fb_standardSnapshot]; return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isAccessibilityElement; } @@ -33,8 +31,20 @@ - (BOOL)fb_isAccessibilityElement if (nil != isAccessibilityElement) { return isAccessibilityElement.boolValue; } - - return [(NSNumber *)[self fb_attributeValue:FB_XCAXAIsElementAttributeName] boolValue]; + + NSError *error; + NSNumber *attributeValue = [self fb_attributeValue:FB_XCAXAIsElementAttributeName + error:&error]; + if (nil != attributeValue) { + NSMutableDictionary *updatedValue = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}]; + [updatedValue setObject:attributeValue forKey:FB_XCAXAIsElementAttribute]; + self.snapshot.additionalAttributes = updatedValue.copy; + return [attributeValue boolValue]; + } + + NSLog(@"Cannot determine accessibility of '%@' natively: %@. Defaulting to: %@", + self.fb_description, error.description, @(NO)); + return NO; } @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h index 109a26371..119723aaa 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -13,9 +12,6 @@ NS_ASSUME_NONNULL_BEGIN @interface XCUIElement (FBCaching) -/*! This property is set to YES if the given element has been resolved from the cache, so it is safe to use the `lastSnapshot` property */ -@property (nullable, nonatomic) NSNumber *fb_isResolvedFromCache; - @property (nonatomic, readonly) NSString *fb_cacheId; @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m index faa42ea86..c2e9f1121 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBCaching.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBCaching.h" @@ -18,20 +17,6 @@ @implementation XCUIElement (FBCaching) -static char XCUIELEMENT_IS_RESOLVED_FROM_CACHE_KEY; - -@dynamic fb_isResolvedFromCache; - -- (void)setFb_isResolvedFromCache:(NSNumber *)isResolvedFromCache -{ - objc_setAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_FROM_CACHE_KEY, isResolvedFromCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -- (NSNumber *)fb_isResolvedFromCache -{ - return (NSNumber *)objc_getAssociatedObject(self, &XCUIELEMENT_IS_RESOLVED_FROM_CACHE_KEY); -} - static char XCUIELEMENT_CACHE_ID_KEY; @dynamic fb_cacheId; @@ -43,14 +28,7 @@ - (NSString *)fb_cacheId return (NSString *)result; } - NSString *uid; - if ([self isKindOfClass:XCUIApplication.class]) { - uid = self.fb_uid; - } else { - id snapshot = self.fb_cachedSnapshot ?: self.fb_takeSnapshot; - uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; - } - + NSString *uid = self.fb_uid; objc_setAssociatedObject(self, &XCUIELEMENT_CACHE_ID_KEY, uid, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return uid; } diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h b/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h index 1cd1e8b93..0e56bfeaa 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -50,7 +49,8 @@ NS_ASSUME_NONNULL_BEGIN @return an array of descendants matching given class chain @throws FBUnknownAttributeException if any of predicates in the chain contains unknown attribute(s) */ -- (NSArray *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch; +- (NSArray *)fb_descendantsMatchingClassChain:(NSString *)classChainQuery + shouldReturnAfterFirstMatch:(BOOL)shouldReturnAfterFirstMatch; @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m b/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m index 9f70955bb..88197eccd 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBClassChain.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBClassChain.h" diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBCustomActions.h b/WebDriverAgentLib/Categories/XCUIElement+FBCustomActions.h new file mode 100644 index 000000000..b8d32ee82 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBCustomActions.h @@ -0,0 +1,27 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBCustomActions) + +/*! Custom accessibility actions as a string – may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSString *fb_customActions; + +@end + +@interface FBXCElementSnapshotWrapper (FBCustomActions) + +/*! Custom accessibility actions as a string – may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSString *fb_customActions; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBCustomActions.m b/WebDriverAgentLib/Categories/XCUIElement+FBCustomActions.m new file mode 100644 index 000000000..dbdcb6464 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBCustomActions.m @@ -0,0 +1,127 @@ +/** + * 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 "XCUIElement+FBCustomActions.h" +#import "XCUIElement+FBUtilities.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "FBLogger.h" +#import "FBConfiguration.h" +#import "XCTestPrivateSymbols.h" + +@interface FBXCElementSnapshotWrapper (FBCustomActionsInternal) + +- (NSString *)fb_stringAttribute:(NSString *)attributeName + symbol:(NSNumber *)symbol; + +@end + +@implementation XCUIElement (FBCustomActions) + +- (NSString *)fb_customActions +{ + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] + fb_customActions]; + } +} + +@end + + +@implementation FBXCElementSnapshotWrapper(FBCustomActions) + +- (NSString *)fb_customActions +{ + return [self fb_stringAttribute:FB_XCAXACustomActionsAttributeName + symbol:FB_XCAXACustomActionsAttribute]; +} + +- (NSString *)fb_stringAttribute:(NSString *)attributeName + symbol:(NSNumber *)symbol +{ + id cached = (self.snapshot.additionalAttributes ?: @{})[symbol]; + if ([cached isKindOfClass:[NSString class]]) { + return cached; + } + + NSError *error = nil; + id raw = [self fb_attributeValue:attributeName error:&error]; + if (raw == nil) { + [FBLogger logFmt: @"[FBCustomActions] Cannot determine string value for %@: %@", + attributeName, error.localizedDescription]; + return nil; + } + + // Case 1: Already a string + if ([raw isKindOfClass:[NSString class]]) { + return [self retrieveCustomActionsFromString:raw forSymbol:symbol]; + } + + // Case 2: Array of custom actions + if ([raw isKindOfClass:[NSArray class]]) { + return [self retrieveCustomActionsFromArray:raw forSymbol:symbol]; + } + + // Fallback: Try to cast to string + return [self retrieveCustomActionsByCastingToString:raw forSymbol:symbol]; +} + +- (NSString *)retrieveCustomActionsFromString:(NSString *)stringValue + forSymbol:(NSNumber *)symbol +{ + NSMutableDictionary *updated = + (self.additionalAttributes ?: @{}).mutableCopy; + updated[symbol] = stringValue; + self.snapshot.additionalAttributes = updated.copy; + return stringValue; +} + +- (NSString *)retrieveCustomActionsFromArray:(NSArray *)arrayValue + forSymbol:(NSNumber *)symbol +{ + NSMutableArray *stringified = [NSMutableArray array]; + for (id action in arrayValue) { + NSString *title = nil; + + if ([action isKindOfClass:[NSDictionary class]]) { + title = ((NSDictionary *)action)[@"CustomActionName"]; + } + + if (!title || ![title isKindOfClass:[NSString class]]) { + @try { + title = [action valueForKey:@"title"]; + } @catch (__unused NSException * e) { + title = @""; + } + } + + [stringified addObject:title ?: @""]; + [FBLogger logFmt: @"[FBCustomActions] Custom action title: %@", title]; + } + + NSString *joined = [stringified componentsJoinedByString:@","]; + NSMutableDictionary *updated = + (self.additionalAttributes ?: @{}).mutableCopy; + updated[symbol] = joined; + self.snapshot.additionalAttributes = updated.copy; + return joined; +} + +- (NSString *)retrieveCustomActionsByCastingToString:(id)raw + forSymbol:(NSNumber *)symbol +{ + NSString *stringValue = [NSString stringWithFormat:@"%@", raw]; + NSMutableDictionary *updated = + (self.additionalAttributes ?: @{}).mutableCopy; + updated[symbol] = stringValue; + self.snapshot.additionalAttributes = updated.copy; + return stringValue; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBFind.h b/WebDriverAgentLib/Categories/XCUIElement+FBFind.h index 664d828b0..8a91fcdb8 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBFind.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBFind.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBFind.m b/WebDriverAgentLib/Categories/XCUIElement+FBFind.m index 4d46f4c8f..ceab0adf0 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBFind.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBFind.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ @@ -12,6 +11,7 @@ #import "FBMacros.h" #import "FBElementTypeTransformer.h" +#import "FBConfiguration.h" #import "NSPredicate+FBFormat.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "FBXCodeCompatibility.h" @@ -109,9 +109,9 @@ @implementation XCUIElement (FBFind) id snapshot = matchingSnapshots.firstObject; matchingSnapshots = @[snapshot]; } - return [self fb_filterDescendantsWithSnapshots:matchingSnapshots - selfUID:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot] - onlyChildren:NO]; + XCUIElement *scopeRoot = FBConfiguration.limitXpathContextScope ? self : self.application; + return [scopeRoot fb_filterDescendantsWithSnapshots:matchingSnapshots + onlyChildren:NO]; } @@ -122,7 +122,9 @@ @implementation XCUIElement (FBFind) { NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, NSDictionary * _Nullable bindings) { - return [[FBXCElementSnapshotWrapper wdNameWithSnapshot:snapshot] isEqualToString:accessibilityId]; + @autoreleasepool { + return [[FBXCElementSnapshotWrapper wdNameWithSnapshot:snapshot] isEqualToString:accessibilityId]; + } }]; return [self fb_descendantsMatchingPredicate:predicate shouldReturnAfterFirstMatch:shouldReturnAfterFirstMatch]; diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h index 154cab24e..8005b2293 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m index 1c7a9ce0e..bd5d0bdd3 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBForceTouch.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBForceTouch.h" diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h index 2acc56511..fd17acca6 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.h @@ -3,11 +3,10 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ -#import "FBXCElementSnapshotWrapper.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m index 87841fc38..75a228a3e 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBIsVisible.m @@ -3,218 +3,76 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBIsVisible.h" -#import "FBConfiguration.h" #import "FBElementUtils.h" -#import "FBMathUtils.h" -#import "FBActiveAppDetectionPoint.h" -#import "FBSession.h" -#import "FBXCAccessibilityElement.h" #import "FBXCodeCompatibility.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "XCUIElement+FBUtilities.h" -#import "XCUIElement+FBUID.h" +#import "XCUIElement+FBVisibleFrame.h" #import "XCTestPrivateSymbols.h" -@implementation XCUIElement (FBIsVisible) - -- (BOOL)fb_isVisible +NSNumber* _Nullable fetchSnapshotVisibility(id snapshot) { - id snapshot = [self fb_snapshotWithAttributes:@[FB_XCAXAIsVisibleAttributeName] - maxDepth:@1]; - return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isVisible; + return nil == snapshot.additionalAttributes ? nil : snapshot.additionalAttributes[FB_XCAXAIsVisibleAttribute]; } -@end - -@implementation FBXCElementSnapshotWrapper (FBIsVisible) - -+ (NSString *)fb_uniqIdWithSnapshot:(id)snapshot -{ - return [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot] ?: [NSString stringWithFormat:@"%p", (void *)snapshot]; -} +@implementation XCUIElement (FBIsVisible) -- (nullable NSNumber *)fb_cachedVisibilityValue +- (BOOL)fb_isVisible { - NSMutableDictionary *cache = FBSession.activeSession.elementsVisibilityCache; - if (nil == cache) { - return nil; + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_isVisible; } - - NSDictionary *result = cache[@(self.generation)]; - if (nil == result) { - // There is no need to keep the cached data for the previous generations - [cache removeAllObjects]; - cache[@(self.generation)] = [NSMutableDictionary dictionary]; - return nil; - } - return result[[self.class fb_uniqIdWithSnapshot:self.snapshot]]; } -- (BOOL)fb_cacheVisibilityWithValue:(BOOL)isVisible - forAncestors:(nullable NSArray> *)ancestors -{ - NSMutableDictionary *cache = FBSession.activeSession.elementsVisibilityCache; - if (nil == cache) { - return isVisible; - } - NSMutableDictionary *destination = cache[@(self.generation)]; - if (nil == destination) { - return isVisible; - } - - NSNumber *visibleObj = [NSNumber numberWithBool:isVisible]; - destination[[self.class fb_uniqIdWithSnapshot:self.snapshot]] = visibleObj; - if (isVisible && nil != ancestors) { - // if an element is visible then all its ancestors must be visible as well - for (id ancestor in ancestors) { - NSString *ancestorId = [self.class fb_uniqIdWithSnapshot:ancestor]; - if (nil == destination[ancestorId]) { - destination[ancestorId] = visibleObj; - } - } - } - return isVisible; -} +@end -- (CGRect)fb_frameInContainer:(id)container - hierarchyIntersection:(nullable NSValue *)intersectionRectange -{ - CGRect currentRectangle = nil == intersectionRectange ? self.frame : [intersectionRectange CGRectValue]; - id parent = self.parent; - CGRect parentFrame = parent.frame; - CGRect containerFrame = container.frame; - if (CGSizeEqualToSize(parentFrame.size, CGSizeZero) && - CGPointEqualToPoint(parentFrame.origin, CGPointZero)) { - // Special case (or XCTest bug). Shift the origin and return immediately after shift - id nextParent = parent.parent; - BOOL isGrandparent = YES; - while (nextParent && nextParent != container) { - CGRect nextParentFrame = nextParent.frame; - if (isGrandparent && - CGSizeEqualToSize(nextParentFrame.size, CGSizeZero) && - CGPointEqualToPoint(nextParentFrame.origin, CGPointZero)) { - // Double zero-size container inclusion means that element coordinates are absolute - return CGRectIntersection(currentRectangle, containerFrame); - } - isGrandparent = NO; - if (!CGPointEqualToPoint(nextParentFrame.origin, CGPointZero)) { - currentRectangle.origin.x += nextParentFrame.origin.x; - currentRectangle.origin.y += nextParentFrame.origin.y; - return CGRectIntersection(currentRectangle, containerFrame); - } - nextParent = nextParent.parent; - } - return CGRectIntersection(currentRectangle, containerFrame); - } - // Skip parent containers if they are outside of the viewport - CGRect intersectionWithParent = CGRectIntersectsRect(parentFrame, containerFrame) || parent.elementType != XCUIElementTypeOther - ? CGRectIntersection(currentRectangle, parentFrame) - : currentRectangle; - if (CGRectIsEmpty(intersectionWithParent) && - parent != container && - self.elementType == XCUIElementTypeOther) { - // Special case (or XCTest bug). Shift the origin - if (CGSizeEqualToSize(parentFrame.size, containerFrame.size) || - // The size might be inverted in landscape - CGSizeEqualToSize(parentFrame.size, CGSizeMake(containerFrame.size.height, containerFrame.size.width)) || - CGSizeEqualToSize(self.frame.size, CGSizeZero)) { - // Covers ActivityListView and RemoteBridgeView cases - currentRectangle.origin.x += parentFrame.origin.x; - currentRectangle.origin.y += parentFrame.origin.y; - return CGRectIntersection(currentRectangle, containerFrame); - } - } - if (CGRectIsEmpty(intersectionWithParent) || parent == container) { - return intersectionWithParent; - } - return [[FBXCElementSnapshotWrapper ensureWrapped:parent] fb_frameInContainer:container - hierarchyIntersection:[NSValue valueWithCGRect:intersectionWithParent]]; -} +@implementation FBXCElementSnapshotWrapper (FBIsVisible) -- (BOOL)fb_hasAnyVisibleLeafs +- (BOOL)fb_hasVisibleDescendants { - NSArray> *children = self.children; - if (0 == children.count) { - return self.fb_isVisible; - } - - for (id child in children) { - if ([FBXCElementSnapshotWrapper ensureWrapped:child].fb_hasAnyVisibleLeafs) { + for (id descendant in (self._allDescendants ?: @[])) { + if ([fetchSnapshotVisibility(descendant) boolValue]) { return YES; } } - return NO; } - (BOOL)fb_isVisible { - NSNumber *isVisible = self.additionalAttributes[FB_XCAXAIsVisibleAttribute]; - if (isVisible != nil) { + NSNumber *isVisible = fetchSnapshotVisibility(self); + if (nil != isVisible) { return isVisible.boolValue; } - - NSNumber *cachedValue = [self fb_cachedVisibilityValue]; - if (nil != cachedValue) { - return [cachedValue boolValue]; - } - CGRect selfFrame = self.frame; - if (CGRectIsEmpty(selfFrame)) { - return [self fb_cacheVisibilityWithValue:NO forAncestors:nil]; + // Fetching the attribute value is expensive. + // Shortcircuit here to save time and assume if any of descendants + // is already determined as visible then the container should be visible as well + if ([self fb_hasVisibleDescendants]) { + return YES; } - NSArray> *ancestors = self.fb_ancestors; - if ([FBConfiguration shouldUseTestManagerForVisibilityDetection]) { - BOOL visibleAttrValue = [(NSNumber *)[self fb_attributeValue:FB_XCAXAIsVisibleAttributeName] boolValue]; - return [self fb_cacheVisibilityWithValue:visibleAttrValue forAncestors:ancestors]; - } - - id parentWindow = ancestors.count > 1 ? [ancestors objectAtIndex:ancestors.count - 2] : nil; - CGRect visibleRect = selfFrame; - if (nil != parentWindow) { - visibleRect = [self fb_frameInContainer:parentWindow hierarchyIntersection:nil]; - } - if (CGRectIsEmpty(visibleRect)) { - return [self fb_cacheVisibilityWithValue:NO forAncestors:ancestors]; - } - CGPoint midPoint = CGPointMake(visibleRect.origin.x + visibleRect.size.width / 2, - visibleRect.origin.y + visibleRect.size.height / 2); - id hitElement = [FBActiveAppDetectionPoint axElementWithPoint:midPoint]; - if (nil != hitElement) { - if (FBIsAXElementEqualToOther(self.accessibilityElement, hitElement)) { - return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors]; - } - for (id ancestor in ancestors) { - if (FBIsAXElementEqualToOther(hitElement, ancestor.accessibilityElement)) { - return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors]; - } + NSError *error; + NSNumber *attributeValue = [self fb_attributeValue:FB_XCAXAIsVisibleAttributeName + error:&error]; + if (nil != attributeValue) { + NSMutableDictionary *updatedValue = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}]; + [updatedValue setObject:attributeValue forKey:FB_XCAXAIsVisibleAttribute]; + self.snapshot.additionalAttributes = updatedValue.copy; + @autoreleasepool { + return [attributeValue boolValue]; } } - if (self.children.count > 0) { - if (nil != hitElement) { - for (id descendant in self._allDescendants) { - if (FBIsAXElementEqualToOther(hitElement, descendant.accessibilityElement)) { - return [self fb_cacheVisibilityWithValue:YES - forAncestors:[FBXCElementSnapshotWrapper ensureWrapped:descendant].fb_ancestors]; - } - } - } - if (self.fb_hasAnyVisibleLeafs) { - return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors]; - } - } else if (nil == hitElement) { - // Sometimes XCTest returns nil for leaf elements hit test even if such elements are hittable - // Assume such elements are visible if their rectInContainer is visible - return [self fb_cacheVisibilityWithValue:YES forAncestors:ancestors]; - } - return [self fb_cacheVisibilityWithValue:NO forAncestors:ancestors]; + + NSLog(@"Cannot determine visiblity of %@ natively: %@. Defaulting to: %@", + self.fb_description, error.description, @(NO)); + return NO; } @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h b/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h new file mode 100644 index 000000000..0873a65a4 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.h @@ -0,0 +1,33 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBMinMax) + +/*! Minimum value (minValue) – may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_minValue; + +/*! Maximum value (maxValue) - may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_maxValue; + +@end + +@interface FBXCElementSnapshotWrapper (FBMinMax) + +/*! Minimum value (minValue) – may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_minValue; + +/*! Maximum value (maxValue) - may be nil if the element does not have this attribute */ +@property (nonatomic, readonly, nullable) NSNumber *fb_maxValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m b/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m new file mode 100644 index 000000000..875149060 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBMinMax.m @@ -0,0 +1,75 @@ +/** + * 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 "FBLogger.h" +#import "XCUIElement+FBMinMax.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBUtilities.h" +#import "XCTestPrivateSymbols.h" + +@interface FBXCElementSnapshotWrapper (FBMinMaxInternal) + +- (NSNumber *)fb_numericAttribute:(NSString *)attributeName symbol:(NSNumber *)symbol; + +@end + +@implementation XCUIElement (FBMinMax) + +- (NSNumber *)fb_minValue +{ + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_minValue]; + } +} + +- (NSNumber *)fb_maxValue +{ + @autoreleasepool { + id snapshot = [self fb_standardSnapshot]; + return [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_maxValue]; + } +} + +@end + +@implementation FBXCElementSnapshotWrapper (FBMinMax) + +- (NSNumber *)fb_minValue +{ + return [self fb_numericAttribute:FB_XCAXACustomMinValueAttributeName + symbol:FB_XCAXACustomMinValueAttribute]; +} + +- (NSNumber *)fb_maxValue +{ + return [self fb_numericAttribute:FB_XCAXACustomMaxValueAttributeName + symbol:FB_XCAXACustomMaxValueAttribute]; +} + +- (NSNumber *)fb_numericAttribute:(NSString *)attributeName symbol:(NSNumber *)symbol +{ + NSNumber *cached = (self.snapshot.additionalAttributes ?: @{})[symbol]; + if (cached) { + return cached; + } + + NSError *error = nil; + NSNumber *raw = [self fb_attributeValue:attributeName error:&error]; + if (nil != raw) { + NSMutableDictionary *updated = [NSMutableDictionary dictionaryWithDictionary:self.additionalAttributes ?: @{}]; + updated[symbol] = raw; + self.snapshot.additionalAttributes = updated.copy; + return raw; + } + + [FBLogger logFmt:@"[FBMinMax] Cannot determine %@ for %@: %@", attributeName, self.fb_description, error.localizedDescription]; + return nil; +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h index a63cf934e..72762a3c4 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m index 71ebaf652..551892135 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBPickerWheel.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBPickerWheel.h" @@ -12,6 +11,7 @@ #import "FBRunLoopSpinner.h" #import "FBXCElementSnapshot.h" #import "FBXCodeCompatibility.h" +#import "XCUIElement+FBUID.h" #import "XCUICoordinate.h" #import "XCUIElement+FBCaching.h" #import "XCUIElement+FBResolve.h" @@ -23,9 +23,7 @@ @implementation XCUIElement (FBPickerWheel) - (BOOL)fb_scrollWithOffset:(CGFloat)relativeHeightOffset error:(NSError **)error { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_standardSnapshot]; NSString *previousValue = snapshot.value; XCUICoordinate *startCoord = [self coordinateWithNormalizedOffset:CGVectorMake(0.5, 0.5)]; XCUICoordinate *endCoord = [startCoord coordinateWithOffset:CGVectorMake(0.0, relativeHeightOffset * snapshot.frame.size.height)]; @@ -35,7 +33,7 @@ - (BOOL)fb_scrollWithOffset:(CGFloat)relativeHeightOffset error:(NSError **)erro // Fetching stable instance of an element allows it to be bounded to the // unique element identifier (UID), so it could be found next time even if its // id is different from the initial one. See https://github.com/appium/appium/issues/17569 - XCUIElement *stableInstance = self.fb_stableInstance; + XCUIElement *stableInstance = [self fb_stableInstanceWithUid:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]]; [endCoord tap]; return [[[[FBRunLoopSpinner new] timeout:VALUE_CHANGE_TIMEOUT] diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h index 80b218b06..09b174f22 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -27,10 +26,11 @@ NS_ASSUME_NONNULL_BEGIN Although, if the cached element instance is the one returned by this API call then the same element is going to be matched and no staleness exception will be thrown. + @param uid Element UUID @return Either the same element instance if `fb_isResolvedNatively` was set to NO (usually the cache for elements matched by xpath locators) or the stable instance of the self element based on the query by element's UUID. */ -- (XCUIElement *)fb_stableInstance; +- (XCUIElement *)fb_stableInstanceWithUid:(NSString *__nullable)uid; @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m index f52c72bf6..f288114dd 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBResolve.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBResolve.h" @@ -32,23 +31,21 @@ - (NSNumber *)fb_isResolvedNatively return nil == result ? @YES : result; } -- (XCUIElement *)fb_stableInstance +- (XCUIElement *)fb_stableInstanceWithUid:(NSString *)uid { - if (![self.fb_isResolvedNatively boolValue]) { + if (nil == uid || ![self.fb_isResolvedNatively boolValue] || [self isKindOfClass:XCUIApplication.class]) { return self; } - - XCUIElementQuery *query = [self isKindOfClass:XCUIApplication.class] - ? self.application.fb_query - : [self.application.fb_query descendantsMatchingType:XCUIElementTypeAny]; - NSString *uid = nil == self.fb_cachedSnapshot - ? self.fb_uid - : [FBXCElementSnapshotWrapper wdUIDWithSnapshot:(id)self.fb_cachedSnapshot]; - if (nil == uid) { - return self; + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@", FBStringify(FBXCElementSnapshotWrapper, fb_uid), uid]; + @autoreleasepool { + XCUIElementQuery *query = [self.application.fb_query descendantsMatchingType:XCUIElementTypeAny]; + XCUIElement *result = [query matchingPredicate:predicate].allElementsBoundByIndex.firstObject; + if (nil != result) { + result.fb_isResolvedNatively = @NO; + return result; + } } - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K = %@",FBStringify(FBXCElementSnapshotWrapper, fb_uid), uid]; - return [query matchingPredicate:predicate].allElementsBoundByIndex.firstObject ?: self; + return self; } @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h index 680fddbfc..416544e37 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m index 52be1a557..6b63d0a98 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBScrolling.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBScrolling.h" @@ -20,9 +19,11 @@ #import "XCUIApplication.h" #import "XCUICoordinate.h" #import "XCUIElement+FBIsVisible.h" +#import "XCUIElement+FBVisibleFrame.h" #import "XCUIElement.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" +#import "XCTestPrivateSymbols.h" const CGFloat FBFuzzyPointThreshold = 20.f; //Smallest determined value that is not interpreted as touch const CGFloat FBScrollToVisibleNormalizedDistance = .5f; @@ -47,45 +48,35 @@ @implementation XCUIElement (FBScrolling) - (BOOL)fb_nativeScrollToVisibleWithError:(NSError **)error { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; return nil != [self _hitPointByAttemptingToScrollToVisibleSnapshot:snapshot error:error]; } - (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollUpByNormalizedDistance:distance inApplication:self.application]; } - (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollDownByNormalizedDistance:distance inApplication:self.application]; } - (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollLeftByNormalizedDistance:distance inApplication:self.application]; } - (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_customSnapshot]; [[FBXCElementSnapshotWrapper ensureWrapped:snapshot] fb_scrollRightByNormalizedDistance:distance inApplication:self.application]; } @@ -95,7 +86,8 @@ - (BOOL)fb_scrollToVisibleWithError:(NSError **)error return [self fb_scrollToVisibleWithNormalizedScrollDistance:FBScrollToVisibleNormalizedDistance error:error]; } -- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance error:(NSError **)error +- (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScrollDistance + error:(NSError **)error { return [self fb_scrollToVisibleWithNormalizedScrollDistance:normalizedScrollDistance scrollDirection:FBXCUIElementScrollDirectionUnknown @@ -106,7 +98,8 @@ - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScroll scrollDirection:(FBXCUIElementScrollDirection)scrollDirection error:(NSError **)error { - FBXCElementSnapshotWrapper *prescrollSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_takeSnapshot]]; + FBXCElementSnapshotWrapper *prescrollSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]]; + if (prescrollSnapshot.isWDVisible) { return YES; } @@ -138,12 +131,12 @@ - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScroll FBXCElementSnapshotWrapper *wrappedCellSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:cellSnapshot]; if (wrappedCellSnapshot.wdVisible) { [visibleCellSnapshots addObject:cellSnapshot]; + if (visibleCellSnapshots.count > 1) { + return YES; + } } } - if (visibleCellSnapshots.count > 1) { - return YES; - } return NO; }]; @@ -184,23 +177,25 @@ - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScroll FBXCElementSnapshotWrapper *scrollViewWrapped = [FBXCElementSnapshotWrapper ensureWrapped:scrollView]; // Scrolling till cell is visible and get current value of frames while (![self fb_isEquivalentElementSnapshotVisible:prescrollSnapshot] && scrollCount < maxScrollCount) { - if (targetCellIndex < visibleCellIndex) { - scrollDirection == FBXCUIElementScrollDirectionVertical ? - [scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance - inApplication:self.application] : - [scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance - inApplication:self.application]; - } - else { - scrollDirection == FBXCUIElementScrollDirectionVertical ? - [scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance + @autoreleasepool { + if (targetCellIndex < visibleCellIndex) { + scrollDirection == FBXCUIElementScrollDirectionVertical ? + [scrollViewWrapped fb_scrollUpByNormalizedDistance:normalizedScrollDistance inApplication:self.application] : - [scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance - inApplication:self.application]; + [scrollViewWrapped fb_scrollLeftByNormalizedDistance:normalizedScrollDistance + inApplication:self.application]; + } + else { + scrollDirection == FBXCUIElementScrollDirectionVertical ? + [scrollViewWrapped fb_scrollDownByNormalizedDistance:normalizedScrollDistance + inApplication:self.application] : + [scrollViewWrapped fb_scrollRightByNormalizedDistance:normalizedScrollDistance + inApplication:self.application]; + } + scrollCount++; + // Wait for scroll animation + [self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; } - scrollCount++; - // Wait for scroll animation - [self fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; } if (scrollCount >= maxScrollCount) { @@ -213,9 +208,9 @@ - (BOOL)fb_scrollToVisibleWithNormalizedScrollDistance:(CGFloat)normalizedScroll // Cell is now visible, but it might be only partialy visible, scrolling till whole frame is visible. // Sometimes, attempting to grab the parent snapshot of the target cell after scrolling is complete causes a stale element reference exception. // Trying fb_cachedSnapshot first - FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:([self fb_cachedSnapshot] ?: [self fb_takeSnapshot])]; + FBXCElementSnapshotWrapper *targetCellSnapshotWrapped = [FBXCElementSnapshotWrapper ensureWrapped:[self fb_customSnapshot]]; targetCellSnapshot = [targetCellSnapshotWrapped fb_parentCellSnapshot]; - CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrameWithFallback; + CGRect visibleFrame = [FBXCElementSnapshotWrapper ensureWrapped:targetCellSnapshot].fb_visibleFrame; CGVector scrollVector = CGVectorMake(visibleFrame.size.width - targetCellSnapshot.frame.size.width, visibleFrame.size.height - targetCellSnapshot.frame.size.height @@ -233,7 +228,7 @@ - (BOOL)fb_isEquivalentElementSnapshotVisible:(id)snapshot return YES; } - id appSnapshot = [self.application fb_takeSnapshot]; + id appSnapshot = [self.application fb_standardSnapshot]; for (id elementSnapshot in appSnapshot._allDescendants.copy) { FBXCElementSnapshotWrapper *wrappedElementSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:elementSnapshot]; // We are comparing pre-scroll snapshot so frames are irrelevant. @@ -255,27 +250,32 @@ - (CGRect)scrollingFrame return self.visibleFrame; } -- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application +- (void)fb_scrollUpByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application { [self fb_scrollByNormalizedVector:CGVectorMake(0.0, distance) inApplication:application]; } -- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application +- (void)fb_scrollDownByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application { [self fb_scrollByNormalizedVector:CGVectorMake(0.0, -distance) inApplication:application]; } -- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application +- (void)fb_scrollLeftByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application { [self fb_scrollByNormalizedVector:CGVectorMake(distance, 0.0) inApplication:application]; } -- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance inApplication:(XCUIApplication *)application +- (void)fb_scrollRightByNormalizedDistance:(CGFloat)distance + inApplication:(XCUIApplication *)application { [self fb_scrollByNormalizedVector:CGVectorMake(-distance, 0.0) inApplication:application]; } -- (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector inApplication:(XCUIApplication *)application +- (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector + inApplication:(XCUIApplication *)application { CGVector scrollVector = CGVectorMake(CGRectGetWidth(self.scrollingFrame) * normalizedScrollVector.dx, CGRectGetHeight(self.scrollingFrame) * normalizedScrollVector.dy @@ -283,7 +283,9 @@ - (BOOL)fb_scrollByNormalizedVector:(CGVector)normalizedScrollVector inApplicati return [self fb_scrollByVector:scrollVector inApplication:application error:nil]; } -- (BOOL)fb_scrollByVector:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error +- (BOOL)fb_scrollByVector:(CGVector)vector + inApplication:(XCUIApplication *)application + error:(NSError **)error { CGVector scrollBoundingVector = CGVectorMake( CGRectGetWidth(self.scrollingFrame) * FBScrollTouchProportion, @@ -314,7 +316,9 @@ - (CGVector)fb_hitPointOffsetForScrollingVector:(CGVector)scrollingVector return CGVectorMake((CGFloat)floor(x), (CGFloat)floor(y)); } -- (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector inApplication:(XCUIApplication *)application error:(NSError **)error +- (BOOL)fb_scrollAncestorScrollViewByVectorWithinScrollViewFrame:(CGVector)vector + inApplication:(XCUIApplication *)application + error:(NSError **)error { CGVector hitpointOffset = [self fb_hitPointOffsetForScrollingVector:vector]; diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h b/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h index b1451f484..4a2b5c1a1 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -22,4 +21,18 @@ NS_ASSUME_NONNULL_BEGIN @end +#if !TARGET_OS_TV +@interface XCUICoordinate (FBSwiping) + +/** + * Performs swipe gesture on the coordinate + * + * @param direction Swipe direction. The following values are supported: up, down, left and right + * @param velocity Swipe speed in pixels per second + */ +- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity; + +@end +#endif + NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m b/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m index f7d8be124..8f64b4832 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBSwiping.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBSwiping.h" @@ -12,29 +11,46 @@ #import "FBLogger.h" #import "XCUIElement.h" -@implementation XCUIElement (FBSwiping) - -- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity -{ +void swipeWithDirection(NSObject *target, NSString *direction, NSNumber* _Nullable velocity) { double velocityValue = .0; if (nil != velocity) { velocityValue = [velocity doubleValue]; } if (velocityValue > 0) { - SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@WithVelocity:", direction.lowercaseString.capitalizedString]); - NSMethodSignature *signature = [self methodSignatureForSelector:selector]; + SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@WithVelocity:", + direction.lowercaseString.capitalizedString]); + NSMethodSignature *signature = [target methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setSelector:selector]; [invocation setArgument:&velocityValue atIndex:2]; - [invocation invokeWithTarget:self]; + [invocation invokeWithTarget:target]; } else { - SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@", direction.lowercaseString.capitalizedString]); - NSMethodSignature *signature = [self methodSignatureForSelector:selector]; + SEL selector = NSSelectorFromString([NSString stringWithFormat:@"swipe%@", + direction.lowercaseString.capitalizedString]); + NSMethodSignature *signature = [target methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setSelector:selector]; - [invocation invokeWithTarget:self]; + [invocation invokeWithTarget:target]; } } +@implementation XCUIElement (FBSwiping) + +- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity +{ + swipeWithDirection(self, direction, velocity); +} + +@end + +#if !TARGET_OS_TV +@implementation XCUICoordinate (FBSwiping) + +- (void)fb_swipeWithDirection:(NSString *)direction velocity:(nullable NSNumber*)velocity +{ + swipeWithDirection(self, direction, velocity); +} + @end +#endif diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h b/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h index 6b25f9ab4..0a3bc0794 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m b/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m index 9c82515ad..5aa850843 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBTVFocuse.m @@ -3,17 +3,16 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBTVFocuse.h" #import -#import "FBApplication.h" #import "FBConfiguration.h" #import "FBErrorBuilder.h" #import +#import "XCUIApplication+FBHelpers.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" @@ -25,7 +24,7 @@ @implementation XCUIElement (FBTVFocuse) - (BOOL)fb_setFocusWithError:(NSError**) error { - [FBApplication.fb_activeApplication fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; + [XCUIApplication.fb_activeApplication fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; if (!self.wdEnabled) { if (error) { diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h index 49ef3c6ce..0b775589d 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.h @@ -3,14 +3,23 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import NS_ASSUME_NONNULL_BEGIN +/** + Types a text into the currently focused element. + + @param text text that should be typed + @param typingSpeed Frequency of typing (letters per sec) + @param error If there is an error, upon return contains an NSError object that describes the problem. + @return YES if the operation succeeds, otherwise NO. + */ +BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error); + @interface XCUIElement (FBTyping) /** diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m index c6e31f4fc..1d76f6ca0 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBTyping.m @@ -3,25 +3,41 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBTyping.h" #import "FBConfiguration.h" #import "FBErrorBuilder.h" -#import "FBKeyboard.h" -#import "NSString+FBVisualLength.h" #import "FBXCElementSnapshotWrapper.h" #import "FBXCElementSnapshotWrapper+Helpers.h" +#import "FBXCodeCompatibility.h" +#import "FBXCTestDaemonsProxy.h" +#import "NSString+FBVisualLength.h" #import "XCUIDevice+FBHelpers.h" #import "XCUIElement+FBCaching.h" #import "XCUIElement+FBUtilities.h" -#import "FBXCodeCompatibility.h" +#import "XCSynthesizedEventRecord.h" +#import "XCPointerEventPath.h" +#define MAX_TEXT_ABBR_LEN 12 #define MAX_CLEAR_RETRIES 3 +BOOL FBTypeText(NSString *text, NSUInteger typingSpeed, NSError **error) +{ + NSString *name = text.length <= MAX_TEXT_ABBR_LEN + ? [NSString stringWithFormat:@"Type '%@'", text] + : [NSString stringWithFormat:@"Type '%@…'", [text substringToIndex:MAX_TEXT_ABBR_LEN]]; + XCSynthesizedEventRecord *eventRecord = [[XCSynthesizedEventRecord alloc] initWithName:name]; + XCPointerEventPath *ep = [[XCPointerEventPath alloc] initForTextInput]; + [ep typeText:text + atOffset:0.0 + typingSpeed:typingSpeed + shouldRedact:NO]; + [eventRecord addPointerEventPath:ep]; + return [FBXCTestDaemonsProxy synthesizeEventWithRecord:eventRecord error:error]; +} @interface NSString (FBRepeat) @@ -74,7 +90,7 @@ - (void)fb_prepareForTextInputWithSnapshot:(FBXCElementSnapshotWrapper *)snapsho [FBLogger logFmt:@"Trying to tap the \"%@\" element to have it focused", snapshot.fb_description]; [self tap]; // It might take some time to update the UI - [self fb_takeSnapshot]; + [self fb_standardSnapshot]; #endif } @@ -93,23 +109,18 @@ - (BOOL)fb_typeText:(NSString *)text frequency:(NSUInteger)frequency error:(NSError **)error { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; - [self fb_prepareForTextInputWithSnapshot:[FBXCElementSnapshotWrapper ensureWrapped:snapshot]]; - if (shouldClear && ![self fb_clearTextWithSnapshot:self.lastSnapshot - shouldPrepareForInput:NO - error:error]) { + id snapshot = [self fb_standardSnapshot]; + FBXCElementSnapshotWrapper *wrapped = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + [self fb_prepareForTextInputWithSnapshot:wrapped]; + if (shouldClear && ![self fb_clearTextWithSnapshot:wrapped shouldPrepareForInput:NO error:error]) { return NO; } - return [FBKeyboard typeText:text frequency:frequency error:error]; + return FBTypeText(text, frequency, error); } - (BOOL)fb_clearTextWithError:(NSError **)error { - id snapshot = self.fb_isResolvedFromCache.boolValue - ? self.lastSnapshot - : self.fb_takeSnapshot; + id snapshot = [self fb_standardSnapshot]; return [self fb_clearTextWithSnapshot:[FBXCElementSnapshotWrapper ensureWrapped:snapshot] shouldPrepareForInput:YES error:error]; @@ -122,15 +133,15 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot id currentValue = snapshot.value; if (nil != currentValue && ![currentValue isKindOfClass:NSString.class]) { return [[[FBErrorBuilder builder] - withDescriptionFormat:@"The value of '%@' is not a string and thus cannot be edited", snapshot.fb_description] - buildError:error]; + withDescriptionFormat:@"The value of '%@' is not a string and thus cannot be edited", snapshot.fb_description] + buildError:error]; } - + if (nil == currentValue || 0 == [currentValue fb_visualLength]) { // Short circuit if the content is not present return YES; } - + static NSString *backspaceDeleteSequence; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -150,7 +161,7 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot [self fb_prepareForTextInputWithSnapshot:snapshot]; } - if (retry == 0) { + if (retry == 0 && FBConfiguration.useClearTextShortcut) { // 1st attempt is via the IOHIDEvent as the fastest operation // https://github.com/appium/appium/issues/19389 [[XCUIDevice sharedDevice] fb_performIOHIDEventWithPage:0x07 // kHIDPage_KeyboardOrKeypad @@ -160,13 +171,13 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot } else if (retry >= MAX_CLEAR_RETRIES - 1) { // Last chance retry. Tripple-tap the field to select its content [self tapWithNumberOfTaps:3 numberOfTouches:1]; - return [FBKeyboard typeText:backspaceDeleteSequence error:error]; - } else if (![FBKeyboard typeText:backspacesToType error:error]) { + return FBTypeText(backspaceDeleteSequence, FBConfiguration.defaultTypingFrequency, error); + } else if (!FBTypeText(backspacesToType, FBConfiguration.defaultTypingFrequency, error)) { // 2nd operation return NO; } - currentValue = self.fb_takeSnapshot.value; + currentValue = [self fb_standardSnapshot].value; if (nil != placeholderValue && [currentValue isEqualToString:placeholderValue]) { // Short circuit if only the placeholder value left return YES; @@ -181,7 +192,7 @@ - (BOOL)fb_clearTextWithSnapshot:(FBXCElementSnapshotWrapper *)snapshot // kHIDPage_KeyboardOrKeypad did not work for tvOS's search field. (tvOS 17 at least) // Tested XCUIElementTypeSearchField and XCUIElementTypeTextView whch were // common search field and email/passowrd input in tvOS apps. - return [FBKeyboard typeText:backspacesToType error:error]; + return FBTypeText(backspacesToType, FBConfiguration.defaultTypingFrequency, error); #endif } diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUID.h b/WebDriverAgentLib/Categories/XCUIElement+FBUID.h index 8b1a2da64..5a406adea 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBUID.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUID.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCElementSnapshotWrapper.h" diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUID.m b/WebDriverAgentLib/Categories/XCUIElement+FBUID.m index 765b410c1..ebbb06654 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBUID.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUID.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -22,14 +21,14 @@ - (unsigned long long)fb_accessibiltyId { return [FBElementUtils idWithAccessibilityElement:([self isKindOfClass:XCUIApplication.class] ? [(XCUIApplication *)self accessibilityElement] - : [self fb_takeSnapshot].accessibilityElement)]; + : [self fb_standardSnapshot].accessibilityElement)]; } - (NSString *)fb_uid { return [self isKindOfClass:XCUIApplication.class] ? [FBElementUtils uidWithAccessibilityElement:[(XCUIApplication *)self accessibilityElement]] - : [FBXCElementSnapshotWrapper ensureWrapped:[self fb_takeSnapshot]].fb_uid; + : [FBXCElementSnapshotWrapper ensureWrapped:[self fb_standardSnapshot]].fb_uid; } @end @@ -42,6 +41,7 @@ static void swizzled_validatePredicateWithExpressionsAllowed(id self, SEL _cmd, #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-load-method" +#pragma clang diagnostic ignored "-Wcast-function-type-strict" + (void)load { Class XCElementSnapshotCls = objc_lookUpClass("XCElementSnapshot"); diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h index 66b9ee2c9..b67be4e72 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.h @@ -3,13 +3,12 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import #import -#import "FBXCElementSnapshot.h" +#import NS_ASSUME_NONNULL_BEGIN @@ -18,70 +17,74 @@ NS_ASSUME_NONNULL_BEGIN /** Gets the most recent snapshot of the current element. The element will be automatically resolved if the snapshot is not available yet. - Calls to this method mutate the `lastSnapshot` instance property.. - Calls to this method reset the `fb_isResolvedFromCache` property value to `NO`. + Calls to this method mutate the `lastSnapshot` instance property. + The snapshot is taken by the native API provided by XCTest. + The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth` + + Snapshot specifics: + - Most performant + - Memory-friedly + - `children` property is set to `nil` if not taken from XCUIApplication + - `value` property is cut off to max 512 bytes + - Sometimes `frame` properties may be off. @return The recent snapshot of the element @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made */ -- (id)fb_takeSnapshot; +- (id)fb_standardSnapshot; /** - Extracts the cached element snapshot from its query. - No requests to the accessiblity framework is made. - It is only safe to use this call right after element lookup query - has been executed. + Gets the most recent snapshot of the current element. The element will be + automatically resolved if the snapshot is not available yet. + Calls to this method mutate the `lastSnapshot` instance property. + The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth` - @return Either the cached snapshot or nil + Snapshot specifics: + - Less performant in comparison to the standard one + - `children` property is always defined + - `value` property is not cut off + + @return The recent snapshot of the element + @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made */ -- (nullable id)fb_cachedSnapshot; +- (id)fb_customSnapshot; /** - Gets the most recent snapshot of the current element and already resolves the accessibility attributes - needed for creating the page source of this element. No additional calls to the accessibility layer - are required. + Gets the most recent snapshot of the current element. The element will be + automatically resolved if the snapshot is not available yet. Calls to this method mutate the `lastSnapshot` instance property. - Calls to this method reset the `fb_isResolvedFromCache` property value to `NO`. + The maximum snapshot tree depth is set by `FBConfiguration.snapshotMaxDepth` + + Snapshot specifics: + - Less performant in comparison to the custom one. Internally, it calls same APIs + that fb_customSnapshot does, although this one has some additional logic to ensure + the snapshot is valid. It also may make retries, which slows the call down significantly. + - The `hittable` property calculation is aligned with the native calculation logic - @param maxDepth The maximum depth of the snapshot. nil value means to use the default depth. - with custom attributes cannot be resolved - - @return The recent snapshot of the element with all attributes resolved or a snapshot with default - attributes resolved if there was a failure while resolving additional attributes + @return The recent snapshot of the element @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made */ -- (nullable id)fb_snapshotWithAllAttributesAndMaxDepth:(nullable NSNumber *)maxDepth; +- (id)fb_nativeSnapshot; /** - Gets the most recent snapshot of the current element with given attributes resolved. - No additional calls to the accessibility layer are required. - Calls to this method mutate the `lastSnapshot` instance property. - Calls to this method reset the `fb_isResolvedFromCache` property value to `NO`. - - @param attributeNames The list of attribute names to resolve. Must be one of - FB_...Name values exported by XCTestPrivateSymbols.h module. - `nil` value means that only the default attributes must be extracted - @param maxDepth The maximum depth of the snapshot. nil value means to use the default depth. + Extracts the cached element snapshot from its query. + No requests to the accessiblity framework is made. + It is only safe to use this call right after element lookup query + has been executed. - @return The recent snapshot of the element with the given attributes resolved or a snapshot with default - attributes resolved if there was a failure while resolving additional attributes - @throws FBStaleElementException if the element is not present in DOM and thus no snapshot could be made -*/ -- (nullable id)fb_snapshotWithAttributes:(nullable NSArray *)attributeNames - maxDepth:(nullable NSNumber *)maxDepth; + @return Either the cached snapshot or nil + */ +- (nullable id)fb_cachedSnapshot; /** Filters elements by matching them to snapshots from the corresponding array @param snapshots Array of snapshots to be matched with - @param selfUID Optionally the unique identifier of the current element. - Providing it as an argument improves the performance of the method. @param onlyChildren Whether to only look for direct element children @return Array of filtered elements, which have matches in snapshots array */ - (NSArray *)fb_filterDescendantsWithSnapshots:(NSArray> *)snapshots - selfUID:(nullable NSString *)selfUID onlyChildren:(BOOL)onlyChildren; /** diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m index b5a072c06..35b9b7da5 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBUtilities.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBUtilities.h" @@ -14,6 +13,7 @@ #import "FBConfiguration.h" #import "FBExceptions.h" #import "FBImageUtils.h" +#import "FBElementUtils.h" #import "FBLogger.h" #import "FBMacros.h" #import "FBMathUtils.h" @@ -31,6 +31,8 @@ #import "XCTElementSetTransformer-Protocol.h" #import "XCTestPrivateSymbols.h" #import "XCTRunnerDaemonSession.h" +#import "XCUIApplicationProcess+FBQuiescence.h" +#import "XCUIApplication.h" #import "XCUIElement+FBCaching.h" #import "XCUIElement+FBWebDriverAttributes.h" #import "XCUIElementQuery.h" @@ -39,94 +41,50 @@ #import "XCUIScreen.h" #import "XCUIElement+FBResolve.h" -#define DEFAULT_AX_TIMEOUT 60. - @implementation XCUIElement (FBUtilities) -- (id)fb_takeSnapshot +- (id)fb_takeSnapshot:(BOOL)isCustom { - NSError *error = nil; - self.fb_isResolvedFromCache = @(NO); - self.lastSnapshot = [self.fb_query fb_uniqueSnapshotWithError:&error]; - if (nil == self.lastSnapshot) { - NSString *hintText = @"Make sure the application UI has the expected state"; - if (nil != error - && [error.localizedDescription containsString:@"Identity Binding"]) { - hintText = [NSString stringWithFormat:@"%@. You could also try to switch the binding strategy using the 'boundElementsByIndex' setting for the element lookup", hintText]; - } - NSString *reason = [NSString stringWithFormat:@"The previously found element \"%@\" is not present in the current view anymore. %@", self.description, hintText]; - if (nil != error) { - reason = [NSString stringWithFormat:@"%@. Original error: %@", reason, error.localizedDescription]; + __block id snapshot = nil; + @autoreleasepool { + NSError *error = nil; + snapshot = isCustom + ? [self.fb_query fb_uniqueSnapshotWithError:&error] + : (id)[self snapshotWithError:&error]; + if (nil == snapshot) { + [self fb_raiseStaleElementExceptionWithError:error]; } - @throw [NSException exceptionWithName:FBStaleElementException reason:reason userInfo:@{}]; } + self.lastSnapshot = snapshot; return self.lastSnapshot; } -- (id)fb_cachedSnapshot +- (id)fb_standardSnapshot { - return [self.query fb_cachedSnapshot]; + return [self fb_takeSnapshot:NO]; } -- (nullable id)fb_snapshotWithAllAttributesAndMaxDepth:(NSNumber *)maxDepth +- (id)fb_customSnapshot { - NSMutableArray *allNames = [NSMutableArray arrayWithArray:FBStandardAttributeNames()]; - [allNames addObjectsFromArray:FBCustomAttributeNames()]; - return [self fb_snapshotWithAttributes:allNames.copy - maxDepth:maxDepth]; + return [self fb_takeSnapshot:YES]; } -- (nullable id)fb_snapshotWithAttributes:(NSArray *)attributeNames - maxDepth:(NSNumber *)maxDepth +- (id)fb_nativeSnapshot { - NSSet *standardAttributes = [NSSet setWithArray:FBStandardAttributeNames()]; - id snapshot = self.fb_takeSnapshot; - NSTimeInterval axTimeout = FBConfiguration.customSnapshotTimeout; - if (nil == attributeNames - || [[NSSet setWithArray:attributeNames] isSubsetOfSet:standardAttributes] - || axTimeout < DBL_EPSILON) { - // return the "normal" element snapshot if no custom attributes are requested - return snapshot; - } - - id axElement = snapshot.accessibilityElement; - if (nil == axElement) { - return nil; - } - - NSError *setTimeoutError; - BOOL isTimeoutSet = [FBXCAXClientProxy.sharedClient setAXTimeout:axTimeout - error:&setTimeoutError]; - if (!isTimeoutSet) { - [FBLogger logFmt:@"Cannot set snapshoting timeout to %.1fs. Original error: %@", - axTimeout, setTimeoutError.localizedDescription]; - } - - NSError *error; - id snapshotWithAttributes = [FBXCAXClientProxy.sharedClient snapshotForElement:axElement - attributes:attributeNames - maxDepth:maxDepth - error:&error]; - if (nil == snapshotWithAttributes) { - NSString *description = [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_description; - [FBLogger logFmt:@"Cannot take a snapshot with attribute(s) %@ of '%@' after %.2f seconds", - attributeNames, description, axTimeout]; - [FBLogger logFmt:@"This timeout could be customized via '%@' setting", FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT]; - [FBLogger logFmt:@"Internal error: %@", error.localizedDescription]; - [FBLogger logFmt:@"Falling back to the default snapshotting mechanism for the element '%@' (some attribute values, like visibility or accessibility might not be precise though)", description]; - snapshotWithAttributes = self.lastSnapshot; - } else { - self.lastSnapshot = snapshotWithAttributes; + NSError *error = nil; + BOOL isSuccessful = [self resolveOrRaiseTestFailure:NO error:&error]; + if (nil == self.lastSnapshot || !isSuccessful) { + [self fb_raiseStaleElementExceptionWithError:error]; } + return self.lastSnapshot; +} - if (isTimeoutSet) { - [FBXCAXClientProxy.sharedClient setAXTimeout:DEFAULT_AX_TIMEOUT error:nil]; - } - return snapshotWithAttributes; +- (id)fb_cachedSnapshot +{ + return [self.query fb_cachedSnapshot]; } - (NSArray *)fb_filterDescendantsWithSnapshots:(NSArray> *)snapshots - selfUID:(NSString *)selfUID onlyChildren:(BOOL)onlyChildren { if (0 == snapshots.count) { @@ -134,21 +92,19 @@ @implementation XCUIElement (FBUtilities) } NSMutableArray *matchedIds = [NSMutableArray new]; for (id snapshot in snapshots) { - NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; - if (nil != uid) { - [matchedIds addObject:uid]; + @autoreleasepool { + NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; + if (nil != uid) { + [matchedIds addObject:uid]; + } } } NSMutableArray *matchedElements = [NSMutableArray array]; - NSString *uid = selfUID; - if (nil == uid) { - uid = self.fb_isResolvedFromCache.boolValue - ? [FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot] - : self.fb_uid; - } + NSString *uid = nil == self.lastSnapshot + ? self.fb_uid + : [FBXCElementSnapshotWrapper wdUIDWithSnapshot:self.lastSnapshot]; if (nil != uid && [matchedIds containsObject:uid]) { - XCUIElement *stableSelf = self.fb_stableInstance; - stableSelf.fb_isResolvedNatively = @NO; + XCUIElement *stableSelf = [self fb_stableInstanceWithUid:uid]; if (1 == snapshots.count) { return @[stableSelf]; } @@ -157,7 +113,7 @@ @implementation XCUIElement (FBUtilities) XCUIElementType type = XCUIElementTypeAny; NSArray *uniqueTypes = [snapshots valueForKeyPath:[NSString stringWithFormat:@"@distinctUnionOfObjects.%@", FBStringify(XCUIElement, elementType)]]; if (uniqueTypes && [uniqueTypes count] == 1) { - type = [uniqueTypes.firstObject intValue]; + type = (XCUIElementType)[uniqueTypes.firstObject intValue]; } XCUIElementQuery *query = onlyChildren ? [self.fb_query childrenMatchingType:type] @@ -190,11 +146,25 @@ - (void)fb_waitUntilStableWithTimeout:(NSTimeInterval)timeout self.application.fb_shouldWaitForQuiescence = YES; } [[[self.application applicationImpl] currentProcess] - waitForQuiescenceIncludingAnimationsIdle:YES]; + fb_waitForQuiescenceIncludingAnimationsIdle:YES]; if (previousQuiescence != self.application.fb_shouldWaitForQuiescence) { self.application.fb_shouldWaitForQuiescence = previousQuiescence; } FBConfiguration.waitForIdleTimeout = previousTimeout; } +- (void)fb_raiseStaleElementExceptionWithError:(NSError *)error __attribute__((noreturn)) +{ + NSString *hintText = @"Make sure the application UI has the expected state"; + if (nil != error && [error.localizedDescription containsString:@"Identity Binding"]) { + hintText = [NSString stringWithFormat:@"%@. You could also try to switch the binding strategy using the 'boundElementsByIndex' setting for the element lookup", hintText]; + } + NSString *reason = [NSString stringWithFormat:@"The previously found element \"%@\" is not present in the current view anymore. %@", + self.description, hintText]; + if (nil != error) { + reason = [NSString stringWithFormat:@"%@. Original error: %@", reason, error.localizedDescription]; + } + @throw [NSException exceptionWithName:FBStaleElementException reason:reason userInfo:@{}]; +} + @end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h new file mode 100644 index 000000000..e2355c995 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.h @@ -0,0 +1,35 @@ +/** + * 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 "FBXCElementSnapshotWrapper.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface XCUIElement (FBVisibleFrame) + +/** + Returns the snapshot visibleFrame with a fallback to direct attribute retrieval from FBXCAXClient in case of a snapshot fault (nil visibleFrame) + + @return the snapshot visibleFrame + */ +- (CGRect)fb_visibleFrame; + +@end + +@interface FBXCElementSnapshotWrapper (FBVisibleFrame) + +/** + Returns the snapshot visibleFrame with a fallback to direct attribute retrieval from FBXCAXClient in case of a snapshot fault (nil visibleFrame) + + @return the snapshot visibleFrame + */ +- (CGRect)fb_visibleFrame; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m new file mode 100644 index 000000000..09ce6a769 --- /dev/null +++ b/WebDriverAgentLib/Categories/XCUIElement+FBVisibleFrame.m @@ -0,0 +1,61 @@ +/** + * 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 "XCUIElement+FBVisibleFrame.h" +#import "FBElementUtils.h" +#import "FBXCodeCompatibility.h" +#import "FBXCElementSnapshotWrapper+Helpers.h" +#import "XCUIElement+FBUtilities.h" +#import "XCTestPrivateSymbols.h" + +@implementation XCUIElement (FBVisibleFrame) + +- (CGRect)fb_visibleFrame +{ + id snapshot = [self fb_standardSnapshot]; + return [FBXCElementSnapshotWrapper ensureWrapped:snapshot].fb_visibleFrame; +} + +@end + +@implementation FBXCElementSnapshotWrapper (FBVisibleFrame) + +- (CGRect)fb_visibleFrame +{ + CGRect thisVisibleFrame = [self visibleFrame]; + if (!CGRectIsEmpty(thisVisibleFrame)) { + return CGRectMake(CGRectGetMinX(thisVisibleFrame), + CGRectGetMinY(thisVisibleFrame), + CGRectGetWidth(thisVisibleFrame), + CGRectGetHeight(thisVisibleFrame)); + } + + NSDictionary *visibleFrameDict = [self fb_attributeValue:FB_XCAXAVisibleFrameAttributeName + error:nil]; + if (nil == visibleFrameDict) { + return CGRectMake(CGRectGetMinX(thisVisibleFrame), + CGRectGetMinY(thisVisibleFrame), + CGRectGetWidth(thisVisibleFrame), + CGRectGetHeight(thisVisibleFrame)); + } + + id x = [visibleFrameDict objectForKey:@"X"]; + id y = [visibleFrameDict objectForKey:@"Y"]; + id height = [visibleFrameDict objectForKey:@"Height"]; + id width = [visibleFrameDict objectForKey:@"Width"]; + if (x != nil && y != nil && height != nil && width != nil) { + return CGRectMake([x doubleValue], [y doubleValue], [width doubleValue], [height doubleValue]); + } + + return CGRectMake(CGRectGetMinX(thisVisibleFrame), + CGRectGetMinY(thisVisibleFrame), + CGRectGetWidth(thisVisibleFrame), + CGRectGetHeight(thisVisibleFrame)); +} + +@end diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h index fdeedaf91..8e598d7a6 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h +++ b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.h @@ -3,13 +3,12 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import #import -#import "FBXCElementSnapshotWrapper.h" +#import NS_ASSUME_NONNULL_BEGIN diff --git a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m index b29c5a8b2..a80db0218 100644 --- a/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m +++ b/WebDriverAgentLib/Categories/XCUIElement+FBWebDriverAttributes.m @@ -3,13 +3,14 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElement+FBWebDriverAttributes.h" +#import "FBConfiguration.h" #import "FBElementTypeTransformer.h" +#import "FBElementHelpers.h" #import "FBLogger.h" #import "FBMacros.h" #import "FBXCElementSnapshotWrapper.h" @@ -21,6 +22,9 @@ #import "FBElementUtils.h" #import "XCTestPrivateSymbols.h" #import "XCUIHitPointResult.h" +#import "FBAccessibilityTraits.h" +#import "XCUIElement+FBMinMax.h" +#import "XCUIElement+FBCustomActions.h" #define BROKEN_RECT CGRectMake(-1, -1, 0, 0) @@ -28,24 +32,24 @@ @implementation XCUIElement (WebDriverAttributesForwarding) - (id)fb_snapshotForAttributeName:(NSString *)name { - // These attributes are special, because we can only retrieve them from - // the snapshot if we explicitly ask XCTest to include them into the query while taking it. - // That is why fb_snapshotWithAllAttributes method must be used instead of the default snapshot - // call - if ([name isEqualToString:FBStringify(XCUIElement, isWDVisible)]) { - return [self fb_snapshotWithAttributes:@[FB_XCAXAIsVisibleAttributeName] - maxDepth:@1]; + // https://github.com/appium/appium-xcuitest-driver/pull/2565 + if ([name isEqualToString:FBStringify(XCUIElement, isWDHittable)]) { + return [self fb_nativeSnapshot]; } - if ([name isEqualToString:FBStringify(XCUIElement, isWDAccessible)]) { - return [self fb_snapshotWithAttributes:@[FB_XCAXAIsElementAttributeName] - maxDepth:@1]; + // https://github.com/appium/WebDriverAgent/issues/1085 + if (FBConfiguration.enforceCustomSnapshots) { + return [self fb_customSnapshot]; } - if ([name isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)]) { - return [self fb_snapshotWithAttributes:@[FB_XCAXAIsElementAttributeName] - maxDepth:nil]; + // https://github.com/appium/appium-xcuitest-driver/issues/2552 + BOOL isValueRequest = [name isEqualToString:FBStringify(XCUIElement, wdValue)]; + if ([self isKindOfClass:XCUIApplication.class] && !isValueRequest) { + return [self fb_standardSnapshot]; } - - return self.fb_takeSnapshot; + BOOL isCustomSnapshot = [name isEqualToString:FBStringify(XCUIElement, isWDAccessible)] + || [name isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)] + || [name isEqualToString:FBStringify(XCUIElement, wdIndex)] + || isValueRequest; + return isCustomSnapshot ? [self fb_customSnapshot] : [self fb_standardSnapshot]; } - (id)fb_valueForWDAttributeName:(NSString *)name @@ -78,6 +82,16 @@ - (id)fb_valueForWDAttributeName:(NSString *)name return [self valueForKey:[FBElementUtils wdAttributeNameForAttributeName:name]]; } +- (NSNumber *)wdMinValue +{ + return self.fb_minValue; +} + +- (NSNumber *)wdMaxValue +{ + return self.fb_maxValue; +} + - (NSString *)wdValue { id value = self.value; @@ -90,9 +104,7 @@ - (NSString *)wdValue value = FBFirstNonEmptyValue(value, isSelected); } else if (elementType == XCUIElementTypeSwitch) { value = @([value boolValue]); - } else if (elementType == XCUIElementTypeTextView || - elementType == XCUIElementTypeTextField || - elementType == XCUIElementTypeSecureTextField) { + } else if (FBDoesElementSupportInnerText(elementType)) { NSString *placeholderValue = self.placeholderValue; value = FBFirstNonEmptyValue(value, placeholderValue); } @@ -120,12 +132,18 @@ - (NSString *)wdName - (NSString *)wdLabel { - NSString *label = self.label; XCUIElementType elementType = self.elementType; - if (elementType == XCUIElementTypeTextField || elementType == XCUIElementTypeSecureTextField ) { - return label; - } - return FBTransferEmptyStringToNil(label); + return (elementType == XCUIElementTypeTextField + || elementType == XCUIElementTypeSecureTextField) + ? self.label + : FBTransferEmptyStringToNil(self.label); +} + +- (NSString *)wdPlaceholderValue +{ + return FBDoesElementSupportInnerText(self.elementType) + ? self.placeholderValue + : FBTransferEmptyStringToNil(self.placeholderValue); } - (NSString *)wdType @@ -150,6 +168,30 @@ - (CGRect)wdFrame : CGRectIntegral(frame); } +- (CGRect)wdNativeFrame +{ + // To avoid confusion regarding the frame returned by `wdFrame`, + // the current property is provided to represent the element's + // actual rendered frame. + return self.frame; +} + +/** + Returns a comma-separated string of accessibility traits for the element. + This method converts the element's accessibility traits bitmask into human-readable strings + using FBAccessibilityTraitsToStringsArray. The traits represent various accessibility + characteristics of the element such as Button, Link, Image, etc. + You can find the list of possible traits in the Apple documentation: + https://developer.apple.com/documentation/uikit/uiaccessibilitytraits?language=objc + + @return A comma-separated string of accessibility traits, or an empty string if no traits are set + */ +- (NSString *)wdTraits +{ + NSArray *traits = FBAccessibilityTraitsToStringsArray(self.snapshot.traits); + return [traits componentsJoinedByString:@", "]; +} + - (BOOL)isWDVisible { return self.fb_isVisible; @@ -183,8 +225,8 @@ - (BOOL)isWDAccessible // In the scenario when table provides Search results controller, table could be marked as accessible element, even though it isn't // As it is highly unlikely that table view should ever be an accessibility element itself, // for now we work around that by skipping Table View in container checks - if ([FBXCElementSnapshotWrapper ensureWrapped:parentSnapshot].fb_isAccessibilityElement - && parentSnapshot.elementType != XCUIElementTypeTable) { + if (parentSnapshot.elementType != XCUIElementTypeTable + && [FBXCElementSnapshotWrapper ensureWrapped:parentSnapshot].fb_isAccessibilityElement) { return NO; } parentSnapshot = parentSnapshot.parent; @@ -244,4 +286,9 @@ - (NSDictionary *)wdRect }; } +- (NSString *)wdCustomActions +{ + return self.fb_customActions; +} + @end diff --git a/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h b/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h index 04ff8bfad..8c8289074 100644 --- a/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h +++ b/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m b/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m index 81e2a522e..d2438a0cb 100644 --- a/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m +++ b/WebDriverAgentLib/Categories/XCUIElementQuery+FBHelpers.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIElementQuery+FBHelpers.h" diff --git a/WebDriverAgentLib/Commands/FBAlertViewCommands.h b/WebDriverAgentLib/Commands/FBAlertViewCommands.h index 3d4c2638c..687c39ce0 100644 --- a/WebDriverAgentLib/Commands/FBAlertViewCommands.h +++ b/WebDriverAgentLib/Commands/FBAlertViewCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBAlertViewCommands.m b/WebDriverAgentLib/Commands/FBAlertViewCommands.m index dd195fbb8..50479936e 100644 --- a/WebDriverAgentLib/Commands/FBAlertViewCommands.m +++ b/WebDriverAgentLib/Commands/FBAlertViewCommands.m @@ -3,16 +3,15 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBAlertViewCommands.h" #import "FBAlert.h" -#import "FBApplication.h" #import "FBRouteRequest.h" #import "FBSession.h" +#import "XCUIApplication+FBHelpers.h" @implementation FBAlertViewCommands @@ -38,7 +37,7 @@ + (NSArray *)routes + (id)handleAlertGetTextCommand:(FBRouteRequest *)request { - FBApplication *application = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; NSString *alertText = [FBAlert alertWithApplication:application].text; if (!alertText) { return FBResponseWithStatus([FBCommandStatus noAlertOpenErrorWithMessage:nil @@ -73,7 +72,7 @@ + (NSArray *)routes + (id)handleAlertAcceptCommand:(FBRouteRequest *)request { - FBApplication *application = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; NSString *name = request.arguments[@"name"]; FBAlert *alert = [FBAlert alertWithApplication:application]; NSError *error; @@ -96,7 +95,7 @@ + (NSArray *)routes + (id)handleAlertDismissCommand:(FBRouteRequest *)request { - FBApplication *application = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; NSString *name = request.arguments[@"name"]; FBAlert *alert = [FBAlert alertWithApplication:application]; NSError *error; diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.h b/WebDriverAgentLib/Commands/FBCustomCommands.h index 869d14fd1..dca154229 100644 --- a/WebDriverAgentLib/Commands/FBCustomCommands.h +++ b/WebDriverAgentLib/Commands/FBCustomCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBCustomCommands.m b/WebDriverAgentLib/Commands/FBCustomCommands.m index 5b1ce72ae..b1490d921 100644 --- a/WebDriverAgentLib/Commands/FBCustomCommands.m +++ b/WebDriverAgentLib/Commands/FBCustomCommands.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBCustomCommands.h" @@ -12,10 +11,10 @@ #import #import -#import "FBApplication.h" #import "FBConfiguration.h" #import "FBKeyboard.h" #import "FBNotificationsHelper.h" +#import "FBMathUtils.h" #import "FBPasteboard.h" #import "FBResponsePayload.h" #import "FBRoute.h" @@ -24,6 +23,7 @@ #import "FBScreen.h" #import "FBSession.h" #import "FBXCodeCompatibility.h" +#import "XCUIApplication.h" #import "XCUIApplication+FBHelpers.h" #import "XCUIDevice+FBHelpers.h" #import "XCUIElement.h" @@ -48,6 +48,7 @@ + (NSArray *)routes [[FBRoute GET:@"/wda/locked"].withoutSession respondWithTarget:self action:@selector(handleIsLocked:)], [[FBRoute GET:@"/wda/locked"] respondWithTarget:self action:@selector(handleIsLocked:)], [[FBRoute GET:@"/wda/screen"] respondWithTarget:self action:@selector(handleGetScreen:)], + [[FBRoute GET:@"/wda/screen"].withoutSession respondWithTarget:self action:@selector(handleGetScreen:)], [[FBRoute GET:@"/wda/activeAppInfo"] respondWithTarget:self action:@selector(handleActiveAppInfo:)], [[FBRoute GET:@"/wda/activeAppInfo"].withoutSession respondWithTarget:self action:@selector(handleActiveAppInfo:)], #if !TARGET_OS_TV // tvOS does not provide relevant APIs @@ -134,10 +135,22 @@ + (NSArray *)routes + (id)handleGetScreen:(FBRouteRequest *)request { - FBSession *session = request.session; - CGSize statusBarSize = [FBScreen statusBarSizeForApplication:session.activeApplication]; + XCUIApplication *app = XCUIApplication.fb_systemApplication; + + XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject; + CGSize statusBarSize = (nil == mainStatusBar) ? CGSizeZero : mainStatusBar.frame.size; + +#if TARGET_OS_TV + CGSize screenSize = app.frame.size; +#else + CGSize screenSize = FBAdjustDimensionsForApplication(app.wdFrame.size, app.interfaceOrientation); +#endif + return FBResponseWithObject( @{ + @"screenSize":@{@"width": @(screenSize.width), + @"height": @(screenSize.height) + }, @"statusBarSize": @{@"width": @(statusBarSize.width), @"height": @(statusBarSize.height), }, @@ -171,7 +184,7 @@ + (NSArray *)routes + (id)handleActiveAppInfo:(FBRouteRequest *)request { - XCUIApplication *app = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; return FBResponseWithObject(@{ @"pid": @(app.processID), @"bundleId": app.bundleID, @@ -240,7 +253,7 @@ + (NSDictionary *)processArguments:(XCUIApplication *)app if (nil == result) { return FBResponseWithUnknownError(error); } - return FBResponseWithObject([result base64EncodedStringWithOptions:0]); + return FBResponseWithObject([result base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]); } + (id)handleGetBatteryInfo:(FBRouteRequest *)request @@ -553,7 +566,8 @@ + (NSString *)timeZone FBElementCache *elementCache = request.session.elementCache; BOOL hasElement = ![request.parameters[@"uuid"] isEqual:@"0"]; XCUIElement *destination = hasElement - ? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]] + ? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES] : request.session.activeApplication; id keys = request.arguments[@"keys"]; @@ -585,7 +599,7 @@ + (NSString *)timeZone modifierFlags = [(NSNumber *)modifiers unsignedIntValue]; } NSString *keyValue = [FBKeyboard keyValueForName:item] ?: key; - [destination typeKey:keyValue modifierFlags:modifierFlags]; + [destination typeKey:keyValue modifierFlags:(XCUIKeyModifierFlags)modifierFlags]; } else { NSString *message = @"All items of the 'keys' array must be either dictionaries or strings"; return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message diff --git a/WebDriverAgentLib/Commands/FBDebugCommands.h b/WebDriverAgentLib/Commands/FBDebugCommands.h index 32f28b1b2..99728e03a 100644 --- a/WebDriverAgentLib/Commands/FBDebugCommands.h +++ b/WebDriverAgentLib/Commands/FBDebugCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBDebugCommands.m b/WebDriverAgentLib/Commands/FBDebugCommands.m index d290ebe6c..42ea74b3d 100644 --- a/WebDriverAgentLib/Commands/FBDebugCommands.m +++ b/WebDriverAgentLib/Commands/FBDebugCommands.m @@ -3,13 +3,11 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBDebugCommands.h" -#import "FBApplication.h" #import "FBRouteRequest.h" #import "FBSession.h" #import "FBXMLGenerationOptions.h" @@ -42,7 +40,7 @@ + (NSArray *)routes + (id)handleGetSourceCommand:(FBRouteRequest *)request { // This method might be called without session - FBApplication *application = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; NSString *sourceType = request.parameters[@"format"] ?: SOURCE_FORMAT_XML; NSString *sourceScope = request.parameters[@"scope"]; id result; @@ -55,7 +53,12 @@ + (NSArray *)routes withExcludedAttributes:excludedAttributes] withScope:sourceScope]]; } else if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_JSON] == NSOrderedSame) { - result = application.fb_tree; + NSString *excludedAttributesString = request.parameters[@"excluded_attributes"]; + NSSet *excludedAttributes = (excludedAttributesString == nil) + ? nil + : [NSSet setWithArray:[excludedAttributesString componentsSeparatedByString:@","]]; + + result = [application fb_tree:excludedAttributes]; } else if ([sourceType caseInsensitiveCompare:SOURCE_FORMAT_DESCRIPTION] == NSOrderedSame) { result = application.fb_descriptionRepresentation; } else { @@ -71,7 +74,7 @@ + (NSArray *)routes + (id)handleGetAccessibleSourceCommand:(FBRouteRequest *)request { // This method might be called without session - FBApplication *application = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; return FBResponseWithObject(application.fb_accessibilityTree ?: @{}); } diff --git a/WebDriverAgentLib/Commands/FBElementCommands.h b/WebDriverAgentLib/Commands/FBElementCommands.h index fcd1c86ea..3c07ee6d6 100644 --- a/WebDriverAgentLib/Commands/FBElementCommands.h +++ b/WebDriverAgentLib/Commands/FBElementCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBElementCommands.m b/WebDriverAgentLib/Commands/FBElementCommands.m index 0f163521b..3efaf9ab2 100644 --- a/WebDriverAgentLib/Commands/FBElementCommands.m +++ b/WebDriverAgentLib/Commands/FBElementCommands.m @@ -3,13 +3,11 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBElementCommands.h" -#import "FBApplication.h" #import "FBConfiguration.h" #import "FBKeyboard.h" #import "FBRoute.h" @@ -18,7 +16,6 @@ #import "FBElementCache.h" #import "FBErrorBuilder.h" #import "FBSession.h" -#import "FBApplication.h" #import "FBElementUtils.h" #import "FBMacros.h" #import "FBMathUtils.h" @@ -37,6 +34,7 @@ #import "XCUIElement+FBWebDriverAttributes.h" #import "XCUIElement+FBTVFocuse.h" #import "XCUIElement+FBResolve.h" +#import "XCUIElement+FBUID.h" #import "FBElementTypeTransformer.h" #import "XCUIElement.h" #import "XCUIElementQuery.h" @@ -54,6 +52,7 @@ + (NSArray *)routes return @[ [[FBRoute GET:@"/window/size"] respondWithTarget:self action:@selector(handleGetWindowSize:)], + [[FBRoute GET:@"/window/rect"] respondWithTarget:self action:@selector(handleGetWindowRect:)], [[FBRoute GET:@"/window/size"].withoutSession respondWithTarget:self action:@selector(handleGetWindowSize:)], [[FBRoute GET:@"/element/:uuid/enabled"] respondWithTarget:self action:@selector(handleGetEnabled:)], [[FBRoute GET:@"/element/:uuid/rect"] respondWithTarget:self action:@selector(handleGetRect:)], @@ -76,24 +75,46 @@ + (NSArray *)routes [[FBRoute POST:@"/wda/element/:uuid/focuse"] respondWithTarget:self action:@selector(handleFocuse:)], #else [[FBRoute POST:@"/wda/element/:uuid/swipe"] respondWithTarget:self action:@selector(handleSwipe:)], + [[FBRoute POST:@"/wda/swipe"] respondWithTarget:self action:@selector(handleSwipe:)], + [[FBRoute POST:@"/wda/element/:uuid/pinch"] respondWithTarget:self action:@selector(handlePinch:)], + [[FBRoute POST:@"/wda/pinch"] respondWithTarget:self action:@selector(handlePinch:)], + [[FBRoute POST:@"/wda/element/:uuid/rotate"] respondWithTarget:self action:@selector(handleRotate:)], + [[FBRoute POST:@"/wda/rotate"] respondWithTarget:self action:@selector(handleRotate:)], + [[FBRoute POST:@"/wda/element/:uuid/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)], + [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTap:)], + [[FBRoute POST:@"/wda/element/:uuid/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)], - [[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self action:@selector(handleTapWithNumberOfTaps:)], + [[FBRoute POST:@"/wda/twoFingerTap"] respondWithTarget:self action:@selector(handleTwoFingerTap:)], + + [[FBRoute POST:@"/wda/element/:uuid/tapWithNumberOfTaps"] respondWithTarget:self + action:@selector(handleTapWithNumberOfTaps:)], + [[FBRoute POST:@"/wda/tapWithNumberOfTaps"] respondWithTarget:self + action:@selector(handleTapWithNumberOfTaps:)], + [[FBRoute POST:@"/wda/element/:uuid/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)], + [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHold:)], + [[FBRoute POST:@"/wda/element/:uuid/scroll"] respondWithTarget:self action:@selector(handleScroll:)], + [[FBRoute POST:@"/wda/scroll"] respondWithTarget:self action:@selector(handleScroll:)], + [[FBRoute POST:@"/wda/element/:uuid/scrollTo"] respondWithTarget:self action:@selector(handleScrollTo:)], + [[FBRoute POST:@"/wda/element/:uuid/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)], + [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDrag:)], + [[FBRoute POST:@"/wda/element/:uuid/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragWithVelocity:)], - [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)], - [[FBRoute POST:@"/wda/dragfromtoforduration"] respondWithTarget:self action:@selector(handleDragCoordinate:)], [[FBRoute POST:@"/wda/pressAndDragWithVelocity"] respondWithTarget:self action:@selector(handlePressAndDragCoordinateWithVelocity:)], - [[FBRoute POST:@"/wda/tap/:uuid"] respondWithTarget:self action:@selector(handleTap:)], - [[FBRoute POST:@"/wda/touchAndHold"] respondWithTarget:self action:@selector(handleTouchAndHoldCoordinate:)], - [[FBRoute POST:@"/wda/doubleTap"] respondWithTarget:self action:@selector(handleDoubleTapCoordinate:)], - [[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)], + + [[FBRoute POST:@"/wda/element/:uuid/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)], [[FBRoute POST:@"/wda/forceTouch"] respondWithTarget:self action:@selector(handleForceTouch:)], + + [[FBRoute POST:@"/wda/element/:uuid/tap"] respondWithTarget:self action:@selector(handleTap:)], + [[FBRoute POST:@"/wda/tap"] respondWithTarget:self action:@selector(handleTap:)], + + [[FBRoute POST:@"/wda/pickerwheel/:uuid/select"] respondWithTarget:self action:@selector(handleWheelSelect:)], #endif [[FBRoute POST:@"/wda/keys"] respondWithTarget:self action:@selector(handleKeys:)], ]; @@ -106,38 +127,22 @@ + (NSArray *)routes { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - BOOL isEnabled = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDEnabled; - return FBResponseWithObject(isEnabled ? @YES : @NO); + return FBResponseWithObject(@(element.isWDEnabled)); } + (id)handleGetRect:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - return FBResponseWithObject([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdRect); + return FBResponseWithObject(element.wdRect); } + (id)handleGetAttribute:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; NSString *attributeName = request.parameters[@"name"]; - NSString *wdAttributeName = [FBElementUtils wdAttributeNameForAttributeName:attributeName]; - NSArray *additionalAttributes = nil; - NSNumber *maxDepth = nil; - if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDVisible)]) { - additionalAttributes = @[FB_XCAXAIsVisibleAttributeName]; - maxDepth = @1; - } else if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDEnabled)]) { - additionalAttributes = @[FB_XCAXAIsElementAttributeName]; - maxDepth = @1; - } else if ([wdAttributeName isEqualToString:FBStringify(XCUIElement, isWDAccessibilityContainer)]) { - additionalAttributes = @[FB_XCAXAIsElementAttributeName]; - } - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:additionalAttributes - andMaxDepth:maxDepth]; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot]; - id attributeValue = [wrappedSnapshot fb_valueForWDAttributeName:attributeName]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + id attributeValue = [element fb_valueForWDAttributeName:attributeName]; return FBResponseWithObject(attributeValue ?: [NSNull null]); } @@ -145,7 +150,9 @@ + (NSArray *)routes { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot]; + // https://github.com/appium/appium-xcuitest-driver/issues/2552 + id snapshot = [element fb_customSnapshot]; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; id text = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel); return FBResponseWithObject(text ?: @""); } @@ -153,48 +160,43 @@ + (NSArray *)routes + (id)handleGetDisplayed:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:@[FB_XCAXAIsVisibleAttributeName] - andMaxDepth:@1]; - return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDVisible)); + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDVisible)); } + (id)handleGetAccessible:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:@[FB_XCAXAIsElementAttributeName] - andMaxDepth:@1]; - return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDAccessible)); + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDAccessible)); } + (id)handleGetIsAccessibilityContainer:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:@[FB_XCAXAIsElementAttributeName] - andMaxDepth:nil]; - return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].isWDAccessibilityContainer)); + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + return FBResponseWithObject(@(element.isWDAccessibilityContainer)); } + (id)handleGetName:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - return FBResponseWithObject([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdType); + return FBResponseWithObject(element.wdType); } + (id)handleGetSelected:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - return FBResponseWithObject(@([FBXCElementSnapshotWrapper ensureWrapped:element.lastSnapshot].wdSelected)); + return FBResponseWithObject(@(element.wdSelected)); } + (id)handleSetValue:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES]; id value = request.arguments[@"value"] ?: request.arguments[@"text"]; if (!value) { return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Neither 'value' nor 'text' parameter is provided" traceback:nil]); @@ -202,7 +204,7 @@ + (NSArray *)routes NSString *textToType = [value isKindOfClass:NSArray.class] ? [value componentsJoinedByString:@""] : value; - XCUIElementType elementType = [(id)element.lastSnapshot elementType]; + XCUIElementType elementType = [element elementType]; #if !TARGET_OS_TV if (elementType == XCUIElementTypePickerWheel) { [element adjustToPickerWheelValue:textToType]; @@ -231,7 +233,7 @@ + (NSArray *)routes + (id)handleClick:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] checkStaleness:YES]; #if TARGET_OS_IOS [element tap]; #elif TARGET_OS_TV @@ -265,7 +267,10 @@ + (NSArray *)routes if (focusedElement != nil) { FBElementCache *elementCache = request.session.elementCache; BOOL useNativeCachingStrategy = request.session.useNativeCachingStrategy; - NSString *focusedUUID = [elementCache storeElement:(useNativeCachingStrategy ? focusedElement : focusedElement.fb_stableInstance)]; + NSString *focusedUUID = [elementCache storeElement:(useNativeCachingStrategy + ? focusedElement + : [focusedElement fb_stableInstanceWithUid:focusedElement.fb_uid])]; + focusedElement.lastSnapshot = nil; if (focusedUUID && [focusedUUID isEqualToString:(id)request.parameters[@"uuid"]]) { isFocused = YES; } @@ -287,38 +292,30 @@ + (NSArray *)routes #else + (id)handleDoubleTap:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - [element doubleTap]; - return FBResponseWithOK(); -} - -+ (id)handleDoubleTapCoordinate:(FBRouteRequest *)request -{ - CGVector offset = CGVectorMake([request.arguments[@"x"] doubleValue], - [request.arguments[@"y"] doubleValue]); - XCUICoordinate *doubleTapCoordinate = [self.class gestureCoordinateWithOffset:offset - element:request.session.activeApplication]; - [doubleTapCoordinate doubleTap]; + NSError *error; + id target = [self targetWithXyCoordinatesFromRequest:request error:&error]; + if (nil == target) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); + } + [target doubleTap]; return FBResponseWithOK(); } + (id)handleTwoFingerTap:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [self targetFromRequest:request]; [element twoFingerTap]; return FBResponseWithOK(); } + (id)handleTapWithNumberOfTaps:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; if (nil == request.arguments[@"numberOfTaps"] || nil == request.arguments[@"numberOfTouches"]) { return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Both 'numberOfTaps' and 'numberOfTouches' arguments must be provided" traceback:nil]); } - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [self targetFromRequest:request]; [element tapWithNumberOfTaps:[request.arguments[@"numberOfTaps"] integerValue] numberOfTouches:[request.arguments[@"numberOfTouches"] integerValue]]; return FBResponseWithOK(); @@ -326,32 +323,22 @@ + (NSArray *)routes + (id)handleTouchAndHold:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - [element pressForDuration:[request.arguments[@"duration"] doubleValue]]; - return FBResponseWithOK(); -} - -+ (id)handleTouchAndHoldCoordinate:(FBRouteRequest *)request -{ - CGVector offset = CGVectorMake([request.arguments[@"x"] doubleValue], - [request.arguments[@"y"] doubleValue]); - XCUICoordinate *pressCoordinate = [self.class gestureCoordinateWithOffset:offset - element:request.session.activeApplication]; - [pressCoordinate pressForDuration:[request.arguments[@"duration"] doubleValue]]; + NSError *error; + id target = [self targetWithXyCoordinatesFromRequest:request error:&error]; + if (nil == target) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); + } + [target pressForDuration:[request.arguments[@"duration"] doubleValue]]; return FBResponseWithOK(); } + (id)handlePressAndDragWithVelocity:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - if (![element respondsToSelector:@selector(pressForDuration:thenDragToElement:withVelocity:thenHoldForDuration:)]) { - return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"This method is only supported in Xcode 12 and above" - traceback:nil]); - } + XCUIElement *element = [self targetFromRequest:request]; [element pressForDuration:[request.arguments[@"pressDuration"] doubleValue] - thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"]] + thenDragToElement:[elementCache elementForUUID:(NSString *)request.arguments[@"toElement"] checkStaleness:YES] withVelocity:[request.arguments[@"velocity"] doubleValue] thenHoldForDuration:[request.arguments[@"holdDuration"] doubleValue]]; return FBResponseWithOK(); @@ -364,10 +351,6 @@ + (NSArray *)routes (CGFloat)[request.arguments[@"fromY"] doubleValue]); XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:session.activeApplication]; - if (![startCoordinate respondsToSelector:@selector(pressForDuration:thenDragToCoordinate:withVelocity:thenHoldForDuration:)]) { - return FBResponseWithStatus([FBCommandStatus unsupportedOperationErrorWithMessage:@"This method is only supported in Xcode 12 and above" - traceback:nil]); - } CGVector endOffset = CGVectorMake((CGFloat)[request.arguments[@"toX"] doubleValue], (CGFloat)[request.arguments[@"toY"] doubleValue]); XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset @@ -381,8 +364,7 @@ + (NSArray *)routes + (id)handleScroll:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [self targetFromRequest:request]; // Using presence of arguments as a way to convey control flow seems like a pretty bad idea but it's // what ios-driver did and sadly, we must copy them. NSString *const name = request.arguments[@"name"]; @@ -442,33 +424,15 @@ + (NSArray *)routes traceback:nil]); } -+ (id)handleDragCoordinate:(FBRouteRequest *)request -{ - FBSession *session = request.session; - CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue], - [request.arguments[@"fromY"] doubleValue]); - XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset - element:session.activeApplication]; - CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue], - [request.arguments[@"toY"] doubleValue]); - XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset - element:session.activeApplication]; - NSTimeInterval duration = [request.arguments[@"duration"] doubleValue]; - [startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate]; - return FBResponseWithOK(); -} - + (id)handleDrag:(FBRouteRequest *)request { - FBSession *session = request.session; - FBElementCache *elementCache = session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *target = [self targetFromRequest:request]; CGVector startOffset = CGVectorMake([request.arguments[@"fromX"] doubleValue], [request.arguments[@"fromY"] doubleValue]); - XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:element]; + XCUICoordinate *startCoordinate = [self.class gestureCoordinateWithOffset:startOffset element:target]; CGVector endOffset = CGVectorMake([request.arguments[@"toX"] doubleValue], [request.arguments[@"toY"] doubleValue]); - XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset element:element]; + XCUICoordinate *endCoordinate = [self.class gestureCoordinateWithOffset:endOffset element:target]; NSTimeInterval duration = [request.arguments[@"duration"] doubleValue]; [startCoordinate pressForDuration:duration thenDragToCoordinate:endCoordinate]; return FBResponseWithOK(); @@ -476,39 +440,41 @@ + (NSArray *)routes + (id)handleSwipe:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; NSString *const direction = request.arguments[@"direction"]; if (!direction) { return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"Missing 'direction' parameter" traceback:nil]); } NSArray *supportedDirections = @[@"up", @"down", @"left", @"right"]; if (![supportedDirections containsObject:direction.lowercaseString]) { - return FBResponseWithStatus([FBCommandStatus - invalidArgumentErrorWithMessage:[NSString stringWithFormat: @"Unsupported swipe direction '%@'. Only the following directions are supported: %@", direction, supportedDirections] - traceback:nil]); + NSString *message = [NSString stringWithFormat:@"Unsupported swipe direction '%@'. Only the following directions are supported: %@", direction, supportedDirections]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:message + traceback:nil]); + } + NSError *error; + id target = [self targetWithXyCoordinatesFromRequest:request error:&error]; + if (nil == target) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); } - [element fb_swipeWithDirection:direction.lowercaseString velocity:request.arguments[@"velocity"]]; + [target fb_swipeWithDirection:direction velocity:request.arguments[@"velocity"]]; return FBResponseWithOK(); } + (id)handleTap:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - CGVector offset = CGVectorMake([request.arguments[@"x"] doubleValue], - [request.arguments[@"y"] doubleValue]); - XCUIElement *element = [elementCache hasElementWithUUID:request.parameters[@"uuid"]] - ? [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]] - : request.session.activeApplication; - XCUICoordinate *tapCoordinate = [self.class gestureCoordinateWithOffset:offset element:element]; - [tapCoordinate tap]; + NSError *error; + id target = [self targetWithXyCoordinatesFromRequest:request error:&error]; + if (nil == target) { + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); + } + [target tap]; return FBResponseWithOK(); } + (id)handlePinch:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [self targetFromRequest:request]; CGFloat scale = (CGFloat)[request.arguments[@"scale"] doubleValue]; CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue]; [element pinchWithScale:scale velocity:velocity]; @@ -517,8 +483,7 @@ + (NSArray *)routes + (id)handleRotate:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [self targetFromRequest:request]; CGFloat rotation = (CGFloat)[request.arguments[@"rotation"] doubleValue]; CGFloat velocity = (CGFloat)[request.arguments[@"velocity"] doubleValue]; [element rotate:rotation withVelocity:velocity]; @@ -527,13 +492,7 @@ + (NSArray *)routes + (id)handleForceTouch:(FBRouteRequest *)request { - XCUIElement *element = nil; - if (nil == request.parameters[@"uuid"]) { - element = [FBApplication fb_activeApplication]; - } else { - FBElementCache *elementCache = request.session.elementCache; - element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - } + XCUIElement *element = [self targetFromRequest:request]; NSNumber *pressure = request.arguments[@"pressure"]; NSNumber *duration = request.arguments[@"duration"]; NSNumber *x = request.arguments[@"x"]; @@ -558,7 +517,7 @@ + (NSArray *)routes NSString *textToType = [request.arguments[@"value"] componentsJoinedByString:@""]; NSUInteger frequency = [request.arguments[@"frequency"] unsignedIntegerValue] ?: [FBConfiguration maxTypingFrequency]; NSError *error; - if (![FBKeyboard typeText:textToType frequency:frequency error:&error]) { + if (!FBTypeText(textToType, frequency, &error)) { return FBResponseWithStatus([FBCommandStatus invalidElementStateErrorWithMessage:error.description traceback:nil]); } @@ -567,15 +526,34 @@ + (NSArray *)routes + (id)handleGetWindowSize:(FBRouteRequest *)request { - XCUIApplication *app = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + CGRect frame = app.wdFrame; #if TARGET_OS_TV - CGSize screenSize = app.frame.size; + CGSize screenSize = frame.size; #else + CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation); +#endif + return FBResponseWithObject(@{ + @"width": @(screenSize.width), + @"height": @(screenSize.height), + }); +} + + ++ (id)handleGetWindowRect:(FBRouteRequest *)request +{ + XCUIApplication *app = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; + CGRect frame = app.wdFrame; +#if TARGET_OS_TV + CGSize screenSize = frame.size; +#else CGSize screenSize = FBAdjustDimensionsForApplication(frame.size, app.interfaceOrientation); #endif return FBResponseWithObject(@{ + @"x": @(frame.origin.x), + @"y": @(frame.origin.y), @"width": @(screenSize.width), @"height": @(screenSize.height), }); @@ -583,16 +561,23 @@ + (NSArray *)routes + (id)handleElementScreenshot:(FBRouteRequest *)request { - FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - NSData *screenshotData = [element.screenshot PNGRepresentation]; - if (nil == screenshotData) { - NSString *errMsg = [NSString stringWithFormat:@"Cannot take a screenshot of %@", element.description]; - return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:errMsg - traceback:nil]); + @autoreleasepool { + FBElementCache *elementCache = request.session.elementCache; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES]; + NSData *screenshotData = nil; + @autoreleasepool { + screenshotData = [element.screenshot PNGRepresentation]; + if (nil == screenshotData) { + NSString *errMsg = [NSString stringWithFormat:@"Cannot take a screenshot of %@", element.description]; + return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:errMsg + traceback:nil]); + } + } + NSString *screenshot = [screenshotData base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; + screenshotData = nil; + return FBResponseWithObject(screenshot); } - NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; - return FBResponseWithObject(screenshot); } @@ -604,8 +589,9 @@ + (NSArray *)routes + (id)handleWheelSelect:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; - if ([element.lastSnapshot elementType] != XCUIElementTypePickerWheel) { + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:YES]; + if ([element elementType] != XCUIElementTypePickerWheel) { NSString *errMsg = [NSString stringWithFormat:@"The element is expected to be a valid Picker Wheel control. '%@' was given instead", element.wdType]; return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:errMsg traceback:[NSString stringWithFormat:@"%@", NSThread.callStackSymbols]]); @@ -675,6 +661,45 @@ + (XCUICoordinate *)gestureCoordinateWithOffset:(CGVector)offset return [[element coordinateWithNormalizedOffset:CGVectorMake(0, 0)] coordinateWithOffset:offset]; } +/** + Returns either coordinates or the target element for the given request that expects 'x' and 'y' coordannates + + @param request HTTP request object + @param error Error instance if any + @return Either XCUICoordinate or XCUIElement instance. nil if the input data is invalid + */ ++ (nullable id)targetWithXyCoordinatesFromRequest:(FBRouteRequest *)request error:(NSError **)error +{ + NSNumber *x = request.arguments[@"x"]; + NSNumber *y = request.arguments[@"y"]; + if (nil == x && nil == y) { + return [self targetFromRequest:request]; + } + if ((nil == x && nil != y) || (nil != x && nil == y)) { + [[[FBErrorBuilder alloc] + withDescription:@"Both x and y coordinates must be provided"] + buildError:error]; + return nil; + } + return [self gestureCoordinateWithOffset:CGVectorMake(x.doubleValue, y.doubleValue) + element:[self targetFromRequest:request]]; +} + +/** + Returns the target element for the given request + + @param request HTTP request object + @return Matching XCUIElement instance + */ ++ (XCUIElement *)targetFromRequest:(FBRouteRequest *)request +{ + FBElementCache *elementCache = request.session.elementCache; + NSString *elementUuid = (NSString *)request.parameters[@"uuid"]; + return nil == elementUuid + ? request.session.activeApplication + : [elementCache elementForUUID:elementUuid checkStaleness:YES]; +} + #endif @end diff --git a/WebDriverAgentLib/Commands/FBFindElementCommands.h b/WebDriverAgentLib/Commands/FBFindElementCommands.h index 9c20b0d45..7349e10c9 100644 --- a/WebDriverAgentLib/Commands/FBFindElementCommands.h +++ b/WebDriverAgentLib/Commands/FBFindElementCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBFindElementCommands.m b/WebDriverAgentLib/Commands/FBFindElementCommands.m index 037dbae15..c370f11ce 100644 --- a/WebDriverAgentLib/Commands/FBFindElementCommands.m +++ b/WebDriverAgentLib/Commands/FBFindElementCommands.m @@ -3,14 +3,12 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBFindElementCommands.h" #import "FBAlert.h" -#import "FBApplication.h" #import "FBConfiguration.h" #import "FBElementCache.h" #import "FBExceptions.h" @@ -81,15 +79,13 @@ + (NSArray *)routes + (id)handleFindVisibleCells:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] - resolveForAdditionalAttributes:@[FB_XCAXAIsVisibleAttributeName] - andMaxDepth:nil]; - NSArray> *visibleCellSnapshots = [element.lastSnapshot descendantsByFilteringWithBlock:^BOOL(id snapshot) { - return snapshot.elementType == XCUIElementTypeCell - && [FBXCElementSnapshotWrapper ensureWrapped:snapshot].wdVisible; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + id snapshot = [element fb_customSnapshot]; + NSArray> *visibleCellSnapshots = [snapshot descendantsByFilteringWithBlock:^BOOL(id shot) { + return shot.elementType == XCUIElementTypeCell + && [FBXCElementSnapshotWrapper ensureWrapped:shot].wdVisible; }]; NSArray *cells = [element fb_filterDescendantsWithSnapshots:visibleCellSnapshots - selfUID:[FBXCElementSnapshotWrapper wdUIDWithSnapshot:element.lastSnapshot] onlyChildren:NO]; return FBResponseWithCachedElements(cells, request.session.elementCache, FBConfiguration.shouldUseCompactResponses); } @@ -97,7 +93,8 @@ + (NSArray *)routes + (id)handleFindSubElement:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:NO]; XCUIElement *foundElement = [self.class elementUsing:request.arguments[@"using"] withValue:request.arguments[@"value"] under:element]; @@ -110,7 +107,8 @@ + (NSArray *)routes + (id)handleFindSubElements:(FBRouteRequest *)request { FBElementCache *elementCache = request.session.elementCache; - XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"]]; + XCUIElement *element = [elementCache elementForUUID:(NSString *)request.parameters[@"uuid"] + checkStaleness:NO]; NSArray *foundElements = [self.class elementsUsing:request.arguments[@"using"] withValue:request.arguments[@"value"] under:element diff --git a/WebDriverAgentLib/Commands/FBOrientationCommands.h b/WebDriverAgentLib/Commands/FBOrientationCommands.h index 50bd5b6b1..1aaaacd63 100644 --- a/WebDriverAgentLib/Commands/FBOrientationCommands.h +++ b/WebDriverAgentLib/Commands/FBOrientationCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBOrientationCommands.m b/WebDriverAgentLib/Commands/FBOrientationCommands.m index 185b6f2f3..bfadd984d 100644 --- a/WebDriverAgentLib/Commands/FBOrientationCommands.m +++ b/WebDriverAgentLib/Commands/FBOrientationCommands.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBOrientationCommands.h" @@ -12,7 +11,8 @@ #import "FBRouteRequest.h" #import "FBMacros.h" #import "FBSession.h" -#import "FBApplication.h" +#import "XCUIApplication.h" +#import "XCUIApplication+FBHelpers.h" #import "XCUIDevice.h" extern const struct FBWDOrientationValues { @@ -55,14 +55,14 @@ + (NSArray *)routes + (id)handleGetOrientation:(FBRouteRequest *)request { - FBApplication *application = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; NSString *orientation = [self.class interfaceOrientationForApplication:application]; return FBResponseWithObject([[self _wdOrientationsMapping] objectForKey:orientation]); } + (id)handleSetOrientation:(FBRouteRequest *)request { - FBApplication *application = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; if ([self.class setDeviceOrientation:request.arguments[@"orientation"] forApplication:application]) { return FBResponseWithOK(); } @@ -73,7 +73,7 @@ + (NSArray *)routes + (id)handleGetRotation:(FBRouteRequest *)request { XCUIDevice *device = [XCUIDevice sharedDevice]; - FBApplication *application = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; UIInterfaceOrientation orientation = application.interfaceOrientation; return FBResponseWithObject(device.fb_rotationMapping[@(orientation)]); } @@ -101,7 +101,7 @@ + (NSArray *)routes traceback:nil]); } - FBApplication *application = request.session.activeApplication ?: FBApplication.fb_activeApplication; + XCUIApplication *application = request.session.activeApplication ?: XCUIApplication.fb_activeApplication; if (![self.class setDeviceRotation:request.arguments forApplication:application]) { NSString *errMessage = [ NSString stringWithFormat:@"The current rotation cannot be set to %@. Make sure the %@ application supports it", @@ -116,7 +116,7 @@ + (NSArray *)routes #pragma mark - Helpers -+ (NSString *)interfaceOrientationForApplication:(FBApplication *)application ++ (NSString *)interfaceOrientationForApplication:(XCUIApplication *)application { NSNumber *orientation = @(application.interfaceOrientation); NSSet *keys = [[self _orientationsMapping] keysOfEntriesPassingTest:^BOOL(id key, NSNumber *obj, BOOL *stop) { @@ -128,18 +128,18 @@ + (NSString *)interfaceOrientationForApplication:(FBApplication *)application return keys.anyObject; } -+ (BOOL)setDeviceRotation:(NSDictionary *)rotationObj forApplication:(FBApplication *)application ++ (BOOL)setDeviceRotation:(NSDictionary *)rotationObj forApplication:(XCUIApplication *)application { return [[XCUIDevice sharedDevice] fb_setDeviceRotation:rotationObj]; } -+ (BOOL)setDeviceOrientation:(NSString *)orientation forApplication:(FBApplication *)application ++ (BOOL)setDeviceOrientation:(NSString *)orientation forApplication:(XCUIApplication *)application { NSNumber *orientationValue = [[self _orientationsMapping] objectForKey:[orientation uppercaseString]]; if (orientationValue == nil) { return NO; } - return [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:orientationValue.integerValue]; + return [[XCUIDevice sharedDevice] fb_setDeviceInterfaceOrientation:(UIDeviceOrientation)orientationValue.integerValue]; } + (NSDictionary *)_orientationsMapping diff --git a/WebDriverAgentLib/Commands/FBScreenshotCommands.h b/WebDriverAgentLib/Commands/FBScreenshotCommands.h index ecb4a5eb8..3f4fa4a2c 100644 --- a/WebDriverAgentLib/Commands/FBScreenshotCommands.h +++ b/WebDriverAgentLib/Commands/FBScreenshotCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBScreenshotCommands.m b/WebDriverAgentLib/Commands/FBScreenshotCommands.m index 0b6e42427..e2b090722 100644 --- a/WebDriverAgentLib/Commands/FBScreenshotCommands.m +++ b/WebDriverAgentLib/Commands/FBScreenshotCommands.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScreenshotCommands.h" @@ -34,7 +33,7 @@ + (NSArray *)routes if (nil == screenshotData) { return FBResponseWithStatus([FBCommandStatus unableToCaptureScreenErrorWithMessage:error.description traceback:nil]); } - NSString *screenshot = [screenshotData base64EncodedStringWithOptions:0]; + NSString *screenshot = [screenshotData base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]; return FBResponseWithObject(screenshot); } diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.h b/WebDriverAgentLib/Commands/FBSessionCommands.h index 3f925aca8..95f3f258f 100644 --- a/WebDriverAgentLib/Commands/FBSessionCommands.h +++ b/WebDriverAgentLib/Commands/FBSessionCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBSessionCommands.m b/WebDriverAgentLib/Commands/FBSessionCommands.m index 4a7ae1a8d..0522633fd 100644 --- a/WebDriverAgentLib/Commands/FBSessionCommands.m +++ b/WebDriverAgentLib/Commands/FBSessionCommands.m @@ -3,21 +3,20 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBSessionCommands.h" -#import "FBApplication.h" #import "FBCapabilities.h" +#import "FBClassChainQueryParser.h" #import "FBConfiguration.h" +#import "FBExceptions.h" #import "FBLogger.h" #import "FBProtocolHelpers.h" #import "FBRouteRequest.h" #import "FBSession.h" #import "FBSettings.h" -#import "FBApplication.h" #import "FBRuntimeUtils.h" #import "FBActiveAppDetectionPoint.h" #import "FBXCodeCompatibility.h" @@ -67,6 +66,7 @@ + (NSArray *)routes return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:@"URL is required" traceback:nil]); } NSString* bundleId = request.arguments[@"bundleId"]; + NSNumber* idleTimeoutMs = request.arguments[@"idleTimeoutMs"]; NSError *error; if (nil == bundleId) { if (![XCUIDevice.sharedDevice fb_openUrl:urlString error:&error]) { @@ -76,6 +76,10 @@ + (NSArray *)routes if (![XCUIDevice.sharedDevice fb_openUrl:urlString withApplication:bundleId error:&error]) { return FBResponseWithUnknownError(error); } + if (idleTimeoutMs.doubleValue > 0) { + XCUIApplication *app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleId]; + [app fb_waitUntilStableWithTimeout:FBMillisToSeconds(idleTimeoutMs.doubleValue)]; + } } return FBResponseWithOK(); } @@ -93,11 +97,10 @@ + (NSArray *)routes traceback:nil]); } if (nil == (capabilities = FBParseCapabilities((NSDictionary *)request.arguments[@"capabilities"], &error))) { - return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:error.description traceback:nil]); + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:error.localizedDescription traceback:nil]); } [FBConfiguration resetSessionSettings]; - [FBConfiguration setShouldUseTestManagerForVisibilityDetection:[capabilities[FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION] boolValue]]; if (capabilities[FB_SETTING_USE_COMPACT_RESPONSES]) { [FBConfiguration setShouldUseCompactResponses:[capabilities[FB_SETTING_USE_COMPACT_RESPONSES] boolValue]]; } @@ -138,28 +141,71 @@ + (NSArray *)routes } NSString *bundleID = capabilities[FB_CAP_BUNDLE_ID]; - FBApplication *app = nil; + NSString *initialUrl = capabilities[FB_CAP_INITIAL_URL]; + XCUIApplication *app = nil; if (bundleID != nil) { - app = [[FBApplication alloc] initWithBundleIdentifier:bundleID]; + app = [[XCUIApplication alloc] initWithBundleIdentifier:bundleID]; BOOL forceAppLaunch = YES; if (nil != capabilities[FB_CAP_FORCE_APP_LAUNCH]) { forceAppLaunch = [capabilities[FB_CAP_FORCE_APP_LAUNCH] boolValue]; } - NSUInteger appState = [app fb_state]; - BOOL isAppRunning = appState >= 2; + XCUIApplicationState appState = app.state; + BOOL isAppRunning = appState >= XCUIApplicationStateRunningBackground; if (!isAppRunning || (isAppRunning && forceAppLaunch)) { app.fb_shouldWaitForQuiescence = nil == capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] || [capabilities[FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE] boolValue]; app.launchArguments = (NSArray *)capabilities[FB_CAP_ARGUMENTS] ?: @[]; app.launchEnvironment = (NSDictionary *)capabilities[FB_CAP_ENVIRNOMENT] ?: @{}; - [app launch]; - if (![app running]) { + if (nil != initialUrl) { + if (app.running) { + [app terminate]; + } + id errorResponse = [self openDeepLink:initialUrl + withApplication:bundleID + timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]]; + if (nil != errorResponse) { + return errorResponse; + } + } else { + NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout(); + if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) { + _XCTSetApplicationStateTimeout([capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC] doubleValue]); + } + @try { + [app launch]; + } @catch (NSException *e) { + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:e.reason traceback:nil]); + } @finally { + if (nil != capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]) { + _XCTSetApplicationStateTimeout(defaultTimeout); + } + } + } + if (!app.running) { NSString *errorMsg = [NSString stringWithFormat:@"Cannot launch %@ application. Make sure the correct bundle identifier has been provided in capabilities and check the device log for possible crash report occurrences", bundleID]; return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg traceback:nil]); } - } else if (appState < 4 && !forceAppLaunch) { - [app fb_activate]; + } else if (appState == XCUIApplicationStateRunningBackground && !forceAppLaunch) { + if (nil != initialUrl) { + id errorResponse = [self openDeepLink:initialUrl + withApplication:bundleID + timeout:nil]; + if (nil != errorResponse) { + return errorResponse; + } + } else { + [app activate]; + } + } + } + + if (nil != initialUrl && nil == bundleID) { + id errorResponse = [self openDeepLink:initialUrl + withApplication:nil + timeout:capabilities[FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC]]; + if (nil != errorResponse) { + return errorResponse; } } @@ -237,6 +283,11 @@ + (NSArray *)routes if (nil != upgradeTimestamp && upgradeTimestamp.length > 0) { [buildInfo setObject:upgradeTimestamp forKey:@"upgradedAt"]; } + NSDictionary *infoDict = [[NSBundle bundleForClass:self.class] infoDictionary]; + NSString *version = [infoDict objectForKey:@"CFBundleShortVersionString"]; + if (nil != version) { + [buildInfo setObject:version forKey:@"version"]; + } return FBResponseWithObject( @{ @@ -265,7 +316,7 @@ + (NSArray *)routes + (id)handleGetHealthCheck:(FBRouteRequest *)request { - if (![[XCUIDevice sharedDevice] fb_healthCheckWithApplication:[FBApplication fb_activeApplication]]) { + if (![[XCUIDevice sharedDevice] fb_healthCheckWithApplication:[XCUIApplication fb_activeApplication]]) { return FBResponseWithUnknownErrorFormat(@"Health check failed"); } return FBResponseWithOK(); @@ -284,8 +335,8 @@ + (NSArray *)routes FB_SETTING_SCREENSHOT_QUALITY: @([FBConfiguration screenshotQuality]), FB_SETTING_KEYBOARD_AUTOCORRECTION: @([FBConfiguration keyboardAutocorrection]), FB_SETTING_KEYBOARD_PREDICTION: @([FBConfiguration keyboardPrediction]), - FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT: @([FBConfiguration customSnapshotTimeout]), FB_SETTING_SNAPSHOT_MAX_DEPTH: @([FBConfiguration snapshotMaxDepth]), + FB_SETTING_SNAPSHOT_MAX_CHILDREN: @([FBConfiguration snapshotMaxChildren]), FB_SETTING_USE_FIRST_MATCH: @([FBConfiguration useFirstMatch]), FB_SETTING_WAIT_FOR_IDLE_TIMEOUT: @([FBConfiguration waitForIdleTimeout]), FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT: @([FBConfiguration animationCoolOffTimeout]), @@ -293,10 +344,19 @@ + (NSArray *)routes FB_SETTING_REDUCE_MOTION: @([FBConfiguration reduceMotionEnabled]), FB_SETTING_DEFAULT_ACTIVE_APPLICATION: request.session.defaultActiveApplication, FB_SETTING_ACTIVE_APP_DETECTION_POINT: FBActiveAppDetectionPoint.sharedInstance.stringCoordinates, - FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS: @([FBConfiguration includeNonModalElements]), FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR: FBConfiguration.acceptAlertButtonSelector, FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR: FBConfiguration.dismissAlertButtonSelector, + FB_SETTING_AUTO_CLICK_ALERT_SELECTOR: FBConfiguration.autoClickAlertSelector, FB_SETTING_DEFAULT_ALERT_ACTION: request.session.defaultAlertAction ?: @"", + FB_SETTING_MAX_TYPING_FREQUENCY: @([FBConfiguration maxTypingFrequency]), + FB_SETTING_RESPECT_SYSTEM_ALERTS: @([FBConfiguration shouldRespectSystemAlerts]), + FB_SETTING_USE_CLEAR_TEXT_SHORTCUT: @([FBConfiguration useClearTextShortcut]), + FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE: @([FBConfiguration includeHittableInPageSource]), + FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE: @([FBConfiguration includeNativeFrameInPageSource]), + FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE: @([FBConfiguration includeMinMaxValueInPageSource]), + FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE: @([FBConfiguration includeCustomActionsInPageSource]), + FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS: @([FBConfiguration enforceCustomSnapshots]), + FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE: @([FBConfiguration limitXpathContextScope]), #if !TARGET_OS_TV FB_SETTING_SCREENSHOT_ORIENTATION: [FBConfiguration humanReadableScreenshotOrientation], #endif @@ -326,7 +386,7 @@ + (NSArray *)routes [FBConfiguration setScreenshotQuality:[[settings objectForKey:FB_SETTING_SCREENSHOT_QUALITY] unsignedIntegerValue]]; } if (nil != [settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR]) { - [FBConfiguration setMjpegScalingFactor:[[settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR] unsignedIntegerValue]]; + [FBConfiguration setMjpegScalingFactor:[[settings objectForKey:FB_SETTING_MJPEG_SCALING_FACTOR] floatValue]]; } if (nil != [settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION]) { [FBConfiguration setMjpegShouldFixOrientation:[[settings objectForKey:FB_SETTING_MJPEG_FIX_ORIENTATION] boolValue]]; @@ -337,16 +397,15 @@ + (NSArray *)routes if (nil != [settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION]) { [FBConfiguration setKeyboardPrediction:[[settings objectForKey:FB_SETTING_KEYBOARD_PREDICTION] boolValue]]; } - // SNAPSHOT_TIMEOUT setting is deprecated. Please use CUSTOM_SNAPSHOT_TIMEOUT instead - if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_TIMEOUT]) { - [FBConfiguration setCustomSnapshotTimeout:[[settings objectForKey:FB_SETTING_SNAPSHOT_TIMEOUT] doubleValue]]; - } - if (nil != [settings objectForKey:FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT]) { - [FBConfiguration setCustomSnapshotTimeout:[[settings objectForKey:FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT] doubleValue]]; + if (nil != [settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS]) { + [FBConfiguration setShouldRespectSystemAlerts:[[settings objectForKey:FB_SETTING_RESPECT_SYSTEM_ALERTS] boolValue]]; } if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH]) { [FBConfiguration setSnapshotMaxDepth:[[settings objectForKey:FB_SETTING_SNAPSHOT_MAX_DEPTH] intValue]]; } + if (nil != [settings objectForKey:FB_SETTING_SNAPSHOT_MAX_CHILDREN]) { + [FBConfiguration setSnapshotMaxChildren:[[settings objectForKey:FB_SETTING_SNAPSHOT_MAX_CHILDREN] intValue]]; + } if (nil != [settings objectForKey:FB_SETTING_USE_FIRST_MATCH]) { [FBConfiguration setUseFirstMatch:[[settings objectForKey:FB_SETTING_USE_FIRST_MATCH] boolValue]]; } @@ -363,14 +422,8 @@ + (NSArray *)routes NSError *error; if (![FBActiveAppDetectionPoint.sharedInstance setCoordinatesWithString:(NSString *)[settings objectForKey:FB_SETTING_ACTIVE_APP_DETECTION_POINT] error:&error]) { - return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.description traceback:nil]); - } - } - if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS]) { - if ([XCUIElement fb_supportsNonModalElementsInclusion]) { - [FBConfiguration setIncludeNonModalElements:[[settings objectForKey:FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS] boolValue]]; - } else { - [FBLogger logFmt:@"'%@' settings value cannot be assigned, because non modal elements inclusion is not supported by the current iOS SDK", FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS]; + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription + traceback:nil]); } } if (nil != [settings objectForKey:FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR]) { @@ -379,6 +432,13 @@ + (NSArray *)routes if (nil != [settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]) { [FBConfiguration setDismissAlertButtonSelector:(NSString *)[settings objectForKey:FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR]]; } + if (nil != [settings objectForKey:FB_SETTING_AUTO_CLICK_ALERT_SELECTOR]) { + FBCommandStatus *status = [self.class configureAutoClickAlertWithSelector:settings[FB_SETTING_AUTO_CLICK_ALERT_SELECTOR] + forSession:request.session]; + if (status.hasError) { + return FBResponseWithStatus(status); + } + } if (nil != [settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT]) { [FBConfiguration setWaitForIdleTimeout:[[settings objectForKey:FB_SETTING_WAIT_FOR_IDLE_TIMEOUT] doubleValue]]; } @@ -388,13 +448,37 @@ + (NSArray *)routes if ([[settings objectForKey:FB_SETTING_DEFAULT_ALERT_ACTION] isKindOfClass:NSString.class]) { request.session.defaultAlertAction = [settings[FB_SETTING_DEFAULT_ALERT_ACTION] lowercaseString]; } + if (nil != [settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY]) { + [FBConfiguration setMaxTypingFrequency:[[settings objectForKey:FB_SETTING_MAX_TYPING_FREQUENCY] unsignedIntegerValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT]) { + [FBConfiguration setUseClearTextShortcut:[[settings objectForKey:FB_SETTING_USE_CLEAR_TEXT_SHORTCUT] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeHittableInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeNativeFrameInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeMinMaxValueInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE]) { + [FBConfiguration setIncludeCustomActionsInPageSource:[[settings objectForKey:FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS]) { + [FBConfiguration setEnforceCustomSnapshots:[[settings objectForKey:FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS] boolValue]]; + } + if (nil != [settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE]) { + [FBConfiguration setLimitXpathContextScope:[[settings objectForKey:FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE] boolValue]]; + } #if !TARGET_OS_TV if (nil != [settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION]) { NSError *error; if (![FBConfiguration setScreenshotOrientation:(NSString *)[settings objectForKey:FB_SETTING_SCREENSHOT_ORIENTATION] error:&error]) { - return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.description + return FBResponseWithStatus([FBCommandStatus invalidArgumentErrorWithMessage:error.localizedDescription traceback:nil]); } } @@ -406,6 +490,26 @@ + (NSArray *)routes #pragma mark - Helpers ++ (FBCommandStatus *)configureAutoClickAlertWithSelector:(NSString *)selector + forSession:(FBSession *)session +{ + if (0 == [selector length]) { + [FBConfiguration setAutoClickAlertSelector:selector]; + [session disableAlertsMonitor]; + return [FBCommandStatus ok]; + } + + NSError *error; + FBClassChain *parsedChain = [FBClassChainQueryParser parseQuery:selector error:&error]; + if (nil == parsedChain) { + return [FBCommandStatus invalidSelectorErrorWithMessage:error.localizedDescription + traceback:nil]; + } + [FBConfiguration setAutoClickAlertSelector:selector]; + [session enableAlertsMonitor]; + return [FBCommandStatus ok]; +} + + (NSString *)buildTimestamp { return [NSString stringWithFormat:@"%@ %@", @@ -441,7 +545,7 @@ + (NSString *)deviceNameByUserInterfaceIdiom:(UIUserInterfaceIdiom) userInterfac } // CarPlay, Mac, Vision UI or unknown are possible return @"Unknown"; - + } + (NSDictionary *)currentCapabilities @@ -453,4 +557,33 @@ + (NSDictionary *)currentCapabilities }; } ++(nullable id)openDeepLink:(NSString *)initialUrl + withApplication:(nullable NSString *)bundleID + timeout:(nullable NSNumber *)timeout +{ + NSError *openError; + NSTimeInterval defaultTimeout = _XCTApplicationStateTimeout(); + if (nil != timeout) { + _XCTSetApplicationStateTimeout([timeout doubleValue]); + } + @try { + BOOL result = nil == bundleID + ? [XCUIDevice.sharedDevice fb_openUrl:initialUrl + error:&openError] + : [XCUIDevice.sharedDevice fb_openUrl:initialUrl + withApplication:(id)bundleID + error:&openError]; + if (result) { + return nil; + } + NSString *errorMsg = [NSString stringWithFormat:@"Cannot open the URL %@ with the %@ application. Original error: %@", + initialUrl, bundleID ?: @"default", openError.localizedDescription]; + return FBResponseWithStatus([FBCommandStatus sessionNotCreatedError:errorMsg traceback:nil]); + } @finally { + if (nil != timeout) { + _XCTSetApplicationStateTimeout(defaultTimeout); + } + } +} + @end diff --git a/WebDriverAgentLib/Commands/FBTouchActionCommands.h b/WebDriverAgentLib/Commands/FBTouchActionCommands.h index b3a8e3f2a..d9b84fc7d 100644 --- a/WebDriverAgentLib/Commands/FBTouchActionCommands.h +++ b/WebDriverAgentLib/Commands/FBTouchActionCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBTouchActionCommands.m b/WebDriverAgentLib/Commands/FBTouchActionCommands.m index 64ef8eabe..7788175e8 100644 --- a/WebDriverAgentLib/Commands/FBTouchActionCommands.m +++ b/WebDriverAgentLib/Commands/FBTouchActionCommands.m @@ -3,13 +3,11 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTouchActionCommands.h" -#import "FBApplication.h" #import "FBRoute.h" #import "FBRouteRequest.h" #import "FBSession.h" @@ -23,25 +21,12 @@ + (NSArray *)routes { return @[ - [[FBRoute POST:@"/wda/touch/perform"] respondWithTarget:self action:@selector(handlePerformAppiumTouchActions:)], - [[FBRoute POST:@"/wda/touch/multi/perform"] respondWithTarget:self action:@selector(handlePerformAppiumTouchActions:)], [[FBRoute POST:@"/actions"] respondWithTarget:self action:@selector(handlePerformW3CTouchActions:)], ]; } #pragma mark - Commands -+ (id)handlePerformAppiumTouchActions:(FBRouteRequest *)request -{ - XCUIApplication *application = request.session.activeApplication; - NSArray *actions = (NSArray *)request.arguments[@"actions"]; - NSError *error; - if (![application fb_performAppiumTouchActions:actions elementCache:request.session.elementCache error:&error]) { - return FBResponseWithUnknownError(error); - } - return FBResponseWithOK(); -} - + (id)handlePerformW3CTouchActions:(FBRouteRequest *)request { XCUIApplication *application = request.session.activeApplication; diff --git a/WebDriverAgentLib/Commands/FBTouchIDCommands.h b/WebDriverAgentLib/Commands/FBTouchIDCommands.h index 6240a5f0a..ffcf2e812 100644 --- a/WebDriverAgentLib/Commands/FBTouchIDCommands.h +++ b/WebDriverAgentLib/Commands/FBTouchIDCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBTouchIDCommands.m b/WebDriverAgentLib/Commands/FBTouchIDCommands.m index f965c1f08..9594dd6f5 100644 --- a/WebDriverAgentLib/Commands/FBTouchIDCommands.m +++ b/WebDriverAgentLib/Commands/FBTouchIDCommands.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTouchIDCommands.h" diff --git a/WebDriverAgentLib/Commands/FBUnknownCommands.h b/WebDriverAgentLib/Commands/FBUnknownCommands.h index 54262cafb..3e37d7894 100644 --- a/WebDriverAgentLib/Commands/FBUnknownCommands.h +++ b/WebDriverAgentLib/Commands/FBUnknownCommands.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Commands/FBUnknownCommands.m b/WebDriverAgentLib/Commands/FBUnknownCommands.m index ca27e9a75..7fc35b96b 100644 --- a/WebDriverAgentLib/Commands/FBUnknownCommands.m +++ b/WebDriverAgentLib/Commands/FBUnknownCommands.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBUnknownCommands.h" diff --git a/WebDriverAgentLib/Commands/FBVideoCommands.h b/WebDriverAgentLib/Commands/FBVideoCommands.h new file mode 100644 index 000000000..a3e7a0a65 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBVideoCommands.h @@ -0,0 +1,19 @@ +/** + * 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 + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FBVideoCommands : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Commands/FBVideoCommands.m b/WebDriverAgentLib/Commands/FBVideoCommands.m new file mode 100644 index 000000000..a5c36a564 --- /dev/null +++ b/WebDriverAgentLib/Commands/FBVideoCommands.m @@ -0,0 +1,84 @@ +/** + * 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 "FBVideoCommands.h" + +#import "FBRouteRequest.h" +#import "FBScreenRecordingContainer.h" +#import "FBScreenRecordingPromise.h" +#import "FBScreenRecordingRequest.h" +#import "FBSession.h" +#import "FBXCTestDaemonsProxy.h" + +const NSUInteger DEFAULT_FPS = 24; +const NSUInteger DEFAULT_CODEC = 0; + +@implementation FBVideoCommands + ++ (NSArray *)routes +{ + return + @[ + [[FBRoute POST:@"/wda/video/start"] respondWithTarget:self action:@selector(handleStartVideoRecording:)], + [[FBRoute POST:@"/wda/video/stop"] respondWithTarget:self action:@selector(handleStopVideoRecording:)], + [[FBRoute GET:@"/wda/video"] respondWithTarget:self action:@selector(handleGetVideoRecording:)], + + [[FBRoute POST:@"/wda/video/start"].withoutSession respondWithTarget:self action:@selector(handleStartVideoRecording:)], + [[FBRoute POST:@"/wda/video/stop"].withoutSession respondWithTarget:self action:@selector(handleStopVideoRecording:)], + [[FBRoute GET:@"/wda/video"].withoutSession respondWithTarget:self action:@selector(handleGetVideoRecording:)], + ]; +} + ++ (id)handleStartVideoRecording:(FBRouteRequest *)request +{ + FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise; + if (nil != activeScreenRecording) { + return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary] ?: [NSNull null]); + } + + NSNumber *fps = (NSNumber *)request.arguments[@"fps"] ?: @(DEFAULT_FPS); + NSNumber *codec = (NSNumber *)request.arguments[@"codec"] ?: @(DEFAULT_CODEC); + FBScreenRecordingRequest *recordingRequest = [[FBScreenRecordingRequest alloc] initWithFps:fps.integerValue + codec:codec.longLongValue]; + NSError *error; + FBScreenRecordingPromise* promise = [FBXCTestDaemonsProxy startScreenRecordingWithRequest:recordingRequest + error:&error]; + if (nil == promise) { + [FBScreenRecordingContainer.sharedInstance reset]; + return FBResponseWithUnknownError(error); + } + [FBScreenRecordingContainer.sharedInstance storeScreenRecordingPromise:promise + fps:fps.integerValue + codec:codec.longLongValue]; + return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary]); +} + ++ (id)handleStopVideoRecording:(FBRouteRequest *)request +{ + FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise; + if (nil == activeScreenRecording) { + return FBResponseWithOK(); + } + + NSUUID *recordingId = activeScreenRecording.identifier; + NSDictionary *response = [FBScreenRecordingContainer.sharedInstance toDictionary]; + NSError *error; + if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:recordingId error:&error]) { + [FBScreenRecordingContainer.sharedInstance reset]; + return FBResponseWithUnknownError(error); + } + [FBScreenRecordingContainer.sharedInstance reset]; + return FBResponseWithObject(response); +} + ++ (id)handleGetVideoRecording:(FBRouteRequest *)request +{ + return FBResponseWithObject([FBScreenRecordingContainer.sharedInstance toDictionary] ?: [NSNull null]); +} + +@end diff --git a/WebDriverAgentLib/FBAlert.h b/WebDriverAgentLib/FBAlert.h index 22f28a710..8e9ec8cda 100644 --- a/WebDriverAgentLib/FBAlert.h +++ b/WebDriverAgentLib/FBAlert.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/FBAlert.m b/WebDriverAgentLib/FBAlert.m index d692b04b2..2e2de763d 100644 --- a/WebDriverAgentLib/FBAlert.m +++ b/WebDriverAgentLib/FBAlert.m @@ -3,18 +3,17 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBAlert.h" -#import "FBApplication.h" #import "FBConfiguration.h" #import "FBErrorBuilder.h" #import "FBLogger.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "FBXCodeCompatibility.h" +#import "XCUIApplication.h" #import "XCUIApplication+FBAlert.h" #import "XCUIElement+FBClassChain.h" #import "XCUIElement+FBTyping.h" @@ -50,7 +49,7 @@ - (BOOL)isPresent if (nil == self.alertElement) { return NO; } - [self.alertElement fb_takeSnapshot]; + [self.alertElement fb_customSnapshot]; return YES; } @catch (NSException *) { return NO; @@ -82,7 +81,7 @@ - (NSString *)text } NSMutableArray *resultText = [NSMutableArray array]; - id snapshot = self.alertElement.lastSnapshot; + id snapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; BOOL isSafariAlert = [self.class isSafariWebAlertWithSnapshot:snapshot]; [snapshot enumerateDescendantsUsingBlock:^(id descendant) { XCUIElementType elementType = descendant.elementType; @@ -145,7 +144,8 @@ - (NSArray *)buttonLabels } NSMutableArray *labels = [NSMutableArray array]; - [self.alertElement.lastSnapshot enumerateDescendantsUsingBlock:^(id descendant) { + id alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; + [alertSnapshot enumerateDescendantsUsingBlock:^(id descendant) { if (descendant.elementType != XCUIElementTypeButton) { return; } @@ -163,7 +163,7 @@ - (BOOL)acceptWithError:(NSError **)error return [self notPresentWithError:error]; } - id alertSnapshot = self.alertElement.lastSnapshot; + id alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; XCUIElement *acceptButton = nil; if (FBConfiguration.acceptAlertButtonSelector.length) { NSString *errorReason = nil; @@ -204,7 +204,7 @@ - (BOOL)dismissWithError:(NSError **)error return [self notPresentWithError:error]; } - id alertSnapshot = self.alertElement.lastSnapshot; + id alertSnapshot = self.alertElement.lastSnapshot ?: [self.alertElement fb_customSnapshot]; XCUIElement *dismissButton = nil; if (FBConfiguration.dismissAlertButtonSelector.length) { NSString *errorReason = nil; @@ -261,15 +261,11 @@ - (BOOL)clickAlertButton:(NSString *)label error:(NSError **)error - (XCUIElement *)alertElement { if (nil == self.element) { - self.element = self.application.fb_alertElement; - if (nil == self.element) { - FBApplication *systemApp = FBApplication.fb_systemApplication; - for (FBApplication *activeApp in FBApplication.fb_activeApplications) { - if (systemApp.processID == activeApp.processID) { - self.element = activeApp.fb_alertElement; - break; - } - } + XCUIApplication *systemApp = XCUIApplication.fb_systemApplication; + if ([systemApp fb_isSameAppAs:self.application]) { + self.element = systemApp.fb_alertElement; + } else { + self.element = systemApp.fb_alertElement ?: self.application.fb_alertElement; } } return self.element; diff --git a/WebDriverAgentLib/FBApplication.h b/WebDriverAgentLib/FBApplication.h deleted file mode 100644 index 00adb527d..000000000 --- a/WebDriverAgentLib/FBApplication.h +++ /dev/null @@ -1,49 +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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import - -NS_ASSUME_NONNULL_BEGIN - -@interface FBApplication : XCUIApplication - -/** - Constructor used to get current active application - */ -+ (instancetype)fb_activeApplication; - -/** - Constructor used to get current active application - - @param bundleId The bundle identifier of an app, which should be selected as active by default - if it is present in the list of active applications - */ -+ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId; - -/** - Constructor used to get the system application (e.g. Springboard on iOS) - */ -+ (instancetype)fb_systemApplication; - -/** - Retrieves the list of all currently active applications - */ -+ (NSArray *)fb_activeApplications; - -/** - Switch to system app (called Springboard on iOS) - - @param error If there is an error, upon return contains an NSError object that describes the problem. - @return YES if the operation succeeds, otherwise NO. - */ -+ (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error; - -@end - -NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/FBApplication.m b/WebDriverAgentLib/FBApplication.m deleted file mode 100644 index fe46393fb..000000000 --- a/WebDriverAgentLib/FBApplication.m +++ /dev/null @@ -1,185 +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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "FBApplication.h" - -#import "FBXCAccessibilityElement.h" -#import "FBLogger.h" -#import "FBExceptions.h" -#import "FBRunLoopSpinner.h" -#import "FBMacros.h" -#import "FBActiveAppDetectionPoint.h" -#import "FBXCodeCompatibility.h" -#import "FBXCTestDaemonsProxy.h" -#import "XCUIApplication.h" -#import "XCUIApplication+FBHelpers.h" -#import "XCUIApplicationImpl.h" -#import "XCUIApplicationProcess.h" -#import "XCUIElement.h" -#import "XCUIElementQuery.h" -#import "FBXCAXClientProxy.h" - - -static const NSTimeInterval APP_STATE_CHANGE_TIMEOUT = 5.0; - -@interface FBApplication () -@end - -@implementation FBApplication - -+ (instancetype)fb_activeApplication -{ - return [self fb_activeApplicationWithDefaultBundleId:nil]; -} - -+ (NSArray *)fb_activeApplications -{ - NSArray> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications]; - NSMutableArray *result = [NSMutableArray array]; - if (activeApplicationElements.count > 0) { - for (id applicationElement in activeApplicationElements) { - FBApplication *app = [FBApplication fb_applicationWithPID:applicationElement.processIdentifier]; - if (nil != app) { - [result addObject:app]; - } - } - } - return result.count > 0 ? result.copy : @[self.class.fb_systemApplication]; -} - -+ (instancetype)fb_activeApplicationWithDefaultBundleId:(nullable NSString *)bundleId -{ - NSArray> *activeApplicationElements = [FBXCAXClientProxy.sharedClient activeApplications]; - id activeApplicationElement = nil; - id currentElement = nil; - if (nil != bundleId) { - currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement; - if (nil != currentElement) { - NSArray *appInfos = [self fb_appsInfoWithAxElements:@[currentElement]]; - [FBLogger logFmt:@"Detected on-screen application: %@", appInfos.firstObject[@"bundleId"]]; - if ([[appInfos.firstObject objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) { - activeApplicationElement = currentElement; - } - } - } - if (nil == activeApplicationElement && activeApplicationElements.count > 1) { - if (nil != bundleId) { - NSArray *appInfos = [self fb_appsInfoWithAxElements:activeApplicationElements]; - NSMutableArray *bundleIds = [NSMutableArray array]; - for (NSDictionary *appInfo in appInfos) { - [bundleIds addObject:(NSString *)appInfo[@"bundleId"]]; - } - [FBLogger logFmt:@"Detected system active application(s): %@", bundleIds]; - // Try to select the desired application first - for (NSUInteger appIdx = 0; appIdx < appInfos.count; appIdx++) { - if ([[[appInfos objectAtIndex:appIdx] objectForKey:@"bundleId"] isEqualToString:(id)bundleId]) { - activeApplicationElement = [activeApplicationElements objectAtIndex:appIdx]; - break; - } - } - } - // Fall back to the "normal" algorithm if the desired application is either - // not set or is not active - if (nil == activeApplicationElement) { - if (nil == currentElement) { - currentElement = FBActiveAppDetectionPoint.sharedInstance.axElement; - } - if (nil == currentElement) { - [FBLogger log:@"Cannot precisely detect the current application. Will use the system's recently active one"]; - if (nil == bundleId) { - [FBLogger log:@"Consider changing the 'defaultActiveApplication' setting to the bundle identifier of the desired application under test"]; - } - } else { - for (id appElement in activeApplicationElements) { - if (appElement.processIdentifier == currentElement.processIdentifier) { - activeApplicationElement = appElement; - break; - } - } - } - } - } - - if (nil != activeApplicationElement) { - FBApplication *application = [FBApplication fb_applicationWithPID:activeApplicationElement.processIdentifier]; - if (nil != application) { - return application; - } - [FBLogger log:@"Cannot translate the active process identifier into an application object"]; - } - - if (activeApplicationElements.count > 0) { - [FBLogger logFmt:@"Getting the most recent active application (out of %@ total items)", @(activeApplicationElements.count)]; - for (id appElement in activeApplicationElements) { - FBApplication *application = [FBApplication fb_applicationWithPID:appElement.processIdentifier]; - if (nil != application) { - return application; - } - } - } - - [FBLogger log:@"Cannot retrieve any active applications. Assuming the system application is the active one"]; - return [self fb_systemApplication]; -} - -+ (instancetype)fb_systemApplication -{ - return [self fb_applicationWithPID: - [[FBXCAXClientProxy.sharedClient systemApplication] processIdentifier]]; -} - -+ (instancetype)applicationWithPID:(pid_t)processID -{ - if ([NSProcessInfo processInfo].processIdentifier == processID) { - return nil; - } - return (FBApplication *)[FBXCAXClientProxy.sharedClient monitoredApplicationWithProcessIdentifier:processID]; -} - -- (void)launch -{ - [super launch]; - if (![self fb_waitForAppElement:APP_STATE_CHANGE_TIMEOUT]) { - [FBLogger logFmt:@"The application '%@' is not running in foreground after %.2f seconds", self.bundleID, APP_STATE_CHANGE_TIMEOUT]; - } -} - -- (void)terminate -{ - [super terminate]; - if (![self waitForState:XCUIApplicationStateNotRunning timeout:APP_STATE_CHANGE_TIMEOUT]) { - [FBLogger logFmt:@"The active application is still '%@' after %.2f seconds timeout", self.bundleID, APP_STATE_CHANGE_TIMEOUT]; - } -} - -+ (BOOL)fb_switchToSystemApplicationWithError:(NSError **)error -{ - FBApplication *systemApp = self.fb_systemApplication; - @try { - if ([systemApp fb_state] < 2) { - [systemApp launch]; - } else { - [systemApp fb_activate]; - } - } @catch (NSException *e) { - return [[[FBErrorBuilder alloc] - withDescription:nil == e ? @"Cannot open the home screen" : e.reason] - buildError:error]; - } - return [[[[FBRunLoopSpinner new] - timeout:5] - timeoutErrorMessage:@"Timeout waiting until the home screen is visible"] - spinUntilTrue:^BOOL{ - FBApplication *activeApp = self.fb_activeApplication; - return nil != activeApp && [activeApp.bundleID isEqualToString:systemApp.bundleID]; - } - error:error]; -} - -@end diff --git a/WebDriverAgentLib/Info.plist b/WebDriverAgentLib/Info.plist index 1b89f4fcd..b0fd318ab 100644 --- a/WebDriverAgentLib/Info.plist +++ b/WebDriverAgentLib/Info.plist @@ -1,26 +1,26 @@ - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - com.facebook.wda.lib - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - NSPrincipalClass - - - + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 13.2.0 + CFBundleSignature + ???? + CFBundleVersion + 13.2.0 + NSPrincipalClass + + + \ No newline at end of file diff --git a/WebDriverAgentLib/Routing/FBCommandHandler.h b/WebDriverAgentLib/Routing/FBCommandHandler.h index bde028d68..c4ac83b71 100644 --- a/WebDriverAgentLib/Routing/FBCommandHandler.h +++ b/WebDriverAgentLib/Routing/FBCommandHandler.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBCommandStatus.h b/WebDriverAgentLib/Routing/FBCommandStatus.h index 82bee1f60..0929ff10c 100644 --- a/WebDriverAgentLib/Routing/FBCommandStatus.h +++ b/WebDriverAgentLib/Routing/FBCommandStatus.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -19,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable, readonly) NSString* message; @property (nonatomic, nullable, readonly) NSString* traceback; @property (nonatomic, readonly) HTTPStatusCode statusCode; - +@property (nonatomic, readonly) BOOL hasError; + (instancetype)ok; diff --git a/WebDriverAgentLib/Routing/FBCommandStatus.m b/WebDriverAgentLib/Routing/FBCommandStatus.m index 453fb6192..fb5d2439e 100644 --- a/WebDriverAgentLib/Routing/FBCommandStatus.m +++ b/WebDriverAgentLib/Routing/FBCommandStatus.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBCommandStatus.h" @@ -109,6 +108,11 @@ - (instancetype)initWithError:(NSString *)error return self; } +- (BOOL)hasError +{ + return self.statusCode != kHTTPStatusCodeOK; +} + + (instancetype)ok { return [[FBCommandStatus alloc] initWithValue:nil]; diff --git a/WebDriverAgentLib/Routing/FBElement.h b/WebDriverAgentLib/Routing/FBElement.h index 7cc8f269c..2df9c6644 100644 --- a/WebDriverAgentLib/Routing/FBElement.h +++ b/WebDriverAgentLib/Routing/FBElement.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -20,6 +19,9 @@ NS_ASSUME_NONNULL_BEGIN /*! Element's frame in normalized (rounded dimensions without Infinity values) CGRect format */ @property (nonatomic, readonly, assign) CGRect wdFrame; +/*! Represents the element's frame as a CGRect, preserving the actual values. */ +@property (nonatomic, readonly, assign) CGRect wdNativeFrame; + /*! Element's wsFrame in NSDictionary format */ @property (nonatomic, readonly, copy) NSDictionary *wdRect; @@ -35,6 +37,9 @@ NS_ASSUME_NONNULL_BEGIN /*! Element's type */ @property (nonatomic, readonly, copy) NSString *wdType; +/*! Element's accessibility traits as a comma-separated string */ +@property (nonatomic, readonly, copy) NSString *wdTraits; + /*! Element's value */ @property (nonatomic, readonly, strong, nullable) NSString *wdValue; @@ -62,6 +67,18 @@ NS_ASSUME_NONNULL_BEGIN /*! Element's index relatively to its parent. Starts from zero */ @property (nonatomic, readonly) NSUInteger wdIndex; +/*! Element's placeholder value */ +@property (nonatomic, readonly, copy, nullable) NSString *wdPlaceholderValue; + +/*! Element's minimum value */ +@property (nonatomic, readonly, strong, nullable) NSNumber *wdMinValue; + +/*! Element's maximum value */ +@property (nonatomic, readonly, strong, nullable) NSNumber *wdMaxValue; + +/*! Element's custom actions */ +@property (nonatomic, readonly, strong, nullable) NSString *wdCustomActions; + /** Returns value of given property specified in WebDriver Spec Check the FBElement protocol to get list of supported attributes. diff --git a/WebDriverAgentLib/Routing/FBElementCache.h b/WebDriverAgentLib/Routing/FBElementCache.h index 13100eeb2..1c7eb4764 100644 --- a/WebDriverAgentLib/Routing/FBElementCache.h +++ b/WebDriverAgentLib/Routing/FBElementCache.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -35,26 +34,20 @@ extern const int ELEMENT_CACHE_SIZE; @param uuid uuid of element to fetch @return element - @throws FBStaleElementException if the found element is not present in DOM anymore @throws FBInvalidArgumentException if uuid is nil */ - (XCUIElement *)elementForUUID:(NSString *)uuid; /** - Returns cached element + Returns cached element resolved with default snapshot attributes @param uuid uuid of element to fetch - @param additionalAttributes Add additonal attribute names if the snapshot should contain - them in `addtionalAttributes` section. nil value resolves the snapshot with standard attributes. - @param maxDepth The maximum depth of the snapshot. Only works if additional attributes are provided. - `nil` value means to use the default maximum depth value. + @param checkStaleness Whether to throw FBStaleElementException if the found element is not present in DOM anymore @return element - @throws FBStaleElementException if the found element is not present in DOM anymore + @throws FBStaleElementException if `checkStaleness` is enabled @throws FBInvalidArgumentException if uuid is nil */ -- (XCUIElement *)elementForUUID:(NSString *)uuid - resolveForAdditionalAttributes:(nullable NSArray *)additionalAttributes - andMaxDepth:(nullable NSNumber *)maxDepth; +- (XCUIElement *)elementForUUID:(NSString *)uuid checkStaleness:(BOOL)checkStaleness; /** Checks element existence in the cache diff --git a/WebDriverAgentLib/Routing/FBElementCache.m b/WebDriverAgentLib/Routing/FBElementCache.m index 1f069bec6..756624246 100644 --- a/WebDriverAgentLib/Routing/FBElementCache.m +++ b/WebDriverAgentLib/Routing/FBElementCache.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBElementCache.h" @@ -26,7 +25,6 @@ @interface FBElementCache () @property (nonatomic, strong) LRUCache *elementCache; -@property (nonatomic) BOOL elementsNeedReset; @end @implementation FBElementCache @@ -38,7 +36,6 @@ - (instancetype)init return nil; } _elementCache = [[LRUCache alloc] initWithCapacity:ELEMENT_CACHE_SIZE]; - _elementsNeedReset = NO; return self; } @@ -50,19 +47,16 @@ - (NSString *)storeElement:(XCUIElement *)element } @synchronized (self.elementCache) { [self.elementCache setObject:element forKey:uuid]; - self.elementsNeedReset = YES; } return uuid; } - (XCUIElement *)elementForUUID:(NSString *)uuid { - return [self elementForUUID:uuid resolveForAdditionalAttributes:nil andMaxDepth:nil]; + return [self elementForUUID:uuid checkStaleness:NO]; } -- (XCUIElement *)elementForUUID:(NSString *)uuid - resolveForAdditionalAttributes:(NSArray *)additionalAttributes - andMaxDepth:(NSNumber *)maxDepth +- (XCUIElement *)elementForUUID:(NSString *)uuid checkStaleness:(BOOL)checkStaleness { if (!uuid) { NSString *reason = [NSString stringWithFormat:@"Cannot extract cached element for UUID: %@", uuid]; @@ -71,23 +65,25 @@ - (XCUIElement *)elementForUUID:(NSString *)uuid XCUIElement *element; @synchronized (self.elementCache) { - [self resetElements]; element = [self.elementCache objectForKey:uuid]; } if (nil == element) { NSString *reason = [NSString stringWithFormat:@"The element identified by \"%@\" is either not present or it has expired from the internal cache. Try to find it again", uuid]; @throw [NSException exceptionWithName:FBStaleElementException reason:reason userInfo:@{}]; } - // This will throw FBStaleElementException exception if the element is stale - // or resolve the element and set lastSnapshot property - if (nil == additionalAttributes) { - [element fb_takeSnapshot]; - } else { - NSMutableArray *attributes = [NSMutableArray arrayWithArray:FBStandardAttributeNames()]; - [attributes addObjectsFromArray:additionalAttributes]; - [element fb_snapshotWithAttributes:attributes.copy maxDepth:maxDepth]; + if (checkStaleness) { + @try { + [element fb_standardSnapshot]; + } @catch (NSException *exception) { + // if the snapshot method threw FBStaleElementException (implying the element is stale) we need to explicitly remove it from the cache, PR: https://github.com/appium/WebDriverAgent/pull/985 + if ([exception.name isEqualToString:FBStaleElementException]) { + @synchronized (self.elementCache) { + [self.elementCache removeObjectForKey:uuid]; + } + } + @throw exception; + } } - element.fb_isResolvedFromCache = @(YES); return element; } @@ -101,20 +97,4 @@ - (BOOL)hasElementWithUUID:(NSString *)uuid } } -- (void)resetElements -{ - if (!self.elementsNeedReset) { - return; - } - - for (XCUIElement *element in self.elementCache.allObjects) { - element.lastSnapshot = nil; - if (nil != element.query) { - element.query.rootElementSnapshot = nil; - } - element.fb_isResolvedFromCache = @(NO); - } - self.elementsNeedReset = NO; -} - @end diff --git a/WebDriverAgentLib/Routing/FBElementUtils.h b/WebDriverAgentLib/Routing/FBElementUtils.h index a0b6da839..0c4de4b7f 100644 --- a/WebDriverAgentLib/Routing/FBElementUtils.h +++ b/WebDriverAgentLib/Routing/FBElementUtils.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBElementUtils.m b/WebDriverAgentLib/Routing/FBElementUtils.m index 8aec9d8a8..cc89d2fc7 100644 --- a/WebDriverAgentLib/Routing/FBElementUtils.m +++ b/WebDriverAgentLib/Routing/FBElementUtils.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBExceptionHandler.h b/WebDriverAgentLib/Routing/FBExceptionHandler.h index 693e5c407..cc1dc0f22 100644 --- a/WebDriverAgentLib/Routing/FBExceptionHandler.h +++ b/WebDriverAgentLib/Routing/FBExceptionHandler.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBExceptionHandler.m b/WebDriverAgentLib/Routing/FBExceptionHandler.m index 7b7a8263f..b5e1ec060 100644 --- a/WebDriverAgentLib/Routing/FBExceptionHandler.m +++ b/WebDriverAgentLib/Routing/FBExceptionHandler.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBExceptionHandler.h" @@ -45,6 +44,9 @@ - (void)handleException:(NSException *)exception forResponse:(RouteResponse *)re } else if ([exception.name isEqualToString:FBTimeoutException]) { commandStatus = [FBCommandStatus timeoutErrorWithMessage:exception.reason traceback:traceback]; + } else if ([exception.name isEqualToString:FBSessionCreationException]) { + commandStatus = [FBCommandStatus sessionNotCreatedError:exception.reason + traceback:traceback]; } else { commandStatus = [FBCommandStatus unknownErrorWithMessage:exception.reason traceback:traceback]; diff --git a/WebDriverAgentLib/Routing/FBExceptions.h b/WebDriverAgentLib/Routing/FBExceptions.h index 1c9507a19..b802da673 100644 --- a/WebDriverAgentLib/Routing/FBExceptions.h +++ b/WebDriverAgentLib/Routing/FBExceptions.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -14,6 +13,9 @@ NS_ASSUME_NONNULL_BEGIN /*! Exception used to notify about missing session */ extern NSString *const FBSessionDoesNotExistException; +/*! Exception used to notify about session creation issues */ +extern NSString *const FBSessionCreationException; + /*! Exception used to notify about application deadlock */ extern NSString *const FBApplicationDeadlockDetectedException; @@ -52,4 +54,7 @@ extern NSString *const FBApplicationCrashedException; /*! Exception used to notify about the application is not installed */ extern NSString *const FBApplicationMissingException; +/*! Exception used to notify about WDA incompatibility with the current platform version */ +extern NSString *const FBIncompatibleWdaException; + NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBExceptions.m b/WebDriverAgentLib/Routing/FBExceptions.m index 571cea4fb..feb8a4d50 100644 --- a/WebDriverAgentLib/Routing/FBExceptions.m +++ b/WebDriverAgentLib/Routing/FBExceptions.m @@ -3,13 +3,13 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBExceptions.h" NSString *const FBInvalidArgumentException = @"FBInvalidArgumentException"; +NSString *const FBSessionCreationException = @"FBSessionCreationException"; NSString *const FBSessionDoesNotExistException = @"FBSessionDoesNotExistException"; NSString *const FBApplicationDeadlockDetectedException = @"FBApplicationDeadlockDetectedException"; NSString *const FBElementAttributeUnknownException = @"FBElementAttributeUnknownException"; @@ -21,3 +21,4 @@ NSString *const FBClassChainQueryParseException = @"FBClassChainQueryParseException"; NSString *const FBApplicationCrashedException = @"FBApplicationCrashedException"; NSString *const FBApplicationMissingException = @"FBApplicationMissingException"; +NSString *const FBIncompatibleWdaException = @"FBIncompatibleWdaException"; diff --git a/WebDriverAgentLib/Routing/FBResponseJSONPayload.h b/WebDriverAgentLib/Routing/FBResponseJSONPayload.h index 14f6c1c84..8c863e999 100644 --- a/WebDriverAgentLib/Routing/FBResponseJSONPayload.h +++ b/WebDriverAgentLib/Routing/FBResponseJSONPayload.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBResponseJSONPayload.m b/WebDriverAgentLib/Routing/FBResponseJSONPayload.m index 4ad1c37ad..8782b8e9e 100644 --- a/WebDriverAgentLib/Routing/FBResponseJSONPayload.m +++ b/WebDriverAgentLib/Routing/FBResponseJSONPayload.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBResponseJSONPayload.h" diff --git a/WebDriverAgentLib/Routing/FBResponsePayload.h b/WebDriverAgentLib/Routing/FBResponsePayload.h index 9ff7a9308..b29b8f98a 100644 --- a/WebDriverAgentLib/Routing/FBResponsePayload.h +++ b/WebDriverAgentLib/Routing/FBResponsePayload.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBResponsePayload.m b/WebDriverAgentLib/Routing/FBResponsePayload.m index 08fcd63fe..809cd41e8 100644 --- a/WebDriverAgentLib/Routing/FBResponsePayload.m +++ b/WebDriverAgentLib/Routing/FBResponsePayload.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBResponsePayload.h" @@ -34,24 +33,41 @@ return FBResponseWithStatus([FBCommandStatus okWithValue:object]); } -id FBResponseWithCachedElement(XCUIElement *element, FBElementCache *elementCache, BOOL compact) +XCUIElement *maybeStable(XCUIElement *element) { BOOL useNativeCachingStrategy = nil == FBSession.activeSession ? YES : FBSession.activeSession.useNativeCachingStrategy; - [elementCache storeElement:(useNativeCachingStrategy ? element : element.fb_stableInstance)]; - return FBResponseWithStatus([FBCommandStatus okWithValue:FBDictionaryResponseWithElement(element, compact)]); + if (useNativeCachingStrategy) { + return element; + } + + XCUIElement *result = element; + id snapshot = element.lastSnapshot + ?: element.fb_cachedSnapshot + ?: [element fb_standardSnapshot]; + NSString *uid = [FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]; + if (nil != uid) { + result = [element fb_stableInstanceWithUid:uid]; + } + return result; +} + +id FBResponseWithCachedElement(XCUIElement *element, FBElementCache *elementCache, BOOL compact) +{ + [elementCache storeElement:maybeStable(element)]; + NSDictionary *response = FBDictionaryResponseWithElement(element, compact); + element.lastSnapshot = nil; + return FBResponseWithStatus([FBCommandStatus okWithValue:response]); } id FBResponseWithCachedElements(NSArray *elements, FBElementCache *elementCache, BOOL compact) { NSMutableArray *elementsResponse = [NSMutableArray array]; - BOOL useNativeCachingStrategy = nil == FBSession.activeSession - ? YES - : FBSession.activeSession.useNativeCachingStrategy; for (XCUIElement *element in elements) { - [elementCache storeElement:(useNativeCachingStrategy ? element : element.fb_stableInstance)]; + [elementCache storeElement:maybeStable(element)]; [elementsResponse addObject:FBDictionaryResponseWithElement(element, compact)]; + element.lastSnapshot = nil; } return FBResponseWithStatus([FBCommandStatus okWithValue:elementsResponse]); } @@ -91,41 +107,42 @@ inline NSDictionary *FBDictionaryResponseWithElement(XCUIElement *element, BOOL compact) { - id snapshot = nil; - if (nil != element.query.rootElementSnapshot) { - snapshot = element.fb_cachedSnapshot; - } - if (nil == snapshot) { - snapshot = element.lastSnapshot ?: element.fb_takeSnapshot; - } - NSDictionary *compactResult = FBToElementDict((NSString *)[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]); - if (compact) { - return compactResult; - } + __block NSDictionary *elementResponse = nil; + @autoreleasepool { + id snapshot = element.lastSnapshot + ?: element.fb_cachedSnapshot + ?: [element fb_customSnapshot]; + NSDictionary *compactResult = FBToElementDict((NSString *)[FBXCElementSnapshotWrapper wdUIDWithSnapshot:snapshot]); + if (compact) { + elementResponse = compactResult; + return elementResponse; + } - NSMutableDictionary *result = compactResult.mutableCopy; - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; - NSArray *fields = [FBConfiguration.elementResponseAttributes componentsSeparatedByString:@","]; - for (NSString *field in fields) { - // 'name' here is the w3c-approved identifier for what we mean by 'type' - if ([field isEqualToString:@"name"] || [field isEqualToString:@"type"]) { - result[field] = wrappedSnapshot.wdType; - } else if ([field isEqualToString:@"text"]) { - result[field] = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel) ?: [NSNull null]; - } else if ([field isEqualToString:@"rect"]) { - result[field] = wrappedSnapshot.wdRect; - } else if ([field isEqualToString:@"enabled"]) { - result[field] = @(wrappedSnapshot.wdEnabled); - } else if ([field isEqualToString:@"displayed"]) { - result[field] = @(wrappedSnapshot.wdVisible); - } else if ([field isEqualToString:@"selected"]) { - result[field] = @(wrappedSnapshot.wdSelected); - } else if ([field isEqualToString:@"label"]) { - result[field] = wrappedSnapshot.wdLabel ?: [NSNull null]; - } else if ([field hasPrefix:arbitraryAttrPrefix]) { - NSString *attributeName = [field substringFromIndex:[arbitraryAttrPrefix length]]; - result[field] = [wrappedSnapshot fb_valueForWDAttributeName:attributeName] ?: [NSNull null]; + NSMutableDictionary *result = compactResult.mutableCopy; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:snapshot]; + NSArray *fields = [FBConfiguration.elementResponseAttributes componentsSeparatedByString:@","]; + for (NSString *field in fields) { + // 'name' here is the w3c-approved identifier for what we mean by 'type' + if ([field isEqualToString:@"name"] || [field isEqualToString:@"type"]) { + result[field] = wrappedSnapshot.wdType; + } else if ([field isEqualToString:@"text"]) { + result[field] = FBFirstNonEmptyValue(wrappedSnapshot.wdValue, wrappedSnapshot.wdLabel) ?: [NSNull null]; + } else if ([field isEqualToString:@"rect"]) { + result[field] = wrappedSnapshot.wdRect; + } else if ([field isEqualToString:@"enabled"]) { + result[field] = @(wrappedSnapshot.wdEnabled); + } else if ([field isEqualToString:@"displayed"]) { + result[field] = @(wrappedSnapshot.wdVisible); + } else if ([field isEqualToString:@"selected"]) { + result[field] = @(wrappedSnapshot.wdSelected); + } else if ([field isEqualToString:@"label"]) { + result[field] = wrappedSnapshot.wdLabel ?: [NSNull null]; + } else if ([field hasPrefix:arbitraryAttrPrefix]) { + NSString *attributeName = [field substringFromIndex:[arbitraryAttrPrefix length]]; + result[field] = [wrappedSnapshot fb_valueForWDAttributeName:attributeName] ?: [NSNull null]; + } } + elementResponse = result.copy; } - return result.copy; + return elementResponse; } diff --git a/WebDriverAgentLib/Routing/FBRoute.h b/WebDriverAgentLib/Routing/FBRoute.h index 2d3138686..fce8dd8a9 100644 --- a/WebDriverAgentLib/Routing/FBRoute.h +++ b/WebDriverAgentLib/Routing/FBRoute.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBRoute.m b/WebDriverAgentLib/Routing/FBRoute.m index 745ddaf31..fbe69b8c3 100644 --- a/WebDriverAgentLib/Routing/FBRoute.m +++ b/WebDriverAgentLib/Routing/FBRoute.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBRoute.h" @@ -39,7 +38,10 @@ @implementation FBRoute_TargetAction - (void)mountRequest:(FBRouteRequest *)request intoResponse:(RouteResponse *)response { [self decorateRequest:request]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-function-type-strict" id (*requestMsgSend)(id, SEL, FBRouteRequest *) = ((id(*)(id, SEL, FBRouteRequest *))objc_msgSend); +#pragma clang diagnostic pop id payload = requestMsgSend(self.target, self.action, request); [payload dispatchWithResponse:response]; } diff --git a/WebDriverAgentLib/Routing/FBRouteRequest-Private.h b/WebDriverAgentLib/Routing/FBRouteRequest-Private.h index d5bd31f10..7144bd80b 100644 --- a/WebDriverAgentLib/Routing/FBRouteRequest-Private.h +++ b/WebDriverAgentLib/Routing/FBRouteRequest-Private.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBRouteRequest.h b/WebDriverAgentLib/Routing/FBRouteRequest.h index 7647cd823..c7938d5d6 100644 --- a/WebDriverAgentLib/Routing/FBRouteRequest.h +++ b/WebDriverAgentLib/Routing/FBRouteRequest.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBRouteRequest.m b/WebDriverAgentLib/Routing/FBRouteRequest.m index e7d2743f2..b8656d285 100644 --- a/WebDriverAgentLib/Routing/FBRouteRequest.m +++ b/WebDriverAgentLib/Routing/FBRouteRequest.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBRouteRequest-Private.h" diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h b/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h new file mode 100644 index 000000000..ce655dd7c --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingContainer.h @@ -0,0 +1,56 @@ +/** + * + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@class FBScreenRecordingPromise; + +@interface FBScreenRecordingContainer : NSObject + +/** The amount of video FPS */ +@property (readonly, nonatomic) NSUInteger fps; +/** Codec to use, where 0 is h264, 1 - HEVC */ +@property (readonly, nonatomic) long long codec; +/** Keep the currently active screen resording promise. Equals to nil if no active screen recordings are running */ +@property (readonly, nonatomic, nullable) FBScreenRecordingPromise* screenRecordingPromise; +/** The timestamp of the video startup as Unix float seconds */ +@property (readonly, nonatomic, nullable) NSNumber *startedAt; + +/** +@return singleton instance + */ ++ (instancetype)sharedInstance; + +/** + Keeps current screen recording promise + + @param screenRecordingPromise a promise to set + @param fps FPS value + @param codec Codec value + */ +- (void)storeScreenRecordingPromise:(FBScreenRecordingPromise *)screenRecordingPromise + fps:(NSUInteger)fps + codec:(long long)codec; +/** + Resets the current screen recording promise + */ +- (void)reset; + +/** + Transforms the container content to a dictionary. + + @return May return nil if no screen recording is currently running + */ +- (nullable NSDictionary *)toDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m b/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m new file mode 100644 index 000000000..608d7bfdf --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingContainer.m @@ -0,0 +1,72 @@ +/** + * + * 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 "FBScreenRecordingContainer.h" + +#import "FBScreenRecordingPromise.h" + +@interface FBScreenRecordingContainer () + +@property (readwrite) NSUInteger fps; +@property (readwrite) long long codec; +@property (readwrite) FBScreenRecordingPromise* screenRecordingPromise; +@property (readwrite) NSNumber *startedAt; + +@end + +@implementation FBScreenRecordingContainer + ++ (instancetype)sharedInstance +{ + static FBScreenRecordingContainer *instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +- (void)storeScreenRecordingPromise:(FBScreenRecordingPromise *)screenRecordingPromise + fps:(NSUInteger)fps + codec:(long long)codec; +{ + self.fps = fps; + self.codec = codec; + self.screenRecordingPromise = screenRecordingPromise; + self.startedAt = @([NSDate.date timeIntervalSince1970]); +} + +- (void)reset; +{ + self.fps = 0; + self.codec = 0; + if (nil != self.screenRecordingPromise) { + [XCTContext runActivityNamed:@"Video Cleanup" block:^(id activity){ + [activity addAttachment:(XCTAttachment *)self.screenRecordingPromise.nativePromise]; + }]; + self.screenRecordingPromise = nil; + } + self.startedAt = nil; +} + +- (nullable NSDictionary *)toDictionary +{ + if (nil == self.screenRecordingPromise) { + return nil; + } + + return @{ + @"fps": @(self.fps), + @"codec": @(self.codec), + @"uuid": [self.screenRecordingPromise identifier].UUIDString ?: [NSNull null], + @"startedAt": self.startedAt ?: [NSNull null], + }; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h b/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h new file mode 100644 index 000000000..6b3fcb3c6 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingPromise.h @@ -0,0 +1,29 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FBScreenRecordingPromise : NSObject + +/** Unique identiifier of the video recording, also used as the default file name */ +@property (nonatomic, readonly) NSUUID *identifier; +/** Native screen recording promise */ +@property (nonatomic, readonly) id nativePromise; + +/** + Creates a wrapper object for a native screen recording promise + + @param promise Native promise object to be wrapped + */ +- (instancetype)initWithNativePromise:(id)promise; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m b/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m new file mode 100644 index 000000000..e1193a133 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingPromise.m @@ -0,0 +1,31 @@ +/** + * + * 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 "FBScreenRecordingPromise.h" + +@interface FBScreenRecordingPromise () +@property (readwrite) id nativePromise; +@end + +@implementation FBScreenRecordingPromise + +- (instancetype)initWithNativePromise:(id)promise +{ + if ((self = [super init])) { + self.nativePromise = promise; + } + return self; +} + +- (NSUUID *)identifier +{ + return (NSUUID *)[self.nativePromise valueForKey:@"_UUID"]; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h new file mode 100644 index 000000000..f0a61ef24 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.h @@ -0,0 +1,39 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FBScreenRecordingRequest : NSObject + +/** The amount of video FPS */ +@property (readonly, nonatomic) NSUInteger fps; +/** Codec to use, where 0 is h264, 1 - HEVC */ +@property (readonly, nonatomic) long long codec; + +/** + Creates a custom wrapper for a screen recording reqeust + + @param fps FPS value, see baove + @param codec Codex value, see above + */ +- (instancetype)initWithFps:(NSUInteger)fps codec:(long long)codec; + +/** + Transforms the current wrapper instance to a native object, + which is ready to be passed to XCTest APIs + + @param error If there was a failure converting the instance to a native object + @returns Native object instance + */ +- (nullable id)toNativeRequestWithError:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m new file mode 100644 index 000000000..06f9d7790 --- /dev/null +++ b/WebDriverAgentLib/Routing/FBScreenRecordingRequest.m @@ -0,0 +1,94 @@ +/** + * 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 "FBScreenRecordingRequest.h" + +#import "FBErrorBuilder.h" +#import "XCUIScreen.h" + +@implementation FBScreenRecordingRequest + +- (instancetype)initWithFps:(NSUInteger)fps codec:(long long)codec +{ + if ((self = [super init])) { + _fps = fps; + _codec = codec; + } + return self; +} + +- (nullable id)createVideoEncodingWithError:(NSError **)error +{ + Class videoEncodingClass = NSClassFromString(@"XCTVideoEncoding"); + if (nil == videoEncodingClass) { + [[[FBErrorBuilder builder] + withDescription:@"Cannot find XCTVideoEncoding class"] + buildError:error]; + return nil; + } + + id videoEncodingAllocated = [videoEncodingClass alloc]; + SEL videoEncodingConstructorSelector = NSSelectorFromString(@"initWithCodec:frameRate:"); + if (![videoEncodingAllocated respondsToSelector:videoEncodingConstructorSelector]) { + [[[FBErrorBuilder builder] + withDescription:@"'initWithCodec:frameRate:' contructor is not found on XCTVideoEncoding class"] + buildError:error]; + return nil; + } + + NSMethodSignature *videoEncodingContructorSignature = [videoEncodingAllocated methodSignatureForSelector:videoEncodingConstructorSelector]; + NSInvocation *videoEncodingInitInvocation = [NSInvocation invocationWithMethodSignature:videoEncodingContructorSignature]; + [videoEncodingInitInvocation setSelector:videoEncodingConstructorSelector]; + long long codec = self.codec; + [videoEncodingInitInvocation setArgument:&codec atIndex:2]; + double frameRate = (double)self.fps; + [videoEncodingInitInvocation setArgument:&frameRate atIndex:3]; + [videoEncodingInitInvocation invokeWithTarget:videoEncodingAllocated]; + id __unsafe_unretained result; + [videoEncodingInitInvocation getReturnValue:&result]; + return result; +} + +- (id)toNativeRequestWithError:(NSError **)error +{ + Class screenRecordingRequestClass = NSClassFromString(@"XCTScreenRecordingRequest"); + if (nil == screenRecordingRequestClass) { + [[[FBErrorBuilder builder] + withDescription:@"Cannot find XCTScreenRecordingRequest class"] + buildError:error]; + return nil; + } + + id screenRecordingRequestAllocated = [screenRecordingRequestClass alloc]; + SEL screenRecordingRequestConstructorSelector = NSSelectorFromString(@"initWithScreenID:rect:preferredEncoding:"); + if (![screenRecordingRequestAllocated respondsToSelector:screenRecordingRequestConstructorSelector]) { + [[[FBErrorBuilder builder] + withDescription:@"'initWithScreenID:rect:preferredEncoding:' contructor is not found on XCTScreenRecordingRequest class"] + buildError:error]; + return nil; + } + id videoEncoding = [self createVideoEncodingWithError:error]; + if (nil == videoEncoding) { + return nil; + } + + NSMethodSignature *screenRecordingRequestContructorSignature = [screenRecordingRequestAllocated methodSignatureForSelector:screenRecordingRequestConstructorSelector]; + NSInvocation *screenRecordingRequestInitInvocation = [NSInvocation invocationWithMethodSignature:screenRecordingRequestContructorSignature]; + [screenRecordingRequestInitInvocation setSelector:screenRecordingRequestConstructorSelector]; + long long mainScreenId = XCUIScreen.mainScreen.displayID; + [screenRecordingRequestInitInvocation setArgument:&mainScreenId atIndex:2]; + CGRect fullScreenRect = CGRectNull; + [screenRecordingRequestInitInvocation setArgument:&fullScreenRect atIndex:3]; + [screenRecordingRequestInitInvocation setArgument:&videoEncoding atIndex:4]; + [screenRecordingRequestInitInvocation invokeWithTarget:screenRecordingRequestAllocated]; + id __unsafe_unretained result; + [screenRecordingRequestInitInvocation getReturnValue:&result]; + return result; +} + +@end diff --git a/WebDriverAgentLib/Routing/FBSession-Private.h b/WebDriverAgentLib/Routing/FBSession-Private.h index 45d5a8c3c..792fcd738 100644 --- a/WebDriverAgentLib/Routing/FBSession-Private.h +++ b/WebDriverAgentLib/Routing/FBSession-Private.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBSession.h b/WebDriverAgentLib/Routing/FBSession.h index be7f2bbba..61b1f8742 100644 --- a/WebDriverAgentLib/Routing/FBSession.h +++ b/WebDriverAgentLib/Routing/FBSession.h @@ -3,33 +3,35 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import -@class FBApplication; @class FBElementCache; +@class XCUIApplication; NS_ASSUME_NONNULL_BEGIN +/** Bundle identifier of Mobile Safari browser */ +extern NSString* const FB_SAFARI_BUNDLE_ID; + /** Class that represents testing session */ @interface FBSession : NSObject /*! Application tested during that session */ -@property (nonatomic, strong, readonly) FBApplication *activeApplication; +@property (nonatomic, readonly) XCUIApplication *activeApplication; /*! Session's identifier */ -@property (nonatomic, copy, readonly) NSString *identifier; +@property (nonatomic, readonly) NSString *identifier; /*! Element cache related to that session */ -@property (nonatomic, strong, readonly) FBElementCache *elementCache; +@property (nonatomic, readonly) FBElementCache *elementCache; /*! The identifier of the active application */ -@property (nonatomic, copy) NSString *defaultActiveApplication; +@property (nonatomic) NSString *defaultActiveApplication; /*! The action to apply to unexpected alerts. Either "accept"/"dismiss" or nil/empty string (by default) to do nothing */ @property (nonatomic, nullable) NSString *defaultAlertAction; @@ -57,7 +59,7 @@ NS_ASSUME_NONNULL_BEGIN @param application The application that we want to create session for @return new session */ -+ (instancetype)initWithApplication:(nullable FBApplication *)application; ++ (instancetype)initWithApplication:(nullable XCUIApplication *)application; /** Creates and saves new session for application with default alert handling behaviour @@ -66,7 +68,8 @@ NS_ASSUME_NONNULL_BEGIN @param defaultAlertAction The default reaction to on-screen alert. Either 'accept' or 'dismiss' @return new session */ -+ (instancetype)initWithApplication:(nullable FBApplication *)application defaultAlertAction:(NSString *)defaultAlertAction; ++ (instancetype)initWithApplication:(nullable XCUIApplication *)application + defaultAlertAction:(NSString *)defaultAlertAction; /** Kills application associated with that session and removes session @@ -82,12 +85,11 @@ NS_ASSUME_NONNULL_BEGIN @param arguments The optional array of application command line arguments. The arguments are going to be applied if the application was not running before. @param environment The optional dictionary of environment variables for the application, which is going to be executed. The environment variables are going to be applied if the application was not running before. @return The application instance - @throws FBApplicationMethodNotSupportedException if the method is not supported with the current XCTest SDK */ -- (FBApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier - shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence - arguments:(nullable NSArray *)arguments - environment:(nullable NSDictionary *)environment; +- (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier + shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence + arguments:(nullable NSArray *)arguments + environment:(nullable NSDictionary *)environment; /** Activate an application with given bundle identifier in scope of current session. @@ -95,9 +97,8 @@ NS_ASSUME_NONNULL_BEGIN @param bundleIdentifier Valid bundle identifier of the application to be activated @return The application instance - @throws FBApplicationMethodNotSupportedException if the method is not supported with the current XCTest SDK */ -- (FBApplication *)activateApplicationWithBundleId:(NSString *)bundleIdentifier; +- (XCUIApplication *)activateApplicationWithBundleId:(NSString *)bundleIdentifier; /** Terminate an application with the given bundle id. The application should be previously @@ -119,6 +120,22 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSUInteger)applicationStateWithBundleId:(NSString *)bundleIdentifier; +/** + Allows to enable automated session alerts monitoring. + Repeated calls are ignored if alerts monitoring has been already enabled. + + @returns YES if the actual alerts monitoring state has been changed + */ +- (BOOL)enableAlertsMonitor; + +/** + Allows to disable automated alerts monitoring + Repeated calls are ignored if alerts monitoring has been already disabled. + + @returns YES if the actual alerts monitoring state has been changed + */ +- (BOOL)disableAlertsMonitor; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Routing/FBSession.m b/WebDriverAgentLib/Routing/FBSession.m index 69d0fff78..b8de5edf6 100644 --- a/WebDriverAgentLib/Routing/FBSession.m +++ b/WebDriverAgentLib/Routing/FBSession.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBSession.h" @@ -14,14 +13,18 @@ #import "FBXCAccessibilityElement.h" #import "FBAlertsMonitor.h" -#import "FBApplication.h" #import "FBConfiguration.h" #import "FBElementCache.h" #import "FBExceptions.h" #import "FBMacros.h" +#import "FBScreenRecordingContainer.h" +#import "FBScreenRecordingPromise.h" +#import "FBScreenRecordingRequest.h" #import "FBXCodeCompatibility.h" +#import "FBXCTestDaemonsProxy.h" #import "XCUIApplication+FBQuiescence.h" #import "XCUIElement.h" +#import "XCUIElement+FBClassChain.h" /*! The intial value for the default application property. @@ -30,8 +33,10 @@ */ NSString *const FBDefaultApplicationAuto = @"auto"; +NSString *const FB_SAFARI_BUNDLE_ID = @"com.apple.mobilesafari"; + @interface FBSession () -@property (nonatomic) NSString *testedApplicationBundleId; +@property (nullable, nonatomic) XCUIApplication *testedApplication; @property (nonatomic) BOOL isTestedApplicationExpectedToRun; @property (nonatomic) BOOL shouldAppsWaitForQuiescence; @property (nonatomic, nullable) FBAlertsMonitor *alertsMonitor; @@ -48,6 +53,22 @@ @implementation FBSession (FBAlertsMonitorDelegate) - (void)didDetectAlert:(FBAlert *)alert { + NSString *autoClickAlertSelector = FBConfiguration.autoClickAlertSelector; + if ([autoClickAlertSelector length] > 0) { + @try { + NSArray *matches = [alert.alertElement fb_descendantsMatchingClassChain:autoClickAlertSelector + shouldReturnAfterFirstMatch:YES]; + if (matches.count > 0) { + [[matches objectAtIndex:0] tap]; + } + } @catch (NSException *e) { + [FBLogger logFmt:@"Could not click at the alert element '%@'. Original error: %@", + autoClickAlertSelector, e.description]; + } + // This setting has priority over other settings if enabled + return; + } + if (nil == self.defaultAlertAction || 0 == self.defaultAlertAction.length) { return; } @@ -96,7 +117,7 @@ + (instancetype)sessionWithIdentifier:(NSString *)identifier return _activeSession; } -+ (instancetype)initWithApplication:(FBApplication *)application ++ (instancetype)initWithApplication:(XCUIApplication *)application { FBSession *session = [FBSession new]; session.useNativeCachingStrategy = YES; @@ -105,10 +126,10 @@ + (instancetype)initWithApplication:(FBApplication *)application session.elementsVisibilityCache = [NSMutableDictionary dictionary]; session.identifier = [[NSUUID UUID] UUIDString]; session.defaultActiveApplication = FBDefaultApplicationAuto; - session.testedApplicationBundleId = nil; + session.testedApplication = nil; session.isTestedApplicationExpectedToRun = nil != application && application.running; if (application) { - session.testedApplicationBundleId = application.bundleID; + session.testedApplication = application; session.shouldAppsWaitForQuiescence = application.fb_shouldWaitForQuiescence; } session.elementCache = [FBElementCache new]; @@ -116,103 +137,143 @@ + (instancetype)initWithApplication:(FBApplication *)application return session; } -+ (instancetype)initWithApplication:(nullable FBApplication *)application ++ (instancetype)initWithApplication:(nullable XCUIApplication *)application defaultAlertAction:(NSString *)defaultAlertAction { FBSession *session = [self.class initWithApplication:application]; - session.alertsMonitor = [[FBAlertsMonitor alloc] init]; - session.alertsMonitor.delegate = (id)session; session.defaultAlertAction = [defaultAlertAction lowercaseString]; - [session.alertsMonitor enable]; + [session enableAlertsMonitor]; return session; } +- (BOOL)enableAlertsMonitor +{ + if (nil != self.alertsMonitor) { + return NO; + } + + self.alertsMonitor = [[FBAlertsMonitor alloc] init]; + self.alertsMonitor.delegate = (id)self; + [self.alertsMonitor enable]; + return YES; +} + +- (BOOL)disableAlertsMonitor +{ + if (nil == self.alertsMonitor) { + return NO; + } + + [self.alertsMonitor disable]; + self.alertsMonitor = nil; + return YES; +} + - (void)kill { if (nil == _activeSession) { return; } - if (nil != self.alertsMonitor) { - [self.alertsMonitor disable]; - self.alertsMonitor = nil; - } - - if (self.testedApplicationBundleId && [FBConfiguration shouldTerminateApp] - && ![self.testedApplicationBundleId isEqualToString:FBApplication.fb_systemApplication.bundleID]) { - FBApplication *app = [[FBApplication alloc] initWithBundleIdentifier:self.testedApplicationBundleId]; - if ([app running]) { - @try { - [app terminate]; - } @catch (NSException *e) { - [FBLogger logFmt:@"%@", e.description]; - } + [self disableAlertsMonitor]; + + FBScreenRecordingPromise *activeScreenRecording = FBScreenRecordingContainer.sharedInstance.screenRecordingPromise; + if (nil != activeScreenRecording) { + NSError *error; + if (![FBXCTestDaemonsProxy stopScreenRecordingWithUUID:activeScreenRecording.identifier error:&error]) { + [FBLogger logFmt:@"%@", error]; + } + [FBScreenRecordingContainer.sharedInstance reset]; + } + + if (nil != self.testedApplication + && FBConfiguration.shouldTerminateApp + && self.testedApplication.running + && ![self.testedApplication fb_isSameAppAs:XCUIApplication.fb_systemApplication]) { + @try { + [self.testedApplication terminate]; + } @catch (NSException *e) { + [FBLogger logFmt:@"%@", e.description]; } } _activeSession = nil; } -- (FBApplication *)activeApplication +- (XCUIApplication *)activeApplication { - NSString *defaultBundleId = [self.defaultActiveApplication isEqualToString:FBDefaultApplicationAuto] - ? nil - : self.defaultActiveApplication; - FBApplication *application = [FBApplication fb_activeApplicationWithDefaultBundleId:defaultBundleId]; - FBApplication *testedApplication = nil; - if (self.testedApplicationBundleId && self.isTestedApplicationExpectedToRun) { - testedApplication = nil != application.bundleID && [application.bundleID isEqualToString:self.testedApplicationBundleId] - ? application - : [[FBApplication alloc] initWithBundleIdentifier:self.testedApplicationBundleId]; - } - if (testedApplication && !testedApplication.running) { - NSString *description = [NSString stringWithFormat:@"The application under test with bundle id '%@' is not running, possibly crashed", self.testedApplicationBundleId]; - [[NSException exceptionWithName:FBApplicationCrashedException reason:description userInfo:nil] raise]; - } - return application; + BOOL isAuto = [self.defaultActiveApplication isEqualToString:FBDefaultApplicationAuto]; + NSString *defaultBundleId = isAuto ? nil : self.defaultActiveApplication; + + if (nil != defaultBundleId && [self applicationStateWithBundleId:defaultBundleId] >= XCUIApplicationStateRunningForeground) { + return [self makeApplicationWithBundleId:defaultBundleId]; + } + + if (nil != self.testedApplication) { + XCUIApplicationState testedAppState = self.testedApplication.state; + if (testedAppState >= XCUIApplicationStateRunningForeground) { + NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"%K == %@ OR %K IN {%@, %@}", + @"elementType", @(XCUIElementTypeAlert), + // To look for `SBTransientOverlayWindow` elements. See https://github.com/appium/WebDriverAgent/pull/946 + @"identifier", @"SBTransientOverlayWindow", + // To look for 'criticalAlertSetting' elements https://developer.apple.com/documentation/usernotifications/unnotificationsettings/criticalalertsetting + // See https://github.com/appium/appium/issues/20835 + @"NotificationShortLookView"]; + if ([FBConfiguration shouldRespectSystemAlerts] + && [[XCUIApplication.fb_systemApplication descendantsMatchingType:XCUIElementTypeAny] + matchingPredicate:searchPredicate].count > 0) { + return XCUIApplication.fb_systemApplication; + } + return (XCUIApplication *)self.testedApplication; + } + if (self.isTestedApplicationExpectedToRun && testedAppState <= XCUIApplicationStateNotRunning) { + NSString *description = [NSString stringWithFormat:@"The application under test with bundle id '%@' is not running, possibly crashed", self.testedApplication.bundleID]; + @throw [NSException exceptionWithName:FBApplicationCrashedException reason:description userInfo:nil]; + } + } + + return [XCUIApplication fb_activeApplicationWithDefaultBundleId:defaultBundleId]; } -- (FBApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier - shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence - arguments:(nullable NSArray *)arguments - environment:(nullable NSDictionary *)environment +- (XCUIApplication *)launchApplicationWithBundleId:(NSString *)bundleIdentifier + shouldWaitForQuiescence:(nullable NSNumber *)shouldWaitForQuiescence + arguments:(nullable NSArray *)arguments + environment:(nullable NSDictionary *)environment { - FBApplication *app = [[FBApplication alloc] initWithBundleIdentifier:bundleIdentifier]; + XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier]; if (nil == shouldWaitForQuiescence) { // Iherit the quiescence check setting from the main app under test by default - app.fb_shouldWaitForQuiescence = nil != self.testedApplicationBundleId && self.shouldAppsWaitForQuiescence; + app.fb_shouldWaitForQuiescence = nil != self.testedApplication && self.shouldAppsWaitForQuiescence; } else { app.fb_shouldWaitForQuiescence = [shouldWaitForQuiescence boolValue]; } - if (app.fb_state < 2) { + if (!app.running) { app.launchArguments = arguments ?: @[]; app.launchEnvironment = environment ?: @{}; [app launch]; } else { - [app fb_activate]; + [app activate]; } - if (nil != self.testedApplicationBundleId - && [bundleIdentifier isEqualToString:(NSString *)self.testedApplicationBundleId]) { + if ([app fb_isSameAppAs:self.testedApplication]) { self.isTestedApplicationExpectedToRun = YES; } return app; } -- (FBApplication *)activateApplicationWithBundleId:(NSString *)bundleIdentifier +- (XCUIApplication *)activateApplicationWithBundleId:(NSString *)bundleIdentifier { - FBApplication *app = [[FBApplication alloc] initWithBundleIdentifier:bundleIdentifier]; - [app fb_activate]; + XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier]; + [app activate]; return app; } - (BOOL)terminateApplicationWithBundleId:(NSString *)bundleIdentifier { - FBApplication *app = [[FBApplication alloc] initWithBundleIdentifier:bundleIdentifier]; - if (nil != self.testedApplicationBundleId - && [bundleIdentifier isEqualToString:(NSString *)self.testedApplicationBundleId]) { + XCUIApplication *app = [self makeApplicationWithBundleId:bundleIdentifier]; + if ([app fb_isSameAppAs:self.testedApplication]) { self.isTestedApplicationExpectedToRun = NO; } - if (app.fb_state >= 2) { + if (app.running) { [app terminate]; return YES; } @@ -221,7 +282,14 @@ - (BOOL)terminateApplicationWithBundleId:(NSString *)bundleIdentifier - (NSUInteger)applicationStateWithBundleId:(NSString *)bundleIdentifier { - return [[FBApplication alloc] initWithBundleIdentifier:bundleIdentifier].fb_state; + return [self makeApplicationWithBundleId:bundleIdentifier].state; +} + +- (XCUIApplication *)makeApplicationWithBundleId:(NSString *)bundleIdentifier +{ + return nil != self.testedApplication && [bundleIdentifier isEqualToString:(NSString *)self.testedApplication.bundleID] + ? self.testedApplication + : [[XCUIApplication alloc] initWithBundleIdentifier:bundleIdentifier]; } @end diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.h b/WebDriverAgentLib/Routing/FBTCPSocket.h index 895ae75f5..72947e2a0 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.h +++ b/WebDriverAgentLib/Routing/FBTCPSocket.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "GCDAsyncSocket.h" @@ -39,7 +38,11 @@ NS_ASSUME_NONNULL_BEGIN @interface FBTCPSocket : NSObject -@property (nullable, nonatomic) id delegate; +#if __has_feature(objc_arc_weak) +@property (nullable, nonatomic, weak) id delegate; +#else +@property (nullable, nonatomic, assign) id delegate; +#endif /** Creates TCP socket isntance which is going to be started on the specified port diff --git a/WebDriverAgentLib/Routing/FBTCPSocket.m b/WebDriverAgentLib/Routing/FBTCPSocket.m index 527acdf4a..e23876d83 100644 --- a/WebDriverAgentLib/Routing/FBTCPSocket.m +++ b/WebDriverAgentLib/Routing/FBTCPSocket.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTCPSocket.h" @@ -40,7 +39,7 @@ - (instancetype)initWithPort:(uint16_t)port - (BOOL)startWithError:(NSError **)error { if (![self.listeningSocket acceptOnPort:self.port error:error]) { - return NO;; + return NO; } return YES; @@ -49,11 +48,14 @@ - (BOOL)startWithError:(NSError **)error - (void)stop { @synchronized(self.connectedClients) { - for (NSUInteger i = 0; i < [self.connectedClients count]; i++) { - [[self.connectedClients objectAtIndex:i] disconnect]; + NSArray *clients = self.connectedClients.copy; + [self.connectedClients removeAllObjects]; + for (GCDAsyncSocket *client in clients) { + [client disconnect]; } } + self.delegate = nil; [self.listeningSocket disconnect]; } @@ -67,12 +69,18 @@ - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSo @synchronized(self.connectedClients) { [self.connectedClients addObject:newSocket]; } - [self.delegate didClientConnect:newSocket]; + id delegate = self.delegate; + if (nil != delegate) { + [delegate didClientConnect:newSocket]; + } } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { - [self.delegate didClientSendData:sock]; + id delegate = self.delegate; + if (nil != delegate) { + [delegate didClientSendData:sock]; + } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err @@ -80,7 +88,10 @@ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err @synchronized(self.connectedClients) { [self.connectedClients removeObject:sock]; } - [self.delegate didClientDisconnect:sock]; + id delegate = self.delegate; + if (nil != delegate) { + [delegate didClientDisconnect:sock]; + } } @end diff --git a/WebDriverAgentLib/Routing/FBWebServer.h b/WebDriverAgentLib/Routing/FBWebServer.h index a857a10c9..34bca6515 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.h +++ b/WebDriverAgentLib/Routing/FBWebServer.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBWebServer.m b/WebDriverAgentLib/Routing/FBWebServer.m index 7e991e0e2..29a2e16e6 100644 --- a/WebDriverAgentLib/Routing/FBWebServer.m +++ b/WebDriverAgentLib/Routing/FBWebServer.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBWebServer.h" @@ -48,10 +47,16 @@ @interface FBWebServer () @property (nonatomic, strong) RoutingHTTPServer *server; @property (atomic, assign) BOOL keepAlive; @property (nonatomic, nullable) FBTCPSocket *screenshotsBroadcaster; +@property (nonatomic, nullable, strong) FBMjpegServer *mjpegServer; @end @implementation FBWebServer +- (void)dealloc +{ + [self stopScreenshotsBroadcaster]; +} + + (NSArray> *)collectCommandHandlerClasses { NSArray *handlersClasses = FBClassesThatConformsToProtocol(@protocol(FBCommandHandler)); @@ -93,6 +98,12 @@ - (void)startHTTPServer [self registerServerKeyRouteHandlers]; NSRange serverPortRange = FBConfiguration.bindingPortRange; + NSString *bindingIP = FBConfiguration.bindingIPAddress; + if (bindingIP != nil) { + [self.server setInterface:bindingIP]; + [FBLogger logFmt:@"Using custom binding IP address: %@", bindingIP]; + } + NSError *error; BOOL serverStarted = NO; @@ -112,18 +123,23 @@ - (void)startHTTPServer [FBLogger logFmt:@"Last attempt to start web server failed with error %@", [error description]]; abort(); } - [FBLogger logFmt:@"%@http://%@:%d%@", FBServerURLBeginMarker, [XCUIDevice sharedDevice].fb_wifiIPAddress ?: @"localhost", [self.server port], FBServerURLEndMarker]; + + NSString *serverHost = bindingIP ?: ([XCUIDevice sharedDevice].fb_wifiIPAddress ?: @"127.0.0.1"); + [FBLogger logFmt:@"%@http://%@:%d%@", FBServerURLBeginMarker, serverHost, [self.server port], FBServerURLEndMarker]; } - (void)initScreenshotsBroadcaster { [self readMjpegSettingsFromEnv]; + self.mjpegServer = [[FBMjpegServer alloc] init]; self.screenshotsBroadcaster = [[FBTCPSocket alloc] initWithPort:(uint16_t)FBConfiguration.mjpegServerPort]; - self.screenshotsBroadcaster.delegate = [[FBMjpegServer alloc] init]; + self.screenshotsBroadcaster.delegate = self.mjpegServer; NSError *error; if (![self.screenshotsBroadcaster startWithError:&error]) { [FBLogger logFmt:@"Cannot init screenshots broadcaster service on port %@. Original error: %@", @(FBConfiguration.mjpegServerPort), error.description]; + [self.mjpegServer stopStreaming]; + self.mjpegServer = nil; self.screenshotsBroadcaster = nil; } } @@ -131,10 +147,18 @@ - (void)initScreenshotsBroadcaster - (void)stopScreenshotsBroadcaster { if (nil == self.screenshotsBroadcaster) { + self.mjpegServer = nil; return; } + id delegate = self.screenshotsBroadcaster.delegate; + if ([(NSObject *)delegate respondsToSelector:@selector(stopStreaming)]) { + [(FBMjpegServer *)delegate stopStreaming]; + } + self.screenshotsBroadcaster.delegate = nil; [self.screenshotsBroadcaster stop]; + self.screenshotsBroadcaster = nil; + self.mjpegServer = nil; } - (void)readMjpegSettingsFromEnv @@ -142,7 +166,7 @@ - (void)readMjpegSettingsFromEnv NSDictionary *env = NSProcessInfo.processInfo.environment; NSString *scalingFactor = [env objectForKey:@"MJPEG_SCALING_FACTOR"]; if (scalingFactor != nil && [scalingFactor length] > 0) { - [FBConfiguration setMjpegScalingFactor:[scalingFactor integerValue]]; + [FBConfiguration setMjpegScalingFactor:[scalingFactor floatValue]]; } NSString *screenshotQuality = [env objectForKey:@"MJPEG_SERVER_SCREENSHOT_QUALITY"]; if (screenshotQuality != nil && [screenshotQuality length] > 0) { @@ -157,6 +181,8 @@ - (void)stopServing if (self.server.isRunning) { [self.server stop:NO]; } + self.server = nil; + self.exceptionHandler = nil; self.keepAlive = NO; } @@ -185,10 +211,15 @@ - (BOOL)attemptToStartServer:(RoutingHTTPServer *)server onPort:(NSInteger)port - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses { + __weak typeof(self) weakSelf = self; 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) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } NSDictionary *arguments = [NSJSONSerialization JSONObjectWithData:request.body options:NSJSONReadingMutableContainers error:NULL]; FBRouteRequest *routeParams = [FBRouteRequest routeRequestWithURL:request.url @@ -202,7 +233,7 @@ - (void)registerRouteHandlers:(NSArray *)commandHandlerClasses [route mountRequest:routeParams intoResponse:response]; } @catch (NSException *exception) { - [self handleException:exception forResponse:response]; + [strongSelf handleException:exception forResponse:response]; } }]; } @@ -217,7 +248,7 @@ - (void)handleException:(NSException *)exception forResponse:(RouteResponse *)re - (void)registerServerKeyRouteHandlers { [self.server get:@"/health" withBlock:^(RouteRequest *request, RouteResponse *response) { - [response respondWithString:@"I-AM-ALIVE"]; + [response respondWithString:@"Health Check

I-AM-ALIVE

"]; }]; NSString *calibrationPage = @"" @@ -230,9 +261,14 @@ - (void)registerServerKeyRouteHandlers [response respondWithString:calibrationPage]; }]; + __weak typeof(self) weakSelf = self; [self.server get:@"/wda/shutdown" withBlock:^(RouteRequest *request, RouteResponse *response) { + __strong typeof(weakSelf) strongSelf = weakSelf; + if (nil == strongSelf) { + return; + } [response respondWithString:@"Shutting down"]; - [self.delegate webServerDidRequestShutdown:self]; + [strongSelf.delegate webServerDidRequestShutdown:strongSelf]; }]; [self registerRouteHandlers:@[FBUnknownCommands.class]]; diff --git a/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h b/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h index 701edcd8d..dd35a3785 100644 --- a/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h +++ b/WebDriverAgentLib/Routing/FBXCAccessibilityElement.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m b/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m index 4535be676..39f22913c 100644 --- a/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m +++ b/WebDriverAgentLib/Routing/FBXCAccessibilityElement.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCAccessibilityElement.h" diff --git a/WebDriverAgentLib/Routing/FBXCDeviceEvent.h b/WebDriverAgentLib/Routing/FBXCDeviceEvent.h index b27b667e0..64139f9f6 100644 --- a/WebDriverAgentLib/Routing/FBXCDeviceEvent.h +++ b/WebDriverAgentLib/Routing/FBXCDeviceEvent.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBXCDeviceEvent.m b/WebDriverAgentLib/Routing/FBXCDeviceEvent.m index 8f2c9609b..6a673e151 100644 --- a/WebDriverAgentLib/Routing/FBXCDeviceEvent.m +++ b/WebDriverAgentLib/Routing/FBXCDeviceEvent.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCDeviceEvent.h" diff --git a/WebDriverAgentLib/Routing/FBXCElementSnapshot.h b/WebDriverAgentLib/Routing/FBXCElementSnapshot.h index a3076ec25..500d9a846 100644 --- a/WebDriverAgentLib/Routing/FBXCElementSnapshot.h +++ b/WebDriverAgentLib/Routing/FBXCElementSnapshot.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Routing/FBXCElementSnapshot.m b/WebDriverAgentLib/Routing/FBXCElementSnapshot.m index 11d106890..5fe602048 100644 --- a/WebDriverAgentLib/Routing/FBXCElementSnapshot.m +++ b/WebDriverAgentLib/Routing/FBXCElementSnapshot.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCElementSnapshot.h" diff --git a/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h index e9b763143..2309d6b58 100644 --- a/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h +++ b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCElementSnapshot.h" diff --git a/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m index 1beb87bb2..4b3e0e7f5 100644 --- a/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m +++ b/WebDriverAgentLib/Routing/FBXCElementSnapshotWrapper.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCElementSnapshotWrapper.h" diff --git a/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h b/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h new file mode 100644 index 000000000..b5ffa5998 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBAccessibilityTraits.h @@ -0,0 +1,20 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + Converts accessibility traits bitmask to an array of string representations + @param traits The accessibility traits bitmask + @return Array of strings representing the accessibility traits + */ +NSArray *FBAccessibilityTraitsToStringsArray(unsigned long long traits); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m b/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m new file mode 100644 index 000000000..74ce9ec7c --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBAccessibilityTraits.m @@ -0,0 +1,61 @@ +/** + * 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 "FBAccessibilityTraits.h" + +NSArray *FBAccessibilityTraitsToStringsArray(unsigned long long traits) { + NSMutableArray *traitStringsArray; + NSNumber *key; + + static NSDictionary *traitsMapping; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + NSMutableDictionary *mapping = [@{ + @(UIAccessibilityTraitNone): @"None", + @(UIAccessibilityTraitButton): @"Button", + @(UIAccessibilityTraitLink): @"Link", + @(UIAccessibilityTraitHeader): @"Header", + @(UIAccessibilityTraitSearchField): @"SearchField", + @(UIAccessibilityTraitImage): @"Image", + @(UIAccessibilityTraitSelected): @"Selected", + @(UIAccessibilityTraitPlaysSound): @"PlaysSound", + @(UIAccessibilityTraitKeyboardKey): @"KeyboardKey", + @(UIAccessibilityTraitStaticText): @"StaticText", + @(UIAccessibilityTraitSummaryElement): @"SummaryElement", + @(UIAccessibilityTraitNotEnabled): @"NotEnabled", + @(UIAccessibilityTraitUpdatesFrequently): @"UpdatesFrequently", + @(UIAccessibilityTraitStartsMediaSession): @"StartsMediaSession", + @(UIAccessibilityTraitAdjustable): @"Adjustable", + @(UIAccessibilityTraitAllowsDirectInteraction): @"AllowsDirectInteraction", + @(UIAccessibilityTraitCausesPageTurn): @"CausesPageTurn", + @(UIAccessibilityTraitTabBar): @"TabBar" + } mutableCopy]; + + #if __clang_major__ >= 16 + // Add iOS 17.0 specific traits if available + if (@available(iOS 17.0, *)) { + [mapping addEntriesFromDictionary:@{ + @(UIAccessibilityTraitToggleButton): @"ToggleButton", + @(UIAccessibilityTraitSupportsZoom): @"SupportsZoom" + }]; + } + #endif + + traitsMapping = [mapping copy]; + }); + + traitStringsArray = [NSMutableArray array]; + for (key in traitsMapping) { + if (traits & [key unsignedLongLongValue] && nil != traitsMapping[key]) { + [traitStringsArray addObject:(id)traitsMapping[key]]; + } + } + + return [traitStringsArray copy]; +} diff --git a/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h b/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h index 6bc4887e9..a65bd3880 100644 --- a/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h +++ b/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m b/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m index db571b584..2f05ab878 100644 --- a/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m +++ b/WebDriverAgentLib/Utilities/FBActiveAppDetectionPoint.m @@ -3,8 +3,7 @@ * 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. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "FBActiveAppDetectionPoint.h" diff --git a/WebDriverAgentLib/Utilities/FBAlertsMonitor.h b/WebDriverAgentLib/Utilities/FBAlertsMonitor.h index 79bb6a3f1..069a73474 100644 --- a/WebDriverAgentLib/Utilities/FBAlertsMonitor.h +++ b/WebDriverAgentLib/Utilities/FBAlertsMonitor.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBAlertsMonitor.m b/WebDriverAgentLib/Utilities/FBAlertsMonitor.m index c2c625084..3dd221198 100644 --- a/WebDriverAgentLib/Utilities/FBAlertsMonitor.m +++ b/WebDriverAgentLib/Utilities/FBAlertsMonitor.m @@ -3,16 +3,15 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBAlertsMonitor.h" #import "FBAlert.h" -#import "FBApplication.h" #import "FBLogger.h" #import "XCUIApplication+FBAlert.h" +#import "XCUIApplication+FBHelpers.h" static const NSTimeInterval FB_MONTORING_INTERVAL = 2.0; @@ -49,8 +48,8 @@ - (void)scheduleNextTick } dispatch_async(dispatch_get_main_queue(), ^{ - NSArray *activeApps = FBApplication.fb_activeApplications; - for (FBApplication *activeApp in activeApps) { + NSArray *activeApps = XCUIApplication.fb_activeApplications; + for (XCUIApplication *activeApp in activeApps) { XCUIElement *alertElement = nil; @try { alertElement = activeApp.fb_alertElement; diff --git a/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.h b/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.h deleted file mode 100644 index 090bacdd7..000000000 --- a/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.h +++ /dev/null @@ -1,20 +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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "FBBaseActionsSynthesizer.h" - -NS_ASSUME_NONNULL_BEGIN - -#if !TARGET_OS_TV -@interface FBAppiumActionsSynthesizer : FBBaseActionsSynthesizer - -@end -#endif - -NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.m b/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.m deleted file mode 100644 index 235d97e63..000000000 --- a/WebDriverAgentLib/Utilities/FBAppiumActionsSynthesizer.m +++ /dev/null @@ -1,554 +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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "FBAppiumActionsSynthesizer.h" - -#import "FBErrorBuilder.h" -#import "FBElementCache.h" -#import "FBLogger.h" -#import "FBMacros.h" -#import "FBMathUtils.h" -#import "FBXCTestDaemonsProxy.h" -#import "FBProtocolHelpers.h" -#import "XCUIElement+FBUtilities.h" -#import "XCUIElement.h" -#import "XCSynthesizedEventRecord.h" -#import "XCPointerEventPath.h" -#import "XCPointerEvent.h" - -static NSString *const FB_ACTION_KEY = @"action"; -static NSString *const FB_ACTION_TAP = @"tap"; -static NSString *const FB_ACTION_PRESS = @"press"; -static NSString *const FB_ACTION_LONG_PRESS = @"longPress"; -static NSString *const FB_ACTION_MOVE_TO = @"moveTo"; -static NSString *const FB_ACTION_RELEASE = @"release"; -static NSString *const FB_ACTION_CANCEL = @"cancel"; -static NSString *const FB_ACTION_WAIT = @"wait"; - -static NSString *const FB_OPTION_DURATION = @"duration"; -static NSString *const FB_OPTION_COUNT = @"count"; -static NSString *const FB_OPTION_MS = @"ms"; -static NSString *const FB_OPTION_PRESSURE = @"pressure"; - -static NSString *const FB_OPTIONS_KEY = @"options"; - -#if !TARGET_OS_TV -// Some useful constants might be found at -// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewConfiguration.java -static const double FB_TAP_DURATION_MS = 100.0; -static const double FB_INTERTAP_MIN_DURATION_MS = 40.0; -static const double FB_LONG_TAP_DURATION_MS = 600.0; - -@interface FBAppiumGestureItem : FBBaseGestureItem - -@end - -@interface FBTapItem : FBAppiumGestureItem - -@end - -@interface FBPressItem : FBAppiumGestureItem -@property (nonatomic, nullable, readonly) NSNumber *pressure; -@end - -@interface FBLongPressItem : FBAppiumGestureItem - -@end - -@interface FBWaitItem : FBAppiumGestureItem - -@end - -@interface FBMoveToItem : FBAppiumGestureItem - -@property (nonatomic, nonnull) NSValue *recentPosition; - -@end - -@interface FBReleaseItem : FBAppiumGestureItem - -@end - - -@implementation FBAppiumGestureItem - -- (nullable instancetype)initWithActionItem:(NSDictionary *)item - application:(XCUIApplication *)application - atPosition:(nullable XCUICoordinate *)atPosition - offset:(double)offset - error:(NSError **)error -{ - self = [super init]; - if (self) { - self.actionItem = item; - self.application = application; - self.offset = offset; - id options = [item objectForKey:FB_OPTIONS_KEY]; - if (nil != atPosition) { - self.atPosition = (id) atPosition; - } else { - XCUICoordinate *result = [self coordinatesWithOptions:options error:error]; - if (nil == result) { - return nil; - } - self.atPosition = result; - } - self.duration = [self durationWithOptions:options]; - if (self.duration < 0) { - NSString *description = [NSString stringWithFormat:@"%@ value cannot be negative for '%@' action", FB_OPTION_DURATION, self.class.actionName]; - if (error) { - *error = [[FBErrorBuilder.builder withDescription:description] build]; - } - return nil; - } - } - return self; -} - -+ (BOOL)hasAbsolutePositioning -{ - @throw [[FBErrorBuilder.builder withDescription:@"Override this method in subclasses"] build]; - return NO; -} - -- (double)durationWithOptions:(nullable NSDictionary *)options -{ - return (options && [options objectForKey:FB_OPTION_DURATION]) ? - ((NSNumber *)[options objectForKey:FB_OPTION_DURATION]).doubleValue : - 0.0; -} - -- (nullable XCUICoordinate *)coordinatesWithOptions:(nullable NSDictionary *)options - error:(NSError **)error -{ - if (![options isKindOfClass:NSDictionary.class]) { - NSString *description = [NSString stringWithFormat:@"'%@' key is mandatory for '%@' action", FB_OPTIONS_KEY, self.class.actionName]; - if (error) { - *error = [[FBErrorBuilder.builder withDescription:description] build]; - } - return nil; - } - XCUIElement *element = FBExtractElement((id) options); - NSNumber *x = [options objectForKey:@"x"]; - NSNumber *y = [options objectForKey:@"y"]; - if ((nil != x && nil == y) || (nil != y && nil == x) || (nil == x && nil == y && nil == element)) { - NSString *description = [NSString stringWithFormat:@"Either element or 'x' and 'y' options should be set for '%@' action", self.class.actionName]; - if (error) { - *error = [[FBErrorBuilder.builder withDescription:description] build]; - } - return nil; - } - NSValue *offset = (nil != x && nil != y) ? [NSValue valueWithCGPoint:CGPointMake(x.floatValue, y.floatValue)] : nil; - return [self hitpointWithElement:element positionOffset:offset error:error]; -} - -@end - -@implementation FBTapItem - -+ (NSString *)actionName -{ - return FB_ACTION_TAP; -} - -+ (BOOL)hasAbsolutePositioning -{ - return YES; -} - -- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath - allItems:(NSArray *)allItems - currentItemIndex:(NSUInteger)currentItemIndex - error:(NSError **)error -{ - NSTimeInterval currentOffset = FBMillisToSeconds(self.offset); - NSMutableArray *result = [NSMutableArray array]; - XCPointerEventPath *currentPath = [[XCPointerEventPath alloc] - initForTouchAtPoint:self.atPosition.screenPoint - offset:currentOffset]; - [result addObject:currentPath]; - currentOffset += FBMillisToSeconds(FB_TAP_DURATION_MS); - [currentPath liftUpAtOffset:currentOffset]; - - id options = [self.actionItem objectForKey:FB_OPTIONS_KEY]; - if ([options isKindOfClass:NSDictionary.class]) { - NSNumber *tapCount = [options objectForKey:FB_OPTION_COUNT] ?: @1; - for (NSInteger times = 1; times < tapCount.integerValue; ++times) { - currentOffset += FBMillisToSeconds(FB_INTERTAP_MIN_DURATION_MS); - XCPointerEventPath *nextPath = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint - offset:currentOffset]; - [result addObject:nextPath]; - currentOffset += FBMillisToSeconds(FB_TAP_DURATION_MS); - [nextPath liftUpAtOffset:currentOffset]; - } - } - return result.copy; -} - -- (double)durationWithOptions:(nullable NSDictionary *)options -{ - NSNumber *tapCount = @1; - if ([options isKindOfClass:NSDictionary.class]) { - tapCount = [options objectForKey:FB_OPTION_COUNT] ?: tapCount; - } - return FB_TAP_DURATION_MS * tapCount.integerValue + FB_INTERTAP_MIN_DURATION_MS * (tapCount.integerValue - 1); -} - -@end - -@implementation FBPressItem - -- (nullable instancetype)initWithActionItem:(NSDictionary *)item - application:(XCUIApplication *)application - atPosition:(nullable XCUICoordinate *)atPosition - offset:(double)offset - error:(NSError **)error -{ - self = [super initWithActionItem:item - application:application - atPosition:atPosition - offset:offset - error:error]; - if (self) { - _pressure = nil; - id options = [item objectForKey:FB_OPTIONS_KEY]; - if ([options isKindOfClass:NSDictionary.class]) { - _pressure = [options objectForKey:FB_OPTION_PRESSURE]; - } - } - return self; -} - -+ (NSString *)actionName -{ - return FB_ACTION_PRESS; -} - -+ (BOOL)hasAbsolutePositioning -{ - return YES; -} - -- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath - allItems:(NSArray *)allItems - currentItemIndex:(NSUInteger)currentItemIndex - error:(NSError **)error -{ - XCPointerEventPath *result = [[XCPointerEventPath alloc] - initForTouchAtPoint:self.atPosition.screenPoint - offset:FBMillisToSeconds(self.offset)]; - if (nil != self.pressure && nil != result.pointerEvents.lastObject) { - XCPointerEvent *pointerEvent = (XCPointerEvent *)result.pointerEvents.lastObject; - pointerEvent.pressure = self.pressure.doubleValue; - } - return @[result]; -} - -- (double)durationWithOptions:(nullable NSDictionary *)options -{ - return 0.0; -} - -@end - -@implementation FBLongPressItem - -+ (NSString *)actionName -{ - return FB_ACTION_LONG_PRESS; -} - -+ (BOOL)hasAbsolutePositioning -{ - return YES; -} - -- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath - allItems:(NSArray *)allItems - currentItemIndex:(NSUInteger)currentItemIndex - error:(NSError **)error -{ - return @[[[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint - offset:FBMillisToSeconds(self.offset)]]; -} - -- (double)durationWithOptions:(nullable NSDictionary *)options -{ - return (options && [options objectForKey:FB_OPTION_DURATION]) ? - ((NSNumber *)[options objectForKey:FB_OPTION_DURATION]).doubleValue : - FB_LONG_TAP_DURATION_MS; -} - -@end - -@implementation FBWaitItem - -+ (NSString *)actionName -{ - return FB_ACTION_WAIT; -} - -+ (BOOL)hasAbsolutePositioning -{ - return NO; -} - -- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath - allItems:(NSArray *)allItems - currentItemIndex:(NSUInteger)currentItemIndex - error:(NSError **)error -{ - if (nil != eventPath) { - if (0 == currentItemIndex) { - return @[]; - } - FBBaseGestureItem *preceedingItem = [allItems objectAtIndex:currentItemIndex - 1]; - if (![preceedingItem isKindOfClass:FBReleaseItem.class] && currentItemIndex < allItems.count - 1) { - return @[]; - } - } - NSTimeInterval currentOffset = FBMillisToSeconds(self.offset + self.duration); - XCPointerEventPath *result = [[XCPointerEventPath alloc] initForTouchAtPoint:self.atPosition.screenPoint - offset:currentOffset]; - if (currentItemIndex == allItems.count - 1) { - [result liftUpAtOffset:currentOffset]; - } - return @[result]; -} - -- (double)durationWithOptions:(nullable NSDictionary *)options -{ - return (options && [options objectForKey:FB_OPTION_MS]) ? - ((NSNumber *)[options objectForKey:FB_OPTION_MS]).doubleValue : - 0.0; -} - -@end - -@implementation FBMoveToItem - -+ (NSString *)actionName -{ - return FB_ACTION_MOVE_TO; -} - -+ (BOOL)hasAbsolutePositioning -{ - return YES; -} - -- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath - allItems:(NSArray *)allItems - currentItemIndex:(NSUInteger)currentItemIndex - error:(NSError **)error -{ - if (nil == eventPath) { - NSString *description = [NSString stringWithFormat:@"Move To must not be the first action in '%@'", self.actionItem]; - if (error) { - *error = [[FBErrorBuilder.builder withDescription:description] build]; - } - return nil; - } - - [eventPath moveToPoint:self.atPosition.screenPoint - atOffset:FBMillisToSeconds(self.offset)]; - return @[]; -} - -@end - -@implementation FBReleaseItem - -+ (NSString *)actionName -{ - return FB_ACTION_RELEASE; -} - -+ (BOOL)hasAbsolutePositioning -{ - return NO; -} - -- (NSArray *)addToEventPath:(XCPointerEventPath *)eventPath - allItems:(NSArray *)allItems - currentItemIndex:(NSUInteger)currentItemIndex - error:(NSError **)error -{ - if (nil == eventPath) { - NSString *description = [NSString stringWithFormat:@"Pointer Up must not be the first action in '%@'", self.actionItem]; - if (error) { - *error = [[FBErrorBuilder.builder withDescription:description] build]; - } - return nil; - } - - [eventPath liftUpAtOffset:FBMillisToSeconds(self.offset)]; - return @[]; -} - -- (double)durationWithOptions:(nullable NSDictionary *)options -{ - return 0.0; -} - -@end - - -@interface FBAppiumGestureItemsChain : FBBaseActionItemsChain - -@end - -@implementation FBAppiumGestureItemsChain - -- (void)addItem:(FBBaseActionItem *)item -{ - self.durationOffset += ((FBAppiumGestureItem *) item).duration; - [self.items addObject:item]; -} - -- (void)reset -{ - [self.items removeAllObjects]; - self.durationOffset = 0.0; -} - -@end - -@implementation FBAppiumActionsSynthesizer - -- (NSArray *> *)preprocessAction:(NSArray *> *)touchActionItems -{ - NSMutableArray *> *result = [NSMutableArray array]; - BOOL shouldSkipNextItem = NO; - for (NSDictionary *touchItem in [touchActionItems reverseObjectEnumerator]) { - id actionItemName = [touchItem objectForKey:FB_ACTION_KEY]; - if ([actionItemName isKindOfClass:NSString.class] && [actionItemName isEqualToString:FB_ACTION_CANCEL]) { - shouldSkipNextItem = YES; - continue; - } - if (shouldSkipNextItem) { - shouldSkipNextItem = NO; - continue; - } - - id options = [touchItem objectForKey:FB_OPTIONS_KEY]; - if (![options isKindOfClass:NSDictionary.class]) { - [result addObject:touchItem]; - continue; - } - id origin = FBExtractElement(options); - XCUIElement *element; - if ([origin isKindOfClass:XCUIElement.class]) { - element = origin; - } else if ([origin isKindOfClass:NSString.class]) { - element = [self.elementCache elementForUUID:(NSString *)origin]; - } else { - [result addObject:touchItem]; - continue; - } - NSMutableDictionary *elementDict = FBCleanupElements(options).mutableCopy; - [elementDict addEntriesFromDictionary:FBToElementDict(element)]; - NSMutableDictionary *processedItem = touchItem.mutableCopy; - processedItem[FB_OPTIONS_KEY] = elementDict.copy; - [result addObject:processedItem.copy]; - } - return [[result reverseObjectEnumerator] allObjects]; -} - -- (nullable NSArray *)eventPathsWithAction:(NSArray *> *)action - error:(NSError **)error -{ - static NSDictionary *gestureItemsMapping; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - NSMutableDictionary *itemsMapping = [NSMutableDictionary dictionary]; - for (Class cls in @[FBTapItem.class, - FBPressItem.class, - FBLongPressItem.class, - FBMoveToItem.class, - FBWaitItem.class, - FBReleaseItem.class]) { - [itemsMapping setObject:cls forKey:[cls actionName]]; - } - gestureItemsMapping = itemsMapping.copy; - }); - - FBAppiumGestureItemsChain *chain = [[FBAppiumGestureItemsChain alloc] init]; - BOOL isAbsoluteTouchPositionSet = NO; - for (NSDictionary *actionItem in action) { - id actionItemName = [actionItem objectForKey:FB_ACTION_KEY]; - if (![actionItemName isKindOfClass:NSString.class]) { - NSString *description = [NSString stringWithFormat:@"'%@' property is mandatory for gesture chain item %@", FB_ACTION_KEY, actionItem]; - if (error) { - *error = [[FBErrorBuilder.builder withDescription:description] build]; - } - return nil; - } - - Class gestureItemClass = [gestureItemsMapping objectForKey:actionItemName]; - if (nil == gestureItemClass) { - NSString *description = [NSString stringWithFormat:@"%@ value '%@' is unknown", FB_ACTION_KEY, actionItemName]; - if (error) { - *error = [[FBErrorBuilder.builder withDescription:description] build]; - } - return nil; - } - - FBAppiumGestureItem *gestureItem = nil; - if ([gestureItemClass hasAbsolutePositioning]) { - gestureItem = [[gestureItemClass alloc] initWithActionItem:actionItem application:self.application atPosition:nil offset:chain.durationOffset error:error]; - isAbsoluteTouchPositionSet = YES; - } else { - if (!isAbsoluteTouchPositionSet) { - if (error) { - NSString *description = [NSString stringWithFormat:@"'%@' %@ should be preceded by an item with absolute positioning", actionItemName, FB_ACTION_KEY]; - *error = [[FBErrorBuilder.builder withDescription:description] build]; - } - return nil; - } - FBAppiumGestureItem *lastItem = [chain.items lastObject]; - gestureItem = [[gestureItemClass alloc] initWithActionItem:actionItem - application:self.application - atPosition:lastItem.atPosition - offset:chain.durationOffset - error:error]; - } - if (nil == gestureItem) { - return nil; - } - - [chain addItem:gestureItem]; - } - - return [chain asEventPathsWithError:error]; -} - -- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error -{ - XCSynthesizedEventRecord *eventRecord; - BOOL isMultiTouch = [self.actions.firstObject isKindOfClass:NSArray.class]; - eventRecord = [[XCSynthesizedEventRecord alloc] - initWithName:(isMultiTouch ? @"Multi-Finger Touch Action" : @"Single-Finger Touch Action") - interfaceOrientation:self.application.interfaceOrientation]; - for (NSArray *> *action in (isMultiTouch ? self.actions : @[self.actions])) { - NSArray *> *preprocessedAction = [self preprocessAction:action]; - NSArray *eventPaths = [self eventPathsWithAction:preprocessedAction error:error]; - if (nil == eventPaths) { - return nil; - } - for (XCPointerEventPath *eventPath in eventPaths) { - [eventRecord addPointerEventPath:eventPath]; - } - } - return eventRecord; -} - -@end - -#endif diff --git a/WebDriverAgentLib/Utilities/FBBaseActionsParser.m b/WebDriverAgentLib/Utilities/FBBaseActionsParser.m deleted file mode 100644 index 872dc268e..000000000 --- a/WebDriverAgentLib/Utilities/FBBaseActionsParser.m +++ /dev/null @@ -1,32 +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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - */ - -#import "FBBaseActionsSynthesizer.h" - -#import "FBErrorBuilder.h" - -@implementation FBBaseActionsSynthesizer - -- (instancetype)initWithActions:(NSArray *)actions forApplication:(XCUIApplication *)application -{ - self = [super init]; - if (self) { - _actions = actions; - _application = application; - } - return self; -} - -- (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error -{ - @throw [[FBErrorBuilder.builder withDescription:@"Override this method in subclasses"] build]; - return nil; -} - -@end diff --git a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h index 5f779194f..7dc7c424f 100644 --- a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h +++ b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBElementCache.h" diff --git a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m index 6d4c046a5..57f9d2d8e 100644 --- a/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m +++ b/WebDriverAgentLib/Utilities/FBBaseActionsSynthesizer.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBBaseActionsSynthesizer.h" diff --git a/WebDriverAgentLib/Utilities/FBCapabilities.h b/WebDriverAgentLib/Utilities/FBCapabilities.h index 116c408df..649a227ce 100644 --- a/WebDriverAgentLib/Utilities/FBCapabilities.h +++ b/WebDriverAgentLib/Utilities/FBCapabilities.h @@ -3,22 +3,41 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import -extern NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION; +/** Set the maximum amount of characters that could be typed within a minute (60 by default) */ extern NSString* const FB_CAP_MAX_TYPING_FREQUENCY; +/** this setting was needed for some legacy stuff */ extern NSString* const FB_CAP_USE_SINGLETON_TEST_MANAGER; +/** Whether to disable screneshots that XCTest automaticallly creates after each step */ extern NSString* const FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS; +/** Whether to terminate the application under test after the session ends */ extern NSString* const FB_CAP_SHOULD_TERMINATE_APP; +/** The maximum amount of seconds to wait for the event loop to become idle */ extern NSString* const FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC; +/** Bundle identifier of the application to run the test for */ extern NSString* const FB_CAP_BUNDLE_ID; +/** + Usually an URL used as initial link to run Mobile Safari, but could be any other deep link. + This might also work together with `FB_CAP_BUNLDE_ID`, which tells XCTest to open + the given deep link in the particular app. + Only works since iOS 16.4 + */ +extern NSString* const FB_CAP_INITIAL_URL; +/** Whether to enforrce (re)start of the application under test on session startup */ extern NSString* const FB_CAP_FORCE_APP_LAUNCH; +/** Whether to wait for quiescence before starting interaction with apps laucnhes in scope of the test session */ extern NSString* const FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE; +/** Array of command line arguments to be passed to the application under test */ extern NSString* const FB_CAP_ARGUMENTS; +/** Dictionary of environment variables to be passed to the application under test */ extern NSString* const FB_CAP_ENVIRNOMENT; +/** Whether to use native XCTest caching strategy */ extern NSString* const FB_CAP_USE_NATIVE_CACHING_STRATEGY; +/** Whether to enforce software keyboard presence on simulator */ extern NSString* const FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE; +/** Sets the application state change timeout for the initial app startup */ +extern NSString* const FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC; diff --git a/WebDriverAgentLib/Utilities/FBCapabilities.m b/WebDriverAgentLib/Utilities/FBCapabilities.m index cafd0f168..4693fc8a2 100644 --- a/WebDriverAgentLib/Utilities/FBCapabilities.m +++ b/WebDriverAgentLib/Utilities/FBCapabilities.m @@ -3,22 +3,22 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBCapabilities.h" -NSString* const FB_CAP_USE_TEST_MANAGER_FOR_VISIBLITY_DETECTION = @"shouldUseTestManagerForVisibilityDetection"; NSString* const FB_CAP_MAX_TYPING_FREQUENCY = @"maxTypingFrequency"; NSString* const FB_CAP_USE_SINGLETON_TEST_MANAGER = @"shouldUseSingletonTestManager"; NSString* const FB_CAP_DISABLE_AUTOMATIC_SCREENSHOTS = @"disableAutomaticScreenshots"; NSString* const FB_CAP_SHOULD_TERMINATE_APP = @"shouldTerminateApp"; NSString* const FB_CAP_EVENT_LOOP_IDLE_DELAY_SEC = @"eventloopIdleDelaySec"; NSString* const FB_CAP_BUNDLE_ID = @"bundleId"; +NSString* const FB_CAP_INITIAL_URL = @"initialUrl"; NSString* const FB_CAP_FORCE_APP_LAUNCH = @"forceAppLaunch"; NSString* const FB_CAP_SHOULD_WAIT_FOR_QUIESCENCE = @"shouldWaitForQuiescence"; NSString* const FB_CAP_ARGUMENTS = @"arguments"; NSString* const FB_CAP_ENVIRNOMENT = @"environment"; NSString* const FB_CAP_USE_NATIVE_CACHING_STRATEGY = @"useNativeCachingStrategy"; NSString* const FB_CAP_FORCE_SIMULATOR_SOFTWARE_KEYBOARD_PRESENCE = @"forceSimulatorSoftwareKeyboardPresence"; +NSString* const FB_CAP_APP_LAUNCH_STATE_TIMEOUT_SEC = @"appLaunchStateTimeoutSec"; diff --git a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h index 080810647..c2ca9c524 100644 --- a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h +++ b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m index f04240d0d..219706b8f 100644 --- a/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m +++ b/WebDriverAgentLib/Utilities/FBClassChainQueryParser.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBClassChainQueryParser.h" @@ -118,7 +117,7 @@ - (void)appendChar:(unichar)character { NSMutableString *value = [NSMutableString stringWithString:self.asString]; [value appendFormat:@"%C", character]; - self.asString = value.copy;; + self.asString = value.copy; } - (nullable FBBaseClassChainToken*)followingTokenBasedOn:(unichar)character diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.h b/WebDriverAgentLib/Utilities/FBConfiguration.h index dd0015e56..e8c7754bf 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.h +++ b/WebDriverAgentLib/Utilities/FBConfiguration.h @@ -3,18 +3,14 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import -#import "AXSettings.h" -#import "UIKeyboardImpl.h" -#import "TIPreferencesController.h" - NS_ASSUME_NONNULL_BEGIN +extern NSString *const FBSnapshotMaxChildrenKey; extern NSString *const FBSnapshotMaxDepthKey; /** @@ -22,10 +18,6 @@ extern NSString *const FBSnapshotMaxDepthKey; */ @interface FBConfiguration : NSObject -/*! If set to YES will ask TestManagerDaemon for element visibility */ -+ (void)setShouldUseTestManagerForVisibilityDetection:(BOOL)value; -+ (BOOL)shouldUseTestManagerForVisibilityDetection; - /*! If set to YES will use compact (standards-compliant) & faster responses */ + (void)setShouldUseCompactResponses:(BOOL)value; + (BOOL)shouldUseCompactResponses; @@ -60,11 +52,16 @@ extern NSString *const FBSnapshotMaxDepthKey; /* The maximum typing frequency for all typing activities */ + (void)setMaxTypingFrequency:(NSUInteger)value; + (NSUInteger)maxTypingFrequency; ++ (NSUInteger)defaultTypingFrequency; /* Use singleton test manager proxy */ + (void)setShouldUseSingletonTestManager:(BOOL)value; + (BOOL)shouldUseSingletonTestManager; +/* Enforces WDA to verify the presense of system alerts while checking for an active app */ ++ (void)setShouldRespectSystemAlerts:(BOOL)value; ++ (BOOL)shouldRespectSystemAlerts; + /** * Extract switch value from arguments * @@ -102,6 +99,14 @@ extern NSString *const FBSnapshotMaxDepthKey; + (NSUInteger)mjpegServerFramerate; + (void)setMjpegServerFramerate:(NSUInteger)framerate; +/** + Whether to limit the XPath scope to descendant items only while performing a lookup + in an element context. Enabled by default. Being disabled, allows to use XPath locators + like ".." in order to match parent items of the current context root. + */ ++ (BOOL)limitXpathContextScope; ++ (void)setLimitXpathContextScope:(BOOL)enabled; + /** The quality of display screenshots. The higher quality you set is the bigger screenshot size is. The highest quality value is 0 (lossless PNG) or 3 (lossless HEIC). The lowest quality is 2 (highly compressed JPEG). @@ -116,6 +121,12 @@ extern NSString *const FBSnapshotMaxDepthKey; */ + (NSRange)bindingPortRange; +/** + The IP address that the HTTP Server should bind to on launch. + Returns nil if not specified, which causes the server to listen on all interfaces. + */ ++ (NSString * _Nullable)bindingIPAddress; + /** The port number where the background screenshots broadcaster is supposed to run */ @@ -127,8 +138,8 @@ extern NSString *const FBSnapshotMaxDepthKey; ! Setting this to a value less than 100, especially together with orientation fixing enabled ! may lead to WDA process termination because of an excessive CPU usage. */ -+ (NSUInteger)mjpegScalingFactor; -+ (void)setMjpegScalingFactor:(NSUInteger)scalingFactor; ++ (CGFloat)mjpegScalingFactor; ++ (void)setMjpegScalingFactor:(CGFloat)scalingFactor; /** YES if verbose logging is enabled. NO otherwise. @@ -176,14 +187,6 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { + (void)setKeyboardPrediction:(BOOL)isEnabled; + (FBConfigurationKeyboardPreference)keyboardPrediction; -/** - * The maximum time to wait until accessibility snapshot is taken - * - * @param timeout The number of float seconds to wait (15 seconds by default) - */ -+ (void)setCustomSnapshotTimeout:(NSTimeInterval)timeout; -+ (NSTimeInterval)customSnapshotTimeout; - /** Sets maximum depth for traversing elements tree from parents to children while requesting XCElementSnapshot. Used to set maxDepth value in a dictionary provided by XCAXClient_iOS's method defaultParams. @@ -200,6 +203,22 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { */ + (int)snapshotMaxDepth; +/** + Sets the maximum number of element children to traverse in each snapshot + while requesting XCElementSnapshot. + Used to set the `maxChildren` value in a dictionary provided by + XCAXClient_iOS's `defaultParameters` method. + The original XCAXClient_iOS `maxChildren` value is `INT_MAX`. + + @param maxChildren The number of maximum element children for traversing elements tree + */ ++ (void)setSnapshotMaxChildren:(int)maxChildren; + +/** + @return The maximum number of element children for traversing elements tree + */ ++ (int)snapshotMaxChildren; + /** * Whether to use fast search result matching while searching for elements. * By default this is disabled due to https://github.com/appium/appium/issues/10101 @@ -253,17 +272,6 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { + (void)setAnimationCoolOffTimeout:(NSTimeInterval)timeout; + (NSTimeInterval)animationCoolOffTimeout; -/** - Enforces the page hierarchy to include non modal elements, - like Contacts. By default such elements are not present there. - See https://github.com/appium/appium/issues/13227 - - @param isEnabled Set to YES in order to enable non modal elements inclusion. - Setting this value to YES will have no effect if the current iOS SDK does not support such feature. - */ -+ (void)setIncludeNonModalElements:(BOOL)isEnabled; -+ (BOOL)includeNonModalElements; - /** Sets custom class chain locators for accept/dismiss alert buttons location. This might be useful if the default buttons detection algorithm fails to determine alert buttons properly @@ -282,6 +290,21 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { + (void)setDismissAlertButtonSelector:(NSString *)classChainSelector; + (NSString *)dismissAlertButtonSelector; +/** + Sets class chain selector to apply for an automated alert click + */ ++ (void)setAutoClickAlertSelector:(NSString *)classChainSelector; ++ (NSString *)autoClickAlertSelector; + +/** + * Whether to use HIDEvent for text clear. + * By default this is enabled and HIDEvent is used for text clear. + * + * @param enabled Either YES or NO + */ ++ (void)setUseClearTextShortcut:(BOOL)enabled; ++ (BOOL)useClearTextShortcut; + #if !TARGET_OS_TV /** Set the screenshot orientation for iOS @@ -316,6 +339,67 @@ typedef NS_ENUM(NSInteger, FBConfigurationKeyboardPreference) { */ + (void)resetSessionSettings; +/** + * Whether to calculate `hittable` attribute using native APIs + * instead of legacy heuristics. + * This flag improves accuracy, but may affect performance. + * Disabled by default. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeHittableInPageSource:(BOOL)enabled; ++ (BOOL)includeHittableInPageSource; + +/** + * Whether to include `nativeFrame` attribute in the XML page source. + * + * When enabled, the XML representation will contain the precise rendered + * frame of the UI element. + * + * This value is more accurate than the legacy `wdFrame`, which applies rounding + * and may introduce inconsistencies in size and position calculations. + * + * The value is disabled by default to avoid potential performance overhead. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeNativeFrameInPageSource:(BOOL)enabled; ++ (BOOL)includeNativeFrameInPageSource; + +/** + * Whether to include `minValue`/`maxValue` attributes in the page source. + * These attributes are retrieved from native element snapshots and represent + * value boundaries for elements like sliders or progress indicators. + * This may affect performance if used on many elements. + * Disabled by default. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled; ++ (BOOL)includeMinMaxValueInPageSource; + +/** + * Whether to include `customActions` attribute in the XML page source. + * Custom actions represent accessibility actions available on UI elements. + * This may affect performance if used on many elements. + * Disabled by default. + * + * @param enabled Either YES or NO + */ ++ (void)setIncludeCustomActionsInPageSource:(BOOL)enabled; ++ (BOOL)includeCustomActionsInPageSource; + +/** + * Whether to enforce the use of custom snapshots instead of standard snapshots. + * When enabled, fb_customSnapshot is always invoked instead of fb_standardSnapshot + * for XPath tree building and element attributes fetching. + * Disabled by default. + * + * @param enabled Either YES or NO + */ ++ (void)setEnforceCustomSnapshots:(BOOL)enabled; ++ (BOOL)enforceCustomSnapshots; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBConfiguration.m b/WebDriverAgentLib/Utilities/FBConfiguration.m index 03b6a6b1b..dcd1a62e3 100644 --- a/WebDriverAgentLib/Utilities/FBConfiguration.m +++ b/WebDriverAgentLib/Utilities/FBConfiguration.m @@ -3,13 +3,17 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBConfiguration.h" +#import "AXSettings.h" +#import "UIKeyboardImpl.h" +#import "TIPreferencesController.h" + #include +#include #import #include "TargetConditionals.h" @@ -29,34 +33,47 @@ static NSString *const FBKeyboardPredictionKey = @"KeyboardPrediction"; static NSString *const axSettingsClassName = @"AXSettings"; -static BOOL FBShouldUseTestManagerForVisibilityDetection = NO; static BOOL FBShouldUseSingletonTestManager = YES; +static BOOL FBShouldRespectSystemAlerts = NO; -static NSUInteger FBMjpegScalingFactor = 100; +static CGFloat FBMjpegScalingFactor = 100.0; static BOOL FBMjpegShouldFixOrientation = NO; static NSUInteger FBMjpegServerScreenshotQuality = 25; static NSUInteger FBMjpegServerFramerate = 10; // Session-specific settings static BOOL FBShouldTerminateApp; -static NSUInteger FBMaxTypingFrequency; +static NSNumber* FBMaxTypingFrequency; static NSUInteger FBScreenshotQuality; -static NSTimeInterval FBCustomSnapshotTimeout; static BOOL FBShouldUseFirstMatch; static BOOL FBShouldBoundElementsByIndex; -static BOOL FBIncludeNonModalElements; static NSString *FBAcceptAlertButtonSelector; static NSString *FBDismissAlertButtonSelector; +static NSString *FBAutoClickAlertSelector; static NSTimeInterval FBWaitForIdleTimeout; static NSTimeInterval FBAnimationCoolOffTimeout; static BOOL FBShouldUseCompactResponses; static NSString *FBElementResponseAttributes; +static BOOL FBUseClearTextShortcut; +static BOOL FBLimitXpathContextScope = YES; #if !TARGET_OS_TV static UIInterfaceOrientation FBScreenshotOrientation; #endif +static BOOL FBShouldIncludeHittableInPageSource = NO; +static BOOL FBShouldIncludeNativeFrameInPageSource = NO; +static BOOL FBShouldIncludeMinMaxValueInPageSource = NO; +static BOOL FBShouldIncludeCustomActionsInPageSource = NO; +static BOOL FBShouldEnforceCustomSnapshots = NO; @implementation FBConfiguration ++ (NSUInteger)defaultTypingFrequency +{ + NSInteger defaultFreq = [[NSUserDefaults standardUserDefaults] + integerForKey:@"com.apple.xctest.iOSMaximumTypingFrequency"]; + return defaultFreq > 0 ? defaultFreq : 60; +} + + (void)initialize { [FBConfiguration resetSessionSettings]; @@ -121,12 +138,23 @@ + (NSRange)bindingPortRange return NSMakeRange(DefaultStartingPort, DefaultPortRange); } ++ (NSString *)bindingIPAddress +{ + // Existence of USE_IP in the environment allows specifying which interface to bind to + if (NSProcessInfo.processInfo.environment[@"USE_IP"] && + [NSProcessInfo.processInfo.environment[@"USE_IP"] length] > 0) { + return NSProcessInfo.processInfo.environment[@"USE_IP"]; + } + + return nil; +} + + (NSInteger)mjpegServerPort { if (self.mjpegServerPortFromArguments != NSNotFound) { return self.mjpegServerPortFromArguments; } - + if (NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] && [NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] length] > 0) { return [NSProcessInfo.processInfo.environment[@"MJPEG_SERVER_PORT"] integerValue]; @@ -135,12 +163,12 @@ + (NSInteger)mjpegServerPort return DefaultMjpegServerPort; } -+ (NSUInteger)mjpegScalingFactor ++ (CGFloat)mjpegScalingFactor { return FBMjpegScalingFactor; } -+ (void)setMjpegScalingFactor:(NSUInteger)scalingFactor { ++ (void)setMjpegScalingFactor:(CGFloat)scalingFactor { FBMjpegScalingFactor = scalingFactor; } @@ -158,16 +186,6 @@ + (BOOL)verboseLoggingEnabled return [NSProcessInfo.processInfo.environment[@"VERBOSE_LOGGING"] boolValue]; } -+ (void)setShouldUseTestManagerForVisibilityDetection:(BOOL)value -{ - FBShouldUseTestManagerForVisibilityDetection = value; -} - -+ (BOOL)shouldUseTestManagerForVisibilityDetection -{ - return FBShouldUseTestManagerForVisibilityDetection; -} - + (void)setShouldUseCompactResponses:(BOOL)value { FBShouldUseCompactResponses = value; @@ -200,12 +218,17 @@ + (NSString *)elementResponseAttributes + (void)setMaxTypingFrequency:(NSUInteger)value { - FBMaxTypingFrequency = value; + FBMaxTypingFrequency = @(value); } + (NSUInteger)maxTypingFrequency { - return FBMaxTypingFrequency; + if (nil == FBMaxTypingFrequency) { + return [self defaultTypingFrequency]; + } + return FBMaxTypingFrequency.integerValue <= 0 + ? [self defaultTypingFrequency] + : FBMaxTypingFrequency.integerValue; } + (void)setShouldUseSingletonTestManager:(BOOL)value @@ -341,24 +364,34 @@ + (void)setKeyboardPrediction:(BOOL)isEnabled [self configureKeyboardsPreference:isEnabled forPreferenceKey:FBKeyboardPredictionKey]; } -+ (void)setCustomSnapshotTimeout:(NSTimeInterval)timeout ++ (void)setSnapshotMaxDepth:(int)maxDepth { - FBCustomSnapshotTimeout = timeout; + FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @(maxDepth)); } -+ (NSTimeInterval)customSnapshotTimeout ++ (int)snapshotMaxDepth { - return FBCustomSnapshotTimeout; + return [FBGetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey) intValue]; } -+ (void)setSnapshotMaxDepth:(int)maxDepth ++ (void)setSnapshotMaxChildren:(int)maxChildren { - FBSetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey, @(maxDepth)); + FBSetCustomParameterForElementSnapshot(FBSnapshotMaxChildrenKey, @(maxChildren)); } -+ (int)snapshotMaxDepth ++ (int)snapshotMaxChildren { - return [FBGetCustomParameterForElementSnapshot(FBSnapshotMaxDepthKey) intValue]; + return [FBGetCustomParameterForElementSnapshot(FBSnapshotMaxChildrenKey) intValue]; +} + ++ (void)setShouldRespectSystemAlerts:(BOOL)value +{ + FBShouldRespectSystemAlerts = value; +} + ++ (BOOL)shouldRespectSystemAlerts +{ + return FBShouldRespectSystemAlerts; } + (void)setUseFirstMatch:(BOOL)enabled @@ -381,16 +414,6 @@ + (BOOL)boundElementsByIndex return FBShouldBoundElementsByIndex; } -+ (void)setIncludeNonModalElements:(BOOL)isEnabled -{ - FBIncludeNonModalElements = isEnabled; -} - -+ (BOOL)includeNonModalElements -{ - return FBIncludeNonModalElements; -} - + (void)setAcceptAlertButtonSelector:(NSString *)classChainSelector { FBAcceptAlertButtonSelector = classChainSelector; @@ -411,6 +434,36 @@ + (NSString *)dismissAlertButtonSelector return FBDismissAlertButtonSelector; } ++ (void)setAutoClickAlertSelector:(NSString *)classChainSelector +{ + FBAutoClickAlertSelector = classChainSelector; +} + ++ (NSString *)autoClickAlertSelector +{ + return FBAutoClickAlertSelector; +} + ++ (void)setUseClearTextShortcut:(BOOL)enabled +{ + FBUseClearTextShortcut = enabled; +} + ++ (BOOL)useClearTextShortcut +{ + return FBUseClearTextShortcut; +} + ++ (BOOL)limitXpathContextScope +{ + return FBLimitXpathContextScope; +} + ++ (void)setLimitXpathContextScope:(BOOL)enabled +{ + FBLimitXpathContextScope = enabled; +} + #if !TARGET_OS_TV + (BOOL)setScreenshotOrientation:(NSString *)orientation error:(NSError **)error { @@ -453,6 +506,7 @@ + (NSString *)humanReadableScreenshotOrientation return @"landscapeLeft"; case UIInterfaceOrientationUnknown: return @"auto"; + default: break; } } #endif @@ -462,20 +516,20 @@ + (void)resetSessionSettings FBShouldTerminateApp = YES; FBShouldUseCompactResponses = YES; FBElementResponseAttributes = @"type,label"; - FBMaxTypingFrequency = 60; + FBMaxTypingFrequency = @([self defaultTypingFrequency]); FBScreenshotQuality = 3; - FBCustomSnapshotTimeout = 15.; FBShouldUseFirstMatch = NO; FBShouldBoundElementsByIndex = NO; - // This is diabled by default because enabling it prevents the accessbility snapshot to be taken - // (it always errors with kxIllegalArgument error) - FBIncludeNonModalElements = NO; FBAcceptAlertButtonSelector = @""; FBDismissAlertButtonSelector = @""; + FBAutoClickAlertSelector = @""; FBWaitForIdleTimeout = 10.; FBAnimationCoolOffTimeout = 2.; // 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); + FBUseClearTextShortcut = YES; + FBLimitXpathContextScope = YES; #if !TARGET_OS_TV FBScreenshotOrientation = UIInterfaceOrientationUnknown; #endif @@ -591,4 +645,54 @@ + (BOOL)reduceMotionEnabled return NO; } ++ (void)setIncludeHittableInPageSource:(BOOL)enabled +{ + FBShouldIncludeHittableInPageSource = enabled; +} + ++ (BOOL)includeHittableInPageSource +{ + return FBShouldIncludeHittableInPageSource; +} + ++ (void)setIncludeNativeFrameInPageSource:(BOOL)enabled +{ + FBShouldIncludeNativeFrameInPageSource = enabled; +} + ++ (BOOL)includeNativeFrameInPageSource +{ + return FBShouldIncludeNativeFrameInPageSource; +} + ++ (void)setIncludeMinMaxValueInPageSource:(BOOL)enabled +{ + FBShouldIncludeMinMaxValueInPageSource = enabled; +} + ++ (BOOL)includeMinMaxValueInPageSource +{ + return FBShouldIncludeMinMaxValueInPageSource; +} + ++ (void)setIncludeCustomActionsInPageSource:(BOOL)enabled +{ + FBShouldIncludeCustomActionsInPageSource = enabled; +} + ++ (BOOL)includeCustomActionsInPageSource +{ + return FBShouldIncludeCustomActionsInPageSource; +} + ++ (void)setEnforceCustomSnapshots:(BOOL)enabled +{ + FBShouldEnforceCustomSnapshots = enabled; +} + ++ (BOOL)enforceCustomSnapshots +{ + return FBShouldEnforceCustomSnapshots; +} + @end diff --git a/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h b/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h index edc1c0fc4..02ebf89dd 100644 --- a/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h +++ b/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m b/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m index 2846a1813..b4c2c5399 100644 --- a/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m +++ b/WebDriverAgentLib/Utilities/FBDebugLogDelegateDecorator.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBDebugLogDelegateDecorator.h" diff --git a/WebDriverAgentLib/Utilities/FBElementHelpers.h b/WebDriverAgentLib/Utilities/FBElementHelpers.h new file mode 100644 index 000000000..d0cf1b3db --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBElementHelpers.h @@ -0,0 +1,29 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +/** + Checks if the element is a text field + + @param elementType XCTest element type + @return YES if the element is a text field + */ +BOOL FBDoesElementSupportInnerText(XCUIElementType elementType); + +/** + Checks if the element supports min/max value attributes + + @param elementType XCTest element type + @return YES if the element type supports min/max value attributes + */ +BOOL FBDoesElementSupportMinMaxValue(XCUIElementType elementType); + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBElementHelpers.m b/WebDriverAgentLib/Utilities/FBElementHelpers.m new file mode 100644 index 000000000..6b9d340fe --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBElementHelpers.m @@ -0,0 +1,21 @@ +/** + * 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 "FBElementHelpers.h" + +BOOL FBDoesElementSupportInnerText(XCUIElementType elementType) { + return elementType == XCUIElementTypeTextView + || elementType == XCUIElementTypeTextField + || elementType == XCUIElementTypeSearchField + || elementType == XCUIElementTypeSecureTextField; +} + +BOOL FBDoesElementSupportMinMaxValue(XCUIElementType elementType) { + return elementType == XCUIElementTypeSlider + || elementType == XCUIElementTypeStepper; +} diff --git a/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h b/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h index b4097eee4..a94ff62a9 100644 --- a/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h +++ b/WebDriverAgentLib/Utilities/FBElementTypeTransformer.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m b/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m index 7dc5b1e62..d3748a44c 100644 --- a/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m +++ b/WebDriverAgentLib/Utilities/FBElementTypeTransformer.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBElementTypeTransformer.h" diff --git a/WebDriverAgentLib/Utilities/FBErrorBuilder.h b/WebDriverAgentLib/Utilities/FBErrorBuilder.h index 7678daf19..8435eba98 100644 --- a/WebDriverAgentLib/Utilities/FBErrorBuilder.h +++ b/WebDriverAgentLib/Utilities/FBErrorBuilder.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBErrorBuilder.m b/WebDriverAgentLib/Utilities/FBErrorBuilder.m index cafaf503b..3ee5d8fa7 100644 --- a/WebDriverAgentLib/Utilities/FBErrorBuilder.m +++ b/WebDriverAgentLib/Utilities/FBErrorBuilder.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBErrorBuilder.h" diff --git a/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h b/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h index 5d744107e..80d86483e 100644 --- a/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h +++ b/WebDriverAgentLib/Utilities/FBFailureProofTestCase.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m b/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m index 72d79ee1c..ab91fd94d 100644 --- a/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m +++ b/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBFailureProofTestCase.h" diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.h b/WebDriverAgentLib/Utilities/FBImageProcessor.h index a350bd9ce..e83b830f7 100644 --- a/WebDriverAgentLib/Utilities/FBImageProcessor.h +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBImageProcessor.m b/WebDriverAgentLib/Utilities/FBImageProcessor.m index 16601ce8e..fe197c940 100644 --- a/WebDriverAgentLib/Utilities/FBImageProcessor.m +++ b/WebDriverAgentLib/Utilities/FBImageProcessor.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBImageProcessor.h" @@ -26,8 +25,10 @@ @interface FBImageProcessor () @property (nonatomic) NSData *nextImage; +@property (nonatomic) NSMutableArray *pendingCompletionHandlers; @property (nonatomic, readonly) NSLock *nextImageLock; @property (nonatomic, readonly) dispatch_queue_t scalingQueue; +@property (atomic, assign) BOOL isScalingScheduled; @end @@ -38,7 +39,9 @@ - (id)init self = [super init]; if (self) { _nextImageLock = [[NSLock alloc] init]; + _pendingCompletionHandlers = [NSMutableArray array]; _scalingQueue = dispatch_queue_create("image.scaling.queue", NULL); + _isScalingScheduled = NO; } return self; } @@ -52,34 +55,50 @@ - (void)submitImageData:(NSData *)image [FBLogger verboseLog:@"Discarding screenshot"]; } self.nextImage = image; + [self.pendingCompletionHandlers addObject:[completionHandler copy]]; + BOOL shouldSchedule = !self.isScalingScheduled; + if (shouldSchedule) { + self.isScalingScheduled = YES; + } [self.nextImageLock unlock]; + if (!shouldSchedule) { + return; + } -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wcompletion-handler" dispatch_async(self.scalingQueue, ^{ - [self.nextImageLock lock]; - NSData *nextImageData = self.nextImage; - self.nextImage = nil; - [self.nextImageLock unlock]; - if (nextImageData == nil) { - return; - } + while (YES) { + @autoreleasepool { + [self.nextImageLock lock]; + NSData *nextImageData = self.nextImage; + self.nextImage = nil; + NSArray *handlers = [self.pendingCompletionHandlers copy]; + [self.pendingCompletionHandlers removeAllObjects]; + if (nextImageData == nil) { + self.isScalingScheduled = NO; + [self.nextImageLock unlock]; + return; + } + [self.nextImageLock unlock]; - // We do not want this value to be too high because then we get images larger in size than original ones - // Although, we also don't want to lose too much of the quality on recompression - CGFloat recompressionQuality = MAX(0.9, - MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); - NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData - scalingFactor:scalingFactor - uti:UTTypeJPEG - compressionQuality:recompressionQuality - // iOS always returns screnshots in portrait orientation, but puts the real value into the metadata - // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 - fixOrientation:FBConfiguration.mjpegShouldFixOrientation - desiredOrientation:nil]; - completionHandler(thumbnailData ?: nextImageData); + // We do not want this value to be too high because then we get images larger in size than original ones + // Although, we also don't want to lose too much of the quality on recompression + CGFloat recompressionQuality = MAX(0.9, + MIN(FBMaxCompressionQuality, (double)FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + NSData *thumbnailData = [self.class fixedImageDataWithImageData:nextImageData + scalingFactor:scalingFactor + uti:UTTypeJPEG + compressionQuality:recompressionQuality + // iOS always returns screenshots in portrait orientation, but puts the real value into the metadata + // Use it with care. See https://github.com/appium/WebDriverAgent/pull/812 + fixOrientation:FBConfiguration.mjpegShouldFixOrientation + desiredOrientation:nil]; + NSData *processedImageData = thumbnailData ?: nextImageData; + for (void (^handler)(NSData *) in handlers) { + handler(processedImageData); + } + } + } }); -#pragma clang diagnostic pop } + (nullable NSData *)fixedImageDataWithImageData:(NSData *)imageData diff --git a/WebDriverAgentLib/Utilities/FBImageUtils.h b/WebDriverAgentLib/Utilities/FBImageUtils.h index 15a04f6fe..07501657f 100644 --- a/WebDriverAgentLib/Utilities/FBImageUtils.h +++ b/WebDriverAgentLib/Utilities/FBImageUtils.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBImageUtils.m b/WebDriverAgentLib/Utilities/FBImageUtils.m index b1d2f9b5b..d991cea18 100644 --- a/WebDriverAgentLib/Utilities/FBImageUtils.m +++ b/WebDriverAgentLib/Utilities/FBImageUtils.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBImageUtils.h" diff --git a/WebDriverAgentLib/Utilities/FBKeyboard.h b/WebDriverAgentLib/Utilities/FBKeyboard.h index 5a16e1aed..c3c5f8ecf 100644 --- a/WebDriverAgentLib/Utilities/FBKeyboard.h +++ b/WebDriverAgentLib/Utilities/FBKeyboard.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -23,19 +22,6 @@ NS_ASSUME_NONNULL_BEGIN + (nullable NSString *)keyValueForName:(NSString *)name; #endif -/** - Types a string into active element. There must be element with keyboard focus; otherwise an - error is raised. - - This API discards any modifiers set in the current context by +performWithKeyModifiers:block: so that - it strictly interprets the provided text. To input keys with modifier flags, use -typeKey:modifierFlags:. - - @param text that should be typed - @param error If there is an error, upon return contains an NSError object that describes the problem. - @return YES if the operation succeeds, otherwise NO. - */ -+ (BOOL)typeText:(NSString *)text error:(NSError **)error; - /** Waits until the keyboard is visible on the screen or a timeout happens @@ -46,20 +32,6 @@ NS_ASSUME_NONNULL_BEGIN */ + (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app timeout:(NSTimeInterval)timeout error:(NSError **)error; -/** - Types a string into active element. There must be element with keyboard focus; otherwise an - error is raised. - - This API discards any modifiers set in the current context by +performWithKeyModifiers:block: so that - it strictly interprets the provided text. To input keys with modifier flags, use -typeKey:modifierFlags:. - - @param text that should be typed - @param frequency Frequency of typing (letters per sec) - @param error If there is an error, upon return contains an NSError object that describes the problem. - @return YES if the operation succeeds, otherwise NO. - */ -+ (BOOL)typeText:(NSString *)text frequency:(NSUInteger)frequency error:(NSError **)error; - @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBKeyboard.m b/WebDriverAgentLib/Utilities/FBKeyboard.m index 0cf9964c6..0975300bf 100644 --- a/WebDriverAgentLib/Utilities/FBKeyboard.m +++ b/WebDriverAgentLib/Utilities/FBKeyboard.m @@ -3,14 +3,11 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBKeyboard.h" - -#import "FBApplication.h" #import "FBConfiguration.h" #import "FBXCTestDaemonsProxy.h" #import "FBErrorBuilder.h" @@ -25,43 +22,21 @@ @implementation FBKeyboard -+ (BOOL)typeText:(NSString *)text error:(NSError **)error -{ - return [self typeText:text frequency:[FBConfiguration maxTypingFrequency] error:error]; -} - -+ (BOOL)typeText:(NSString *)text frequency:(NSUInteger)frequency error:(NSError **)error -{ - __block BOOL didSucceed = NO; - __block NSError *innerError; - [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ - [[FBXCTestDaemonsProxy testRunnerProxy] - _XCT_sendString:text - maximumFrequency:frequency - completion:^(NSError *typingError){ - didSucceed = (typingError == nil); - innerError = typingError; - completion(); - }]; - }]; - if (error) { - *error = innerError; - } - return didSucceed; -} - -+ (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app timeout:(NSTimeInterval)timeout error:(NSError **)error ++ (BOOL)waitUntilVisibleForApplication:(XCUIApplication *)app + timeout:(NSTimeInterval)timeout + error:(NSError **)error { BOOL (^isKeyboardVisible)(void) = ^BOOL(void) { - if (!app.keyboard.exists) { + XCUIElement *keyboard = app.keyboards.fb_firstMatch; + if (nil == keyboard) { return NO; } NSPredicate *keySearchPredicate = [NSPredicate predicateWithBlock:^BOOL(id snapshot, NSDictionary *bindings) { - return snapshot.label.length > 0; + return snapshot.label.length > 0 && !CGRectIsEmpty(snapshot.frame); }]; - XCUIElement *firstKey = [[app.keyboard descendantsMatchingType:XCUIElementTypeKey] + XCUIElement *firstKey = [[keyboard descendantsMatchingType:XCUIElementTypeKey] matchingPredicate:keySearchPredicate].allElementsBoundByIndex.firstObject; return firstKey.exists && firstKey.hittable; }; diff --git a/WebDriverAgentLib/Utilities/FBLogger.h b/WebDriverAgentLib/Utilities/FBLogger.h index 3626fa520..c92b7ef7e 100644 --- a/WebDriverAgentLib/Utilities/FBLogger.h +++ b/WebDriverAgentLib/Utilities/FBLogger.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBLogger.m b/WebDriverAgentLib/Utilities/FBLogger.m index 9466b86c4..060b8f00b 100644 --- a/WebDriverAgentLib/Utilities/FBLogger.m +++ b/WebDriverAgentLib/Utilities/FBLogger.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBLogger.h" diff --git a/WebDriverAgentLib/Utilities/FBMacros.h b/WebDriverAgentLib/Utilities/FBMacros.h index dae0c5faf..ba77c2b59 100644 --- a/WebDriverAgentLib/Utilities/FBMacros.h +++ b/WebDriverAgentLib/Utilities/FBMacros.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ // Typedef to help with storing constant strings for enums. diff --git a/WebDriverAgentLib/Utilities/FBMathUtils.h b/WebDriverAgentLib/Utilities/FBMathUtils.h index 2d52d3e6d..2dbce05a5 100644 --- a/WebDriverAgentLib/Utilities/FBMathUtils.h +++ b/WebDriverAgentLib/Utilities/FBMathUtils.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBMathUtils.m b/WebDriverAgentLib/Utilities/FBMathUtils.m index 44ee257dc..87a7ecf63 100644 --- a/WebDriverAgentLib/Utilities/FBMathUtils.m +++ b/WebDriverAgentLib/Utilities/FBMathUtils.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBMathUtils.h" diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.h b/WebDriverAgentLib/Utilities/FBMjpegServer.h index 6ab15a300..a9b47cada 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.h +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTCPSocket.h" @@ -20,6 +19,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (instancetype)init; +/** + Stops screenshot broadcasting and prevents future scheduling. + */ +- (void)stopStreaming; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBMjpegServer.m b/WebDriverAgentLib/Utilities/FBMjpegServer.m index 14a734c94..6d6250422 100644 --- a/WebDriverAgentLib/Utilities/FBMjpegServer.m +++ b/WebDriverAgentLib/Utilities/FBMjpegServer.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBMjpegServer.h" @@ -13,7 +12,6 @@ @import UniformTypeIdentifiers; #import "GCDAsyncSocket.h" -#import "FBApplication.h" #import "FBConfiguration.h" #import "FBLogger.h" #import "FBScreenshot.h" @@ -23,10 +21,17 @@ static const NSUInteger MAX_FPS = 60; static const NSTimeInterval FRAME_TIMEOUT = 1.; +static const NSTimeInterval FAILURE_BACKOFF_MIN = 1.0; +static const NSTimeInterval FAILURE_BACKOFF_MAX = 10.0; static NSString *const SERVER_NAME = @"WDA MJPEG Server"; static const char *QUEUE_NAME = "JPEG Screenshots Provider Queue"; +static NSUInteger FBNormalizedMjpegFramerate(NSUInteger framerate) +{ + return (0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate; +} + @interface FBMjpegServer() @@ -34,6 +39,10 @@ @interface FBMjpegServer() @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; @end @@ -43,38 +52,50 @@ @implementation FBMjpegServer - (instancetype)init { if ((self = [super init])) { + _consecutiveScreenshotFailures = 0; + _isStreaming = YES; + _sentFramesCount = 0; + _sentBytesCount = 0; _listeningClients = [NSMutableArray array]; + _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); _backgroundQueue = dispatch_queue_create(QUEUE_NAME, queueAttributes); + __weak typeof(self) weakSelf = self; dispatch_async(_backgroundQueue, ^{ - [self streamScreenshot]; + [weakSelf streamScreenshot]; }); - _imageProcessor = [[FBImageProcessor alloc] init]; - _mainScreenID = [XCUIScreen.mainScreen displayID]; } return self; } - (void)scheduleNextScreenshotWithInterval:(uint64_t)timerInterval timeStarted:(uint64_t)timeStarted { + if (!self.isStreaming) { + return; + } uint64_t timeElapsed = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW) - timeStarted; - int64_t nextTickDelta = timerInterval - timeElapsed; + int64_t nextTickDelta = (int64_t)timerInterval - (int64_t)timeElapsed; + __weak typeof(self) weakSelf = self; if (nextTickDelta > 0) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, nextTickDelta), self.backgroundQueue, ^{ - [self streamScreenshot]; + [weakSelf streamScreenshot]; }); } else { // Try to do our best to keep the FPS at a decent level dispatch_async(self.backgroundQueue, ^{ - [self streamScreenshot]; + [weakSelf streamScreenshot]; }); } } - (void)streamScreenshot { - NSUInteger framerate = FBConfiguration.mjpegServerFramerate; - uint64_t timerInterval = (uint64_t)(1.0 / ((0 == framerate || framerate > MAX_FPS) ? MAX_FPS : framerate) * NSEC_PER_SEC); + if (!self.isStreaming) { + return; + } + NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.mjpegServerFramerate); + uint64_t timerInterval = (uint64_t)(1.0 / (double)framerate * NSEC_PER_SEC); uint64_t timeStarted = clock_gettime_nsec_np(CLOCK_MONOTONIC_RAW); @synchronized (self.listeningClients) { if (0 == self.listeningClients.count) { @@ -85,7 +106,7 @@ - (void)streamScreenshot NSError *error; CGFloat compressionQuality = MAX(FBMinCompressionQuality, - MIN(FBMaxCompressionQuality, FBConfiguration.mjpegServerScreenshotQuality / 100.0)); + MIN(FBMaxCompressionQuality, (double)FBConfiguration.mjpegServerScreenshotQuality / 100.0)); NSData *screenshotData = [FBScreenshot takeInOriginalResolutionWithScreenID:self.mainScreenID compressionQuality:compressionQuality uti:UTTypeJPEG @@ -93,28 +114,52 @@ - (void)streamScreenshot error:&error]; if (nil == screenshotData) { [FBLogger logFmt:@"%@", error.description]; - [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted]; + self.consecutiveScreenshotFailures++; + NSTimeInterval backoffSeconds = MIN(FAILURE_BACKOFF_MAX, + FAILURE_BACKOFF_MIN * (1 << MIN(self.consecutiveScreenshotFailures, 4))); + uint64_t backoffInterval = (uint64_t)(backoffSeconds * NSEC_PER_SEC); + [self scheduleNextScreenshotWithInterval:backoffInterval timeStarted:timeStarted]; return; } + self.consecutiveScreenshotFailures = 0; + CGFloat scalingFactor = FBConfiguration.mjpegScalingFactor / 100.0; + __weak typeof(self) weakSelf = self; [self.imageProcessor submitImageData:screenshotData scalingFactor:scalingFactor completionHandler:^(NSData * _Nonnull scaled) { - [self sendScreenshot:scaled]; + [weakSelf sendScreenshot:scaled]; }]; [self scheduleNextScreenshotWithInterval:timerInterval timeStarted:timeStarted]; } - (void)sendScreenshot:(NSData *)screenshotData { + if (!self.isStreaming) { + return; + } NSString *chunkHeader = [NSString stringWithFormat:@"--BoundaryString\r\nContent-type: image/jpeg\r\nContent-Length: %@\r\n\r\n", @(screenshotData.length)]; NSMutableData *chunk = [[chunkHeader dataUsingEncoding:NSUTF8StringEncoding] mutableCopy]; [chunk appendData:screenshotData]; [chunk appendData:(id)[@"\r\n\r\n" dataUsingEncoding:NSUTF8StringEncoding]]; @synchronized (self.listeningClients) { + if (!self.isStreaming || 0 == self.listeningClients.count) { + return; + } + NSUInteger clientCount = self.listeningClients.count; for (GCDAsyncSocket *client in self.listeningClients) { - [client writeData:chunk withTimeout:-1 tag:0]; + // Slow clients should fail/close instead of buffering indefinitely. + [client writeData:chunk withTimeout:FRAME_TIMEOUT tag:0]; + } + self.sentFramesCount++; + self.sentBytesCount += chunk.length * clientCount; + NSUInteger framerate = FBNormalizedMjpegFramerate(FBConfiguration.mjpegServerFramerate); + if (0 == self.sentFramesCount % framerate) { + [FBLogger verboseLog:[NSString stringWithFormat:@"MJPEG stats: clients=%@ sentFrames=%@ sentBytes=%@", + @(clientCount), + @(self.sentFramesCount), + @(self.sentBytesCount)]]; } } } @@ -150,4 +195,22 @@ - (void)didClientDisconnect:(GCDAsyncSocket *)client [FBLogger log:@"Disconnected a client from screenshots broadcast"]; } +- (void)stopStreaming +{ + self.isStreaming = NO; + @synchronized (self.listeningClients) { + NSArray *clients = self.listeningClients.copy; + [self.listeningClients removeAllObjects]; + for (GCDAsyncSocket *client in clients) { + [client disconnect]; + } + } +} + +- (void)dealloc +{ + [self stopStreaming]; + [FBLogger verboseLog:@"FBMjpegServer deallocated"]; +} + @end diff --git a/WebDriverAgentLib/Utilities/FBNotificationsHelper.h b/WebDriverAgentLib/Utilities/FBNotificationsHelper.h index b0ca35b36..b337cb116 100644 --- a/WebDriverAgentLib/Utilities/FBNotificationsHelper.h +++ b/WebDriverAgentLib/Utilities/FBNotificationsHelper.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBNotificationsHelper.m b/WebDriverAgentLib/Utilities/FBNotificationsHelper.m index 60cc509d6..457047888 100644 --- a/WebDriverAgentLib/Utilities/FBNotificationsHelper.m +++ b/WebDriverAgentLib/Utilities/FBNotificationsHelper.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBNotificationsHelper.h" diff --git a/WebDriverAgentLib/Utilities/FBPasteboard.h b/WebDriverAgentLib/Utilities/FBPasteboard.h index 9595162d4..f61d7d64c 100644 --- a/WebDriverAgentLib/Utilities/FBPasteboard.h +++ b/WebDriverAgentLib/Utilities/FBPasteboard.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBPasteboard.m b/WebDriverAgentLib/Utilities/FBPasteboard.m index 6340efff3..08e4e9f33 100644 --- a/WebDriverAgentLib/Utilities/FBPasteboard.m +++ b/WebDriverAgentLib/Utilities/FBPasteboard.m @@ -3,17 +3,16 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBPasteboard.h" #import #import "FBAlert.h" -#import "FBApplication.h" #import "FBErrorBuilder.h" #import "FBMacros.h" +#import "XCUIApplication+FBHelpers.h" #import "XCUIApplication+FBAlert.h" #define ALERT_TIMEOUT_SEC 30 @@ -101,7 +100,7 @@ + (nullable id)pasteboardContentForItem:(NSString *)item break; } - XCUIElement *alertElement = FBApplication.fb_systemApplication.fb_alertElement; + XCUIElement *alertElement = XCUIApplication.fb_systemApplication.fb_alertElement; if (nil != alertElement) { FBAlert *alert = [FBAlert alertWithElement:alertElement]; [alert acceptWithError:nil]; diff --git a/WebDriverAgentLib/Utilities/FBProtocolHelpers.h b/WebDriverAgentLib/Utilities/FBProtocolHelpers.h index eff0a518b..e25573e7e 100644 --- a/WebDriverAgentLib/Utilities/FBProtocolHelpers.h +++ b/WebDriverAgentLib/Utilities/FBProtocolHelpers.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBProtocolHelpers.m b/WebDriverAgentLib/Utilities/FBProtocolHelpers.m index 3cd037586..a463294a9 100644 --- a/WebDriverAgentLib/Utilities/FBProtocolHelpers.m +++ b/WebDriverAgentLib/Utilities/FBProtocolHelpers.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBProtocolHelpers.h" diff --git a/WebDriverAgentLib/Utilities/FBReflectionUtils.h b/WebDriverAgentLib/Utilities/FBReflectionUtils.h index 0e4cca314..262702a07 100644 --- a/WebDriverAgentLib/Utilities/FBReflectionUtils.h +++ b/WebDriverAgentLib/Utilities/FBReflectionUtils.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBReflectionUtils.m b/WebDriverAgentLib/Utilities/FBReflectionUtils.m index 9114c312f..8ef8419fa 100644 --- a/WebDriverAgentLib/Utilities/FBReflectionUtils.m +++ b/WebDriverAgentLib/Utilities/FBReflectionUtils.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBReflectionUtils.h" diff --git a/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h b/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h index bc6b543c3..5f8faf52e 100644 --- a/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h +++ b/WebDriverAgentLib/Utilities/FBRunLoopSpinner.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m b/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m index 386238734..091232542 100644 --- a/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m +++ b/WebDriverAgentLib/Utilities/FBRunLoopSpinner.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBRunLoopSpinner.h" diff --git a/WebDriverAgentLib/Utilities/FBRuntimeUtils.h b/WebDriverAgentLib/Utilities/FBRuntimeUtils.h index 8e54ed4c5..b16b123ad 100644 --- a/WebDriverAgentLib/Utilities/FBRuntimeUtils.h +++ b/WebDriverAgentLib/Utilities/FBRuntimeUtils.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBRuntimeUtils.m b/WebDriverAgentLib/Utilities/FBRuntimeUtils.m index bb08de7e3..6904cf760 100644 --- a/WebDriverAgentLib/Utilities/FBRuntimeUtils.m +++ b/WebDriverAgentLib/Utilities/FBRuntimeUtils.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBRuntimeUtils.h" diff --git a/WebDriverAgentLib/Utilities/FBScreen.h b/WebDriverAgentLib/Utilities/FBScreen.h index 61dc4aeb4..87c22c7cb 100644 --- a/WebDriverAgentLib/Utilities/FBScreen.h +++ b/WebDriverAgentLib/Utilities/FBScreen.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -18,11 +17,6 @@ NS_ASSUME_NONNULL_BEGIN */ + (double)scale; -/** - The absolute size of application's status bar or CGSizeZero if it's hidden or does not exist - */ -+ (CGSize)statusBarSizeForApplication:(XCUIApplication *)application; - @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBScreen.m b/WebDriverAgentLib/Utilities/FBScreen.m index e2b25855b..dc6536408 100644 --- a/WebDriverAgentLib/Utilities/FBScreen.m +++ b/WebDriverAgentLib/Utilities/FBScreen.m @@ -3,12 +3,10 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScreen.h" -#import "FBApplication.h" #import "XCUIElement+FBIsVisible.h" #import "FBXCodeCompatibility.h" #import "XCUIScreen.h" @@ -20,17 +18,4 @@ + (double)scale return [XCUIScreen.mainScreen scale]; } -+ (CGSize)statusBarSizeForApplication:(XCUIApplication *)application -{ - XCUIApplication *app = FBApplication.fb_systemApplication; - // Since iOS 13 the status bar is no longer part of the application, it’s part of the SpringBoard - XCUIElement *mainStatusBar = app.statusBars.allElementsBoundByIndex.firstObject; - if (nil == mainStatusBar) { - return CGSizeZero; - } - CGSize result = mainStatusBar.frame.size; - // Workaround for https://github.com/appium/appium/issues/15961 - return CGSizeMake(MAX(result.width, result.height), MIN(result.width, result.height)); -} - @end diff --git a/WebDriverAgentLib/Utilities/FBScreenshot.h b/WebDriverAgentLib/Utilities/FBScreenshot.h index 74e6b5886..b0fdfbe71 100644 --- a/WebDriverAgentLib/Utilities/FBScreenshot.h +++ b/WebDriverAgentLib/Utilities/FBScreenshot.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBScreenshot.m b/WebDriverAgentLib/Utilities/FBScreenshot.m index 3d9716dea..618fa308f 100644 --- a/WebDriverAgentLib/Utilities/FBScreenshot.m +++ b/WebDriverAgentLib/Utilities/FBScreenshot.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScreenshot.h" @@ -28,7 +27,7 @@ NSString *formatTimeInterval(NSTimeInterval interval) { NSUInteger milliseconds = (NSUInteger)(interval * 1000); - return [NSString stringWithFormat:@"%ld ms", milliseconds]; + return [NSString stringWithFormat:@"%lu ms", milliseconds]; } @implementation FBScreenshot @@ -127,7 +126,7 @@ + (NSData *)takeInOriginalResolutionWithScreenID:(long long)screenID withDescription:timeoutMsg] buildError:error]; } - }; + } if (nil != error && nil != innerError) { *error = innerError; } diff --git a/WebDriverAgentLib/Utilities/FBSettings.h b/WebDriverAgentLib/Utilities/FBSettings.h index dd82d740c..c1fc6e346 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.h +++ b/WebDriverAgentLib/Utilities/FBSettings.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -22,22 +21,28 @@ extern NSString* const FB_SETTING_MJPEG_SCALING_FACTOR; extern NSString* const FB_SETTING_SCREENSHOT_QUALITY; extern NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION; extern NSString* const FB_SETTING_KEYBOARD_PREDICTION; -// This setting is deprecated. Please use CUSTOM_SNAPSHOT_TIMEOUT instead -extern NSString* const FB_SETTING_SNAPSHOT_TIMEOUT; -extern NSString* const FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT; extern NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH; +extern NSString* const FB_SETTING_SNAPSHOT_MAX_CHILDREN; extern NSString* const FB_SETTING_USE_FIRST_MATCH; extern NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX; extern NSString* const FB_SETTING_REDUCE_MOTION; extern NSString* const FB_SETTING_DEFAULT_ACTIVE_APPLICATION; extern NSString* const FB_SETTING_ACTIVE_APP_DETECTION_POINT; -extern NSString* const FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS; extern NSString* const FB_SETTING_DEFAULT_ALERT_ACTION; extern NSString* const FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR; extern NSString* const FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR; extern NSString* const FB_SETTING_SCREENSHOT_ORIENTATION; extern NSString* const FB_SETTING_WAIT_FOR_IDLE_TIMEOUT; extern NSString* const FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT; - +extern NSString* const FB_SETTING_MAX_TYPING_FREQUENCY; +extern NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS; +extern NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT; +extern NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE; +extern NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR; +extern NSString *const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE; +extern NSString *const FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS; NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBSettings.m b/WebDriverAgentLib/Utilities/FBSettings.m index 6b12a4cd4..d333c58f1 100644 --- a/WebDriverAgentLib/Utilities/FBSettings.m +++ b/WebDriverAgentLib/Utilities/FBSettings.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBSettings.h" @@ -18,18 +17,26 @@ NSString* const FB_SETTING_SCREENSHOT_QUALITY = @"screenshotQuality"; NSString* const FB_SETTING_KEYBOARD_AUTOCORRECTION = @"keyboardAutocorrection"; NSString* const FB_SETTING_KEYBOARD_PREDICTION = @"keyboardPrediction"; -NSString* const FB_SETTING_SNAPSHOT_TIMEOUT = @"snapshotTimeout"; -NSString* const FB_SETTING_CUSTOM_SNAPSHOT_TIMEOUT = @"customSnapshotTimeout"; NSString* const FB_SETTING_SNAPSHOT_MAX_DEPTH = @"snapshotMaxDepth"; +NSString* const FB_SETTING_SNAPSHOT_MAX_CHILDREN = @"snapshotMaxChildren"; NSString* const FB_SETTING_USE_FIRST_MATCH = @"useFirstMatch"; NSString* const FB_SETTING_BOUND_ELEMENTS_BY_INDEX = @"boundElementsByIndex"; NSString* const FB_SETTING_REDUCE_MOTION = @"reduceMotion"; NSString* const FB_SETTING_DEFAULT_ACTIVE_APPLICATION = @"defaultActiveApplication"; NSString* const FB_SETTING_ACTIVE_APP_DETECTION_POINT = @"activeAppDetectionPoint"; -NSString* const FB_SETTING_INCLUDE_NON_MODAL_ELEMENTS = @"includeNonModalElements"; NSString* const FB_SETTING_DEFAULT_ALERT_ACTION = @"defaultAlertAction"; NSString* const FB_SETTING_ACCEPT_ALERT_BUTTON_SELECTOR = @"acceptAlertButtonSelector"; NSString* const FB_SETTING_DISMISS_ALERT_BUTTON_SELECTOR = @"dismissAlertButtonSelector"; NSString* const FB_SETTING_SCREENSHOT_ORIENTATION = @"screenshotOrientation"; NSString* const FB_SETTING_WAIT_FOR_IDLE_TIMEOUT = @"waitForIdleTimeout"; NSString* const FB_SETTING_ANIMATION_COOL_OFF_TIMEOUT = @"animationCoolOffTimeout"; +NSString* const FB_SETTING_MAX_TYPING_FREQUENCY = @"maxTypingFrequency"; +NSString* const FB_SETTING_RESPECT_SYSTEM_ALERTS = @"respectSystemAlerts"; +NSString* const FB_SETTING_USE_CLEAR_TEXT_SHORTCUT = @"useClearTextShortcut"; +NSString* const FB_SETTING_LIMIT_XPATH_CONTEXT_SCOPE = @"limitXPathContextScope"; +NSString* const FB_SETTING_AUTO_CLICK_ALERT_SELECTOR = @"autoClickAlertSelector"; +NSString* const FB_SETTING_INCLUDE_HITTABLE_IN_PAGE_SOURCE = @"includeHittableInPageSource"; +NSString* const FB_SETTING_INCLUDE_NATIVE_FRAME_IN_PAGE_SOURCE = @"includeNativeFrameInPageSource"; +NSString* const FB_SETTING_INCLUDE_MIN_MAX_VALUE_IN_PAGE_SOURCE = @"includeMinMaxValueInPageSource"; +NSString* const FB_SETTING_INCLUDE_CUSTOM_ACTIONS_IN_PAGE_SOURCE = @"includeCustomActionsInPageSource"; +NSString* const FB_SETTING_ENFORCE_CUSTOM_SNAPSHOTS = @"enforceCustomSnapshots"; diff --git a/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h b/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h index 2496ccbda..8b7f775df 100644 --- a/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h +++ b/WebDriverAgentLib/Utilities/FBTVNavigationTracker-Private.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h b/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h index 9e539f9e0..3a80583b4 100644 --- a/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h +++ b/WebDriverAgentLib/Utilities/FBTVNavigationTracker.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m b/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m index 7476f09bb..19fa4a0b6 100644 --- a/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m +++ b/WebDriverAgentLib/Utilities/FBTVNavigationTracker.m @@ -3,14 +3,12 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTVNavigationTracker.h" #import "FBTVNavigationTracker-Private.h" -#import "FBApplication.h" #import "FBMathUtils.h" #import "XCUIElement+FBCaching.h" #import "XCUIElement+FBUtilities.h" @@ -58,9 +56,7 @@ - (instancetype)initWithTargetElement:(XCUIElement *)targetElement self = [super init]; if (self) { _targetElement = targetElement; - CGRect frame = targetElement.fb_isResolvedFromCache.boolValue - ? [FBXCElementSnapshotWrapper ensureWrapped:targetElement.lastSnapshot].wdFrame - : targetElement.wdFrame; + CGRect frame = targetElement.wdFrame; _targetCenter = FBRectGetCenter(frame); _navigationItems = [NSMutableDictionary dictionary]; } @@ -69,7 +65,7 @@ - (instancetype)initWithTargetElement:(XCUIElement *)targetElement - (FBTVDirection)directionToFocusedElement { - XCUIElement *focused = FBApplication.fb_activeApplication.fb_focusedElement; + XCUIElement *focused = XCUIApplication.fb_activeApplication.fb_focusedElement; CGPoint focusedCenter = FBRectGetCenter(focused.wdFrame); FBTVNavigationItem *item = [self navigationItemWithElement:focused]; diff --git a/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h index eabf476f7..f640405b0 100644 --- a/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h +++ b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m index f877b67c6..a8679a872 100644 --- a/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m +++ b/WebDriverAgentLib/Utilities/FBUnattachedAppLauncher.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBUnattachedAppLauncher.h" diff --git a/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h b/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h index afd8249f8..3add9b1b6 100644 --- a/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h +++ b/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.h @@ -3,8 +3,7 @@ * 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. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import @@ -30,19 +29,14 @@ NSString *_Nullable FBRequireValue(NSDictionary *actionItem, NSE */ NSNumber *_Nullable FBOptDuration(NSDictionary *actionItem, NSNumber *_Nullable defaultValue, NSError **error); -/** - * Checks whether the given key action value is a W3C meta modifier - * @param value key action value - * @returns YES if the value is a meta modifier - */ -BOOL FBIsMetaModifier(NSString *value); - /** * Maps W3C meta modifier to XCUITest compatible-one + * See https://w3c.github.io/webdriver/#keyboard-actions * * @param value key action value - * @returns the mapped modifier value or 0 in case of failure + * @returns the mapped modifier value or the same input character + * if no mapped value could be found for it. */ -NSUInteger FBToMetaModifier(NSString *value); +NSString * FBMapIfSpecialCharacter(NSString *value); NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m b/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m index bcbf5a783..f7a052d4f 100644 --- a/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m +++ b/WebDriverAgentLib/Utilities/FBW3CActionsHelpers.m @@ -3,8 +3,7 @@ * 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. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "FBW3CActionsHelpers.h" @@ -53,32 +52,68 @@ return durationObj; } -BOOL FBIsMetaModifier(NSString *value) +NSString *FBMapIfSpecialCharacter(NSString *value) { - unichar charCode = [value characterAtIndex:0]; - return charCode >= 0xE000 && charCode <= 0xF8FF; -} - -NSUInteger FBToMetaModifier(NSString *value) -{ - if (!FBIsMetaModifier(value)) { - return 0; + if (0 == [value length]) { + return value; } unichar charCode = [value characterAtIndex:0]; switch (charCode) { case 0xE000: - return XCUIKeyModifierNone; - case 0xE03D: - return XCUIKeyModifierCommand; - case 0xE009: - return XCUIKeyModifierControl; - case 0xE00A: - return XCUIKeyModifierOption; - case 0xE008: - return XCUIKeyModifierShift; + return @""; + case 0xE003: + return [NSString stringWithFormat:@"%C", 0x0008]; + case 0xE004: + return [NSString stringWithFormat:@"%C", 0x0009]; + case 0xE006: + return [NSString stringWithFormat:@"%C", 0x000D]; + case 0xE007: + return [NSString stringWithFormat:@"%C", 0x000A]; + case 0xE00C: + return [NSString stringWithFormat:@"%C", 0x001B]; + case 0xE00D: + case 0xE05D: + return @" "; + case 0xE017: + return [NSString stringWithFormat:@"%C", 0x007F]; + case 0xE018: + return @";"; + case 0xE019: + return @"="; + case 0xE01A: + return @"0"; + case 0xE01B: + return @"1"; + case 0xE01C: + return @"2"; + case 0xE01D: + return @"3"; + case 0xE01E: + return @"4"; + case 0xE01F: + return @"5"; + case 0xE020: + return @"6"; + case 0xE021: + return @"7"; + case 0xE022: + return @"8"; + case 0xE023: + return @"9"; + case 0xE024: + return @"*"; + case 0xE025: + return @"+"; + case 0xE026: + return @","; + case 0xE027: + return @"-"; + case 0xE028: + return @"."; + case 0xE029: + return @"/"; default: - [FBLogger logFmt:@"Skipping the unsupported meta modifier with code %@", @(charCode)]; - return 0; + return charCode >= 0xE000 && charCode <= 0xE05D ? @"" : value; } } diff --git a/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h index e5ed479a7..746c38e36 100644 --- a/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h +++ b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBBaseActionsSynthesizer.h" diff --git a/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m index dc5060568..ef17e0350 100644 --- a/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m +++ b/WebDriverAgentLib/Utilities/FBW3CActionsSynthesizer.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBW3CActionsSynthesizer.h" @@ -410,17 +409,12 @@ - (BOOL)hasDownPairInItems:(NSArray *)allItems currentItemIndex:(NSUInteger)currentItemIndex { NSInteger balance = 1; - BOOL isSelfMetaModifier = FBIsMetaModifier(self.value); for (NSInteger index = currentItemIndex - 1; index >= 0; index--) { FBW3CKeyItem *item = [allItems objectAtIndex:index]; BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class]; BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class]; if (!isKeyUp && !isKeyDown) { - if (isSelfMetaModifier) { - continue; - } else { - break; - } + break; } NSString *value = [item performSelector:@selector(value)]; @@ -434,32 +428,6 @@ - (BOOL)hasDownPairInItems:(NSArray *)allItems return 0 == balance; } -- (NSUInteger)collectModifersWithItems:(NSArray *)allItems - currentItemIndex:(NSUInteger)currentItemIndex -{ - NSUInteger modifiers = 0; - for (NSUInteger index = 0; index < currentItemIndex; index++) { - FBW3CKeyItem *item = [allItems objectAtIndex:index]; - BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class]; - BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class]; - if (!isKeyUp && !isKeyDown) { - continue; - } - - NSString *value = [item performSelector:@selector(value)]; - NSUInteger modifier = FBToMetaModifier(value); - if (modifier > 0) { - if (isKeyDown) { - modifiers |= modifier; - } else if (item.offset < self.offset) { - // only cancel the modifier if it is not in the same group - modifiers &= ~modifier; - } - } - } - return modifiers; -} - - (NSString *)collectTextWithItems:(NSArray *)allItems currentItemIndex:(NSUInteger)currentItemIndex { @@ -473,12 +441,8 @@ - (NSString *)collectTextWithItems:(NSArray *)allItems } NSString *value = [item performSelector:@selector(value)]; - if (FBIsMetaModifier(value)) { - continue; - } - if (isKeyUp) { - [result addObject:value]; + [result addObject:FBMapIfSpecialCharacter(value)]; } } return [result.reverseObjectEnumerator.allObjects componentsJoinedByString:@""]; @@ -497,10 +461,6 @@ - (NSString *)collectTextWithItems:(NSArray *)allItems return nil; } - if (FBIsMetaModifier(self.value)) { - return @[]; - } - BOOL isLastKeyUpInGroup = currentItemIndex == allItems.count - 1 || [[allItems objectAtIndex:currentItemIndex + 1] isKindOfClass:FBKeyPauseItem.class]; if (!isLastKeyUpInGroup) { @@ -510,10 +470,6 @@ - (NSString *)collectTextWithItems:(NSArray *)allItems NSString *text = [self collectTextWithItems:allItems currentItemIndex:currentItemIndex]; NSTimeInterval offset = FBMillisToSeconds(self.offset); XCPointerEventPath *resultPath = [[XCPointerEventPath alloc] initForTextInput]; - // TODO: Figure out how meta modifiers could be applied - // TODO: The current approach throws zero division error on execution - // NSUInteger modifiers = [self collectModifersWithItems:allItems currentItemIndex:currentItemIndex]; - // [resultPath setModifiers:modifiers mergeWithCurrentModifierFlags:NO atOffset:0]; [resultPath typeText:text atOffset:offset typingSpeed:FBConfiguration.maxTypingFrequency @@ -555,17 +511,12 @@ - (BOOL)hasUpPairInItems:(NSArray *)allItems currentItemIndex:(NSUInteger)currentItemIndex { NSInteger balance = 1; - BOOL isSelfMetaModifier = FBIsMetaModifier(self.value); for (NSUInteger index = currentItemIndex + 1; index < allItems.count; index++) { FBW3CKeyItem *item = [allItems objectAtIndex:index]; BOOL isKeyDown = [item isKindOfClass:FBKeyDownItem.class]; BOOL isKeyUp = !isKeyDown && [item isKindOfClass:FBKeyUpItem.class]; if (!isKeyUp && !isKeyDown) { - if (isSelfMetaModifier) { - continue; - } else { - break; - } + break; } NSString *value = [item performSelector:@selector(value)]; @@ -709,7 +660,7 @@ @implementation FBW3CActionsSynthesizer if ([origin isKindOfClass:XCUIElement.class]) { instance = origin; } else if ([origin isKindOfClass:NSString.class]) { - instance = [self.elementCache elementForUUID:(NSString *)origin]; + instance = [self.elementCache elementForUUID:(NSString *)origin checkStaleness:YES]; } else { [result addObject:actionItem]; continue; @@ -819,7 +770,7 @@ @implementation FBW3CActionsSynthesizer NSArray *> *actionItems = [actionDescription objectForKey:FB_KEY_ACTIONS]; if (nil == actionItems || 0 == actionItems.count) { - NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one gesture item defined for each action. Action with id '%@' contains none", actionId]; + NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one gesture item defined for each action. Action with id '%@' contains none", actionId]; if (error) { *error = [[FBErrorBuilder.builder withDescription:description] build]; } @@ -900,7 +851,20 @@ - (nullable XCSynthesizedEventRecord *)synthesizeWithError:(NSError **)error *error = [[FBErrorBuilder.builder withDescription:description] build]; } return nil; + } + NSArray *> *actionItems = [action objectForKey:FB_KEY_ACTIONS]; + if (nil == actionItems) { + NSString *description = [NSString stringWithFormat:@"It is mandatory to have at least one item defined for each action. Action with id '%@' contains none", actionId]; + if (error) { + *error = [[FBErrorBuilder.builder withDescription:description] build]; + } + return nil; } + if (0 == actionItems.count) { + [FBLogger logFmt:@"Action items in the action id '%@' had an empty array. Skipping the action.", actionId]; + continue; + } + [actionIds addObject:actionId]; [actionsMapping setObject:action forKey:actionId]; } diff --git a/WebDriverAgentLib/Utilities/FBWebServerParams.h b/WebDriverAgentLib/Utilities/FBWebServerParams.h new file mode 100644 index 000000000..aba0fa34f --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBWebServerParams.h @@ -0,0 +1,22 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface FBWebServerParams : NSObject + +/** The local port number WDA server is running on */ +@property (nonatomic, nullable) NSNumber *port; + ++ (id)sharedInstance; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBWebServerParams.m b/WebDriverAgentLib/Utilities/FBWebServerParams.m new file mode 100644 index 000000000..9ced0a38d --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBWebServerParams.m @@ -0,0 +1,23 @@ +/** + * 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 "FBWebServerParams.h" + +@implementation FBWebServerParams + ++ (instancetype)sharedInstance +{ + static FBWebServerParams *instance; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[self alloc] init]; + }); + return instance; +} + +@end diff --git a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h index cf6f497a9..b216e303f 100644 --- a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h +++ b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -27,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN - (nullable id)snapshotForElement:(id)element attributes:(nullable NSArray *)attributes - maxDepth:(nullable NSNumber *)maxDepth + inDepth:(BOOL)inDepth error:(NSError **)error; - (NSArray> *)activeApplications; @@ -39,10 +38,11 @@ NS_ASSUME_NONNULL_BEGIN - (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)application reply:(void (^)(void))reply; -- (NSDictionary *)attributesForElement:(id)element - attributes:(NSArray *)attributes; +- (nullable NSDictionary *)attributesForElement:(id)element + attributes:(NSArray *)attributes + error:(NSError**)error; -- (XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid; +- (nullable XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid; @end diff --git a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m index 907bb25b1..f84f803fb 100644 --- a/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m +++ b/WebDriverAgentLib/Utilities/FBXCAXClientProxy.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCAXClientProxy.h" @@ -14,9 +13,16 @@ #import "FBMacros.h" #import "XCAXClient_iOS+FBSnapshotReqParams.h" #import "XCUIDevice.h" +#import "XCUIApplication.h" static id FBAXClient = nil; +@interface FBXCAXClientProxy () + +@property (nonatomic) NSMutableDictionary *appsCache; + +@end + @implementation FBXCAXClientProxy + (instancetype)sharedClient @@ -25,6 +31,7 @@ + (instancetype)sharedClient static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; + instance.appsCache = [NSMutableDictionary dictionary]; FBAXClient = [XCUIDevice.sharedDevice accessibilityInterface]; }); return instance; @@ -37,12 +44,12 @@ - (BOOL)setAXTimeout:(NSTimeInterval)timeout error:(NSError **)error - (id)snapshotForElement:(id)element attributes:(NSArray *)attributes - maxDepth:(nullable NSNumber *)maxDepth + inDepth:(BOOL)inDepth error:(NSError **)error { NSMutableDictionary *parameters = [NSMutableDictionary dictionaryWithDictionary:self.defaultParameters]; - if (nil != maxDepth) { - parameters[FBSnapshotMaxDepthKey] = maxDepth; + if (!inDepth) { + parameters[FBSnapshotMaxDepthKey] = @1; } id result = [FBAXClient requestSnapshotForElement:element @@ -76,20 +83,37 @@ - (void)notifyWhenNoAnimationsAreActiveForApplication:(XCUIApplication *)applica - (NSDictionary *)attributesForElement:(id)element attributes:(NSArray *)attributes + error:(NSError**)error; { - NSError *error = nil; - NSDictionary* result = [FBAXClient attributesForElement:element - attributes:attributes - error:&error]; - if (error) { - [FBLogger logFmt:@"Cannot retrieve element attribute(s) %@. Original error: %@", attributes, error.description]; - } - return result; + return [FBAXClient attributesForElement:element + attributes:attributes + error:error]; } - (XCUIApplication *)monitoredApplicationWithProcessIdentifier:(int)pid { - return [[FBAXClient applicationProcessTracker] monitoredApplicationWithProcessIdentifier:pid]; + 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]; + } + + XCUIApplication *result = [self.appsCache objectForKey:@(pid)]; + if (nil != result) { + return result; + } + + XCUIApplication *app = [[FBAXClient applicationProcessTracker] + monitoredApplicationWithProcessIdentifier:pid]; + if (nil == app) { + return nil; + } + [self.appsCache setObject:app forKey:@(pid)]; + return app; } @end diff --git a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h index 307edb40d..41514a5cd 100644 --- a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h +++ b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -18,6 +17,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol XCTestManager_ManagerInterface; +@class FBScreenRecordingRequest, FBScreenRecordingPromise; @interface FBXCTestDaemonsProxy : NSObject @@ -29,6 +29,11 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSError **)error; + (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError **)error; ++ (nullable FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecordingRequest *)request + error:(NSError **)error; ++ (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid + error:(NSError **)error; + #if !TARGET_OS_TV + (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError **)error; + (nullable CLLocation *)getSimulatedLocation:(NSError **)error; diff --git a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m index 180689800..e29b94e99 100644 --- a/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m +++ b/WebDriverAgentLib/Utilities/FBXCTestDaemonsProxy.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCTestDaemonsProxy.h" @@ -16,6 +15,8 @@ #import "FBExceptions.h" #import "FBLogger.h" #import "FBRunLoopSpinner.h" +#import "FBScreenRecordingPromise.h" +#import "FBScreenRecordingRequest.h" #import "XCTestDriver.h" #import "XCTRunnerDaemonSession.h" #import "XCUIApplication.h" @@ -71,9 +72,12 @@ + (void)swizzleLaunchApp { [FBLogger log:@"Could not find method -[XCTRunnerDaemonSession launchApplicationWithPath:]"]; return; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-function-type-strict" // Workaround for https://github.com/appium/WebDriverAgent/issues/702 originalLaunchAppMethod = (void(*)(id, SEL, NSString*, NSString*, NSArray*, NSDictionary*, void (^)(_Bool, NSError *))) method_getImplementation(original); method_setImplementation(original, (IMP)swizzledLaunchApp); +#pragma clang diagnostic pop } + (id)testRunnerProxy @@ -129,10 +133,9 @@ + (BOOL)openURL:(NSURL *)url usingApplication:(NSString *)bundleId error:(NSErro { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(openURL:usingApplication:completion:)]) { - [[[FBErrorBuilder builder] + return [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs with given application"] buildError:error]; - return NO; } __block NSError *innerError = nil; @@ -157,10 +160,9 @@ + (BOOL)openDefaultApplicationForURL:(NSURL *)url error:(NSError *__autoreleasin { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(openDefaultApplicationForURL:completion:)]) { - [[[FBErrorBuilder builder] + return [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support opening of URLs. Consider upgrading to Xcode 14.3+/iOS 16.4+"] buildError:error]; - return NO; } __block NSError *innerError = nil; @@ -186,16 +188,14 @@ + (BOOL)setSimulatedLocation:(CLLocation *)location error:(NSError *__autoreleas { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(setSimulatedLocation:completion:)]) { - [[[FBErrorBuilder builder] + return [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] buildError:error]; - return NO; } if (![session supportsLocationSimulation]) { - [[[FBErrorBuilder builder] + return [[[FBErrorBuilder builder] withDescriptionFormat:@"Your device does not support location simulation"] buildError:error]; - return NO; } __block NSError *innerError = nil; @@ -254,20 +254,14 @@ + (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error { XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; if (![session respondsToSelector:@selector(clearSimulatedLocationWithReply:)]) { - if (error) { - [[[FBErrorBuilder builder] + return [[[FBErrorBuilder builder] withDescriptionFormat:@"The current Xcode SDK does not support location simulation. Consider upgrading to Xcode 14.3+/iOS 16.4+"] buildError:error]; - } - return NO; } if (![session supportsLocationSimulation]) { - if (error) { - [[[FBErrorBuilder builder] + return [[[FBErrorBuilder builder] withDescriptionFormat:@"Your device does not support location simulation"] buildError:error]; - } - return NO; } __block NSError *innerError = nil; @@ -289,4 +283,77 @@ + (BOOL)clearSimulatedLocation:(NSError *__autoreleasing*)error } #endif ++ (FBScreenRecordingPromise *)startScreenRecordingWithRequest:(FBScreenRecordingRequest *)request + error:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(startScreenRecordingWithRequest:withReply:)]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"] + buildError:error]; + return nil; + } + if (![session supportsScreenRecording]) { + [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support screen recording"] + buildError:error]; + return nil; + } + + id nativeRequest = [request toNativeRequestWithError:error]; + if (nil == nativeRequest) { + return nil; + } + + __block id futureMetadata = nil; + __block NSError *innerError = nil; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session startScreenRecordingWithRequest:nativeRequest withReply:^(id reply, NSError *invokeError) { + if (nil == invokeError) { + futureMetadata = reply; + } else { + innerError = invokeError; + } + completion(); + }]; + }]; + if (nil != innerError) { + if (error) { + *error = innerError; + } + return nil; + } + return [[FBScreenRecordingPromise alloc] initWithNativePromise:futureMetadata]; +} + ++ (BOOL)stopScreenRecordingWithUUID:(NSUUID *)uuid error:(NSError *__autoreleasing*)error +{ + XCTRunnerDaemonSession *session = [XCTRunnerDaemonSession sharedSession]; + if (![session respondsToSelector:@selector(stopScreenRecordingWithUUID:withReply:)]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"The current Xcode SDK does not support screen recording. Consider upgrading to Xcode 15+/iOS 17+"] + buildError:error]; + + } + if (![session supportsScreenRecording]) { + return [[[FBErrorBuilder builder] + withDescriptionFormat:@"Your device does not support screen recording"] + buildError:error]; + } + + __block NSError *innerError = nil; + [FBRunLoopSpinner spinUntilCompletion:^(void(^completion)(void)){ + [session stopScreenRecordingWithUUID:uuid withReply:^(NSError *invokeError) { + if (nil != invokeError) { + innerError = invokeError; + } + completion(); + }]; + }]; + if (nil != innerError && error) { + *error = innerError; + } + return nil == innerError; +} + @end diff --git a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h index 3922a876d..247f056bd 100644 --- a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h +++ b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.h @@ -3,13 +3,14 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import #import "XCPointerEvent.h" +@class FBXCElementSnapshot; + /** The version of testmanagerd process which is running on the device. @@ -28,39 +29,6 @@ NSInteger FBTestmanagerdVersion(void); NS_ASSUME_NONNULL_BEGIN -/** - The exception happends if one tries to call application method, - which is not supported in the current iOS version - */ -extern NSString *const FBApplicationMethodNotSupportedException; - -@interface XCUIApplication (FBCompatibility) - -+ (nullable instancetype)fb_applicationWithPID:(pid_t)processID; - -/** - Get the state of the application. This method only returns reliable results on Xcode SDK 9+ - - @return State value as enum item. See https://developer.apple.com/documentation/xctest/xcuiapplicationstate?language=objc for more details. - */ -- (NSUInteger)fb_state; - -/** - Activate the application by restoring it from the background. - Nothing will happen if the application is already in foreground. - This method is only supported since Xcode9. - - @throws FBTimeoutException if the app is still not active after the timeout - */ -- (void)fb_activate; - -/** - Terminate the application and wait until it disappears from the list of active apps - */ -- (void)fb_terminate; - -@end - @interface XCUIElementQuery (FBCompatibility) /* Performs short-circuit UI tree traversion in iOS 11+ to get the first element matched by the query. Equals to nil if no matching elements are found */ @@ -79,7 +47,7 @@ extern NSString *const FBApplicationMethodNotSupportedException; @param error The error instance if there was a failure while retrieveing the snapshot @returns The cached unqiue snapshot or nil if the element is stale */ -- (nullable XCElementSnapshot *)fb_uniqueSnapshotWithError:(NSError **)error; +- (nullable id)fb_uniqueSnapshotWithError:(NSError **)error; @end @@ -93,17 +61,10 @@ extern NSString *const FBApplicationMethodNotSupportedException; @interface XCUIElement (FBCompatibility) -/** - Determines whether current iOS SDK supports non modal elements inlusion into snapshots - - @return Either YES or NO - */ -+ (BOOL)fb_supportsNonModalElementsInclusion; - /** Retrieves element query - @return Element query property extended with non modal elements depending on the actual configuration + @return Element query */ - (XCUIElementQuery *)fb_query; diff --git a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m index 4d7bb621d..5f88ccb56 100644 --- a/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m +++ b/WebDriverAgentLib/Utilities/FBXCodeCompatibility.m @@ -3,12 +3,12 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXCodeCompatibility.h" +#import "FBXCAXClientProxy.h" #import "FBConfiguration.h" #import "FBErrorBuilder.h" #import "FBLogger.h" @@ -17,50 +17,11 @@ #import "FBXCTestDaemonsProxy.h" #import "XCTestManager_ManagerInterface-Protocol.h" -static const NSTimeInterval APP_STATE_CHANGE_TIMEOUT = 5.0; - -NSString *const FBApplicationMethodNotSupportedException = @"FBApplicationMethodNotSupportedException"; - -@implementation XCUIApplication (FBCompatibility) - -+ (instancetype)fb_applicationWithPID:(pid_t)processID -{ - if (0 == processID) { - return nil; - } - - return [self applicationWithPID:processID]; -} - -- (void)fb_activate -{ - [self activate]; - if (![self waitForState:XCUIApplicationStateRunningForeground timeout:APP_STATE_CHANGE_TIMEOUT / 2] || ![self fb_waitForAppElement:APP_STATE_CHANGE_TIMEOUT / 2]) { - [FBLogger logFmt:@"The application '%@' is not running in foreground after %.2f seconds", self.bundleID, APP_STATE_CHANGE_TIMEOUT]; - } -} - -- (void)fb_terminate -{ - [self terminate]; - if (![self waitForState:XCUIApplicationStateNotRunning timeout:APP_STATE_CHANGE_TIMEOUT]) { - [FBLogger logFmt:@"The active application is still '%@' after %.2f seconds timeout", self.bundleID, APP_STATE_CHANGE_TIMEOUT]; - } -} - -- (NSUInteger)fb_state -{ - return [[self valueForKey:@"state"] intValue]; -} - -@end - - @implementation XCUIElementQuery (FBCompatibility) -- (XCElementSnapshot *)fb_uniqueSnapshotWithError:(NSError **)error +- (id)fb_uniqueSnapshotWithError:(NSError **)error { - return [self uniqueMatchingSnapshotWithError:error]; + return (id)[self uniqueMatchingSnapshotWithError:error]; } - (XCUIElement *)fb_firstMatch @@ -84,21 +45,9 @@ - (XCUIElement *)fb_firstMatch @implementation XCUIElement (FBCompatibility) -+ (BOOL)fb_supportsNonModalElementsInclusion -{ - static dispatch_once_t hasIncludingNonModalElements; - static BOOL result; - dispatch_once(&hasIncludingNonModalElements, ^{ - result = [FBApplication.fb_systemApplication.query respondsToSelector:@selector(includingNonModalElements)]; - }); - return result; -} - - (XCUIElementQuery *)fb_query { - return FBConfiguration.includeNonModalElements && self.class.fb_supportsNonModalElementsInclusion - ? self.query.includingNonModalElements - : self.query; + return self.query; } @end diff --git a/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h b/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h index 0fd71e139..8a014c12b 100644 --- a/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h +++ b/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m b/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m index 23bfa5892..40dcebd23 100644 --- a/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m +++ b/WebDriverAgentLib/Utilities/FBXMLGenerationOptions.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXMLGenerationOptions.h" diff --git a/WebDriverAgentLib/Utilities/FBXPath-Private.h b/WebDriverAgentLib/Utilities/FBXPath-Private.h index 31bb403bf..fa4b16265 100644 --- a/WebDriverAgentLib/Utilities/FBXPath-Private.h +++ b/WebDriverAgentLib/Utilities/FBXPath-Private.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -25,7 +24,7 @@ NS_ASSUME_NONNULL_BEGIN If `query` argument is assigned then `excludedAttributes` argument is effectively ignored. @return zero if the method has completed successfully */ -+ (int)xmlRepresentationWithRootElement:(id)root ++ (int)xmlRepresentationWithRootElement:(id)root writer:(xmlTextWriterPtr)writer elementStore:(nullable NSMutableDictionary *)elementStore query:(nullable NSString*)query @@ -45,9 +44,17 @@ NS_ASSUME_NONNULL_BEGIN @param xpathQuery actual query. Should be valid XPath 1.0-compatible expression @param document libxml2-compatible document pointer + @param contextNode Optonal context node instance @return pointer to a libxml2-compatible structure with set of matched nodes or NULL in case of failure */ -+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc; ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode; + ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode + errorMessage:(NSString * _Nullable * _Nullable)errorMessage; @end diff --git a/WebDriverAgentLib/Utilities/FBXPath.h b/WebDriverAgentLib/Utilities/FBXPath.h index 50c851978..1135c7228 100644 --- a/WebDriverAgentLib/Utilities/FBXPath.h +++ b/WebDriverAgentLib/Utilities/FBXPath.h @@ -3,13 +3,12 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import #import -#import "FBXCElementSnapshot.h" +#import #ifdef __clang__ #pragma clang diagnostic push diff --git a/WebDriverAgentLib/Utilities/FBXPath.m b/WebDriverAgentLib/Utilities/FBXPath.m index eae3ee438..18f01a3fc 100644 --- a/WebDriverAgentLib/Utilities/FBXPath.m +++ b/WebDriverAgentLib/Utilities/FBXPath.m @@ -3,24 +3,29 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBXPath.h" #import "FBConfiguration.h" #import "FBExceptions.h" +#import "FBElementUtils.h" #import "FBLogger.h" #import "FBMacros.h" #import "FBXMLGenerationOptions.h" +#import "FBXPathExtensions.h" #import "FBXCElementSnapshotWrapper+Helpers.h" #import "NSString+FBXMLSafeString.h" +#import "XCUIApplication.h" #import "XCUIElement.h" #import "XCUIElement+FBCaching.h" #import "XCUIElement+FBUtilities.h" #import "XCUIElement+FBWebDriverAttributes.h" #import "XCTestPrivateSymbols.h" +#import "FBElementHelpers.h" +#import "FBXCAXClientProxy.h" +#import "FBXCAccessibilityElement.h" @interface FBElementAttribute : NSObject @@ -31,6 +36,7 @@ + (nonnull NSString *)name; + (nullable NSString *)valueForElement:(id)element; + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element; ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value; + (NSArray *)supportedAttributes; @@ -96,7 +102,37 @@ @interface FBInternalIndexAttribute : FBElementAttribute @property (nonatomic, nonnull, readonly) NSString* indexValue; -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value; +@end + +@interface FBApplicationBundleIdAttribute : FBElementAttribute + +@end + +@interface FBApplicationPidAttribute : FBElementAttribute + +@end + +@interface FBPlaceholderValueAttribute : FBElementAttribute + +@end + +@interface FBNativeFrameAttribute : FBElementAttribute + +@end + +@interface FBTraitsAttribute : FBElementAttribute + +@end + +@interface FBMinValueAttribute : FBElementAttribute + +@end + +@interface FBMaxValueAttribute : FBElementAttribute + +@end + +@interface FBCustomActionsAttribute : FBElementAttribute @end @@ -117,7 +153,17 @@ @implementation FBXPath + (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery { - NSString *reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery]; + return [self throwException:name forQuery:xpathQuery detail:nil]; +} + ++ (id)throwException:(NSString *)name forQuery:(NSString *)xpathQuery detail:(nullable NSString *)detail +{ + NSString *reason; + if (nil != detail) { + reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\": %@", xpathQuery, detail]; + } else { + reason = [NSString stringWithFormat:@"Cannot evaluate results for XPath expression \"%@\"", xpathQuery]; + } @throw [NSException exceptionWithName:name reason:reason userInfo:@{}]; return nil; } @@ -141,7 +187,11 @@ + (nullable NSString *)xmlStringWithRootElement:(id)root } if (rc >= 0) { - rc = [self xmlRepresentationWithRootElement:root + [self waitUntilStableWithElement:root]; + // If 'includeHittableInPageSource' setting is enabled, then use native snapshots + // to calculate a more accurate value for the 'hittable' attribute. + rc = [self xmlRepresentationWithRootElement:[self snapshotWithRoot:root + useNative:FBConfiguration.includeHittableInPageSource] writer:writer elementStore:nil query:nil @@ -189,10 +239,35 @@ + (nullable NSString *)xmlStringWithRootElement:(id)root } NSMutableDictionary *elementStore = [NSMutableDictionary dictionary]; int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL); + id lookupScopeSnapshot = nil; + id contextRootSnapshot = nil; + BOOL useNativeSnapshot = nil == xpathQuery + ? NO + : [[self.class elementAttributesWithXPathQuery:xpathQuery] containsObject:FBHittableAttribute.class]; if (rc < 0) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc]; } else { - rc = [self xmlRepresentationWithRootElement:root + [self waitUntilStableWithElement:root]; + if (FBConfiguration.limitXpathContextScope) { + lookupScopeSnapshot = [self snapshotWithRoot:root useNative:useNativeSnapshot]; + } else { + if ([root isKindOfClass:XCUIElement.class]) { + lookupScopeSnapshot = [self snapshotWithRoot:[(XCUIElement *)root application] + useNative:useNativeSnapshot]; + contextRootSnapshot = [root isKindOfClass:XCUIApplication.class] + ? nil + : ([(XCUIElement *)root lastSnapshot] ?: [self snapshotWithRoot:(XCUIElement *)root + useNative:useNativeSnapshot]); + } else { + lookupScopeSnapshot = (id)root; + contextRootSnapshot = nil == lookupScopeSnapshot.parent ? nil : (id)root; + while (nil != lookupScopeSnapshot.parent) { + lookupScopeSnapshot = lookupScopeSnapshot.parent; + } + } + } + + rc = [self xmlRepresentationWithRootElement:lookupScopeSnapshot writer:writer elementStore:elementStore query:xpathQuery @@ -210,11 +285,28 @@ + (nullable NSString *)xmlStringWithRootElement:(id)root return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; } - xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery document:doc]; + xmlXPathObjectPtr contextNodeQueryResult = [self matchNodeInDocument:doc + elementStore:elementStore.copy + forSnapshot:contextRootSnapshot]; + xmlNodePtr contextNode = NULL; + if (NULL != contextNodeQueryResult) { + xmlNodeSetPtr nodeSet = contextNodeQueryResult->nodesetval; + if (!xmlXPathNodeSetIsEmpty(nodeSet)) { + contextNode = nodeSet->nodeTab[0]; + } + } + NSString *evaluationError = nil; + xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery + document:doc + contextNode:contextNode + errorMessage:&evaluationError]; + if (NULL != contextNodeQueryResult) { + xmlXPathFreeObject(contextNodeQueryResult); + } if (NULL == queryResult) { xmlFreeTextWriter(writer); xmlFreeDoc(doc); - return [self throwException:FBInvalidXPathException forQuery:xpathQuery]; + return [self throwException:FBInvalidXPathException forQuery:xpathQuery detail:evaluationError]; } NSArray *matchingSnapshots = [self collectMatchingSnapshots:queryResult->nodesetval @@ -252,6 +344,36 @@ + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet return matchingSnapshots.copy; } ++ (nullable xmlXPathObjectPtr)matchNodeInDocument:(xmlDocPtr)doc + elementStore:(NSDictionary> *)elementStore + forSnapshot:(nullable id)snapshot +{ + if (nil == snapshot) { + return NULL; + } + + NSString *contextRootUid = [FBElementUtils uidWithAccessibilityElement:[(id)snapshot accessibilityElement]]; + if (nil == contextRootUid) { + return NULL; + } + + for (NSString *key in elementStore) { + id value = [elementStore objectForKey:key]; + NSString *snapshotUid = [FBElementUtils uidWithAccessibilityElement:[value accessibilityElement]]; + if (nil == snapshotUid || ![snapshotUid isEqualToString:contextRootUid]) { + continue; + } + NSString *indexQuery = [NSString stringWithFormat:@"//*[@%@=\"%@\"]", kXMLIndexPathKey, key]; + xmlXPathObjectPtr queryResult = [self evaluate:indexQuery + document:doc + contextNode:NULL]; + if (NULL != queryResult) { + return queryResult; + } + } + return NULL; +} + + (NSSet *)elementAttributesWithXPathQuery:(NSString *)query { if ([query rangeOfString:@"[^\\w@]@\\*[^\\w]" options:NSRegularExpressionSearch].location != NSNotFound) { @@ -267,7 +389,7 @@ + (NSArray *)collectMatchingSnapshots:(xmlNodeSetPtr)nodeSet return result.copy; } -+ (int)xmlRepresentationWithRootElement:(id)root ++ (int)xmlRepresentationWithRootElement:(id)root writer:(xmlTextWriterPtr)writer elementStore:(nullable NSMutableDictionary *)elementStore query:(nullable NSString*)query @@ -278,9 +400,24 @@ + (int)xmlRepresentationWithRootElement:(id)root NSMutableSet *includedAttributes; if (nil == query) { includedAttributes = [NSMutableSet setWithArray:FBElementAttribute.supportedAttributes]; - // The hittable attribute is expensive to calculate for each snapshot item - // thus we only include it when requested by an xPath query - [includedAttributes removeObject:FBHittableAttribute.class]; + if (!FBConfiguration.includeHittableInPageSource) { + // The hittable attribute is expensive to calculate for each snapshot item + // thus we only include it when requested explicitly + [includedAttributes removeObject:FBHittableAttribute.class]; + } + if (!FBConfiguration.includeNativeFrameInPageSource) { + // Include nativeFrame only when requested + [includedAttributes removeObject:FBNativeFrameAttribute.class]; + } + if (!FBConfiguration.includeMinMaxValueInPageSource) { + // minValue/maxValue are retrieved from private APIs and may be slow on deep trees + [includedAttributes removeObject:FBMinValueAttribute.class]; + [includedAttributes removeObject:FBMaxValueAttribute.class]; + } + if (!FBConfiguration.includeCustomActionsInPageSource) { + // customActions are retrieved from accessibility attributes and may be slow on deep trees + [includedAttributes removeObject:FBCustomActionsAttribute.class]; + } if (nil != excludedAttributes) { for (NSString *excludedAttributeName in excludedAttributes) { for (Class supportedAttribute in FBElementAttribute.supportedAttributes) { @@ -308,19 +445,40 @@ + (int)xmlRepresentationWithRootElement:(id)root return 0; } -+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode +{ + return [self evaluate:xpathQuery document:doc contextNode:contextNode errorMessage:nil]; +} + ++ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery + document:(xmlDocPtr)doc + contextNode:(nullable xmlNodePtr)contextNode + errorMessage:(NSString * _Nullable * _Nullable)errorMessage { xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); if (NULL == xpathCtx) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery]; return NULL; } - xpathCtx->node = doc->children; + xpathCtx->node = NULL == contextNode ? doc->children : contextNode; + + FBXPathExtensions *extensions = [FBXPathExtensions new]; + [extensions registerFunctionsWithContext:xpathCtx]; xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((const xmlChar *)[xpathQuery UTF8String], xpathCtx); if (NULL == xpathObj) { + NSString *detail = extensions.lastEvaluationError; + if (NULL != errorMessage) { + *errorMessage = detail; + } + if (nil != detail) { + [FBLogger logFmt:@"Failed to evaluate XPath query \"%@\": %@", xpathQuery, detail]; + } else { + [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery]; + } xmlXPathFreeContext(xpathCtx); - [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery]; return NULL; } xmlXPathFreeContext(xpathCtx); @@ -342,6 +500,16 @@ + (int)recordElementAttributes:(xmlTextWriterPtr)writer if (includedAttributes && ![includedAttributes containsObject:attributeCls]) { continue; } + // Text-input placeholder (only for elements that support inner text) + if ((attributeCls == FBPlaceholderValueAttribute.class) && + !FBDoesElementSupportInnerText(element.elementType)) { + continue; + } + // Only for elements that support min/max value + if ((attributeCls == FBMinValueAttribute.class || attributeCls == FBMaxValueAttribute.class) && + !FBDoesElementSupportMinMaxValue(element.elementType)) { + continue; + } int rc = [attributeCls recordWithWriter:writer forElement:[FBXCElementSnapshotWrapper ensureWrapped:element]]; if (rc < 0) { @@ -353,10 +521,31 @@ + (int)recordElementAttributes:(xmlTextWriterPtr)writer // index path is the special case return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath]; } + if (element.elementType == XCUIElementTypeApplication) { + // only record process identifier and bundle identifier for the application element + int pid = [element.accessibilityElement processIdentifier]; + if (pid > 0) { + int rc = [FBApplicationPidAttribute recordWithWriter:writer + forValue:[NSString stringWithFormat:@"%d", pid]]; + if (rc < 0) { + return rc; + } + XCUIApplication *app = [[FBXCAXClientProxy sharedClient] + monitoredApplicationWithProcessIdentifier:pid]; + NSString *bundleID = [app bundleID]; + if (nil != bundleID) { + rc = [FBApplicationBundleIdAttribute recordWithWriter:writer + forValue:bundleID]; + if (rc < 0) { + return rc; + } + } + } + } return 0; } -+ (int)writeXmlWithRootElement:(id)root ++ (int)writeXmlWithRootElement:(id)root indexPath:(nullable NSString *)indexPath elementStore:(nullable NSMutableDictionary *)elementStore includedAttributes:(nullable NSSet *)includedAttributes @@ -364,30 +553,13 @@ + (int)writeXmlWithRootElement:(id)root { NSAssert((indexPath == nil && elementStore == nil) || (indexPath != nil && elementStore != nil), @"Either both or none of indexPath and elementStore arguments should be equal to nil", nil); - id currentSnapshot; - NSArray> *children; - if ([root isKindOfClass:XCUIElement.class]) { - XCUIElement *element = (XCUIElement *)root; - NSMutableArray *snapshotAttributes = [NSMutableArray arrayWithArray:FBStandardAttributeNames()]; - if (nil == includedAttributes || [includedAttributes containsObject:FBVisibleAttribute.class]) { - [snapshotAttributes addObject:FB_XCAXAIsVisibleAttributeName]; - // If the app is not idle state while we retrieve the visiblity state - // then the snapshot retrieval operation might freeze and time out - [element.application fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; - } - currentSnapshot = [element fb_snapshotWithAttributes:snapshotAttributes.copy - maxDepth:nil]; - children = currentSnapshot.children; - } else { - currentSnapshot = (id)root; - children = currentSnapshot.children; - } + NSArray> *children = root.children; if (elementStore != nil && indexPath != nil && [indexPath isEqualToString:topNodeIndexPath]) { - [elementStore setObject:currentSnapshot forKey:topNodeIndexPath]; + [elementStore setObject:root forKey:topNodeIndexPath]; } - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:currentSnapshot]; + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:root]; int rc = xmlTextWriterStartElement(writer, (xmlChar *)[wrappedSnapshot.wdType UTF8String]); if (rc < 0) { [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", wrappedSnapshot.wdType, rc]; @@ -395,7 +567,7 @@ + (int)writeXmlWithRootElement:(id)root } rc = [self recordElementAttributes:writer - forElement:currentSnapshot + forElement:root indexPath:indexPath includedAttributes:includedAttributes]; if (rc < 0) { @@ -403,18 +575,20 @@ + (int)writeXmlWithRootElement:(id)root } for (NSUInteger i = 0; i < [children count]; i++) { - id childSnapshot = [children objectAtIndex:i]; - NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil; - if (elementStore != nil && newIndexPath != nil) { - [elementStore setObject:childSnapshot forKey:(id)newIndexPath]; - } - rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot] - indexPath:newIndexPath - elementStore:elementStore - includedAttributes:includedAttributes - writer:writer]; - if (rc < 0) { - return rc; + @autoreleasepool { + id childSnapshot = [children objectAtIndex:i]; + NSString *newIndexPath = (indexPath != nil) ? [indexPath stringByAppendingFormat:@",%lu", (unsigned long)i] : nil; + if (elementStore != nil && newIndexPath != nil) { + [elementStore setObject:childSnapshot forKey:(id)newIndexPath]; + } + rc = [self writeXmlWithRootElement:[FBXCElementSnapshotWrapper ensureWrapped:childSnapshot] + indexPath:newIndexPath + elementStore:elementStore + includedAttributes:includedAttributes + writer:writer]; + if (rc < 0) { + return rc; + } } } @@ -426,6 +600,32 @@ + (int)writeXmlWithRootElement:(id)root return 0; } ++ (id)snapshotWithRoot:(id)root + useNative:(BOOL)useNative +{ + if (![root isKindOfClass:XCUIElement.class]) { + return (id)root; + } + + // https://github.com/appium/appium-xcuitest-driver/pull/2565 + if (useNative) { + return [(XCUIElement *)root fb_nativeSnapshot]; + } + // https://github.com/appium/WebDriverAgent/issues/1085 + return [root isKindOfClass:XCUIApplication.class] && !FBConfiguration.enforceCustomSnapshots + ? [(XCUIElement *)root fb_standardSnapshot] + : [(XCUIElement *)root fb_customSnapshot]; +} + ++ (void)waitUntilStableWithElement:(id)root +{ + if ([root isKindOfClass:XCUIElement.class]) { + // If the app is not idle state while we retrieve the visiblity state + // then the snapshot retrieval operation might freeze and time out + [[(XCUIElement *)root application] fb_waitUntilStableWithTimeout:FBConfiguration.animationCoolOffTimeout]; + } +} + @end @@ -457,6 +657,11 @@ + (NSString *)valueForElement:(id)element + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element { NSString *value = [self valueForElement:element]; + return [self recordWithWriter:writer forValue:value]; +} + ++ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(nullable NSString *)value +{ if (nil == value) { // Skip the attribute if the value equals to nil return 0; @@ -490,6 +695,12 @@ + (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)eleme FBHeightAttribute.class, FBIndexAttribute.class, FBHittableAttribute.class, + FBPlaceholderValueAttribute.class, + FBTraitsAttribute.class, + FBNativeFrameAttribute.class, + FBMinValueAttribute.class, + FBMaxValueAttribute.class, + FBCustomActionsAttribute.class, ]; } @@ -697,18 +908,104 @@ + (NSString *)name return kXMLIndexPathKey; } -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value +@end + +@implementation FBApplicationBundleIdAttribute : FBElementAttribute + ++ (NSString *)name { - if (nil == value) { - // Skip the attribute if the value equals to nil - return 0; - } - int rc = xmlTextWriterWriteAttribute(writer, - (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String], - (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]); - if (rc < 0) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc]; - } - return rc; + return @"bundleId"; +} + +@end + +@implementation FBApplicationPidAttribute : FBElementAttribute + ++ (NSString *)name +{ + return @"processId"; +} + +@end + +@implementation FBPlaceholderValueAttribute + ++ (NSString *)name +{ + return @"placeholderValue"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdPlaceholderValue; +} +@end + +@implementation FBNativeFrameAttribute + ++ (NSString *)name +{ + return @"nativeFrame"; +} + ++ (NSString *)valueForElement:(id)element +{ + return NSStringFromCGRect(element.wdNativeFrame); +} +@end + +@implementation FBTraitsAttribute + ++ (NSString *)name +{ + return @"traits"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdTraits; } + +@end + +@implementation FBMinValueAttribute + ++ (NSString *)name +{ + return @"minValue"; +} + ++ (NSString *)valueForElement:(id)element +{ + return [element.wdMinValue stringValue]; +} + +@end + +@implementation FBMaxValueAttribute + ++ (NSString *)name +{ + return @"maxValue"; +} + ++ (NSString *)valueForElement:(id)element +{ + return [element.wdMaxValue stringValue]; +} + +@end + +@implementation FBCustomActionsAttribute + ++ (NSString *)name +{ + return @"customActions"; +} + ++ (NSString *)valueForElement:(id)element +{ + return element.wdCustomActions; +} + @end diff --git a/WebDriverAgentLib/Utilities/FBXPathExtensions.h b/WebDriverAgentLib/Utilities/FBXPathExtensions.h new file mode 100644 index 000000000..11f95bbda --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXPathExtensions.h @@ -0,0 +1,40 @@ +/** + * 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 + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpadded" +#endif + +#import + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface FBXPathExtensions : NSObject + +/** + Registers XPath 2-compatible extension functions on the given libxml2 context. + */ +- (void)registerFunctionsWithContext:(xmlXPathContextPtr)xpathCtx; + +/** + Human-readable message for the most recent XPath extension evaluation failure on this instance, + for example an invalid regular expression pattern or flags. Nil when no extension error has occurred. + Scoped to the libxml2 context this instance is registered with; each evaluation should use its own instance. + */ +@property (nonatomic, nullable, readonly, copy) NSString *lastEvaluationError; + +@end + +NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/FBXPathExtensions.m b/WebDriverAgentLib/Utilities/FBXPathExtensions.m new file mode 100644 index 000000000..46cc42413 --- /dev/null +++ b/WebDriverAgentLib/Utilities/FBXPathExtensions.m @@ -0,0 +1,418 @@ +/** + * 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 "FBXPathExtensions.h" + +#import "FBLogger.h" + +#import + +static void FBRegisterXPathExtensions(xmlXPathContextPtr xpathCtx); + +static NSString *const FBXPathTokenSequenceSeparator = @"\x1E"; +static const NSRegularExpressionOptions FBXPathNoRegexOptions = (NSRegularExpressionOptions)0; +static const NSMatchingOptions FBXPathNoMatchingOptions = (NSMatchingOptions)0; + +@interface FBXPathExtensions () +@property (nonatomic, nullable, readwrite, copy) NSString *lastEvaluationError; +@end + +static FBXPathExtensions *FBXPathExtensionsFromParserContext(xmlXPathParserContextPtr ctxt) +{ + if (NULL == ctxt || NULL == ctxt->context || NULL == ctxt->context->userData) { + return nil; + } + return (__bridge FBXPathExtensions *)ctxt->context->userData; +} + +static void FBXPathSetEvaluationError(xmlXPathParserContextPtr ctxt, int xpathErrorCode, NSString *message) +{ + FBXPathExtensions *extensions = FBXPathExtensionsFromParserContext(ctxt); + extensions.lastEvaluationError = message; + [FBLogger logFmt:@"XPath extension evaluation error: %@", message]; + if (NULL == ctxt) { + return; + } + xmlXPatherror(ctxt, __FILE__, __LINE__, xpathErrorCode); + ctxt->error = xpathErrorCode; +} + +static void FBXPathSetInvalidArityError(xmlXPathParserContextPtr ctxt) +{ + if (NULL == ctxt) { + return; + } + xmlXPatherror(ctxt, __FILE__, __LINE__, XPATH_INVALID_ARITY); + ctxt->error = XPATH_INVALID_ARITY; +} + +static BOOL FBXPathFlagsAreValid(NSString *flags, BOOL allowsQFlag) +{ + if (nil == flags || 0 == flags.length) { + return YES; + } + + NSString *validFlags = allowsQFlag ? @"imsxq" : @"imsx"; + for (NSUInteger index = 0; index < flags.length; index++) { + unichar flag = [flags characterAtIndex:index]; + if ([validFlags rangeOfString:[NSString stringWithCharacters:&flag length:1]].location == NSNotFound) { + return NO; + } + } + return YES; +} + +static NSString *FBXPathStringFromUTF8Bytes(const xmlChar *bytes) +{ + if (NULL == bytes) { + return nil; + } + return [NSString stringWithUTF8String:(const char *)bytes]; +} + +@implementation FBXPathExtensions + +- (void)registerFunctionsWithContext:(xmlXPathContextPtr)xpathCtx +{ + xpathCtx->userData = (__bridge void *)self; + FBRegisterXPathExtensions(xpathCtx); +} + +@end + +static NSString *FBXPathPopNSString(xmlXPathParserContextPtr ctxt) +{ + xmlChar *value = xmlXPathPopString(ctxt); + if (NULL == value || xmlXPathCheckError(ctxt)) { + return nil; + } + NSString *result = [NSString stringWithUTF8String:(const char *)value]; + xmlFree(value); + return result; +} + +static NSRegularExpressionOptions FBXPathRegexOptionsFromFlags(NSString *flags) +{ + NSRegularExpressionOptions options = FBXPathNoRegexOptions; + if (nil != flags && [flags rangeOfString:@"i"].location != NSNotFound) { + options |= NSRegularExpressionCaseInsensitive; + } + return options; +} + +static NSRegularExpression *FBXPathRegexWithPattern(NSString *pattern, + NSString *flags, + BOOL allowsQFlag, + xmlXPathParserContextPtr ctxt) +{ + if (!FBXPathFlagsAreValid(flags, allowsQFlag)) { + FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, @"Invalid regular expression flags"); + return nil; + } + + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern + options:FBXPathRegexOptionsFromFlags(flags) + error:&error]; + if (nil == regex) { + NSString *message = error.localizedDescription ?: @"Invalid regular expression"; + FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, message); + return nil; + } + return regex; +} + +static BOOL FBXPathTokenizeString(NSString *input, + NSString *pattern, + xmlXPathParserContextPtr ctxt, + NSArray **outTokens) +{ + if (0 == input.length) { + *outTokens = @[]; + return YES; + } + + if (nil == pattern) { + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\S+" + options:FBXPathNoRegexOptions + error:nil]; + if (nil == regex) { + FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, @"Invalid regular expression"); + return NO; + } + NSMutableArray *tokens = [NSMutableArray array]; + [regex enumerateMatchesInString:input + options:FBXPathNoMatchingOptions + range:NSMakeRange(0, input.length) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + if (nil != result) { + [tokens addObject:[input substringWithRange:result.range]]; + } + }]; + *outTokens = tokens.copy; + return YES; + } + + if (0 == pattern.length) { + NSMutableArray *tokens = [NSMutableArray array]; + [input enumerateSubstringsInRange:NSMakeRange(0, input.length) + options:NSStringEnumerationByComposedCharacterSequences + usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { + if (substring.length > 0) { + [tokens addObject:substring]; + } + }]; + *outTokens = tokens.copy; + return YES; + } + + NSError *error = nil; + NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern + options:FBXPathNoRegexOptions + error:&error]; + if (nil == regex) { + NSString *message = error.localizedDescription ?: @"Invalid regular expression"; + FBXPathSetEvaluationError(ctxt, XPATH_EXPR_ERROR, message); + return NO; + } + + NSMutableArray *tokens = [NSMutableArray array]; + __block NSUInteger lastIndex = 0; + [regex enumerateMatchesInString:input + options:FBXPathNoMatchingOptions + range:NSMakeRange(0, input.length) + usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { + if (nil == result) { + return; + } + if (result.range.location > lastIndex) { + NSString *token = [input substringWithRange:NSMakeRange(lastIndex, result.range.location - lastIndex)]; + if (token.length > 0) { + [tokens addObject:token]; + } + } + lastIndex = NSMaxRange(result.range); + }]; + if (lastIndex < input.length) { + NSString *token = [input substringFromIndex:lastIndex]; + if (token.length > 0) { + [tokens addObject:token]; + } + } + *outTokens = tokens.copy; + return YES; +} + +static void FBXPathReturnNSString(xmlXPathParserContextPtr ctxt, NSString *value) +{ + if (nil == value) { + xmlXPathReturnEmptyString(ctxt); + return; + } + xmlChar *copiedValue = xmlStrdup((const xmlChar *)[value UTF8String]); + if (NULL == copiedValue) { + xmlXPathReturnEmptyString(ctxt); + return; + } + // xmlXPathWrapString takes ownership of the buffer passed to xmlXPathReturnString. + xmlXPathReturnString(ctxt, copiedValue); +} + +static NSArray *FBXPathPartsFromXPathObject(xmlXPathObjectPtr sequence) +{ + if (sequence->type == XPATH_NODESET && NULL != sequence->nodesetval) { + NSMutableArray *parts = [NSMutableArray array]; + for (int index = 0; index < sequence->nodesetval->nodeNr; index++) { + xmlChar *content = xmlNodeGetContent(sequence->nodesetval->nodeTab[index]); + if (NULL != content) { + NSString *part = FBXPathStringFromUTF8Bytes(content); + xmlFree(content); + if (nil != part) { + [parts addObject:part]; + } + } + } + return parts.copy; + } + + xmlChar *asString = xmlXPathCastToString(sequence); + if (NULL == asString) { + return @[]; + } + NSString *value = FBXPathStringFromUTF8Bytes(asString); + xmlFree(asString); + if (nil == value || 0 == value.length) { + return @[]; + } + if ([value rangeOfString:FBXPathTokenSequenceSeparator].location != NSNotFound) { + return [value componentsSeparatedByString:FBXPathTokenSequenceSeparator]; + } + return @[value]; +} + +static void FBXPathMatchesFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs < 2 || nargs > 3) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *flags = nargs == 3 ? FBXPathPopNSString(ctxt) : nil; + NSString *pattern = FBXPathPopNSString(ctxt); + NSString *input = FBXPathPopNSString(ctxt); + if (nil == pattern || nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + NSRegularExpression *regex = FBXPathRegexWithPattern(pattern, flags, NO, ctxt); + if (nil == regex) { + return; + } + + NSRange range = NSMakeRange(0, input.length); + NSTextCheckingResult *match = [regex firstMatchInString:input options:FBXPathNoMatchingOptions range:range]; + xmlXPathReturnBoolean(ctxt, nil != match); +} + +static void FBXPathEndsWithFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs != 2) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *suffix = FBXPathPopNSString(ctxt); + NSString *input = FBXPathPopNSString(ctxt); + if (nil == suffix || nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + xmlXPathReturnBoolean(ctxt, [input hasSuffix:suffix]); +} + +static void FBXPathLowerCaseFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs != 1) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *input = FBXPathPopNSString(ctxt); + if (nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + FBXPathReturnNSString(ctxt, input.lowercaseString); +} + +static void FBXPathUpperCaseFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs != 1) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *input = FBXPathPopNSString(ctxt); + if (nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + FBXPathReturnNSString(ctxt, input.uppercaseString); +} + +static void FBXPathReplaceFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs < 3 || nargs > 4) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *flags = nargs == 4 ? FBXPathPopNSString(ctxt) : nil; + NSString *replacement = FBXPathPopNSString(ctxt); + NSString *pattern = FBXPathPopNSString(ctxt); + NSString *input = FBXPathPopNSString(ctxt); + if (nil == replacement || nil == pattern || nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + NSRegularExpression *regex = FBXPathRegexWithPattern(pattern, flags, YES, ctxt); + if (nil == regex) { + return; + } + + NSRange range = NSMakeRange(0, input.length); + NSString *result = [regex stringByReplacingMatchesInString:input + options:FBXPathNoMatchingOptions + range:range + withTemplate:replacement]; + FBXPathReturnNSString(ctxt, result); +} + +static void FBXPathTokenizeFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs < 1 || nargs > 2) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + NSString *pattern = nargs == 2 ? FBXPathPopNSString(ctxt) : nil; + NSString *input = FBXPathPopNSString(ctxt); + if (nil == input || xmlXPathCheckError(ctxt)) { + return; + } + + NSArray *tokens = nil; + if (!FBXPathTokenizeString(input, pattern, ctxt, &tokens)) { + return; + } + + FBXPathReturnNSString(ctxt, [tokens componentsJoinedByString:FBXPathTokenSequenceSeparator]); +} + +static void FBXPathStringJoinFunction(xmlXPathParserContextPtr ctxt, int nargs) +{ + if (nargs != 2) { + FBXPathSetInvalidArityError(ctxt); + return; + } + + xmlChar *separatorChars = xmlXPathPopString(ctxt); + xmlXPathObjectPtr sequence = valuePop(ctxt); + if (xmlXPathCheckError(ctxt) || NULL == sequence || NULL == separatorChars) { + if (NULL != separatorChars) { + xmlFree(separatorChars); + } + if (NULL != sequence) { + xmlXPathFreeObject(sequence); + } + return; + } + + NSString *separator = FBXPathStringFromUTF8Bytes(separatorChars); + xmlFree(separatorChars); + if (nil == separator) { + xmlXPathFreeObject(sequence); + return; + } + + NSArray *parts = FBXPathPartsFromXPathObject(sequence); + xmlXPathFreeObject(sequence); + + FBXPathReturnNSString(ctxt, [parts componentsJoinedByString:separator]); +} + +static void FBRegisterXPathExtensions(xmlXPathContextPtr xpathCtx) +{ + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "matches", FBXPathMatchesFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "ends-with", FBXPathEndsWithFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "lower-case", FBXPathLowerCaseFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "upper-case", FBXPathUpperCaseFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "replace", FBXPathReplaceFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "tokenize", FBXPathTokenizeFunction); + xmlXPathRegisterFunc(xpathCtx, BAD_CAST "string-join", FBXPathStringJoinFunction); +} diff --git a/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h b/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h index 70614235f..dbfb4cfbc 100644 --- a/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h +++ b/WebDriverAgentLib/Utilities/LRUCache/LRUCache.h @@ -54,6 +54,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (NSArray *)allObjects; +/** + Removes the object associated with the specified key from the cache. + + @param key The key identifying the object to remove. + */ +- (void)removeObjectForKey:(id)key; + @end NS_ASSUME_NONNULL_END diff --git a/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m b/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m index 4f3a9ad0d..731d74404 100644 --- a/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m +++ b/WebDriverAgentLib/Utilities/LRUCache/LRUCache.m @@ -135,4 +135,12 @@ - (void)alignSize } } +- (void)removeObjectForKey:(id)key +{ + LRUCacheNode *node = self.store[key]; + if (node != nil) { + [self removeNode:node]; + } +} + @end diff --git a/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h index 89ce4adee..a7ad285d5 100644 --- a/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h +++ b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m index 8a033e707..65af951a0 100644 --- a/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m +++ b/WebDriverAgentLib/Utilities/NSPredicate+FBFormat.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "NSPredicate+FBFormat.h" @@ -59,8 +58,10 @@ + (instancetype)fb_snapshotBlockPredicateWithPredicate:(NSPredicate *)input NSPredicate *wdPredicate = [self.class fb_formatSearchPredicate:input]; return [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary * _Nullable bindings) { - FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:evaluatedObject]; - return [wdPredicate evaluateWithObject:wrappedSnapshot]; + @autoreleasepool { + FBXCElementSnapshotWrapper *wrappedSnapshot = [FBXCElementSnapshotWrapper ensureWrapped:evaluatedObject]; + return [wdPredicate evaluateWithObject:wrappedSnapshot]; + } }]; } diff --git a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h index 87199ff35..d6460b131 100644 --- a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h +++ b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -19,6 +18,21 @@ extern NSString *FB_XCAXAIsVisibleAttributeName; extern NSNumber *FB_XCAXAIsElementAttribute; extern NSString *FB_XCAXAIsElementAttributeName; +/*! Accessibility identifier for visible frame attribute */ +extern NSString *FB_XCAXAVisibleFrameAttributeName; + +/*! Accessibility identifier для минимума */ +extern NSNumber *FB_XCAXACustomMinValueAttribute; +extern NSString *FB_XCAXACustomMinValueAttributeName; + +/*! Accessibility identifier для максимума */ +extern NSNumber *FB_XCAXACustomMaxValueAttribute; +extern NSString *FB_XCAXACustomMaxValueAttributeName; + +/*! Accessibility identifier for custom actions attribute */ +extern NSNumber *FB_XCAXACustomActionsAttribute; +extern NSString *FB_XCAXACustomActionsAttributeName; + /*! Getter for XCTest logger */ extern id (*XCDebugLogger)(void); @@ -36,8 +50,8 @@ extern NSArray *(*XCAXAccessibilityAttributesForStringAttributes)(id */ void *FBRetrieveXCTestSymbol(const char *name); -/*! Static constructor that will retrieve XCTest private symbols */ -__attribute__((constructor)) void FBLoadXCTestSymbols(void); +/*! Loads XCTest private symbols. Safe to call multiple times. */ +void FBLoadXCTestSymbols(void); /** Method is used to tranform attribute names into the format, which diff --git a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m index 861a78660..60ac241a7 100644 --- a/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m +++ b/WebDriverAgentLib/Utilities/XCTestPrivateSymbols.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCTestPrivateSymbols.h" @@ -18,29 +17,74 @@ NSString *FB_XCAXAIsVisibleAttributeName = @"XC_kAXXCAttributeIsVisible"; NSNumber *FB_XCAXAIsElementAttribute; NSString *FB_XCAXAIsElementAttributeName = @"XC_kAXXCAttributeIsElement"; +NSString *FB_XCAXAVisibleFrameAttributeName = @"XC_kAXXCAttributeVisibleFrame"; +NSNumber *FB_XCAXACustomMinValueAttribute; +NSString *FB_XCAXACustomMinValueAttributeName = @"XC_kAXXCAttributeMinValue"; +NSNumber *FB_XCAXACustomMaxValueAttribute; +NSString *FB_XCAXACustomMaxValueAttributeName = @"XC_kAXXCAttributeMaxValue"; +NSNumber *FB_XCAXACustomActionsAttribute; +NSString *FB_XCAXACustomActionsAttributeName = @"XC_kAXXCAttributeCustomActions"; void (*XCSetDebugLogger)(id ); id (*XCDebugLogger)(void); NSArray *(*XCAXAccessibilityAttributesForStringAttributes)(id); -__attribute__((constructor)) void FBLoadXCTestSymbols(void) +@interface FBXCTestSymbolsLoader : NSObject +@end + +@implementation FBXCTestSymbolsLoader + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-load-method" + ++ (void)load { - NSString *XC_kAXXCAttributeIsVisible = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsVisibleAttributeName UTF8String]); - NSString *XC_kAXXCAttributeIsElement = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsElementAttributeName UTF8String]); + FBLoadXCTestSymbols(); +} - XCAXAccessibilityAttributesForStringAttributes = - (NSArray *(*)(id))FBRetrieveXCTestSymbol("XCAXAccessibilityAttributesForStringAttributes"); +#pragma clang diagnostic pop + +@end + +void FBLoadXCTestSymbols(void) +{ + static dispatch_once_t loadOnceToken; + dispatch_once(&loadOnceToken, ^{ + NSString *XC_kAXXCAttributeIsVisible = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsVisibleAttributeName UTF8String]); + NSString *XC_kAXXCAttributeIsElement = *(NSString*__autoreleasing*)FBRetrieveXCTestSymbol([FB_XCAXAIsElementAttributeName UTF8String]); - XCSetDebugLogger = (void (*)(id ))FBRetrieveXCTestSymbol("XCSetDebugLogger"); - XCDebugLogger = (id(*)(void))FBRetrieveXCTestSymbol("XCDebugLogger"); + XCAXAccessibilityAttributesForStringAttributes = + (NSArray *(*)(id))FBRetrieveXCTestSymbol("XCAXAccessibilityAttributesForStringAttributes"); - NSArray *accessibilityAttributes = XCAXAccessibilityAttributesForStringAttributes(@[XC_kAXXCAttributeIsVisible, XC_kAXXCAttributeIsElement]); - FB_XCAXAIsVisibleAttribute = accessibilityAttributes[0]; - FB_XCAXAIsElementAttribute = accessibilityAttributes[1]; + XCSetDebugLogger = (void (*)(id ))FBRetrieveXCTestSymbol("XCSetDebugLogger"); + XCDebugLogger = (id(*)(void))FBRetrieveXCTestSymbol("XCDebugLogger"); - NSCAssert(FB_XCAXAIsVisibleAttribute != nil , @"Failed to retrieve FB_XCAXAIsVisibleAttribute", FB_XCAXAIsVisibleAttribute); - NSCAssert(FB_XCAXAIsElementAttribute != nil , @"Failed to retrieve FB_XCAXAIsElementAttribute", FB_XCAXAIsElementAttribute); + NSArray *accessibilityAttributes = XCAXAccessibilityAttributesForStringAttributes(@[XC_kAXXCAttributeIsVisible, XC_kAXXCAttributeIsElement]); + FB_XCAXAIsVisibleAttribute = accessibilityAttributes[0]; + FB_XCAXAIsElementAttribute = accessibilityAttributes[1]; + + NSCAssert(FB_XCAXAIsVisibleAttribute != nil , @"Failed to retrieve FB_XCAXAIsVisibleAttribute", FB_XCAXAIsVisibleAttribute); + NSCAssert(FB_XCAXAIsElementAttribute != nil , @"Failed to retrieve FB_XCAXAIsElementAttribute", FB_XCAXAIsElementAttribute); + + NSString *XC_kAXXCAttributeMinValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMinValueAttributeName UTF8String]); + NSString *XC_kAXXCAttributeMaxValue = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomMaxValueAttributeName UTF8String]); + + NSString *XC_kAXXCAttributeCustomActions = *(NSString *__autoreleasing *)FBRetrieveXCTestSymbol([FB_XCAXACustomActionsAttributeName UTF8String]); + + NSArray *customAttrs = XCAXAccessibilityAttributesForStringAttributes(@[ + XC_kAXXCAttributeMinValue, + XC_kAXXCAttributeMaxValue, + XC_kAXXCAttributeCustomActions + ]); + FB_XCAXACustomMinValueAttribute = customAttrs[0]; + FB_XCAXACustomMaxValueAttribute = customAttrs[1]; + FB_XCAXACustomActionsAttribute = customAttrs[2]; + + NSCAssert(FB_XCAXACustomMinValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMinValueAttribute", FB_XCAXACustomMinValueAttribute); + NSCAssert(FB_XCAXACustomMaxValueAttribute != nil, @"Failed to retrieve FB_XCAXACustomMaxValueAttribute", FB_XCAXACustomMaxValueAttribute); + NSCAssert(FB_XCAXACustomActionsAttribute != nil, @"Failed to retrieve FB_XCAXACustomActionsAttribute", FB_XCAXACustomActionsAttribute); + }); } void *FBRetrieveXCTestSymbol(const char *name) @@ -73,7 +117,10 @@ dispatch_once(&onceCustomAttributeNamesToken, ^{ customNames = @[ FB_XCAXAIsVisibleAttributeName, - FB_XCAXAIsElementAttributeName + FB_XCAXAIsElementAttributeName, + FB_XCAXACustomMinValueAttributeName, + FB_XCAXACustomMaxValueAttributeName, + FB_XCAXACustomActionsAttributeName ]; }); return customNames; diff --git a/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h b/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h index ed11a9a51..3cbb5efb0 100644 --- a/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h +++ b/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m b/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m index c50ca4138..e41a0a78c 100644 --- a/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m +++ b/WebDriverAgentLib/Utilities/XCUIApplicationProcessDelay.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "XCUIApplicationProcessDelay.h" @@ -61,7 +60,10 @@ + (void)swizzleSetEventLoopHasIdled { [FBLogger log:@"Could not find method -[XCUIApplicationProcess setEventLoopHasIdled:]"]; return; } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wcast-function-type-strict" orig_set_event_loop_has_idled = (void(*)(id, SEL, BOOL)) method_getImplementation(original); +#pragma clang diagnostic pop Method replace = class_getClassMethod([XCUIApplicationProcessDelay class], @selector(setEventLoopHasIdled:)); method_setImplementation(original, method_getImplementation(replace)); isSwizzled = YES; diff --git a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m index de18142fe..a30d261b4 100755 --- a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m +++ b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncSocket.m @@ -1,6 +1,6 @@ // // GCDAsyncSocket.m -// +// // This class is in the public domain. // Originally created by Robbie Hanson in Q4 2010. // Updated and maintained by Deusty LLC and the Apple development community. @@ -11,7 +11,17 @@ #import "GCDAsyncSocket.h" #if TARGET_OS_IPHONE -#import +#import +#import +// Note: CFStream APIs are still used for TLS support and are part of CoreFoundation +// CFStream SSL constants are needed only for the legacy CFStream TLS path (when GCDAsyncSocketUseCFStreamForTLS is set) +// The default path uses SecureTransport which doesn't require these constants +// Declare SSL constants as extern to avoid importing deprecated CFNetwork framework +// These symbols are linked at runtime from CFNetwork framework +extern const CFStringRef kCFStreamPropertySSLSettings; +extern const CFStringRef kCFStreamSSLPeerName; +extern const CFStringRef kCFStreamSSLCertificates; +extern const CFStringRef kCFStreamSSLIsServer; #endif #import @@ -48,7 +58,7 @@ // Logging uses the CocoaLumberjack framework (which is also GCD based). // https://github.com/robbiehanson/CocoaLumberjack -// +// // It allows us to do a lot of logging without significantly slowing down the code. #import "DDLog.h" @@ -82,18 +92,18 @@ // Logging Disabled -#define LogError(frmt, ...) {} -#define LogWarn(frmt, ...) {} -#define LogInfo(frmt, ...) {} -#define LogVerbose(frmt, ...) {} +#define LogError(frmt, ...) do {} while (0) +#define LogWarn(frmt, ...) do {} while (0) +#define LogInfo(frmt, ...) do {} while (0) +#define LogVerbose(frmt, ...) do {} while (0) -#define LogCError(frmt, ...) {} -#define LogCWarn(frmt, ...) {} -#define LogCInfo(frmt, ...) {} -#define LogCVerbose(frmt, ...) {} +#define LogCError(frmt, ...) do {} while (0) +#define LogCWarn(frmt, ...) do {} while (0) +#define LogCInfo(frmt, ...) do {} while (0) +#define LogCVerbose(frmt, ...) do {} while (0) -#define LogTrace() {} -#define LogCTrace(frmt, ...) {} +#define LogTrace() do {} while (0) +#define LogCTrace(frmt, ...) do {} while (0) #endif @@ -101,14 +111,14 @@ * Seeing a return statements within an inner block * can sometimes be mistaken for a return point of the enclosing method. * This makes inline blocks a bit easier to read. -**/ + **/ #define return_from_block return /** * A socket file descriptor is really just an integer. * It represents the index of the socket within the kernel. * This makes invalid file descriptor comparisons easier to read. -**/ + **/ #define SOCKET_NULL -1 @@ -135,44 +145,44 @@ enum GCDAsyncSocketFlags { - kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) - kConnected = 1 << 1, // If set, the socket is connected - kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed - kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout - kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout - kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued - kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued - kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. - kReadSourceSuspended = 1 << 8, // If set, the read source is suspended - kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended - kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS - kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete - kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete - kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS - kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket - kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained - kDealloc = 1 << 16, // If set, the socket is being deallocated + kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) + kConnected = 1 << 1, // If set, the socket is connected + kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed + kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout + kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout + kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued + kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. + kReadSourceSuspended = 1 << 8, // If set, the read source is suspended + kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended + kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS + kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete + kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS + kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket + kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained + kDealloc = 1 << 16, // If set, the socket is being deallocated #if TARGET_OS_IPHONE - kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread - kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport - kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available + kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread + kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport + kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available #endif }; enum GCDAsyncSocketConfig { - kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled - kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled - kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 - kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 + kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes }; #if TARGET_OS_IPHONE - static NSThread *cfstreamThread; // Used for CFStreams +static NSThread *cfstreamThread; // Used for CFStreams - static uint64_t cfstreamThreadRetainCount; // setup & teardown - static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown +static uint64_t cfstreamThreadRetainCount; // setup & teardown +static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -184,26 +194,26 @@ * than is being requested by current read request. * In this case we slurp up all data from the socket (to minimize sys calls), * and store additional yet unread data in a "prebuffer". - * + * * The prebuffer is entirely drained before we read from the socket again. * In other words, a large chunk of data is written is written to the prebuffer. * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). - * + * * A ring buffer was once used for this purpose. * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. - * + * * The current design is very simple and straight-forward, while also keeping memory requirements lower. -**/ + **/ @interface GCDAsyncSocketPreBuffer : NSObject { - uint8_t *preBuffer; - size_t preBufferSize; - - uint8_t *readPointer; - uint8_t *writePointer; + uint8_t *preBuffer; + size_t preBufferSize; + + uint8_t *readPointer; + uint8_t *writePointer; } - (instancetype)initWithCapacity:(size_t)numBytes NS_DESIGNATED_INITIALIZER; @@ -232,104 +242,104 @@ @implementation GCDAsyncSocketPreBuffer // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { - NSAssert(0, @"Use the designated initializer"); - return nil; + NSAssert(0, @"Use the designated initializer"); + return nil; } - (instancetype)initWithCapacity:(size_t)numBytes { - if ((self = [super init])) - { - preBufferSize = numBytes; - preBuffer = malloc(preBufferSize); - - readPointer = preBuffer; - writePointer = preBuffer; - } - return self; + if ((self = [super init])) + { + preBufferSize = numBytes; + preBuffer = (uint8_t *)malloc(preBufferSize); + + readPointer = preBuffer; + writePointer = preBuffer; + } + return self; } - (void)dealloc { - if (preBuffer) - free(preBuffer); + if (preBuffer) + free(preBuffer); } - (void)ensureCapacityForWrite:(size_t)numBytes { - size_t availableSpace = [self availableSpace]; - - if (numBytes > availableSpace) - { - size_t additionalBytes = numBytes - availableSpace; - - size_t newPreBufferSize = preBufferSize + additionalBytes; - uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); - - size_t readPointerOffset = readPointer - preBuffer; - size_t writePointerOffset = writePointer - preBuffer; - - preBuffer = newPreBuffer; - preBufferSize = newPreBufferSize; - - readPointer = preBuffer + readPointerOffset; - writePointer = preBuffer + writePointerOffset; - } + size_t availableSpace = [self availableSpace]; + + if (numBytes > availableSpace) + { + size_t additionalBytes = numBytes - availableSpace; + + size_t newPreBufferSize = preBufferSize + additionalBytes; + uint8_t *newPreBuffer = (uint8_t *)realloc(preBuffer, newPreBufferSize); + + size_t readPointerOffset = readPointer - preBuffer; + size_t writePointerOffset = writePointer - preBuffer; + + preBuffer = newPreBuffer; + preBufferSize = newPreBufferSize; + + readPointer = preBuffer + readPointerOffset; + writePointer = preBuffer + writePointerOffset; + } } - (size_t)availableBytes { - return writePointer - readPointer; + return writePointer - readPointer; } - (uint8_t *)readBuffer { - return readPointer; + return readPointer; } - (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr { - if (bufferPtr) *bufferPtr = readPointer; - if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; + if (bufferPtr) *bufferPtr = readPointer; + if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; } - (void)didRead:(size_t)bytesRead { - readPointer += bytesRead; - - if (readPointer == writePointer) - { - // The prebuffer has been drained. Reset pointers. - readPointer = preBuffer; - writePointer = preBuffer; - } + readPointer += bytesRead; + + if (readPointer == writePointer) + { + // The prebuffer has been drained. Reset pointers. + readPointer = preBuffer; + writePointer = preBuffer; + } } - (size_t)availableSpace { - return preBufferSize - (writePointer - preBuffer); + return preBufferSize - (writePointer - preBuffer); } - (uint8_t *)writeBuffer { - return writePointer; + return writePointer; } - (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr { - if (bufferPtr) *bufferPtr = writePointer; - if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; + if (bufferPtr) *bufferPtr = writePointer; + if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; } - (void)didWrite:(size_t)bytesWritten { - writePointer += bytesWritten; + writePointer += bytesWritten; } - (void)reset { - readPointer = preBuffer; - writePointer = preBuffer; + readPointer = preBuffer; + writePointer = preBuffer; } @end @@ -344,20 +354,20 @@ - (void)reset * - reading to a certain length * - reading to a certain separator * - or simply reading the first chunk of available data -**/ + **/ @interface GCDAsyncReadPacket : NSObject { - @public - NSMutableData *buffer; - NSUInteger startOffset; - NSUInteger bytesDone; - NSUInteger maxLength; - NSTimeInterval timeout; - NSUInteger readLength; - NSData *term; - BOOL bufferOwner; - NSUInteger originalBufferLength; - long tag; +@public + NSMutableData *buffer; + NSUInteger startOffset; + NSUInteger bytesDone; + NSUInteger maxLength; + NSTimeInterval timeout; + NSUInteger readLength; + NSData *term; + BOOL bufferOwner; + NSUInteger originalBufferLength; + long tag; } - (instancetype)initWithData:(NSMutableData *)d startOffset:(NSUInteger)s @@ -384,8 +394,8 @@ @implementation GCDAsyncReadPacket // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { - NSAssert(0, @"Use the designated initializer"); - return nil; + NSAssert(0, @"Use the designated initializer"); + return nil; } - (instancetype)initWithData:(NSMutableData *)d @@ -396,418 +406,418 @@ - (instancetype)initWithData:(NSMutableData *)d terminator:(NSData *)e tag:(long)i { - if((self = [super init])) - { - bytesDone = 0; - maxLength = m; - timeout = t; - readLength = l; - term = [e copy]; - tag = i; - - if (d) - { - buffer = d; - startOffset = s; - bufferOwner = NO; - originalBufferLength = [d length]; - } - else - { - if (readLength > 0) - buffer = [[NSMutableData alloc] initWithLength:readLength]; - else - buffer = [[NSMutableData alloc] initWithLength:0]; - - startOffset = 0; - bufferOwner = YES; - originalBufferLength = 0; - } - } - return self; + if((self = [super init])) + { + bytesDone = 0; + maxLength = m; + timeout = t; + readLength = l; + term = [e copy]; + tag = i; + + if (d) + { + buffer = d; + startOffset = s; + bufferOwner = NO; + originalBufferLength = [d length]; + } + else + { + if (readLength > 0) + buffer = [[NSMutableData alloc] initWithLength:readLength]; + else + buffer = [[NSMutableData alloc] initWithLength:0]; + + startOffset = 0; + bufferOwner = YES; + originalBufferLength = 0; + } + } + return self; } /** * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. -**/ + **/ - (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead { - NSUInteger buffSize = [buffer length]; - NSUInteger buffUsed = startOffset + bytesDone; - - NSUInteger buffSpace = buffSize - buffUsed; - - if (bytesToRead > buffSpace) - { - NSUInteger buffInc = bytesToRead - buffSpace; - - [buffer increaseLengthBy:buffInc]; - } + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (bytesToRead > buffSpace) + { + NSUInteger buffInc = bytesToRead - buffSpace; + + [buffer increaseLengthBy:buffInc]; + } } /** * This method is used when we do NOT know how much data is available to be read from the socket. * This method returns the default value unless it exceeds the specified readLength or maxLength. - * + * * Furthermore, the shouldPreBuffer decision is based upon the packet type, * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. -**/ + **/ - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr { - NSUInteger result; - - if (readLength > 0) - { - // Read a specific length of data - result = readLength - bytesDone; - - // There is no need to prebuffer since we know exactly how much data we need to read. - // Even if the buffer isn't currently big enough to fit this amount of data, - // it would have to be resized eventually anyway. - - if (shouldPreBufferPtr) - *shouldPreBufferPtr = NO; - } - else - { - // Either reading until we find a specified terminator, - // or we're simply reading all available data. - // - // In other words, one of: - // - // - readDataToData packet - // - readDataWithTimeout packet - - if (maxLength > 0) - result = MIN(defaultValue, (maxLength - bytesDone)); - else - result = defaultValue; - - // Since we don't know the size of the read in advance, - // the shouldPreBuffer decision is based upon whether the returned value would fit - // in the current buffer without requiring a resize of the buffer. - // - // This is because, in all likelyhood, the amount read from the socket will be less than the default value. - // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. - - if (shouldPreBufferPtr) - { - NSUInteger buffSize = [buffer length]; - NSUInteger buffUsed = startOffset + bytesDone; - - NSUInteger buffSpace = buffSize - buffUsed; - - if (buffSpace >= result) - *shouldPreBufferPtr = NO; - else - *shouldPreBufferPtr = YES; - } - } - - return result; + NSUInteger result; + + if (readLength > 0) + { + // Read a specific length of data + result = readLength - bytesDone; + + // There is no need to prebuffer since we know exactly how much data we need to read. + // Even if the buffer isn't currently big enough to fit this amount of data, + // it would have to be resized eventually anyway. + + if (shouldPreBufferPtr) + *shouldPreBufferPtr = NO; + } + else + { + // Either reading until we find a specified terminator, + // or we're simply reading all available data. + // + // In other words, one of: + // + // - readDataToData packet + // - readDataWithTimeout packet + + if (maxLength > 0) + result = MIN(defaultValue, (maxLength - bytesDone)); + else + result = defaultValue; + + // Since we don't know the size of the read in advance, + // the shouldPreBuffer decision is based upon whether the returned value would fit + // in the current buffer without requiring a resize of the buffer. + // + // This is because, in all likelyhood, the amount read from the socket will be less than the default value. + // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (buffSpace >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + } + + return result; } /** * For read packets without a set terminator, returns the amount of data * that can be read without exceeding the readLength or maxLength. - * + * * The given parameter indicates the number of bytes estimated to be available on the socket, * which is taken into consideration during the calculation. - * + * * The given hint MUST be greater than zero. -**/ + **/ - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable { - NSAssert(term == nil, @"This method does not apply to term reads"); - NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); - - if (readLength > 0) - { - // Read a specific length of data - - return MIN(bytesAvailable, (readLength - bytesDone)); - - // No need to avoid resizing the buffer. - // If the user provided their own buffer, - // and told us to read a certain length of data that exceeds the size of the buffer, - // then it is clear that our code will resize the buffer during the read operation. - // - // This method does not actually do any resizing. - // The resizing will happen elsewhere if needed. - } - else - { - // Read all available data - - NSUInteger result = bytesAvailable; - - if (maxLength > 0) - { - result = MIN(result, (maxLength - bytesDone)); - } - - // No need to avoid resizing the buffer. - // If the user provided their own buffer, - // and told us to read all available data without giving us a maxLength, - // then it is clear that our code might resize the buffer during the read operation. - // - // This method does not actually do any resizing. - // The resizing will happen elsewhere if needed. - - return result; - } + NSAssert(term == nil, @"This method does not apply to term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + if (readLength > 0) + { + // Read a specific length of data + + return MIN(bytesAvailable, (readLength - bytesDone)); + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read a certain length of data that exceeds the size of the buffer, + // then it is clear that our code will resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + } + else + { + // Read all available data + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read all available data without giving us a maxLength, + // then it is clear that our code might resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + + return result; + } } /** * For read packets with a set terminator, returns the amount of data * that can be read without exceeding the maxLength. - * + * * The given parameter indicates the number of bytes estimated to be available on the socket, * which is taken into consideration during the calculation. - * + * * To optimize memory allocations, mem copies, and mem moves * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, * or if the data can be read directly into the read packet's buffer. -**/ + **/ - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr { - NSAssert(term != nil, @"This method does not apply to non-term reads"); - NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); - - - NSUInteger result = bytesAvailable; - - if (maxLength > 0) - { - result = MIN(result, (maxLength - bytesDone)); - } - - // Should the data be read into the read packet's buffer, or into a pre-buffer first? - // - // One would imagine the preferred option is the faster one. - // So which one is faster? - // - // Reading directly into the packet's buffer requires: - // 1. Possibly resizing packet buffer (malloc/realloc) - // 2. Filling buffer (read) - // 3. Searching for term (memcmp) - // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) - // - // Reading into prebuffer first: - // 1. Possibly resizing prebuffer (malloc/realloc) - // 2. Filling buffer (read) - // 3. Searching for term (memcmp) - // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) - // 5. Removing underflow from prebuffer (memmove) - // - // Comparing the performance of the two we can see that reading - // data into the prebuffer first is slower due to the extra memove. - // - // However: - // The implementation of NSMutableData is open source via core foundation's CFMutableData. - // Decreasing the length of a mutable data object doesn't cause a realloc. - // In other words, the capacity of a mutable data object can grow, but doesn't shrink. - // - // This means the prebuffer will rarely need a realloc. - // The packet buffer, on the other hand, may often need a realloc. - // This is especially true if we are the buffer owner. - // Furthermore, if we are constantly realloc'ing the packet buffer, - // and then moving the overflow into the prebuffer, - // then we're consistently over-allocating memory for each term read. - // And now we get into a bit of a tradeoff between speed and memory utilization. - // - // The end result is that the two perform very similarly. - // And we can answer the original question very simply by another means. - // - // If we can read all the data directly into the packet's buffer without resizing it first, - // then we do so. Otherwise we use the prebuffer. - - if (shouldPreBufferPtr) - { - NSUInteger buffSize = [buffer length]; - NSUInteger buffUsed = startOffset + bytesDone; - - if ((buffSize - buffUsed) >= result) - *shouldPreBufferPtr = NO; - else - *shouldPreBufferPtr = YES; - } - - return result; + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // Should the data be read into the read packet's buffer, or into a pre-buffer first? + // + // One would imagine the preferred option is the faster one. + // So which one is faster? + // + // Reading directly into the packet's buffer requires: + // 1. Possibly resizing packet buffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) + // + // Reading into prebuffer first: + // 1. Possibly resizing prebuffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) + // 5. Removing underflow from prebuffer (memmove) + // + // Comparing the performance of the two we can see that reading + // data into the prebuffer first is slower due to the extra memove. + // + // However: + // The implementation of NSMutableData is open source via core foundation's CFMutableData. + // Decreasing the length of a mutable data object doesn't cause a realloc. + // In other words, the capacity of a mutable data object can grow, but doesn't shrink. + // + // This means the prebuffer will rarely need a realloc. + // The packet buffer, on the other hand, may often need a realloc. + // This is especially true if we are the buffer owner. + // Furthermore, if we are constantly realloc'ing the packet buffer, + // and then moving the overflow into the prebuffer, + // then we're consistently over-allocating memory for each term read. + // And now we get into a bit of a tradeoff between speed and memory utilization. + // + // The end result is that the two perform very similarly. + // And we can answer the original question very simply by another means. + // + // If we can read all the data directly into the packet's buffer without resizing it first, + // then we do so. Otherwise we use the prebuffer. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + if ((buffSize - buffUsed) >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + + return result; } /** * For read packets with a set terminator, * returns the amount of data that can be read from the given preBuffer, * without going over a terminator or the maxLength. - * + * * It is assumed the terminator has not already been read. -**/ + **/ - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr { - NSAssert(term != nil, @"This method does not apply to non-term reads"); - NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); - - // We know that the terminator, as a whole, doesn't exist in our own buffer. - // But it is possible that a _portion_ of it exists in our buffer. - // So we're going to look for the terminator starting with a portion of our own buffer. - // - // Example: - // - // term length = 3 bytes - // bytesDone = 5 bytes - // preBuffer length = 5 bytes - // - // If we append the preBuffer to our buffer, - // it would look like this: - // - // --------------------- - // |B|B|B|B|B|P|P|P|P|P| - // --------------------- - // - // So we start our search here: - // - // --------------------- - // |B|B|B|B|B|P|P|P|P|P| - // -------^-^-^--------- - // - // And move forwards... - // - // --------------------- - // |B|B|B|B|B|P|P|P|P|P| - // ---------^-^-^------- - // - // Until we find the terminator or reach the end. - // - // --------------------- - // |B|B|B|B|B|P|P|P|P|P| - // ---------------^-^-^- - - BOOL found = NO; - - NSUInteger termLength = [term length]; - NSUInteger preBufferLength = [preBuffer availableBytes]; - - if ((bytesDone + preBufferLength) < termLength) - { - // Not enough data for a full term sequence yet - return preBufferLength; - } - - NSUInteger maxPreBufferLength; - if (maxLength > 0) { - maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); - - // Note: maxLength >= termLength - } - else { - maxPreBufferLength = preBufferLength; - } - + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); + + // We know that the terminator, as a whole, doesn't exist in our own buffer. + // But it is possible that a _portion_ of it exists in our buffer. + // So we're going to look for the terminator starting with a portion of our own buffer. + // + // Example: + // + // term length = 3 bytes + // bytesDone = 5 bytes + // preBuffer length = 5 bytes + // + // If we append the preBuffer to our buffer, + // it would look like this: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // --------------------- + // + // So we start our search here: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // -------^-^-^--------- + // + // And move forwards... + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------^-^-^------- + // + // Until we find the terminator or reach the end. + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------------^-^-^- + + BOOL found = NO; + + NSUInteger termLength = [term length]; + NSUInteger preBufferLength = [preBuffer availableBytes]; + + if ((bytesDone + preBufferLength) < termLength) + { + // Not enough data for a full term sequence yet + return preBufferLength; + } + + NSUInteger maxPreBufferLength; + if (maxLength > 0) { + maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); + + // Note: maxLength >= termLength + } + else { + maxPreBufferLength = preBufferLength; + } + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wvla" - uint8_t seq[termLength]; + uint8_t seq[termLength]; #pragma clang diagnostic pop - const void *termBuf = [term bytes]; - - NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); - uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; - - NSUInteger preLen = termLength - bufLen; - const uint8_t *pre = [preBuffer readBuffer]; - - NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. - - NSUInteger result = maxPreBufferLength; - - NSUInteger i; - for (i = 0; i < loopCount; i++) - { - if (bufLen > 0) - { - // Combining bytes from buffer and preBuffer - - memcpy(seq, buf, bufLen); - memcpy(seq + bufLen, pre, preLen); - - if (memcmp(seq, termBuf, termLength) == 0) - { - result = preLen; - found = YES; - break; - } - - buf++; - bufLen--; - preLen++; - } - else - { - // Comparing directly from preBuffer - - if (memcmp(pre, termBuf, termLength) == 0) - { - NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic - - result = preOffset + termLength; - found = YES; - break; - } - - pre++; - } - } - - // There is no need to avoid resizing the buffer in this particular situation. - - if (foundPtr) *foundPtr = found; - return result; + const void *termBuf = [term bytes]; + + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); + uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; + + NSUInteger preLen = termLength - bufLen; + const uint8_t *pre = [preBuffer readBuffer]; + + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. + + NSUInteger result = maxPreBufferLength; + + NSUInteger i; + for (i = 0; i < loopCount; i++) + { + if (bufLen > 0) + { + // Combining bytes from buffer and preBuffer + + memcpy(seq, buf, bufLen); + memcpy(seq + bufLen, pre, preLen); + + if (memcmp(seq, termBuf, termLength) == 0) + { + result = preLen; + found = YES; + break; + } + + buf++; + bufLen--; + preLen++; + } + else + { + // Comparing directly from preBuffer + + if (memcmp(pre, termBuf, termLength) == 0) + { + NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic + + result = preOffset + termLength; + found = YES; + break; + } + + pre++; + } + } + + // There is no need to avoid resizing the buffer in this particular situation. + + if (foundPtr) *foundPtr = found; + return result; } /** * For read packets with a set terminator, scans the packet buffer for the term. * It is assumed the terminator had not been fully read prior to the new bytes. - * + * * If the term is found, the number of excess bytes after the term are returned. * If the term is not found, this method will return -1. - * + * * Note: A return value of zero means the term was found at the very end. - * + * * Prerequisites: * The given number of bytes have been added to the end of our buffer. * Our bytesDone variable has NOT been changed due to the prebuffered bytes. -**/ + **/ - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes { - NSAssert(term != nil, @"This method does not apply to non-term reads"); - - // The implementation of this method is very similar to the above method. - // See the above method for a discussion of the algorithm used here. - - uint8_t *buff = [buffer mutableBytes]; - NSUInteger buffLength = bytesDone + numBytes; - - const void *termBuff = [term bytes]; - NSUInteger termLength = [term length]; - - // Note: We are dealing with unsigned integers, - // so make sure the math doesn't go below zero. - - NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; - - while (i + termLength <= buffLength) - { - uint8_t *subBuffer = buff + startOffset + i; - - if (memcmp(subBuffer, termBuff, termLength) == 0) - { - return buffLength - (i + termLength); - } - - i++; - } - - return -1; + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + // The implementation of this method is very similar to the above method. + // See the above method for a discussion of the algorithm used here. + + uint8_t *buff = (uint8_t *)[buffer mutableBytes]; + NSUInteger buffLength = bytesDone + numBytes; + + const void *termBuff = [term bytes]; + NSUInteger termLength = [term length]; + + // Note: We are dealing with unsigned integers, + // so make sure the math doesn't go below zero. + + NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; + + while (i + termLength <= buffLength) + { + uint8_t *subBuffer = buff + startOffset + i; + + if (memcmp(subBuffer, termBuff, termLength) == 0) + { + return buffLength - (i + termLength); + } + + i++; + } + + return -1; } @@ -819,14 +829,14 @@ - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes /** * The GCDAsyncWritePacket encompasses the instructions for any given write. -**/ + **/ @interface GCDAsyncWritePacket : NSObject { - @public - NSData *buffer; - NSUInteger bytesDone; - long tag; - NSTimeInterval timeout; +@public + NSData *buffer; + NSUInteger bytesDone; + long tag; + NSTimeInterval timeout; } - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i NS_DESIGNATED_INITIALIZER; @end @@ -836,20 +846,20 @@ @implementation GCDAsyncWritePacket // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { - NSAssert(0, @"Use the designated initializer"); - return nil; + NSAssert(0, @"Use the designated initializer"); + return nil; } - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i { - if((self = [super init])) - { - buffer = d; // Retain not copy. For performance as documented in header file. - bytesDone = 0; - timeout = t; - tag = i; - } - return self; + if((self = [super init])) + { + buffer = d; // Retain not copy. For performance as documented in header file. + bytesDone = 0; + timeout = t; + tag = i; + } + return self; } @@ -862,13 +872,14 @@ - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i /** * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. * This class my be altered to support more than just TLS in the future. -**/ + **/ @interface GCDAsyncSpecialPacket : NSObject { - @public - NSDictionary *tlsSettings; +@public + NSDictionary *tlsSettings; } -- (instancetype)initWithTLSSettings:(NSDictionary *)settings NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithTLSSettings:(NSDictionary *)settings NS_DESIGNATED_INITIALIZER; @end @implementation GCDAsyncSpecialPacket @@ -876,17 +887,18 @@ @implementation GCDAsyncSpecialPacket // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { - NSAssert(0, @"Use the designated initializer"); - return nil; + NSAssert(0, @"Use the designated initializer"); + return nil; } -- (instancetype)initWithTLSSettings:(NSDictionary *)settings +- (instancetype)initWithTLSSettings:(NSDictionary *)settings { - if((self = [super init])) - { - tlsSettings = [settings copy]; - } - return self; + if((self = [super init])) + { + tlsSettings = [settings copy]; + } + return self; } @@ -898,176 +910,181 @@ - (instancetype)initWithTLSSettings:(NSDictionary *)setting @implementation GCDAsyncSocket { - uint32_t flags; - uint16_t config; - - __weak id delegate; - dispatch_queue_t delegateQueue; - - int socket4FD; - int socket6FD; - int socketUN; - NSURL *socketUrl; - int stateIndex; - NSData * connectInterface4; - NSData * connectInterface6; - NSData * connectInterfaceUN; - - dispatch_queue_t socketQueue; - - dispatch_source_t accept4Source; - dispatch_source_t accept6Source; - dispatch_source_t acceptUNSource; - dispatch_source_t connectTimer; - dispatch_source_t readSource; - dispatch_source_t writeSource; - dispatch_source_t readTimer; - dispatch_source_t writeTimer; - - NSMutableArray *readQueue; - NSMutableArray *writeQueue; - - GCDAsyncReadPacket *currentRead; - GCDAsyncWritePacket *currentWrite; - - unsigned long socketFDBytesAvailable; - - GCDAsyncSocketPreBuffer *preBuffer; - + uint32_t flags; + uint16_t config; + + __weak id delegate; + dispatch_queue_t delegateQueue; + + int socket4FD; + int socket6FD; + int socketUN; + NSURL *socketUrl; + int stateIndex; + NSData * connectInterface4; + NSData * connectInterface6; + NSData * connectInterfaceUN; + + dispatch_queue_t socketQueue; + + dispatch_source_t accept4Source; + dispatch_source_t accept6Source; + dispatch_source_t acceptUNSource; + dispatch_source_t connectTimer; + dispatch_source_t readSource; + dispatch_source_t writeSource; + dispatch_source_t readTimer; + dispatch_source_t writeTimer; + + NSMutableArray *readQueue; + NSMutableArray *writeQueue; + + GCDAsyncReadPacket *currentRead; + GCDAsyncWritePacket *currentWrite; + + unsigned long socketFDBytesAvailable; + + GCDAsyncSocketPreBuffer *preBuffer; + #if TARGET_OS_IPHONE - CFStreamClientContext streamContext; - CFReadStreamRef readStream; - CFWriteStreamRef writeStream; + CFStreamClientContext streamContext; + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; #endif - SSLContextRef sslContext; - GCDAsyncSocketPreBuffer *sslPreBuffer; - size_t sslWriteCachedLength; - OSStatus sslErrCode; - OSStatus lastSSLHandshakeError; - - void *IsOnSocketQueueOrTargetQueueKey; - - id userData; - NSTimeInterval alternateAddressDelay; + SSLContextRef sslContext; + GCDAsyncSocketPreBuffer *sslPreBuffer; + size_t sslWriteCachedLength; + OSStatus sslErrCode; + OSStatus lastSSLHandshakeError; + + void *IsOnSocketQueueOrTargetQueueKey; + + id userData; + NSTimeInterval alternateAddressDelay; } - (instancetype)init { - return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; } - (instancetype)initWithSocketQueue:(dispatch_queue_t)sq { - return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq { - return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq { - if((self = [super init])) - { - delegate = aDelegate; - delegateQueue = dq; - - #if !OS_OBJECT_USE_OBJC - if (dq) dispatch_retain(dq); - #endif - - socket4FD = SOCKET_NULL; - socket6FD = SOCKET_NULL; - socketUN = SOCKET_NULL; - socketUrl = nil; - stateIndex = 0; - - if (sq) - { - NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), - @"The given socketQueue parameter must not be a concurrent queue."); - NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), - @"The given socketQueue parameter must not be a concurrent queue."); - NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), - @"The given socketQueue parameter must not be a concurrent queue."); - - socketQueue = sq; - #if !OS_OBJECT_USE_OBJC - dispatch_retain(sq); - #endif - } - else - { - socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); - } - - // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. - // From the documentation: - // - // > Keys are only compared as pointers and are never dereferenced. - // > Thus, you can use a pointer to a static variable for a specific subsystem or - // > any other value that allows you to identify the value uniquely. - // - // We're just going to use the memory address of an ivar. - // Specifically an ivar that is explicitly named for our purpose to make the code more readable. - // - // However, it feels tedious (and less readable) to include the "&" all the time: - // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) - // - // So we're going to make it so it doesn't matter if we use the '&' or not, - // by assigning the value of the ivar to the address of the ivar. - // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; - - IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; - - void *nonNullUnusedPointer = (__bridge void *)self; - dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); - - readQueue = [[NSMutableArray alloc] initWithCapacity:5]; - currentRead = nil; - - writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; - currentWrite = nil; - - preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; - alternateAddressDelay = 0.3; - } - return self; + if((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + +#if !OS_OBJECT_USE_OBJC + if (dq) dispatch_retain(dq); +#endif + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + socketUrl = nil; + stateIndex = 0; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, + 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; +#if !OS_OBJECT_USE_OBJC + dispatch_retain(sq); +#endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], + NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, + IsOnSocketQueueOrTargetQueueKey, + nonNullUnusedPointer, + NULL); + + readQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentRead = nil; + + writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentWrite = nil; + + preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + alternateAddressDelay = 0.3; + } + return self; } - (void)dealloc { - LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); - - // Set dealloc flag. - // This is used by closeWithError to ensure we don't accidentally retain ourself. - flags |= kDealloc; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - [self closeWithError:nil]; - } - else - { - dispatch_sync(socketQueue, ^{ - [self closeWithError:nil]; - }); - } - - delegate = nil; - - #if !OS_OBJECT_USE_OBJC - if (delegateQueue) dispatch_release(delegateQueue); - #endif - delegateQueue = NULL; - - #if !OS_OBJECT_USE_OBJC - if (socketQueue) dispatch_release(socketQueue); - #endif - socketQueue = NULL; - - LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + + // Set dealloc flag. + // This is used by closeWithError to ensure we don't accidentally retain ourself. + flags |= kDealloc; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + +#if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); +#endif + delegateQueue = NULL; + +#if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); +#endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); } #pragma mark - @@ -1085,16 +1102,19 @@ + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nul GCDAsyncSocket *socket = [[[self class] alloc] initWithDelegate:aDelegate delegateQueue:dq socketQueue:sq]; __block NSError *innerError = nil; - dispatch_sync(socket->socketQueue, ^{ @autoreleasepool { + dispatch_sync(socket->socketQueue, + ^{ @autoreleasepool { struct sockaddr addr; socklen_t addr_size = sizeof(struct sockaddr); int retVal = getpeername(socketFD, (struct sockaddr *)&addr, &addr_size); if (retVal) { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", - @"GCDAsyncSocket", [NSBundle mainBundle], - @"Attempt to create socket from socket FD failed. getpeername() failed", nil); - + @"GCDAsyncSocket", + [NSBundle mainBundle], + @"Attempt to create socket from socket FD failed. getpeername() failed", + nil); + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; innerError = [NSError errorWithDomain:GCDAsyncSocketErrorDomain @@ -1102,7 +1122,7 @@ + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nul userInfo:userInfo]; return; } - + if (addr.sa_family == AF_INET) { socket->socket4FD = socketFD; @@ -1114,11 +1134,13 @@ + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nul else { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", - @"GCDAsyncSocket", [NSBundle mainBundle], - @"Attempt to create socket from socket FD failed. socket FD is neither IPv4 nor IPv6", nil); - + @"GCDAsyncSocket", + [NSBundle mainBundle], + @"Attempt to create socket from socket FD failed. socket FD is neither IPv4 nor IPv6", + nil); + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - + innerError = [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; @@ -1141,324 +1163,324 @@ + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nul - (id)delegate { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return delegate; - } - else - { - __block id result; - - dispatch_sync(socketQueue, ^{ - result = self->delegate; - }); - - return result; - } + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result; + + dispatch_sync(socketQueue, ^{ + result = self->delegate; + }); + + return result; + } } - (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously { - dispatch_block_t block = ^{ - self->delegate = newDelegate; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { - block(); - } - else { - if (synchronously) - dispatch_sync(socketQueue, block); - else - dispatch_async(socketQueue, block); - } + dispatch_block_t block = ^{ + self->delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } } - (void)setDelegate:(id)newDelegate { - [self setDelegate:newDelegate synchronously:NO]; + [self setDelegate:newDelegate synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate { - [self setDelegate:newDelegate synchronously:YES]; + [self setDelegate:newDelegate synchronously:YES]; } - (dispatch_queue_t)delegateQueue { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return delegateQueue; - } - else - { - __block dispatch_queue_t result; - - dispatch_sync(socketQueue, ^{ - result = self->delegateQueue; - }); - - return result; - } + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result; + + dispatch_sync(socketQueue, ^{ + result = self->delegateQueue; + }); + + return result; + } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { - dispatch_block_t block = ^{ - - #if !OS_OBJECT_USE_OBJC - if (self->delegateQueue) dispatch_release(self->delegateQueue); - if (newDelegateQueue) dispatch_retain(newDelegateQueue); - #endif - - self->delegateQueue = newDelegateQueue; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { - block(); - } - else { - if (synchronously) - dispatch_sync(socketQueue, block); - else - dispatch_async(socketQueue, block); - } + dispatch_block_t block = ^{ + +#if !OS_OBJECT_USE_OBJC + if (self->delegateQueue) dispatch_release(self->delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); +#endif + + self->delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue { - [self setDelegateQueue:newDelegateQueue synchronously:NO]; + [self setDelegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue { - [self setDelegateQueue:newDelegateQueue synchronously:YES]; + [self setDelegateQueue:newDelegateQueue synchronously:YES]; } - (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - if (delegatePtr) *delegatePtr = delegate; - if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; - } - else - { - __block id dPtr = NULL; - __block dispatch_queue_t dqPtr = NULL; - - dispatch_sync(socketQueue, ^{ - dPtr = self->delegate; - dqPtr = self->delegateQueue; - }); - - if (delegatePtr) *delegatePtr = dPtr; - if (delegateQueuePtr) *delegateQueuePtr = dqPtr; - } + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = self->delegate; + dqPtr = self->delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { - dispatch_block_t block = ^{ - - self->delegate = newDelegate; - - #if !OS_OBJECT_USE_OBJC - if (self->delegateQueue) dispatch_release(self->delegateQueue); - if (newDelegateQueue) dispatch_retain(newDelegateQueue); - #endif - - self->delegateQueue = newDelegateQueue; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { - block(); - } - else { - if (synchronously) - dispatch_sync(socketQueue, block); - else - dispatch_async(socketQueue, block); - } + dispatch_block_t block = ^{ + + self->delegate = newDelegate; + +#if !OS_OBJECT_USE_OBJC + if (self->delegateQueue) dispatch_release(self->delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); +#endif + + self->delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { - [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { - [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; } - (BOOL)isIPv4Enabled { - // Note: YES means kIPv4Disabled is OFF - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return ((config & kIPv4Disabled) == 0); - } - else - { - __block BOOL result; - - dispatch_sync(socketQueue, ^{ - result = ((self->config & kIPv4Disabled) == 0); - }); - - return result; - } + // Note: YES means kIPv4Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv4Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((self->config & kIPv4Disabled) == 0); + }); + + return result; + } } - (void)setIPv4Enabled:(BOOL)flag { - // Note: YES means kIPv4Disabled is OFF - - dispatch_block_t block = ^{ - - if (flag) - self->config &= ~kIPv4Disabled; - else - self->config |= kIPv4Disabled; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + self->config &= ~kIPv4Disabled; + else + self->config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (BOOL)isIPv6Enabled { - // Note: YES means kIPv6Disabled is OFF - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return ((config & kIPv6Disabled) == 0); - } - else - { - __block BOOL result; - - dispatch_sync(socketQueue, ^{ - result = ((self->config & kIPv6Disabled) == 0); - }); - - return result; - } + // Note: YES means kIPv6Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv6Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((self->config & kIPv6Disabled) == 0); + }); + + return result; + } } - (void)setIPv6Enabled:(BOOL)flag { - // Note: YES means kIPv6Disabled is OFF - - dispatch_block_t block = ^{ - - if (flag) - self->config &= ~kIPv6Disabled; - else - self->config |= kIPv6Disabled; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + self->config &= ~kIPv6Disabled; + else + self->config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (BOOL)isIPv4PreferredOverIPv6 { - // Note: YES means kPreferIPv6 is OFF - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return ((config & kPreferIPv6) == 0); - } - else - { - __block BOOL result; - - dispatch_sync(socketQueue, ^{ - result = ((self->config & kPreferIPv6) == 0); - }); - - return result; - } + // Note: YES means kPreferIPv6 is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kPreferIPv6) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((self->config & kPreferIPv6) == 0); + }); + + return result; + } } - (void)setIPv4PreferredOverIPv6:(BOOL)flag { - // Note: YES means kPreferIPv6 is OFF - - dispatch_block_t block = ^{ - - if (flag) - self->config &= ~kPreferIPv6; - else - self->config |= kPreferIPv6; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + // Note: YES means kPreferIPv6 is OFF + + dispatch_block_t block = ^{ + + if (flag) + self->config &= ~kPreferIPv6; + else + self->config |= kPreferIPv6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (NSTimeInterval) alternateAddressDelay { - __block NSTimeInterval delay; - dispatch_block_t block = ^{ - delay = self->alternateAddressDelay; - }; - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - return delay; + __block NSTimeInterval delay; + dispatch_block_t block = ^{ + delay = self->alternateAddressDelay; + }; + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + return delay; } - (void) setAlternateAddressDelay:(NSTimeInterval)delay { - dispatch_block_t block = ^{ - self->alternateAddressDelay = delay; - }; - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + dispatch_block_t block = ^{ + self->alternateAddressDelay = delay; + }; + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (id)userData { - __block id result = nil; - - dispatch_block_t block = ^{ - - result = self->userData; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block id result = nil; + + dispatch_block_t block = ^{ + + result = self->userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (void)setUserData:(id)arbitraryUserData { - dispatch_block_t block = ^{ - - if (self->userData != arbitraryUserData) - { - self->userData = arbitraryUserData; - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + dispatch_block_t block = ^{ + + if (self->userData != arbitraryUserData) + { + self->userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1467,6124 +1489,6289 @@ - (void)setUserData:(id)arbitraryUserData - (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr { - return [self acceptOnInterface:nil port:port error:errPtr]; + return [self acceptOnInterface:nil port:port error:errPtr]; } - (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr { - LogTrace(); - - // Just in-case interface parameter is immutable. - NSString *interface = [inInterface copy]; - - __block BOOL result = NO; - __block NSError *err = nil; - - // CreateSocket Block - // This block will be invoked within the dispatch block below. - - int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { - - int socketFD = socket(domain, SOCK_STREAM, 0); - - if (socketFD == SOCKET_NULL) - { - NSString *reason = @"Error in socket() function"; - err = [self errorWithErrno:errno reason:reason]; - - return SOCKET_NULL; - } - - int status; - - // Set socket options - - status = fcntl(socketFD, F_SETFL, O_NONBLOCK); - if (status == -1) - { - NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; - err = [self errorWithErrno:errno reason:reason]; - - LogVerbose(@"close(socketFD)"); - close(socketFD); - return SOCKET_NULL; - } - - int reuseOn = 1; - status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); - if (status == -1) - { - NSString *reason = @"Error enabling address reuse (setsockopt)"; - err = [self errorWithErrno:errno reason:reason]; - - LogVerbose(@"close(socketFD)"); - close(socketFD); - return SOCKET_NULL; - } - - // Bind socket - - status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); - if (status == -1) - { - NSString *reason = @"Error in bind() function"; - err = [self errorWithErrno:errno reason:reason]; - - LogVerbose(@"close(socketFD)"); - close(socketFD); - return SOCKET_NULL; - } - - // Listen - - status = listen(socketFD, 1024); - if (status == -1) - { - NSString *reason = @"Error in listen() function"; - err = [self errorWithErrno:errno reason:reason]; - - LogVerbose(@"close(socketFD)"); - close(socketFD); - return SOCKET_NULL; - } - - return socketFD; - }; - - // Create dispatch block and run on socketQueue - - dispatch_block_t block = ^{ @autoreleasepool { - - if (self->delegate == nil) // Must have delegate set - { - NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; - err = [self badConfigError:msg]; - - return_from_block; - } - - if (self->delegateQueue == NULL) // Must have delegate queue set - { - NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; - err = [self badConfigError:msg]; - - return_from_block; - } - - BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; - BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; - - if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled - { - NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; - err = [self badConfigError:msg]; - - return_from_block; - } - - if (![self isDisconnected]) // Must be disconnected - { - NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; - err = [self badConfigError:msg]; - - return_from_block; - } - - // Clear queues (spurious read/write requests post disconnect) - [self->readQueue removeAllObjects]; - [self->writeQueue removeAllObjects]; - - // Resolve interface from description - - NSMutableData *interface4 = nil; - NSMutableData *interface6 = nil; - - [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; - - if ((interface4 == nil) && (interface6 == nil)) - { - NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; - err = [self badParamError:msg]; - - return_from_block; - } - - if (isIPv4Disabled && (interface6 == nil)) - { - NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; - err = [self badParamError:msg]; - - return_from_block; - } - - if (isIPv6Disabled && (interface4 == nil)) - { - NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; - err = [self badParamError:msg]; - - return_from_block; - } - - BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); - BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); - - // Create sockets, configure, bind, and listen - - if (enableIPv4) - { - LogVerbose(@"Creating IPv4 socket"); - self->socket4FD = createSocket(AF_INET, interface4); - - if (self->socket4FD == SOCKET_NULL) - { - return_from_block; - } - } - - if (enableIPv6) - { - LogVerbose(@"Creating IPv6 socket"); - - if (enableIPv4 && (port == 0)) - { - // No specific port was specified, so we allowed the OS to pick an available port for us. - // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. - - struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; - addr6->sin6_port = htons([self localPort4]); - } - - self->socket6FD = createSocket(AF_INET6, interface6); - - if (self->socket6FD == SOCKET_NULL) - { - if (self->socket4FD != SOCKET_NULL) - { - LogVerbose(@"close(socket4FD)"); - close(self->socket4FD); - self->socket4FD = SOCKET_NULL; - } - - return_from_block; - } - } - - // Create accept sources - - if (enableIPv4) - { - self->accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socket4FD, 0, self->socketQueue); - - int socketFD = self->socket4FD; - dispatch_source_t acceptSource = self->accept4Source; - - __weak GCDAsyncSocket *weakSelf = self; - - dispatch_source_set_event_handler(self->accept4Source, ^{ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf == nil) return_from_block; - - LogVerbose(@"event4Block"); - - unsigned long i = 0; - unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); - - LogVerbose(@"numPendingConnections: %lu", numPendingConnections); - - while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); - - #pragma clang diagnostic pop - }}); - - - dispatch_source_set_cancel_handler(self->accept4Source, ^{ - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - #if !OS_OBJECT_USE_OBJC - LogVerbose(@"dispatch_release(accept4Source)"); - dispatch_release(acceptSource); - #endif - - LogVerbose(@"close(socket4FD)"); - close(socketFD); - - #pragma clang diagnostic pop - }); - - LogVerbose(@"dispatch_resume(accept4Source)"); - dispatch_resume(self->accept4Source); - } - - if (enableIPv6) - { - self->accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socket6FD, 0, self->socketQueue); - - int socketFD = self->socket6FD; - dispatch_source_t acceptSource = self->accept6Source; - - __weak GCDAsyncSocket *weakSelf = self; - - dispatch_source_set_event_handler(self->accept6Source, ^{ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf == nil) return_from_block; - - LogVerbose(@"event6Block"); - - unsigned long i = 0; - unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); - - LogVerbose(@"numPendingConnections: %lu", numPendingConnections); - - while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); - - #pragma clang diagnostic pop - }}); - - dispatch_source_set_cancel_handler(self->accept6Source, ^{ - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - #if !OS_OBJECT_USE_OBJC - LogVerbose(@"dispatch_release(accept6Source)"); - dispatch_release(acceptSource); - #endif - - LogVerbose(@"close(socket6FD)"); - close(socketFD); - - #pragma clang diagnostic pop - }); - - LogVerbose(@"dispatch_resume(accept6Source)"); - dispatch_resume(self->accept6Source); - } - - self->flags |= kSocketStarted; - - result = YES; - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (result == NO) - { - LogInfo(@"Error in accept: %@", err); - - if (errPtr) - *errPtr = err; - } - - return result; -} + LogTrace(); -- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr -{ - LogTrace(); - - __block BOOL result = NO; - __block NSError *err = nil; - - // CreateSocket Block - // This block will be invoked within the dispatch block below. - - int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { - - int socketFD = socket(domain, SOCK_STREAM, 0); - - if (socketFD == SOCKET_NULL) - { - NSString *reason = @"Error in socket() function"; - err = [self errorWithErrno:errno reason:reason]; - - return SOCKET_NULL; - } - - int status; - - // Set socket options - - status = fcntl(socketFD, F_SETFL, O_NONBLOCK); - if (status == -1) - { - NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; - err = [self errorWithErrno:errno reason:reason]; - - LogVerbose(@"close(socketFD)"); - close(socketFD); - return SOCKET_NULL; - } - - int reuseOn = 1; - status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); - if (status == -1) - { - NSString *reason = @"Error enabling address reuse (setsockopt)"; - err = [self errorWithErrno:errno reason:reason]; - - LogVerbose(@"close(socketFD)"); - close(socketFD); - return SOCKET_NULL; - } - - // Bind socket - - status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); - if (status == -1) - { - NSString *reason = @"Error in bind() function"; - err = [self errorWithErrno:errno reason:reason]; - - LogVerbose(@"close(socketFD)"); - close(socketFD); - return SOCKET_NULL; - } - - // Listen - - status = listen(socketFD, 1024); - if (status == -1) - { - NSString *reason = @"Error in listen() function"; - err = [self errorWithErrno:errno reason:reason]; - - LogVerbose(@"close(socketFD)"); - close(socketFD); - return SOCKET_NULL; - } - - return socketFD; - }; - - // Create dispatch block and run on socketQueue - - dispatch_block_t block = ^{ @autoreleasepool { - - if (self->delegate == nil) // Must have delegate set - { - NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; - err = [self badConfigError:msg]; - - return_from_block; - } - - if (self->delegateQueue == NULL) // Must have delegate queue set - { - NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; - err = [self badConfigError:msg]; - - return_from_block; - } - - if (![self isDisconnected]) // Must be disconnected - { - NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; - err = [self badConfigError:msg]; - - return_from_block; - } - - // Clear queues (spurious read/write requests post disconnect) - [self->readQueue removeAllObjects]; - [self->writeQueue removeAllObjects]; - - // Remove a previous socket - - NSError *error = nil; - NSFileManager *fileManager = [NSFileManager defaultManager]; - NSString *urlPath = url.path; - if (urlPath && [fileManager fileExistsAtPath:urlPath]) { - if (![fileManager removeItemAtURL:url error:&error]) { - NSString *msg = @"Could not remove previous unix domain socket at given url."; - err = [self otherError:msg]; - - return_from_block; - } - } - - // Resolve interface from description - - NSData *interface = [self getInterfaceAddressFromUrl:url]; - - if (interface == nil) - { - NSString *msg = @"Invalid unix domain url. Specify a valid file url that does not exist (e.g. \"file:///tmp/socket\")"; - err = [self badParamError:msg]; - - return_from_block; - } - - // Create sockets, configure, bind, and listen - - LogVerbose(@"Creating unix domain socket"); - self->socketUN = createSocket(AF_UNIX, interface); - - if (self->socketUN == SOCKET_NULL) - { - return_from_block; - } - - self->socketUrl = url; - - // Create accept sources - - self->acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socketUN, 0, self->socketQueue); - - int socketFD = self->socketUN; - dispatch_source_t acceptSource = self->acceptUNSource; - - __weak GCDAsyncSocket *weakSelf = self; - - dispatch_source_set_event_handler(self->acceptUNSource, ^{ @autoreleasepool { - - __strong GCDAsyncSocket *strongSelf = weakSelf; - - LogVerbose(@"eventUNBlock"); - - unsigned long i = 0; - unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); - - LogVerbose(@"numPendingConnections: %lu", numPendingConnections); - - while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); - }}); - - dispatch_source_set_cancel_handler(self->acceptUNSource, ^{ - -#if !OS_OBJECT_USE_OBJC - LogVerbose(@"dispatch_release(acceptUNSource)"); - dispatch_release(acceptSource); -#endif - - LogVerbose(@"close(socketUN)"); - close(socketFD); - }); - - LogVerbose(@"dispatch_resume(acceptUNSource)"); - dispatch_resume(self->acceptUNSource); - - self->flags |= kSocketStarted; - - result = YES; - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (result == NO) - { - LogInfo(@"Error in accept: %@", err); - - if (errPtr) - *errPtr = err; - } - - return result; -} + // Just in-case interface parameter is immutable. + NSString *interface = [inInterface copy]; -- (BOOL)doAccept:(int)parentSocketFD -{ - LogTrace(); - - int socketType; - int childSocketFD; - NSData *childSocketAddress; - - if (parentSocketFD == socket4FD) - { - socketType = 0; - - struct sockaddr_in addr; - socklen_t addrLen = sizeof(addr); - - childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); - - if (childSocketFD == -1) - { - LogWarn(@"Accept failed with error: %@", [self errnoError]); - return NO; - } - - childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; - } - else if (parentSocketFD == socket6FD) - { - socketType = 1; - - struct sockaddr_in6 addr; - socklen_t addrLen = sizeof(addr); - - childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); - - if (childSocketFD == -1) - { - LogWarn(@"Accept failed with error: %@", [self errnoError]); - return NO; - } - - childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; - } - else // if (parentSocketFD == socketUN) - { - socketType = 2; - - struct sockaddr_un addr; - socklen_t addrLen = sizeof(addr); - - childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); - - if (childSocketFD == -1) - { - LogWarn(@"Accept failed with error: %@", [self errnoError]); - return NO; - } - - childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; - } - - // Enable non-blocking IO on the socket - - int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); - if (result == -1) - { - LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); - LogVerbose(@"close(childSocketFD)"); - close(childSocketFD); - return NO; - } - - // Prevent SIGPIPE signals - - int nosigpipe = 1; - setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); - - // Notify delegate - - if (delegateQueue) - { - __strong id theDelegate = delegate; - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - // Query delegate for custom socket queue - - dispatch_queue_t childSocketQueue = NULL; - - if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) - { - childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress - onSocket:self]; - } - - // Create GCDAsyncSocket instance for accepted socket - - GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate - delegateQueue:self->delegateQueue - socketQueue:childSocketQueue]; - - if (socketType == 0) - acceptedSocket->socket4FD = childSocketFD; - else if (socketType == 1) - acceptedSocket->socket6FD = childSocketFD; - else - acceptedSocket->socketUN = childSocketFD; - - acceptedSocket->flags = (kSocketStarted | kConnected); - - // Setup read and write sources for accepted socket - - dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { - - [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; - }}); - - // Notify delegate - - if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) - { - [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; - } - - // Release the socket queue returned from the delegate (it was retained by acceptedSocket) - #if !OS_OBJECT_USE_OBJC - if (childSocketQueue) dispatch_release(childSocketQueue); - #endif - - // The accepted socket should have been retained by the delegate. - // Otherwise it gets properly released when exiting the block. - }}); - } - - return YES; -} + __block BOOL result = NO; + __block NSError *err = nil; -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Connecting -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // CreateSocket Block + // This block will be invoked within the dispatch block below. -/** - * This method runs through the various checks required prior to a connection attempt. - * It is shared between the connectToHost and connectToAddress methods. - * -**/ -- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr -{ - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - if (delegate == nil) // Must have delegate set - { - if (errPtr) - { - NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - if (delegateQueue == NULL) // Must have delegate queue set - { - if (errPtr) - { - NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - if (![self isDisconnected]) // Must be disconnected - { - if (errPtr) - { - NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; - BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - - if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled - { - if (errPtr) - { - NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - if (interface) - { - NSMutableData *interface4 = nil; - NSMutableData *interface6 = nil; - - [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; - - if ((interface4 == nil) && (interface6 == nil)) - { - if (errPtr) - { - NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; - *errPtr = [self badParamError:msg]; - } - return NO; - } - - if (isIPv4Disabled && (interface6 == nil)) - { - if (errPtr) - { - NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; - *errPtr = [self badParamError:msg]; - } - return NO; - } - - if (isIPv6Disabled && (interface4 == nil)) - { - if (errPtr) - { - NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; - *errPtr = [self badParamError:msg]; - } - return NO; - } - - connectInterface4 = interface4; - connectInterface6 = interface6; - } - - // Clear queues (spurious read/write requests post disconnect) - [readQueue removeAllObjects]; - [writeQueue removeAllObjects]; - - return YES; -} + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { -- (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr -{ - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - if (delegate == nil) // Must have delegate set - { - if (errPtr) - { - NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - if (delegateQueue == NULL) // Must have delegate queue set - { - if (errPtr) - { - NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - if (![self isDisconnected]) // Must be disconnected - { - if (errPtr) - { - NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - NSData *interface = [self getInterfaceAddressFromUrl:url]; - - if (interface == nil) - { - if (errPtr) - { - NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; - *errPtr = [self badParamError:msg]; - } - return NO; - } - - connectInterfaceUN = interface; - - // Clear queues (spurious read/write requests post disconnect) - [readQueue removeAllObjects]; - [writeQueue removeAllObjects]; - - return YES; -} + int socketFD = socket(domain, SOCK_STREAM, 0); -- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr -{ - return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; -} + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errorWithErrno:errno reason:reason]; -- (BOOL)connectToHost:(NSString *)host - onPort:(uint16_t)port - withTimeout:(NSTimeInterval)timeout - error:(NSError **)errPtr -{ - return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; -} + return SOCKET_NULL; + } -- (BOOL)connectToHost:(NSString *)inHost - onPort:(uint16_t)port - viaInterface:(NSString *)inInterface - withTimeout:(NSTimeInterval)timeout - error:(NSError **)errPtr -{ - LogTrace(); - - // Just in case immutable objects were passed - NSString *host = [inHost copy]; - NSString *interface = [inInterface copy]; - - __block BOOL result = NO; - __block NSError *preConnectErr = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - // Check for problems with host parameter - - if ([host length] == 0) - { - NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; - preConnectErr = [self badParamError:msg]; - - return_from_block; - } - - // Run through standard pre-connect checks - - if (![self preConnectWithInterface:interface error:&preConnectErr]) - { - return_from_block; - } - - // We've made it past all the checks. - // It's time to start the connection process. - - self->flags |= kSocketStarted; - - LogVerbose(@"Dispatching DNS lookup..."); - - // It's possible that the given host parameter is actually a NSMutableString. - // So we want to copy it now, within this block that will be executed synchronously. - // This way the asynchronous lookup block below doesn't have to worry about it changing. - - NSString *hostCpy = [host copy]; - - int aStateIndex = self->stateIndex; - __weak GCDAsyncSocket *weakSelf = self; - - dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - NSError *lookupErr = nil; - NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf == nil) return_from_block; - - if (lookupErr) - { - dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { - - [strongSelf lookup:aStateIndex didFail:lookupErr]; - }}); - } - else - { - NSData *address4 = nil; - NSData *address6 = nil; - - for (NSData *address in addresses) - { - if (!address4 && [[self class] isIPv4Address:address]) - { - address4 = address; - } - else if (!address6 && [[self class] isIPv6Address:address]) - { - address6 = address; - } - } - - dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { - - [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; - }}); - } - - #pragma clang diagnostic pop - }}); - - [self startConnectTimeout:timeout]; - - result = YES; - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - - if (errPtr) *errPtr = preConnectErr; - return result; -} + int status; -- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr -{ - return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; -} + // Set socket options -- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr -{ - return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; -} + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errorWithErrno:errno reason:reason]; -- (BOOL)connectToAddress:(NSData *)inRemoteAddr - viaInterface:(NSString *)inInterface - withTimeout:(NSTimeInterval)timeout - error:(NSError **)errPtr -{ - LogTrace(); - - // Just in case immutable objects were passed - NSData *remoteAddr = [inRemoteAddr copy]; - NSString *interface = [inInterface copy]; - - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - // Check for problems with remoteAddr parameter - - NSData *address4 = nil; - NSData *address6 = nil; - - if ([remoteAddr length] >= sizeof(struct sockaddr)) - { - const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; - - if (sockaddr->sa_family == AF_INET) - { - if ([remoteAddr length] == sizeof(struct sockaddr_in)) - { - address4 = remoteAddr; - } - } - else if (sockaddr->sa_family == AF_INET6) - { - if ([remoteAddr length] == sizeof(struct sockaddr_in6)) - { - address6 = remoteAddr; - } - } - } - - if ((address4 == nil) && (address6 == nil)) - { - NSString *msg = @"A valid IPv4 or IPv6 address was not given"; - err = [self badParamError:msg]; - - return_from_block; - } - - BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; - BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; - - if (isIPv4Disabled && (address4 != nil)) - { - NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; - err = [self badParamError:msg]; - - return_from_block; - } - - if (isIPv6Disabled && (address6 != nil)) - { - NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; - err = [self badParamError:msg]; - - return_from_block; - } - - // Run through standard pre-connect checks - - if (![self preConnectWithInterface:interface error:&err]) - { - return_from_block; - } - - // We've made it past all the checks. - // It's time to start the connection process. - - if (![self connectWithAddress4:address4 address6:address6 error:&err]) - { - return_from_block; - } - - self->flags |= kSocketStarted; - - [self startConnectTimeout:timeout]; - - result = YES; - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (result == NO) - { - if (errPtr) - *errPtr = err; - } - - return result; -} + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } -- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr -{ - LogTrace(); - - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - // Check for problems with host parameter - - if ([url.path length] == 0) - { - NSString *msg = @"Invalid unix domain socket url."; - err = [self badParamError:msg]; - - return_from_block; - } - - // Run through standard pre-connect checks - - if (![self preConnectWithUrl:url error:&err]) - { - return_from_block; - } - - // We've made it past all the checks. - // It's time to start the connection process. - - self->flags |= kSocketStarted; - - // Start the normal connection process - - NSError *connectError = nil; - if (![self connectWithAddressUN:self->connectInterfaceUN error:&connectError]) - { - [self closeWithError:connectError]; - - return_from_block; - } - - [self startConnectTimeout:timeout]; - - result = YES; - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (result == NO) - { - if (errPtr) - *errPtr = err; - } - - return result; -} + int reuseOn = 1; + status = setsockopt(socketFD, + SOL_SOCKET, + SO_REUSEADDR, + &reuseOn, + sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errorWithErrno:errno reason:reason]; -- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 -{ - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert(address4 || address6, @"Expected at least one valid address"); - - if (aStateIndex != stateIndex) - { - LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); - - // The connect operation has been cancelled. - // That is, socket was disconnected, or connection has already timed out. - return; - } - - // Check for problems - - BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; - BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - - if (isIPv4Disabled && (address6 == nil)) - { - NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; - - [self closeWithError:[self otherError:msg]]; - return; - } - - if (isIPv6Disabled && (address4 == nil)) - { - NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; - - [self closeWithError:[self otherError:msg]]; - return; - } - - // Start the normal connection process - - NSError *err = nil; - if (![self connectWithAddress4:address4 address6:address6 error:&err]) - { - [self closeWithError:err]; - } -} + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } -/** - * This method is called if the DNS lookup fails. - * This method is executed on the socketQueue. - * - * Since the DNS lookup executed synchronously on a global concurrent queue, - * the original connection request may have already been cancelled or timed-out by the time this method is invoked. - * The lookupIndex tells us whether the lookup is still valid or not. -**/ -- (void)lookup:(int)aStateIndex didFail:(NSError *)error -{ - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - - if (aStateIndex != stateIndex) - { - LogInfo(@"Ignoring lookup:didFail: - already disconnected"); - - // The connect operation has been cancelled. - // That is, socket was disconnected, or connection has already timed out. - return; - } - - [self endConnectTimeout]; - [self closeWithError:error]; -} + // Bind socket -- (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr -{ - // Bind the socket to the desired interface (if needed) - - if (connectInterface) + status = bind(socketFD, + (const struct sockaddr *)[interfaceAddr bytes], + (socklen_t)[interfaceAddr length]); + if (status == -1) { - LogVerbose(@"Binding socket..."); - - if ([[self class] portFromAddress:connectInterface] > 0) - { - // Since we're going to be binding to a specific port, - // we should turn on reuseaddr to allow us to override sockets in time_wait. - - int reuseOn = 1; - setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); - } - - const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; - - int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); - if (result != 0) - { - if (errPtr) - *errPtr = [self errorWithErrno:errno reason:@"Error in bind() function"]; - - return NO; - } - } - - return YES; -} + NSString *reason = @"Error in bind() function"; + err = [self errorWithErrno:errno reason:reason]; -- (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr -{ - int socketFD = socket(family, SOCK_STREAM, 0); - - if (socketFD == SOCKET_NULL) - { - if (errPtr) - *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; - - return socketFD; + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; } - - if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) { - [self closeSocket:socketFD]; - - return SOCKET_NULL; + NSString *reason = @"Error in listen() function"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; } - - // Prevent SIGPIPE signals - - int nosigpipe = 1; - setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); - + return socketFD; -} + }; -- (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex -{ - // If there already is a socket connected, we close socketFD and return - if (self.isConnected) + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (self->delegate == nil) // Must have delegate set { - [self closeSocket:socketFD]; - return; + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; } - - // Start the connection process in a background queue - - __weak GCDAsyncSocket *weakSelf = self; - - dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(globalConcurrentQueue, ^{ -#pragma clang diagnostic push -#pragma clang diagnostic warning "-Wimplicit-retain-self" - - int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); - int err = errno; - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf == nil) return_from_block; - - dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { - - if (strongSelf.isConnected) - { - [strongSelf closeSocket:socketFD]; - return_from_block; - } - - if (result == 0) - { - [self closeUnusedSocket:socketFD]; - - [strongSelf didConnect:aStateIndex]; - } - else - { - [strongSelf closeSocket:socketFD]; - - // If there are no more sockets trying to connect, we inform the error to the delegate - if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL) - { - NSError *error = [strongSelf errorWithErrno:err reason:@"Error in connect() function"]; - [strongSelf didNotConnect:aStateIndex error:error]; - } - } - }}); - -#pragma clang diagnostic pop - }); - - LogVerbose(@"Connecting..."); -} -- (void)closeSocket:(int)socketFD -{ - if (socketFD != SOCKET_NULL && - (socketFD == socket6FD || socketFD == socket4FD)) + if (self->delegateQueue == NULL) // Must have delegate queue set { - close(socketFD); - - if (socketFD == socket4FD) - { - LogVerbose(@"close(socket4FD)"); - socket4FD = SOCKET_NULL; - } - else if (socketFD == socket6FD) - { - LogVerbose(@"close(socket6FD)"); - socket6FD = SOCKET_NULL; - } + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; } -} -- (void)closeUnusedSocket:(int)usedSocketFD -{ - if (usedSocketFD != socket4FD) + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { - [self closeSocket:socket4FD]; + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + err = [self badConfigError:msg]; + + return_from_block; } - else if (usedSocketFD != socket6FD) + + if (![self isDisconnected]) // Must be disconnected { - [self closeSocket:socket6FD]; - } -} + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; -- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr -{ - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); - LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); - - // Determine socket type - - BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; - - // Create and bind the sockets - - if (address4) - { - LogVerbose(@"Creating IPv4 socket"); - - socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; - } - - if (address6) - { - LogVerbose(@"Creating IPv6 socket"); - - socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; - } - - if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) - { - return NO; - } - - int socketFD, alternateSocketFD; - NSData *address, *alternateAddress; - - if ((preferIPv6 && socket6FD != SOCKET_NULL) || socket4FD == SOCKET_NULL) - { - socketFD = socket6FD; - alternateSocketFD = socket4FD; - address = address6; - alternateAddress = address4; + return_from_block; } - else + + // Clear queues (spurious read/write requests post disconnect) + [self->readQueue removeAllObjects]; + [self->writeQueue removeAllObjects]; + + // Resolve interface from description + + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; + + if ((interface4 == nil) && (interface6 == nil)) { - socketFD = socket4FD; - alternateSocketFD = socket6FD; - address = address4; - alternateAddress = address6; + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; } - int aStateIndex = stateIndex; - - [self connectSocket:socketFD address:address stateIndex:aStateIndex]; - - if (alternateAddress) + if (isIPv4Disabled && (interface6 == nil)) { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{ - [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; - }); + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; } - - return YES; -} -- (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr -{ - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - // Create the socket - - int socketFD; - - LogVerbose(@"Creating unix domain socket"); - - socketUN = socket(AF_UNIX, SOCK_STREAM, 0); - - socketFD = socketUN; - - if (socketFD == SOCKET_NULL) - { - if (errPtr) - *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; - - return NO; - } - - // Bind the socket to the desired interface (if needed) - - LogVerbose(@"Binding socket..."); - - int reuseOn = 1; - setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); - -// const struct sockaddr *interfaceAddr = (const struct sockaddr *)[address bytes]; -// -// int result = bind(socketFD, interfaceAddr, (socklen_t)[address length]); -// if (result != 0) -// { -// if (errPtr) -// *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; -// -// return NO; -// } - - // Prevent SIGPIPE signals - - int nosigpipe = 1; - setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); - - // Start the connection process in a background queue - - int aStateIndex = stateIndex; - - dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(globalConcurrentQueue, ^{ - - const struct sockaddr *addr = (const struct sockaddr *)[address bytes]; - int result = connect(socketFD, addr, addr->sa_len); - if (result == 0) - { - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - [self didConnect:aStateIndex]; - }}); - } - else - { - // TODO: Bad file descriptor - perror("connect"); - NSError *error = [self errorWithErrno:errno reason:@"Error in connect() function"]; - - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - [self didNotConnect:aStateIndex error:error]; - }}); - } - }); - - LogVerbose(@"Connecting..."); - - return YES; -} + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; -- (void)didConnect:(int)aStateIndex -{ - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - - if (aStateIndex != stateIndex) - { - LogInfo(@"Ignoring didConnect, already disconnected"); - - // The connect operation has been cancelled. - // That is, socket was disconnected, or connection has already timed out. - return; - } - - flags |= kConnected; - - [self endConnectTimeout]; - - #if TARGET_OS_IPHONE - // The endConnectTimeout method executed above incremented the stateIndex. - aStateIndex = stateIndex; - #endif - - // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) - // - // Note: - // There may be configuration options that must be set by the delegate before opening the streams. - // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. - // - // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. - // This gives the delegate time to properly configure the streams if needed. - - dispatch_block_t SetupStreamsPart1 = ^{ - #if TARGET_OS_IPHONE - - if (![self createReadAndWriteStream]) - { - [self closeWithError:[self otherError:@"Error creating CFStreams"]]; - return; - } - - if (![self registerForStreamCallbacksIncludingReadWrite:NO]) - { - [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; - return; - } - - #endif - }; - dispatch_block_t SetupStreamsPart2 = ^{ - #if TARGET_OS_IPHONE - - if (aStateIndex != self->stateIndex) - { - // The socket has been disconnected. - return; - } - - if (![self addStreamsToRunLoop]) - { - [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; - return; - } - - if (![self openStreams]) - { - [self closeWithError:[self otherError:@"Error creating CFStreams"]]; - return; - } - - #endif - }; - - // Notify delegate - - NSString *host = [self connectedHost]; - uint16_t port = [self connectedPort]; - NSURL *url = [self connectedUrl]; - - __strong id theDelegate = delegate; - - if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) - { - SetupStreamsPart1(); - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socket:self didConnectToHost:host port:port]; - - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - SetupStreamsPart2(); - }}); - }}); - } - else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)]) - { - SetupStreamsPart1(); - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socket:self didConnectToUrl:url]; - - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - SetupStreamsPart2(); - }}); - }}); - } - else - { - SetupStreamsPart1(); - SetupStreamsPart2(); - } - - // Get the connected socket - - int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - - // Enable non-blocking IO on the socket - - int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); - if (result == -1) - { - NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; - [self closeWithError:[self otherError:errMsg]]; - - return; - } - - // Setup our read/write sources - - [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; - - // Dequeue any pending read/write requests - - [self maybeDequeueRead]; - [self maybeDequeueWrite]; -} + return_from_block; + } -- (void)didNotConnect:(int)aStateIndex error:(NSError *)error -{ - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - - if (aStateIndex != stateIndex) - { - LogInfo(@"Ignoring didNotConnect, already disconnected"); - - // The connect operation has been cancelled. - // That is, socket was disconnected, or connection has already timed out. - return; - } - - [self closeWithError:error]; -} + BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); -- (void)startConnectTimeout:(NSTimeInterval)timeout -{ - if (timeout >= 0.0) - { - connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - - __weak GCDAsyncSocket *weakSelf = self; - - dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf == nil) return_from_block; - - [strongSelf doConnectTimeout]; - - #pragma clang diagnostic pop - }}); - - #if !OS_OBJECT_USE_OBJC - dispatch_source_t theConnectTimer = connectTimer; - dispatch_source_set_cancel_handler(connectTimer, ^{ - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - LogVerbose(@"dispatch_release(connectTimer)"); - dispatch_release(theConnectTimer); - - #pragma clang diagnostic pop - }); - #endif - - dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); - dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); - - dispatch_resume(connectTimer); - } -} + // Create sockets, configure, bind, and listen -- (void)endConnectTimeout -{ - LogTrace(); - - if (connectTimer) - { - dispatch_source_cancel(connectTimer); - connectTimer = NULL; - } - - // Increment stateIndex. - // This will prevent us from processing results from any related background asynchronous operations. - // - // Note: This should be called from close method even if connectTimer is NULL. - // This is because one might disconnect a socket prior to a successful connection which had no timeout. - - stateIndex++; - - if (connectInterface4) - { - connectInterface4 = nil; - } - if (connectInterface6) - { - connectInterface6 = nil; - } -} + if (enableIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + self->socket4FD = createSocket(AF_INET, interface4); -- (void)doConnectTimeout -{ - LogTrace(); - - [self endConnectTimeout]; - [self closeWithError:[self connectTimeoutError]]; -} + if (self->socket4FD == SOCKET_NULL) + { + return_from_block; + } + } -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Disconnecting -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if (enableIPv6) + { + LogVerbose(@"Creating IPv6 socket"); -- (void)closeWithError:(NSError *)error -{ - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - [self endConnectTimeout]; - - if (currentRead != nil) [self endCurrentRead]; - if (currentWrite != nil) [self endCurrentWrite]; - - [readQueue removeAllObjects]; - [writeQueue removeAllObjects]; - - [preBuffer reset]; - - #if TARGET_OS_IPHONE - { - if (readStream || writeStream) - { - [self removeStreamsFromRunLoop]; - - if (readStream) - { - CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); - CFReadStreamClose(readStream); - CFRelease(readStream); - readStream = NULL; - } - if (writeStream) - { - CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); - CFWriteStreamClose(writeStream); - CFRelease(writeStream); - writeStream = NULL; - } - } - } - #endif - - [sslPreBuffer reset]; - sslErrCode = lastSSLHandshakeError = noErr; - - if (sslContext) - { - // Getting a linker error here about the SSLx() functions? - // You need to add the Security Framework to your application. - - SSLClose(sslContext); - - #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) - CFRelease(sslContext); - #else - SSLDisposeContext(sslContext); - #endif - - sslContext = NULL; - } - - // For some crazy reason (in my opinion), cancelling a dispatch source doesn't - // invoke the cancel handler if the dispatch source is paused. - // So we have to unpause the source if needed. - // This allows the cancel handler to be run, which in turn releases the source and closes the socket. - - if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource) - { - LogVerbose(@"manually closing close"); - - if (socket4FD != SOCKET_NULL) - { - LogVerbose(@"close(socket4FD)"); - close(socket4FD); - socket4FD = SOCKET_NULL; - } - - if (socket6FD != SOCKET_NULL) - { - LogVerbose(@"close(socket6FD)"); - close(socket6FD); - socket6FD = SOCKET_NULL; - } - - if (socketUN != SOCKET_NULL) - { - LogVerbose(@"close(socketUN)"); - close(socketUN); - socketUN = SOCKET_NULL; - unlink(socketUrl.path.fileSystemRepresentation); - socketUrl = nil; - } - } - else - { - if (accept4Source) - { - LogVerbose(@"dispatch_source_cancel(accept4Source)"); - dispatch_source_cancel(accept4Source); - - // We never suspend accept4Source - - accept4Source = NULL; - } - - if (accept6Source) - { - LogVerbose(@"dispatch_source_cancel(accept6Source)"); - dispatch_source_cancel(accept6Source); - - // We never suspend accept6Source - - accept6Source = NULL; - } - - if (acceptUNSource) - { - LogVerbose(@"dispatch_source_cancel(acceptUNSource)"); - dispatch_source_cancel(acceptUNSource); - - // We never suspend acceptUNSource - - acceptUNSource = NULL; - } - - if (readSource) - { - LogVerbose(@"dispatch_source_cancel(readSource)"); - dispatch_source_cancel(readSource); - - [self resumeReadSource]; - - readSource = NULL; - } - - if (writeSource) - { - LogVerbose(@"dispatch_source_cancel(writeSource)"); - dispatch_source_cancel(writeSource); - - [self resumeWriteSource]; - - writeSource = NULL; - } - - // The sockets will be closed by the cancel handlers of the corresponding source - - socket4FD = SOCKET_NULL; - socket6FD = SOCKET_NULL; - socketUN = SOCKET_NULL; - } - - // If the client has passed the connect/accept method, then the connection has at least begun. - // Notify delegate that it is now ending. - BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; - BOOL isDeallocating = (flags & kDealloc) ? YES : NO; - - // Clear stored socket info and all flags (config remains as is) - socketFDBytesAvailable = 0; - flags = 0; - sslWriteCachedLength = 0; - - if (shouldCallDelegate) - { - __strong id theDelegate = delegate; - __strong id theSelf = isDeallocating ? nil : self; - - if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socketDidDisconnect:theSelf withError:error]; - }}); - } - } -} + if (enableIPv4 && (port == 0)) + { + // No specific port was specified, so we allowed the OS to pick an available port for us. + // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. -- (void)disconnect -{ - dispatch_block_t block = ^{ @autoreleasepool { - - if (self->flags & kSocketStarted) - { - [self closeWithError:nil]; - } - }}; - - // Synchronous disconnection, as documented in the header file - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); -} + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; + addr6->sin6_port = htons([self localPort4]); + } -- (void)disconnectAfterReading -{ - dispatch_async(socketQueue, ^{ @autoreleasepool { - - if (self->flags & kSocketStarted) - { - self->flags |= (kForbidReadsWrites | kDisconnectAfterReads); - [self maybeClose]; - } - }}); -} + self->socket6FD = createSocket(AF_INET6, interface6); -- (void)disconnectAfterWriting -{ - dispatch_async(socketQueue, ^{ @autoreleasepool { - - if (self->flags & kSocketStarted) - { - self->flags |= (kForbidReadsWrites | kDisconnectAfterWrites); - [self maybeClose]; - } - }}); -} + if (self->socket6FD == SOCKET_NULL) + { + if (self->socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(self->socket4FD); + self->socket4FD = SOCKET_NULL; + } -- (void)disconnectAfterReadingAndWriting -{ - dispatch_async(socketQueue, ^{ @autoreleasepool { - - if (self->flags & kSocketStarted) - { - self->flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); - [self maybeClose]; - } - }}); -} + return_from_block; + } + } -/** - * Closes the socket if possible. - * That is, if all writes have completed, and we're set to disconnect after writing, - * or if all reads have completed, and we're set to disconnect after reading. -**/ -- (void)maybeClose -{ - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - BOOL shouldClose = NO; - - if (flags & kDisconnectAfterReads) - { - if (([readQueue count] == 0) && (currentRead == nil)) - { - if (flags & kDisconnectAfterWrites) - { - if (([writeQueue count] == 0) && (currentWrite == nil)) - { - shouldClose = YES; - } - } - else - { - shouldClose = YES; - } - } - } - else if (flags & kDisconnectAfterWrites) - { - if (([writeQueue count] == 0) && (currentWrite == nil)) - { - shouldClose = YES; - } - } - - if (shouldClose) - { - [self closeWithError:nil]; - } -} + // Create accept sources -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Errors -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if (enableIPv4) + { + self->accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + self->socket4FD, + 0, + self->socketQueue); -- (NSError *)badConfigError:(NSString *)errMsg -{ - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; -} + int socketFD = self->socket4FD; + dispatch_source_t acceptSource = self->accept4Source; -- (NSError *)badParamError:(NSString *)errMsg -{ - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; -} + __weak GCDAsyncSocket *weakSelf = self; -+ (NSError *)gaiError:(int)gai_error -{ - NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; -} + dispatch_source_set_event_handler(self->accept4Source, + ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" -- (NSError *)errorWithErrno:(int)err reason:(NSString *)reason -{ - NSString *errMsg = [NSString stringWithUTF8String:strerror(err)]; - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg, - NSLocalizedFailureReasonErrorKey : reason}; - - return [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:userInfo]; -} + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; -- (NSError *)errnoError -{ - NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; -} + LogVerbose(@"event4Block"); -- (NSError *)sslError:(OSStatus)ssl_error -{ - NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; - NSDictionary *userInfo = @{NSLocalizedRecoverySuggestionErrorKey : msg}; - - return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; -} + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); -- (NSError *)connectTimeoutError -{ - NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", - @"GCDAsyncSocket", [NSBundle mainBundle], - @"Attempt to connect to host timed out", nil); - - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; -} + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); -/** - * Returns a standard AsyncSocket maxed out error. -**/ -- (NSError *)readMaxedOutError -{ - NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", - @"GCDAsyncSocket", [NSBundle mainBundle], - @"Read operation reached set maximum length", nil); - - NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; -} + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); -/** - * Returns a standard AsyncSocket write timeout error. -**/ -- (NSError *)readTimeoutError -{ - NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", - @"GCDAsyncSocket", [NSBundle mainBundle], - @"Read operation timed out", nil); - - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; -} +#pragma clang diagnostic pop + }}); -/** - * Returns a standard AsyncSocket write timeout error. -**/ -- (NSError *)writeTimeoutError -{ - NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", - @"GCDAsyncSocket", [NSBundle mainBundle], - @"Write operation timed out", nil); - - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; -} -- (NSError *)connectionClosedError -{ - NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", - @"GCDAsyncSocket", [NSBundle mainBundle], - @"Socket closed by remote peer", nil); - - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; -} + dispatch_source_set_cancel_handler(self->accept4Source, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" -- (NSError *)otherError:(NSString *)errMsg -{ - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; -} +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); +#endif -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Diagnostics -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + LogVerbose(@"close(socket4FD)"); + close(socketFD); -- (BOOL)isDisconnected -{ - __block BOOL result = NO; - - dispatch_block_t block = ^{ - result = (self->flags & kSocketStarted) ? NO : YES; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; -} +#pragma clang diagnostic pop + }); -- (BOOL)isConnected -{ - __block BOOL result = NO; - - dispatch_block_t block = ^{ - result = (self->flags & kConnected) ? YES : NO; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; -} + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(self->accept4Source); + } -- (NSString *)connectedHost -{ - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - if (socket4FD != SOCKET_NULL) - return [self connectedHostFromSocket4:socket4FD]; - if (socket6FD != SOCKET_NULL) - return [self connectedHostFromSocket6:socket6FD]; - - return nil; - } - else - { - __block NSString *result = nil; - - dispatch_sync(socketQueue, ^{ @autoreleasepool { - - if (self->socket4FD != SOCKET_NULL) - result = [self connectedHostFromSocket4:self->socket4FD]; - else if (self->socket6FD != SOCKET_NULL) - result = [self connectedHostFromSocket6:self->socket6FD]; - }}); - - return result; - } -} + if (enableIPv6) + { + self->accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + self->socket6FD, + 0, + self->socketQueue); -- (uint16_t)connectedPort -{ - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - if (socket4FD != SOCKET_NULL) - return [self connectedPortFromSocket4:socket4FD]; - if (socket6FD != SOCKET_NULL) - return [self connectedPortFromSocket6:socket6FD]; - - return 0; - } - else - { - __block uint16_t result = 0; - - dispatch_sync(socketQueue, ^{ - // No need for autorelease pool - - if (self->socket4FD != SOCKET_NULL) - result = [self connectedPortFromSocket4:self->socket4FD]; - else if (self->socket6FD != SOCKET_NULL) - result = [self connectedPortFromSocket6:self->socket6FD]; - }); - - return result; - } -} + int socketFD = self->socket6FD; + dispatch_source_t acceptSource = self->accept6Source; -- (NSURL *)connectedUrl -{ - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - if (socketUN != SOCKET_NULL) - return [self connectedUrlFromSocketUN:socketUN]; - - return nil; - } - else - { - __block NSURL *result = nil; - - dispatch_sync(socketQueue, ^{ @autoreleasepool { - - if (self->socketUN != SOCKET_NULL) - result = [self connectedUrlFromSocketUN:self->socketUN]; - }}); - - return result; - } -} + __weak GCDAsyncSocket *weakSelf = self; -- (NSString *)localHost -{ - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - if (socket4FD != SOCKET_NULL) - return [self localHostFromSocket4:socket4FD]; - if (socket6FD != SOCKET_NULL) - return [self localHostFromSocket6:socket6FD]; - - return nil; - } - else - { - __block NSString *result = nil; - - dispatch_sync(socketQueue, ^{ @autoreleasepool { - - if (self->socket4FD != SOCKET_NULL) - result = [self localHostFromSocket4:self->socket4FD]; - else if (self->socket6FD != SOCKET_NULL) - result = [self localHostFromSocket6:self->socket6FD]; - }}); - - return result; - } -} + dispatch_source_set_event_handler(self->accept6Source, + ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" -- (uint16_t)localPort -{ - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - if (socket4FD != SOCKET_NULL) - return [self localPortFromSocket4:socket4FD]; - if (socket6FD != SOCKET_NULL) - return [self localPortFromSocket6:socket6FD]; - - return 0; - } - else - { - __block uint16_t result = 0; - - dispatch_sync(socketQueue, ^{ - // No need for autorelease pool - - if (self->socket4FD != SOCKET_NULL) - result = [self localPortFromSocket4:self->socket4FD]; - else if (self->socket6FD != SOCKET_NULL) - result = [self localPortFromSocket6:self->socket6FD]; - }); - - return result; - } -} + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; -- (NSString *)connectedHost4 -{ - if (socket4FD != SOCKET_NULL) - return [self connectedHostFromSocket4:socket4FD]; - - return nil; -} + LogVerbose(@"event6Block"); -- (NSString *)connectedHost6 -{ - if (socket6FD != SOCKET_NULL) - return [self connectedHostFromSocket6:socket6FD]; - - return nil; -} + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); -- (uint16_t)connectedPort4 -{ - if (socket4FD != SOCKET_NULL) - return [self connectedPortFromSocket4:socket4FD]; - - return 0; -} + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); -- (uint16_t)connectedPort6 -{ - if (socket6FD != SOCKET_NULL) - return [self connectedPortFromSocket6:socket6FD]; - - return 0; -} + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); -- (NSString *)localHost4 -{ - if (socket4FD != SOCKET_NULL) - return [self localHostFromSocket4:socket4FD]; - - return nil; -} +#pragma clang diagnostic pop + }}); -- (NSString *)localHost6 -{ - if (socket6FD != SOCKET_NULL) - return [self localHostFromSocket6:socket6FD]; - - return nil; -} + dispatch_source_set_cancel_handler(self->accept6Source, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" -- (uint16_t)localPort4 -{ - if (socket4FD != SOCKET_NULL) - return [self localPortFromSocket4:socket4FD]; - - return 0; -} +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept6Source)"); + dispatch_release(acceptSource); +#endif -- (uint16_t)localPort6 -{ - if (socket6FD != SOCKET_NULL) - return [self localPortFromSocket6:socket6FD]; - - return 0; -} + LogVerbose(@"close(socket6FD)"); + close(socketFD); -- (NSString *)connectedHostFromSocket4:(int)socketFD -{ - struct sockaddr_in sockaddr4; - socklen_t sockaddr4len = sizeof(sockaddr4); - - if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) - { - return nil; - } - return [[self class] hostFromSockaddr4:&sockaddr4]; -} +#pragma clang diagnostic pop + }); -- (NSString *)connectedHostFromSocket6:(int)socketFD -{ - struct sockaddr_in6 sockaddr6; - socklen_t sockaddr6len = sizeof(sockaddr6); - - if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) - { - return nil; - } - return [[self class] hostFromSockaddr6:&sockaddr6]; -} + LogVerbose(@"dispatch_resume(accept6Source)"); + dispatch_resume(self->accept6Source); + } -- (uint16_t)connectedPortFromSocket4:(int)socketFD -{ - struct sockaddr_in sockaddr4; - socklen_t sockaddr4len = sizeof(sockaddr4); - - if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) - { - return 0; - } - return [[self class] portFromSockaddr4:&sockaddr4]; -} + self->flags |= kSocketStarted; -- (uint16_t)connectedPortFromSocket6:(int)socketFD -{ - struct sockaddr_in6 sockaddr6; - socklen_t sockaddr6len = sizeof(sockaddr6); - - if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) - { - return 0; - } - return [[self class] portFromSockaddr6:&sockaddr6]; -} + result = YES; + }}; -- (NSURL *)connectedUrlFromSocketUN:(int)socketFD -{ - struct sockaddr_un sockaddr; - socklen_t sockaddrlen = sizeof(sockaddr); - - if (getpeername(socketFD, (struct sockaddr *)&sockaddr, &sockaddrlen) < 0) - { - return 0; - } - return [[self class] urlFromSockaddrUN:&sockaddr]; -} + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); -- (NSString *)localHostFromSocket4:(int)socketFD -{ - struct sockaddr_in sockaddr4; - socklen_t sockaddr4len = sizeof(sockaddr4); - - if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) - { - return nil; - } - return [[self class] hostFromSockaddr4:&sockaddr4]; -} + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); -- (NSString *)localHostFromSocket6:(int)socketFD -{ - struct sockaddr_in6 sockaddr6; - socklen_t sockaddr6len = sizeof(sockaddr6); - - if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) - { - return nil; - } - return [[self class] hostFromSockaddr6:&sockaddr6]; + if (errPtr) + *errPtr = err; + } + + return result; } -- (uint16_t)localPortFromSocket4:(int)socketFD +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr { - struct sockaddr_in sockaddr4; - socklen_t sockaddr4len = sizeof(sockaddr4); - - if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) - { - return 0; - } - return [[self class] portFromSockaddr4:&sockaddr4]; + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errorWithErrno:errno reason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, + SOL_SOCKET, + SO_REUSEADDR, + &reuseOn, + sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, + (const struct sockaddr *)[interfaceAddr bytes], + (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errorWithErrno:errno reason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (self->delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (self->delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [self->readQueue removeAllObjects]; + [self->writeQueue removeAllObjects]; + + // Remove a previous socket + + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *urlPath = url.path; + if (urlPath && [fileManager fileExistsAtPath:urlPath]) { + if (![fileManager removeItemAtURL:url error:&error]) { + NSString *msg = @"Could not remove previous unix domain socket at given url."; + err = [self otherError:msg]; + + return_from_block; + } + } + + // Resolve interface from description + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + NSString *msg = @"Invalid unix domain url. Specify a valid file url that does not exist (e.g. \"file:///tmp/socket\")"; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create sockets, configure, bind, and listen + + LogVerbose(@"Creating unix domain socket"); + self->socketUN = createSocket(AF_UNIX, interface); + + if (self->socketUN == SOCKET_NULL) + { + return_from_block; + } + + self->socketUrl = url; + + // Create accept sources + + self->acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + self->socketUN, + 0, + self->socketQueue); + + int socketFD = self->socketUN; + dispatch_source_t acceptSource = self->acceptUNSource; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(self->acceptUNSource, + ^{ @autoreleasepool { + + __strong GCDAsyncSocket *strongSelf = weakSelf; + + LogVerbose(@"eventUNBlock"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + }}); + + dispatch_source_set_cancel_handler(self->acceptUNSource, ^{ + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(acceptUNSource)"); + dispatch_release(acceptSource); +#endif + + LogVerbose(@"close(socketUN)"); + close(socketFD); + }); + + LogVerbose(@"dispatch_resume(acceptUNSource)"); + dispatch_resume(self->acceptUNSource); + + self->flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; } -- (uint16_t)localPortFromSocket6:(int)socketFD +- (BOOL)doAccept:(int)parentSocketFD { - struct sockaddr_in6 sockaddr6; - socklen_t sockaddr6len = sizeof(sockaddr6); - - if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) - { - return 0; - } - return [[self class] portFromSockaddr6:&sockaddr6]; + LogTrace(); + + int socketType; + int childSocketFD; + NSData *childSocketAddress; + + if (parentSocketFD == socket4FD) + { + socketType = 0; + + struct sockaddr_in addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else if (parentSocketFD == socket6FD) + { + socketType = 1; + + struct sockaddr_in6 addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else // if (parentSocketFD == socketUN) + { + socketType = 2; + + struct sockaddr_un addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + + // Enable non-blocking IO on the socket + + int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); + LogVerbose(@"close(childSocketFD)"); + close(childSocketFD); + return NO; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(childSocketFD, + SOL_SOCKET, + SO_NOSIGPIPE, + &nosigpipe, + sizeof(nosigpipe)); + + // Notify delegate + + if (delegateQueue) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, + ^{ @autoreleasepool { + + // Query delegate for custom socket queue + + dispatch_queue_t childSocketQueue = NULL; + + if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) + { + childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress + onSocket:self]; + } + + // Create GCDAsyncSocket instance for accepted socket + + GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate + delegateQueue:self->delegateQueue + socketQueue:childSocketQueue]; + + if (socketType == 0) + acceptedSocket->socket4FD = childSocketFD; + else if (socketType == 1) + acceptedSocket->socket6FD = childSocketFD; + else + acceptedSocket->socketUN = childSocketFD; + + acceptedSocket->flags = (kSocketStarted | kConnected); + + // Setup read and write sources for accepted socket + + dispatch_async(acceptedSocket->socketQueue, + ^{ @autoreleasepool { + + [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; + }}); + + // Notify delegate + + if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) + { + [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; + } + + // Release the socket queue returned from the delegate (it was retained by acceptedSocket) +#if !OS_OBJECT_USE_OBJC + if (childSocketQueue) dispatch_release(childSocketQueue); +#endif + + // The accepted socket should have been retained by the delegate. + // Otherwise it gets properly released when exiting the block. + }}); + } + + return YES; } -- (NSData *)connectedAddress +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connection attempt. + * It is shared between the connectToHost and connectToAddress methods. + * + **/ +- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr { - __block NSData *result = nil; - - dispatch_block_t block = ^{ - if (self->socket4FD != SOCKET_NULL) - { - struct sockaddr_in sockaddr4; - socklen_t sockaddr4len = sizeof(sockaddr4); - - if (getpeername(self->socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) - { - result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; - } - } - - if (self->socket6FD != SOCKET_NULL) - { - struct sockaddr_in6 sockaddr6; - socklen_t sockaddr6len = sizeof(sockaddr6); - - if (getpeername(self->socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) - { - result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; - } - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (interface) + { + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; + + if ((interface4 == nil) && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterface4 = interface4; + connectInterface6 = interface6; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; } -- (NSData *)localAddress +- (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr { - __block NSData *result = nil; - - dispatch_block_t block = ^{ - if (self->socket4FD != SOCKET_NULL) - { - struct sockaddr_in sockaddr4; - socklen_t sockaddr4len = sizeof(sockaddr4); - - if (getsockname(self->socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) - { - result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; - } - } - - if (self->socket6FD != SOCKET_NULL) - { - struct sockaddr_in6 sockaddr6; - socklen_t sockaddr6len = sizeof(sockaddr6); - - if (getsockname(self->socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) - { - result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; - } - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterfaceUN = interface; + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; } -- (BOOL)isIPv4 +- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return (socket4FD != SOCKET_NULL); - } - else - { - __block BOOL result = NO; - - dispatch_sync(socketQueue, ^{ - result = (self->socket4FD != SOCKET_NULL); - }); - - return result; - } + return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; } -- (BOOL)isIPv6 +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return (socket6FD != SOCKET_NULL); - } - else - { - __block BOOL result = NO; - - dispatch_sync(socketQueue, ^{ - result = (self->socket6FD != SOCKET_NULL); - }); - - return result; - } + return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; } -- (BOOL)isSecure +- (BOOL)connectToHost:(NSString *)inHost + onPort:(uint16_t)port + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return (flags & kSocketSecure) ? YES : NO; - } - else - { - __block BOOL result; - - dispatch_sync(socketQueue, ^{ - result = (self->flags & kSocketSecure) ? YES : NO; - }); - - return result; - } -} + LogTrace(); -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Utilities -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Just in case immutable objects were passed + NSString *host = [inHost copy]; + NSString *interface = [inInterface copy]; -/** + __block BOOL result = NO; + __block NSError *preConnectErr = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([host length] == 0) + { + NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; + preConnectErr = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&preConnectErr]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + self->flags |= kSocketStarted; + + LogVerbose(@"Dispatching DNS lookup..."); + + // It's possible that the given host parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *hostCpy = [host copy]; + + int aStateIndex = self->stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, + 0); + dispatch_async(globalConcurrentQueue, + ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + NSError *lookupErr = nil; + NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (lookupErr) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didFail:lookupErr]; + }}); + } + else + { + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + if (!address4 && [[self class] isIPv4Address:address]) + { + address4 = address; + } + else if (!address6 && [[self class] isIPv6Address:address]) + { + address6 = address; + } + } + + dispatch_async(strongSelf->socketQueue, + ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; + }}); + } + +#pragma clang diagnostic pop + }}); + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + + if (errPtr) *errPtr = preConnectErr; + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)inRemoteAddr + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSData *remoteAddr = [inRemoteAddr copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with remoteAddr parameter + + NSData *address4 = nil; + NSData *address6 = nil; + + if ([remoteAddr length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; + + if (sockaddr->sa_family == AF_INET) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in)) + { + address4 = remoteAddr; + } + } + else if (sockaddr->sa_family == AF_INET6) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in6)) + { + address6 = remoteAddr; + } + } + } + + if ((address4 == nil) && (address6 == nil)) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address4 != nil)) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (address6 != nil)) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + return_from_block; + } + + self->flags |= kSocketStarted; + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([url.path length] == 0) + { + NSString *msg = @"Invalid unix domain socket url."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithUrl:url error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + self->flags |= kSocketStarted; + + // Start the normal connection process + + NSError *connectError = nil; + if (![self connectWithAddressUN:self->connectInterfaceUN error:&connectError]) + { + [self closeWithError:connectError]; + + return_from_block; + } + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert(address4 || address6, @"Expected at least one valid address"); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + // Check for problems + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + if (isIPv6Disabled && (address4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + // Start the normal connection process + + NSError *err = nil; + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + [self closeWithError:err]; + } +} + +/** + * This method is called if the DNS lookup fails. + * This method is executed on the socketQueue. + * + * Since the DNS lookup executed synchronously on a global concurrent queue, + * the original connection request may have already been cancelled or timed-out by the time this method is invoked. + * The lookupIndex tells us whether the lookup is still valid or not. + **/ +- (void)lookup:(int)aStateIndex didFail:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookup:didFail: - already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self endConnectTimeout]; + [self closeWithError:error]; +} + +- (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr +{ + // Bind the socket to the desired interface (if needed) + + if (connectInterface) + { + LogVerbose(@"Binding socket..."); + + if ([[self class] portFromAddress:connectInterface] > 0) + { + // Since we're going to be binding to a specific port, + // we should turn on reuseaddr to allow us to override sockets in time_wait. + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + } + + const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; + + int result = bind(socketFD, + interfaceAddr, + (socklen_t)[connectInterface length]); + if (result != 0) + { + if (errPtr) + *errPtr = [self errorWithErrno:errno reason:@"Error in bind() function"]; + + return NO; + } + } + + return YES; +} + +- (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr +{ + int socketFD = socket(family, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; + + return socketFD; + } + + if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) + { + [self closeSocket:socketFD]; + + return SOCKET_NULL; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + return socketFD; +} + +- (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex +{ + // If there already is a socket connected, we close socketFD and return + if (self.isConnected) + { + [self closeSocket:socketFD]; + return; + } + + // Start the connection process in a background queue + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, + 0); + dispatch_async(globalConcurrentQueue, + ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + int result = connect(socketFD, + (const struct sockaddr *)[address bytes], + (socklen_t)[address length]); + int err = errno; + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + dispatch_async(strongSelf->socketQueue, + ^{ @autoreleasepool { + + if (strongSelf.isConnected) + { + [strongSelf closeSocket:socketFD]; + return_from_block; + } + + if (result == 0) + { + [self closeUnusedSocket:socketFD]; + + [strongSelf didConnect:aStateIndex]; + } + else + { + [strongSelf closeSocket:socketFD]; + + // If there are no more sockets trying to connect, we inform the error to the delegate + if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL) + { + NSError *error = [strongSelf errorWithErrno:err reason:@"Error in connect() function"]; + [strongSelf didNotConnect:aStateIndex error:error]; + } + } + }}); + +#pragma clang diagnostic pop + }); + + LogVerbose(@"Connecting..."); +} + +- (void)closeSocket:(int)socketFD +{ + if (socketFD != SOCKET_NULL && + (socketFD == socket6FD || socketFD == socket4FD)) + { + close(socketFD); + + if (socketFD == socket4FD) + { + LogVerbose(@"close(socket4FD)"); + socket4FD = SOCKET_NULL; + } + else if (socketFD == socket6FD) + { + LogVerbose(@"close(socket6FD)"); + socket6FD = SOCKET_NULL; + } + } +} + +- (void)closeUnusedSocket:(int)usedSocketFD +{ + if (usedSocketFD != socket4FD) + { + [self closeSocket:socket4FD]; + } + else if (usedSocketFD != socket6FD) + { + [self closeSocket:socket6FD]; + } +} + +- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + LogVerbose(@"IPv4: %@:%hu", + [[self class] hostFromAddress:address4], + [[self class] portFromAddress:address4]); + LogVerbose(@"IPv6: %@:%hu", + [[self class] hostFromAddress:address6], + [[self class] portFromAddress:address6]); + + // Determine socket type + + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + // Create and bind the sockets + + if (address4) + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; + } + + if (address6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; + } + + if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) + { + return NO; + } + + int socketFD, alternateSocketFD; + NSData *address, *alternateAddress; + + if ((preferIPv6 && socket6FD != SOCKET_NULL) || socket4FD == SOCKET_NULL) + { + socketFD = socket6FD; + alternateSocketFD = socket4FD; + address = address6; + alternateAddress = address4; + } + else + { + socketFD = socket4FD; + alternateSocketFD = socket6FD; + address = address4; + alternateAddress = address6; + } + + int aStateIndex = stateIndex; + + [self connectSocket:socketFD address:address stateIndex:aStateIndex]; + + if (alternateAddress) + { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), + socketQueue, + ^{ + [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; + }); + } + + return YES; +} + +- (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + // Create the socket + + int socketFD; + + LogVerbose(@"Creating unix domain socket"); + + socketUN = socket(AF_UNIX, SOCK_STREAM, 0); + + socketFD = socketUN; + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + LogVerbose(@"Binding socket..."); + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + + // const struct sockaddr *interfaceAddr = (const struct sockaddr *)[address bytes]; + // + // int result = bind(socketFD, interfaceAddr, (socklen_t)[address length]); + // if (result != 0) + // { + // if (errPtr) + // *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; + // + // return NO; + // } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aStateIndex = stateIndex; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, + 0); + dispatch_async(globalConcurrentQueue, + ^{ + + const struct sockaddr *addr = (const struct sockaddr *)[address bytes]; + int result = connect(socketFD, addr, addr->sa_len); + if (result == 0) + { + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self didConnect:aStateIndex]; + }}); + } + else + { + // TODO: Bad file descriptor + perror("connect"); + NSError *error = [self errorWithErrno:errno reason:@"Error in connect() function"]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self didNotConnect:aStateIndex error:error]; + }}); + } + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (void)didConnect:(int)aStateIndex +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + flags |= kConnected; + + [self endConnectTimeout]; + +#if TARGET_OS_IPHONE + // The endConnectTimeout method executed above incremented the stateIndex. + aStateIndex = stateIndex; +#endif + + // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) + // + // Note: + // There may be configuration options that must be set by the delegate before opening the streams. + // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. + // + // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. + // This gives the delegate time to properly configure the streams if needed. + + dispatch_block_t SetupStreamsPart1 = ^{ +#if TARGET_OS_IPHONE + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:NO]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + +#endif + }; + dispatch_block_t SetupStreamsPart2 = ^{ +#if TARGET_OS_IPHONE + + if (aStateIndex != self->stateIndex) + { + // The socket has been disconnected. + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + +#endif + }; + + // Notify delegate + + NSString *host = [self connectedHost]; + uint16_t port = [self connectedPort]; + NSURL *url = [self connectedUrl]; + + __strong id theDelegate = delegate; + + if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToHost:host port:port]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToUrl:url]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else + { + SetupStreamsPart1(); + SetupStreamsPart2(); + } + + // Get the connected socket + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + // Enable non-blocking IO on the socket + + int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; + [self closeWithError:[self otherError:errMsg]]; + + return; + } + + // Setup our read/write sources + + [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; + + // Dequeue any pending read/write requests + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; +} + +- (void)didNotConnect:(int)aStateIndex error:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didNotConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self closeWithError:error]; +} + +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, + 0, + 0, + socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doConnectTimeout]; + +#pragma clang diagnostic pop + }}); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(connectTimer)"); + dispatch_release(theConnectTimer); + +#pragma clang diagnostic pop + }); +#endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +- (void)endConnectTimeout +{ + LogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } + + // Increment stateIndex. + // This will prevent us from processing results from any related background asynchronous operations. + // + // Note: This should be called from close method even if connectTimer is NULL. + // This is because one might disconnect a socket prior to a successful connection which had no timeout. + + stateIndex++; + + if (connectInterface4) + { + connectInterface4 = nil; + } + if (connectInterface6) + { + connectInterface6 = nil; + } +} + +- (void)doConnectTimeout +{ + LogTrace(); + + [self endConnectTimeout]; + [self closeWithError:[self connectTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + [self endConnectTimeout]; + + if (currentRead != nil) [self endCurrentRead]; + if (currentWrite != nil) [self endCurrentWrite]; + + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + [preBuffer reset]; + +#if TARGET_OS_IPHONE + { + if (readStream || writeStream) + { + [self removeStreamsFromRunLoop]; + + if (readStream) + { + CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + } + } +#endif + + [sslPreBuffer reset]; + sslErrCode = lastSSLHandshakeError = noErr; + + if (sslContext) + { + // Getting a linker error here about the SSLx() functions? + // You need to add the Security Framework to your application. + + SSLClose(sslContext); + +#if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + CFRelease(sslContext); +#else + SSLDisposeContext(sslContext); +#endif + + sslContext = NULL; + } + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource) + { + LogVerbose(@"manually closing close"); + + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"close(socket6FD)"); + close(socket6FD); + socket6FD = SOCKET_NULL; + } + + if (socketUN != SOCKET_NULL) + { + LogVerbose(@"close(socketUN)"); + close(socketUN); + socketUN = SOCKET_NULL; + unlink(socketUrl.path.fileSystemRepresentation); + socketUrl = nil; + } + } + else + { + if (accept4Source) + { + LogVerbose(@"dispatch_source_cancel(accept4Source)"); + dispatch_source_cancel(accept4Source); + + // We never suspend accept4Source + + accept4Source = NULL; + } + + if (accept6Source) + { + LogVerbose(@"dispatch_source_cancel(accept6Source)"); + dispatch_source_cancel(accept6Source); + + // We never suspend accept6Source + + accept6Source = NULL; + } + + if (acceptUNSource) + { + LogVerbose(@"dispatch_source_cancel(acceptUNSource)"); + dispatch_source_cancel(acceptUNSource); + + // We never suspend acceptUNSource + + acceptUNSource = NULL; + } + + if (readSource) + { + LogVerbose(@"dispatch_source_cancel(readSource)"); + dispatch_source_cancel(readSource); + + [self resumeReadSource]; + + readSource = NULL; + } + + if (writeSource) + { + LogVerbose(@"dispatch_source_cancel(writeSource)"); + dispatch_source_cancel(writeSource); + + [self resumeWriteSource]; + + writeSource = NULL; + } + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + } + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; + BOOL isDeallocating = (flags & kDealloc) ? YES : NO; + + // Clear stored socket info and all flags (config remains as is) + socketFDBytesAvailable = 0; + flags = 0; + sslWriteCachedLength = 0; + + if (shouldCallDelegate) + { + __strong id theDelegate = delegate; + __strong id theSelf = isDeallocating ? nil : self; + + if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidDisconnect:theSelf withError:error]; + }}); + } + } +} + +- (void)disconnect +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (self->flags & kSocketStarted) + { + [self closeWithError:nil]; + } + }}; + + // Synchronous disconnection, as documented in the header file + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)disconnectAfterReading +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (self->flags & kSocketStarted) + { + self->flags |= (kForbidReadsWrites | kDisconnectAfterReads); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (self->flags & kSocketStarted) + { + self->flags |= (kForbidReadsWrites | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterReadingAndWriting +{ + dispatch_async(socketQueue, + ^{ @autoreleasepool { + + if (self->flags & kSocketStarted) + { + self->flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +/** + * Closes the socket if possible. + * That is, if all writes have completed, and we're set to disconnect after writing, + * or if all reads have completed, and we're set to disconnect after reading. + **/ +- (void)maybeClose +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + BOOL shouldClose = NO; + + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + else + { + shouldClose = YES; + } + } + } + else if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + + if (shouldClose) + { + [self closeWithError:nil]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; +} + ++ (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errorWithErrno:(int)err reason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(err)]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg, + NSLocalizedFailureReasonErrorKey : reason}; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)sslError:(OSStatus)ssl_error +{ + NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; + NSDictionary *userInfo = @{NSLocalizedRecoverySuggestionErrorKey : msg}; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; +} + +- (NSError *)connectTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", + @"GCDAsyncSocket", + [NSBundle mainBundle], + @"Attempt to connect to host timed out", + nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket maxed out error. + **/ +- (NSError *)readMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", + @"GCDAsyncSocket", + [NSBundle mainBundle], + @"Read operation reached set maximum length", + nil); + + NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. + **/ +- (NSError *)readTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", + @"GCDAsyncSocket", + [NSBundle mainBundle], + @"Read operation timed out", + nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket write timeout error. + **/ +- (NSError *)writeTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", + @"GCDAsyncSocket", + [NSBundle mainBundle], + @"Write operation timed out", + nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; +} + +- (NSError *)connectionClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", + @"GCDAsyncSocket", + [NSBundle mainBundle], + @"Socket closed by remote peer", + nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->flags & kSocketStarted) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->flags & kConnected) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSString *)connectedHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (self->socket4FD != SOCKET_NULL) + result = [self connectedHostFromSocket4:self->socket4FD]; + else if (self->socket6FD != SOCKET_NULL) + result = [self connectedHostFromSocket6:self->socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)connectedPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (self->socket4FD != SOCKET_NULL) + result = [self connectedPortFromSocket4:self->socket4FD]; + else if (self->socket6FD != SOCKET_NULL) + result = [self connectedPortFromSocket6:self->socket6FD]; + }); + + return result; + } +} + +- (NSURL *)connectedUrl +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socketUN != SOCKET_NULL) + return [self connectedUrlFromSocketUN:socketUN]; + + return nil; + } + else + { + __block NSURL *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (self->socketUN != SOCKET_NULL) + result = [self connectedUrlFromSocketUN:self->socketUN]; + }}); + + return result; + } +} + +- (NSString *)localHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (self->socket4FD != SOCKET_NULL) + result = [self localHostFromSocket4:self->socket4FD]; + else if (self->socket6FD != SOCKET_NULL) + result = [self localHostFromSocket6:self->socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)localPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (self->socket4FD != SOCKET_NULL) + result = [self localPortFromSocket4:self->socket4FD]; + else if (self->socket6FD != SOCKET_NULL) + result = [self localPortFromSocket6:self->socket6FD]; + }); + + return result; + } +} + +- (NSString *)connectedHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)connectedHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)connectedPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)connectedPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)localHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)localHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)localPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)localPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)connectedHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)connectedHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)connectedPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)connectedPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSURL *)connectedUrlFromSocketUN:(int)socketFD +{ + struct sockaddr_un sockaddr; + socklen_t sockaddrlen = sizeof(sockaddr); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr, &sockaddrlen) < 0) + { + return 0; + } + return [[self class] urlFromSockaddrUN:&sockaddr]; +} + +- (NSString *)localHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)localHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)localPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)localPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (self->socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(self->socket4FD, + (struct sockaddr *)&sockaddr4, + &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (self->socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(self->socket6FD, + (struct sockaddr *)&sockaddr6, + &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (self->socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(self->socket4FD, + (struct sockaddr *)&sockaddr4, + &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (self->socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(self->socket6FD, + (struct sockaddr *)&sockaddr6, + &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket4FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (self->socket4FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isIPv6 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket6FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (self->socket6FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isSecure +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (flags & kSocketSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = (self->flags & kSocketSecure) ? YES : NO; + }); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** * Finds the address of an interface description. * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). - * + * * The interface description may optionally contain a port number at the end, separated by a colon. * If a non-zero port parameter is provided, any port number in the interface description is ignored. - * + * * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. -**/ + **/ - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr address6:(NSMutableData **)interfaceAddr6Ptr fromDescription:(NSString *)interfaceDescription port:(uint16_t)port { - NSMutableData *addr4 = nil; - NSMutableData *addr6 = nil; - - NSString *interface = nil; - - NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; - if ([components count] > 0) - { - NSString *temp = [components objectAtIndex:0]; - if ([temp length] > 0) - { - interface = temp; - } - } - if ([components count] > 1 && port == 0) - { - NSString *temp = [components objectAtIndex:1]; - long portL = strtol([temp UTF8String], NULL, 10); - - if (portL > 0 && portL <= UINT16_MAX) - { - port = (uint16_t)portL; - } - } - - if (interface == nil) - { - // ANY address - - struct sockaddr_in sockaddr4; - memset(&sockaddr4, 0, sizeof(sockaddr4)); - - sockaddr4.sin_len = sizeof(sockaddr4); - sockaddr4.sin_family = AF_INET; - sockaddr4.sin_port = htons(port); - sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); - - struct sockaddr_in6 sockaddr6; - memset(&sockaddr6, 0, sizeof(sockaddr6)); - - sockaddr6.sin6_len = sizeof(sockaddr6); - sockaddr6.sin6_family = AF_INET6; - sockaddr6.sin6_port = htons(port); - sockaddr6.sin6_addr = in6addr_any; - - addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; - addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; - } - else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) - { - // LOOPBACK address - - struct sockaddr_in sockaddr4; - memset(&sockaddr4, 0, sizeof(sockaddr4)); - - sockaddr4.sin_len = sizeof(sockaddr4); - sockaddr4.sin_family = AF_INET; - sockaddr4.sin_port = htons(port); - sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - - struct sockaddr_in6 sockaddr6; - memset(&sockaddr6, 0, sizeof(sockaddr6)); - - sockaddr6.sin6_len = sizeof(sockaddr6); - sockaddr6.sin6_family = AF_INET6; - sockaddr6.sin6_port = htons(port); - sockaddr6.sin6_addr = in6addr_loopback; - - addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; - addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; - } - else - { - const char *iface = [interface UTF8String]; - - struct ifaddrs *addrs; - const struct ifaddrs *cursor; - - if ((getifaddrs(&addrs) == 0)) - { - cursor = addrs; - while (cursor != NULL) - { - if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) - { - // IPv4 - - struct sockaddr_in nativeAddr4; - memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); - - if (strcmp(cursor->ifa_name, iface) == 0) - { - // Name match - - nativeAddr4.sin_port = htons(port); - - addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; - } - else - { - char ip[INET_ADDRSTRLEN]; - - const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); - - if ((conversion != NULL) && (strcmp(ip, iface) == 0)) - { - // IP match - - nativeAddr4.sin_port = htons(port); - - addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; - } - } - } - else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) - { - // IPv6 - - struct sockaddr_in6 nativeAddr6; - memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); - - if (strcmp(cursor->ifa_name, iface) == 0) - { - // Name match - - nativeAddr6.sin6_port = htons(port); - - addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; - } - else - { - char ip[INET6_ADDRSTRLEN]; - - const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); - - if ((conversion != NULL) && (strcmp(ip, iface) == 0)) - { - // IP match - - nativeAddr6.sin6_port = htons(port); - - addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; - } - } - } - - cursor = cursor->ifa_next; - } - - freeifaddrs(addrs); - } - } - - if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; - if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; + NSMutableData *addr4 = nil; + NSMutableData *addr6 = nil; + + NSString *interface = nil; + + NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; + if ([components count] > 0) + { + NSString *temp = [components objectAtIndex:0]; + if ([temp length] > 0) + { + interface = temp; + } + } + if ([components count] > 1 && port == 0) + { + NSString *temp = [components objectAtIndex:1]; + long portL = strtol([temp UTF8String], NULL, 10); + + if (portL > 0 && portL <= UINT16_MAX) + { + port = (uint16_t)portL; + } + } + + if (interface == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interface UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in nativeAddr4; + memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET, + &nativeAddr4.sin_addr, + ip, + sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + struct sockaddr_in6 nativeAddr6; + memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET6, + &nativeAddr6.sin6_addr, + ip, + sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +- (NSData *)getInterfaceAddressFromUrl:(NSURL *)url +{ + NSString *path = url.path; + if (path.length == 0) { + return nil; + } + + struct sockaddr_un nativeAddr; + nativeAddr.sun_family = AF_UNIX; + strlcpy(nativeAddr.sun_path, + path.fileSystemRepresentation, + sizeof(nativeAddr.sun_path)); + nativeAddr.sun_len = (unsigned char)SUN_LEN(&nativeAddr); + NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)]; + + return interface; +} + +- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD +{ + readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + socketFD, + 0, + socketQueue); + writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, + socketFD, + 0, + socketQueue); + + // Setup event handlers + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readSource, + ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"readEventBlock"); + + strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); + LogVerbose(@"socketFDBytesAvailable: %lu", + strongSelf->socketFDBytesAvailable); + + if (strongSelf->socketFDBytesAvailable > 0) + [strongSelf doReadData]; + else + [strongSelf doReadEOF]; + +#pragma clang diagnostic pop + }}); + + dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"writeEventBlock"); + + strongSelf->flags |= kSocketCanAcceptBytes; + [strongSelf doWriteData]; + +#pragma clang diagnostic pop + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadSource = readSource; + dispatch_source_t theWriteSource = writeSource; +#endif + + dispatch_source_set_cancel_handler(readSource, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"readCancelBlock"); + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(readSource)"); + dispatch_release(theReadSource); +#endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + +#pragma clang diagnostic pop + }); + + dispatch_source_set_cancel_handler(writeSource, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"writeCancelBlock"); + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(writeSource)"); + dispatch_release(theWriteSource); +#endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + +#pragma clang diagnostic pop + }); + + // We will not be able to read until data arrives. + // But we should be able to write immediately. + + socketFDBytesAvailable = 0; + flags &= ~kReadSourceSuspended; + + LogVerbose(@"dispatch_resume(readSource)"); + dispatch_resume(readSource); + + flags |= kSocketCanAcceptBytes; + flags |= kWriteSourceSuspended; +} + +- (BOOL)usingCFStreamForTLS +{ +#if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return YES; + } + +#endif + + return NO; +} + +- (BOOL)usingSecureTransportForTLS +{ + // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) + +#if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return NO; + } + +#endif + + return YES; +} + +- (void)suspendReadSource +{ + if (!(flags & kReadSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(readSource)"); + + dispatch_suspend(readSource); + flags |= kReadSourceSuspended; + } +} + +- (void)resumeReadSource +{ + if (flags & kReadSourceSuspended) + { + LogVerbose(@"dispatch_resume(readSource)"); + + dispatch_resume(readSource); + flags &= ~kReadSourceSuspended; + } +} + +- (void)suspendWriteSource +{ + if (!(flags & kWriteSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(writeSource)"); + + dispatch_suspend(writeSource); + flags |= kWriteSourceSuspended; + } +} + +- (void)resumeWriteSource +{ + if (flags & kWriteSourceSuspended) + { + LogVerbose(@"dispatch_resume(writeSource)"); + + dispatch_resume(writeSource); + flags &= ~kWriteSourceSuspended; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) + { + [self->readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + if (length == 0) { + LogWarn(@"Cannot read: length == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + readLength:length + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) + { + [self->readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)maxLength + tag:(long)tag +{ + if ([data length] == 0) { + LogWarn(@"Cannot read: [data length] == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + if (maxLength > 0 && maxLength < [data length]) { + LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:maxLength + timeout:timeout + readLength:0 + terminator:data + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) + { + [self->readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!self->currentRead || ![self->currentRead isKindOfClass:[GCDAsyncReadPacket class]]) + { + // We're not reading anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + // It's only possible to know the progress of our read if we're reading to a certain length. + // If we're reading to data, we of course have no idea when the data will arrive. + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + + NSUInteger done = self->currentRead->bytesDone; + NSUInteger total = self->currentRead->readLength; + + if (tagPtr != NULL) *tagPtr = self->currentRead->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + if (total > 0) + result = (float)done / (float)total; + else + result = 1.0F; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * This method starts a new read, if needed. + * + * It is called when: + * - a user requests a read + * - after a read request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. + **/ +- (void)maybeDequeueRead +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + // If we're not currently processing a read AND we have an available read stream + if ((currentRead == nil) && (flags & kConnected)) + { + if ([readQueue count] > 0) + { + // Dequeue the next object in the write queue + currentRead = [readQueue objectAtIndex:0]; + [readQueue removeObjectAtIndex:0]; + + + if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingReadTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncReadPacket"); + + // Setup read timer (if needed) + [self setupReadTimerWithTimeout:currentRead->timeout]; + + // Immediately read, if possible + [self doReadData]; + } + } + else if (flags & kDisconnectAfterReads) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + else if (flags & kSocketSecure) + { + [self flushSSLBuffers]; + + // Edge case: + // + // We just drained all data from the ssl buffers, + // and all known data from the socket (socketFDBytesAvailable). + // + // If we didn't get any data from this process, + // then we may have reached the end of the TCP stream. + // + // Be sure callbacks are enabled so we're notified about a disconnection. + + if ([preBuffer availableBytes] == 0) + { + if ([self usingCFStreamForTLS]) { + // Callbacks never disabled + } + else { + [self resumeReadSource]; + } + } + } + } +} + +- (void)flushSSLBuffers +{ + LogTrace(); + + NSAssert((flags & kSocketSecure), + @"Cannot flush ssl buffers on non-secure socket"); + + if ([preBuffer availableBytes] > 0) + { + // Only flush the ssl buffers if the prebuffer is empty. + // This is to avoid growing the prebuffer inifinitely large. + + return; + } + +#if TARGET_OS_IPHONE + + if ([self usingCFStreamForTLS]) + { + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + CFIndex defaultBytesToRead = (1024 * 4); + + [preBuffer ensureCapacityForWrite:defaultBytesToRead]; + + uint8_t *buffer = [preBuffer writeBuffer]; + + CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); + LogVerbose(@"%@ - CFReadStreamRead(): result = %i", + THIS_METHOD, + (int)result); + + if (result > 0) + { + [preBuffer didWrite:result]; + } + + flags &= ~kSecureSocketHasBytesAvailable; + } + + return; + } + +#endif + + __block NSUInteger estimatedBytesAvailable = 0; + + dispatch_block_t updateEstimatedBytesAvailable = ^{ + + // Figure out if there is any data available to be read + // + // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket + // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket + // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered + // + // We call the variable "estimated" because we don't know how many decrypted bytes we'll get + // from the encrypted bytes in the sslPreBuffer. + // However, we do know this is an upper bound on the estimation. + + estimatedBytesAvailable = self->socketFDBytesAvailable + [self->sslPreBuffer availableBytes]; + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(self->sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + }; + + updateEstimatedBytesAvailable(); + + if (estimatedBytesAvailable > 0) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + BOOL done = NO; + do + { + LogVerbose(@"%@ - estimatedBytesAvailable = %lu", + THIS_METHOD, + (unsigned long)estimatedBytesAvailable); + + // Make sure there's enough room in the prebuffer + + [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; + + // Read data into prebuffer + + uint8_t *buffer = [preBuffer writeBuffer]; + size_t bytesRead = 0; + + OSStatus result = SSLRead(sslContext, + buffer, + (size_t)estimatedBytesAvailable, + &bytesRead); + LogVerbose(@"%@ - read from secure socket = %u", + THIS_METHOD, + (unsigned)bytesRead); + + if (bytesRead > 0) + { + [preBuffer didWrite:bytesRead]; + } + + LogVerbose(@"%@ - prebuffer.length = %zu", + THIS_METHOD, + [preBuffer availableBytes]); + + if (result != noErr) + { + done = YES; + } + else + { + updateEstimatedBytesAvailable(); + } + + } while (!done && estimatedBytesAvailable > 0); + } +} + +- (void)doReadData +{ + LogTrace(); + + // This method is called on the socketQueue. + // It might be called directly, or via the readSource when data is available to be read. + + if ((currentRead == nil) || (flags & kReadsPaused)) + { + LogVerbose(@"No currentRead or kReadsPaused"); + + // Unable to read at this time + + if (flags & kSocketSecure) + { + // Here's the situation: + // + // We have an established secure connection. + // There may not be a currentRead, but there might be encrypted data sitting around for us. + // When the user does get around to issuing a read, that encrypted data will need to be decrypted. + // + // So why make the user wait? + // We might as well get a head start on decrypting some data now. + // + // The other reason we do this has to do with detecting a socket disconnection. + // The SSL/TLS protocol has it's own disconnection handshake. + // So when a secure socket is closed, a "goodbye" packet comes across the wire. + // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. + + [self flushSSLBuffers]; + } + + if ([self usingCFStreamForTLS]) + { + // CFReadStream only fires once when there is available data. + // It won't fire again until we've invoked CFReadStreamRead. + } + else + { + // If the readSource is firing, we need to pause it + // or else it will continue to fire over and over again. + // + // If the readSource is not firing, + // we want it to continue monitoring the socket. + + if (socketFDBytesAvailable > 0) + { + [self suspendReadSource]; + } + } + return; + } + + BOOL hasBytesAvailable = NO; + unsigned long estimatedBytesAvailable = 0; + + if ([self usingCFStreamForTLS]) + { +#if TARGET_OS_IPHONE + + // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) + + estimatedBytesAvailable = 0; + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + hasBytesAvailable = YES; + else + hasBytesAvailable = NO; + +#endif + } + else + { + estimatedBytesAvailable = socketFDBytesAvailable; + + if (flags & kSocketSecure) + { + // There are 2 buffers to be aware of here. + // + // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. + // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. + // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. + // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. + // + // The first buffer is one we create. + // SecureTransport often requests small amounts of data. + // This has to do with the encypted packets that are coming across the TCP stream. + // But it's non-optimal to do a bunch of small reads from the BSD socket. + // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) + // and may store excess in the sslPreBuffer. + + estimatedBytesAvailable += [sslPreBuffer availableBytes]; + + // The second buffer is within SecureTransport. + // As mentioned earlier, there are encrypted packets coming across the TCP stream. + // SecureTransport needs the entire packet to decrypt it. + // But if the entire packet produces X bytes of decrypted data, + // and we only asked SecureTransport for X/2 bytes of data, + // it must store the extra X/2 bytes of decrypted data for the next read. + // + // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. + // From the documentation: + // + // "This function does not block or cause any low-level read operations to occur." + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + } + + hasBytesAvailable = (estimatedBytesAvailable > 0); + } + + if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) + { + LogVerbose(@"No data available to read..."); + + // No data available to read. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + } + return; + } + + if (flags & kStartingReadTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The readQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingWriteTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for incoming data which has just arrived. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the writeQueue to drain and start the SSL/TLS process. + // We now know data is available to read. + + if (![self usingCFStreamForTLS]) + { + // Suspend the read source or else it will continue to fire nonstop. + + [self suspendReadSource]; + } + } + + return; + } + + BOOL done = NO; // Completed read operation + NSError *error = nil; // Error occurred + + NSUInteger totalBytesReadForCurrentRead = 0; + + // + // STEP 1 - READ FROM PREBUFFER + // + + if ([preBuffer availableBytes] > 0) + { + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + NSUInteger bytesToCopy; + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + } + else + { + // Read type #1 or #2 + + bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; + } + + // Make sure we have enough room in the buffer for our read. + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into packet buffer + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(buffer, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the preBuffer + [preBuffer didRead:bytesToCopy]; + + LogVerbose(@"copied(%lu) preBufferLength(%zu)", + (unsigned long)bytesToCopy, + [preBuffer availableBytes]); + + // Update totals + + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + // + // We're done as soon as + // - we've read all available data (in prebuffer and socket) + // - we've read the maxLength of read packet. + + done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); + } + + } + + // + // STEP 2 - READ FROM SOCKET + // + + BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) + BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more + + if (!done && !error && !socketEOF && hasBytesAvailable) + { + NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); + + BOOL readIntoPreBuffer = NO; + uint8_t *buffer = NULL; + size_t bytesRead = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { +#if TARGET_OS_IPHONE + + // Using CFStream, rather than SecureTransport, for TLS + + NSUInteger defaultReadLength = (1024 * 32); + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + CFIndex result = CFReadStreamRead(readStream, + buffer, + (CFIndex)bytesToRead); + LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); + } + else if (result == 0) + { + socketEOF = YES; + } + else + { + waiting = YES; + bytesRead = (size_t)result; + } + + // We only know how many decrypted bytes were read. + // The actual number of bytes read was likely more due to the overhead of the encryption. + // So we reset our flag, and rely on the next callback to alert us of more data. + flags &= ~kSecureSocketHasBytesAvailable; + +#endif + } + else + { + // Using SecureTransport for TLS + // + // We know: + // - how many bytes are available on the socket + // - how many encrypted bytes are sitting in the sslPreBuffer + // - how many decypted bytes are sitting in the sslContext + // + // But we do NOT know: + // - how many encypted bytes are sitting in the sslContext + // + // So we play the regular game of using an upper bound instead. + + NSUInteger defaultReadLength = (1024 * 32); + + if (defaultReadLength < estimatedBytesAvailable) { + defaultReadLength = estimatedBytesAvailable + (1024 * 16); + } + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // The documentation from Apple states: + // + // "a read operation might return errSSLWouldBlock, + // indicating that less data than requested was actually transferred" + // + // However, starting around 10.7, the function will sometimes return noErr, + // even if it didn't read as much data as requested. So we need to watch out for that. + + OSStatus result; + do + { + void *loop_buffer = buffer + bytesRead; + size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; + size_t loop_bytesRead = 0; + + result = SSLRead(sslContext, + loop_buffer, + loop_bytesToRead, + &loop_bytesRead); + LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); + + bytesRead += loop_bytesRead; + + } while ((result == noErr) && (bytesRead < bytesToRead)); + + + if (result != noErr) + { + if (result == errSSLWouldBlock) + waiting = YES; + else + { + if (result == errSSLClosedGraceful || result == errSSLClosedAbort) + { + // We've reached the end of the stream. + // Handle this the same way we would an EOF from the socket. + socketEOF = YES; + sslErrCode = result; + } + else + { + error = [self sslError:result]; + } + } + // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. + // This happens when the SSLRead function is able to read some data, + // but not the entire amount we requested. + + if (bytesRead <= 0) + { + bytesRead = 0; + } + } + + // Do not modify socketFDBytesAvailable. + // It will be updated via the SSLReadFunction(). + } + } + else + { + // Normal socket operation + + NSUInteger bytesToRead; + + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable + shouldPreBuffer:&readIntoPreBuffer]; + } + else + { + // Read type #1 or #2 + + bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; + } + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); + LogVerbose(@"read from socket = %i", (int)result); + + if (result < 0) + { + if (errno == EWOULDBLOCK) + waiting = YES; + else + error = [self errorWithErrno:errno reason:@"Error in read() function"]; + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + socketEOF = YES; + socketFDBytesAvailable = 0; + } + else + { + bytesRead = result; + + if (bytesRead < bytesToRead) + { + // The read returned less data than requested. + // This means socketFDBytesAvailable was a bit off due to timing, + // because we read from the socket right when the readSource event was firing. + socketFDBytesAvailable = 0; + } + else + { + if (socketFDBytesAvailable <= bytesRead) + socketFDBytesAvailable = 0; + else + socketFDBytesAvailable -= bytesRead; + } + + if (socketFDBytesAvailable == 0) + { + waiting = YES; + } + } + } + + if (bytesRead > 0) + { + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + // + // Note: We should never be using a prebuffer when we're reading a specific length of data. + + NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + if (readIntoPreBuffer) + { + // We just read a big chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", + [preBuffer availableBytes]); + + // Search for the terminating sequence + + NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + LogVerbose(@"copying %lu bytes from preBuffer", + (unsigned long)bytesToCopy); + + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesToCopy]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Update totals + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above + } + else + { + // We just read a big chunk of data directly into the packet's buffer. + // We need to move any overflow into the prebuffer. + + NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; + + if (overflow == 0) + { + // Perfect match! + // Every byte we read stays in the read buffer, + // and the last byte we read was the last byte of the term. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = YES; + } + else if (overflow > 0) + { + // The term was found within the data that we read, + // and there are extra bytes that extend past the end of the term. + // We need to move these excess bytes out of the read packet and into the prebuffer. + + NSInteger underflow = bytesRead - overflow; + + // Copy excess data into preBuffer + + LogVerbose(@"copying %ld overflow bytes into preBuffer", + (long)overflow); + [preBuffer ensureCapacityForWrite:overflow]; + + uint8_t *overflowBuffer = buffer + underflow; + memcpy([preBuffer writeBuffer], overflowBuffer, overflow); + + [preBuffer didWrite:overflow]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Note: The completeCurrentRead method will trim the buffer for us. + + currentRead->bytesDone += underflow; + totalBytesReadForCurrentRead += underflow; + done = YES; + } + else + { + // The term was not found within the data that we read. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = NO; + } + } + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + + if (readIntoPreBuffer) + { + // We just read a chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + + // Now copy the data into the read packet. + // + // Recall that we didn't read directly into the packet's buffer to avoid + // over-allocating memory since we had no clue how much data was available to be read. + // + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesRead); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesRead]; + + // Update totals + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + else + { + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + + done = YES; + } + + } // if (bytesRead > 0) + + } // if (!done && !error && !socketEOF && hasBytesAvailable) + + + if (!done && currentRead->readLength == 0 && currentRead->term == nil) + { + // Read type #1 - read all available data + // + // We might arrive here if we read data from the prebuffer but not from the socket. + + done = (totalBytesReadForCurrentRead > 0); + } + + // Check to see if we're done, or if we've made progress + + if (done) + { + [self completeCurrentRead]; + + if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) + { + [self maybeDequeueRead]; + } + } + else if (totalBytesReadForCurrentRead > 0) + { + // We're not done read type #2 or #3 yet, but we have read in some bytes + // + // We ensure that `waiting` is set in order to resume the readSource (if it is suspended). It is + // possible to reach this point and `waiting` not be set, if the current read's length is + // sufficiently large. In that case, we may have read to some upperbound successfully, but + // that upperbound could be smaller than the desired length. + waiting = YES; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) + { + long theReadTag = currentRead->tag; + + dispatch_async(delegateQueue, + ^{ @autoreleasepool { + + [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; + }}); + } + } + + // Check for errors + + if (error) + { + [self closeWithError:error]; + } + else if (socketEOF) + { + [self doReadEOF]; + } + else if (waiting) + { + if (![self usingCFStreamForTLS]) + { + // Monitor the socket for readability (if we're not already doing so) + [self resumeReadSource]; + } + } + + // Do not add any code here without first adding return statements in the error cases above. +} + +- (void)doReadEOF +{ + LogTrace(); + + // This method may be called more than once. + // If the EOF is read while there is still data in the preBuffer, + // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. + + flags |= kSocketHasReadEOF; + + if (flags & kSocketSecure) + { + // If the SSL layer has any buffered data, flush it into the preBuffer now. + + [self flushSSLBuffers]; + } + + BOOL shouldDisconnect = NO; + NSError *error = nil; + + if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) + { + // We received an EOF during or prior to startTLS. + // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. + + shouldDisconnect = YES; + + if ([self usingSecureTransportForTLS]) + { + error = [self sslError:errSSLClosedAbort]; + } + } + else if (flags & kReadStreamClosed) + { + // The preBuffer has already been drained. + // The config allows half-duplex connections. + // We've previously checked the socket, and it appeared writeable. + // So we marked the read stream as closed and notified the delegate. + // + // As per the half-duplex contract, the socket will be closed when a write fails, + // or when the socket is manually closed. + + shouldDisconnect = NO; + } + else if ([preBuffer availableBytes] > 0) + { + LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); + + // Although we won't be able to read any more data from the socket, + // there is existing data that has been prebuffered that we can read. + + shouldDisconnect = NO; + } + else if (config & kAllowHalfDuplexConnection) + { + // We just received an EOF (end of file) from the socket's read stream. + // This means the remote end of the socket (the peer we're connected to) + // has explicitly stated that it will not be sending us any more data. + // + // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + struct pollfd pfd[1]; + pfd[0].fd = socketFD; + pfd[0].events = POLLOUT; + pfd[0].revents = 0; + + poll(pfd, 1, 0); + + if (pfd[0].revents & POLLOUT) + { + // Socket appears to still be writeable + + shouldDisconnect = NO; + flags |= kReadStreamClosed; + + // Notify the delegate that we're going half-duplex + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidCloseReadStream:self]; + }}); + } + } + else + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + + + if (shouldDisconnect) + { + if (error == nil) + { + if ([self usingSecureTransportForTLS]) + { + if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) + { + error = [self sslError:sslErrCode]; + } + else + { + error = [self connectionClosedError]; + } + } + else + { + error = [self connectionClosedError]; + } + } + [self closeWithError:error]; + } + else + { + if (![self usingCFStreamForTLS]) + { + // Suspend the read source (if needed) + + [self suspendReadSource]; + } + } +} + +- (void)completeCurrentRead +{ + LogTrace(); + + NSAssert(currentRead, + @"Trying to complete current read when there is no current read."); + + + NSData *result = nil; + + if (currentRead->bufferOwner) + { + // We created the buffer on behalf of the user. + // Trim our buffer to be the proper size. + [currentRead->buffer setLength:currentRead->bytesDone]; + + result = currentRead->buffer; + } + else + { + // We did NOT create the buffer. + // The buffer is owned by the caller. + // Only trim the buffer if we had to increase its size. + + if ([currentRead->buffer length] > currentRead->originalBufferLength) + { + NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; + NSUInteger origSize = currentRead->originalBufferLength; + + NSUInteger buffSize = MAX(readSize, origSize); + + [currentRead->buffer setLength:buffSize]; + } + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; + + result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; + } + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) + { + GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadData:result withTag:theRead->tag]; + }}); + } + + [self endCurrentRead]; +} + +- (void)endCurrentRead +{ + if (readTimer) + { + dispatch_source_cancel(readTimer); + readTimer = NULL; + } + + currentRead = nil; +} + +- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, + 0, + 0, + socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doReadTimeout]; + +#pragma clang diagnostic pop + }}); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadTimer = readTimer; + dispatch_source_set_cancel_handler(readTimer, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(readTimer)"); + dispatch_release(theReadTimer); + +#pragma clang diagnostic pop + }); +#endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(readTimer); + } +} + +- (void)doReadTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kReadsPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) + { + GCDAsyncReadPacket *theRead = currentRead; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag + elapsed:theRead->timeout + bytesDone:theRead->bytesDone]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self doReadTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doReadTimeoutWithExtension:0.0]; + } +} + +- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentRead) + { + if (timeoutExtension > 0.0) + { + currentRead->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause reads, and continue + flags &= ~kReadsPaused; + [self doReadData]; + } + else + { + LogVerbose(@"ReadTimeout"); + + [self closeWithError:[self readTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if ([data length] == 0) return; + + GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) + { + [self->writeQueue addObject:packet]; + [self maybeDequeueWrite]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!self->currentWrite || ![self->currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) + { + // We're not writing anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + NSUInteger done = self->currentWrite->bytesDone; + NSUInteger total = [self->currentWrite->buffer length]; + + if (tagPtr != NULL) *tagPtr = self->currentWrite->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + result = (float)done / (float)total; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * Conditionally starts a new write. + * + * It is called when: + * - a user requests a write + * - after a write request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. + **/ +- (void)maybeDequeueWrite +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + + // If we're not currently processing a write AND we have an available write stream + if ((currentWrite == nil) && (flags & kConnected)) + { + if ([writeQueue count] > 0) + { + // Dequeue the next object in the write queue + currentWrite = [writeQueue objectAtIndex:0]; + [writeQueue removeObjectAtIndex:0]; + + + if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingWriteTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncWritePacket"); + + // Setup write timer (if needed) + [self setupWriteTimerWithTimeout:currentWrite->timeout]; + + // Immediately write, if possible + [self doWriteData]; + } + } + else if (flags & kDisconnectAfterWrites) + { + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + } +} + +- (void)doWriteData +{ + LogTrace(); + + // This method is called by the writeSource via the socketQueue + + if ((currentWrite == nil) || (flags & kWritesPaused)) + { + LogVerbose(@"No currentWrite or kWritesPaused"); + + // Unable to write at this time + + if ([self usingCFStreamForTLS]) + { + // CFWriteStream only fires once when there is available data. + // It won't fire again until we've invoked CFWriteStreamWrite. + } + else + { + // If the writeSource is firing, we need to pause it + // or else it will continue to fire over and over again. + + if (flags & kSocketCanAcceptBytes) + { + [self suspendWriteSource]; + } + } + return; + } + + if (!(flags & kSocketCanAcceptBytes)) + { + LogVerbose(@"No space available to write..."); + + // No space available to write. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + } + return; + } + + if (flags & kStartingWriteTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The writeQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingReadTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for available space in the socket's internal OS buffer to continue writing. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the readQueue to drain and start the SSL/TLS process. + // We now know we can write to the socket. + + if (![self usingCFStreamForTLS]) + { + // Suspend the write source or else it will continue to fire nonstop. + + [self suspendWriteSource]; + } + } + + return; + } + + // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) + + BOOL waiting = NO; + NSError *error = nil; + size_t bytesWritten = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { +#if TARGET_OS_IPHONE + + // + // Writing data using CFStream (over internal TLS) + // + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + CFIndex result = CFWriteStreamWrite(writeStream, + buffer, + (CFIndex)bytesToWrite); + LogVerbose(@"CFWriteStreamWrite(%lu) = %li", + (unsigned long)bytesToWrite, + result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); + } + else + { + bytesWritten = (size_t)result; + + // We always set waiting to true in this scenario. + // CFStream may have altered our underlying socket to non-blocking. + // Thus if we attempt to write without a callback, we may end up blocking our queue. + waiting = YES; + } + +#endif + } + else + { + // We're going to use the SSLWrite function. + // + // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) + // + // Parameters: + // context - An SSL session context reference. + // data - A pointer to the buffer of data to write. + // dataLength - The amount, in bytes, of data to write. + // processed - On return, the length, in bytes, of the data actually written. + // + // It sounds pretty straight-forward, + // but there are a few caveats you should be aware of. + // + // The SSLWrite method operates in a non-obvious (and rather annoying) manner. + // According to the documentation: + // + // Because you may configure the underlying connection to operate in a non-blocking manner, + // a write operation might return errSSLWouldBlock, indicating that less data than requested + // was actually transferred. In this case, you should repeat the call to SSLWrite until some + // other result is returned. + // + // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, + // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), + // but it sets processed to dataLength !! + // + // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, + // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to + // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. + // + // You might be wondering: + // If the SSLWrite function doesn't tell us how many bytes were written, + // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) + // for the next time we invoke SSLWrite? + // + // The answer is that SSLWrite cached all the data we told it to write, + // and it will push out that data next time we call SSLWrite. + // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. + // If we call SSLWrite with empty data, then it will simply push out the cached data. + // + // For this purpose we're going to break large writes into a series of smaller writes. + // This allows us to report progress back to the delegate. + + OSStatus result; + + BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); + BOOL hasNewDataToWrite = YES; + + if (hasCachedDataToWrite) + { + size_t processed = 0; + + result = SSLWrite(sslContext, NULL, 0, &processed); + + if (result == noErr) + { + bytesWritten = sslWriteCachedLength; + sslWriteCachedLength = 0; + + if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) + { + // We've written all data for the current write. + hasNewDataToWrite = NO; + } + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + } + else + { + error = [self sslError:result]; + } + + // Can't write any new data since we were unable to write the cached data. + hasNewDataToWrite = NO; + } + } + + if (hasNewDataToWrite) + { + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + + currentWrite->bytesDone + + bytesWritten; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + size_t bytesRemaining = bytesToWrite; + + BOOL keepLooping = YES; + while (keepLooping) + { + const size_t sslMaxBytesToWrite = 32768; + size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); + size_t sslBytesWritten = 0; + + result = SSLWrite(sslContext, + buffer, + sslBytesToWrite, + &sslBytesWritten); + + if (result == noErr) + { + buffer += sslBytesWritten; + bytesWritten += sslBytesWritten; + bytesRemaining -= sslBytesWritten; + + keepLooping = (bytesRemaining > 0); + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + sslWriteCachedLength = sslBytesToWrite; + } + else + { + error = [self sslError:result]; + } + + keepLooping = NO; + } + + } // while (keepLooping) + + } // if (hasNewDataToWrite) + } + } + else + { + // + // Writing data directly over raw socket + // + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); + LogVerbose(@"wrote to socket = %zd", result); + + // Check results + if (result < 0) + { + if (errno == EWOULDBLOCK) + { + waiting = YES; + } + else + { + error = [self errorWithErrno:errno reason:@"Error in write() function"]; + } + } + else + { + bytesWritten = result; + } + } + + // We're done with our writing. + // If we explictly ran into a situation where the socket told us there was no room in the buffer, + // then we immediately resume listening for notifications. + // + // We must do this before we dequeue another write, + // as that may in turn invoke this method again. + // + // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. + + if (waiting) + { + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + // Check our results + + BOOL done = NO; + + if (bytesWritten > 0) + { + // Update total amount read for the current write + currentWrite->bytesDone += bytesWritten; + LogVerbose(@"currentWrite->bytesDone = %lu", + (unsigned long)currentWrite->bytesDone); + + // Is packet done? + done = (currentWrite->bytesDone == [currentWrite->buffer length]); + } + + if (done) + { + [self completeCurrentWrite]; + + if (!error) + { + dispatch_async(socketQueue, ^{ @autoreleasepool{ + + [self maybeDequeueWrite]; + }}); + } + } + else + { + // We were unable to finish writing the data, + // so we're waiting for another callback to notify us of available space in the lower-level output buffer. + + if (!waiting && !error) + { + // This would be the case if our write was able to accept some data, but not all of it. + + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + if (bytesWritten > 0) + { + // We're not done with the entire write, but we have written some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, + ^{ @autoreleasepool { + + [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; + }}); + } + } + } + + // Check for errors + + if (error) + { + [self closeWithError:[self errorWithErrno:errno reason:@"Error in write() function"]]; + } + + // Do not add any code here without first adding a return statement in the error case above. +} + +- (void)completeCurrentWrite +{ + LogTrace(); + + NSAssert(currentWrite, + @"Trying to complete current write when there is no current write."); + + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWriteDataWithTag:theWriteTag]; + }}); + } + + [self endCurrentWrite]; +} + +- (void)endCurrentWrite +{ + if (writeTimer) + { + dispatch_source_cancel(writeTimer); + writeTimer = NULL; + } + + currentWrite = nil; +} + +- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, + 0, + 0, + socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doWriteTimeout]; + +#pragma clang diagnostic pop + }}); + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theWriteTimer = writeTimer; + dispatch_source_set_cancel_handler(writeTimer, ^{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(writeTimer)"); + dispatch_release(theWriteTimer); + +#pragma clang diagnostic pop + }); +#endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(writeTimer); + } +} + +- (void)doWriteTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kWritesPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) + { + GCDAsyncWritePacket *theWrite = currentWrite; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag + elapsed:theWrite->timeout + bytesDone:theWrite->bytesDone]; + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self doWriteTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doWriteTimeoutWithExtension:0.0]; + } +} + +- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentWrite) + { + if (timeoutExtension > 0.0) + { + currentWrite->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause writes, and continue + flags &= ~kWritesPaused; + [self doWriteData]; + } + else + { + LogVerbose(@"WriteTimeout"); + + [self closeWithError:[self writeTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startTLS:(NSDictionary *)tlsSettings +{ + LogTrace(); + + if (tlsSettings == nil) + { + // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, + // but causes problems if we later try to fetch the remote host's certificate. + // + // To be exact, it causes the following to return NULL instead of the normal result: + // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) + // + // So we use an empty dictionary instead, which works perfectly. + + tlsSettings = [NSDictionary dictionary]; + } + + GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; + + dispatch_async(socketQueue, + ^{ @autoreleasepool { + + if ((self->flags & kSocketStarted) && !(self->flags & kQueuedTLS) && !(self->flags & kForbidReadsWrites)) + { + [self->readQueue addObject:packet]; + [self->writeQueue addObject:packet]; + + self->flags |= kQueuedTLS; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + }}); + +} + +- (void)maybeStartTLS +{ + // We can't start TLS until: + // - All queued reads prior to the user calling startTLS are complete + // - All queued writes prior to the user calling startTLS are complete + // + // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + BOOL useSecureTransport = YES; + +#if TARGET_OS_IPHONE + { + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + NSDictionary *tlsSettings = @{}; + if (tlsPacket) { + tlsSettings = tlsPacket->tlsSettings; + } + NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; + if (value && [value boolValue]) + useSecureTransport = NO; + } +#endif + + if (useSecureTransport) + { + [self ssl_startTLS]; + } + else + { +#if TARGET_OS_IPHONE + [self cf_startTLS]; +#endif + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via SecureTransport +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength +{ + LogVerbose(@"sslReadWithBuffer:%p length:%lu", + buffer, + (unsigned long)*bufferLength); + + if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) + { + LogVerbose(@"%@ - No data available to read...", THIS_METHOD); + + // No data available to read. + // + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t totalBytesRead = 0; + size_t totalBytesLeftToBeRead = *bufferLength; + + BOOL done = NO; + BOOL socketError = NO; + + // + // STEP 1 : READ FROM SSL PRE BUFFER + // + + size_t sslPreBufferLength = [sslPreBuffer availableBytes]; + + if (sslPreBufferLength > 0) + { + LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); + + size_t bytesToCopy; + if (sslPreBufferLength > totalBytesLeftToBeRead) + bytesToCopy = totalBytesLeftToBeRead; + else + bytesToCopy = sslPreBufferLength; + + LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", + THIS_METHOD, + bytesToCopy); + + memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", + THIS_METHOD, + [sslPreBuffer availableBytes]); + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + + // + // STEP 2 : READ FROM SOCKET + // + + if (!done && (socketFDBytesAvailable > 0)) + { + LogVerbose(@"%@: Reading from socket...", THIS_METHOD); + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + BOOL readIntoPreBuffer; + size_t bytesToRead; + uint8_t *buf; + + if (socketFDBytesAvailable > totalBytesLeftToBeRead) + { + // Read all available data from socket into sslPreBuffer. + // Then copy requested amount into dataBuffer. + + LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); + + [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; + + readIntoPreBuffer = YES; + bytesToRead = (size_t)socketFDBytesAvailable; + buf = [sslPreBuffer writeBuffer]; + } + else + { + // Read available data from socket directly into dataBuffer. + + LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); + + readIntoPreBuffer = NO; + bytesToRead = totalBytesLeftToBeRead; + buf = (uint8_t *)buffer + totalBytesRead; + } + + ssize_t result = read(socketFD, buf, bytesToRead); + LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); + + if (result < 0) + { + LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); + + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + LogVerbose(@"%@: read EOF", THIS_METHOD); + + socketError = YES; + socketFDBytesAvailable = 0; + } + else + { + size_t bytesReadFromSocket = result; + + if (socketFDBytesAvailable > bytesReadFromSocket) + socketFDBytesAvailable -= bytesReadFromSocket; + else + socketFDBytesAvailable = 0; + + if (readIntoPreBuffer) + { + [sslPreBuffer didWrite:bytesReadFromSocket]; + + size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); + + LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", + THIS_METHOD, + bytesToCopy); + + memcpy((uint8_t *)buffer + totalBytesRead, + [sslPreBuffer readBuffer], + bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", + THIS_METHOD, + [sslPreBuffer availableBytes]); + } + else + { + totalBytesRead += bytesReadFromSocket; + totalBytesLeftToBeRead -= bytesReadFromSocket; + } + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + } + + *bufferLength = totalBytesRead; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength +{ + if (!(flags & kSocketCanAcceptBytes)) + { + // Unable to write. + // + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t bytesToWrite = *bufferLength; + size_t bytesWritten = 0; + + BOOL done = NO; + BOOL socketError = NO; + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = write(socketFD, buffer, bytesToWrite); + + if (result < 0) + { + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + flags &= ~kSocketCanAcceptBytes; + } + else if (result == 0) + { + flags &= ~kSocketCanAcceptBytes; + } + else + { + bytesWritten = result; + + done = (bytesWritten == bytesToWrite); + } + + *bufferLength = bytesWritten; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +static OSStatus SSLReadFunction(SSLConnectionRef connection, + void *data, + size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), + @"What the deuce?"); + + return [asyncSocket sslReadWithBuffer:data length:dataLength]; +} + +static OSStatus SSLWriteFunction(SSLConnectionRef connection, + const void *data, + size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), + @"What the deuce?"); + + return [asyncSocket sslWriteWithBuffer:data length:dataLength]; } -- (NSData *)getInterfaceAddressFromUrl:(NSURL *)url -{ - NSString *path = url.path; - if (path.length == 0) { - return nil; - } - - struct sockaddr_un nativeAddr; - nativeAddr.sun_family = AF_UNIX; - strlcpy(nativeAddr.sun_path, path.fileSystemRepresentation, sizeof(nativeAddr.sun_path)); - nativeAddr.sun_len = (unsigned char)SUN_LEN(&nativeAddr); - NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)]; - - return interface; -} +- (void)ssl_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via SecureTransport)..."); + + OSStatus status; + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + if (tlsPacket == nil) // Code to quiet the analyzer + { + NSAssert(NO, @"Logic error"); + + [self closeWithError:[self otherError:@"Logic error"]]; + return; + } + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + // Create SSLContext, and setup IO callbacks and connection ref + + NSNumber *isServerNumber = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer]; + BOOL isServer = [isServerNumber boolValue]; + +#if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + { + if (isServer) + sslContext = SSLCreateContext(kCFAllocatorDefault, + kSSLServerSide, + kSSLStreamType); + else + sslContext = SSLCreateContext(kCFAllocatorDefault, + kSSLClientSide, + kSSLStreamType); + + if (sslContext == NULL) + { + [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; + return; + } + } +#else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + { + status = SSLNewContext(isServer, &sslContext); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; + return; + } + } +#endif + + status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; + return; + } + + status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; + return; + } + + + NSNumber *shouldManuallyEvaluateTrust = [tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust]; + if ([shouldManuallyEvaluateTrust boolValue]) + { + if (isServer) + { + [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; + return; + } + + status = SSLSetSessionOption(sslContext, + kSSLSessionOptionBreakOnServerAuth, + true); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; + return; + } + + } + + // Configure SSLContext from given settings + // + // Checklist: + // 1. kCFStreamSSLPeerName + // 2. kCFStreamSSLCertificates + // 3. GCDAsyncSocketSSLPeerID + // 4. GCDAsyncSocketSSLProtocolVersionMin + // 5. GCDAsyncSocketSSLProtocolVersionMax + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + // 8. GCDAsyncSocketSSLCipherSuites + // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) + // 10. GCDAsyncSocketSSLALPN + // + // Deprecated (throw error): + // 10. kCFStreamSSLAllowsAnyRoot + // 11. kCFStreamSSLAllowsExpiredRoots + // 12. kCFStreamSSLAllowsExpiredCertificates + // 13. kCFStreamSSLValidatesCertificateChain + // 14. kCFStreamSSLLevel + + NSObject *value; + + // 1. kCFStreamSSLPeerName + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName]; + if ([value isKindOfClass:[NSString class]]) + { + NSString *peerName = (NSString *)value; + + const char *peer = [peerName UTF8String]; + size_t peerLen = strlen(peer); + + status = SSLSetPeerDomainName(sslContext, peer, peerLen); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; + return; + } + } + else if (value) + { + NSAssert(NO, + @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; + return; + } + + // 2. kCFStreamSSLCertificates + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLCertificates]; + if ([value isKindOfClass:[NSArray class]]) + { + NSArray *certs = (NSArray *)value; + + status = SSLSetCertificate(sslContext, (__bridge CFArrayRef)certs); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; + return; + } + } + else if (value) + { + NSAssert(NO, + @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; + return; + } + + // 3. GCDAsyncSocketSSLPeerID + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *peerIdData = (NSData *)value; + + status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." + @" (You can convert strings to data using a method like" + @" [string dataUsingEncoding:NSUTF8StringEncoding])"); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; + return; + } + + // 4. GCDAsyncSocketSSLProtocolVersionMin + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (minProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMin(sslContext, minProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, + @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; + return; + } + + // 5. GCDAsyncSocketSSLProtocolVersionMax + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (maxProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMax(sslContext, maxProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, + @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; + return; + } + + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; + if ([value isKindOfClass:[NSNumber class]]) + { + NSNumber *falseStart = (NSNumber *)value; + status = SSLSetSessionOption(sslContext, + kSSLSessionOptionFalseStart, + [falseStart boolValue]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, + @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; + return; + } + + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; + if ([value isKindOfClass:[NSNumber class]]) + { + NSNumber *oneByteRecord = (NSNumber *)value; + status = SSLSetSessionOption(sslContext, + kSSLSessionOptionSendOneByteRecord, + [oneByteRecord boolValue]); + if (status != noErr) + { + [self closeWithError: + [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." + @" Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; + return; + } + + // 8. GCDAsyncSocketSSLCipherSuites + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; + if ([value isKindOfClass:[NSArray class]]) + { + NSArray *cipherSuites = (NSArray *)value; + NSUInteger numberCiphers = [cipherSuites count]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wvla" + SSLCipherSuite ciphers[numberCiphers]; +#pragma clang diagnostic pop + + NSUInteger cipherIndex; + for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) + { + NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; + ciphers[cipherIndex] = (SSLCipherSuite)[cipherObject unsignedIntValue]; + } + + status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; + return; + } + } + else if (value) + { + NSAssert(NO, + @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; + return; + } -- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD -{ - readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); - writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); - - // Setup event handlers - - __weak GCDAsyncSocket *weakSelf = self; - - dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf == nil) return_from_block; - - LogVerbose(@"readEventBlock"); - - strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); - LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); - - if (strongSelf->socketFDBytesAvailable > 0) - [strongSelf doReadData]; - else - [strongSelf doReadEOF]; - - #pragma clang diagnostic pop - }}); - - dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf == nil) return_from_block; - - LogVerbose(@"writeEventBlock"); - - strongSelf->flags |= kSocketCanAcceptBytes; - [strongSelf doWriteData]; - - #pragma clang diagnostic pop - }}); - - // Setup cancel handlers - - __block int socketFDRefCount = 2; - - #if !OS_OBJECT_USE_OBJC - dispatch_source_t theReadSource = readSource; - dispatch_source_t theWriteSource = writeSource; - #endif - - dispatch_source_set_cancel_handler(readSource, ^{ - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - LogVerbose(@"readCancelBlock"); - - #if !OS_OBJECT_USE_OBJC - LogVerbose(@"dispatch_release(readSource)"); - dispatch_release(theReadSource); - #endif - - if (--socketFDRefCount == 0) - { - LogVerbose(@"close(socketFD)"); - close(socketFD); - } - - #pragma clang diagnostic pop - }); - - dispatch_source_set_cancel_handler(writeSource, ^{ - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - LogVerbose(@"writeCancelBlock"); - - #if !OS_OBJECT_USE_OBJC - LogVerbose(@"dispatch_release(writeSource)"); - dispatch_release(theWriteSource); - #endif - - if (--socketFDRefCount == 0) - { - LogVerbose(@"close(socketFD)"); - close(socketFD); - } - - #pragma clang diagnostic pop - }); - - // We will not be able to read until data arrives. - // But we should be able to write immediately. - - socketFDBytesAvailable = 0; - flags &= ~kReadSourceSuspended; - - LogVerbose(@"dispatch_resume(readSource)"); - dispatch_resume(readSource); - - flags |= kSocketCanAcceptBytes; - flags |= kWriteSourceSuspended; -} + // 9. GCDAsyncSocketSSLDiffieHellmanParameters -- (BOOL)usingCFStreamForTLS -{ - #if TARGET_OS_IPHONE - - if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) - { - // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. - - return YES; - } - - #endif - - return NO; -} +#if !TARGET_OS_IPHONE + value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *diffieHellmanData = (NSData *)value; + + status = SSLSetDiffieHellmanParams(sslContext, + [diffieHellmanData bytes], + [diffieHellmanData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; + return; + } + } + else if (value) + { + NSAssert(NO, + @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); -- (BOOL)usingSecureTransportForTLS -{ - // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) - - #if TARGET_OS_IPHONE - - if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) - { - // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. - - return NO; - } - - #endif - - return YES; -} + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; + return; + } +#endif -- (void)suspendReadSource -{ - if (!(flags & kReadSourceSuspended)) - { - LogVerbose(@"dispatch_suspend(readSource)"); - - dispatch_suspend(readSource); - flags |= kReadSourceSuspended; - } -} + // 10. kCFStreamSSLCertificates + value = [tlsSettings objectForKey:GCDAsyncSocketSSLALPN]; + if ([value isKindOfClass:[NSArray class]]) + { + CFArrayRef protocols = (__bridge CFArrayRef)((NSArray *) value); + status = SSLSetALPNProtocols(sslContext, protocols); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetALPNProtocols"]]; + return; + } + } + else if (value) + { + NSAssert(NO, + @"Invalid value for GCDAsyncSocketSSLALPN. Value must be of type NSArray."); -- (void)resumeReadSource -{ - if (flags & kReadSourceSuspended) - { - LogVerbose(@"dispatch_resume(readSource)"); - - dispatch_resume(readSource); - flags &= ~kReadSourceSuspended; - } -} + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLALPN."]]; + return; + } -- (void)suspendWriteSource -{ - if (!(flags & kWriteSourceSuspended)) - { - LogVerbose(@"dispatch_suspend(writeSource)"); - - dispatch_suspend(writeSource); - flags |= kWriteSourceSuspended; - } -} + // DEPRECATED checks -- (void)resumeWriteSource -{ - if (flags & kWriteSourceSuspended) - { - LogVerbose(@"dispatch_resume(writeSource)"); - - dispatch_resume(writeSource); - flags &= ~kWriteSourceSuspended; - } -} + // 10. kCFStreamSSLAllowsAnyRoot -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Reading -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsAnyRoot]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" + @" - You must use manual trust evaluation"); -- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag -{ - [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; -} + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; + return; + } -- (void)readDataWithTimeout:(NSTimeInterval)timeout - buffer:(NSMutableData *)buffer - bufferOffset:(NSUInteger)offset - tag:(long)tag -{ - [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; -} + // 11. kCFStreamSSLAllowsExpiredRoots -- (void)readDataWithTimeout:(NSTimeInterval)timeout - buffer:(NSMutableData *)buffer - bufferOffset:(NSUInteger)offset - maxLength:(NSUInteger)length - tag:(long)tag -{ - if (offset > [buffer length]) { - LogWarn(@"Cannot read: offset > [buffer length]"); - return; - } - - GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer - startOffset:offset - maxLength:length - timeout:timeout - readLength:0 - terminator:nil - tag:tag]; - - dispatch_async(socketQueue, ^{ @autoreleasepool { - - LogTrace(); - - if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) - { - [self->readQueue addObject:packet]; - [self maybeDequeueRead]; - } - }}); - - // Do not rely on the block being run in order to release the packet, - // as the queue might get released without the block completing. -} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredRoots]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" + @" - You must use manual trust evaluation"); -- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag -{ - [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; -} + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; + return; + } -- (void)readDataToLength:(NSUInteger)length - withTimeout:(NSTimeInterval)timeout - buffer:(NSMutableData *)buffer - bufferOffset:(NSUInteger)offset - tag:(long)tag -{ - if (length == 0) { - LogWarn(@"Cannot read: length == 0"); - return; - } - if (offset > [buffer length]) { - LogWarn(@"Cannot read: offset > [buffer length]"); - return; - } - - GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer - startOffset:offset - maxLength:0 - timeout:timeout - readLength:length - terminator:nil - tag:tag]; - - dispatch_async(socketQueue, ^{ @autoreleasepool { - - LogTrace(); - - if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) - { - [self->readQueue addObject:packet]; - [self maybeDequeueRead]; - } - }}); - - // Do not rely on the block being run in order to release the packet, - // as the queue might get released without the block completing. -} + // 12. kCFStreamSSLValidatesCertificateChain -- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag -{ - [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; -} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLValidatesCertificateChain]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" + @" - You must use manual trust evaluation"); -- (void)readDataToData:(NSData *)data - withTimeout:(NSTimeInterval)timeout - buffer:(NSMutableData *)buffer - bufferOffset:(NSUInteger)offset - tag:(long)tag -{ - [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; -} + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; + return; + } -- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag -{ - [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; -} + // 13. kCFStreamSSLAllowsExpiredCertificates -- (void)readDataToData:(NSData *)data - withTimeout:(NSTimeInterval)timeout - buffer:(NSMutableData *)buffer - bufferOffset:(NSUInteger)offset - maxLength:(NSUInteger)maxLength - tag:(long)tag -{ - if ([data length] == 0) { - LogWarn(@"Cannot read: [data length] == 0"); - return; - } - if (offset > [buffer length]) { - LogWarn(@"Cannot read: offset > [buffer length]"); - return; - } - if (maxLength > 0 && maxLength < [data length]) { - LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); - return; - } - - GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer - startOffset:offset - maxLength:maxLength - timeout:timeout - readLength:0 - terminator:data - tag:tag]; - - dispatch_async(socketQueue, ^{ @autoreleasepool { - - LogTrace(); - - if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) - { - [self->readQueue addObject:packet]; - [self maybeDequeueRead]; - } - }}); - - // Do not rely on the block being run in order to release the packet, - // as the queue might get released without the block completing. -} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredCertificates]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" + @" - You must use manual trust evaluation"); -- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr -{ - __block float result = 0.0F; - - dispatch_block_t block = ^{ - - if (!self->currentRead || ![self->currentRead isKindOfClass:[GCDAsyncReadPacket class]]) - { - // We're not reading anything right now. - - if (tagPtr != NULL) *tagPtr = 0; - if (donePtr != NULL) *donePtr = 0; - if (totalPtr != NULL) *totalPtr = 0; - - result = NAN; - } - else - { - // It's only possible to know the progress of our read if we're reading to a certain length. - // If we're reading to data, we of course have no idea when the data will arrive. - // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. - - NSUInteger done = self->currentRead->bytesDone; - NSUInteger total = self->currentRead->readLength; - - if (tagPtr != NULL) *tagPtr = self->currentRead->tag; - if (donePtr != NULL) *donePtr = done; - if (totalPtr != NULL) *totalPtr = total; - - if (total > 0) - result = (float)done / (float)total; - else - result = 1.0F; - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; -} + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; + return; + } -/** - * This method starts a new read, if needed. - * - * It is called when: - * - a user requests a read - * - after a read request has finished (to handle the next request) - * - immediately after the socket opens to handle any pending requests - * - * This method also handles auto-disconnect post read/write completion. -**/ -- (void)maybeDequeueRead -{ - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - // If we're not currently processing a read AND we have an available read stream - if ((currentRead == nil) && (flags & kConnected)) - { - if ([readQueue count] > 0) - { - // Dequeue the next object in the write queue - currentRead = [readQueue objectAtIndex:0]; - [readQueue removeObjectAtIndex:0]; - - - if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) - { - LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); - - // Attempt to start TLS - flags |= kStartingReadTLS; - - // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set - [self maybeStartTLS]; - } - else - { - LogVerbose(@"Dequeued GCDAsyncReadPacket"); - - // Setup read timer (if needed) - [self setupReadTimerWithTimeout:currentRead->timeout]; - - // Immediately read, if possible - [self doReadData]; - } - } - else if (flags & kDisconnectAfterReads) - { - if (flags & kDisconnectAfterWrites) - { - if (([writeQueue count] == 0) && (currentWrite == nil)) - { - [self closeWithError:nil]; - } - } - else - { - [self closeWithError:nil]; - } - } - else if (flags & kSocketSecure) - { - [self flushSSLBuffers]; - - // Edge case: - // - // We just drained all data from the ssl buffers, - // and all known data from the socket (socketFDBytesAvailable). - // - // If we didn't get any data from this process, - // then we may have reached the end of the TCP stream. - // - // Be sure callbacks are enabled so we're notified about a disconnection. - - if ([preBuffer availableBytes] == 0) - { - if ([self usingCFStreamForTLS]) { - // Callbacks never disabled - } - else { - [self resumeReadSource]; - } - } - } - } -} + // 14. kCFStreamSSLLevel -- (void)flushSSLBuffers -{ - LogTrace(); - - NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); - - if ([preBuffer availableBytes] > 0) - { - // Only flush the ssl buffers if the prebuffer is empty. - // This is to avoid growing the prebuffer inifinitely large. - - return; - } - - #if TARGET_OS_IPHONE - - if ([self usingCFStreamForTLS]) - { - if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) - { - LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); - - CFIndex defaultBytesToRead = (1024 * 4); - - [preBuffer ensureCapacityForWrite:defaultBytesToRead]; - - uint8_t *buffer = [preBuffer writeBuffer]; - - CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); - LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); - - if (result > 0) - { - [preBuffer didWrite:result]; - } - - flags &= ~kSecureSocketHasBytesAvailable; - } - - return; - } - - #endif - - __block NSUInteger estimatedBytesAvailable = 0; - - dispatch_block_t updateEstimatedBytesAvailable = ^{ - - // Figure out if there is any data available to be read - // - // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket - // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket - // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered - // - // We call the variable "estimated" because we don't know how many decrypted bytes we'll get - // from the encrypted bytes in the sslPreBuffer. - // However, we do know this is an upper bound on the estimation. - - estimatedBytesAvailable = self->socketFDBytesAvailable + [self->sslPreBuffer availableBytes]; - - size_t sslInternalBufSize = 0; - SSLGetBufferedReadSize(self->sslContext, &sslInternalBufSize); - - estimatedBytesAvailable += sslInternalBufSize; - }; - - updateEstimatedBytesAvailable(); - - if (estimatedBytesAvailable > 0) - { - LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); - - BOOL done = NO; - do - { - LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); - - // Make sure there's enough room in the prebuffer - - [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; - - // Read data into prebuffer - - uint8_t *buffer = [preBuffer writeBuffer]; - size_t bytesRead = 0; - - OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); - LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); - - if (bytesRead > 0) - { - [preBuffer didWrite:bytesRead]; - } - - LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); - - if (result != noErr) - { - done = YES; - } - else - { - updateEstimatedBytesAvailable(); - } - - } while (!done && estimatedBytesAvailable > 0); - } -} +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLLevel]; +#pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" + @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); -- (void)doReadData -{ - LogTrace(); - - // This method is called on the socketQueue. - // It might be called directly, or via the readSource when data is available to be read. - - if ((currentRead == nil) || (flags & kReadsPaused)) - { - LogVerbose(@"No currentRead or kReadsPaused"); - - // Unable to read at this time - - if (flags & kSocketSecure) - { - // Here's the situation: - // - // We have an established secure connection. - // There may not be a currentRead, but there might be encrypted data sitting around for us. - // When the user does get around to issuing a read, that encrypted data will need to be decrypted. - // - // So why make the user wait? - // We might as well get a head start on decrypting some data now. - // - // The other reason we do this has to do with detecting a socket disconnection. - // The SSL/TLS protocol has it's own disconnection handshake. - // So when a secure socket is closed, a "goodbye" packet comes across the wire. - // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. - - [self flushSSLBuffers]; - } - - if ([self usingCFStreamForTLS]) - { - // CFReadStream only fires once when there is available data. - // It won't fire again until we've invoked CFReadStreamRead. - } - else - { - // If the readSource is firing, we need to pause it - // or else it will continue to fire over and over again. - // - // If the readSource is not firing, - // we want it to continue monitoring the socket. - - if (socketFDBytesAvailable > 0) - { - [self suspendReadSource]; - } - } - return; - } - - BOOL hasBytesAvailable = NO; - unsigned long estimatedBytesAvailable = 0; - - if ([self usingCFStreamForTLS]) - { - #if TARGET_OS_IPHONE - - // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) - - estimatedBytesAvailable = 0; - if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) - hasBytesAvailable = YES; - else - hasBytesAvailable = NO; - - #endif - } - else - { - estimatedBytesAvailable = socketFDBytesAvailable; - - if (flags & kSocketSecure) - { - // There are 2 buffers to be aware of here. - // - // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. - // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. - // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. - // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. - // - // The first buffer is one we create. - // SecureTransport often requests small amounts of data. - // This has to do with the encypted packets that are coming across the TCP stream. - // But it's non-optimal to do a bunch of small reads from the BSD socket. - // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) - // and may store excess in the sslPreBuffer. - - estimatedBytesAvailable += [sslPreBuffer availableBytes]; - - // The second buffer is within SecureTransport. - // As mentioned earlier, there are encrypted packets coming across the TCP stream. - // SecureTransport needs the entire packet to decrypt it. - // But if the entire packet produces X bytes of decrypted data, - // and we only asked SecureTransport for X/2 bytes of data, - // it must store the extra X/2 bytes of decrypted data for the next read. - // - // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. - // From the documentation: - // - // "This function does not block or cause any low-level read operations to occur." - - size_t sslInternalBufSize = 0; - SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); - - estimatedBytesAvailable += sslInternalBufSize; - } - - hasBytesAvailable = (estimatedBytesAvailable > 0); - } - - if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) - { - LogVerbose(@"No data available to read..."); - - // No data available to read. - - if (![self usingCFStreamForTLS]) - { - // Need to wait for readSource to fire and notify us of - // available data in the socket's internal read buffer. - - [self resumeReadSource]; - } - return; - } - - if (flags & kStartingReadTLS) - { - LogVerbose(@"Waiting for SSL/TLS handshake to complete"); - - // The readQueue is waiting for SSL/TLS handshake to complete. - - if (flags & kStartingWriteTLS) - { - if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) - { - // We are in the process of a SSL Handshake. - // We were waiting for incoming data which has just arrived. - - [self ssl_continueSSLHandshake]; - } - } - else - { - // We are still waiting for the writeQueue to drain and start the SSL/TLS process. - // We now know data is available to read. - - if (![self usingCFStreamForTLS]) - { - // Suspend the read source or else it will continue to fire nonstop. - - [self suspendReadSource]; - } - } - - return; - } - - BOOL done = NO; // Completed read operation - NSError *error = nil; // Error occurred - - NSUInteger totalBytesReadForCurrentRead = 0; - - // - // STEP 1 - READ FROM PREBUFFER - // - - if ([preBuffer availableBytes] > 0) - { - // There are 3 types of read packets: - // - // 1) Read all available data. - // 2) Read a specific length of data. - // 3) Read up to a particular terminator. - - NSUInteger bytesToCopy; - - if (currentRead->term != nil) - { - // Read type #3 - read up to a terminator - - bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; - } - else - { - // Read type #1 or #2 - - bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; - } - - // Make sure we have enough room in the buffer for our read. - - [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; - - // Copy bytes from prebuffer into packet buffer - - uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + - currentRead->bytesDone; - - memcpy(buffer, [preBuffer readBuffer], bytesToCopy); - - // Remove the copied bytes from the preBuffer - [preBuffer didRead:bytesToCopy]; - - LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); - - // Update totals - - currentRead->bytesDone += bytesToCopy; - totalBytesReadForCurrentRead += bytesToCopy; - - // Check to see if the read operation is done - - if (currentRead->readLength > 0) - { - // Read type #2 - read a specific length of data - - done = (currentRead->bytesDone == currentRead->readLength); - } - else if (currentRead->term != nil) - { - // Read type #3 - read up to a terminator - - // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method - - if (!done && currentRead->maxLength > 0) - { - // We're not done and there's a set maxLength. - // Have we reached that maxLength yet? - - if (currentRead->bytesDone >= currentRead->maxLength) - { - error = [self readMaxedOutError]; - } - } - } - else - { - // Read type #1 - read all available data - // - // We're done as soon as - // - we've read all available data (in prebuffer and socket) - // - we've read the maxLength of read packet. - - done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); - } - - } - - // - // STEP 2 - READ FROM SOCKET - // - - BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) - BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more - - if (!done && !error && !socketEOF && hasBytesAvailable) - { - NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); - - BOOL readIntoPreBuffer = NO; - uint8_t *buffer = NULL; - size_t bytesRead = 0; - - if (flags & kSocketSecure) - { - if ([self usingCFStreamForTLS]) - { - #if TARGET_OS_IPHONE - - // Using CFStream, rather than SecureTransport, for TLS - - NSUInteger defaultReadLength = (1024 * 32); - - NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength - shouldPreBuffer:&readIntoPreBuffer]; - - // Make sure we have enough room in the buffer for our read. - // - // We are either reading directly into the currentRead->buffer, - // or we're reading into the temporary preBuffer. - - if (readIntoPreBuffer) - { - [preBuffer ensureCapacityForWrite:bytesToRead]; - - buffer = [preBuffer writeBuffer]; - } - else - { - [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; - - buffer = (uint8_t *)[currentRead->buffer mutableBytes] - + currentRead->startOffset - + currentRead->bytesDone; - } - - // Read data into buffer - - CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); - LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); - - if (result < 0) - { - error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); - } - else if (result == 0) - { - socketEOF = YES; - } - else - { - waiting = YES; - bytesRead = (size_t)result; - } - - // We only know how many decrypted bytes were read. - // The actual number of bytes read was likely more due to the overhead of the encryption. - // So we reset our flag, and rely on the next callback to alert us of more data. - flags &= ~kSecureSocketHasBytesAvailable; - - #endif - } - else - { - // Using SecureTransport for TLS - // - // We know: - // - how many bytes are available on the socket - // - how many encrypted bytes are sitting in the sslPreBuffer - // - how many decypted bytes are sitting in the sslContext - // - // But we do NOT know: - // - how many encypted bytes are sitting in the sslContext - // - // So we play the regular game of using an upper bound instead. - - NSUInteger defaultReadLength = (1024 * 32); - - if (defaultReadLength < estimatedBytesAvailable) { - defaultReadLength = estimatedBytesAvailable + (1024 * 16); - } - - NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength - shouldPreBuffer:&readIntoPreBuffer]; - - if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t - bytesToRead = SIZE_MAX; - } - - // Make sure we have enough room in the buffer for our read. - // - // We are either reading directly into the currentRead->buffer, - // or we're reading into the temporary preBuffer. - - if (readIntoPreBuffer) - { - [preBuffer ensureCapacityForWrite:bytesToRead]; - - buffer = [preBuffer writeBuffer]; - } - else - { - [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; - - buffer = (uint8_t *)[currentRead->buffer mutableBytes] - + currentRead->startOffset - + currentRead->bytesDone; - } - - // The documentation from Apple states: - // - // "a read operation might return errSSLWouldBlock, - // indicating that less data than requested was actually transferred" - // - // However, starting around 10.7, the function will sometimes return noErr, - // even if it didn't read as much data as requested. So we need to watch out for that. - - OSStatus result; - do - { - void *loop_buffer = buffer + bytesRead; - size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; - size_t loop_bytesRead = 0; - - result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); - LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); - - bytesRead += loop_bytesRead; - - } while ((result == noErr) && (bytesRead < bytesToRead)); - - - if (result != noErr) - { - if (result == errSSLWouldBlock) - waiting = YES; - else - { - if (result == errSSLClosedGraceful || result == errSSLClosedAbort) - { - // We've reached the end of the stream. - // Handle this the same way we would an EOF from the socket. - socketEOF = YES; - sslErrCode = result; - } - else - { - error = [self sslError:result]; - } - } - // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. - // This happens when the SSLRead function is able to read some data, - // but not the entire amount we requested. - - if (bytesRead <= 0) - { - bytesRead = 0; - } - } - - // Do not modify socketFDBytesAvailable. - // It will be updated via the SSLReadFunction(). - } - } - else - { - // Normal socket operation - - NSUInteger bytesToRead; - - // There are 3 types of read packets: - // - // 1) Read all available data. - // 2) Read a specific length of data. - // 3) Read up to a particular terminator. - - if (currentRead->term != nil) - { - // Read type #3 - read up to a terminator - - bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable - shouldPreBuffer:&readIntoPreBuffer]; - } - else - { - // Read type #1 or #2 - - bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; - } - - if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) - bytesToRead = SIZE_MAX; - } - - // Make sure we have enough room in the buffer for our read. - // - // We are either reading directly into the currentRead->buffer, - // or we're reading into the temporary preBuffer. - - if (readIntoPreBuffer) - { - [preBuffer ensureCapacityForWrite:bytesToRead]; - - buffer = [preBuffer writeBuffer]; - } - else - { - [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; - - buffer = (uint8_t *)[currentRead->buffer mutableBytes] - + currentRead->startOffset - + currentRead->bytesDone; - } - - // Read data into buffer - - int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - - ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); - LogVerbose(@"read from socket = %i", (int)result); - - if (result < 0) - { - if (errno == EWOULDBLOCK) - waiting = YES; - else - error = [self errorWithErrno:errno reason:@"Error in read() function"]; - - socketFDBytesAvailable = 0; - } - else if (result == 0) - { - socketEOF = YES; - socketFDBytesAvailable = 0; - } - else - { - bytesRead = result; - - if (bytesRead < bytesToRead) - { - // The read returned less data than requested. - // This means socketFDBytesAvailable was a bit off due to timing, - // because we read from the socket right when the readSource event was firing. - socketFDBytesAvailable = 0; - } - else - { - if (socketFDBytesAvailable <= bytesRead) - socketFDBytesAvailable = 0; - else - socketFDBytesAvailable -= bytesRead; - } - - if (socketFDBytesAvailable == 0) - { - waiting = YES; - } - } - } - - if (bytesRead > 0) - { - // Check to see if the read operation is done - - if (currentRead->readLength > 0) - { - // Read type #2 - read a specific length of data - // - // Note: We should never be using a prebuffer when we're reading a specific length of data. - - NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); - - currentRead->bytesDone += bytesRead; - totalBytesReadForCurrentRead += bytesRead; - - done = (currentRead->bytesDone == currentRead->readLength); - } - else if (currentRead->term != nil) - { - // Read type #3 - read up to a terminator - - if (readIntoPreBuffer) - { - // We just read a big chunk of data into the preBuffer - - [preBuffer didWrite:bytesRead]; - LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); - - // Search for the terminating sequence - - NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; - LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); - - // Ensure there's room on the read packet's buffer - - [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; - - // Copy bytes from prebuffer into read buffer - - uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset - + currentRead->bytesDone; - - memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); - - // Remove the copied bytes from the prebuffer - [preBuffer didRead:bytesToCopy]; - LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); - - // Update totals - currentRead->bytesDone += bytesToCopy; - totalBytesReadForCurrentRead += bytesToCopy; - - // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above - } - else - { - // We just read a big chunk of data directly into the packet's buffer. - // We need to move any overflow into the prebuffer. - - NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; - - if (overflow == 0) - { - // Perfect match! - // Every byte we read stays in the read buffer, - // and the last byte we read was the last byte of the term. - - currentRead->bytesDone += bytesRead; - totalBytesReadForCurrentRead += bytesRead; - done = YES; - } - else if (overflow > 0) - { - // The term was found within the data that we read, - // and there are extra bytes that extend past the end of the term. - // We need to move these excess bytes out of the read packet and into the prebuffer. - - NSInteger underflow = bytesRead - overflow; - - // Copy excess data into preBuffer - - LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); - [preBuffer ensureCapacityForWrite:overflow]; - - uint8_t *overflowBuffer = buffer + underflow; - memcpy([preBuffer writeBuffer], overflowBuffer, overflow); - - [preBuffer didWrite:overflow]; - LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); - - // Note: The completeCurrentRead method will trim the buffer for us. - - currentRead->bytesDone += underflow; - totalBytesReadForCurrentRead += underflow; - done = YES; - } - else - { - // The term was not found within the data that we read. - - currentRead->bytesDone += bytesRead; - totalBytesReadForCurrentRead += bytesRead; - done = NO; - } - } - - if (!done && currentRead->maxLength > 0) - { - // We're not done and there's a set maxLength. - // Have we reached that maxLength yet? - - if (currentRead->bytesDone >= currentRead->maxLength) - { - error = [self readMaxedOutError]; - } - } - } - else - { - // Read type #1 - read all available data - - if (readIntoPreBuffer) - { - // We just read a chunk of data into the preBuffer - - [preBuffer didWrite:bytesRead]; - - // Now copy the data into the read packet. - // - // Recall that we didn't read directly into the packet's buffer to avoid - // over-allocating memory since we had no clue how much data was available to be read. - // - // Ensure there's room on the read packet's buffer - - [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; - - // Copy bytes from prebuffer into read buffer - - uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset - + currentRead->bytesDone; - - memcpy(readBuf, [preBuffer readBuffer], bytesRead); - - // Remove the copied bytes from the prebuffer - [preBuffer didRead:bytesRead]; - - // Update totals - currentRead->bytesDone += bytesRead; - totalBytesReadForCurrentRead += bytesRead; - } - else - { - currentRead->bytesDone += bytesRead; - totalBytesReadForCurrentRead += bytesRead; - } - - done = YES; - } - - } // if (bytesRead > 0) - - } // if (!done && !error && !socketEOF && hasBytesAvailable) - - - if (!done && currentRead->readLength == 0 && currentRead->term == nil) - { - // Read type #1 - read all available data - // - // We might arrive here if we read data from the prebuffer but not from the socket. - - done = (totalBytesReadForCurrentRead > 0); - } - - // Check to see if we're done, or if we've made progress - - if (done) - { - [self completeCurrentRead]; - - if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) - { - [self maybeDequeueRead]; - } - } - else if (totalBytesReadForCurrentRead > 0) - { - // We're not done read type #2 or #3 yet, but we have read in some bytes - // - // We ensure that `waiting` is set in order to resume the readSource (if it is suspended). It is - // possible to reach this point and `waiting` not be set, if the current read's length is - // sufficiently large. In that case, we may have read to some upperbound successfully, but - // that upperbound could be smaller than the desired length. - waiting = YES; - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) - { - long theReadTag = currentRead->tag; - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; - }}); - } - } - - // Check for errors - - if (error) - { - [self closeWithError:error]; - } - else if (socketEOF) - { - [self doReadEOF]; - } - else if (waiting) - { - if (![self usingCFStreamForTLS]) - { - // Monitor the socket for readability (if we're not already doing so) - [self resumeReadSource]; - } - } - - // Do not add any code here without first adding return statements in the error cases above. -} + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; + return; + } -- (void)doReadEOF -{ - LogTrace(); - - // This method may be called more than once. - // If the EOF is read while there is still data in the preBuffer, - // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. - - flags |= kSocketHasReadEOF; - - if (flags & kSocketSecure) - { - // If the SSL layer has any buffered data, flush it into the preBuffer now. - - [self flushSSLBuffers]; - } - - BOOL shouldDisconnect = NO; - NSError *error = nil; - - if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) - { - // We received an EOF during or prior to startTLS. - // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. - - shouldDisconnect = YES; - - if ([self usingSecureTransportForTLS]) - { - error = [self sslError:errSSLClosedAbort]; - } - } - else if (flags & kReadStreamClosed) - { - // The preBuffer has already been drained. - // The config allows half-duplex connections. - // We've previously checked the socket, and it appeared writeable. - // So we marked the read stream as closed and notified the delegate. - // - // As per the half-duplex contract, the socket will be closed when a write fails, - // or when the socket is manually closed. - - shouldDisconnect = NO; - } - else if ([preBuffer availableBytes] > 0) - { - LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); - - // Although we won't be able to read any more data from the socket, - // there is existing data that has been prebuffered that we can read. - - shouldDisconnect = NO; - } - else if (config & kAllowHalfDuplexConnection) - { - // We just received an EOF (end of file) from the socket's read stream. - // This means the remote end of the socket (the peer we're connected to) - // has explicitly stated that it will not be sending us any more data. - // - // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) - - int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - - struct pollfd pfd[1]; - pfd[0].fd = socketFD; - pfd[0].events = POLLOUT; - pfd[0].revents = 0; - - poll(pfd, 1, 0); - - if (pfd[0].revents & POLLOUT) - { - // Socket appears to still be writeable - - shouldDisconnect = NO; - flags |= kReadStreamClosed; - - // Notify the delegate that we're going half-duplex - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socketDidCloseReadStream:self]; - }}); - } - } - else - { - shouldDisconnect = YES; - } - } - else - { - shouldDisconnect = YES; - } - - - if (shouldDisconnect) - { - if (error == nil) - { - if ([self usingSecureTransportForTLS]) - { - if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) - { - error = [self sslError:sslErrCode]; - } - else - { - error = [self connectionClosedError]; - } - } - else - { - error = [self connectionClosedError]; - } - } - [self closeWithError:error]; - } - else - { - if (![self usingCFStreamForTLS]) - { - // Suspend the read source (if needed) - - [self suspendReadSource]; - } - } -} + // Setup the sslPreBuffer + // + // Any data in the preBuffer needs to be moved into the sslPreBuffer, + // as this data is now part of the secure read stream. -- (void)completeCurrentRead -{ - LogTrace(); - - NSAssert(currentRead, @"Trying to complete current read when there is no current read."); - - - NSData *result = nil; - - if (currentRead->bufferOwner) - { - // We created the buffer on behalf of the user. - // Trim our buffer to be the proper size. - [currentRead->buffer setLength:currentRead->bytesDone]; - - result = currentRead->buffer; - } - else - { - // We did NOT create the buffer. - // The buffer is owned by the caller. - // Only trim the buffer if we had to increase its size. - - if ([currentRead->buffer length] > currentRead->originalBufferLength) - { - NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; - NSUInteger origSize = currentRead->originalBufferLength; - - NSUInteger buffSize = MAX(readSize, origSize); - - [currentRead->buffer setLength:buffSize]; - } - - uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; - - result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; - } - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) - { - GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socket:self didReadData:result withTag:theRead->tag]; - }}); - } - - [self endCurrentRead]; -} + sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; -- (void)endCurrentRead -{ - if (readTimer) - { - dispatch_source_cancel(readTimer); - readTimer = NULL; - } - - currentRead = nil; -} + size_t preBufferLength = [preBuffer availableBytes]; -- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout -{ - if (timeout >= 0.0) - { - readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - - __weak GCDAsyncSocket *weakSelf = self; - - dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf == nil) return_from_block; - - [strongSelf doReadTimeout]; - - #pragma clang diagnostic pop - }}); - - #if !OS_OBJECT_USE_OBJC - dispatch_source_t theReadTimer = readTimer; - dispatch_source_set_cancel_handler(readTimer, ^{ - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - LogVerbose(@"dispatch_release(readTimer)"); - dispatch_release(theReadTimer); - - #pragma clang diagnostic pop - }); - #endif - - dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); - - dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); - dispatch_resume(readTimer); - } -} + if (preBufferLength > 0) + { + [sslPreBuffer ensureCapacityForWrite:preBufferLength]; -- (void)doReadTimeout -{ - // This is a little bit tricky. - // Ideally we'd like to synchronously query the delegate about a timeout extension. - // But if we do so synchronously we risk a possible deadlock. - // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. - - flags |= kReadsPaused; - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) - { - GCDAsyncReadPacket *theRead = currentRead; - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - NSTimeInterval timeoutExtension = 0.0; - - timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag - elapsed:theRead->timeout - bytesDone:theRead->bytesDone]; - - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - [self doReadTimeoutWithExtension:timeoutExtension]; - }}); - }}); - } - else - { - [self doReadTimeoutWithExtension:0.0]; - } -} + memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); + [preBuffer didRead:preBufferLength]; + [sslPreBuffer didWrite:preBufferLength]; + } -- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension -{ - if (currentRead) - { - if (timeoutExtension > 0.0) - { - currentRead->timeout += timeoutExtension; - - // Reschedule the timer - dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); - dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); - - // Unpause reads, and continue - flags &= ~kReadsPaused; - [self doReadData]; - } - else - { - LogVerbose(@"ReadTimeout"); - - [self closeWithError:[self readTimeoutError]]; - } - } -} + sslErrCode = lastSSLHandshakeError = noErr; -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Writing -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Start the SSL Handshake process -- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag -{ - if ([data length] == 0) return; - - GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; - - dispatch_async(socketQueue, ^{ @autoreleasepool { - - LogTrace(); - - if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) - { - [self->writeQueue addObject:packet]; - [self maybeDequeueWrite]; - } - }}); - - // Do not rely on the block being run in order to release the packet, - // as the queue might get released without the block completing. + [self ssl_continueSSLHandshake]; } -- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +- (void)ssl_continueSSLHandshake { - __block float result = 0.0F; - - dispatch_block_t block = ^{ - - if (!self->currentWrite || ![self->currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) - { - // We're not writing anything right now. - - if (tagPtr != NULL) *tagPtr = 0; - if (donePtr != NULL) *donePtr = 0; - if (totalPtr != NULL) *totalPtr = 0; - - result = NAN; - } - else - { - NSUInteger done = self->currentWrite->bytesDone; - NSUInteger total = [self->currentWrite->buffer length]; - - if (tagPtr != NULL) *tagPtr = self->currentWrite->tag; - if (donePtr != NULL) *donePtr = done; - if (totalPtr != NULL) *totalPtr = total; - - result = (float)done / (float)total; - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; -} + LogTrace(); -/** - * Conditionally starts a new write. - * - * It is called when: - * - a user requests a write - * - after a write request has finished (to handle the next request) - * - immediately after the socket opens to handle any pending requests - * - * This method also handles auto-disconnect post read/write completion. -**/ -- (void)maybeDequeueWrite -{ - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - - // If we're not currently processing a write AND we have an available write stream - if ((currentWrite == nil) && (flags & kConnected)) - { - if ([writeQueue count] > 0) - { - // Dequeue the next object in the write queue - currentWrite = [writeQueue objectAtIndex:0]; - [writeQueue removeObjectAtIndex:0]; - - - if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) - { - LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); - - // Attempt to start TLS - flags |= kStartingWriteTLS; - - // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set - [self maybeStartTLS]; - } - else - { - LogVerbose(@"Dequeued GCDAsyncWritePacket"); - - // Setup write timer (if needed) - [self setupWriteTimerWithTimeout:currentWrite->timeout]; - - // Immediately write, if possible - [self doWriteData]; - } - } - else if (flags & kDisconnectAfterWrites) - { - if (flags & kDisconnectAfterReads) - { - if (([readQueue count] == 0) && (currentRead == nil)) - { - [self closeWithError:nil]; - } - } - else - { - [self closeWithError:nil]; - } - } - } -} + // If the return value is noErr, the session is ready for normal secure communication. + // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. + // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the + // server and then call SSLHandshake again to resume the handshake or close the connection + // errSSLPeerBadCert SSL error. + // Otherwise, the return value indicates an error code. -- (void)doWriteData -{ - LogTrace(); - - // This method is called by the writeSource via the socketQueue - - if ((currentWrite == nil) || (flags & kWritesPaused)) - { - LogVerbose(@"No currentWrite or kWritesPaused"); - - // Unable to write at this time - - if ([self usingCFStreamForTLS]) - { - // CFWriteStream only fires once when there is available data. - // It won't fire again until we've invoked CFWriteStreamWrite. - } - else - { - // If the writeSource is firing, we need to pause it - // or else it will continue to fire over and over again. - - if (flags & kSocketCanAcceptBytes) - { - [self suspendWriteSource]; - } - } - return; - } - - if (!(flags & kSocketCanAcceptBytes)) - { - LogVerbose(@"No space available to write..."); - - // No space available to write. - - if (![self usingCFStreamForTLS]) - { - // Need to wait for writeSource to fire and notify us of - // available space in the socket's internal write buffer. - - [self resumeWriteSource]; - } - return; - } - - if (flags & kStartingWriteTLS) - { - LogVerbose(@"Waiting for SSL/TLS handshake to complete"); - - // The writeQueue is waiting for SSL/TLS handshake to complete. - - if (flags & kStartingReadTLS) - { - if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) - { - // We are in the process of a SSL Handshake. - // We were waiting for available space in the socket's internal OS buffer to continue writing. - - [self ssl_continueSSLHandshake]; - } - } - else - { - // We are still waiting for the readQueue to drain and start the SSL/TLS process. - // We now know we can write to the socket. - - if (![self usingCFStreamForTLS]) - { - // Suspend the write source or else it will continue to fire nonstop. - - [self suspendWriteSource]; - } - } - - return; - } - - // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) - - BOOL waiting = NO; - NSError *error = nil; - size_t bytesWritten = 0; - - if (flags & kSocketSecure) - { - if ([self usingCFStreamForTLS]) - { - #if TARGET_OS_IPHONE - - // - // Writing data using CFStream (over internal TLS) - // - - const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; - - NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; - - if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) - { - bytesToWrite = SIZE_MAX; - } - - CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); - LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); - - if (result < 0) - { - error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); - } - else - { - bytesWritten = (size_t)result; - - // We always set waiting to true in this scenario. - // CFStream may have altered our underlying socket to non-blocking. - // Thus if we attempt to write without a callback, we may end up blocking our queue. - waiting = YES; - } - - #endif - } - else - { - // We're going to use the SSLWrite function. - // - // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) - // - // Parameters: - // context - An SSL session context reference. - // data - A pointer to the buffer of data to write. - // dataLength - The amount, in bytes, of data to write. - // processed - On return, the length, in bytes, of the data actually written. - // - // It sounds pretty straight-forward, - // but there are a few caveats you should be aware of. - // - // The SSLWrite method operates in a non-obvious (and rather annoying) manner. - // According to the documentation: - // - // Because you may configure the underlying connection to operate in a non-blocking manner, - // a write operation might return errSSLWouldBlock, indicating that less data than requested - // was actually transferred. In this case, you should repeat the call to SSLWrite until some - // other result is returned. - // - // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, - // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), - // but it sets processed to dataLength !! - // - // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, - // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to - // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. - // - // You might be wondering: - // If the SSLWrite function doesn't tell us how many bytes were written, - // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) - // for the next time we invoke SSLWrite? - // - // The answer is that SSLWrite cached all the data we told it to write, - // and it will push out that data next time we call SSLWrite. - // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. - // If we call SSLWrite with empty data, then it will simply push out the cached data. - // - // For this purpose we're going to break large writes into a series of smaller writes. - // This allows us to report progress back to the delegate. - - OSStatus result; - - BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); - BOOL hasNewDataToWrite = YES; - - if (hasCachedDataToWrite) - { - size_t processed = 0; - - result = SSLWrite(sslContext, NULL, 0, &processed); - - if (result == noErr) - { - bytesWritten = sslWriteCachedLength; - sslWriteCachedLength = 0; - - if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) - { - // We've written all data for the current write. - hasNewDataToWrite = NO; - } - } - else - { - if (result == errSSLWouldBlock) - { - waiting = YES; - } - else - { - error = [self sslError:result]; - } - - // Can't write any new data since we were unable to write the cached data. - hasNewDataToWrite = NO; - } - } - - if (hasNewDataToWrite) - { - const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] - + currentWrite->bytesDone - + bytesWritten; - - NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; - - if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) - { - bytesToWrite = SIZE_MAX; - } - - size_t bytesRemaining = bytesToWrite; - - BOOL keepLooping = YES; - while (keepLooping) - { - const size_t sslMaxBytesToWrite = 32768; - size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); - size_t sslBytesWritten = 0; - - result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); - - if (result == noErr) - { - buffer += sslBytesWritten; - bytesWritten += sslBytesWritten; - bytesRemaining -= sslBytesWritten; - - keepLooping = (bytesRemaining > 0); - } - else - { - if (result == errSSLWouldBlock) - { - waiting = YES; - sslWriteCachedLength = sslBytesToWrite; - } - else - { - error = [self sslError:result]; - } - - keepLooping = NO; - } - - } // while (keepLooping) - - } // if (hasNewDataToWrite) - } - } - else - { - // - // Writing data directly over raw socket - // - - int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - - const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; - - NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; - - if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) - { - bytesToWrite = SIZE_MAX; - } - - ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); - LogVerbose(@"wrote to socket = %zd", result); - - // Check results - if (result < 0) - { - if (errno == EWOULDBLOCK) - { - waiting = YES; - } - else - { - error = [self errorWithErrno:errno reason:@"Error in write() function"]; - } - } - else - { - bytesWritten = result; - } - } - - // We're done with our writing. - // If we explictly ran into a situation where the socket told us there was no room in the buffer, - // then we immediately resume listening for notifications. - // - // We must do this before we dequeue another write, - // as that may in turn invoke this method again. - // - // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. - - if (waiting) - { - flags &= ~kSocketCanAcceptBytes; - - if (![self usingCFStreamForTLS]) - { - [self resumeWriteSource]; - } - } - - // Check our results - - BOOL done = NO; - - if (bytesWritten > 0) - { - // Update total amount read for the current write - currentWrite->bytesDone += bytesWritten; - LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); - - // Is packet done? - done = (currentWrite->bytesDone == [currentWrite->buffer length]); - } - - if (done) - { - [self completeCurrentWrite]; - - if (!error) - { - dispatch_async(socketQueue, ^{ @autoreleasepool{ - - [self maybeDequeueWrite]; - }}); - } - } - else - { - // We were unable to finish writing the data, - // so we're waiting for another callback to notify us of available space in the lower-level output buffer. - - if (!waiting && !error) - { - // This would be the case if our write was able to accept some data, but not all of it. - - flags &= ~kSocketCanAcceptBytes; - - if (![self usingCFStreamForTLS]) - { - [self resumeWriteSource]; - } - } - - if (bytesWritten > 0) - { - // We're not done with the entire write, but we have written some bytes - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) - { - long theWriteTag = currentWrite->tag; - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; - }}); - } - } - } - - // Check for errors - - if (error) - { - [self closeWithError:[self errorWithErrno:errno reason:@"Error in write() function"]]; - } - - // Do not add any code here without first adding a return statement in the error case above. -} + OSStatus status = SSLHandshake(sslContext); + lastSSLHandshakeError = status; -- (void)completeCurrentWrite -{ - LogTrace(); - - NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); - - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) - { - long theWriteTag = currentWrite->tag; - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socket:self didWriteDataWithTag:theWriteTag]; - }}); - } - - [self endCurrentWrite]; -} + if (status == noErr) + { + LogVerbose(@"SSLHandshake complete"); -- (void)endCurrentWrite -{ - if (writeTimer) - { - dispatch_source_cancel(writeTimer); - writeTimer = NULL; - } - - currentWrite = nil; -} + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; -- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout -{ - if (timeout >= 0.0) - { - writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - - __weak GCDAsyncSocket *weakSelf = self; - - dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf == nil) return_from_block; - - [strongSelf doWriteTimeout]; - - #pragma clang diagnostic pop - }}); - - #if !OS_OBJECT_USE_OBJC - dispatch_source_t theWriteTimer = writeTimer; - dispatch_source_set_cancel_handler(writeTimer, ^{ - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - LogVerbose(@"dispatch_release(writeTimer)"); - dispatch_release(theWriteTimer); - - #pragma clang diagnostic pop - }); - #endif - - dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); - - dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); - dispatch_resume(writeTimer); - } -} + flags |= kSocketSecure; -- (void)doWriteTimeout -{ - // This is a little bit tricky. - // Ideally we'd like to synchronously query the delegate about a timeout extension. - // But if we do so synchronously we risk a possible deadlock. - // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. - - flags |= kWritesPaused; - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) - { - GCDAsyncWritePacket *theWrite = currentWrite; - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - NSTimeInterval timeoutExtension = 0.0; - - timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag - elapsed:theWrite->timeout - bytesDone:theWrite->bytesDone]; - - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - [self doWriteTimeoutWithExtension:timeoutExtension]; - }}); - }}); - } - else - { - [self doWriteTimeoutWithExtension:0.0]; - } -} + __strong id theDelegate = delegate; -- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension -{ - if (currentWrite) - { - if (timeoutExtension > 0.0) - { - currentWrite->timeout += timeoutExtension; - - // Reschedule the timer - dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); - dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); - - // Unpause writes, and continue - flags &= ~kWritesPaused; - [self doWriteData]; - } - else - { - LogVerbose(@"WriteTimeout"); - - [self closeWithError:[self writeTimeoutError]]; - } - } -} + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Security -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + [theDelegate socketDidSecure:self]; + }}); + } -- (void)startTLS:(NSDictionary *)tlsSettings -{ - LogTrace(); - - if (tlsSettings == nil) - { - // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, - // but causes problems if we later try to fetch the remote host's certificate. - // - // To be exact, it causes the following to return NULL instead of the normal result: - // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) - // - // So we use an empty dictionary instead, which works perfectly. - - tlsSettings = [NSDictionary dictionary]; - } - - GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; - - dispatch_async(socketQueue, ^{ @autoreleasepool { - - if ((self->flags & kSocketStarted) && !(self->flags & kQueuedTLS) && !(self->flags & kForbidReadsWrites)) - { - [self->readQueue addObject:packet]; - [self->writeQueue addObject:packet]; - - self->flags |= kQueuedTLS; - - [self maybeDequeueRead]; - [self maybeDequeueWrite]; - } - }}); - -} + [self endCurrentRead]; + [self endCurrentWrite]; -- (void)maybeStartTLS -{ - // We can't start TLS until: - // - All queued reads prior to the user calling startTLS are complete - // - All queued writes prior to the user calling startTLS are complete - // - // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set - - if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) - { - BOOL useSecureTransport = YES; - - #if TARGET_OS_IPHONE - { - GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; - NSDictionary *tlsSettings = @{}; - if (tlsPacket) { - tlsSettings = tlsPacket->tlsSettings; - } - NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; - if (value && [value boolValue]) - useSecureTransport = NO; - } - #endif - - if (useSecureTransport) - { - [self ssl_startTLS]; - } - else - { - #if TARGET_OS_IPHONE - [self cf_startTLS]; - #endif - } - } -} + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + else if (status == errSSLPeerAuthCompleted) + { + LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#pragma mark Security via SecureTransport -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + __block SecTrustRef trust = NULL; + status = SSLCopyPeerTrust(sslContext, &trust); + if (status != noErr) + { + [self closeWithError:[self sslError:status]]; + return; + } -- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength -{ - LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); - - if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) - { - LogVerbose(@"%@ - No data available to read...", THIS_METHOD); - - // No data available to read. - // - // Need to wait for readSource to fire and notify us of - // available data in the socket's internal read buffer. - - [self resumeReadSource]; - - *bufferLength = 0; - return errSSLWouldBlock; - } - - size_t totalBytesRead = 0; - size_t totalBytesLeftToBeRead = *bufferLength; - - BOOL done = NO; - BOOL socketError = NO; - - // - // STEP 1 : READ FROM SSL PRE BUFFER - // - - size_t sslPreBufferLength = [sslPreBuffer availableBytes]; - - if (sslPreBufferLength > 0) - { - LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); - - size_t bytesToCopy; - if (sslPreBufferLength > totalBytesLeftToBeRead) - bytesToCopy = totalBytesLeftToBeRead; - else - bytesToCopy = sslPreBufferLength; - - LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); - - memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); - [sslPreBuffer didRead:bytesToCopy]; - - LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); - - totalBytesRead += bytesToCopy; - totalBytesLeftToBeRead -= bytesToCopy; - - done = (totalBytesLeftToBeRead == 0); - - if (done) LogVerbose(@"%@: Complete", THIS_METHOD); - } - - // - // STEP 2 : READ FROM SOCKET - // - - if (!done && (socketFDBytesAvailable > 0)) - { - LogVerbose(@"%@: Reading from socket...", THIS_METHOD); - - int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - - BOOL readIntoPreBuffer; - size_t bytesToRead; - uint8_t *buf; - - if (socketFDBytesAvailable > totalBytesLeftToBeRead) - { - // Read all available data from socket into sslPreBuffer. - // Then copy requested amount into dataBuffer. - - LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); - - [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; - - readIntoPreBuffer = YES; - bytesToRead = (size_t)socketFDBytesAvailable; - buf = [sslPreBuffer writeBuffer]; - } - else - { - // Read available data from socket directly into dataBuffer. - - LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); - - readIntoPreBuffer = NO; - bytesToRead = totalBytesLeftToBeRead; - buf = (uint8_t *)buffer + totalBytesRead; - } - - ssize_t result = read(socketFD, buf, bytesToRead); - LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); - - if (result < 0) - { - LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); - - if (errno != EWOULDBLOCK) - { - socketError = YES; - } - - socketFDBytesAvailable = 0; - } - else if (result == 0) - { - LogVerbose(@"%@: read EOF", THIS_METHOD); - - socketError = YES; - socketFDBytesAvailable = 0; - } - else - { - size_t bytesReadFromSocket = result; - - if (socketFDBytesAvailable > bytesReadFromSocket) - socketFDBytesAvailable -= bytesReadFromSocket; - else - socketFDBytesAvailable = 0; - - if (readIntoPreBuffer) - { - [sslPreBuffer didWrite:bytesReadFromSocket]; - - size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); - - LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); - - memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); - [sslPreBuffer didRead:bytesToCopy]; - - totalBytesRead += bytesToCopy; - totalBytesLeftToBeRead -= bytesToCopy; - - LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); - } - else - { - totalBytesRead += bytesReadFromSocket; - totalBytesLeftToBeRead -= bytesReadFromSocket; - } - - done = (totalBytesLeftToBeRead == 0); - - if (done) LogVerbose(@"%@: Complete", THIS_METHOD); - } - } - - *bufferLength = totalBytesRead; - - if (done) - return noErr; - - if (socketError) - return errSSLClosedAbort; - - return errSSLWouldBlock; -} + int aStateIndex = stateIndex; + dispatch_queue_t theSocketQueue = socketQueue; -- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength -{ - if (!(flags & kSocketCanAcceptBytes)) - { - // Unable to write. - // - // Need to wait for writeSource to fire and notify us of - // available space in the socket's internal write buffer. - - [self resumeWriteSource]; - - *bufferLength = 0; - return errSSLWouldBlock; - } - - size_t bytesToWrite = *bufferLength; - size_t bytesWritten = 0; - - BOOL done = NO; - BOOL socketError = NO; - - int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - - ssize_t result = write(socketFD, buffer, bytesToWrite); - - if (result < 0) - { - if (errno != EWOULDBLOCK) - { - socketError = YES; - } - - flags &= ~kSocketCanAcceptBytes; - } - else if (result == 0) - { - flags &= ~kSocketCanAcceptBytes; - } - else - { - bytesWritten = result; - - done = (bytesWritten == bytesToWrite); - } - - *bufferLength = bytesWritten; - - if (done) - return noErr; - - if (socketError) - return errSSLClosedAbort; - - return errSSLWouldBlock; -} - -static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) -{ - GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; - - NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); - - return [asyncSocket sslReadWithBuffer:data length:dataLength]; -} - -static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) -{ - GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; - - NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); - - return [asyncSocket sslWriteWithBuffer:data length:dataLength]; -} + __weak GCDAsyncSocket *weakSelf = self; -- (void)ssl_startTLS -{ - LogTrace(); - - LogVerbose(@"Starting TLS (via SecureTransport)..."); - - OSStatus status; - - GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; - if (tlsPacket == nil) // Code to quiet the analyzer - { - NSAssert(NO, @"Logic error"); - - [self closeWithError:[self otherError:@"Logic error"]]; - return; - } - NSDictionary *tlsSettings = tlsPacket->tlsSettings; - - // Create SSLContext, and setup IO callbacks and connection ref - - NSNumber *isServerNumber = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer]; - BOOL isServer = [isServerNumber boolValue]; - - #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) - { - if (isServer) - sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); - else - sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); - - if (sslContext == NULL) - { - [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; - return; - } - } - #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) - { - status = SSLNewContext(isServer, &sslContext); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; - return; - } - } - #endif - - status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; - return; - } - - status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; - return; - } - - - NSNumber *shouldManuallyEvaluateTrust = [tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust]; - if ([shouldManuallyEvaluateTrust boolValue]) - { - if (isServer) - { - [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; - return; - } - - status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; - return; - } - - #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) - - // Note from Apple's documentation: - // - // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. - // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the - // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus - // SSLSetEnableCertVerify is not available on that platform at all. - - status = SSLSetEnableCertVerify(sslContext, NO); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; - return; - } - - #endif - } - - // Configure SSLContext from given settings - // - // Checklist: - // 1. kCFStreamSSLPeerName - // 2. kCFStreamSSLCertificates - // 3. GCDAsyncSocketSSLPeerID - // 4. GCDAsyncSocketSSLProtocolVersionMin - // 5. GCDAsyncSocketSSLProtocolVersionMax - // 6. GCDAsyncSocketSSLSessionOptionFalseStart - // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord - // 8. GCDAsyncSocketSSLCipherSuites - // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) - // 10. GCDAsyncSocketSSLALPN - // - // Deprecated (throw error): - // 10. kCFStreamSSLAllowsAnyRoot - // 11. kCFStreamSSLAllowsExpiredRoots - // 12. kCFStreamSSLAllowsExpiredCertificates - // 13. kCFStreamSSLValidatesCertificateChain - // 14. kCFStreamSSLLevel - - NSObject *value; - - // 1. kCFStreamSSLPeerName - - value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName]; - if ([value isKindOfClass:[NSString class]]) - { - NSString *peerName = (NSString *)value; - - const char *peer = [peerName UTF8String]; - size_t peerLen = strlen(peer); - - status = SSLSetPeerDomainName(sslContext, peer, peerLen); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; - return; - } - } - else if (value) - { - NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); - - [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; - return; - } - - // 2. kCFStreamSSLCertificates - - value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLCertificates]; - if ([value isKindOfClass:[NSArray class]]) - { - NSArray *certs = (NSArray *)value; - - status = SSLSetCertificate(sslContext, (__bridge CFArrayRef)certs); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; - return; - } - } - else if (value) - { - NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); - - [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; - return; - } - - // 3. GCDAsyncSocketSSLPeerID - - value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; - if ([value isKindOfClass:[NSData class]]) - { - NSData *peerIdData = (NSData *)value; - - status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; - return; - } - } - else if (value) - { - NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." - @" (You can convert strings to data using a method like" - @" [string dataUsingEncoding:NSUTF8StringEncoding])"); - - [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; - return; - } - - // 4. GCDAsyncSocketSSLProtocolVersionMin - - value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; - if ([value isKindOfClass:[NSNumber class]]) - { - SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; - if (minProtocol != kSSLProtocolUnknown) - { - status = SSLSetProtocolVersionMin(sslContext, minProtocol); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; - return; - } - } - } - else if (value) - { - NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); - - [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; - return; - } - - // 5. GCDAsyncSocketSSLProtocolVersionMax - - value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; - if ([value isKindOfClass:[NSNumber class]]) - { - SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; - if (maxProtocol != kSSLProtocolUnknown) - { - status = SSLSetProtocolVersionMax(sslContext, maxProtocol); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; - return; - } - } - } - else if (value) - { - NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); - - [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; - return; - } - - // 6. GCDAsyncSocketSSLSessionOptionFalseStart - - value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; - if ([value isKindOfClass:[NSNumber class]]) - { - NSNumber *falseStart = (NSNumber *)value; - status = SSLSetSessionOption(sslContext, kSSLSessionOptionFalseStart, [falseStart boolValue]); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; - return; - } - } - else if (value) - { - NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); - - [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; - return; - } - - // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord - - value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; - if ([value isKindOfClass:[NSNumber class]]) - { - NSNumber *oneByteRecord = (NSNumber *)value; - status = SSLSetSessionOption(sslContext, kSSLSessionOptionSendOneByteRecord, [oneByteRecord boolValue]); - if (status != noErr) - { - [self closeWithError: - [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; - return; - } - } - else if (value) - { - NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." - @" Value must be of type NSNumber."); - - [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; - return; - } - - // 8. GCDAsyncSocketSSLCipherSuites - - value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; - if ([value isKindOfClass:[NSArray class]]) - { - NSArray *cipherSuites = (NSArray *)value; - NSUInteger numberCiphers = [cipherSuites count]; + void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { #pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wvla" - SSLCipherSuite ciphers[numberCiphers]; -#pragma clang diagnostic pop - - NSUInteger cipherIndex; - for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) - { - NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; - ciphers[cipherIndex] = (SSLCipherSuite)[cipherObject unsignedIntValue]; - } - - status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; - return; - } - } - else if (value) - { - NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); - - [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; - return; - } - - // 9. GCDAsyncSocketSSLDiffieHellmanParameters - - #if !TARGET_OS_IPHONE - value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; - if ([value isKindOfClass:[NSData class]]) - { - NSData *diffieHellmanData = (NSData *)value; - - status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; - return; - } - } - else if (value) - { - NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); - - [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; - return; - } - #endif - - // 10. kCFStreamSSLCertificates - value = [tlsSettings objectForKey:GCDAsyncSocketSSLALPN]; - if ([value isKindOfClass:[NSArray class]]) - { - if (@available(iOS 11.0, macOS 10.13, tvOS 11.0, *)) - { - CFArrayRef protocols = (__bridge CFArrayRef)((NSArray *) value); - status = SSLSetALPNProtocols(sslContext, protocols); - if (status != noErr) - { - [self closeWithError:[self otherError:@"Error in SSLSetALPNProtocols"]]; - return; - } +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + dispatch_async(theSocketQueue, ^{ @autoreleasepool { + + if (trust) { + CFRelease(trust); + trust = NULL; } - else + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf) { - NSAssert(NO, @"Security option unavailable - GCDAsyncSocketSSLALPN" - @" - iOS 11.0, macOS 10.13 required"); - [self closeWithError:[self otherError:@"Security option unavailable - GCDAsyncSocketSSLALPN"]]; + [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; } + }}); + +#pragma clang diagnostic pop + }}; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) + { + dispatch_async(delegateQueue, + ^{ @autoreleasepool { + + [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; + }}); } - else if (value) + else { - NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLALPN. Value must be of type NSArray."); - - [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLALPN."]]; - return; + if (trust) { + CFRelease(trust); + trust = NULL; + } + + NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," + @" but delegate doesn't implement socket:shouldTrustPeer:"; + + [self closeWithError:[self otherError:msg]]; + return; } - - // DEPRECATED checks - - // 10. kCFStreamSSLAllowsAnyRoot - - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated-declarations" - value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsAnyRoot]; - #pragma clang diagnostic pop - if (value) - { - NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" - @" - You must use manual trust evaluation"); - - [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; - return; - } - - // 11. kCFStreamSSLAllowsExpiredRoots - - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated-declarations" - value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredRoots]; - #pragma clang diagnostic pop - if (value) - { - NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" - @" - You must use manual trust evaluation"); - - [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; - return; - } - - // 12. kCFStreamSSLValidatesCertificateChain - - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated-declarations" - value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLValidatesCertificateChain]; - #pragma clang diagnostic pop - if (value) - { - NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" - @" - You must use manual trust evaluation"); - - [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; - return; - } - - // 13. kCFStreamSSLAllowsExpiredCertificates - - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated-declarations" - value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredCertificates]; - #pragma clang diagnostic pop - if (value) - { - NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" - @" - You must use manual trust evaluation"); - - [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; - return; - } - - // 14. kCFStreamSSLLevel - - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdeprecated-declarations" - value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLLevel]; - #pragma clang diagnostic pop - if (value) - { - NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" - @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); - - [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; - return; - } - - // Setup the sslPreBuffer - // - // Any data in the preBuffer needs to be moved into the sslPreBuffer, - // as this data is now part of the secure read stream. - - sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; - - size_t preBufferLength = [preBuffer availableBytes]; - - if (preBufferLength > 0) - { - [sslPreBuffer ensureCapacityForWrite:preBufferLength]; - - memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); - [preBuffer didRead:preBufferLength]; - [sslPreBuffer didWrite:preBufferLength]; - } - - sslErrCode = lastSSLHandshakeError = noErr; - - // Start the SSL Handshake process - - [self ssl_continueSSLHandshake]; -} + } + else if (status == errSSLWouldBlock) + { + LogVerbose(@"SSLHandshake continues..."); -- (void)ssl_continueSSLHandshake -{ - LogTrace(); - - // If the return value is noErr, the session is ready for normal secure communication. - // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. - // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the - // server and then call SSLHandshake again to resume the handshake or close the connection - // errSSLPeerBadCert SSL error. - // Otherwise, the return value indicates an error code. - - OSStatus status = SSLHandshake(sslContext); - lastSSLHandshakeError = status; - - if (status == noErr) - { - LogVerbose(@"SSLHandshake complete"); - - flags &= ~kStartingReadTLS; - flags &= ~kStartingWriteTLS; - - flags |= kSocketSecure; - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socketDidSecure:self]; - }}); - } - - [self endCurrentRead]; - [self endCurrentWrite]; - - [self maybeDequeueRead]; - [self maybeDequeueWrite]; - } - else if (status == errSSLPeerAuthCompleted) - { - LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); - - __block SecTrustRef trust = NULL; - status = SSLCopyPeerTrust(sslContext, &trust); - if (status != noErr) - { - [self closeWithError:[self sslError:status]]; - return; - } - - int aStateIndex = stateIndex; - dispatch_queue_t theSocketQueue = socketQueue; - - __weak GCDAsyncSocket *weakSelf = self; - - void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - dispatch_async(theSocketQueue, ^{ @autoreleasepool { - - if (trust) { - CFRelease(trust); - trust = NULL; - } - - __strong GCDAsyncSocket *strongSelf = weakSelf; - if (strongSelf) - { - [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; - } - }}); - - #pragma clang diagnostic pop - }}; - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; - }}); - } - else - { - if (trust) { - CFRelease(trust); - trust = NULL; - } - - NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," - @" but delegate doesn't implement socket:shouldTrustPeer:"; - - [self closeWithError:[self otherError:msg]]; - return; - } - } - else if (status == errSSLWouldBlock) - { - LogVerbose(@"SSLHandshake continues..."); - - // Handshake continues... - // - // This method will be called again from doReadData or doWriteData. - } - else - { - [self closeWithError:[self sslError:status]]; - } + // Handshake continues... + // + // This method will be called again from doReadData or doWriteData. + } + else + { + [self closeWithError:[self sslError:status]]; + } } - (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex { - LogTrace(); - - if (aStateIndex != stateIndex) - { - LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); - - // One of the following is true - // - the socket was disconnected - // - the startTLS operation timed out - // - the completionHandler was already invoked once - - return; - } - - // Increment stateIndex to ensure completionHandler can only be called once. - stateIndex++; - - if (shouldTrust) - { - NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError); - [self ssl_continueSSLHandshake]; - } - else - { - [self closeWithError:[self sslError:errSSLPeerBadCert]]; - } + LogTrace(); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); + + // One of the following is true + // - the socket was disconnected + // - the startTLS operation timed out + // - the completionHandler was already invoked once + + return; + } + + // Increment stateIndex to ensure completionHandler can only be called once. + stateIndex++; + + if (shouldTrust) + { + NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, + @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", + (int)lastSSLHandshakeError); + [self ssl_continueSSLHandshake]; + } + else + { + [self closeWithError:[self sslError:errSSLPeerBadCert]]; + } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Security via CFStream //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -#if TARGET_OS_IPHONE +#if TARGET_OS_IPHONE + +- (void)cf_finishSSLHandshake +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } -- (void)cf_finishSSLHandshake -{ - LogTrace(); - - if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) - { - flags &= ~kStartingReadTLS; - flags &= ~kStartingWriteTLS; - - flags |= kSocketSecure; - - __strong id theDelegate = delegate; - - if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate socketDidSecure:self]; - }}); - } - - [self endCurrentRead]; - [self endCurrentWrite]; - - [self maybeDequeueRead]; - [self maybeDequeueWrite]; - } + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } } - (void)cf_abortSSLHandshake:(NSError *)error { - LogTrace(); - - if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) - { - flags &= ~kStartingReadTLS; - flags &= ~kStartingWriteTLS; - - [self closeWithError:error]; - } + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + [self closeWithError:error]; + } } - (void)cf_startTLS { - LogTrace(); - - LogVerbose(@"Starting TLS (via CFStream)..."); - - if ([preBuffer availableBytes] > 0) - { - NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; - - [self closeWithError:[self otherError:msg]]; - return; - } - - [self suspendReadSource]; - [self suspendWriteSource]; - - socketFDBytesAvailable = 0; - flags &= ~kSocketCanAcceptBytes; - flags &= ~kSecureSocketHasBytesAvailable; - - flags |= kUsingCFStreamForTLS; - - if (![self createReadAndWriteStream]) - { - [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; - return; - } - - if (![self registerForStreamCallbacksIncludingReadWrite:YES]) - { - [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; - return; - } - - if (![self addStreamsToRunLoop]) - { - [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; - return; - } - - NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); - NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); - - GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; - CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; - - // Getting an error concerning kCFStreamPropertySSLSettings ? - // You need to add the CFNetwork framework to your iOS application. - - BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); - BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); - - // For some reason, starting around the time of iOS 4.3, - // the first call to set the kCFStreamPropertySSLSettings will return true, - // but the second will return false. - // - // Order doesn't seem to matter. - // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. - // Either way, the first call will return true, and the second returns false. - // - // Interestingly, this doesn't seem to affect anything. - // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) - // setting it on one side of the stream automatically sets it for the other side of the stream. - // - // Although there isn't anything in the documentation to suggest that the second attempt would fail. - // - // Furthermore, this only seems to affect streams that are negotiating a security upgrade. - // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure - // connection, and then a startTLS is issued. - // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). - - if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. - { - [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; - return; - } - - if (![self openStreams]) - { - [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; - return; - } - - LogVerbose(@"Waiting for SSL Handshake to complete..."); + LogTrace(); + + LogVerbose(@"Starting TLS (via CFStream)..."); + + if ([preBuffer availableBytes] > 0) + { + NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + [self suspendReadSource]; + [self suspendWriteSource]; + + socketFDBytesAvailable = 0; + flags &= ~kSocketCanAcceptBytes; + flags &= ~kSecureSocketHasBytesAvailable; + + flags |= kUsingCFStreamForTLS; + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:YES]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], + @"Invalid read packet for startTLS"); + NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], + @"Invalid write packet for startTLS"); + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; + + // Getting an error concerning kCFStreamPropertySSLSettings ? + // CFNetwork/CFStream.h is imported for SSL constants needed by CFStream TLS support. + + BOOL r1 = CFReadStreamSetProperty(readStream, + kCFStreamPropertySSLSettings, + tlsSettings); + BOOL r2 = CFWriteStreamSetProperty(writeStream, + kCFStreamPropertySSLSettings, + tlsSettings); + + // For some reason, starting around the time of iOS 4.3, + // the first call to set the kCFStreamPropertySSLSettings will return true, + // but the second will return false. + // + // Order doesn't seem to matter. + // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. + // Either way, the first call will return true, and the second returns false. + // + // Interestingly, this doesn't seem to affect anything. + // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) + // setting it on one side of the stream automatically sets it for the other side of the stream. + // + // Although there isn't anything in the documentation to suggest that the second attempt would fail. + // + // Furthermore, this only seems to affect streams that are negotiating a security upgrade. + // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure + // connection, and then a startTLS is issued. + // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). + + if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. + { + [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; + return; + } + + LogVerbose(@"Waiting for SSL Handshake to complete..."); } #endif @@ -7600,427 +7787,471 @@ + (void)ignore:(id)_ + (void)startCFStreamThreadIfNeeded { - LogTrace(); - - static dispatch_once_t predicate; - dispatch_once(&predicate, ^{ - - cfstreamThreadRetainCount = 0; - cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); - }); - - dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { - - if (++cfstreamThreadRetainCount == 1) - { - cfstreamThread = [[NSThread alloc] initWithTarget:self - selector:@selector(cfstreamThread:) - object:nil]; - [cfstreamThread start]; - } - }}); + LogTrace(); + + static dispatch_once_t predicate; + dispatch_once(&predicate, + ^{ + + cfstreamThreadRetainCount = 0; + cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", + DISPATCH_QUEUE_SERIAL); + }); + + dispatch_sync(cfstreamThreadSetupQueue, + ^{ @autoreleasepool { + + if (++cfstreamThreadRetainCount == 1) + { + cfstreamThread = [[NSThread alloc] initWithTarget:self + selector:@selector(cfstreamThread:) + object:nil]; + [cfstreamThread start]; + } + }}); } + (void)stopCFStreamThreadIfNeeded { - LogTrace(); - - // The creation of the cfstreamThread is relatively expensive. - // So we'd like to keep it available for recycling. - // However, there's a tradeoff here, because it shouldn't remain alive forever. - // So what we're going to do is use a little delay before taking it down. - // This way it can be reused properly in situations where multiple sockets are continually in flux. - - int delayInSeconds = 30; - dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); - dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { - #pragma clang diagnostic push - #pragma clang diagnostic warning "-Wimplicit-retain-self" - - if (cfstreamThreadRetainCount == 0) - { - LogWarn(@"Logic error concerning cfstreamThread start / stop"); - return_from_block; - } - - if (--cfstreamThreadRetainCount == 0) - { - [cfstreamThread cancel]; // set isCancelled flag - - // wake up the thread - [[self class] performSelector:@selector(ignore:) - onThread:cfstreamThread - withObject:[NSNull null] - waitUntilDone:NO]; - - cfstreamThread = nil; - } - - #pragma clang diagnostic pop - }}); + LogTrace(); + + // The creation of the cfstreamThread is relatively expensive. + // So we'd like to keep it available for recycling. + // However, there's a tradeoff here, because it shouldn't remain alive forever. + // So what we're going to do is use a little delay before taking it down. + // This way it can be reused properly in situations where multiple sockets are continually in flux. + + int delayInSeconds = 30; + dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wimplicit-retain-self" + + if (cfstreamThreadRetainCount == 0) + { + LogWarn(@"Logic error concerning cfstreamThread start / stop"); + return_from_block; + } + + if (--cfstreamThreadRetainCount == 0) + { + [cfstreamThread cancel]; // set isCancelled flag + + // wake up the thread + [[self class] performSelector:@selector(ignore:) + onThread:cfstreamThread + withObject:[NSNull null] + waitUntilDone:NO]; + + cfstreamThread = nil; + } + +#pragma clang diagnostic pop + }}); } + (void)cfstreamThread:(id)unused { @autoreleasepool -{ - [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; - - LogInfo(@"CFStreamThread: Started"); - - // We can't run the run loop unless it has an associated input source or a timer. - // So we'll just create a timer that will never fire - unless the server runs for decades. - [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] - target:self - selector:@selector(ignore:) - userInfo:nil - repeats:YES]; - - NSThread *currentThread = [NSThread currentThread]; - NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; - - BOOL isCancelled = [currentThread isCancelled]; - - while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) - { - isCancelled = [currentThread isCancelled]; - } - - LogInfo(@"CFStreamThread: Stopped"); -}} + { + [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; + + LogInfo(@"CFStreamThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + NSThread *currentThread = [NSThread currentThread]; + NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; + + BOOL isCancelled = [currentThread isCancelled]; + + while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) + { + isCancelled = [currentThread isCancelled]; + } + + LogInfo(@"CFStreamThread: Stopped"); + }} + (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket { - LogTrace(); - NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); - - CFRunLoopRef runLoop = CFRunLoopGetCurrent(); - - if (asyncSocket->readStream) - CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); - - if (asyncSocket->writeStream) - CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, + @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, + runLoop, + kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, + runLoop, + kCFRunLoopDefaultMode); } + (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket { - LogTrace(); - NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); - - CFRunLoopRef runLoop = CFRunLoopGetCurrent(); - - if (asyncSocket->readStream) - CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); - - if (asyncSocket->writeStream) - CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, + @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, + runLoop, + kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, + runLoop, + kCFRunLoopDefaultMode); } -static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +static void CFReadStreamCallback (CFReadStreamRef stream, + CFStreamEventType type, + void *pInfo) { - GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wswitch-enum" - switch(type) - { - case kCFStreamEventHasBytesAvailable: - { - dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { - - LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); - - if (asyncSocket->readStream != stream) - return_from_block; - - if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) - { - // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. - // (A callback related to the tcp stream, but not to the SSL layer). - - if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) - { - asyncSocket->flags |= kSecureSocketHasBytesAvailable; - [asyncSocket cf_finishSSLHandshake]; - } - } - else - { - asyncSocket->flags |= kSecureSocketHasBytesAvailable; - [asyncSocket doReadData]; - } - }}); - - break; - } - default: - { - NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); - - if (error == nil && type == kCFStreamEventEndEncountered) - { - error = [asyncSocket connectionClosedError]; - } - - dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { - - LogCVerbose(@"CFReadStreamCallback - Other"); - - if (asyncSocket->readStream != stream) - return_from_block; - - if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) - { - [asyncSocket cf_abortSSLHandshake:error]; - } - else - { - [asyncSocket closeWithError:error]; - } - }}); - - break; - } - } + switch(type) + { + case kCFStreamEventHasBytesAvailable: + { + dispatch_async(asyncSocket->socketQueue, + ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket doReadData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, + ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - Other"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } #pragma clang diagnostic pop } -static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +static void CFWriteStreamCallback (CFWriteStreamRef stream, + CFStreamEventType type, + void *pInfo) { - GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wswitch-enum" - switch(type) - { - case kCFStreamEventCanAcceptBytes: - { - dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { - - LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); - - if (asyncSocket->writeStream != stream) - return_from_block; - - if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) - { - // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. - // (A callback related to the tcp stream, but not to the SSL layer). - - if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) - { - asyncSocket->flags |= kSocketCanAcceptBytes; - [asyncSocket cf_finishSSLHandshake]; - } - } - else - { - asyncSocket->flags |= kSocketCanAcceptBytes; - [asyncSocket doWriteData]; - } - }}); - - break; - } - default: - { - NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); - - if (error == nil && type == kCFStreamEventEndEncountered) - { - error = [asyncSocket connectionClosedError]; - } - - dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { - - LogCVerbose(@"CFWriteStreamCallback - Other"); - - if (asyncSocket->writeStream != stream) - return_from_block; - - if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) - { - [asyncSocket cf_abortSSLHandshake:error]; - } - else - { - [asyncSocket closeWithError:error]; - } - }}); - - break; - } - } + switch(type) + { + case kCFStreamEventCanAcceptBytes: + { + dispatch_async(asyncSocket->socketQueue, + ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket doWriteData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, + ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - Other"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } #pragma clang diagnostic pop } - (BOOL)createReadAndWriteStream { - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - - if (readStream || writeStream) - { - // Streams already created - return YES; - } - - int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; - - if (socketFD == SOCKET_NULL) - { - // Cannot create streams without a file descriptor - return NO; - } - - if (![self isConnected]) - { - // Cannot create streams until file descriptor is connected - return NO; - } - - LogVerbose(@"Creating read and write stream..."); - - CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); - - // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). - // But let's not take any chances. - - if (readStream) - CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); - if (writeStream) - CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); - - if ((readStream == NULL) || (writeStream == NULL)) - { - LogWarn(@"Unable to create read and write stream..."); - - if (readStream) - { - CFReadStreamClose(readStream); - CFRelease(readStream); - readStream = NULL; - } - if (writeStream) - { - CFWriteStreamClose(writeStream); - CFRelease(writeStream); - writeStream = NULL; - } - - return NO; - } - - return YES; + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + + if (readStream || writeStream) + { + // Streams already created + return YES; + } + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + if (socketFD == SOCKET_NULL) + { + // Cannot create streams without a file descriptor + return NO; + } + + if (![self isConnected]) + { + // Cannot create streams until file descriptor is connected + return NO; + } + + LogVerbose(@"Creating read and write stream..."); + + CFStreamCreatePairWithSocket(NULL, + (CFSocketNativeHandle)socketFD, + &readStream, + &writeStream); + + // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). + // But let's not take any chances. + + if (readStream) + CFReadStreamSetProperty(readStream, + kCFStreamPropertyShouldCloseNativeSocket, + kCFBooleanFalse); + if (writeStream) + CFWriteStreamSetProperty(writeStream, + kCFStreamPropertyShouldCloseNativeSocket, + kCFBooleanFalse); + + if ((readStream == NULL) || (writeStream == NULL)) + { + LogWarn(@"Unable to create read and write stream..."); + + if (readStream) + { + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + + return NO; + } + + return YES; } - (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite { - LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); - - streamContext.version = 0; - streamContext.info = (__bridge void *)(self); - streamContext.retain = nil; - streamContext.release = nil; - streamContext.copyDescription = nil; - - CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; - if (includeReadWrite) - readStreamEvents |= kCFStreamEventHasBytesAvailable; - - if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) - { - return NO; - } - - CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; - if (includeReadWrite) - writeStreamEvents |= kCFStreamEventCanAcceptBytes; - - if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) - { - return NO; - } - - return YES; + LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), + @"Read/Write stream is null"); + + streamContext.version = 0; + streamContext.info = (__bridge void *)(self); + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + readStreamEvents |= kCFStreamEventHasBytesAvailable; + + if (!CFReadStreamSetClient(readStream, + readStreamEvents, + &CFReadStreamCallback, + &streamContext)) + { + return NO; + } + + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + writeStreamEvents |= kCFStreamEventCanAcceptBytes; + + if (!CFWriteStreamSetClient(writeStream, + writeStreamEvents, + &CFWriteStreamCallback, + &streamContext)) + { + return NO; + } + + return YES; } - (BOOL)addStreamsToRunLoop { - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); - - if (!(flags & kAddedStreamsToRunLoop)) - { - LogVerbose(@"Adding streams to runloop..."); - - [[self class] startCFStreamThreadIfNeeded]; - dispatch_sync(cfstreamThreadSetupQueue, ^{ - [[self class] performSelector:@selector(scheduleCFStreams:) - onThread:cfstreamThread - withObject:self - waitUntilDone:YES]; - }); - flags |= kAddedStreamsToRunLoop; - } - - return YES; + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), + @"Read/Write stream is null"); + + if (!(flags & kAddedStreamsToRunLoop)) + { + LogVerbose(@"Adding streams to runloop..."); + + [[self class] startCFStreamThreadIfNeeded]; + dispatch_sync(cfstreamThreadSetupQueue, ^{ + [[self class] performSelector:@selector(scheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + }); + flags |= kAddedStreamsToRunLoop; + } + + return YES; } - (void)removeStreamsFromRunLoop { - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); - - if (flags & kAddedStreamsToRunLoop) - { - LogVerbose(@"Removing streams from runloop..."); - - dispatch_sync(cfstreamThreadSetupQueue, ^{ - [[self class] performSelector:@selector(unscheduleCFStreams:) - onThread:cfstreamThread - withObject:self - waitUntilDone:YES]; - }); - [[self class] stopCFStreamThreadIfNeeded]; - - flags &= ~kAddedStreamsToRunLoop; - } + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), + @"Read/Write stream is null"); + + if (flags & kAddedStreamsToRunLoop) + { + LogVerbose(@"Removing streams from runloop..."); + + dispatch_sync(cfstreamThreadSetupQueue, ^{ + [[self class] performSelector:@selector(unscheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + }); + [[self class] stopCFStreamThreadIfNeeded]; + + flags &= ~kAddedStreamsToRunLoop; + } } - (BOOL)openStreams { - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); - - CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); - CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); - - if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) - { - LogVerbose(@"Opening read and write stream..."); - - BOOL r1 = CFReadStreamOpen(readStream); - BOOL r2 = CFWriteStreamOpen(writeStream); - - if (!r1 || !r2) - { - LogError(@"Error in CFStreamOpen"); - return NO; - } - } - - return YES; + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), + @"Read/Write stream is null"); + + CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); + CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); + + if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) + { + LogVerbose(@"Opening read and write stream..."); + + BOOL r1 = CFReadStreamOpen(readStream); + BOOL r2 = CFWriteStreamOpen(writeStream); + + if (!r1 || !r2) + { + LogError(@"Error in CFStreamOpen"); + return NO; + } + } + + return YES; } #endif @@ -8031,236 +8262,254 @@ - (BOOL)openStreams /** * See header file for big discussion of this method. -**/ + **/ - (BOOL)autoDisconnectOnClosedReadStream { - // Note: YES means kAllowHalfDuplexConnection is OFF - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return ((config & kAllowHalfDuplexConnection) == 0); - } - else - { - __block BOOL result; - - dispatch_sync(socketQueue, ^{ - result = ((self->config & kAllowHalfDuplexConnection) == 0); - }); - - return result; - } + // Note: YES means kAllowHalfDuplexConnection is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kAllowHalfDuplexConnection) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((self->config & kAllowHalfDuplexConnection) == 0); + }); + + return result; + } } /** * See header file for big discussion of this method. -**/ + **/ - (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag { - // Note: YES means kAllowHalfDuplexConnection is OFF - - dispatch_block_t block = ^{ - - if (flag) - self->config &= ~kAllowHalfDuplexConnection; - else - self->config |= kAllowHalfDuplexConnection; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + // Note: YES means kAllowHalfDuplexConnection is OFF + + dispatch_block_t block = ^{ + + if (flag) + self->config &= ~kAllowHalfDuplexConnection; + else + self->config |= kAllowHalfDuplexConnection; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } /** * See header file for big discussion of this method. -**/ + **/ - (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue { - void *nonNullUnusedPointer = (__bridge void *)self; - dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, + IsOnSocketQueueOrTargetQueueKey, + nonNullUnusedPointer, + NULL); } /** * See header file for big discussion of this method. -**/ + **/ - (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue { - dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); + dispatch_queue_set_specific(socketOldTargetQueue, + IsOnSocketQueueOrTargetQueueKey, + NULL, + NULL); } /** * See header file for big discussion of this method. -**/ + **/ - (void)performBlock:(dispatch_block_t)block { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); } /** * Questions? Have you read the header file? -**/ + **/ - (int)socketFD { - if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); - return SOCKET_NULL; - } - - if (socket4FD != SOCKET_NULL) - return socket4FD; - else - return socket6FD; + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", + THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; } /** * Questions? Have you read the header file? -**/ + **/ - (int)socket4FD { - if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); - return SOCKET_NULL; - } - - return socket4FD; + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", + THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; } /** * Questions? Have you read the header file? -**/ + **/ - (int)socket6FD { - if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); - return SOCKET_NULL; - } - - return socket6FD; + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", + THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; } #if TARGET_OS_IPHONE /** * Questions? Have you read the header file? -**/ + **/ - (CFReadStreamRef)readStream { - if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); - return NULL; - } - - if (readStream == NULL) - [self createReadAndWriteStream]; - - return readStream; + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", + THIS_METHOD); + return NULL; + } + + if (readStream == NULL) + [self createReadAndWriteStream]; + + return readStream; } /** * Questions? Have you read the header file? -**/ + **/ - (CFWriteStreamRef)writeStream { - if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); - return NULL; - } - - if (writeStream == NULL) - [self createReadAndWriteStream]; - - return writeStream; + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", + THIS_METHOD); + return NULL; + } + + if (writeStream == NULL) + [self createReadAndWriteStream]; + + return writeStream; } - (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat { - if (![self createReadAndWriteStream]) - { - // Error occurred creating streams (perhaps socket isn't open) - return NO; - } - - BOOL r1, r2; - - LogVerbose(@"Enabling backgrouding on socket"); - + if (![self createReadAndWriteStream]) + { + // Error occurred creating streams (perhaps socket isn't open) + return NO; + } + + BOOL r1, r2; + + LogVerbose(@"Enabling backgrouding on socket"); + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); - r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + r1 = CFReadStreamSetProperty(readStream, + kCFStreamNetworkServiceType, + kCFStreamNetworkServiceTypeVoIP); + r2 = CFWriteStreamSetProperty(writeStream, + kCFStreamNetworkServiceType, + kCFStreamNetworkServiceTypeVoIP); #pragma clang diagnostic pop - if (!r1 || !r2) - { - return NO; - } - - if (!caveat) - { - if (![self openStreams]) - { - return NO; - } - } - - return YES; + if (!r1 || !r2) + { + return NO; + } + + if (!caveat) + { + if (![self openStreams]) + { + return NO; + } + } + + return YES; } /** * Questions? Have you read the header file? -**/ + **/ - (BOOL)enableBackgroundingOnSocket { - LogTrace(); - - if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); - return NO; - } - - return [self enableBackgroundingOnSocketWithCaveat:NO]; + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", + THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:NO]; } - (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? { - // This method was created as a workaround for a bug in iOS. - // Apple has since fixed this bug. - // I'm not entirely sure which version of iOS they fixed it in... - - LogTrace(); - - if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); - return NO; - } - - return [self enableBackgroundingOnSocketWithCaveat:YES]; + // This method was created as a workaround for a bug in iOS. + // Apple has since fixed this bug. + // I'm not entirely sure which version of iOS they fixed it in... + + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", + THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:YES]; } #endif - (SSLContextRef)sslContext { - if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); - return NULL; - } - - return sslContext; + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", + THIS_METHOD); + return NULL; + } + + return sslContext; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -8269,260 +8518,269 @@ - (SSLContextRef)sslContext + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr { - LogTrace(); - - NSMutableArray *addresses = nil; - NSError *error = nil; - - if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) - { - // Use LOOPBACK address - struct sockaddr_in nativeAddr4; - nativeAddr4.sin_len = sizeof(struct sockaddr_in); - nativeAddr4.sin_family = AF_INET; - nativeAddr4.sin_port = htons(port); - nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); - - struct sockaddr_in6 nativeAddr6; - nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); - nativeAddr6.sin6_family = AF_INET6; - nativeAddr6.sin6_port = htons(port); - nativeAddr6.sin6_flowinfo = 0; - nativeAddr6.sin6_addr = in6addr_loopback; - nativeAddr6.sin6_scope_id = 0; - - // Wrap the native address structures - - NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; - NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; - - addresses = [NSMutableArray arrayWithCapacity:2]; - [addresses addObject:address4]; - [addresses addObject:address6]; - } - else - { - NSString *portStr = [NSString stringWithFormat:@"%hu", port]; - - struct addrinfo hints, *res, *res0; - - memset(&hints, 0, sizeof(hints)); - hints.ai_family = PF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = IPPROTO_TCP; - - int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); - - if (gai_error) - { - error = [self gaiError:gai_error]; - } - else - { - NSUInteger capacity = 0; - for (res = res0; res; res = res->ai_next) - { - if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { - capacity++; - } - } - - addresses = [NSMutableArray arrayWithCapacity:capacity]; - - for (res = res0; res; res = res->ai_next) - { - if (res->ai_family == AF_INET) - { - // Found IPv4 address. - // Wrap the native address structure, and add to results. - - NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; - [addresses addObject:address4]; - } - else if (res->ai_family == AF_INET6) - { - // Fixes connection issues with IPv6 - // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 - - // Found IPv6 address. - // Wrap the native address structure, and add to results. - - struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)(void *)res->ai_addr; - in_port_t *portPtr = &sockaddr->sin6_port; - if ((portPtr != NULL) && (*portPtr == 0)) { - *portPtr = htons(port); - } - - NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; - [addresses addObject:address6]; - } - } - freeaddrinfo(res0); - - if ([addresses count] == 0) - { - error = [self gaiError:EAI_FAIL]; - } - } - } - - if (errPtr) *errPtr = error; - return addresses; + LogTrace(); + + NSMutableArray *addresses = nil; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in nativeAddr4; + nativeAddr4.sin_len = sizeof(struct sockaddr_in); + nativeAddr4.sin_family = AF_INET; + nativeAddr4.sin_port = htons(port); + nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures + + NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + addresses = [NSMutableArray arrayWithCapacity:2]; + [addresses addObject:address4]; + [addresses addObject:address6]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + int gai_error = getaddrinfo([host UTF8String], + [portStr UTF8String], + &hints, + &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + NSUInteger capacity = 0; + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { + capacity++; + } + } + + addresses = [NSMutableArray arrayWithCapacity:capacity]; + + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address. + // Wrap the native address structure, and add to results. + + NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address4]; + } + else if (res->ai_family == AF_INET6) + { + // Fixes connection issues with IPv6 + // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 + + // Found IPv6 address. + // Wrap the native address structure, and add to results. + + struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)(void *)res->ai_addr; + in_port_t *portPtr = &sockaddr->sin6_port; + if ((portPtr != NULL) && (*portPtr == 0)) { + *portPtr = htons(port); + } + + NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address6]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + if (errPtr) *errPtr = error; + return addresses; } + (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { - char addrBuf[INET_ADDRSTRLEN]; - - if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) - { - addrBuf[0] = '\0'; - } - - return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, + &pSockaddr4->sin_addr, + addrBuf, + (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { - char addrBuf[INET6_ADDRSTRLEN]; - - if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) - { - addrBuf[0] = '\0'; - } - - return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, + &pSockaddr6->sin6_addr, + addrBuf, + (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { - return ntohs(pSockaddr4->sin_port); + return ntohs(pSockaddr4->sin_port); } + (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { - return ntohs(pSockaddr6->sin6_port); + return ntohs(pSockaddr6->sin6_port); } + (NSURL *)urlFromSockaddrUN:(const struct sockaddr_un *)pSockaddr { - NSString *path = [NSString stringWithUTF8String:pSockaddr->sun_path]; - return [NSURL fileURLWithPath:path]; + NSString *path = [NSString stringWithUTF8String:pSockaddr->sun_path]; + return [NSURL fileURLWithPath:path]; } + (NSString *)hostFromAddress:(NSData *)address { - NSString *host; - - if ([self getHost:&host port:NULL fromAddress:address]) - return host; - else - return nil; + NSString *host; + + if ([self getHost:&host port:NULL fromAddress:address]) + return host; + else + return nil; } + (uint16_t)portFromAddress:(NSData *)address { - uint16_t port; - - if ([self getHost:NULL port:&port fromAddress:address]) - return port; - else - return 0; + uint16_t port; + + if ([self getHost:NULL port:&port fromAddress:address]) + return port; + else + return 0; } + (BOOL)isIPv4Address:(NSData *)address { - if ([address length] >= sizeof(struct sockaddr)) - { - const struct sockaddr *sockaddrX = [address bytes]; - - if (sockaddrX->sa_family == AF_INET) { - return YES; - } - } - - return NO; + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = (const struct sockaddr *)(const void *)[address bytes]; + + if (sockaddrX->sa_family == AF_INET) { + return YES; + } + } + + return NO; } + (BOOL)isIPv6Address:(NSData *)address { - if ([address length] >= sizeof(struct sockaddr)) - { - const struct sockaddr *sockaddrX = [address bytes]; - - if (sockaddrX->sa_family == AF_INET6) { - return YES; - } - } - - return NO; + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = (const struct sockaddr *)(const void *)[address bytes]; + + if (sockaddrX->sa_family == AF_INET6) { + return YES; + } + } + + return NO; } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address { - return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address { - if ([address length] >= sizeof(struct sockaddr)) - { - const struct sockaddr *sockaddrX = [address bytes]; - - if (sockaddrX->sa_family == AF_INET) - { - if ([address length] >= sizeof(struct sockaddr_in)) - { - struct sockaddr_in sockaddr4; - memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); - - if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; - if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; - if (afPtr) *afPtr = AF_INET; - - return YES; - } - } - else if (sockaddrX->sa_family == AF_INET6) - { - if ([address length] >= sizeof(struct sockaddr_in6)) - { - struct sockaddr_in6 sockaddr6; - memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); - - if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; - if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; - if (afPtr) *afPtr = AF_INET6; - - return YES; - } - } - } - - return NO; + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = (const struct sockaddr *)(const void *)[address bytes]; + + if (sockaddrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + struct sockaddr_in sockaddr4; + memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (sockaddrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + struct sockaddr_in6 sockaddr6; + memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + return NO; } + (NSData *)CRLFData { - return [NSData dataWithBytes:"\x0D\x0A" length:2]; + return [NSData dataWithBytes:"\x0D\x0A" length:2]; } + (NSData *)CRData { - return [NSData dataWithBytes:"\x0D" length:1]; + return [NSData dataWithBytes:"\x0D" length:1]; } + (NSData *)LFData { - return [NSData dataWithBytes:"\x0A" length:1]; + return [NSData dataWithBytes:"\x0A" length:1]; } + (NSData *)ZeroData { - return [NSData dataWithBytes:"" length:1]; + return [NSData dataWithBytes:"" length:1]; } -@end +@end #pragma clang diagnostic pop diff --git a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m index 8972caa62..23d05e730 100755 --- a/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m +++ b/WebDriverAgentLib/Vendor/CocoaAsyncSocket/GCDAsyncUdpSocket.m @@ -1,10 +1,10 @@ -// +// // GCDAsyncUdpSocket -// +// // This class is in the public domain. // Originally created by Robbie Hanson of Deusty LLC. // Updated and maintained by Deusty LLC and the Apple development community. -// +// // https://github.com/robbiehanson/CocoaAsyncSocket // @@ -16,8 +16,11 @@ #endif #if TARGET_OS_IPHONE - #import - #import +#import +#import +#import +// Note: CFStream APIs are still used for backgrounding support and are part of CoreFoundation +// kCFStreamPropertyShouldCloseNativeSocket is available from CoreFoundation/CFStream.h #endif #import @@ -37,7 +40,7 @@ // Logging uses the CocoaLumberjack framework (which is also GCD based). // https://github.com/robbiehanson/CocoaLumberjack -// +// // It allows us to do a lot of logging without significantly slowing down the code. #import "DDLog.h" @@ -67,18 +70,18 @@ // Logging Disabled -#define LogError(frmt, ...) {} -#define LogWarn(frmt, ...) {} -#define LogInfo(frmt, ...) {} -#define LogVerbose(frmt, ...) {} +#define LogError(frmt, ...) do {} while (0) +#define LogWarn(frmt, ...) do {} while (0) +#define LogInfo(frmt, ...) do {} while (0) +#define LogVerbose(frmt, ...) do {} while (0) -#define LogCError(frmt, ...) {} -#define LogCWarn(frmt, ...) {} -#define LogCInfo(frmt, ...) {} -#define LogCVerbose(frmt, ...) {} +#define LogCError(frmt, ...) do {} while (0) +#define LogCWarn(frmt, ...) do {} while (0) +#define LogCInfo(frmt, ...) do {} while (0) +#define LogCVerbose(frmt, ...) do {} while (0) -#define LogTrace() {} -#define LogCTrace(frmt, ...) {} +#define LogTrace() do {} while (0) +#define LogCTrace(frmt, ...) do {} while (0) #endif @@ -86,20 +89,20 @@ * Seeing a return statements within an inner block * can sometimes be mistaken for a return point of the enclosing method. * This makes inline blocks a bit easier to read. -**/ + **/ #define return_from_block return /** * A socket file descriptor is really just an integer. * It represents the index of the socket within the kernel. * This makes invalid file descriptor comparisons easier to read. -**/ + **/ #define SOCKET_NULL -1 /** * Just to type less code. -**/ -#define AutoreleasedBlock(block) ^{ @autoreleasepool { block(); }} + **/ +#define AutoreleasedBlock(block) ^{ @autoreleasepool { block(); }} @class GCDAsyncUdpSendPacket; @@ -112,34 +115,34 @@ enum GCDAsyncUdpSocketFlags { - kDidCreateSockets = 1 << 0, // If set, the sockets have been created. - kDidBind = 1 << 1, // If set, bind has been called. - kConnecting = 1 << 2, // If set, a connection attempt is in progress. - kDidConnect = 1 << 3, // If set, socket is connected. - kReceiveOnce = 1 << 4, // If set, one-at-a-time receive is enabled - kReceiveContinuous = 1 << 5, // If set, continuous receive is enabled - kIPv4Deactivated = 1 << 6, // If set, socket4 was closed due to bind or connect on IPv6. - kIPv6Deactivated = 1 << 7, // If set, socket6 was closed due to bind or connect on IPv4. - kSend4SourceSuspended = 1 << 8, // If set, send4Source is suspended. - kSend6SourceSuspended = 1 << 9, // If set, send6Source is suspended. - kReceive4SourceSuspended = 1 << 10, // If set, receive4Source is suspended. - kReceive6SourceSuspended = 1 << 11, // If set, receive6Source is suspended. - kSock4CanAcceptBytes = 1 << 12, // If set, we know socket4 can accept bytes. If unset, it's unknown. - kSock6CanAcceptBytes = 1 << 13, // If set, we know socket6 can accept bytes. If unset, it's unknown. - kForbidSendReceive = 1 << 14, // If set, no new send or receive operations are allowed to be queued. - kCloseAfterSends = 1 << 15, // If set, close as soon as no more sends are queued. - kFlipFlop = 1 << 16, // Used to alternate between IPv4 and IPv6 sockets. + kDidCreateSockets = 1 << 0, // If set, the sockets have been created. + kDidBind = 1 << 1, // If set, bind has been called. + kConnecting = 1 << 2, // If set, a connection attempt is in progress. + kDidConnect = 1 << 3, // If set, socket is connected. + kReceiveOnce = 1 << 4, // If set, one-at-a-time receive is enabled + kReceiveContinuous = 1 << 5, // If set, continuous receive is enabled + kIPv4Deactivated = 1 << 6, // If set, socket4 was closed due to bind or connect on IPv6. + kIPv6Deactivated = 1 << 7, // If set, socket6 was closed due to bind or connect on IPv4. + kSend4SourceSuspended = 1 << 8, // If set, send4Source is suspended. + kSend6SourceSuspended = 1 << 9, // If set, send6Source is suspended. + kReceive4SourceSuspended = 1 << 10, // If set, receive4Source is suspended. + kReceive6SourceSuspended = 1 << 11, // If set, receive6Source is suspended. + kSock4CanAcceptBytes = 1 << 12, // If set, we know socket4 can accept bytes. If unset, it's unknown. + kSock6CanAcceptBytes = 1 << 13, // If set, we know socket6 can accept bytes. If unset, it's unknown. + kForbidSendReceive = 1 << 14, // If set, no new send or receive operations are allowed to be queued. + kCloseAfterSends = 1 << 15, // If set, close as soon as no more sends are queued. + kFlipFlop = 1 << 16, // Used to alternate between IPv4 and IPv6 sockets. #if TARGET_OS_IPHONE - kAddedStreamListener = 1 << 17, // If set, CFStreams have been added to listener thread + kAddedStreamListener = 1 << 17, // If set, CFStreams have been added to listener thread #endif }; enum GCDAsyncUdpSocketConfig { - kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled - kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled - kPreferIPv4 = 1 << 2, // If set, IPv4 is preferred over IPv6 - kPreferIPv6 = 1 << 3, // If set, IPv6 is preferred over IPv4 + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv4 = 1 << 2, // If set, IPv4 is preferred over IPv6 + kPreferIPv6 = 1 << 3, // If set, IPv6 is preferred over IPv4 }; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -149,71 +152,71 @@ @interface GCDAsyncUdpSocket () { #if __has_feature(objc_arc_weak) - __weak id delegate; + __weak id delegate; #else - __unsafe_unretained id delegate; + __unsafe_unretained id delegate; #endif - dispatch_queue_t delegateQueue; - - GCDAsyncUdpSocketReceiveFilterBlock receiveFilterBlock; - dispatch_queue_t receiveFilterQueue; - BOOL receiveFilterAsync; - - GCDAsyncUdpSocketSendFilterBlock sendFilterBlock; - dispatch_queue_t sendFilterQueue; - BOOL sendFilterAsync; - - uint32_t flags; - uint16_t config; - - uint16_t max4ReceiveSize; - uint32_t max6ReceiveSize; - - uint16_t maxSendSize; - - int socket4FD; - int socket6FD; - - dispatch_queue_t socketQueue; - - dispatch_source_t send4Source; - dispatch_source_t send6Source; - dispatch_source_t receive4Source; - dispatch_source_t receive6Source; - dispatch_source_t sendTimer; - - GCDAsyncUdpSendPacket *currentSend; - NSMutableArray *sendQueue; - - unsigned long socket4FDBytesAvailable; - unsigned long socket6FDBytesAvailable; - - uint32_t pendingFilterOperations; - - NSData *cachedLocalAddress4; - NSString *cachedLocalHost4; - uint16_t cachedLocalPort4; - - NSData *cachedLocalAddress6; - NSString *cachedLocalHost6; - uint16_t cachedLocalPort6; - - NSData *cachedConnectedAddress; - NSString *cachedConnectedHost; - uint16_t cachedConnectedPort; - int cachedConnectedFamily; - - void *IsOnSocketQueueOrTargetQueueKey; - + dispatch_queue_t delegateQueue; + + GCDAsyncUdpSocketReceiveFilterBlock receiveFilterBlock; + dispatch_queue_t receiveFilterQueue; + BOOL receiveFilterAsync; + + GCDAsyncUdpSocketSendFilterBlock sendFilterBlock; + dispatch_queue_t sendFilterQueue; + BOOL sendFilterAsync; + + uint32_t flags; + uint16_t config; + + uint16_t max4ReceiveSize; + uint32_t max6ReceiveSize; + + uint16_t maxSendSize; + + int socket4FD; + int socket6FD; + + dispatch_queue_t socketQueue; + + dispatch_source_t send4Source; + dispatch_source_t send6Source; + dispatch_source_t receive4Source; + dispatch_source_t receive6Source; + dispatch_source_t sendTimer; + + GCDAsyncUdpSendPacket *currentSend; + NSMutableArray *sendQueue; + + unsigned long socket4FDBytesAvailable; + unsigned long socket6FDBytesAvailable; + + uint32_t pendingFilterOperations; + + NSData *cachedLocalAddress4; + NSString *cachedLocalHost4; + uint16_t cachedLocalPort4; + + NSData *cachedLocalAddress6; + NSString *cachedLocalHost6; + uint16_t cachedLocalPort6; + + NSData *cachedConnectedAddress; + NSString *cachedConnectedHost; + uint16_t cachedConnectedPort; + int cachedConnectedFamily; + + void *IsOnSocketQueueOrTargetQueueKey; + #if TARGET_OS_IPHONE - CFStreamClientContext streamContext; - CFReadStreamRef readStream4; - CFReadStreamRef readStream6; - CFWriteStreamRef writeStream4; - CFWriteStreamRef writeStream6; + CFStreamClientContext streamContext; + CFReadStreamRef readStream4; + CFReadStreamRef readStream6; + CFWriteStreamRef writeStream4; + CFWriteStreamRef writeStream6; #endif - - id userData; + + id userData; } - (void)resumeSend4Source; @@ -266,21 +269,21 @@ + (void)listenerThread:(id)unused; /** * The GCDAsyncUdpSendPacket encompasses the instructions for a single send/write. -**/ + **/ @interface GCDAsyncUdpSendPacket : NSObject { @public - NSData *buffer; - NSTimeInterval timeout; - long tag; - - BOOL resolveInProgress; - BOOL filterInProgress; - - NSArray *resolvedAddresses; - NSError *resolveError; - - NSData *address; - int addressFamily; + NSData *buffer; + NSTimeInterval timeout; + long tag; + + BOOL resolveInProgress; + BOOL filterInProgress; + + NSArray *resolvedAddresses; + NSError *resolveError; + + NSData *address; + int addressFamily; } - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i NS_DESIGNATED_INITIALIZER; @@ -292,21 +295,21 @@ @implementation GCDAsyncUdpSendPacket // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { - NSAssert(0, @"Use the designated initializer"); - return nil; + NSAssert(0, @"Use the designated initializer"); + return nil; } - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i { - if ((self = [super init])) - { - buffer = d; - timeout = t; - tag = i; - - resolveInProgress = NO; - } - return self; + if ((self = [super init])) + { + buffer = d; + timeout = t; + tag = i; + + resolveInProgress = NO; + } + return self; } @@ -318,12 +321,12 @@ - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i @interface GCDAsyncUdpSpecialPacket : NSObject { @public -// uint8_t type; - - BOOL resolveInProgress; - - NSArray *addresses; - NSError *error; + // uint8_t type; + + BOOL resolveInProgress; + + NSArray *addresses; + NSError *error; } - (instancetype)init NS_DESIGNATED_INITIALIZER; @@ -334,8 +337,8 @@ @implementation GCDAsyncUdpSpecialPacket - (instancetype)init { - self = [super init]; - return self; + self = [super init]; + return self; } @@ -349,134 +352,139 @@ @implementation GCDAsyncUdpSocket - (instancetype)init { - LogTrace(); - - return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; + LogTrace(); + + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; } - (instancetype)initWithSocketQueue:(dispatch_queue_t)sq { - LogTrace(); - - return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; + LogTrace(); + + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq { - LogTrace(); - - return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; + LogTrace(); + + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq { - LogTrace(); - - if ((self = [super init])) - { - delegate = aDelegate; - - if (dq) - { - delegateQueue = dq; - #if !OS_OBJECT_USE_OBJC - dispatch_retain(delegateQueue); - #endif - } - - max4ReceiveSize = 65535; - max6ReceiveSize = 65535; - - maxSendSize = 65535; - - socket4FD = SOCKET_NULL; - socket6FD = SOCKET_NULL; - - if (sq) - { - NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), - @"The given socketQueue parameter must not be a concurrent queue."); - NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), - @"The given socketQueue parameter must not be a concurrent queue."); - NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), - @"The given socketQueue parameter must not be a concurrent queue."); - - socketQueue = sq; - #if !OS_OBJECT_USE_OBJC - dispatch_retain(socketQueue); - #endif - } - else - { - socketQueue = dispatch_queue_create([GCDAsyncUdpSocketQueueName UTF8String], NULL); - } - - // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. - // From the documentation: - // - // > Keys are only compared as pointers and are never dereferenced. - // > Thus, you can use a pointer to a static variable for a specific subsystem or - // > any other value that allows you to identify the value uniquely. - // - // We're just going to use the memory address of an ivar. - // Specifically an ivar that is explicitly named for our purpose to make the code more readable. - // - // However, it feels tedious (and less readable) to include the "&" all the time: - // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) - // - // So we're going to make it so it doesn't matter if we use the '&' or not, - // by assigning the value of the ivar to the address of the ivar. - // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; - - IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; - - void *nonNullUnusedPointer = (__bridge void *)self; - dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); - - currentSend = nil; - sendQueue = [[NSMutableArray alloc] initWithCapacity:5]; - - #if TARGET_OS_IPHONE - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(applicationWillEnterForeground:) - name:UIApplicationWillEnterForegroundNotification - object:nil]; - #endif - } - return self; + LogTrace(); + + if ((self = [super init])) + { + delegate = aDelegate; + + if (dq) + { + delegateQueue = dq; +#if !OS_OBJECT_USE_OBJC + dispatch_retain(delegateQueue); +#endif + } + + max4ReceiveSize = 65535; + max6ReceiveSize = 65535; + + maxSendSize = 65535; + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, + 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; +#if !OS_OBJECT_USE_OBJC + dispatch_retain(socketQueue); +#endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncUdpSocketQueueName UTF8String], + NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, + IsOnSocketQueueOrTargetQueueKey, + nonNullUnusedPointer, + NULL); + + currentSend = nil; + sendQueue = [[NSMutableArray alloc] initWithCapacity:5]; + +#if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; +#endif + } + return self; } - (void)dealloc { - LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); - + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + #if TARGET_OS_IPHONE - [[NSNotificationCenter defaultCenter] removeObserver:self]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; #endif - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - [self closeWithError:nil]; - } - else - { - dispatch_sync(socketQueue, ^{ - [self closeWithError:nil]; - }); - } - - delegate = nil; - #if !OS_OBJECT_USE_OBJC - if (delegateQueue) dispatch_release(delegateQueue); - #endif - delegateQueue = NULL; - - #if !OS_OBJECT_USE_OBJC - if (socketQueue) dispatch_release(socketQueue); - #endif - socketQueue = NULL; - - LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; +#if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); +#endif + delegateQueue = NULL; + +#if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); +#endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -485,461 +493,461 @@ - (void)dealloc - (id)delegate { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return delegate; - } - else - { - __block id result = nil; - - dispatch_sync(socketQueue, ^{ - result = self->delegate; - }); - - return result; - } + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result = nil; + + dispatch_sync(socketQueue, ^{ + result = self->delegate; + }); + + return result; + } } - (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously { - dispatch_block_t block = ^{ - self->delegate = newDelegate; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { - block(); - } - else { - if (synchronously) - dispatch_sync(socketQueue, block); - else - dispatch_async(socketQueue, block); - } + dispatch_block_t block = ^{ + self->delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } } - (void)setDelegate:(id)newDelegate { - [self setDelegate:newDelegate synchronously:NO]; + [self setDelegate:newDelegate synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate { - [self setDelegate:newDelegate synchronously:YES]; + [self setDelegate:newDelegate synchronously:YES]; } - (dispatch_queue_t)delegateQueue { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - return delegateQueue; - } - else - { - __block dispatch_queue_t result = NULL; - - dispatch_sync(socketQueue, ^{ - result = self->delegateQueue; - }); - - return result; - } + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result = NULL; + + dispatch_sync(socketQueue, ^{ + result = self->delegateQueue; + }); + + return result; + } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { - dispatch_block_t block = ^{ - - #if !OS_OBJECT_USE_OBJC - if (self->delegateQueue) dispatch_release(self->delegateQueue); - if (newDelegateQueue) dispatch_retain(newDelegateQueue); - #endif - - self->delegateQueue = newDelegateQueue; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { - block(); - } - else { - if (synchronously) - dispatch_sync(socketQueue, block); - else - dispatch_async(socketQueue, block); - } + dispatch_block_t block = ^{ + +#if !OS_OBJECT_USE_OBJC + if (self->delegateQueue) dispatch_release(self->delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); +#endif + + self->delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue { - [self setDelegateQueue:newDelegateQueue synchronously:NO]; + [self setDelegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue { - [self setDelegateQueue:newDelegateQueue synchronously:YES]; + [self setDelegateQueue:newDelegateQueue synchronously:YES]; } - (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - if (delegatePtr) *delegatePtr = delegate; - if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; - } - else - { - __block id dPtr = NULL; - __block dispatch_queue_t dqPtr = NULL; - - dispatch_sync(socketQueue, ^{ - dPtr = self->delegate; - dqPtr = self->delegateQueue; - }); - - if (delegatePtr) *delegatePtr = dPtr; - if (delegateQueuePtr) *delegateQueuePtr = dqPtr; - } + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = self->delegate; + dqPtr = self->delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { - dispatch_block_t block = ^{ - - self->delegate = newDelegate; - - #if !OS_OBJECT_USE_OBJC - if (self->delegateQueue) dispatch_release(self->delegateQueue); - if (newDelegateQueue) dispatch_retain(newDelegateQueue); - #endif - - self->delegateQueue = newDelegateQueue; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { - block(); - } - else { - if (synchronously) - dispatch_sync(socketQueue, block); - else - dispatch_async(socketQueue, block); - } + dispatch_block_t block = ^{ + + self->delegate = newDelegate; + +#if !OS_OBJECT_USE_OBJC + if (self->delegateQueue) dispatch_release(self->delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); +#endif + + self->delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { - [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { - [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; } - (BOOL)isIPv4Enabled { - // Note: YES means kIPv4Disabled is OFF - - __block BOOL result = NO; - - dispatch_block_t block = ^{ - - result = ((self->config & kIPv4Disabled) == 0); - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + // Note: YES means kIPv4Disabled is OFF + + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + result = ((self->config & kIPv4Disabled) == 0); + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (void)setIPv4Enabled:(BOOL)flag { - // Note: YES means kIPv4Disabled is OFF - - dispatch_block_t block = ^{ - - LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); - - if (flag) - self->config &= ~kIPv4Disabled; - else - self->config |= kIPv4Disabled; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); + + if (flag) + self->config &= ~kIPv4Disabled; + else + self->config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (BOOL)isIPv6Enabled { - // Note: YES means kIPv6Disabled is OFF - - __block BOOL result = NO; - - dispatch_block_t block = ^{ - - result = ((self->config & kIPv6Disabled) == 0); - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + // Note: YES means kIPv6Disabled is OFF + + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + result = ((self->config & kIPv6Disabled) == 0); + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (void)setIPv6Enabled:(BOOL)flag { - // Note: YES means kIPv6Disabled is OFF - - dispatch_block_t block = ^{ - - LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); - - if (flag) - self->config &= ~kIPv6Disabled; - else - self->config |= kIPv6Disabled; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); + + if (flag) + self->config &= ~kIPv6Disabled; + else + self->config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (BOOL)isIPv4Preferred { - __block BOOL result = NO; - - dispatch_block_t block = ^{ - result = (self->config & kPreferIPv4) ? YES : NO; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->config & kPreferIPv4) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (BOOL)isIPv6Preferred { - __block BOOL result = NO; - - dispatch_block_t block = ^{ - result = (self->config & kPreferIPv6) ? YES : NO; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->config & kPreferIPv6) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (BOOL)isIPVersionNeutral { - __block BOOL result = NO; - - dispatch_block_t block = ^{ - result = (self->config & (kPreferIPv4 | kPreferIPv6)) == 0; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->config & (kPreferIPv4 | kPreferIPv6)) == 0; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (void)setPreferIPv4 { - dispatch_block_t block = ^{ - - LogTrace(); - - self->config |= kPreferIPv4; - self->config &= ~kPreferIPv6; - - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + dispatch_block_t block = ^{ + + LogTrace(); + + self->config |= kPreferIPv4; + self->config &= ~kPreferIPv6; + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (void)setPreferIPv6 { - dispatch_block_t block = ^{ - - LogTrace(); - - self->config &= ~kPreferIPv4; - self->config |= kPreferIPv6; - - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + dispatch_block_t block = ^{ + + LogTrace(); + + self->config &= ~kPreferIPv4; + self->config |= kPreferIPv6; + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (void)setIPVersionNeutral { - dispatch_block_t block = ^{ - - LogTrace(); - - self->config &= ~kPreferIPv4; - self->config &= ~kPreferIPv6; - - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + dispatch_block_t block = ^{ + + LogTrace(); + + self->config &= ~kPreferIPv4; + self->config &= ~kPreferIPv6; + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (uint16_t)maxReceiveIPv4BufferSize { - __block uint16_t result = 0; - - dispatch_block_t block = ^{ - - result = self->max4ReceiveSize; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + result = self->max4ReceiveSize; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (void)setMaxReceiveIPv4BufferSize:(uint16_t)max { - dispatch_block_t block = ^{ - - LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); - - self->max4ReceiveSize = max; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); + + self->max4ReceiveSize = max; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (uint32_t)maxReceiveIPv6BufferSize { - __block uint32_t result = 0; - - dispatch_block_t block = ^{ - - result = self->max6ReceiveSize; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block uint32_t result = 0; + + dispatch_block_t block = ^{ + + result = self->max6ReceiveSize; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (void)setMaxReceiveIPv6BufferSize:(uint32_t)max { - dispatch_block_t block = ^{ - - LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); - - self->max6ReceiveSize = max; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); + + self->max6ReceiveSize = max; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (void)setMaxSendBufferSize:(uint16_t)max { - dispatch_block_t block = ^{ - - LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); - - self->maxSendSize = max; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); + + self->maxSendSize = max; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (uint16_t)maxSendBufferSize { - __block uint16_t result = 0; - - dispatch_block_t block = ^{ - - result = self->maxSendSize; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + result = self->maxSendSize; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (id)userData { - __block id result = nil; - - dispatch_block_t block = ^{ - - result = self->userData; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block id result = nil; + + dispatch_block_t block = ^{ + + result = self->userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (void)setUserData:(id)arbitraryUserData { - dispatch_block_t block = ^{ - - if (self->userData != arbitraryUserData) - { - self->userData = arbitraryUserData; - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + dispatch_block_t block = ^{ + + if (self->userData != arbitraryUserData) + { + self->userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -948,90 +956,91 @@ - (void)setUserData:(id)arbitraryUserData - (void)notifyDidConnectToAddress:(NSData *)anAddress { - LogTrace(); - - __strong id theDelegate = delegate; - if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didConnectToAddress:)]) - { - NSData *address = [anAddress copy]; // In case param is NSMutableData - - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate udpSocket:self didConnectToAddress:address]; - }}); - } + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didConnectToAddress:)]) + { + NSData *address = [anAddress copy]; // In case param is NSMutableData + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didConnectToAddress:address]; + }}); + } } - (void)notifyDidNotConnect:(NSError *)error { - LogTrace(); - - __strong id theDelegate = delegate; - if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotConnect:)]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate udpSocket:self didNotConnect:error]; - }}); - } + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotConnect:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didNotConnect:error]; + }}); + } } - (void)notifyDidSendDataWithTag:(long)tag { - LogTrace(); - - __strong id theDelegate = delegate; - if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didSendDataWithTag:)]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate udpSocket:self didSendDataWithTag:tag]; - }}); - } + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didSendDataWithTag:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didSendDataWithTag:tag]; + }}); + } } - (void)notifyDidNotSendDataWithTag:(long)tag dueToError:(NSError *)error { - LogTrace(); - - __strong id theDelegate = delegate; - if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotSendDataWithTag:dueToError:)]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate udpSocket:self didNotSendDataWithTag:tag dueToError:error]; - }}); - } + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotSendDataWithTag:dueToError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didNotSendDataWithTag:tag dueToError:error]; + }}); + } } - (void)notifyDidReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)context { - LogTrace(); - - SEL selector = @selector(udpSocket:didReceiveData:fromAddress:withFilterContext:); - - __strong id theDelegate = delegate; - if (delegateQueue && [theDelegate respondsToSelector:selector]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate udpSocket:self didReceiveData:data fromAddress:address withFilterContext:context]; - }}); - } + LogTrace(); + + SEL selector = @selector(udpSocket:didReceiveData:fromAddress:withFilterContext:); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:selector]) + { + dispatch_async(delegateQueue, + ^{ @autoreleasepool { + + [theDelegate udpSocket:self didReceiveData:data fromAddress:address withFilterContext:context]; + }}); + } } - (void)notifyDidCloseWithError:(NSError *)error { - LogTrace(); - - __strong id theDelegate = delegate; - if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocketDidClose:withError:)]) - { - dispatch_async(delegateQueue, ^{ @autoreleasepool { - - [theDelegate udpSocketDidClose:self withError:error]; - }}); - } + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocketDidClose:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocketDidClose:self withError:error]; + }}); + } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1040,83 +1049,87 @@ - (void)notifyDidCloseWithError:(NSError *)error - (NSError *)badConfigError:(NSString *)errMsg { - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain - code:GCDAsyncUdpSocketBadConfigError - userInfo:userInfo]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketBadConfigError + userInfo:userInfo]; } - (NSError *)badParamError:(NSString *)errMsg { - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain - code:GCDAsyncUdpSocketBadParamError - userInfo:userInfo]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketBadParamError + userInfo:userInfo]; } - (NSError *)gaiError:(int)gai_error { - NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; } - (NSError *)errnoErrorWithReason:(NSString *)reason { - NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; - NSDictionary *userInfo; - - if (reason) - userInfo = @{NSLocalizedDescriptionKey : errMsg, - NSLocalizedFailureReasonErrorKey : reason}; - else - userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo; + + if (reason) + userInfo = @{NSLocalizedDescriptionKey : errMsg, + NSLocalizedFailureReasonErrorKey : reason}; + else + userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; } - (NSError *)errnoError { - return [self errnoErrorWithReason:nil]; + return [self errnoErrorWithReason:nil]; } /** * Returns a standard send timeout error. -**/ + **/ - (NSError *)sendTimeoutError { - NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketSendTimeoutError", - @"GCDAsyncUdpSocket", [NSBundle mainBundle], - @"Send operation timed out", nil); - - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain - code:GCDAsyncUdpSocketSendTimeoutError - userInfo:userInfo]; + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketSendTimeoutError", + @"GCDAsyncUdpSocket", + [NSBundle mainBundle], + @"Send operation timed out", + nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketSendTimeoutError + userInfo:userInfo]; } - (NSError *)socketClosedError { - NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketClosedError", - @"GCDAsyncUdpSocket", [NSBundle mainBundle], - @"Socket closed", nil); - - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketClosedError userInfo:userInfo]; + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketClosedError", + @"GCDAsyncUdpSocket", + [NSBundle mainBundle], + @"Socket closed", + nil); + + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketClosedError userInfo:userInfo]; } - (NSError *)otherError:(NSString *)errMsg { - NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; - - return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain - code:GCDAsyncUdpSocketOtherError - userInfo:userInfo]; + NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketOtherError + userInfo:userInfo]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -1125,1171 +1138,1236 @@ - (NSError *)otherError:(NSString *)errMsg - (BOOL)preOp:(NSError **)errPtr { - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - if (delegate == nil) // Must have delegate set - { - if (errPtr) - { - NSString *msg = @"Attempting to use socket without a delegate. Set a delegate first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - if (delegateQueue == NULL) // Must have delegate queue set - { - if (errPtr) - { - NSString *msg = @"Attempting to use socket without a delegate queue. Set a delegate queue first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - return YES; + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to use socket without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to use socket without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; } /** * This method executes on a global concurrent queue. * When complete, it executes the given completion block on the socketQueue. -**/ + **/ - (void)asyncResolveHost:(NSString *)aHost port:(uint16_t)port - withCompletionBlock:(void (^)(NSArray *addresses, NSError *error))completionBlock -{ - LogTrace(); - - // Check parameter(s) - - if (aHost == nil) - { - NSString *msg = @"The host param is nil. Should be domain name or IP address string."; - NSError *error = [self badParamError:msg]; - - // We should still use dispatch_async since this method is expected to be asynchronous - - dispatch_async(socketQueue, ^{ @autoreleasepool { - - completionBlock(nil, error); - }}); - - return; - } - - // It's possible that the given aHost parameter is actually a NSMutableString. - // So we want to copy it now, within this block that will be executed synchronously. - // This way the asynchronous lookup block below doesn't have to worry about it changing. - - NSString *host = [aHost copy]; - - - dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { - - NSMutableArray *addresses = [NSMutableArray arrayWithCapacity:2]; - NSError *error = nil; - - if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) - { - // Use LOOPBACK address - struct sockaddr_in sockaddr4; - memset(&sockaddr4, 0, sizeof(sockaddr4)); - - sockaddr4.sin_len = sizeof(struct sockaddr_in); - sockaddr4.sin_family = AF_INET; - sockaddr4.sin_port = htons(port); - sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - - struct sockaddr_in6 sockaddr6; - memset(&sockaddr6, 0, sizeof(sockaddr6)); - - sockaddr6.sin6_len = sizeof(struct sockaddr_in6); - sockaddr6.sin6_family = AF_INET6; - sockaddr6.sin6_port = htons(port); - sockaddr6.sin6_addr = in6addr_loopback; - - // Wrap the native address structures and add to list - [addresses addObject:[NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]]; - [addresses addObject:[NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]]; - } - else - { - NSString *portStr = [NSString stringWithFormat:@"%hu", port]; - - struct addrinfo hints, *res, *res0; - - memset(&hints, 0, sizeof(hints)); - hints.ai_family = PF_UNSPEC; - hints.ai_socktype = SOCK_DGRAM; - hints.ai_protocol = IPPROTO_UDP; - - int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); - - if (gai_error) - { - error = [self gaiError:gai_error]; - } - else - { - for(res = res0; res; res = res->ai_next) - { - if (res->ai_family == AF_INET) - { - // Found IPv4 address - // Wrap the native address structure and add to list - - [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; - } - else if (res->ai_family == AF_INET6) - { - - // Fixes connection issues with IPv6, it is the same solution for udp socket. - // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 - struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)(void *)res->ai_addr; - in_port_t *portPtr = &sockaddr->sin6_port; - if ((portPtr != NULL) && (*portPtr == 0)) { - *portPtr = htons(port); - } - - // Found IPv6 address - // Wrap the native address structure and add to list - [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; - } - } - freeaddrinfo(res0); - - if ([addresses count] == 0) - { - error = [self gaiError:EAI_FAIL]; - } - } - } - - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - completionBlock(addresses, error); - }}); - - }}); + withCompletionBlock:(void (^)(NSArray *addresses, + NSError *error))completionBlock +{ + LogTrace(); + + // Check parameter(s) + + if (aHost == nil) + { + NSString *msg = @"The host param is nil. Should be domain name or IP address string."; + NSError *error = [self badParamError:msg]; + + // We should still use dispatch_async since this method is expected to be asynchronous + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + completionBlock(nil, error); + }}); + + return; + } + + // It's possible that the given aHost parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *host = [aHost copy]; + + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, + 0); + dispatch_async(globalConcurrentQueue, + ^{ @autoreleasepool { + + NSMutableArray *addresses = [NSMutableArray arrayWithCapacity:2]; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(struct sockaddr_in); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(struct sockaddr_in6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + // Wrap the native address structures and add to list + [addresses addObject:[NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]]; + [addresses addObject:[NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, + *res, + *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + int gai_error = getaddrinfo([host UTF8String], + [portStr UTF8String], + &hints, + &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + for(res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address + // Wrap the native address structure and add to list + + [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; + } + else if (res->ai_family == AF_INET6) + { + + // Fixes connection issues with IPv6, it is the same solution for udp socket. + // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 + struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)(void *)res->ai_addr; + in_port_t *portPtr = &sockaddr->sin6_port; + if ((portPtr != NULL) && (*portPtr == 0)) { + *portPtr = htons(port); + } + + // Found IPv6 address + // Wrap the native address structure and add to list + [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + completionBlock(addresses, error); + }}); + + }}); } /** * This method picks an address from the given list of addresses. * The address picked depends upon which protocols are disabled, deactived, & preferred. - * + * * Returns the address family (AF_INET or AF_INET6) of the picked address, * or AF_UNSPEC and the corresponding error is there's a problem. -**/ + **/ - (int)getAddress:(NSData **)addressPtr error:(NSError **)errorPtr fromAddresses:(NSArray *)addresses { - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert([addresses count] > 0, @"Expected at least one address"); - - int resultAF = AF_UNSPEC; - NSData *resultAddress = nil; - NSError *resultError = nil; - - // Check for problems - - BOOL resolvedIPv4Address = NO; - BOOL resolvedIPv6Address = NO; - - for (NSData *address in addresses) - { - switch ([[self class] familyFromAddress:address]) - { - case AF_INET : resolvedIPv4Address = YES; break; - case AF_INET6 : resolvedIPv6Address = YES; break; - - default : NSAssert(NO, @"Addresses array contains invalid address"); - } - } - - BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; - BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - - if (isIPv4Disabled && !resolvedIPv6Address) - { - NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address(es)."; - resultError = [self otherError:msg]; - - if (addressPtr) *addressPtr = resultAddress; - if (errorPtr) *errorPtr = resultError; - - return resultAF; - } - - if (isIPv6Disabled && !resolvedIPv4Address) - { - NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address(es)."; - resultError = [self otherError:msg]; - - if (addressPtr) *addressPtr = resultAddress; - if (errorPtr) *errorPtr = resultError; - - return resultAF; - } - - BOOL isIPv4Deactivated = (flags & kIPv4Deactivated) ? YES : NO; - BOOL isIPv6Deactivated = (flags & kIPv6Deactivated) ? YES : NO; - - if (isIPv4Deactivated && !resolvedIPv6Address) - { - NSString *msg = @"IPv4 has been deactivated due to bind/connect, and DNS lookup found no IPv6 address(es)."; - resultError = [self otherError:msg]; - - if (addressPtr) *addressPtr = resultAddress; - if (errorPtr) *errorPtr = resultError; - - return resultAF; - } - - if (isIPv6Deactivated && !resolvedIPv4Address) - { - NSString *msg = @"IPv6 has been deactivated due to bind/connect, and DNS lookup found no IPv4 address(es)."; - resultError = [self otherError:msg]; - - if (addressPtr) *addressPtr = resultAddress; - if (errorPtr) *errorPtr = resultError; - - return resultAF; - } - - // Extract first IPv4 and IPv6 address in list - - BOOL ipv4WasFirstInList = YES; - NSData *address4 = nil; - NSData *address6 = nil; - - for (NSData *address in addresses) - { - int af = [[self class] familyFromAddress:address]; - - if (af == AF_INET) - { - if (address4 == nil) - { - address4 = address; - - if (address6) - break; - else - ipv4WasFirstInList = YES; - } - } - else // af == AF_INET6 - { - if (address6 == nil) - { - address6 = address; - - if (address4) - break; - else - ipv4WasFirstInList = NO; - } - } - } - - // Determine socket type - - BOOL preferIPv4 = (config & kPreferIPv4) ? YES : NO; - BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; - - BOOL useIPv4 = ((preferIPv4 && address4) || (address6 == nil)); - BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); - - NSAssert(!(preferIPv4 && preferIPv6), @"Invalid config state"); - NSAssert(!(useIPv4 && useIPv6), @"Invalid logic"); - - if (useIPv4 || (!useIPv6 && ipv4WasFirstInList)) - { - resultAF = AF_INET; - resultAddress = address4; - } - else - { - resultAF = AF_INET6; - resultAddress = address6; - } - - if (addressPtr) *addressPtr = resultAddress; - if (errorPtr) *errorPtr = resultError; - - return resultAF; + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert([addresses count] > 0, @"Expected at least one address"); + + int resultAF = AF_UNSPEC; + NSData *resultAddress = nil; + NSError *resultError = nil; + + // Check for problems + + BOOL resolvedIPv4Address = NO; + BOOL resolvedIPv6Address = NO; + + for (NSData *address in addresses) + { + switch ([[self class] familyFromAddress:address]) + { + case AF_INET : resolvedIPv4Address = YES; break; + case AF_INET6 : resolvedIPv6Address = YES; break; + + default : NSAssert(NO, @"Addresses array contains invalid address"); + } + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && !resolvedIPv6Address) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + if (isIPv6Disabled && !resolvedIPv4Address) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + BOOL isIPv4Deactivated = (flags & kIPv4Deactivated) ? YES : NO; + BOOL isIPv6Deactivated = (flags & kIPv6Deactivated) ? YES : NO; + + if (isIPv4Deactivated && !resolvedIPv6Address) + { + NSString *msg = @"IPv4 has been deactivated due to bind/connect, and DNS lookup found no IPv6 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + if (isIPv6Deactivated && !resolvedIPv4Address) + { + NSString *msg = @"IPv6 has been deactivated due to bind/connect, and DNS lookup found no IPv4 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + // Extract first IPv4 and IPv6 address in list + + BOOL ipv4WasFirstInList = YES; + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + int af = [[self class] familyFromAddress:address]; + + if (af == AF_INET) + { + if (address4 == nil) + { + address4 = address; + + if (address6) + break; + else + ipv4WasFirstInList = YES; + } + } + else // af == AF_INET6 + { + if (address6 == nil) + { + address6 = address; + + if (address4) + break; + else + ipv4WasFirstInList = NO; + } + } + } + + // Determine socket type + + BOOL preferIPv4 = (config & kPreferIPv4) ? YES : NO; + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + BOOL useIPv4 = ((preferIPv4 && address4) || (address6 == nil)); + BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); + + NSAssert(!(preferIPv4 && preferIPv6), @"Invalid config state"); + NSAssert(!(useIPv4 && useIPv6), @"Invalid logic"); + + if (useIPv4 || (!useIPv6 && ipv4WasFirstInList)) + { + resultAF = AF_INET; + resultAddress = address4; + } + else + { + resultAF = AF_INET6; + resultAddress = address6; + } + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; } /** * Finds the address(es) of an interface description. * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). -**/ + **/ - (void)convertIntefaceDescription:(NSString *)interfaceDescription port:(uint16_t)port intoAddress4:(NSData **)interfaceAddr4Ptr address6:(NSData **)interfaceAddr6Ptr { - NSData *addr4 = nil; - NSData *addr6 = nil; - - if (interfaceDescription == nil) - { - // ANY address - - struct sockaddr_in sockaddr4; - memset(&sockaddr4, 0, sizeof(sockaddr4)); - - sockaddr4.sin_len = sizeof(sockaddr4); - sockaddr4.sin_family = AF_INET; - sockaddr4.sin_port = htons(port); - sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); - - struct sockaddr_in6 sockaddr6; - memset(&sockaddr6, 0, sizeof(sockaddr6)); - - sockaddr6.sin6_len = sizeof(sockaddr6); - sockaddr6.sin6_family = AF_INET6; - sockaddr6.sin6_port = htons(port); - sockaddr6.sin6_addr = in6addr_any; - - addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; - addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; - } - else if ([interfaceDescription isEqualToString:@"localhost"] || - [interfaceDescription isEqualToString:@"loopback"]) - { - // LOOPBACK address - - struct sockaddr_in sockaddr4; - memset(&sockaddr4, 0, sizeof(sockaddr4)); - - sockaddr4.sin_len = sizeof(struct sockaddr_in); - sockaddr4.sin_family = AF_INET; - sockaddr4.sin_port = htons(port); - sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - - struct sockaddr_in6 sockaddr6; - memset(&sockaddr6, 0, sizeof(sockaddr6)); - - sockaddr6.sin6_len = sizeof(struct sockaddr_in6); - sockaddr6.sin6_family = AF_INET6; - sockaddr6.sin6_port = htons(port); - sockaddr6.sin6_addr = in6addr_loopback; - - addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; - addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; - } - else - { - const char *iface = [interfaceDescription UTF8String]; - - struct ifaddrs *addrs; - const struct ifaddrs *cursor; - - if ((getifaddrs(&addrs) == 0)) - { - cursor = addrs; - while (cursor != NULL) - { - if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) - { - // IPv4 - - struct sockaddr_in *addr = (struct sockaddr_in *)(void *)cursor->ifa_addr; - - if (strcmp(cursor->ifa_name, iface) == 0) - { - // Name match - - struct sockaddr_in nativeAddr4 = *addr; - nativeAddr4.sin_port = htons(port); - - addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; - } - else - { - char ip[INET_ADDRSTRLEN]; - - const char *conversion; - conversion = inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)); - - if ((conversion != NULL) && (strcmp(ip, iface) == 0)) - { - // IP match - - struct sockaddr_in nativeAddr4 = *addr; - nativeAddr4.sin_port = htons(port); - - addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; - } - } - } - else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) - { - // IPv6 - - const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; - - if (strcmp(cursor->ifa_name, iface) == 0) - { - // Name match - - struct sockaddr_in6 nativeAddr6 = *addr; - nativeAddr6.sin6_port = htons(port); - - addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; - } - else - { - char ip[INET6_ADDRSTRLEN]; - - const char *conversion; - conversion = inet_ntop(AF_INET6, &addr->sin6_addr, ip, sizeof(ip)); - - if ((conversion != NULL) && (strcmp(ip, iface) == 0)) - { - // IP match - - struct sockaddr_in6 nativeAddr6 = *addr; - nativeAddr6.sin6_port = htons(port); - - addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; - } - } - } - - cursor = cursor->ifa_next; - } - - freeifaddrs(addrs); - } - } - - if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; - if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; -} + NSData *addr4 = nil; + NSData *addr6 = nil; + + if (interfaceDescription == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interfaceDescription isEqualToString:@"localhost"] || + [interfaceDescription isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(struct sockaddr_in); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(struct sockaddr_in6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interfaceDescription UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 -/** - * Converts a numeric hostname into its corresponding address. - * The hostname is expected to be an IPv4 or IPv6 address represented as a human-readable string. (e.g. 192.168.4.34) -**/ -- (void)convertNumericHost:(NSString *)numericHost - port:(uint16_t)port - intoAddress4:(NSData **)addr4Ptr - address6:(NSData **)addr6Ptr -{ - NSData *addr4 = nil; - NSData *addr6 = nil; - - if (numericHost) - { - NSString *portStr = [NSString stringWithFormat:@"%hu", port]; - - struct addrinfo hints, *res, *res0; - - memset(&hints, 0, sizeof(hints)); - hints.ai_family = PF_UNSPEC; - hints.ai_socktype = SOCK_DGRAM; - hints.ai_protocol = IPPROTO_UDP; - hints.ai_flags = AI_NUMERICHOST; // No name resolution should be attempted - - if (getaddrinfo([numericHost UTF8String], [portStr UTF8String], &hints, &res0) == 0) - { - for (res = res0; res; res = res->ai_next) - { - if ((addr4 == nil) && (res->ai_family == AF_INET)) - { - // Found IPv4 address - // Wrap the native address structure - addr4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; - } - else if ((addr6 == nil) && (res->ai_family == AF_INET6)) - { - // Found IPv6 address - // Wrap the native address structure - addr6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; - } - } - freeaddrinfo(res0); - } - } - - if (addr4Ptr) *addr4Ptr = addr4; - if (addr6Ptr) *addr6Ptr = addr6; -} + struct sockaddr_in *addr = (struct sockaddr_in *)(void *)cursor->ifa_addr; -- (BOOL)isConnectedToAddress4:(NSData *)someAddr4 -{ - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert(flags & kDidConnect, @"Not connected"); - NSAssert(cachedConnectedAddress, @"Expected cached connected address"); - - if (cachedConnectedFamily != AF_INET) - { - return NO; - } - - const struct sockaddr_in *sSockaddr4 = (const struct sockaddr_in *)[someAddr4 bytes]; - const struct sockaddr_in *cSockaddr4 = (const struct sockaddr_in *)[cachedConnectedAddress bytes]; - - if (memcmp(&sSockaddr4->sin_addr, &cSockaddr4->sin_addr, sizeof(struct in_addr)) != 0) - { - return NO; - } - if (memcmp(&sSockaddr4->sin_port, &cSockaddr4->sin_port, sizeof(in_port_t)) != 0) - { - return NO; - } - - return YES; -} + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match -- (BOOL)isConnectedToAddress6:(NSData *)someAddr6 -{ - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert(flags & kDidConnect, @"Not connected"); - NSAssert(cachedConnectedAddress, @"Expected cached connected address"); - - if (cachedConnectedFamily != AF_INET6) - { - return NO; - } - - const struct sockaddr_in6 *sSockaddr6 = (const struct sockaddr_in6 *)[someAddr6 bytes]; - const struct sockaddr_in6 *cSockaddr6 = (const struct sockaddr_in6 *)[cachedConnectedAddress bytes]; - - if (memcmp(&sSockaddr6->sin6_addr, &cSockaddr6->sin6_addr, sizeof(struct in6_addr)) != 0) - { - return NO; - } - if (memcmp(&sSockaddr6->sin6_port, &cSockaddr6->sin6_port, sizeof(in_port_t)) != 0) - { - return NO; - } - - return YES; -} + struct sockaddr_in nativeAddr4 = *addr; + nativeAddr4.sin_port = htons(port); -- (unsigned int)indexOfInterfaceAddr4:(NSData *)interfaceAddr4 -{ - if (interfaceAddr4 == nil) - return 0; - if ([interfaceAddr4 length] != sizeof(struct sockaddr_in)) - return 0; - - int result = 0; - const struct sockaddr_in *ifaceAddr = (const struct sockaddr_in *)[interfaceAddr4 bytes]; - - struct ifaddrs *addrs; - const struct ifaddrs *cursor; - - if ((getifaddrs(&addrs) == 0)) - { - cursor = addrs; - while (cursor != NULL) - { - if (cursor->ifa_addr->sa_family == AF_INET) - { - // IPv4 - - const struct sockaddr_in *addr = (const struct sockaddr_in *)(const void *)cursor->ifa_addr; - - if (memcmp(&addr->sin_addr, &ifaceAddr->sin_addr, sizeof(struct in_addr)) == 0) - { - result = if_nametoindex(cursor->ifa_name); - break; - } - } - - cursor = cursor->ifa_next; - } - - freeifaddrs(addrs); - } - - return result; -} + addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; -- (unsigned int)indexOfInterfaceAddr6:(NSData *)interfaceAddr6 + const char *conversion; + conversion = inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + struct sockaddr_in nativeAddr4 = *addr; + nativeAddr4.sin_port = htons(port); + + addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + struct sockaddr_in6 nativeAddr6 = *addr; + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion; + conversion = inet_ntop(AF_INET6, &addr->sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + struct sockaddr_in6 nativeAddr6 = *addr; + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +/** + * Converts a numeric hostname into its corresponding address. + * The hostname is expected to be an IPv4 or IPv6 address represented as a human-readable string. (e.g. 192.168.4.34) + **/ +- (void)convertNumericHost:(NSString *)numericHost + port:(uint16_t)port + intoAddress4:(NSData **)addr4Ptr + address6:(NSData **)addr6Ptr { - if (interfaceAddr6 == nil) - return 0; - if ([interfaceAddr6 length] != sizeof(struct sockaddr_in6)) - return 0; - - int result = 0; - const struct sockaddr_in6 *ifaceAddr = (const struct sockaddr_in6 *)[interfaceAddr6 bytes]; - - struct ifaddrs *addrs; - const struct ifaddrs *cursor; - - if ((getifaddrs(&addrs) == 0)) - { - cursor = addrs; - while (cursor != NULL) - { - if (cursor->ifa_addr->sa_family == AF_INET6) - { - // IPv6 - - const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; - - if (memcmp(&addr->sin6_addr, &ifaceAddr->sin6_addr, sizeof(struct in6_addr)) == 0) - { - result = if_nametoindex(cursor->ifa_name); - break; - } - } - - cursor = cursor->ifa_next; - } - - freeifaddrs(addrs); - } - - return result; + NSData *addr4 = nil; + NSData *addr6 = nil; + + if (numericHost) + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + hints.ai_flags = AI_NUMERICHOST; // No name resolution should be attempted + + if (getaddrinfo([numericHost UTF8String], + [portStr UTF8String], + &hints, + &res0) == 0) + { + for (res = res0; res; res = res->ai_next) + { + if ((addr4 == nil) && (res->ai_family == AF_INET)) + { + // Found IPv4 address + // Wrap the native address structure + addr4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + else if ((addr6 == nil) && (res->ai_family == AF_INET6)) + { + // Found IPv6 address + // Wrap the native address structure + addr6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + } + freeaddrinfo(res0); + } + } + + if (addr4Ptr) *addr4Ptr = addr4; + if (addr6Ptr) *addr6Ptr = addr6; } -- (void)setupSendAndReceiveSourcesForSocket4 +- (BOOL)isConnectedToAddress4:(NSData *)someAddr4 { - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - send4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket4FD, 0, socketQueue); - receive4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); - - // Setup event handlers - - dispatch_source_set_event_handler(send4Source, ^{ @autoreleasepool { - - LogVerbose(@"send4EventBlock"); - LogVerbose(@"dispatch_source_get_data(send4Source) = %lu", dispatch_source_get_data(send4Source)); - - self->flags |= kSock4CanAcceptBytes; - - // If we're ready to send data, do so immediately. - // Otherwise pause the send source or it will continue to fire over and over again. - - if (self->currentSend == nil) - { - LogVerbose(@"Nothing to send"); - [self suspendSend4Source]; - } - else if (self->currentSend->resolveInProgress) - { - LogVerbose(@"currentSend - waiting for address resolve"); - [self suspendSend4Source]; - } - else if (self->currentSend->filterInProgress) - { - LogVerbose(@"currentSend - waiting on sendFilter"); - [self suspendSend4Source]; - } - else - { - [self doSend]; - } - - }}); - - dispatch_source_set_event_handler(receive4Source, ^{ @autoreleasepool { - - LogVerbose(@"receive4EventBlock"); - - self->socket4FDBytesAvailable = dispatch_source_get_data(self->receive4Source); - LogVerbose(@"socket4FDBytesAvailable: %lu", socket4FDBytesAvailable); - - if (self->socket4FDBytesAvailable > 0) - [self doReceive]; - else - [self doReceiveEOF]; - - }}); - - // Setup cancel handlers - - __block int socketFDRefCount = 2; - - int theSocketFD = socket4FD; - - #if !OS_OBJECT_USE_OBJC - dispatch_source_t theSendSource = send4Source; - dispatch_source_t theReceiveSource = receive4Source; - #endif - - dispatch_source_set_cancel_handler(send4Source, ^{ - - LogVerbose(@"send4CancelBlock"); - - #if !OS_OBJECT_USE_OBJC - LogVerbose(@"dispatch_release(send4Source)"); - dispatch_release(theSendSource); - #endif - - if (--socketFDRefCount == 0) - { - LogVerbose(@"close(socket4FD)"); - close(theSocketFD); - } - }); - - dispatch_source_set_cancel_handler(receive4Source, ^{ - - LogVerbose(@"receive4CancelBlock"); - - #if !OS_OBJECT_USE_OBJC - LogVerbose(@"dispatch_release(receive4Source)"); - dispatch_release(theReceiveSource); - #endif - - if (--socketFDRefCount == 0) - { - LogVerbose(@"close(socket4FD)"); - close(theSocketFD); - } - }); - - // We will not be able to receive until the socket is bound to a port, - // either explicitly via bind, or implicitly by connect or by sending data. - // - // But we should be able to send immediately. - - socket4FDBytesAvailable = 0; - flags |= kSock4CanAcceptBytes; - - flags |= kSend4SourceSuspended; - flags |= kReceive4SourceSuspended; + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert(flags & kDidConnect, @"Not connected"); + NSAssert(cachedConnectedAddress, @"Expected cached connected address"); + + if (cachedConnectedFamily != AF_INET) + { + return NO; + } + + const struct sockaddr_in *sSockaddr4 = (const struct sockaddr_in *)[someAddr4 bytes]; + const struct sockaddr_in *cSockaddr4 = (const struct sockaddr_in *)[cachedConnectedAddress bytes]; + + if (memcmp(&sSockaddr4->sin_addr, + &cSockaddr4->sin_addr, + sizeof(struct in_addr)) != 0) + { + return NO; + } + if (memcmp(&sSockaddr4->sin_port, + &cSockaddr4->sin_port, + sizeof(in_port_t)) != 0) + { + return NO; + } + + return YES; } -- (void)setupSendAndReceiveSourcesForSocket6 +- (BOOL)isConnectedToAddress6:(NSData *)someAddr6 { - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - send6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket6FD, 0, socketQueue); - receive6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); - - // Setup event handlers - - dispatch_source_set_event_handler(send6Source, ^{ @autoreleasepool { - - LogVerbose(@"send6EventBlock"); - LogVerbose(@"dispatch_source_get_data(send6Source) = %lu", dispatch_source_get_data(send6Source)); - - self->flags |= kSock6CanAcceptBytes; - - // If we're ready to send data, do so immediately. - // Otherwise pause the send source or it will continue to fire over and over again. - - if (self->currentSend == nil) - { - LogVerbose(@"Nothing to send"); - [self suspendSend6Source]; - } - else if (self->currentSend->resolveInProgress) - { - LogVerbose(@"currentSend - waiting for address resolve"); - [self suspendSend6Source]; - } - else if (self->currentSend->filterInProgress) - { - LogVerbose(@"currentSend - waiting on sendFilter"); - [self suspendSend6Source]; - } - else - { - [self doSend]; - } - - }}); - - dispatch_source_set_event_handler(receive6Source, ^{ @autoreleasepool { - - LogVerbose(@"receive6EventBlock"); - - self->socket6FDBytesAvailable = dispatch_source_get_data(self->receive6Source); - LogVerbose(@"socket6FDBytesAvailable: %lu", socket6FDBytesAvailable); - - if (self->socket6FDBytesAvailable > 0) - [self doReceive]; - else - [self doReceiveEOF]; - - }}); - - // Setup cancel handlers - - __block int socketFDRefCount = 2; - - int theSocketFD = socket6FD; - - #if !OS_OBJECT_USE_OBJC - dispatch_source_t theSendSource = send6Source; - dispatch_source_t theReceiveSource = receive6Source; - #endif - - dispatch_source_set_cancel_handler(send6Source, ^{ - - LogVerbose(@"send6CancelBlock"); - - #if !OS_OBJECT_USE_OBJC - LogVerbose(@"dispatch_release(send6Source)"); - dispatch_release(theSendSource); - #endif - - if (--socketFDRefCount == 0) - { - LogVerbose(@"close(socket6FD)"); - close(theSocketFD); - } - }); - - dispatch_source_set_cancel_handler(receive6Source, ^{ - - LogVerbose(@"receive6CancelBlock"); - - #if !OS_OBJECT_USE_OBJC - LogVerbose(@"dispatch_release(receive6Source)"); - dispatch_release(theReceiveSource); - #endif - - if (--socketFDRefCount == 0) - { - LogVerbose(@"close(socket6FD)"); - close(theSocketFD); - } - }); - - // We will not be able to receive until the socket is bound to a port, - // either explicitly via bind, or implicitly by connect or by sending data. - // - // But we should be able to send immediately. - - socket6FDBytesAvailable = 0; - flags |= kSock6CanAcceptBytes; - - flags |= kSend6SourceSuspended; - flags |= kReceive6SourceSuspended; + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert(flags & kDidConnect, @"Not connected"); + NSAssert(cachedConnectedAddress, @"Expected cached connected address"); + + if (cachedConnectedFamily != AF_INET6) + { + return NO; + } + + const struct sockaddr_in6 *sSockaddr6 = (const struct sockaddr_in6 *)[someAddr6 bytes]; + const struct sockaddr_in6 *cSockaddr6 = (const struct sockaddr_in6 *)[cachedConnectedAddress bytes]; + + if (memcmp(&sSockaddr6->sin6_addr, + &cSockaddr6->sin6_addr, + sizeof(struct in6_addr)) != 0) + { + return NO; + } + if (memcmp(&sSockaddr6->sin6_port, + &cSockaddr6->sin6_port, + sizeof(in_port_t)) != 0) + { + return NO; + } + + return YES; } -- (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __autoreleasing *)errPtr +- (unsigned int)indexOfInterfaceAddr4:(NSData *)interfaceAddr4 { - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert(((flags & kDidCreateSockets) == 0), @"Sockets have already been created"); - - // CreateSocket Block - // This block will be invoked below. - - int(^createSocket)(int) = ^int (int domain) { - - int socketFD = socket(domain, SOCK_DGRAM, 0); - - if (socketFD == SOCKET_NULL) - { - if (errPtr) - *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; - - return SOCKET_NULL; - } - - int status; - - // Set socket options - - status = fcntl(socketFD, F_SETFL, O_NONBLOCK); - if (status == -1) - { - if (errPtr) - *errPtr = [self errnoErrorWithReason:@"Error enabling non-blocking IO on socket (fcntl)"]; - - close(socketFD); - return SOCKET_NULL; - } - - int reuseaddr = 1; - status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr)); - if (status == -1) - { - if (errPtr) - *errPtr = [self errnoErrorWithReason:@"Error enabling address reuse (setsockopt)"]; - - close(socketFD); - return SOCKET_NULL; - } - - int nosigpipe = 1; - status = setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); - if (status == -1) - { - if (errPtr) - *errPtr = [self errnoErrorWithReason:@"Error disabling sigpipe (setsockopt)"]; - - close(socketFD); - return SOCKET_NULL; - } - - /** - * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. - * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. - * - * The default maximum size of the UDP buffer in iOS is 9216 bytes. - * - * This is the reason of #222(GCD does not necessarily return the size of an entire UDP packet) and - * #535(GCDAsyncUDPSocket can not send data when data is greater than 9K) - * - * - * Enlarge the maximum size of UDP packet. - * I can not ensure the protocol type now so that the max size is set to 65535 :) - **/ - - status = setsockopt(socketFD, SOL_SOCKET, SO_SNDBUF, (const char*)&self->maxSendSize, sizeof(int)); - if (status == -1) + if (interfaceAddr4 == nil) + return 0; + if ([interfaceAddr4 length] != sizeof(struct sockaddr_in)) + return 0; + + int result = 0; + const struct sockaddr_in *ifaceAddr = (const struct sockaddr_in *)[interfaceAddr4 bytes]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if (cursor->ifa_addr->sa_family == AF_INET) + { + // IPv4 + + const struct sockaddr_in *addr = (const struct sockaddr_in *)(const void *)cursor->ifa_addr; + + if (memcmp(&addr->sin_addr, + &ifaceAddr->sin_addr, + sizeof(struct in_addr)) == 0) { - if (errPtr) - *errPtr = [self errnoErrorWithReason:@"Error setting send buffer size (setsockopt)"]; - close(socketFD); - return SOCKET_NULL; + result = if_nametoindex(cursor->ifa_name); + break; } - - status = setsockopt(socketFD, SOL_SOCKET, SO_RCVBUF, (const char*)&self->maxSendSize, sizeof(int)); - if (status == -1) + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + + return result; +} + +- (unsigned int)indexOfInterfaceAddr6:(NSData *)interfaceAddr6 +{ + if (interfaceAddr6 == nil) + return 0; + if ([interfaceAddr6 length] != sizeof(struct sockaddr_in6)) + return 0; + + int result = 0; + const struct sockaddr_in6 *ifaceAddr = (const struct sockaddr_in6 *)[interfaceAddr6 bytes]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if (cursor->ifa_addr->sa_family == AF_INET6) + { + // IPv6 + + const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; + + if (memcmp(&addr->sin6_addr, + &ifaceAddr->sin6_addr, + sizeof(struct in6_addr)) == 0) { - if (errPtr) - *errPtr = [self errnoErrorWithReason:@"Error setting receive buffer size (setsockopt)"]; - close(socketFD); - return SOCKET_NULL; + result = if_nametoindex(cursor->ifa_name); + break; } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + + return result; +} + +- (void)setupSendAndReceiveSourcesForSocket4 +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + send4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, + socket4FD, + 0, + socketQueue); + receive4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + socket4FD, + 0, + socketQueue); + + // Setup event handlers + + dispatch_source_set_event_handler(send4Source, + ^{ @autoreleasepool { + + LogVerbose(@"send4EventBlock"); + LogVerbose(@"dispatch_source_get_data(send4Source) = %lu", + dispatch_source_get_data(send4Source)); + + self->flags |= kSock4CanAcceptBytes; + + // If we're ready to send data, do so immediately. + // Otherwise pause the send source or it will continue to fire over and over again. + + if (self->currentSend == nil) + { + LogVerbose(@"Nothing to send"); + [self suspendSend4Source]; + } + else if (self->currentSend->resolveInProgress) + { + LogVerbose(@"currentSend - waiting for address resolve"); + [self suspendSend4Source]; + } + else if (self->currentSend->filterInProgress) + { + LogVerbose(@"currentSend - waiting on sendFilter"); + [self suspendSend4Source]; + } + else + { + [self doSend]; + } + + }}); + + dispatch_source_set_event_handler(receive4Source, + ^{ @autoreleasepool { + + LogVerbose(@"receive4EventBlock"); + + self->socket4FDBytesAvailable = dispatch_source_get_data(self->receive4Source); + LogVerbose(@"socket4FDBytesAvailable: %lu", socket4FDBytesAvailable); + + if (self->socket4FDBytesAvailable > 0) + [self doReceive]; + else + [self doReceiveEOF]; + + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + int theSocketFD = socket4FD; + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theSendSource = send4Source; + dispatch_source_t theReceiveSource = receive4Source; +#endif + + dispatch_source_set_cancel_handler(send4Source, ^{ + + LogVerbose(@"send4CancelBlock"); + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(send4Source)"); + dispatch_release(theSendSource); +#endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket4FD)"); + close(theSocketFD); + } + }); + + dispatch_source_set_cancel_handler(receive4Source, ^{ + + LogVerbose(@"receive4CancelBlock"); + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(receive4Source)"); + dispatch_release(theReceiveSource); +#endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket4FD)"); + close(theSocketFD); + } + }); + + // We will not be able to receive until the socket is bound to a port, + // either explicitly via bind, or implicitly by connect or by sending data. + // + // But we should be able to send immediately. + + socket4FDBytesAvailable = 0; + flags |= kSock4CanAcceptBytes; + + flags |= kSend4SourceSuspended; + flags |= kReceive4SourceSuspended; +} + +- (void)setupSendAndReceiveSourcesForSocket6 +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + send6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, + socket6FD, + 0, + socketQueue); + receive6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, + socket6FD, + 0, + socketQueue); + + // Setup event handlers + + dispatch_source_set_event_handler(send6Source, + ^{ @autoreleasepool { + + LogVerbose(@"send6EventBlock"); + LogVerbose(@"dispatch_source_get_data(send6Source) = %lu", + dispatch_source_get_data(send6Source)); + + self->flags |= kSock6CanAcceptBytes; + + // If we're ready to send data, do so immediately. + // Otherwise pause the send source or it will continue to fire over and over again. + + if (self->currentSend == nil) + { + LogVerbose(@"Nothing to send"); + [self suspendSend6Source]; + } + else if (self->currentSend->resolveInProgress) + { + LogVerbose(@"currentSend - waiting for address resolve"); + [self suspendSend6Source]; + } + else if (self->currentSend->filterInProgress) + { + LogVerbose(@"currentSend - waiting on sendFilter"); + [self suspendSend6Source]; + } + else + { + [self doSend]; + } + + }}); + + dispatch_source_set_event_handler(receive6Source, + ^{ @autoreleasepool { + + LogVerbose(@"receive6EventBlock"); + + self->socket6FDBytesAvailable = dispatch_source_get_data(self->receive6Source); + LogVerbose(@"socket6FDBytesAvailable: %lu", socket6FDBytesAvailable); + + if (self->socket6FDBytesAvailable > 0) + [self doReceive]; + else + [self doReceiveEOF]; + + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + int theSocketFD = socket6FD; + +#if !OS_OBJECT_USE_OBJC + dispatch_source_t theSendSource = send6Source; + dispatch_source_t theReceiveSource = receive6Source; +#endif + + dispatch_source_set_cancel_handler(send6Source, ^{ - - return socketFD; - }; - - // Create sockets depending upon given configuration. - - if (useIPv4) - { - LogVerbose(@"Creating IPv4 socket"); - - socket4FD = createSocket(AF_INET); - if (socket4FD == SOCKET_NULL) - { - // errPtr set in local createSocket() block - return NO; - } - } - - if (useIPv6) - { - LogVerbose(@"Creating IPv6 socket"); - - socket6FD = createSocket(AF_INET6); - if (socket6FD == SOCKET_NULL) - { - // errPtr set in local createSocket() block - - if (socket4FD != SOCKET_NULL) - { - close(socket4FD); - socket4FD = SOCKET_NULL; - } - - return NO; - } - } - - // Setup send and receive sources - - if (useIPv4) - [self setupSendAndReceiveSourcesForSocket4]; - if (useIPv6) - [self setupSendAndReceiveSourcesForSocket6]; - - flags |= kDidCreateSockets; - return YES; + LogVerbose(@"send6CancelBlock"); + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(send6Source)"); + dispatch_release(theSendSource); +#endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket6FD)"); + close(theSocketFD); + } + }); + + dispatch_source_set_cancel_handler(receive6Source, ^{ + + LogVerbose(@"receive6CancelBlock"); + +#if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(receive6Source)"); + dispatch_release(theReceiveSource); +#endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket6FD)"); + close(theSocketFD); + } + }); + + // We will not be able to receive until the socket is bound to a port, + // either explicitly via bind, or implicitly by connect or by sending data. + // + // But we should be able to send immediately. + + socket6FDBytesAvailable = 0; + flags |= kSock6CanAcceptBytes; + + flags |= kSend6SourceSuspended; + flags |= kReceive6SourceSuspended; +} + +- (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __autoreleasing *)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert(((flags & kDidCreateSockets) == 0), + @"Sockets have already been created"); + + // CreateSocket Block + // This block will be invoked below. + + int(^createSocket)(int) = ^int (int domain) { + + int socketFD = socket(domain, SOCK_DGRAM, 0); + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error enabling non-blocking IO on socket (fcntl)"]; + + close(socketFD); + return SOCKET_NULL; + } + + int reuseaddr = 1; + status = setsockopt(socketFD, + SOL_SOCKET, + SO_REUSEADDR, + &reuseaddr, + sizeof(reuseaddr)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error enabling address reuse (setsockopt)"]; + + close(socketFD); + return SOCKET_NULL; + } + + int nosigpipe = 1; + status = setsockopt(socketFD, + SOL_SOCKET, + SO_NOSIGPIPE, + &nosigpipe, + sizeof(nosigpipe)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error disabling sigpipe (setsockopt)"]; + + close(socketFD); + return SOCKET_NULL; + } + + /** + * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. + * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. + * + * The default maximum size of the UDP buffer in iOS is 9216 bytes. + * + * This is the reason of #222(GCD does not necessarily return the size of an entire UDP packet) and + * #535(GCDAsyncUDPSocket can not send data when data is greater than 9K) + * + * + * Enlarge the maximum size of UDP packet. + * I can not ensure the protocol type now so that the max size is set to 65535 :) + **/ + + status = setsockopt(socketFD, + SOL_SOCKET, + SO_SNDBUF, + (const char*)&self->maxSendSize, + sizeof(int)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error setting send buffer size (setsockopt)"]; + close(socketFD); + return SOCKET_NULL; + } + + status = setsockopt(socketFD, + SOL_SOCKET, + SO_RCVBUF, + (const char*)&self->maxSendSize, + sizeof(int)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error setting receive buffer size (setsockopt)"]; + close(socketFD); + return SOCKET_NULL; + } + + + return socketFD; + }; + + // Create sockets depending upon given configuration. + + if (useIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = createSocket(AF_INET); + if (socket4FD == SOCKET_NULL) + { + // errPtr set in local createSocket() block + return NO; + } + } + + if (useIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = createSocket(AF_INET6); + if (socket6FD == SOCKET_NULL) + { + // errPtr set in local createSocket() block + + if (socket4FD != SOCKET_NULL) + { + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + return NO; + } + } + + // Setup send and receive sources + + if (useIPv4) + [self setupSendAndReceiveSourcesForSocket4]; + if (useIPv6) + [self setupSendAndReceiveSourcesForSocket6]; + + flags |= kDidCreateSockets; + return YES; } - (BOOL)createSockets:(NSError **)errPtr { - LogTrace(); - - BOOL useIPv4 = [self isIPv4Enabled]; - BOOL useIPv6 = [self isIPv6Enabled]; - - return [self createSocket4:useIPv4 socket6:useIPv6 error:errPtr]; + LogTrace(); + + BOOL useIPv4 = [self isIPv4Enabled]; + BOOL useIPv6 = [self isIPv6Enabled]; + + return [self createSocket4:useIPv4 socket6:useIPv6 error:errPtr]; } - (void)suspendSend4Source { - if (send4Source && !(flags & kSend4SourceSuspended)) - { - LogVerbose(@"dispatch_suspend(send4Source)"); - - dispatch_suspend(send4Source); - flags |= kSend4SourceSuspended; - } + if (send4Source && !(flags & kSend4SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(send4Source)"); + + dispatch_suspend(send4Source); + flags |= kSend4SourceSuspended; + } } - (void)suspendSend6Source { - if (send6Source && !(flags & kSend6SourceSuspended)) - { - LogVerbose(@"dispatch_suspend(send6Source)"); - - dispatch_suspend(send6Source); - flags |= kSend6SourceSuspended; - } + if (send6Source && !(flags & kSend6SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(send6Source)"); + + dispatch_suspend(send6Source); + flags |= kSend6SourceSuspended; + } } - (void)resumeSend4Source { - if (send4Source && (flags & kSend4SourceSuspended)) - { - LogVerbose(@"dispatch_resume(send4Source)"); - - dispatch_resume(send4Source); - flags &= ~kSend4SourceSuspended; - } + if (send4Source && (flags & kSend4SourceSuspended)) + { + LogVerbose(@"dispatch_resume(send4Source)"); + + dispatch_resume(send4Source); + flags &= ~kSend4SourceSuspended; + } } - (void)resumeSend6Source { - if (send6Source && (flags & kSend6SourceSuspended)) - { - LogVerbose(@"dispatch_resume(send6Source)"); - - dispatch_resume(send6Source); - flags &= ~kSend6SourceSuspended; - } + if (send6Source && (flags & kSend6SourceSuspended)) + { + LogVerbose(@"dispatch_resume(send6Source)"); + + dispatch_resume(send6Source); + flags &= ~kSend6SourceSuspended; + } } - (void)suspendReceive4Source { - if (receive4Source && !(flags & kReceive4SourceSuspended)) - { - LogVerbose(@"dispatch_suspend(receive4Source)"); - - dispatch_suspend(receive4Source); - flags |= kReceive4SourceSuspended; - } + if (receive4Source && !(flags & kReceive4SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(receive4Source)"); + + dispatch_suspend(receive4Source); + flags |= kReceive4SourceSuspended; + } } - (void)suspendReceive6Source { - if (receive6Source && !(flags & kReceive6SourceSuspended)) - { - LogVerbose(@"dispatch_suspend(receive6Source)"); - - dispatch_suspend(receive6Source); - flags |= kReceive6SourceSuspended; - } + if (receive6Source && !(flags & kReceive6SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(receive6Source)"); + + dispatch_suspend(receive6Source); + flags |= kReceive6SourceSuspended; + } } - (void)resumeReceive4Source { - if (receive4Source && (flags & kReceive4SourceSuspended)) - { - LogVerbose(@"dispatch_resume(receive4Source)"); - - dispatch_resume(receive4Source); - flags &= ~kReceive4SourceSuspended; - } + if (receive4Source && (flags & kReceive4SourceSuspended)) + { + LogVerbose(@"dispatch_resume(receive4Source)"); + + dispatch_resume(receive4Source); + flags &= ~kReceive4SourceSuspended; + } } - (void)resumeReceive6Source { - if (receive6Source && (flags & kReceive6SourceSuspended)) - { - LogVerbose(@"dispatch_resume(receive6Source)"); - - dispatch_resume(receive6Source); - flags &= ~kReceive6SourceSuspended; - } + if (receive6Source && (flags & kReceive6SourceSuspended)) + { + LogVerbose(@"dispatch_resume(receive6Source)"); + + dispatch_resume(receive6Source); + flags &= ~kReceive6SourceSuspended; + } } - (void)closeSocket4 { - if (socket4FD != SOCKET_NULL) - { - LogVerbose(@"dispatch_source_cancel(send4Source)"); - dispatch_source_cancel(send4Source); - - LogVerbose(@"dispatch_source_cancel(receive4Source)"); - dispatch_source_cancel(receive4Source); - - // For some crazy reason (in my opinion), cancelling a dispatch source doesn't - // invoke the cancel handler if the dispatch source is paused. - // So we have to unpause the source if needed. - // This allows the cancel handler to be run, which in turn releases the source and closes the socket. - - [self resumeSend4Source]; - [self resumeReceive4Source]; - - // The sockets will be closed by the cancel handlers of the corresponding source - - send4Source = NULL; - receive4Source = NULL; - - socket4FD = SOCKET_NULL; - - // Clear socket states - - socket4FDBytesAvailable = 0; - flags &= ~kSock4CanAcceptBytes; - - // Clear cached info - - cachedLocalAddress4 = nil; - cachedLocalHost4 = nil; - cachedLocalPort4 = 0; - } + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"dispatch_source_cancel(send4Source)"); + dispatch_source_cancel(send4Source); + + LogVerbose(@"dispatch_source_cancel(receive4Source)"); + dispatch_source_cancel(receive4Source); + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + [self resumeSend4Source]; + [self resumeReceive4Source]; + + // The sockets will be closed by the cancel handlers of the corresponding source + + send4Source = NULL; + receive4Source = NULL; + + socket4FD = SOCKET_NULL; + + // Clear socket states + + socket4FDBytesAvailable = 0; + flags &= ~kSock4CanAcceptBytes; + + // Clear cached info + + cachedLocalAddress4 = nil; + cachedLocalHost4 = nil; + cachedLocalPort4 = 0; + } } - (void)closeSocket6 { - if (socket6FD != SOCKET_NULL) - { - LogVerbose(@"dispatch_source_cancel(send6Source)"); - dispatch_source_cancel(send6Source); - - LogVerbose(@"dispatch_source_cancel(receive6Source)"); - dispatch_source_cancel(receive6Source); - - // For some crazy reason (in my opinion), cancelling a dispatch source doesn't - // invoke the cancel handler if the dispatch source is paused. - // So we have to unpause the source if needed. - // This allows the cancel handler to be run, which in turn releases the source and closes the socket. - - [self resumeSend6Source]; - [self resumeReceive6Source]; - - send6Source = NULL; - receive6Source = NULL; - - // The sockets will be closed by the cancel handlers of the corresponding source - - socket6FD = SOCKET_NULL; - - // Clear socket states - - socket6FDBytesAvailable = 0; - flags &= ~kSock6CanAcceptBytes; - - // Clear cached info - - cachedLocalAddress6 = nil; - cachedLocalHost6 = nil; - cachedLocalPort6 = 0; - } + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"dispatch_source_cancel(send6Source)"); + dispatch_source_cancel(send6Source); + + LogVerbose(@"dispatch_source_cancel(receive6Source)"); + dispatch_source_cancel(receive6Source); + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + [self resumeSend6Source]; + [self resumeReceive6Source]; + + send6Source = NULL; + receive6Source = NULL; + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket6FD = SOCKET_NULL; + + // Clear socket states + + socket6FDBytesAvailable = 0; + flags &= ~kSock6CanAcceptBytes; + + // Clear cached info + + cachedLocalAddress6 = nil; + cachedLocalHost6 = nil; + cachedLocalPort6 = 0; + } } - (void)closeSockets { - [self closeSocket4]; - [self closeSocket6]; - - flags &= ~kDidCreateSockets; + [self closeSocket4]; + [self closeSocket6]; + + flags &= ~kDidCreateSockets; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2302,471 +2380,482 @@ - (BOOL)getLocalAddress:(NSData **)dataPtr forSocket:(int)socketFD withFamily:(int)socketFamily { - - NSData *data = nil; - NSString *host = nil; - uint16_t port = 0; - - if (socketFamily == AF_INET) - { - struct sockaddr_in sockaddr4; - socklen_t sockaddr4len = sizeof(sockaddr4); - - if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) - { - data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; - host = [[self class] hostFromSockaddr4:&sockaddr4]; - port = [[self class] portFromSockaddr4:&sockaddr4]; - } - else - { - LogWarn(@"Error in getsockname: %@", [self errnoError]); - } - } - else if (socketFamily == AF_INET6) - { - struct sockaddr_in6 sockaddr6; - socklen_t sockaddr6len = sizeof(sockaddr6); - - if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) - { - data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; - host = [[self class] hostFromSockaddr6:&sockaddr6]; - port = [[self class] portFromSockaddr6:&sockaddr6]; - } - else - { - LogWarn(@"Error in getsockname: %@", [self errnoError]); - } - } - - if (dataPtr) *dataPtr = data; - if (hostPtr) *hostPtr = host; - if (portPtr) *portPtr = port; - - return (data != nil); + + NSData *data = nil; + NSString *host = nil; + uint16_t port = 0; + + if (socketFamily == AF_INET) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, + (struct sockaddr *)&sockaddr4, + &sockaddr4len) == 0) + { + data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; + host = [[self class] hostFromSockaddr4:&sockaddr4]; + port = [[self class] portFromSockaddr4:&sockaddr4]; + } + else + { + LogWarn(@"Error in getsockname: %@", [self errnoError]); + } + } + else if (socketFamily == AF_INET6) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, + (struct sockaddr *)&sockaddr6, + &sockaddr6len) == 0) + { + data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; + host = [[self class] hostFromSockaddr6:&sockaddr6]; + port = [[self class] portFromSockaddr6:&sockaddr6]; + } + else + { + LogWarn(@"Error in getsockname: %@", [self errnoError]); + } + } + + if (dataPtr) *dataPtr = data; + if (hostPtr) *hostPtr = host; + if (portPtr) *portPtr = port; + + return (data != nil); } - (void)maybeUpdateCachedLocalAddress4Info { - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - if ( cachedLocalAddress4 || ((flags & kDidBind) == 0) || (socket4FD == SOCKET_NULL) ) - { - return; - } - - NSData *address = nil; - NSString *host = nil; - uint16_t port = 0; - - if ([self getLocalAddress:&address host:&host port:&port forSocket:socket4FD withFamily:AF_INET]) - { - - cachedLocalAddress4 = address; - cachedLocalHost4 = host; - cachedLocalPort4 = port; - } + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + if ( cachedLocalAddress4 || ((flags & kDidBind) == 0) || (socket4FD == SOCKET_NULL) ) + { + return; + } + + NSData *address = nil; + NSString *host = nil; + uint16_t port = 0; + + if ([self getLocalAddress:&address host:&host port:&port forSocket:socket4FD withFamily:AF_INET]) + { + + cachedLocalAddress4 = address; + cachedLocalHost4 = host; + cachedLocalPort4 = port; + } } - (void)maybeUpdateCachedLocalAddress6Info { - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - if ( cachedLocalAddress6 || ((flags & kDidBind) == 0) || (socket6FD == SOCKET_NULL) ) - { - return; - } - - NSData *address = nil; - NSString *host = nil; - uint16_t port = 0; - - if ([self getLocalAddress:&address host:&host port:&port forSocket:socket6FD withFamily:AF_INET6]) - { - - cachedLocalAddress6 = address; - cachedLocalHost6 = host; - cachedLocalPort6 = port; - } + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + if ( cachedLocalAddress6 || ((flags & kDidBind) == 0) || (socket6FD == SOCKET_NULL) ) + { + return; + } + + NSData *address = nil; + NSString *host = nil; + uint16_t port = 0; + + if ([self getLocalAddress:&address host:&host port:&port forSocket:socket6FD withFamily:AF_INET6]) + { + + cachedLocalAddress6 = address; + cachedLocalHost6 = host; + cachedLocalPort6 = port; + } } - (NSData *)localAddress { - __block NSData *result = nil; - - dispatch_block_t block = ^{ - - if (self->socket4FD != SOCKET_NULL) - { - [self maybeUpdateCachedLocalAddress4Info]; - result = self->cachedLocalAddress4; - } - else - { - [self maybeUpdateCachedLocalAddress6Info]; - result = self->cachedLocalAddress6; - } - - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + if (self->socket4FD != SOCKET_NULL) + { + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalAddress4; + } + else + { + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalAddress6; + } + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (NSString *)localHost { - __block NSString *result = nil; - - dispatch_block_t block = ^{ - - if (self->socket4FD != SOCKET_NULL) - { - [self maybeUpdateCachedLocalAddress4Info]; - result = self->cachedLocalHost4; - } - else - { - [self maybeUpdateCachedLocalAddress6Info]; - result = self->cachedLocalHost6; - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + if (self->socket4FD != SOCKET_NULL) + { + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalHost4; + } + else + { + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalHost6; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (uint16_t)localPort { - __block uint16_t result = 0; - - dispatch_block_t block = ^{ - - if (self->socket4FD != SOCKET_NULL) - { - [self maybeUpdateCachedLocalAddress4Info]; - result = self->cachedLocalPort4; - } - else - { - [self maybeUpdateCachedLocalAddress6Info]; - result = self->cachedLocalPort6; - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + if (self->socket4FD != SOCKET_NULL) + { + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalPort4; + } + else + { + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalPort6; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (NSData *)localAddress_IPv4 { - __block NSData *result = nil; - - dispatch_block_t block = ^{ - - [self maybeUpdateCachedLocalAddress4Info]; - result = self->cachedLocalAddress4; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalAddress4; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (NSString *)localHost_IPv4 { - __block NSString *result = nil; - - dispatch_block_t block = ^{ - - [self maybeUpdateCachedLocalAddress4Info]; - result = self->cachedLocalHost4; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalHost4; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (uint16_t)localPort_IPv4 { - __block uint16_t result = 0; - - dispatch_block_t block = ^{ - - [self maybeUpdateCachedLocalAddress4Info]; - result = self->cachedLocalPort4; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress4Info]; + result = self->cachedLocalPort4; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (NSData *)localAddress_IPv6 { - __block NSData *result = nil; - - dispatch_block_t block = ^{ - - [self maybeUpdateCachedLocalAddress6Info]; - result = self->cachedLocalAddress6; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalAddress6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (NSString *)localHost_IPv6 { - __block NSString *result = nil; - - dispatch_block_t block = ^{ - - [self maybeUpdateCachedLocalAddress6Info]; - result = self->cachedLocalHost6; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalHost6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (uint16_t)localPort_IPv6 { - __block uint16_t result = 0; - - dispatch_block_t block = ^{ - - [self maybeUpdateCachedLocalAddress6Info]; - result = self->cachedLocalPort6; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress6Info]; + result = self->cachedLocalPort6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (void)maybeUpdateCachedConnectedAddressInfo { - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - if (cachedConnectedAddress || (flags & kDidConnect) == 0) - { - return; - } - - NSData *data = nil; - NSString *host = nil; - uint16_t port = 0; - int family = AF_UNSPEC; - - if (socket4FD != SOCKET_NULL) - { - struct sockaddr_in sockaddr4; - socklen_t sockaddr4len = sizeof(sockaddr4); - - if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) - { - data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; - host = [[self class] hostFromSockaddr4:&sockaddr4]; - port = [[self class] portFromSockaddr4:&sockaddr4]; - family = AF_INET; - } - else - { - LogWarn(@"Error in getpeername: %@", [self errnoError]); - } - } - else if (socket6FD != SOCKET_NULL) - { - struct sockaddr_in6 sockaddr6; - socklen_t sockaddr6len = sizeof(sockaddr6); - - if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) - { - data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; - host = [[self class] hostFromSockaddr6:&sockaddr6]; - port = [[self class] portFromSockaddr6:&sockaddr6]; - family = AF_INET6; - } - else - { - LogWarn(@"Error in getpeername: %@", [self errnoError]); - } - } - - - cachedConnectedAddress = data; - cachedConnectedHost = host; - cachedConnectedPort = port; - cachedConnectedFamily = family; + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + if (cachedConnectedAddress || (flags & kDidConnect) == 0) + { + return; + } + + NSData *data = nil; + NSString *host = nil; + uint16_t port = 0; + int family = AF_UNSPEC; + + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socket4FD, + (struct sockaddr *)&sockaddr4, + &sockaddr4len) == 0) + { + data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; + host = [[self class] hostFromSockaddr4:&sockaddr4]; + port = [[self class] portFromSockaddr4:&sockaddr4]; + family = AF_INET; + } + else + { + LogWarn(@"Error in getpeername: %@", [self errnoError]); + } + } + else if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socket6FD, + (struct sockaddr *)&sockaddr6, + &sockaddr6len) == 0) + { + data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; + host = [[self class] hostFromSockaddr6:&sockaddr6]; + port = [[self class] portFromSockaddr6:&sockaddr6]; + family = AF_INET6; + } + else + { + LogWarn(@"Error in getpeername: %@", [self errnoError]); + } + } + + + cachedConnectedAddress = data; + cachedConnectedHost = host; + cachedConnectedPort = port; + cachedConnectedFamily = family; } - (NSData *)connectedAddress { - __block NSData *result = nil; - - dispatch_block_t block = ^{ - - [self maybeUpdateCachedConnectedAddressInfo]; - result = self->cachedConnectedAddress; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedConnectedAddressInfo]; + result = self->cachedConnectedAddress; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (NSString *)connectedHost { - __block NSString *result = nil; - - dispatch_block_t block = ^{ - - [self maybeUpdateCachedConnectedAddressInfo]; - result = self->cachedConnectedHost; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedConnectedAddressInfo]; + result = self->cachedConnectedHost; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (uint16_t)connectedPort { - __block uint16_t result = 0; - - dispatch_block_t block = ^{ - - [self maybeUpdateCachedConnectedAddressInfo]; - result = self->cachedConnectedPort; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, AutoreleasedBlock(block)); - - return result; + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedConnectedAddressInfo]; + result = self->cachedConnectedPort; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; } - (BOOL)isConnected { - __block BOOL result = NO; - - dispatch_block_t block = ^{ - result = (self->flags & kDidConnect) ? YES : NO; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (self->flags & kDidConnect) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (BOOL)isClosed { - __block BOOL result = YES; - - dispatch_block_t block = ^{ - - result = (self->flags & kDidCreateSockets) ? NO : YES; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block BOOL result = YES; + + dispatch_block_t block = ^{ + + result = (self->flags & kDidCreateSockets) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (BOOL)isIPv4 { - __block BOOL result = NO; - - dispatch_block_t block = ^{ - - if (self->flags & kDidCreateSockets) - { - result = (self->socket4FD != SOCKET_NULL); - } - else - { - result = [self isIPv4Enabled]; - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + if (self->flags & kDidCreateSockets) + { + result = (self->socket4FD != SOCKET_NULL); + } + else + { + result = [self isIPv4Enabled]; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } - (BOOL)isIPv6 { - __block BOOL result = NO; - - dispatch_block_t block = ^{ - - if (self->flags & kDidCreateSockets) - { - result = (self->socket6FD != SOCKET_NULL); - } - else - { - result = [self isIPv6Enabled]; - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - return result; + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + if (self->flags & kDidCreateSockets) + { + result = (self->socket6FD != SOCKET_NULL); + } + else + { + result = [self isIPv6Enabled]; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -2776,298 +2865,301 @@ - (BOOL)isIPv6 /** * This method runs through the various checks required prior to a bind attempt. * It is shared between the various bind methods. -**/ + **/ - (BOOL)preBind:(NSError **)errPtr { - if (![self preOp:errPtr]) - { - return NO; - } - - if (flags & kDidBind) - { - if (errPtr) - { - NSString *msg = @"Cannot bind a socket more than once."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - if ((flags & kConnecting) || (flags & kDidConnect)) - { - if (errPtr) - { - NSString *msg = @"Cannot bind after connecting. If needed, bind first, then connect."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; - BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - - if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled - { - if (errPtr) - { - NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - return YES; + if (![self preOp:errPtr]) + { + return NO; + } + + if (flags & kDidBind) + { + if (errPtr) + { + NSString *msg = @"Cannot bind a socket more than once."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if ((flags & kConnecting) || (flags & kDidConnect)) + { + if (errPtr) + { + NSString *msg = @"Cannot bind after connecting. If needed, bind first, then connect."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; } - (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr { - return [self bindToPort:port interface:nil error:errPtr]; + return [self bindToPort:port interface:nil error:errPtr]; } - (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError **)errPtr { - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - // Run through sanity checks - - if (![self preBind:&err]) - { - return_from_block; - } - - // Check the given interface - - NSData *interface4 = nil; - NSData *interface6 = nil; - - [self convertIntefaceDescription:interface port:port intoAddress4:&interface4 address6:&interface6]; - - if ((interface4 == nil) && (interface6 == nil)) - { - NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; - err = [self badParamError:msg]; - - return_from_block; - } - - BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; - BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; - - if (isIPv4Disabled && (interface6 == nil)) - { - NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; - err = [self badParamError:msg]; - - return_from_block; - } - - if (isIPv6Disabled && (interface4 == nil)) - { - NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; - err = [self badParamError:msg]; - - return_from_block; - } - - // Determine protocol(s) - - BOOL useIPv4 = !isIPv4Disabled && (interface4 != nil); - BOOL useIPv6 = !isIPv6Disabled && (interface6 != nil); - - // Create the socket(s) if needed - - if ((self->flags & kDidCreateSockets) == 0) - { - if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) - { - return_from_block; - } - } - - // Bind the socket(s) - - LogVerbose(@"Binding socket to port(%hu) interface(%@)", port, interface); - - if (useIPv4) - { - int status = bind(self->socket4FD, (const struct sockaddr *)[interface4 bytes], (socklen_t)[interface4 length]); - if (status == -1) - { - [self closeSockets]; - - NSString *reason = @"Error in bind() function"; - err = [self errnoErrorWithReason:reason]; - - return_from_block; - } - } - - if (useIPv6) - { - int status = bind(self->socket6FD, (const struct sockaddr *)[interface6 bytes], (socklen_t)[interface6 length]); - if (status == -1) - { - [self closeSockets]; - - NSString *reason = @"Error in bind() function"; - err = [self errnoErrorWithReason:reason]; - - return_from_block; - } - } - - // Update flags - - self->flags |= kDidBind; - - if (!useIPv4) self->flags |= kIPv4Deactivated; - if (!useIPv6) self->flags |= kIPv6Deactivated; - - result = YES; - - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (err) - LogError(@"Error binding to port/interface: %@", err); - - if (errPtr) - *errPtr = err; - - return result; + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks + + if (![self preBind:&err]) + { + return_from_block; + } + + // Check the given interface + + NSData *interface4 = nil; + NSData *interface6 = nil; + + [self convertIntefaceDescription:interface port:port intoAddress4:&interface4 address6:&interface6]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Determine protocol(s) + + BOOL useIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL useIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create the socket(s) if needed + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) + { + return_from_block; + } + } + + // Bind the socket(s) + + LogVerbose(@"Binding socket to port(%hu) interface(%@)", port, interface); + + if (useIPv4) + { + int status = bind(self->socket4FD, + (const struct sockaddr *)[interface4 bytes], + (socklen_t)[interface4 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + + if (useIPv6) + { + int status = bind(self->socket6FD, + (const struct sockaddr *)[interface6 bytes], + (socklen_t)[interface6 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + + // Update flags + + self->flags |= kDidBind; + + if (!useIPv4) self->flags |= kIPv4Deactivated; + if (!useIPv6) self->flags |= kIPv6Deactivated; + + result = YES; + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error binding to port/interface: %@", err); + + if (errPtr) + *errPtr = err; + + return result; } - (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr { - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - // Run through sanity checks - - if (![self preBind:&err]) - { - return_from_block; - } - - // Check the given address - - int addressFamily = [[self class] familyFromAddress:localAddr]; - - if (addressFamily == AF_UNSPEC) - { - NSString *msg = @"A valid IPv4 or IPv6 address was not given"; - err = [self badParamError:msg]; - - return_from_block; - } - - NSData *localAddr4 = (addressFamily == AF_INET) ? localAddr : nil; - NSData *localAddr6 = (addressFamily == AF_INET6) ? localAddr : nil; - - BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; - BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; - - if (isIPv4Disabled && localAddr4) - { - NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; - err = [self badParamError:msg]; - - return_from_block; - } - - if (isIPv6Disabled && localAddr6) - { - NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; - err = [self badParamError:msg]; - - return_from_block; - } - - // Determine protocol(s) - - BOOL useIPv4 = !isIPv4Disabled && (localAddr4 != nil); - BOOL useIPv6 = !isIPv6Disabled && (localAddr6 != nil); - - // Create the socket(s) if needed - - if ((self->flags & kDidCreateSockets) == 0) - { - if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) - { - return_from_block; - } - } - - // Bind the socket(s) - - if (useIPv4) - { - LogVerbose(@"Binding socket to address(%@:%hu)", - [[self class] hostFromAddress:localAddr4], - [[self class] portFromAddress:localAddr4]); - - int status = bind(self->socket4FD, (const struct sockaddr *)[localAddr4 bytes], (socklen_t)[localAddr4 length]); - if (status == -1) - { - [self closeSockets]; - - NSString *reason = @"Error in bind() function"; - err = [self errnoErrorWithReason:reason]; - - return_from_block; - } - } - else - { - LogVerbose(@"Binding socket to address(%@:%hu)", - [[self class] hostFromAddress:localAddr6], - [[self class] portFromAddress:localAddr6]); - - int status = bind(self->socket6FD, (const struct sockaddr *)[localAddr6 bytes], (socklen_t)[localAddr6 length]); - if (status == -1) - { - [self closeSockets]; - - NSString *reason = @"Error in bind() function"; - err = [self errnoErrorWithReason:reason]; - - return_from_block; - } - } - - // Update flags - - self->flags |= kDidBind; - - if (!useIPv4) self->flags |= kIPv4Deactivated; - if (!useIPv6) self->flags |= kIPv6Deactivated; - - result = YES; - - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (err) - LogError(@"Error binding to address: %@", err); - - if (errPtr) - *errPtr = err; - - return result; + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks + + if (![self preBind:&err]) + { + return_from_block; + } + + // Check the given address + + int addressFamily = [[self class] familyFromAddress:localAddr]; + + if (addressFamily == AF_UNSPEC) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + NSData *localAddr4 = (addressFamily == AF_INET) ? localAddr : nil; + NSData *localAddr6 = (addressFamily == AF_INET6) ? localAddr : nil; + + BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && localAddr4) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && localAddr6) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Determine protocol(s) + + BOOL useIPv4 = !isIPv4Disabled && (localAddr4 != nil); + BOOL useIPv6 = !isIPv6Disabled && (localAddr6 != nil); + + // Create the socket(s) if needed + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) + { + return_from_block; + } + } + + // Bind the socket(s) + + if (useIPv4 || useIPv6) + { + NSData *addressData = useIPv4 ? localAddr4 : localAddr6; + int socketFD = useIPv4 ? self->socket4FD : self->socket6FD; + NSString *protocol = useIPv4 ? @"IPv4" : @"IPv6"; + + LogVerbose(@"Binding socket to address(%@:%hu)", + [[self class] hostFromAddress:addressData], + [[self class] portFromAddress:addressData]); + + const struct sockaddr *addr = (const struct sockaddr *)[addressData bytes]; + if (addr == NULL) + { + [self closeSockets]; + + NSString *reason = [NSString stringWithFormat:@"Invalid address data for %@ bind", + protocol]; + err = [self badParamError:reason]; + + return_from_block; + } + + int status = bind(socketFD, addr, (socklen_t)[addressData length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + + // Update flags + + self->flags |= kDidBind; + + if (!useIPv4) self->flags |= kIPv4Deactivated; + if (!useIPv6) self->flags |= kIPv6Deactivated; + + result = YES; + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error binding to address: %@", err); + + if (errPtr) + *errPtr = err; + + return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -3077,291 +3169,300 @@ - (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr /** * This method runs through the various checks required prior to a connect attempt. * It is shared between the various connect methods. -**/ + **/ - (BOOL)preConnect:(NSError **)errPtr { - if (![self preOp:errPtr]) - { - return NO; - } - - if ((flags & kConnecting) || (flags & kDidConnect)) - { - if (errPtr) - { - NSString *msg = @"Cannot connect a socket more than once."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; - BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; - - if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled - { - if (errPtr) - { - NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - return YES; + if (![self preOp:errPtr]) + { + return NO; + } + + if ((flags & kConnecting) || (flags & kDidConnect)) + { + if (errPtr) + { + NSString *msg = @"Cannot connect a socket more than once."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; } - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr { - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - // Run through sanity checks. - - if (![self preConnect:&err]) - { - return_from_block; - } - - // Check parameter(s) - - if (host == nil) - { - NSString *msg = @"The host param is nil. Should be domain name or IP address string."; - err = [self badParamError:msg]; - - return_from_block; - } - - // Create the socket(s) if needed - - if ((self->flags & kDidCreateSockets) == 0) - { - if (![self createSockets:&err]) - { - return_from_block; - } - } - - // Create special connect packet - - GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; - packet->resolveInProgress = YES; - - // Start asynchronous DNS resolve for host:port on background queue - - LogVerbose(@"Dispatching DNS resolve for connect..."); - - [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { - - // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, - // and immediately returns. Once the async resolve task completes, - // this block is executed on our socketQueue. - - packet->resolveInProgress = NO; - - packet->addresses = addresses; - packet->error = error; - - [self maybeConnect]; - }]; - - // Updates flags, add connect packet to send queue, and pump send queue - - self->flags |= kConnecting; - - [self->sendQueue addObject:packet]; - [self maybeDequeueSend]; - - result = YES; - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (err) - LogError(@"Error connecting to host/port: %@", err); - - if (errPtr) - *errPtr = err; - - return result; + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks. + + if (![self preConnect:&err]) + { + return_from_block; + } + + // Check parameter(s) + + if (host == nil) + { + NSString *msg = @"The host param is nil. Should be domain name or IP address string."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create the socket(s) if needed + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + // Create special connect packet + + GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; + packet->resolveInProgress = YES; + + // Start asynchronous DNS resolve for host:port on background queue + + LogVerbose(@"Dispatching DNS resolve for connect..."); + + [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, + NSError *error) { + + // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, + // and immediately returns. Once the async resolve task completes, + // this block is executed on our socketQueue. + + packet->resolveInProgress = NO; + + packet->addresses = addresses; + packet->error = error; + + [self maybeConnect]; + }]; + + // Updates flags, add connect packet to send queue, and pump send queue + + self->flags |= kConnecting; + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error connecting to host/port: %@", err); + + if (errPtr) + *errPtr = err; + + return result; } - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr { - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - // Run through sanity checks. - - if (![self preConnect:&err]) - { - return_from_block; - } - - // Check parameter(s) - - if (remoteAddr == nil) - { - NSString *msg = @"The address param is nil. Should be a valid address."; - err = [self badParamError:msg]; - - return_from_block; - } - - // Create the socket(s) if needed - - if ((self->flags & kDidCreateSockets) == 0) - { - if (![self createSockets:&err]) - { - return_from_block; - } - } - - // The remoteAddr parameter could be of type NSMutableData. - // So we copy it to be safe. - - NSData *address = [remoteAddr copy]; - NSArray *addresses = [NSArray arrayWithObject:address]; - - GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; - packet->addresses = addresses; - - // Updates flags, add connect packet to send queue, and pump send queue - - self->flags |= kConnecting; - - [self->sendQueue addObject:packet]; - [self maybeDequeueSend]; - - result = YES; - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (err) - LogError(@"Error connecting to address: %@", err); - - if (errPtr) - *errPtr = err; - - return result; + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks. + + if (![self preConnect:&err]) + { + return_from_block; + } + + // Check parameter(s) + + if (remoteAddr == nil) + { + NSString *msg = @"The address param is nil. Should be a valid address."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create the socket(s) if needed + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + // The remoteAddr parameter could be of type NSMutableData. + // So we copy it to be safe. + + NSData *address = [remoteAddr copy]; + NSArray *addresses = [NSArray arrayWithObject:address]; + + GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; + packet->addresses = addresses; + + // Updates flags, add connect packet to send queue, and pump send queue + + self->flags |= kConnecting; + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error connecting to address: %@", err); + + if (errPtr) + *errPtr = err; + + return result; } - (void)maybeConnect { - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - - BOOL sendQueueReady = [currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]; - - if (sendQueueReady) - { - GCDAsyncUdpSpecialPacket *connectPacket = (GCDAsyncUdpSpecialPacket *)currentSend; - - if (connectPacket->resolveInProgress) - { - LogVerbose(@"Waiting for DNS resolve..."); - } - else - { - if (connectPacket->error) - { - [self notifyDidNotConnect:connectPacket->error]; - } - else - { - NSData *address = nil; - NSError *error = nil; - - int addressFamily = [self getAddress:&address error:&error fromAddresses:connectPacket->addresses]; - - // Perform connect - - BOOL result = NO; - - switch (addressFamily) - { - case AF_INET : result = [self connectWithAddress4:address error:&error]; break; - case AF_INET6 : result = [self connectWithAddress6:address error:&error]; break; - } - - if (result) - { - flags |= kDidBind; - flags |= kDidConnect; - - cachedConnectedAddress = address; - cachedConnectedHost = [[self class] hostFromAddress:address]; - cachedConnectedPort = [[self class] portFromAddress:address]; - cachedConnectedFamily = addressFamily; - - [self notifyDidConnectToAddress:address]; - } - else - { - [self notifyDidNotConnect:error]; - } - } - - flags &= ~kConnecting; - - [self endCurrentSend]; - [self maybeDequeueSend]; - } - } + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + + BOOL sendQueueReady = [currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]; + + if (sendQueueReady) + { + GCDAsyncUdpSpecialPacket *connectPacket = (GCDAsyncUdpSpecialPacket *)currentSend; + + if (connectPacket->resolveInProgress) + { + LogVerbose(@"Waiting for DNS resolve..."); + } + else + { + if (connectPacket->error) + { + [self notifyDidNotConnect:connectPacket->error]; + } + else + { + NSData *address = nil; + NSError *error = nil; + + int addressFamily = [self getAddress:&address error:&error fromAddresses:connectPacket->addresses]; + + // Perform connect + + BOOL result = NO; + + switch (addressFamily) + { + case AF_INET : result = [self connectWithAddress4:address error:&error]; break; + case AF_INET6 : result = [self connectWithAddress6:address error:&error]; break; + default: break; + } + + if (result) + { + flags |= kDidBind; + flags |= kDidConnect; + + cachedConnectedAddress = address; + cachedConnectedHost = [[self class] hostFromAddress:address]; + cachedConnectedPort = [[self class] portFromAddress:address]; + cachedConnectedFamily = addressFamily; + + [self notifyDidConnectToAddress:address]; + } + else + { + [self notifyDidNotConnect:error]; + } + } + + flags &= ~kConnecting; + + [self endCurrentSend]; + [self maybeDequeueSend]; + } + } } - (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr { - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - int status = connect(socket4FD, (const struct sockaddr *)[address4 bytes], (socklen_t)[address4 length]); - if (status != 0) - { - if (errPtr) - *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; - - return NO; - } - - [self closeSocket6]; - flags |= kIPv6Deactivated; - - return YES; + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + int status = connect(socket4FD, + (const struct sockaddr *)[address4 bytes], + (socklen_t)[address4 length]); + if (status != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; + + return NO; + } + + [self closeSocket6]; + flags |= kIPv6Deactivated; + + return YES; } - (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr { - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - int status = connect(socket6FD, (const struct sockaddr *)[address6 bytes], (socklen_t)[address6 length]); - if (status != 0) - { - if (errPtr) - *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; - - return NO; - } - - [self closeSocket4]; - flags |= kIPv4Deactivated; - - return YES; + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + int status = connect(socket6FD, + (const struct sockaddr *)[address6 bytes], + (socklen_t)[address6 length]); + if (status != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; + + return NO; + } + + [self closeSocket4]; + flags |= kIPv4Deactivated; + + return YES; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -3370,54 +3471,54 @@ - (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr - (BOOL)preJoin:(NSError **)errPtr { - if (![self preOp:errPtr]) - { - return NO; - } - - if (!(flags & kDidBind)) - { - if (errPtr) - { - NSString *msg = @"Must bind a socket before joining a multicast group."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - if ((flags & kConnecting) || (flags & kDidConnect)) - { - if (errPtr) - { - NSString *msg = @"Cannot join a multicast group if connected."; - *errPtr = [self badConfigError:msg]; - } - return NO; - } - - return YES; + if (![self preOp:errPtr]) + { + return NO; + } + + if (!(flags & kDidBind)) + { + if (errPtr) + { + NSString *msg = @"Must bind a socket before joining a multicast group."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if ((flags & kConnecting) || (flags & kDidConnect)) + { + if (errPtr) + { + NSString *msg = @"Cannot join a multicast group if connected."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; } - (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr { - return [self joinMulticastGroup:group onInterface:nil error:errPtr]; + return [self joinMulticastGroup:group onInterface:nil error:errPtr]; } - (BOOL)joinMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr { - // IP_ADD_MEMBERSHIP == IPV6_JOIN_GROUP - return [self performMulticastRequest:IP_ADD_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; + // IP_ADD_MEMBERSHIP == IPV6_JOIN_GROUP + return [self performMulticastRequest:IP_ADD_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; } - (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr { - return [self leaveMulticastGroup:group onInterface:nil error:errPtr]; + return [self leaveMulticastGroup:group onInterface:nil error:errPtr]; } - (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr { - // IP_DROP_MEMBERSHIP == IPV6_LEAVE_GROUP - return [self performMulticastRequest:IP_DROP_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; + // IP_DROP_MEMBERSHIP == IPV6_LEAVE_GROUP + return [self performMulticastRequest:IP_DROP_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; } - (BOOL)performMulticastRequest:(int)requestType @@ -3425,227 +3526,243 @@ - (BOOL)performMulticastRequest:(int)requestType onInterface:(NSString *)interface error:(NSError **)errPtr { - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - // Run through sanity checks - - if (![self preJoin:&err]) - { - return_from_block; - } - - // Convert group to address - - NSData *groupAddr4 = nil; - NSData *groupAddr6 = nil; - - [self convertNumericHost:group port:0 intoAddress4:&groupAddr4 address6:&groupAddr6]; - - if ((groupAddr4 == nil) && (groupAddr6 == nil)) - { - NSString *msg = @"Unknown group. Specify valid group IP address."; - err = [self badParamError:msg]; - - return_from_block; - } - - // Convert interface to address - - NSData *interfaceAddr4 = nil; - NSData *interfaceAddr6 = nil; - - [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; - - if ((interfaceAddr4 == nil) && (interfaceAddr6 == nil)) - { - NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; - err = [self badParamError:msg]; - - return_from_block; - } - - // Perform join - - if ((self->socket4FD != SOCKET_NULL) && groupAddr4 && interfaceAddr4) - { - const struct sockaddr_in *nativeGroup = (const struct sockaddr_in *)[groupAddr4 bytes]; - const struct sockaddr_in *nativeIface = (const struct sockaddr_in *)[interfaceAddr4 bytes]; - - struct ip_mreq imreq; - imreq.imr_multiaddr = nativeGroup->sin_addr; - imreq.imr_interface = nativeIface->sin_addr; - - int status = setsockopt(self->socket4FD, IPPROTO_IP, requestType, (const void *)&imreq, sizeof(imreq)); - if (status != 0) - { - err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - - return_from_block; - } - - // Using IPv4 only - [self closeSocket6]; - - result = YES; - } - else if ((self->socket6FD != SOCKET_NULL) && groupAddr6 && interfaceAddr6) - { - const struct sockaddr_in6 *nativeGroup = (const struct sockaddr_in6 *)[groupAddr6 bytes]; - - struct ipv6_mreq imreq; - imreq.ipv6mr_multiaddr = nativeGroup->sin6_addr; - imreq.ipv6mr_interface = [self indexOfInterfaceAddr6:interfaceAddr6]; - - int status = setsockopt(self->socket6FD, IPPROTO_IPV6, requestType, (const void *)&imreq, sizeof(imreq)); - if (status != 0) - { - err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - - return_from_block; - } - - // Using IPv6 only - [self closeSocket4]; - - result = YES; - } - else - { - NSString *msg = @"Socket, group, and interface do not have matching IP versions"; - err = [self badParamError:msg]; - - return_from_block; - } - - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (errPtr) - *errPtr = err; - - return result; + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks + + if (![self preJoin:&err]) + { + return_from_block; + } + + // Convert group to address + + NSData *groupAddr4 = nil; + NSData *groupAddr6 = nil; + + [self convertNumericHost:group port:0 intoAddress4:&groupAddr4 address6:&groupAddr6]; + + if ((groupAddr4 == nil) && (groupAddr6 == nil)) + { + NSString *msg = @"Unknown group. Specify valid group IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Convert interface to address + + NSData *interfaceAddr4 = nil; + NSData *interfaceAddr6 = nil; + + [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; + + if ((interfaceAddr4 == nil) && (interfaceAddr6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Perform join + + if ((self->socket4FD != SOCKET_NULL) && groupAddr4 && interfaceAddr4) + { + const struct sockaddr_in *nativeGroup = (const struct sockaddr_in *)[groupAddr4 bytes]; + const struct sockaddr_in *nativeIface = (const struct sockaddr_in *)[interfaceAddr4 bytes]; + + struct ip_mreq imreq; + imreq.imr_multiaddr = nativeGroup->sin_addr; + imreq.imr_interface = nativeIface->sin_addr; + + int status = setsockopt(self->socket4FD, + IPPROTO_IP, + requestType, + (const void *)&imreq, + sizeof(imreq)); + if (status != 0) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + + // Using IPv4 only + [self closeSocket6]; + + result = YES; + } + else if ((self->socket6FD != SOCKET_NULL) && groupAddr6 && interfaceAddr6) + { + const struct sockaddr_in6 *nativeGroup = (const struct sockaddr_in6 *)[groupAddr6 bytes]; + + struct ipv6_mreq imreq; + imreq.ipv6mr_multiaddr = nativeGroup->sin6_addr; + imreq.ipv6mr_interface = [self indexOfInterfaceAddr6:interfaceAddr6]; + + int status = setsockopt(self->socket6FD, + IPPROTO_IPV6, + requestType, + (const void *)&imreq, + sizeof(imreq)); + if (status != 0) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + + // Using IPv6 only + [self closeSocket4]; + + result = YES; + } + else + { + NSString *msg = @"Socket, group, and interface do not have matching IP versions"; + err = [self badParamError:msg]; + + return_from_block; + } + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; } - (BOOL)sendIPv4MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr { - __block BOOL result = NO; - __block NSError *err = nil; + __block BOOL result = NO; + __block NSError *err = nil; - dispatch_block_t block = ^{ @autoreleasepool { + dispatch_block_t block = ^{ @autoreleasepool { - if (![self preOp:&err]) - { - return_from_block; - } + if (![self preOp:&err]) + { + return_from_block; + } - if ((self->flags & kDidCreateSockets) == 0) - { - if (![self createSockets:&err]) - { - return_from_block; - } - } - - // Convert interface to address + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } - NSData *interfaceAddr4 = nil; - NSData *interfaceAddr6 = nil; + // Convert interface to address - [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; + NSData *interfaceAddr4 = nil; + NSData *interfaceAddr6 = nil; - if (interfaceAddr4 == nil) - { - NSString *msg = @"Unknown interface. Specify valid interface by IP address."; - err = [self badParamError:msg]; - return_from_block; - } + [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; - if (self->socket4FD != SOCKET_NULL) { - const struct sockaddr_in *nativeIface = (struct sockaddr_in *)[interfaceAddr4 bytes]; - struct in_addr interface_addr = nativeIface->sin_addr; - int status = setsockopt(self->socket4FD, IPPROTO_IP, IP_MULTICAST_IF, &interface_addr, sizeof(interface_addr)); - if (status != 0) { - err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - return_from_block; - result = YES; - } - } - - }}; + if (interfaceAddr4 == nil) + { + NSString *msg = @"Unknown interface. Specify valid interface by IP address."; + err = [self badParamError:msg]; + return_from_block; + } + + if (self->socket4FD != SOCKET_NULL) { + const struct sockaddr_in *nativeIface = (struct sockaddr_in *)[interfaceAddr4 bytes]; + struct in_addr interface_addr = nativeIface->sin_addr; + int status = setsockopt(self->socket4FD, + IPPROTO_IP, + IP_MULTICAST_IF, + &interface_addr, + sizeof(interface_addr)); + if (status != 0) { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + return_from_block; + result = YES; + } + } + + }}; - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); - if (errPtr) - *errPtr = err; + if (errPtr) + *errPtr = err; - return result; + return result; } - (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr { - __block BOOL result = NO; - __block NSError *err = nil; + __block BOOL result = NO; + __block NSError *err = nil; - dispatch_block_t block = ^{ @autoreleasepool { + dispatch_block_t block = ^{ @autoreleasepool { - if (![self preOp:&err]) - { - return_from_block; - } + if (![self preOp:&err]) + { + return_from_block; + } - if ((self->flags & kDidCreateSockets) == 0) - { - if (![self createSockets:&err]) - { - return_from_block; - } - } - - // Convert interface to address + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } - NSData *interfaceAddr4 = nil; - NSData *interfaceAddr6 = nil; + // Convert interface to address - [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; + NSData *interfaceAddr4 = nil; + NSData *interfaceAddr6 = nil; - if (interfaceAddr6 == nil) - { - NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\")."; - err = [self badParamError:msg]; - return_from_block; - } + [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; - if ((self->socket6FD != SOCKET_NULL)) { - uint32_t scope_id = [self indexOfInterfaceAddr6:interfaceAddr6]; - int status = setsockopt(self->socket6FD, IPPROTO_IPV6, IPV6_MULTICAST_IF, &scope_id, sizeof(scope_id)); - if (status != 0) { - err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - return_from_block; - } - result = YES; - } - - }}; + if (interfaceAddr6 == nil) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\")."; + err = [self badParamError:msg]; + return_from_block; + } - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); + if ((self->socket6FD != SOCKET_NULL)) { + uint32_t scope_id = [self indexOfInterfaceAddr6:interfaceAddr6]; + int status = setsockopt(self->socket6FD, + IPPROTO_IPV6, + IPV6_MULTICAST_IF, + &scope_id, + sizeof(scope_id)); + if (status != 0) { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + return_from_block; + } + result = YES; + } - if (errPtr) - *errPtr = err; + }}; - return result; + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -3654,62 +3771,70 @@ - (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errP - (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr { - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - if (![self preOp:&err]) - { - return_from_block; - } - - if ((self->flags & kDidCreateSockets) == 0) - { - if (![self createSockets:&err]) - { - return_from_block; - } - } - - int value = flag ? 1 : 0; - if (self->socket4FD != SOCKET_NULL) - { - int error = setsockopt(self->socket4FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); - - if (error) - { - err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - - return_from_block; - } - result = YES; - } - - if (self->socket6FD != SOCKET_NULL) - { - int error = setsockopt(self->socket6FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); - - if (error) - { - err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - - return_from_block; - } - result = YES; - } - - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (errPtr) - *errPtr = err; - - return result; + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (![self preOp:&err]) + { + return_from_block; + } + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + int value = flag ? 1 : 0; + if (self->socket4FD != SOCKET_NULL) + { + int error = setsockopt(self->socket4FD, + SOL_SOCKET, + SO_REUSEPORT, + (const void *)&value, + sizeof(value)); + + if (error) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + result = YES; + } + + if (self->socket6FD != SOCKET_NULL) + { + int error = setsockopt(self->socket6FD, + SOL_SOCKET, + SO_REUSEPORT, + (const void *)&value, + sizeof(value)); + + if (error) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + result = YES; + } + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -3718,52 +3843,56 @@ - (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr - (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr { - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ @autoreleasepool { - - if (![self preOp:&err]) - { - return_from_block; - } - - if ((self->flags & kDidCreateSockets) == 0) - { - if (![self createSockets:&err]) - { - return_from_block; - } - } - - if (self->socket4FD != SOCKET_NULL) - { - int value = flag ? 1 : 0; - int error = setsockopt(self->socket4FD, SOL_SOCKET, SO_BROADCAST, (const void *)&value, sizeof(value)); - - if (error) - { - err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; - - return_from_block; - } - result = YES; - } - - // IPv6 does not implement broadcast, the ability to send a packet to all hosts on the attached link. - // The same effect can be achieved by sending a packet to the link-local all hosts multicast group. - - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (errPtr) - *errPtr = err; - - return result; + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (![self preOp:&err]) + { + return_from_block; + } + + if ((self->flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + if (self->socket4FD != SOCKET_NULL) + { + int value = flag ? 1 : 0; + int error = setsockopt(self->socket4FD, + SOL_SOCKET, + SO_BROADCAST, + (const void *)&value, + sizeof(value)); + + if (error) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + result = YES; + } + + // IPv6 does not implement broadcast, the ability to send a packet to all hosts on the attached link. + // The same effect can be achieved by sending a packet to the link-local all hosts multicast group. + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -3772,29 +3901,29 @@ - (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr - (void)sendData:(NSData *)data withTag:(long)tag { - [self sendData:data withTimeout:-1.0 tag:tag]; + [self sendData:data withTimeout:-1.0 tag:tag]; } - (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { - LogTrace(); - - if ([data length] == 0) - { - LogWarn(@"Ignoring attempt to send nil/empty data."); - return; - } - - - - GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; - - dispatch_async(socketQueue, ^{ @autoreleasepool { - - [self->sendQueue addObject:packet]; - [self maybeDequeueSend]; - }}); - + LogTrace(); + + if ([data length] == 0) + { + LogWarn(@"Ignoring attempt to send nil/empty data."); + return; + } + + + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + }}); + } - (void)sendData:(NSData *)data @@ -3803,499 +3932,512 @@ - (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { - LogTrace(); - - if ([data length] == 0) - { - LogWarn(@"Ignoring attempt to send nil/empty data."); - return; - } - - GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; - packet->resolveInProgress = YES; - - [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { - - // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, - // and immediately returns. Once the async resolve task completes, - // this block is executed on our socketQueue. - - packet->resolveInProgress = NO; - - packet->resolvedAddresses = addresses; - packet->resolveError = error; - - if (packet == self->currentSend) - { - LogVerbose(@"currentSend - address resolved"); - [self doPreSend]; - } - }]; - - dispatch_async(socketQueue, ^{ @autoreleasepool { - - [self->sendQueue addObject:packet]; - [self maybeDequeueSend]; - - }}); - + LogTrace(); + + if ([data length] == 0) + { + LogWarn(@"Ignoring attempt to send nil/empty data."); + return; + } + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; + packet->resolveInProgress = YES; + + [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, + NSError *error) { + + // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, + // and immediately returns. Once the async resolve task completes, + // this block is executed on our socketQueue. + + packet->resolveInProgress = NO; + + packet->resolvedAddresses = addresses; + packet->resolveError = error; + + if (packet == self->currentSend) + { + LogVerbose(@"currentSend - address resolved"); + [self doPreSend]; + } + }]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + + }}); + } - (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag { - LogTrace(); - - if ([data length] == 0) - { - LogWarn(@"Ignoring attempt to send nil/empty data."); - return; - } - - GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; - packet->addressFamily = [GCDAsyncUdpSocket familyFromAddress:remoteAddr]; - packet->address = remoteAddr; - - dispatch_async(socketQueue, ^{ @autoreleasepool { - - [self->sendQueue addObject:packet]; - [self maybeDequeueSend]; - }}); + LogTrace(); + + if ([data length] == 0) + { + LogWarn(@"Ignoring attempt to send nil/empty data."); + return; + } + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; + packet->addressFamily = [GCDAsyncUdpSocket familyFromAddress:remoteAddr]; + packet->address = remoteAddr; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self->sendQueue addObject:packet]; + [self maybeDequeueSend]; + }}); } - (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue { - [self setSendFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; + [self setSendFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; } - (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue isAsynchronous:(BOOL)isAsynchronous { - GCDAsyncUdpSocketSendFilterBlock newFilterBlock = NULL; - dispatch_queue_t newFilterQueue = NULL; - - if (filterBlock) - { - NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); - - newFilterBlock = [filterBlock copy]; - newFilterQueue = filterQueue; - #if !OS_OBJECT_USE_OBJC - dispatch_retain(newFilterQueue); - #endif - } - - dispatch_block_t block = ^{ - - #if !OS_OBJECT_USE_OBJC - if (self->sendFilterQueue) dispatch_release(self->sendFilterQueue); - #endif - - self->sendFilterBlock = newFilterBlock; - self->sendFilterQueue = newFilterQueue; - self->sendFilterAsync = isAsynchronous; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + GCDAsyncUdpSocketSendFilterBlock newFilterBlock = NULL; + dispatch_queue_t newFilterQueue = NULL; + + if (filterBlock) + { + NSAssert(filterQueue, + @"Must provide a dispatch_queue in which to run the filter block."); + + newFilterBlock = [filterBlock copy]; + newFilterQueue = filterQueue; +#if !OS_OBJECT_USE_OBJC + dispatch_retain(newFilterQueue); +#endif + } + + dispatch_block_t block = ^{ + +#if !OS_OBJECT_USE_OBJC + if (self->sendFilterQueue) dispatch_release(self->sendFilterQueue); +#endif + + self->sendFilterBlock = newFilterBlock; + self->sendFilterQueue = newFilterQueue; + self->sendFilterAsync = isAsynchronous; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (void)maybeDequeueSend { - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - // If we don't have a send operation already in progress - if (currentSend == nil) - { - // Create the sockets if needed - if ((flags & kDidCreateSockets) == 0) - { - NSError *err = nil; - if (![self createSockets:&err]) - { - [self closeWithError:err]; - return; - } - } - - while ([sendQueue count] > 0) - { - // Dequeue the next object in the queue - currentSend = [sendQueue objectAtIndex:0]; - [sendQueue removeObjectAtIndex:0]; - - if ([currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]) - { - [self maybeConnect]; - - return; // The maybeConnect method, if it connects, will invoke this method again - } - else if (currentSend->resolveError) - { - // Notify delegate - [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:currentSend->resolveError]; - - // Clear currentSend - currentSend = nil; - - continue; - } - else - { - // Start preprocessing checks on the send packet - [self doPreSend]; - - break; - } - } - - if ((currentSend == nil) && (flags & kCloseAfterSends)) - { - [self closeWithError:nil]; - } - } + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + // If we don't have a send operation already in progress + if (currentSend == nil) + { + // Create the sockets if needed + if ((flags & kDidCreateSockets) == 0) + { + NSError *err = nil; + if (![self createSockets:&err]) + { + [self closeWithError:err]; + return; + } + } + + while ([sendQueue count] > 0) + { + // Dequeue the next object in the queue + currentSend = [sendQueue objectAtIndex:0]; + [sendQueue removeObjectAtIndex:0]; + + if ([currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]) + { + [self maybeConnect]; + + return; // The maybeConnect method, if it connects, will invoke this method again + } + else if (currentSend->resolveError) + { + // Notify delegate + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:currentSend->resolveError]; + + // Clear currentSend + currentSend = nil; + + continue; + } + else + { + // Start preprocessing checks on the send packet + [self doPreSend]; + + break; + } + } + + if ((currentSend == nil) && (flags & kCloseAfterSends)) + { + [self closeWithError:nil]; + } + } } /** * This method is called after a sendPacket has been dequeued. * It performs various preprocessing checks on the packet, * and queries the sendFilter (if set) to determine if the packet can be sent. - * + * * If the packet passes all checks, it will be passed on to the doSend method. -**/ + **/ - (void)doPreSend { - LogTrace(); - - // - // 1. Check for problems with send packet - // - - BOOL waitingForResolve = NO; - NSError *error = nil; - - if (flags & kDidConnect) - { - // Connected socket - - if (currentSend->resolveInProgress || currentSend->resolvedAddresses || currentSend->resolveError) - { - NSString *msg = @"Cannot specify destination of packet for connected socket"; - error = [self badConfigError:msg]; - } - else - { - currentSend->address = cachedConnectedAddress; - currentSend->addressFamily = cachedConnectedFamily; - } - } - else - { - // Non-Connected socket - - if (currentSend->resolveInProgress) - { - // We're waiting for the packet's destination to be resolved. - waitingForResolve = YES; - } - else if (currentSend->resolveError) - { - error = currentSend->resolveError; - } - else if (currentSend->address == nil) - { - if (currentSend->resolvedAddresses == nil) - { - NSString *msg = @"You must specify destination of packet for a non-connected socket"; - error = [self badConfigError:msg]; - } - else - { - // Pick the proper address to use (out of possibly several resolved addresses) - - NSData *address = nil; - int addressFamily = AF_UNSPEC; - - addressFamily = [self getAddress:&address error:&error fromAddresses:currentSend->resolvedAddresses]; - - currentSend->address = address; - currentSend->addressFamily = addressFamily; - } - } - } - - if (waitingForResolve) - { - // We're waiting for the packet's destination to be resolved. - - LogVerbose(@"currentSend - waiting for address resolve"); - - if (flags & kSock4CanAcceptBytes) { - [self suspendSend4Source]; - } - if (flags & kSock6CanAcceptBytes) { - [self suspendSend6Source]; - } - - return; - } - - if (error) - { - // Unable to send packet due to some error. - // Notify delegate and move on. - - [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:error]; - [self endCurrentSend]; - [self maybeDequeueSend]; - - return; - } - - // - // 2. Query sendFilter (if applicable) - // - - if (sendFilterBlock && sendFilterQueue) - { - // Query sendFilter - - if (sendFilterAsync) - { - // Scenario 1 of 3 - Need to asynchronously query sendFilter - - currentSend->filterInProgress = YES; - GCDAsyncUdpSendPacket *sendPacket = currentSend; - - dispatch_async(sendFilterQueue, ^{ @autoreleasepool { - - BOOL allowed = self->sendFilterBlock(sendPacket->buffer, sendPacket->address, sendPacket->tag); - - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - sendPacket->filterInProgress = NO; - if (sendPacket == self->currentSend) - { - if (allowed) - { - [self doSend]; - } - else - { - LogVerbose(@"currentSend - silently dropped by sendFilter"); - - [self notifyDidSendDataWithTag:self->currentSend->tag]; - [self endCurrentSend]; - [self maybeDequeueSend]; - } - } - }}); - }}); - } - else - { - // Scenario 2 of 3 - Need to synchronously query sendFilter - - __block BOOL allowed = YES; - - dispatch_sync(sendFilterQueue, ^{ @autoreleasepool { - - allowed = self->sendFilterBlock(self->currentSend->buffer, self->currentSend->address, self->currentSend->tag); - }}); - - if (allowed) - { - [self doSend]; - } - else - { - LogVerbose(@"currentSend - silently dropped by sendFilter"); - - [self notifyDidSendDataWithTag:currentSend->tag]; - [self endCurrentSend]; - [self maybeDequeueSend]; - } - } - } - else // if (!sendFilterBlock || !sendFilterQueue) - { - // Scenario 3 of 3 - No sendFilter. Just go straight into sending. - - [self doSend]; - } + LogTrace(); + + // + // 1. Check for problems with send packet + // + + BOOL waitingForResolve = NO; + NSError *error = nil; + + if (flags & kDidConnect) + { + // Connected socket + + if (currentSend->resolveInProgress || currentSend->resolvedAddresses || currentSend->resolveError) + { + NSString *msg = @"Cannot specify destination of packet for connected socket"; + error = [self badConfigError:msg]; + } + else + { + currentSend->address = cachedConnectedAddress; + currentSend->addressFamily = cachedConnectedFamily; + } + } + else + { + // Non-Connected socket + + if (currentSend->resolveInProgress) + { + // We're waiting for the packet's destination to be resolved. + waitingForResolve = YES; + } + else if (currentSend->resolveError) + { + error = currentSend->resolveError; + } + else if (currentSend->address == nil) + { + if (currentSend->resolvedAddresses == nil) + { + NSString *msg = @"You must specify destination of packet for a non-connected socket"; + error = [self badConfigError:msg]; + } + else + { + // Pick the proper address to use (out of possibly several resolved addresses) + + NSData *address = nil; + int addressFamily = AF_UNSPEC; + + addressFamily = [self getAddress:&address error:&error fromAddresses:currentSend->resolvedAddresses]; + + currentSend->address = address; + currentSend->addressFamily = addressFamily; + } + } + } + + if (waitingForResolve) + { + // We're waiting for the packet's destination to be resolved. + + LogVerbose(@"currentSend - waiting for address resolve"); + + if (flags & kSock4CanAcceptBytes) { + [self suspendSend4Source]; + } + if (flags & kSock6CanAcceptBytes) { + [self suspendSend6Source]; + } + + return; + } + + if (error) + { + // Unable to send packet due to some error. + // Notify delegate and move on. + + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:error]; + [self endCurrentSend]; + [self maybeDequeueSend]; + + return; + } + + // + // 2. Query sendFilter (if applicable) + // + + if (sendFilterBlock && sendFilterQueue) + { + // Query sendFilter + + if (sendFilterAsync) + { + // Scenario 1 of 3 - Need to asynchronously query sendFilter + + currentSend->filterInProgress = YES; + GCDAsyncUdpSendPacket *sendPacket = currentSend; + + dispatch_async(sendFilterQueue, + ^{ @autoreleasepool { + + BOOL allowed = self->sendFilterBlock(sendPacket->buffer, + sendPacket->address, + sendPacket->tag); + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + sendPacket->filterInProgress = NO; + if (sendPacket == self->currentSend) + { + if (allowed) + { + [self doSend]; + } + else + { + LogVerbose(@"currentSend - silently dropped by sendFilter"); + + [self notifyDidSendDataWithTag:self->currentSend->tag]; + [self endCurrentSend]; + [self maybeDequeueSend]; + } + } + }}); + }}); + } + else + { + // Scenario 2 of 3 - Need to synchronously query sendFilter + + __block BOOL allowed = YES; + + dispatch_sync(sendFilterQueue, + ^{ @autoreleasepool { + + allowed = self->sendFilterBlock(self->currentSend->buffer, + self->currentSend->address, + self->currentSend->tag); + }}); + + if (allowed) + { + [self doSend]; + } + else + { + LogVerbose(@"currentSend - silently dropped by sendFilter"); + + [self notifyDidSendDataWithTag:currentSend->tag]; + [self endCurrentSend]; + [self maybeDequeueSend]; + } + } + } + else // if (!sendFilterBlock || !sendFilterQueue) + { + // Scenario 3 of 3 - No sendFilter. Just go straight into sending. + + [self doSend]; + } } /** * This method performs the actual sending of data in the currentSend packet. - * It should only be called if the -**/ + * It should only be called if the + **/ - (void)doSend { - LogTrace(); - - NSAssert(currentSend != nil, @"Invalid logic"); - - // Perform the actual send - - ssize_t result = 0; - - if (flags & kDidConnect) - { - // Connected socket - - const void *buffer = [currentSend->buffer bytes]; - size_t length = (size_t)[currentSend->buffer length]; - - if (currentSend->addressFamily == AF_INET) - { - result = send(socket4FD, buffer, length, 0); - LogVerbose(@"send(socket4FD) = %d", result); - } - else - { - result = send(socket6FD, buffer, length, 0); - LogVerbose(@"send(socket6FD) = %d", result); - } - } - else - { - // Non-Connected socket - - const void *buffer = [currentSend->buffer bytes]; - size_t length = (size_t)[currentSend->buffer length]; - - const void *dst = [currentSend->address bytes]; - socklen_t dstSize = (socklen_t)[currentSend->address length]; - - if (currentSend->addressFamily == AF_INET) - { - result = sendto(socket4FD, buffer, length, 0, dst, dstSize); - LogVerbose(@"sendto(socket4FD) = %d", result); - } - else - { - result = sendto(socket6FD, buffer, length, 0, dst, dstSize); - LogVerbose(@"sendto(socket6FD) = %d", result); - } - } - - // If the socket wasn't bound before, it is now - - if ((flags & kDidBind) == 0) - { - flags |= kDidBind; - } - - // Check the results. - // - // From the send() & sendto() manpage: - // - // Upon successful completion, the number of bytes which were sent is returned. - // Otherwise, -1 is returned and the global variable errno is set to indicate the error. - - BOOL waitingForSocket = NO; - NSError *socketError = nil; - - if (result == 0) - { - waitingForSocket = YES; - } - else if (result < 0) - { - if (errno == EAGAIN) - waitingForSocket = YES; - else - socketError = [self errnoErrorWithReason:@"Error in send() function."]; - } - - if (waitingForSocket) - { - // Not enough room in the underlying OS socket send buffer. - // Wait for a notification of available space. - - LogVerbose(@"currentSend - waiting for socket"); - - if (!(flags & kSock4CanAcceptBytes)) { - [self resumeSend4Source]; - } - if (!(flags & kSock6CanAcceptBytes)) { - [self resumeSend6Source]; - } - - if ((sendTimer == NULL) && (currentSend->timeout >= 0.0)) - { - // Unable to send packet right away. - // Start timer to timeout the send operation. - - [self setupSendTimerWithTimeout:currentSend->timeout]; - } - } - else if (socketError) - { - [self closeWithError:socketError]; - } - else // done - { - [self notifyDidSendDataWithTag:currentSend->tag]; - [self endCurrentSend]; - [self maybeDequeueSend]; - } + LogTrace(); + + NSAssert(currentSend != nil, @"Invalid logic"); + + // Perform the actual send + + ssize_t result = 0; + + if (flags & kDidConnect) + { + // Connected socket + + const void *buffer = [currentSend->buffer bytes]; + size_t length = (size_t)[currentSend->buffer length]; + + if (currentSend->addressFamily == AF_INET) + { + result = send(socket4FD, buffer, length, 0); + LogVerbose(@"send(socket4FD) = %d", result); + } + else + { + result = send(socket6FD, buffer, length, 0); + LogVerbose(@"send(socket6FD) = %d", result); + } + } + else + { + // Non-Connected socket + + const void *buffer = [currentSend->buffer bytes]; + size_t length = (size_t)[currentSend->buffer length]; + + const void *dst = [currentSend->address bytes]; + socklen_t dstSize = (socklen_t)[currentSend->address length]; + + if (currentSend->addressFamily == AF_INET) + { + result = sendto(socket4FD, buffer, length, 0, (const struct sockaddr *)dst, dstSize); + LogVerbose(@"sendto(socket4FD) = %d", result); + } + else + { + result = sendto(socket6FD, buffer, length, 0, (const struct sockaddr *)dst, dstSize); + LogVerbose(@"sendto(socket6FD) = %d", result); + } + } + + // If the socket wasn't bound before, it is now + + if ((flags & kDidBind) == 0) + { + flags |= kDidBind; + } + + // Check the results. + // + // From the send() & sendto() manpage: + // + // Upon successful completion, the number of bytes which were sent is returned. + // Otherwise, -1 is returned and the global variable errno is set to indicate the error. + + BOOL waitingForSocket = NO; + NSError *socketError = nil; + + if (result == 0) + { + waitingForSocket = YES; + } + else if (result < 0) + { + if (errno == EAGAIN) + waitingForSocket = YES; + else + socketError = [self errnoErrorWithReason:@"Error in send() function."]; + } + + if (waitingForSocket) + { + // Not enough room in the underlying OS socket send buffer. + // Wait for a notification of available space. + + LogVerbose(@"currentSend - waiting for socket"); + + if (!(flags & kSock4CanAcceptBytes)) { + [self resumeSend4Source]; + } + if (!(flags & kSock6CanAcceptBytes)) { + [self resumeSend6Source]; + } + + if ((sendTimer == NULL) && (currentSend->timeout >= 0.0)) + { + // Unable to send packet right away. + // Start timer to timeout the send operation. + + [self setupSendTimerWithTimeout:currentSend->timeout]; + } + } + else if (socketError) + { + [self closeWithError:socketError]; + } + else // done + { + [self notifyDidSendDataWithTag:currentSend->tag]; + [self endCurrentSend]; + [self maybeDequeueSend]; + } } /** * Releases all resources associated with the currentSend. -**/ + **/ - (void)endCurrentSend { - if (sendTimer) - { - dispatch_source_cancel(sendTimer); - #if !OS_OBJECT_USE_OBJC - dispatch_release(sendTimer); - #endif - sendTimer = NULL; - } - - currentSend = nil; + if (sendTimer) + { + dispatch_source_cancel(sendTimer); +#if !OS_OBJECT_USE_OBJC + dispatch_release(sendTimer); +#endif + sendTimer = NULL; + } + + currentSend = nil; } /** * Performs the operations to timeout the current send operation, and move on. -**/ + **/ - (void)doSendTimeout { - LogTrace(); - - [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:[self sendTimeoutError]]; - [self endCurrentSend]; - [self maybeDequeueSend]; + LogTrace(); + + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:[self sendTimeoutError]]; + [self endCurrentSend]; + [self maybeDequeueSend]; } /** * Sets up a timer that fires to timeout the current send operation. * This method should only be called once per send packet. -**/ + **/ - (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout { - NSAssert(sendTimer == NULL, @"Invalid logic"); - NSAssert(timeout >= 0.0, @"Invalid logic"); - - LogTrace(); - - sendTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); - - dispatch_source_set_event_handler(sendTimer, ^{ @autoreleasepool { - - [self doSendTimeout]; - }}); - - dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); - - dispatch_source_set_timer(sendTimer, tt, DISPATCH_TIME_FOREVER, 0); - dispatch_resume(sendTimer); + NSAssert(sendTimer == NULL, @"Invalid logic"); + NSAssert(timeout >= 0.0, @"Invalid logic"); + + LogTrace(); + + sendTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, + 0, + 0, + socketQueue); + + dispatch_source_set_event_handler(sendTimer, ^{ @autoreleasepool { + + [self doSendTimeout]; + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(sendTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(sendTimer); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -4304,477 +4446,490 @@ - (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout - (BOOL)receiveOnce:(NSError **)errPtr { - LogTrace(); - - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ - - if ((self->flags & kReceiveOnce) == 0) - { - if ((self->flags & kDidCreateSockets) == 0) - { - NSString *msg = @"Must bind socket before you can receive data. " - @"You can do this explicitly via bind, or implicitly via connect or by sending data."; - - err = [self badConfigError:msg]; - return_from_block; - } - - self->flags |= kReceiveOnce; // Enable - self->flags &= ~kReceiveContinuous; // Disable - - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - [self doReceive]; - }}); - } - - result = YES; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (err) - LogError(@"Error in beginReceiving: %@", err); - - if (errPtr) - *errPtr = err; - - return result; + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ + + if ((self->flags & kReceiveOnce) == 0) + { + if ((self->flags & kDidCreateSockets) == 0) + { + NSString *msg = @"Must bind socket before you can receive data. " + @"You can do this explicitly via bind, or implicitly via connect or by sending data."; + + err = [self badConfigError:msg]; + return_from_block; + } + + self->flags |= kReceiveOnce; // Enable + self->flags &= ~kReceiveContinuous; // Disable + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self doReceive]; + }}); + } + + result = YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error in beginReceiving: %@", err); + + if (errPtr) + *errPtr = err; + + return result; } - (BOOL)beginReceiving:(NSError **)errPtr { - LogTrace(); - - __block BOOL result = NO; - __block NSError *err = nil; - - dispatch_block_t block = ^{ - - if ((self->flags & kReceiveContinuous) == 0) - { - if ((self->flags & kDidCreateSockets) == 0) - { - NSString *msg = @"Must bind socket before you can receive data. " - @"You can do this explicitly via bind, or implicitly via connect or by sending data."; - - err = [self badConfigError:msg]; - return_from_block; - } - - self->flags |= kReceiveContinuous; // Enable - self->flags &= ~kReceiveOnce; // Disable - - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - [self doReceive]; - }}); - } - - result = YES; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); - - if (err) - LogError(@"Error in beginReceiving: %@", err); - - if (errPtr) - *errPtr = err; - - return result; + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ + + if ((self->flags & kReceiveContinuous) == 0) + { + if ((self->flags & kDidCreateSockets) == 0) + { + NSString *msg = @"Must bind socket before you can receive data. " + @"You can do this explicitly via bind, or implicitly via connect or by sending data."; + + err = [self badConfigError:msg]; + return_from_block; + } + + self->flags |= kReceiveContinuous; // Enable + self->flags &= ~kReceiveOnce; // Disable + + dispatch_async(self->socketQueue, ^{ @autoreleasepool { + + [self doReceive]; + }}); + } + + result = YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error in beginReceiving: %@", err); + + if (errPtr) + *errPtr = err; + + return result; } - (void)pauseReceiving { - LogTrace(); - - dispatch_block_t block = ^{ - - self->flags &= ~kReceiveOnce; // Disable - self->flags &= ~kReceiveContinuous; // Disable - - if (self->socket4FDBytesAvailable > 0) { - [self suspendReceive4Source]; - } - if (self->socket6FDBytesAvailable > 0) { - [self suspendReceive6Source]; - } - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + LogTrace(); + + dispatch_block_t block = ^{ + + self->flags &= ~kReceiveOnce; // Disable + self->flags &= ~kReceiveContinuous; // Disable + + if (self->socket4FDBytesAvailable > 0) { + [self suspendReceive4Source]; + } + if (self->socket6FDBytesAvailable > 0) { + [self suspendReceive6Source]; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue { - [self setReceiveFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; + [self setReceiveFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; } - (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue isAsynchronous:(BOOL)isAsynchronous { - GCDAsyncUdpSocketReceiveFilterBlock newFilterBlock = NULL; - dispatch_queue_t newFilterQueue = NULL; - - if (filterBlock) - { - NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); - - newFilterBlock = [filterBlock copy]; - newFilterQueue = filterQueue; - #if !OS_OBJECT_USE_OBJC - dispatch_retain(newFilterQueue); - #endif - } - - dispatch_block_t block = ^{ - - #if !OS_OBJECT_USE_OBJC - if (self->receiveFilterQueue) dispatch_release(self->receiveFilterQueue); - #endif - - self->receiveFilterBlock = newFilterBlock; - self->receiveFilterQueue = newFilterQueue; - self->receiveFilterAsync = isAsynchronous; - }; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + GCDAsyncUdpSocketReceiveFilterBlock newFilterBlock = NULL; + dispatch_queue_t newFilterQueue = NULL; + + if (filterBlock) + { + NSAssert(filterQueue, + @"Must provide a dispatch_queue in which to run the filter block."); + + newFilterBlock = [filterBlock copy]; + newFilterQueue = filterQueue; +#if !OS_OBJECT_USE_OBJC + dispatch_retain(newFilterQueue); +#endif + } + + dispatch_block_t block = ^{ + +#if !OS_OBJECT_USE_OBJC + if (self->receiveFilterQueue) dispatch_release(self->receiveFilterQueue); +#endif + + self->receiveFilterBlock = newFilterBlock; + self->receiveFilterQueue = newFilterQueue; + self->receiveFilterAsync = isAsynchronous; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } - (void)doReceive { - LogTrace(); - - if ((flags & (kReceiveOnce | kReceiveContinuous)) == 0) - { - LogVerbose(@"Receiving is paused..."); - - if (socket4FDBytesAvailable > 0) { - [self suspendReceive4Source]; - } - if (socket6FDBytesAvailable > 0) { - [self suspendReceive6Source]; - } - - return; - } - - if ((flags & kReceiveOnce) && (pendingFilterOperations > 0)) - { - LogVerbose(@"Receiving is temporarily paused (pending filter operations)..."); - - if (socket4FDBytesAvailable > 0) { - [self suspendReceive4Source]; - } - if (socket6FDBytesAvailable > 0) { - [self suspendReceive6Source]; - } - - return; - } - - if ((socket4FDBytesAvailable == 0) && (socket6FDBytesAvailable == 0)) - { - LogVerbose(@"No data available to receive..."); - - if (socket4FDBytesAvailable == 0) { - [self resumeReceive4Source]; - } - if (socket6FDBytesAvailable == 0) { - [self resumeReceive6Source]; - } - - return; - } - - // Figure out if we should receive on socket4 or socket6 - - BOOL doReceive4; - - if (flags & kDidConnect) - { - // Connected socket - - doReceive4 = (socket4FD != SOCKET_NULL); - } - else - { - // Non-Connected socket - - if (socket4FDBytesAvailable > 0) - { - if (socket6FDBytesAvailable > 0) - { - // Bytes available on socket4 & socket6 - - doReceive4 = (flags & kFlipFlop) ? YES : NO; - - flags ^= kFlipFlop; // flags = flags xor kFlipFlop; (toggle flip flop bit) - } - else { - // Bytes available on socket4, but not socket6 - doReceive4 = YES; - } - } - else { - // Bytes available on socket6, but not socket4 - doReceive4 = NO; - } - } - - // Perform socket IO - - ssize_t result = 0; - - NSData *data = nil; - NSData *addr4 = nil; - NSData *addr6 = nil; - - if (doReceive4) - { - NSAssert(socket4FDBytesAvailable > 0, @"Invalid logic"); - LogVerbose(@"Receiving on IPv4"); - - struct sockaddr_in sockaddr4; - socklen_t sockaddr4len = sizeof(sockaddr4); - - // #222: GCD does not necessarily return the size of an entire UDP packet - // from dispatch_source_get_data(), so we must use the maximum packet size. - size_t bufSize = max4ReceiveSize; - void *buf = malloc(bufSize); - - result = recvfrom(socket4FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr4, &sockaddr4len); - LogVerbose(@"recvfrom(socket4FD) = %i", (int)result); - - if (result > 0) - { - if ((size_t)result >= socket4FDBytesAvailable) - socket4FDBytesAvailable = 0; - else - socket4FDBytesAvailable -= result; - - if ((size_t)result != bufSize) { - buf = realloc(buf, result); - } - - data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; - addr4 = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; - } - else - { - LogVerbose(@"recvfrom(socket4FD) = %@", [self errnoError]); - socket4FDBytesAvailable = 0; - free(buf); - } - } - else - { - NSAssert(socket6FDBytesAvailable > 0, @"Invalid logic"); - LogVerbose(@"Receiving on IPv6"); - - struct sockaddr_in6 sockaddr6; - socklen_t sockaddr6len = sizeof(sockaddr6); - - // #222: GCD does not necessarily return the size of an entire UDP packet - // from dispatch_source_get_data(), so we must use the maximum packet size. - size_t bufSize = max6ReceiveSize; - void *buf = malloc(bufSize); - - result = recvfrom(socket6FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr6, &sockaddr6len); - LogVerbose(@"recvfrom(socket6FD) -> %i", (int)result); - - if (result > 0) - { - if ((size_t)result >= socket6FDBytesAvailable) - socket6FDBytesAvailable = 0; - else - socket6FDBytesAvailable -= result; - - if ((size_t)result != bufSize) { - buf = realloc(buf, result); - } - - data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; - addr6 = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; - } - else - { - LogVerbose(@"recvfrom(socket6FD) = %@", [self errnoError]); - socket6FDBytesAvailable = 0; - free(buf); - } - } - - - BOOL waitingForSocket = NO; - BOOL notifiedDelegate = NO; - BOOL ignored = NO; - - NSError *socketError = nil; - - if (result == 0) - { - waitingForSocket = YES; - } - else if (result < 0) - { - if (errno == EAGAIN) - waitingForSocket = YES; - else - socketError = [self errnoErrorWithReason:@"Error in recvfrom() function"]; - } - else - { - if (flags & kDidConnect) - { - if (addr4 && ![self isConnectedToAddress4:addr4]) - ignored = YES; - if (addr6 && ![self isConnectedToAddress6:addr6]) - ignored = YES; - } - - NSData *addr = (addr4 != nil) ? addr4 : addr6; - - if (!ignored) - { - if (receiveFilterBlock && receiveFilterQueue) - { - // Run data through filter, and if approved, notify delegate - - __block id filterContext = nil; - __block BOOL allowed = NO; - - if (receiveFilterAsync) - { - pendingFilterOperations++; - dispatch_async(receiveFilterQueue, ^{ @autoreleasepool { - - allowed = self->receiveFilterBlock(data, addr, &filterContext); - - // Transition back to socketQueue to get the current delegate / delegateQueue - dispatch_async(self->socketQueue, ^{ @autoreleasepool { - - self->pendingFilterOperations--; - - if (allowed) - { - [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; - } - else - { - LogVerbose(@"received packet silently dropped by receiveFilter"); - } - - if (self->flags & kReceiveOnce) - { - if (allowed) - { - // The delegate has been notified, - // so our receive once operation has completed. - self->flags &= ~kReceiveOnce; - } - else if (self->pendingFilterOperations == 0) - { - // All pending filter operations have completed, - // and none were allowed through. - // Our receive once operation hasn't completed yet. - [self doReceive]; - } - } - }}); - }}); - } - else // if (!receiveFilterAsync) - { - dispatch_sync(receiveFilterQueue, ^{ @autoreleasepool { - - allowed = self->receiveFilterBlock(data, addr, &filterContext); - }}); - - if (allowed) - { - [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; - notifiedDelegate = YES; - } - else - { - LogVerbose(@"received packet silently dropped by receiveFilter"); - ignored = YES; - } - } - } - else // if (!receiveFilterBlock || !receiveFilterQueue) - { - [self notifyDidReceiveData:data fromAddress:addr withFilterContext:nil]; - notifiedDelegate = YES; - } - } - } - - if (waitingForSocket) - { - // Wait for a notification of available data. - - if (socket4FDBytesAvailable == 0) { - [self resumeReceive4Source]; - } - if (socket6FDBytesAvailable == 0) { - [self resumeReceive6Source]; - } - } - else if (socketError) - { - [self closeWithError:socketError]; - } - else - { - if (flags & kReceiveContinuous) - { - // Continuous receive mode - [self doReceive]; - } - else - { - // One-at-a-time receive mode - if (notifiedDelegate) - { - // The delegate has been notified (no set filter). - // So our receive once operation has completed. - flags &= ~kReceiveOnce; - } - else if (ignored) - { - [self doReceive]; - } - else - { - // Waiting on asynchronous receive filter... - } - } - } + LogTrace(); + + if ((flags & (kReceiveOnce | kReceiveContinuous)) == 0) + { + LogVerbose(@"Receiving is paused..."); + + if (socket4FDBytesAvailable > 0) { + [self suspendReceive4Source]; + } + if (socket6FDBytesAvailable > 0) { + [self suspendReceive6Source]; + } + + return; + } + + if ((flags & kReceiveOnce) && (pendingFilterOperations > 0)) + { + LogVerbose(@"Receiving is temporarily paused (pending filter operations)..."); + + if (socket4FDBytesAvailable > 0) { + [self suspendReceive4Source]; + } + if (socket6FDBytesAvailable > 0) { + [self suspendReceive6Source]; + } + + return; + } + + if ((socket4FDBytesAvailable == 0) && (socket6FDBytesAvailable == 0)) + { + LogVerbose(@"No data available to receive..."); + + if (socket4FDBytesAvailable == 0) { + [self resumeReceive4Source]; + } + if (socket6FDBytesAvailable == 0) { + [self resumeReceive6Source]; + } + + return; + } + + // Figure out if we should receive on socket4 or socket6 + + BOOL doReceive4; + + if (flags & kDidConnect) + { + // Connected socket + + doReceive4 = (socket4FD != SOCKET_NULL); + } + else + { + // Non-Connected socket + + if (socket4FDBytesAvailable > 0) + { + if (socket6FDBytesAvailable > 0) + { + // Bytes available on socket4 & socket6 + + doReceive4 = (flags & kFlipFlop) ? YES : NO; + + flags ^= kFlipFlop; // flags = flags xor kFlipFlop; (toggle flip flop bit) + } + else { + // Bytes available on socket4, but not socket6 + doReceive4 = YES; + } + } + else { + // Bytes available on socket6, but not socket4 + doReceive4 = NO; + } + } + + // Perform socket IO + + ssize_t result = 0; + + NSData *data = nil; + NSData *addr4 = nil; + NSData *addr6 = nil; + + if (doReceive4) + { + NSAssert(socket4FDBytesAvailable > 0, @"Invalid logic"); + LogVerbose(@"Receiving on IPv4"); + + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + // #222: GCD does not necessarily return the size of an entire UDP packet + // from dispatch_source_get_data(), so we must use the maximum packet size. + size_t bufSize = max4ReceiveSize; + void *buf = malloc(bufSize); + + result = recvfrom(socket4FD, + buf, + bufSize, + 0, + (struct sockaddr *)&sockaddr4, + &sockaddr4len); + LogVerbose(@"recvfrom(socket4FD) = %i", (int)result); + + if (result > 0) + { + if ((size_t)result >= socket4FDBytesAvailable) + socket4FDBytesAvailable = 0; + else + socket4FDBytesAvailable -= result; + + if ((size_t)result != bufSize) { + buf = realloc(buf, result); + } + + data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; + addr4 = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; + } + else + { + LogVerbose(@"recvfrom(socket4FD) = %@", [self errnoError]); + socket4FDBytesAvailable = 0; + free(buf); + } + } + else + { + NSAssert(socket6FDBytesAvailable > 0, @"Invalid logic"); + LogVerbose(@"Receiving on IPv6"); + + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + // #222: GCD does not necessarily return the size of an entire UDP packet + // from dispatch_source_get_data(), so we must use the maximum packet size. + size_t bufSize = max6ReceiveSize; + void *buf = malloc(bufSize); + + result = recvfrom(socket6FD, + buf, + bufSize, + 0, + (struct sockaddr *)&sockaddr6, + &sockaddr6len); + LogVerbose(@"recvfrom(socket6FD) -> %i", (int)result); + + if (result > 0) + { + if ((size_t)result >= socket6FDBytesAvailable) + socket6FDBytesAvailable = 0; + else + socket6FDBytesAvailable -= result; + + if ((size_t)result != bufSize) { + buf = realloc(buf, result); + } + + data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; + addr6 = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; + } + else + { + LogVerbose(@"recvfrom(socket6FD) = %@", [self errnoError]); + socket6FDBytesAvailable = 0; + free(buf); + } + } + + + BOOL waitingForSocket = NO; + BOOL notifiedDelegate = NO; + BOOL ignored = NO; + + NSError *socketError = nil; + + if (result == 0) + { + waitingForSocket = YES; + } + else if (result < 0) + { + if (errno == EAGAIN) + waitingForSocket = YES; + else + socketError = [self errnoErrorWithReason:@"Error in recvfrom() function"]; + } + else + { + if (flags & kDidConnect) + { + if (addr4 && ![self isConnectedToAddress4:addr4]) + ignored = YES; + if (addr6 && ![self isConnectedToAddress6:addr6]) + ignored = YES; + } + + NSData *addr = (addr4 != nil) ? addr4 : addr6; + + if (!ignored) + { + if (receiveFilterBlock && receiveFilterQueue) + { + // Run data through filter, and if approved, notify delegate + + __block id filterContext = nil; + __block BOOL allowed = NO; + + if (receiveFilterAsync) + { + pendingFilterOperations++; + dispatch_async(receiveFilterQueue, + ^{ @autoreleasepool { + + allowed = self->receiveFilterBlock(data, addr, &filterContext); + + // Transition back to socketQueue to get the current delegate / delegateQueue + dispatch_async(self->socketQueue, + ^{ @autoreleasepool { + + self->pendingFilterOperations--; + + if (allowed) + { + [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; + } + else + { + LogVerbose(@"received packet silently dropped by receiveFilter"); + } + + if (self->flags & kReceiveOnce) + { + if (allowed) + { + // The delegate has been notified, + // so our receive once operation has completed. + self->flags &= ~kReceiveOnce; + } + else if (self->pendingFilterOperations == 0) + { + // All pending filter operations have completed, + // and none were allowed through. + // Our receive once operation hasn't completed yet. + [self doReceive]; + } + } + }}); + }}); + } + else // if (!receiveFilterAsync) + { + dispatch_sync(receiveFilterQueue, ^{ @autoreleasepool { + + allowed = self->receiveFilterBlock(data, addr, &filterContext); + }}); + + if (allowed) + { + [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; + notifiedDelegate = YES; + } + else + { + LogVerbose(@"received packet silently dropped by receiveFilter"); + ignored = YES; + } + } + } + else // if (!receiveFilterBlock || !receiveFilterQueue) + { + [self notifyDidReceiveData:data fromAddress:addr withFilterContext:nil]; + notifiedDelegate = YES; + } + } + } + + if (waitingForSocket) + { + // Wait for a notification of available data. + + if (socket4FDBytesAvailable == 0) { + [self resumeReceive4Source]; + } + if (socket6FDBytesAvailable == 0) { + [self resumeReceive6Source]; + } + } + else if (socketError) + { + [self closeWithError:socketError]; + } + else + { + if (flags & kReceiveContinuous) + { + // Continuous receive mode + [self doReceive]; + } + else + { + // One-at-a-time receive mode + if (notifiedDelegate) + { + // The delegate has been notified (no set filter). + // So our receive once operation has completed. + flags &= ~kReceiveOnce; + } + else if (ignored) + { + [self doReceive]; + } + else + { + // Waiting on asynchronous receive filter... + } + } + } } - (void)doReceiveEOF { - LogTrace(); - - [self closeWithError:[self socketClosedError]]; + LogTrace(); + + [self closeWithError:[self socketClosedError]]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -4783,66 +4938,67 @@ - (void)doReceiveEOF - (void)closeWithError:(NSError *)error { - LogVerbose(@"closeWithError: %@", error); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - if (currentSend) [self endCurrentSend]; - - [sendQueue removeAllObjects]; - - // If a socket has been created, we should notify the delegate. - BOOL shouldCallDelegate = (flags & kDidCreateSockets) ? YES : NO; - - // Close all sockets, send/receive sources, cfstreams, etc + LogVerbose(@"closeWithError: %@", error); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + if (currentSend) [self endCurrentSend]; + + [sendQueue removeAllObjects]; + + // If a socket has been created, we should notify the delegate. + BOOL shouldCallDelegate = (flags & kDidCreateSockets) ? YES : NO; + + // Close all sockets, send/receive sources, cfstreams, etc #if TARGET_OS_IPHONE - [self removeStreamsFromRunLoop]; - [self closeReadAndWriteStreams]; + [self removeStreamsFromRunLoop]; + [self closeReadAndWriteStreams]; #endif - [self closeSockets]; - - // Clear all flags (config remains as is) - flags = 0; - - if (shouldCallDelegate) - { - [self notifyDidCloseWithError:error]; - } + [self closeSockets]; + + // Clear all flags (config remains as is) + flags = 0; + + if (shouldCallDelegate) + { + [self notifyDidCloseWithError:error]; + } } - (void)close { - LogTrace(); - - dispatch_block_t block = ^{ @autoreleasepool { - - [self closeWithError:nil]; - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); + LogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + [self closeWithError:nil]; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); } - (void)closeAfterSending { - LogTrace(); - - dispatch_block_t block = ^{ @autoreleasepool { - - self->flags |= kCloseAfterSends; - - if (self->currentSend == nil && [self->sendQueue count] == 0) - { - [self closeWithError:nil]; - } - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + LogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + self->flags |= kCloseAfterSends; + + if (self->currentSend == nil && [self->sendQueue count] == 0) + { + [self closeWithError:nil]; + } + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -4858,459 +5014,517 @@ + (void)ignore:(id)_ + (void)startListenerThreadIfNeeded { - static dispatch_once_t predicate; - dispatch_once(&predicate, ^{ - - listenerThread = [[NSThread alloc] initWithTarget:self - selector:@selector(listenerThread:) - object:nil]; - [listenerThread start]; - }); + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + listenerThread = [[NSThread alloc] initWithTarget:self + selector:@selector(listenerThread:) + object:nil]; + [listenerThread start]; + }); } + (void)listenerThread:(id)unused { - @autoreleasepool { - - [[NSThread currentThread] setName:GCDAsyncUdpSocketThreadName]; - - LogInfo(@"ListenerThread: Started"); - - // We can't run the run loop unless it has an associated input source or a timer. - // So we'll just create a timer that will never fire - unless the server runs for a decades. - [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] - target:self - selector:@selector(ignore:) - userInfo:nil - repeats:YES]; - - [[NSRunLoop currentRunLoop] run]; - - LogInfo(@"ListenerThread: Stopped"); - } + @autoreleasepool { + + [[NSThread currentThread] setName:GCDAsyncUdpSocketThreadName]; + + LogInfo(@"ListenerThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for a decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + [[NSRunLoop currentRunLoop] run]; + + LogInfo(@"ListenerThread: Stopped"); + } } + (void)addStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket { - LogTrace(); - NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); - - CFRunLoopRef runLoop = CFRunLoopGetCurrent(); - - if (asyncUdpSocket->readStream4) - CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); - - if (asyncUdpSocket->readStream6) - CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); - - if (asyncUdpSocket->writeStream4) - CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); - - if (asyncUdpSocket->writeStream6) - CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); + LogTrace(); + NSAssert([NSThread currentThread] == listenerThread, + @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncUdpSocket->readStream4) + CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream4, + runLoop, + kCFRunLoopDefaultMode); + + if (asyncUdpSocket->readStream6) + CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream6, + runLoop, + kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream4) + CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream4, + runLoop, + kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream6) + CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream6, + runLoop, + kCFRunLoopDefaultMode); } + (void)removeStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket { - LogTrace(); - NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); - - CFRunLoopRef runLoop = CFRunLoopGetCurrent(); - - if (asyncUdpSocket->readStream4) - CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); - - if (asyncUdpSocket->readStream6) - CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); - - if (asyncUdpSocket->writeStream4) - CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); - - if (asyncUdpSocket->writeStream6) - CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); -} - -static void CFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pInfo) -{ - @autoreleasepool { - GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; + LogTrace(); + NSAssert([NSThread currentThread] == listenerThread, + @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncUdpSocket->readStream4) + CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream4, + runLoop, + kCFRunLoopDefaultMode); + + if (asyncUdpSocket->readStream6) + CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream6, + runLoop, + kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream4) + CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream4, + runLoop, + kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream6) + CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream6, + runLoop, + kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback(CFReadStreamRef stream, + CFStreamEventType type, + void *pInfo) +{ + @autoreleasepool { + GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wswitch-enum" - switch(type) - { - case kCFStreamEventOpenCompleted: - { - LogCVerbose(@"CFReadStreamCallback - Open"); - break; - } - case kCFStreamEventHasBytesAvailable: - { - LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); - break; - } - case kCFStreamEventErrorOccurred: - case kCFStreamEventEndEncountered: - { - NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); - if (error == nil && type == kCFStreamEventEndEncountered) - { - error = [asyncUdpSocket socketClosedError]; - } - - dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { - - LogCVerbose(@"CFReadStreamCallback - %@", - (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); - - if (stream != asyncUdpSocket->readStream4 && - stream != asyncUdpSocket->readStream6 ) - { - LogCVerbose(@"CFReadStreamCallback - Ignored"); - return_from_block; - } - - [asyncUdpSocket closeWithError:error]; - - }}); - - break; - } - default: - { - LogCError(@"CFReadStreamCallback - UnknownType: %i", (int)type); - } - } + switch(type) + { + case kCFStreamEventOpenCompleted: + { + LogCVerbose(@"CFReadStreamCallback - Open"); + break; + } + case kCFStreamEventHasBytesAvailable: + { + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + break; + } + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncUdpSocket socketClosedError]; + } + + dispatch_async(asyncUdpSocket->socketQueue, + ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - %@", + (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); + + if (stream != asyncUdpSocket->readStream4 && + stream != asyncUdpSocket->readStream6 ) + { + LogCVerbose(@"CFReadStreamCallback - Ignored"); + return_from_block; + } + + [asyncUdpSocket closeWithError:error]; + + }}); + + break; + } + default: + { + LogCError(@"CFReadStreamCallback - UnknownType: %i", (int)type); + } + } #pragma clang diagnostic pop - } + } } -static void CFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +static void CFWriteStreamCallback(CFWriteStreamRef stream, + CFStreamEventType type, + void *pInfo) { - @autoreleasepool { - GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; + @autoreleasepool { + GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wswitch-enum" - switch(type) - { - case kCFStreamEventOpenCompleted: - { - LogCVerbose(@"CFWriteStreamCallback - Open"); - break; - } - case kCFStreamEventCanAcceptBytes: - { - LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); - break; - } - case kCFStreamEventErrorOccurred: - case kCFStreamEventEndEncountered: - { - NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); - if (error == nil && type == kCFStreamEventEndEncountered) - { - error = [asyncUdpSocket socketClosedError]; - } - - dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { - - LogCVerbose(@"CFWriteStreamCallback - %@", - (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); - - if (stream != asyncUdpSocket->writeStream4 && - stream != asyncUdpSocket->writeStream6 ) - { - LogCVerbose(@"CFWriteStreamCallback - Ignored"); - return_from_block; - } - - [asyncUdpSocket closeWithError:error]; - - }}); - - break; - } - default: - { - LogCError(@"CFWriteStreamCallback - UnknownType: %i", (int)type); - } - } + switch(type) + { + case kCFStreamEventOpenCompleted: + { + LogCVerbose(@"CFWriteStreamCallback - Open"); + break; + } + case kCFStreamEventCanAcceptBytes: + { + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + break; + } + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncUdpSocket socketClosedError]; + } + + dispatch_async(asyncUdpSocket->socketQueue, + ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - %@", + (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); + + if (stream != asyncUdpSocket->writeStream4 && + stream != asyncUdpSocket->writeStream6 ) + { + LogCVerbose(@"CFWriteStreamCallback - Ignored"); + return_from_block; + } + + [asyncUdpSocket closeWithError:error]; + + }}); + + break; + } + default: + { + LogCError(@"CFWriteStreamCallback - UnknownType: %i", (int)type); + } + } #pragma clang diagnostic pop - } + } } - (BOOL)createReadAndWriteStreams:(NSError **)errPtr { - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - NSError *err = nil; - - if (readStream4 || writeStream4 || readStream6 || writeStream6) - { - // Streams already created - return YES; - } - - if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) - { - err = [self otherError:@"Cannot create streams without a file descriptor"]; - goto Failed; - } - - // Create streams - - LogVerbose(@"Creating read and write stream(s)..."); - - if (socket4FD != SOCKET_NULL) - { - CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket4FD, &readStream4, &writeStream4); - if (!readStream4 || !writeStream4) - { - err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv4]"]; - goto Failed; - } - } - - if (socket6FD != SOCKET_NULL) - { - CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket6FD, &readStream6, &writeStream6); - if (!readStream6 || !writeStream6) - { - err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv6]"]; - goto Failed; - } - } - - // Ensure the CFStream's don't close our underlying socket - - CFReadStreamSetProperty(readStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); - CFWriteStreamSetProperty(writeStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); - - CFReadStreamSetProperty(readStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); - CFWriteStreamSetProperty(writeStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); - - return YES; - + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + NSError *err = nil; + + if (readStream4 || writeStream4 || readStream6 || writeStream6) + { + // Streams already created + return YES; + } + + if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) + { + err = [self otherError:@"Cannot create streams without a file descriptor"]; + goto Failed; + } + + // Create streams + + LogVerbose(@"Creating read and write stream(s)..."); + + if (socket4FD != SOCKET_NULL) + { + CFStreamCreatePairWithSocket(NULL, + (CFSocketNativeHandle)socket4FD, + &readStream4, + &writeStream4); + if (!readStream4 || !writeStream4) + { + err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv4]"]; + goto Failed; + } + } + + if (socket6FD != SOCKET_NULL) + { + CFStreamCreatePairWithSocket(NULL, + (CFSocketNativeHandle)socket6FD, + &readStream6, + &writeStream6); + if (!readStream6 || !writeStream6) + { + err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv6]"]; + goto Failed; + } + } + + // Ensure the CFStream's don't close our underlying socket + + CFReadStreamSetProperty(readStream4, + kCFStreamPropertyShouldCloseNativeSocket, + kCFBooleanFalse); + CFWriteStreamSetProperty(writeStream4, + kCFStreamPropertyShouldCloseNativeSocket, + kCFBooleanFalse); + + CFReadStreamSetProperty(readStream6, + kCFStreamPropertyShouldCloseNativeSocket, + kCFBooleanFalse); + CFWriteStreamSetProperty(writeStream6, + kCFStreamPropertyShouldCloseNativeSocket, + kCFBooleanFalse); + + return YES; + Failed: - if (readStream4) - { - CFReadStreamClose(readStream4); - CFRelease(readStream4); - readStream4 = NULL; - } - if (writeStream4) - { - CFWriteStreamClose(writeStream4); - CFRelease(writeStream4); - writeStream4 = NULL; - } - if (readStream6) - { - CFReadStreamClose(readStream6); - CFRelease(readStream6); - readStream6 = NULL; - } - if (writeStream6) - { - CFWriteStreamClose(writeStream6); - CFRelease(writeStream6); - writeStream6 = NULL; - } - - if (errPtr) - *errPtr = err; - - return NO; + if (readStream4) + { + CFReadStreamClose(readStream4); + CFRelease(readStream4); + readStream4 = NULL; + } + if (writeStream4) + { + CFWriteStreamClose(writeStream4); + CFRelease(writeStream4); + writeStream4 = NULL; + } + if (readStream6) + { + CFReadStreamClose(readStream6); + CFRelease(readStream6); + readStream6 = NULL; + } + if (writeStream6) + { + CFWriteStreamClose(writeStream6); + CFRelease(writeStream6); + writeStream6 = NULL; + } + + if (errPtr) + *errPtr = err; + + return NO; } - (BOOL)registerForStreamCallbacks:(NSError **)errPtr { - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); - - NSError *err = nil; - - streamContext.version = 0; - streamContext.info = (__bridge void *)self; - streamContext.retain = nil; - streamContext.release = nil; - streamContext.copyDescription = nil; - - CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; - CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; - -// readStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventHasBytesAvailable); -// writeStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventCanAcceptBytes); - - if (socket4FD != SOCKET_NULL) - { - if (readStream4 == NULL || writeStream4 == NULL) - { - err = [self otherError:@"Read/Write stream4 is null"]; - goto Failed; - } - - BOOL r1 = CFReadStreamSetClient(readStream4, readStreamEvents, &CFReadStreamCallback, &streamContext); - BOOL r2 = CFWriteStreamSetClient(writeStream4, writeStreamEvents, &CFWriteStreamCallback, &streamContext); - - if (!r1 || !r2) - { - err = [self otherError:@"Error in CFStreamSetClient(), [IPv4]"]; - goto Failed; - } - } - - if (socket6FD != SOCKET_NULL) - { - if (readStream6 == NULL || writeStream6 == NULL) - { - err = [self otherError:@"Read/Write stream6 is null"]; - goto Failed; - } - - BOOL r1 = CFReadStreamSetClient(readStream6, readStreamEvents, &CFReadStreamCallback, &streamContext); - BOOL r2 = CFWriteStreamSetClient(writeStream6, writeStreamEvents, &CFWriteStreamCallback, &streamContext); - - if (!r1 || !r2) - { - err = [self otherError:@"Error in CFStreamSetClient() [IPv6]"]; - goto Failed; - } - } - - return YES; - + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, + @"Read/Write streams are null"); + + NSError *err = nil; + + streamContext.version = 0; + streamContext.info = (__bridge void *)self; + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + + // readStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventHasBytesAvailable); + // writeStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventCanAcceptBytes); + + if (socket4FD != SOCKET_NULL) + { + if (readStream4 == NULL || writeStream4 == NULL) + { + err = [self otherError:@"Read/Write stream4 is null"]; + goto Failed; + } + + BOOL r1 = CFReadStreamSetClient(readStream4, + readStreamEvents, + &CFReadStreamCallback, + &streamContext); + BOOL r2 = CFWriteStreamSetClient(writeStream4, + writeStreamEvents, + &CFWriteStreamCallback, + &streamContext); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamSetClient(), [IPv4]"]; + goto Failed; + } + } + + if (socket6FD != SOCKET_NULL) + { + if (readStream6 == NULL || writeStream6 == NULL) + { + err = [self otherError:@"Read/Write stream6 is null"]; + goto Failed; + } + + BOOL r1 = CFReadStreamSetClient(readStream6, + readStreamEvents, + &CFReadStreamCallback, + &streamContext); + BOOL r2 = CFWriteStreamSetClient(writeStream6, + writeStreamEvents, + &CFWriteStreamCallback, + &streamContext); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamSetClient() [IPv6]"]; + goto Failed; + } + } + + return YES; + Failed: - if (readStream4) { - CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); - } - if (writeStream4) { - CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); - } - if (readStream6) { - CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); - } - if (writeStream6) { - CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); - } - - if (errPtr) *errPtr = err; - return NO; + if (readStream4) { + CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); + } + if (writeStream4) { + CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); + } + if (readStream6) { + CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); + } + if (writeStream6) { + CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); + } + + if (errPtr) *errPtr = err; + return NO; } - (BOOL)addStreamsToRunLoop:(NSError **)errPtr { - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); - - if (!(flags & kAddedStreamListener)) - { - [[self class] startListenerThreadIfNeeded]; - [[self class] performSelector:@selector(addStreamListener:) - onThread:listenerThread - withObject:self - waitUntilDone:YES]; - - flags |= kAddedStreamListener; - } - - return YES; + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, + @"Read/Write streams are null"); + + if (!(flags & kAddedStreamListener)) + { + [[self class] startListenerThreadIfNeeded]; + [[self class] performSelector:@selector(addStreamListener:) + onThread:listenerThread + withObject:self + waitUntilDone:YES]; + + flags |= kAddedStreamListener; + } + + return YES; } - (BOOL)openStreams:(NSError **)errPtr { - LogTrace(); - - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); - - NSError *err = nil; - - if (socket4FD != SOCKET_NULL) - { - BOOL r1 = CFReadStreamOpen(readStream4); - BOOL r2 = CFWriteStreamOpen(writeStream4); - - if (!r1 || !r2) - { - err = [self otherError:@"Error in CFStreamOpen() [IPv4]"]; - goto Failed; - } - } - - if (socket6FD != SOCKET_NULL) - { - BOOL r1 = CFReadStreamOpen(readStream6); - BOOL r2 = CFWriteStreamOpen(writeStream6); - - if (!r1 || !r2) - { - err = [self otherError:@"Error in CFStreamOpen() [IPv6]"]; - goto Failed; - } - } - - return YES; - + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, + @"Read/Write streams are null"); + + NSError *err = nil; + + if (socket4FD != SOCKET_NULL) + { + BOOL r1 = CFReadStreamOpen(readStream4); + BOOL r2 = CFWriteStreamOpen(writeStream4); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamOpen() [IPv4]"]; + goto Failed; + } + } + + if (socket6FD != SOCKET_NULL) + { + BOOL r1 = CFReadStreamOpen(readStream6); + BOOL r2 = CFWriteStreamOpen(writeStream6); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamOpen() [IPv6]"]; + goto Failed; + } + } + + return YES; + Failed: - if (errPtr) *errPtr = err; - return NO; + if (errPtr) *errPtr = err; + return NO; } - (void)removeStreamsFromRunLoop { - LogTrace(); - NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); - - if (flags & kAddedStreamListener) - { - [[self class] performSelector:@selector(removeStreamListener:) - onThread:listenerThread - withObject:self - waitUntilDone:YES]; - - flags &= ~kAddedStreamListener; - } + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), + @"Must be dispatched on socketQueue"); + + if (flags & kAddedStreamListener) + { + [[self class] performSelector:@selector(removeStreamListener:) + onThread:listenerThread + withObject:self + waitUntilDone:YES]; + + flags &= ~kAddedStreamListener; + } } - (void)closeReadAndWriteStreams { - LogTrace(); - - if (readStream4) - { - CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); - CFReadStreamClose(readStream4); - CFRelease(readStream4); - readStream4 = NULL; - } - if (writeStream4) - { - CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); - CFWriteStreamClose(writeStream4); - CFRelease(writeStream4); - writeStream4 = NULL; - } - if (readStream6) - { - CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); - CFReadStreamClose(readStream6); - CFRelease(readStream6); - readStream6 = NULL; - } - if (writeStream6) - { - CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); - CFWriteStreamClose(writeStream6); - CFRelease(writeStream6); - writeStream6 = NULL; - } + LogTrace(); + + if (readStream4) + { + CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream4); + CFRelease(readStream4); + readStream4 = NULL; + } + if (writeStream4) + { + CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream4); + CFRelease(writeStream4); + writeStream4 = NULL; + } + if (readStream6) + { + CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream6); + CFRelease(readStream6); + readStream6 = NULL; + } + if (writeStream6) + { + CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream6); + CFRelease(writeStream6); + writeStream6 = NULL; + } } #endif @@ -5318,21 +5532,21 @@ - (void)closeReadAndWriteStreams #if TARGET_OS_IPHONE - (void)applicationWillEnterForeground:(NSNotification *)notification { - LogTrace(); - - // If the application was backgrounded, then iOS may have shut down our sockets. - // So we take a quick look to see if any of them received an EOF. - - dispatch_block_t block = ^{ @autoreleasepool { - - [self resumeReceive4Source]; - [self resumeReceive6Source]; - }}; - - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_async(socketQueue, block); + LogTrace(); + + // If the application was backgrounded, then iOS may have shut down our sockets. + // So we take a quick look to see if any of them received an EOF. + + dispatch_block_t block = ^{ @autoreleasepool { + + [self resumeReceive4Source]; + [self resumeReceive6Source]; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); } #endif @@ -5345,8 +5559,11 @@ - (void)applicationWillEnterForeground:(NSNotification *)notification **/ - (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue { - void *nonNullUnusedPointer = (__bridge void *)self; - dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, + IsOnSocketQueueOrTargetQueueKey, + nonNullUnusedPointer, + NULL); } /** @@ -5354,161 +5571,164 @@ - (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue **/ - (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue { - dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); + dispatch_queue_set_specific(socketOldTargetQueue, + IsOnSocketQueueOrTargetQueueKey, + NULL, + NULL); } - (void)performBlock:(dispatch_block_t)block { - if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - block(); - else - dispatch_sync(socketQueue, block); + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); } - (int)socketFD { - if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", - THIS_FILE, THIS_METHOD); - return SOCKET_NULL; - } - - if (socket4FD != SOCKET_NULL) - return socket4FD; - else - return socket6FD; + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; } - (int)socket4FD { - if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", - THIS_FILE, THIS_METHOD); - return SOCKET_NULL; - } - - return socket4FD; + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; } - (int)socket6FD { - if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", - THIS_FILE, THIS_METHOD); - return SOCKET_NULL; - } - - return socket6FD; + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; } #if TARGET_OS_IPHONE - (CFReadStreamRef)readStream { - if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", - THIS_FILE, THIS_METHOD); - return NULL; - } - - NSError *err = nil; - if (![self createReadAndWriteStreams:&err]) - { - LogError(@"Error creating CFStream(s): %@", err); - return NULL; - } - - // Todo... - - if (readStream4) - return readStream4; - else - return readStream6; + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return NULL; + } + + NSError *err = nil; + if (![self createReadAndWriteStreams:&err]) + { + LogError(@"Error creating CFStream(s): %@", err); + return NULL; + } + + // Todo... + + if (readStream4) + return readStream4; + else + return readStream6; } - (CFWriteStreamRef)writeStream { - if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", - THIS_FILE, THIS_METHOD); - return NULL; - } - - NSError *err = nil; - if (![self createReadAndWriteStreams:&err]) - { - LogError(@"Error creating CFStream(s): %@", err); - return NULL; - } - - if (writeStream4) - return writeStream4; - else - return writeStream6; + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return NULL; + } + + NSError *err = nil; + if (![self createReadAndWriteStreams:&err]) + { + LogError(@"Error creating CFStream(s): %@", err); + return NULL; + } + + if (writeStream4) + return writeStream4; + else + return writeStream6; } - (BOOL)enableBackgroundingOnSockets { - if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) - { - LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", - THIS_FILE, THIS_METHOD); - return NO; - } - - // Why is this commented out? - // See comments below. - -// NSError *err = nil; -// if (![self createReadAndWriteStreams:&err]) -// { -// LogError(@"Error creating CFStream(s): %@", err); -// return NO; -// } -// -// LogVerbose(@"Enabling backgrouding on socket"); -// -// BOOL r1, r2; -// -// if (readStream4 && writeStream4) -// { -// r1 = CFReadStreamSetProperty(readStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); -// r2 = CFWriteStreamSetProperty(writeStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); -// -// if (!r1 || !r2) -// { -// LogError(@"Error setting voip type (IPv4)"); -// return NO; -// } -// } -// -// if (readStream6 && writeStream6) -// { -// r1 = CFReadStreamSetProperty(readStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); -// r2 = CFWriteStreamSetProperty(writeStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); -// -// if (!r1 || !r2) -// { -// LogError(@"Error setting voip type (IPv6)"); -// return NO; -// } -// } -// -// return YES; - - // The above code will actually appear to work. - // The methods will return YES, and everything will appear fine. - // - // One tiny problem: the sockets will still get closed when the app gets backgrounded. - // - // Apple does not officially support backgrounding UDP sockets. - - return NO; + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return NO; + } + + // Why is this commented out? + // See comments below. + + // NSError *err = nil; + // if (![self createReadAndWriteStreams:&err]) + // { + // LogError(@"Error creating CFStream(s): %@", err); + // return NO; + // } + // + // LogVerbose(@"Enabling backgrouding on socket"); + // + // BOOL r1, r2; + // + // if (readStream4 && writeStream4) + // { + // r1 = CFReadStreamSetProperty(readStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + // r2 = CFWriteStreamSetProperty(writeStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + // + // if (!r1 || !r2) + // { + // LogError(@"Error setting voip type (IPv4)"); + // return NO; + // } + // } + // + // if (readStream6 && writeStream6) + // { + // r1 = CFReadStreamSetProperty(readStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + // r2 = CFWriteStreamSetProperty(writeStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + // + // if (!r1 || !r2) + // { + // LogError(@"Error setting voip type (IPv6)"); + // return NO; + // } + // } + // + // return YES; + + // The above code will actually appear to work. + // The methods will return YES, and everything will appear fine. + // + // One tiny problem: the sockets will still get closed when the app gets backgrounded. + // + // Apple does not officially support backgrounding UDP sockets. + + return NO; } #endif @@ -5519,122 +5739,128 @@ - (BOOL)enableBackgroundingOnSockets + (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { - char addrBuf[INET_ADDRSTRLEN]; - - if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) - { - addrBuf[0] = '\0'; - } - - return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, + &pSockaddr4->sin_addr, + addrBuf, + (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { - char addrBuf[INET6_ADDRSTRLEN]; - - if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) - { - addrBuf[0] = '\0'; - } - - return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, + &pSockaddr6->sin6_addr, + addrBuf, + (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { - return ntohs(pSockaddr4->sin_port); + return ntohs(pSockaddr4->sin_port); } + (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { - return ntohs(pSockaddr6->sin6_port); + return ntohs(pSockaddr6->sin6_port); } + (NSString *)hostFromAddress:(NSData *)address { - NSString *host = nil; - [self getHost:&host port:NULL family:NULL fromAddress:address]; - - return host; + NSString *host = nil; + [self getHost:&host port:NULL family:NULL fromAddress:address]; + + return host; } + (uint16_t)portFromAddress:(NSData *)address { - uint16_t port = 0; - [self getHost:NULL port:&port family:NULL fromAddress:address]; - - return port; + uint16_t port = 0; + [self getHost:NULL port:&port family:NULL fromAddress:address]; + + return port; } + (int)familyFromAddress:(NSData *)address { - int af = AF_UNSPEC; - [self getHost:NULL port:NULL family:&af fromAddress:address]; - - return af; + int af = AF_UNSPEC; + [self getHost:NULL port:NULL family:&af fromAddress:address]; + + return af; } + (BOOL)isIPv4Address:(NSData *)address { - int af = AF_UNSPEC; - [self getHost:NULL port:NULL family:&af fromAddress:address]; - - return (af == AF_INET); + int af = AF_UNSPEC; + [self getHost:NULL port:NULL family:&af fromAddress:address]; + + return (af == AF_INET); } + (BOOL)isIPv6Address:(NSData *)address { - int af = AF_UNSPEC; - [self getHost:NULL port:NULL family:&af fromAddress:address]; - - return (af == AF_INET6); + int af = AF_UNSPEC; + [self getHost:NULL port:NULL family:&af fromAddress:address]; + + return (af == AF_INET6); } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address { - return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPtr fromAddress:(NSData *)address { - if ([address length] >= sizeof(struct sockaddr)) - { - const struct sockaddr *addrX = (const struct sockaddr *)[address bytes]; - - if (addrX->sa_family == AF_INET) - { - if ([address length] >= sizeof(struct sockaddr_in)) - { - const struct sockaddr_in *addr4 = (const struct sockaddr_in *)(const void *)addrX; - - if (hostPtr) *hostPtr = [self hostFromSockaddr4:addr4]; - if (portPtr) *portPtr = [self portFromSockaddr4:addr4]; - if (afPtr) *afPtr = AF_INET; - - return YES; - } - } - else if (addrX->sa_family == AF_INET6) - { - if ([address length] >= sizeof(struct sockaddr_in6)) - { - const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *)(const void *)addrX; - - if (hostPtr) *hostPtr = [self hostFromSockaddr6:addr6]; - if (portPtr) *portPtr = [self portFromSockaddr6:addr6]; - if (afPtr) *afPtr = AF_INET6; - - return YES; - } - } - } - - if (hostPtr) *hostPtr = nil; - if (portPtr) *portPtr = 0; - if (afPtr) *afPtr = AF_UNSPEC; - - return NO; + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *addrX = (const struct sockaddr *)[address bytes]; + + if (addrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + const struct sockaddr_in *addr4 = (const struct sockaddr_in *)(const void *)addrX; + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:addr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:addr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (addrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *)(const void *)addrX; + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:addr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:addr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + if (hostPtr) *hostPtr = nil; + if (portPtr) *portPtr = 0; + if (afPtr) *afPtr = AF_UNSPEC; + + return NO; } @end diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m index 4aef974d7..d8c8c70ca 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Categories/DDRange.m @@ -5,27 +5,21 @@ DDRange DDUnionRange(DDRange range1, DDRange range2) { - DDRange result; - - result.location = MIN(range1.location, range2.location); - result.length = MAX(DDMaxRange(range1), DDMaxRange(range2)) - result.location; - - return result; + 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) { - DDRange result; - if((DDMaxRange(range1) < range2.location) || (DDMaxRange(range2) < range1.location)) { return DDMakeRange(0, 0); } - result.location = MAX(range1.location, range2.location); - result.length = MIN(DDMaxRange(range1), DDMaxRange(range2)) - result.location; - - return result; + return DDMakeRange(MAX(range1.location, range2.location), + MIN(DDMaxRange(range1), DDMaxRange(range2)) - MAX(range1.location, range2.location)); } NSString *DDStringFromRange(DDRange range) diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h index 84ee8da04..4c277f1db 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPLogging.h @@ -95,28 +95,28 @@ // Define logging primitives. -#define HTTPLogError(...) { } +#define HTTPLogError(...) do {} while (0) -#define HTTPLogWarn(...) { } +#define HTTPLogWarn(...) do {} while (0) -#define HTTPLogInfo(...) { } +#define HTTPLogInfo(...) do {} while (0) -#define HTTPLogVerbose(...) { } +#define HTTPLogVerbose(...) do {} while (0) -#define HTTPLogTrace() { } +#define HTTPLogTrace() do {} while (0) -#define HTTPLogTrace2(...) { } +#define HTTPLogTrace2(...) do {} while (0) -#define HTTPLogCError(...) { } +#define HTTPLogCError(...) do {} while (0) -#define HTTPLogCWarn(...) { } +#define HTTPLogCWarn(...) do {} while (0) -#define HTTPLogCInfo(...) { } +#define HTTPLogCInfo(...) do {} while (0) -#define HTTPLogCVerbose(...) { } +#define HTTPLogCVerbose(...) do {} while (0) -#define HTTPLogCTrace() { } +#define HTTPLogCTrace() do {} while (0) -#define HTTPLogCTrace2(...) { } +#define HTTPLogCTrace2(...) do {} while (0) diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h index 04e0bd2f4..401830e56 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.h @@ -1,21 +1,26 @@ /** - * The HTTPMessage class is a simple Objective-C wrapper around Apple's CFHTTPMessage class. + * The HTTPMessage class is a simple Objective-C wrapper for HTTP message parsing. + * Migrated from CFHTTPMessage to use Foundation and Network framework. **/ #import -#if TARGET_OS_IPHONE -// Note: You may need to add the CFNetwork Framework to your project -#import -#endif - -#define HTTPVersion1_0 ((NSString *)kCFHTTPVersion1_0) -#define HTTPVersion1_1 ((NSString *)kCFHTTPVersion1_1) +#define HTTPVersion1_0 @"HTTP/1.0" +#define HTTPVersion1_1 @"HTTP/1.1" @interface HTTPMessage : NSObject { - CFHTTPMessageRef message; + NSMutableDictionary *_headers; + NSMutableData *_body; + NSString *_version; + NSString *_method; + NSURL *_url; + NSInteger _statusCode; + NSString *_statusDescription; + BOOL _isRequest; + BOOL _headerComplete; + NSMutableData *_rawData; } - (id)initEmptyRequest; diff --git a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m index 345d84759..44eaee180 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/HTTPMessage.m @@ -8,107 +8,350 @@ @implementation HTTPMessage -- (id)initEmptyRequest +- (id)init { if ((self = [super init])) { - message = CFHTTPMessageCreateEmpty(NULL, YES); + _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 = [super init])) + if ((self = [self init])) { - message = CFHTTPMessageCreateRequest(NULL, - (__bridge CFStringRef)method, - (__bridge CFURLRef)url, - (__bridge CFStringRef)version); + _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 = [super init])) + if ((self = [self init])) { - message = CFHTTPMessageCreateResponse(NULL, - (CFIndex)code, - (__bridge CFStringRef)description, - (__bridge CFStringRef)version); + _isRequest = NO; + _statusCode = code; + _statusDescription = [description copy]; + _version = version ? [version copy] : HTTPVersion1_1; } return self; } -- (void)dealloc +- (BOOL)appendData:(NSData *)data { - if (message) + if (!data || [data length] == 0) { - CFRelease(message); + 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; } -- (BOOL)appendData:(NSData *)data +- (void)parseHeaders:(NSString *)headerString { - return CFHTTPMessageAppendBytes(message, [data bytes], [data length]); + 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 CFHTTPMessageIsHeaderComplete(message); + return _headerComplete; } - (NSString *)version { - return (__bridge_transfer NSString *)CFHTTPMessageCopyVersion(message); + return _version; } - (NSString *)method { - return (__bridge_transfer NSString *)CFHTTPMessageCopyRequestMethod(message); + return _method; } - (NSURL *)url { - return (__bridge_transfer NSURL *)CFHTTPMessageCopyRequestURL(message); + return _url; } - (NSInteger)statusCode { - return (NSInteger)CFHTTPMessageGetResponseStatusCode(message); + return _statusCode; } - (NSDictionary *)allHeaderFields { - return (__bridge_transfer NSDictionary *)CFHTTPMessageCopyAllHeaderFields(message); + return [_headers copy]; } - (NSString *)headerField:(NSString *)headerField { - return (__bridge_transfer NSString *)CFHTTPMessageCopyHeaderFieldValue(message, (__bridge CFStringRef)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 { - CFHTTPMessageSetHeaderFieldValue(message, - (__bridge CFStringRef)headerField, - (__bridge CFStringRef)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 { - return (__bridge_transfer NSData *)CFHTTPMessageCopySerializedMessage(message); + 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 (__bridge_transfer NSData *)CFHTTPMessageCopyBody(message); + return [_body copy]; } - (void)setBody:(NSData *)body { - CFHTTPMessageSetBody(message, (__bridge CFDataRef)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/Responses/HTTPErrorResponse.m b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m index 3309cf164..a11552008 100644 --- a/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m +++ b/WebDriverAgentLib/Vendor/CocoaHTTPServer/Responses/HTTPErrorResponse.m @@ -22,9 +22,7 @@ - (UInt64) offset { return 0; } -- (void)setOffset:(UInt64)offset { - ; -} +- (void)setOffset:(UInt64)offset {} - (NSData*) readDataOfLength:(NSUInteger)length { return nil; diff --git a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m index 56df1cd3f..68e6a274a 100644 --- a/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m +++ b/WebDriverAgentLib/Vendor/RoutingHTTPServer/RoutingHTTPServer.m @@ -149,16 +149,16 @@ - (Route *)routeWithPath:(NSString *)path { NSRegularExpression *regex = nil; // Escape regex characters - regex = [NSRegularExpression regularExpressionWithPattern:@"[.+()]" options:0 error:nil]; - path = [regex stringByReplacingMatchesInString:path options:0 range:NSMakeRange(0, path.length) withTemplate:@"\\\\$0"]; + 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:0 + options:(NSRegularExpressionOptions)0 error:nil]; NSMutableString *regexPath = [NSMutableString stringWithString:path]; __block NSInteger diff = 0; - [regex enumerateMatchesInString:path options:0 range:NSMakeRange(0, path.length) + [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; @@ -219,7 +219,7 @@ - (RouteResponse *)routeMethod:(NSString *)method return nil; for (Route *route in methodRoutes) { - NSTextCheckingResult *result = [route.regex firstMatchInString:path options:0 range:NSMakeRange(0, path.length)]; + NSTextCheckingResult *result = [route.regex firstMatchInString:path options:(NSMatchingOptions)0 range:NSMakeRange(0, path.length)]; if (!result) continue; diff --git a/WebDriverAgentLib/WebDriverAgentLib.h b/WebDriverAgentLib/WebDriverAgentLib.h index 6d9daa1f6..d0e3f7391 100644 --- a/WebDriverAgentLib/WebDriverAgentLib.h +++ b/WebDriverAgentLib/WebDriverAgentLib.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import @@ -15,8 +14,8 @@ FOUNDATION_EXPORT double WebDriverAgentLib_VersionNumber; //! Project version string for WebDriverAgentLib_. FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[]; +#import #import -#import #import #import #import @@ -39,9 +38,17 @@ FOUNDATION_EXPORT const unsigned char WebDriverAgentLib_VersionString[]; #import #import #import +#import +#import +#import +#import +#import +#import +#import #import #import #import +#import #import #import #import diff --git a/WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/Contents.json b/WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..27a4f3814 --- /dev/null +++ b/WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "icon-1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/icon-1024.png new file mode 100644 index 000000000..8f359b2be Binary files /dev/null and b/WebDriverAgentRunner/Assets.xcassets/AppIcon.appiconset/icon-1024.png differ diff --git a/WebDriverAgentRunner/Assets.xcassets/Contents.json b/WebDriverAgentRunner/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/WebDriverAgentRunner/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/WebDriverAgentRunner/UITestingUITests.m b/WebDriverAgentRunner/UITestingUITests.m index cc7c78073..ad2ff4550 100644 --- a/WebDriverAgentRunner/UITestingUITests.m +++ b/WebDriverAgentRunner/UITestingUITests.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h b/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h index ae3920f7f..ff03ff776 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h +++ b/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.h @@ -3,13 +3,12 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import +#import -@interface AppDelegate : UIResponder -@property (strong, nonatomic) UIWindow *window; +@interface AppDelegate : UIResponder @end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m b/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m index 0a0e1d46d..102b1887b 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/AppDelegate.m @@ -3,14 +3,36 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "AppDelegate.h" +#import "SceneDelegate.h" @interface AppDelegate () @end @implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // Override point for customization after application launch. + return YES; +} + +#pragma mark - UISceneSession lifecycle + +- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role]; + configuration.delegateClass = [SceneDelegate class]; + return configuration; +} + +- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet *)sceneSessions { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after `application:didFinishLaunchingWithOptions`. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. +} + @end diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h b/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h index c657ab618..e5b2088c8 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.h @@ -3,11 +3,11 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import +#import @interface FBAlertViewController : UIViewController diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m b/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m index c90b58ab4..1f1c77e11 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBAlertViewController.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBAlertViewController.h" @@ -37,7 +36,14 @@ - (IBAction)createAppSheet:(UIButton *)sender - (IBAction)createNotificationAlert:(UIButton *)sender { - [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert categories:nil]]; + UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; + [center requestAuthorizationWithOptions:(UNAuthorizationOptionSound|UNAuthorizationOptionAlert|UNAuthorizationOptionBadge) + completionHandler:^(BOOL granted, NSError * _Nullable error) + { + dispatch_async(dispatch_get_main_queue(), ^{ + [[UIApplication sharedApplication] registerForRemoteNotifications]; + }); + }]; } - (IBAction)createCameraRollAccessAlert:(UIButton *)sender diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h b/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h index c83f8d262..e9a6b8d95 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m b/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m index f404c0ba3..3b0bec266 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBNavigationController.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBNavigationController.h" diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h b/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h index 4c79fb0f5..7cd517b43 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m b/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m index 4a49f9ce6..a8fe79773 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBScrollViewController.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBScrollViewController.h" diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h b/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h index 28cbe4783..d82f8b651 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m b/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m index ba28aed79..335f28afd 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/FBTableDataSource.m @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "FBTableDataSource.h" diff --git a/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.h b/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.h new file mode 100644 index 000000000..972eb671e --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.h @@ -0,0 +1,20 @@ +/** + * 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface SceneDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m b/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m new file mode 100644 index 000000000..646192cd4 --- /dev/null +++ b/WebDriverAgentTests/IntegrationApp/Classes/SceneDelegate.m @@ -0,0 +1,48 @@ +/** + * 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 "SceneDelegate.h" + +@implementation SceneDelegate + +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be set and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession:` instead). +} + +- (void)sceneDidDisconnect:(UIScene *)scene { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). +} + +- (void)sceneDidBecomeActive:(UIScene *)scene { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. +} + +- (void)sceneWillResignActive:(UIScene *)scene { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). +} + +- (void)sceneWillEnterForeground:(UIScene *)scene { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. +} + +- (void)sceneDidEnterBackground:(UIScene *)scene { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. +} + +@end + diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h b/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h index 3a312cb88..ed6a9cd6c 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.h @@ -3,8 +3,7 @@ * 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. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m b/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m index 539add43d..ed7969404 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchSpotView.m @@ -3,8 +3,7 @@ * 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. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "TouchSpotView.h" diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h b/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h index 33bd08598..7125306a3 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.h @@ -3,8 +3,7 @@ * 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. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m b/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m index 1fd07671c..25b3f9930 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchViewController.m @@ -3,8 +3,7 @@ * 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. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "TouchViewController.h" diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h b/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h index 46111c018..53d0c1836 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.h @@ -3,8 +3,7 @@ * 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. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m b/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m index 55b2fd11e..9e7412ab7 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/TouchableView.m @@ -3,8 +3,7 @@ * 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. An additional grant -* of patent rights can be found in the PATENTS file in the same directory. +* LICENSE file in the root directory of this source tree. */ #import "TouchableView.h" diff --git a/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h index e4df859ab..934960484 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h +++ b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.h @@ -3,8 +3,7 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import diff --git a/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m index 6a0d0441a..c4981acfc 100644 --- a/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m +++ b/WebDriverAgentTests/IntegrationApp/Classes/ViewController.m @@ -3,18 +3,39 @@ * 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. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. + * LICENSE file in the root directory of this source tree. */ #import "ViewController.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UILabel *orentationLabel; +@property (weak, nonatomic) IBOutlet UIButton *button; @end @implementation ViewController +- (void)viewDidLoad +{ + [super viewDidLoad]; + + UIAccessibilityCustomAction *action1 = + [[UIAccessibilityCustomAction alloc] initWithName:@"Custom Action 1" + target:self + selector:@selector(handleCustomAction:)]; + UIAccessibilityCustomAction *action2 = + [[UIAccessibilityCustomAction alloc] initWithName:@"Custom Action 2" + target:self + selector:@selector(handleCustomAction:)]; + self.button.accessibilityCustomActions = @[action1, action2]; +} + +- (BOOL)handleCustomAction:(UIAccessibilityCustomAction *)action +{ + // Custom action handler - just return YES to indicate success + return YES; +} + - (IBAction)deadlockApp:(id)sender { dispatch_sync(dispatch_get_main_queue(), ^{ @@ -37,7 +58,7 @@ - (void)viewDidLayoutSubviews - (void)updateOrentationLabel { NSString *orientation = nil; - switch (self.interfaceOrientation) { + switch (UIDevice.currentDevice.orientation) { case UIInterfaceOrientationPortrait: orientation = @"Portrait"; break; @@ -50,6 +71,12 @@ - (void)updateOrentationLabel case UIInterfaceOrientationLandscapeRight: orientation = @"LandscapeRight"; break; + case UIDeviceOrientationFaceUp: + orientation = @"FaceUp"; + break; + case UIDeviceOrientationFaceDown: + orientation = @"FaceDown"; + break; case UIInterfaceOrientationUnknown: orientation = @"Unknown"; break; diff --git a/WebDriverAgentTests/IntegrationApp/Info.plist b/WebDriverAgentTests/IntegrationApp/Info.plist index 2f4f17529..5dc963a62 100644 --- a/WebDriverAgentTests/IntegrationApp/Info.plist +++ b/WebDriverAgentTests/IntegrationApp/Info.plist @@ -7,7 +7,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - com.facebook.wda.integrationApp + $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -22,18 +22,33 @@ 1 LSRequiresIPhoneOS + NSLocationAlwaysAndWhenInUseUsageDescription + Yo Yo NSLocationAlwaysUsageDescription Yo Yo NSLocationWhenInUseUsageDescription Yo Yo - NSLocationAlwaysAndWhenInUseUsageDescription - Yo Yo NSPhotoLibraryUsageDescription Yo Yo UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneStoryboardFile + Main + + + + UIRequiredDeviceCapabilities armv7 diff --git a/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard b/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard index db5221045..40f6efb80 100644 --- a/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard +++ b/WebDriverAgentTests/IntegrationApp/Resources/Base.lproj/Main.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -19,8 +20,8 @@ - - - - - - + @@ -238,32 +240,32 @@ - + - + - + - + - - + - + @@ -347,6 +349,9 @@ + + + @@ -365,7 +370,7 @@ - + @@ -374,8 +379,8 @@ - - - - - - - -