diff --git a/.clang-format b/.clang-format index bea088ea3..378d37ada 100644 --- a/.clang-format +++ b/.clang-format @@ -2,6 +2,23 @@ # Defaults for all languages. BasedOnStyle: Google -ColumnLimit: 120 +# Setting ColumnLimit to 0 so developer choices about where to break lines are maintained. +# Developers are responsible for adhering to the 120 character maximum. +ColumnLimit: 120 +SortIncludes: false +DerivePointerAlignment: false +# Avoid adding spaces between tokens in GSL_SUPPRESS arguments. +# E.g., don't change "GSL_SUPPRESS(r.11)" to "GSL_SUPPRESS(r .11)". +WhitespaceSensitiveMacros: ["GSL_SUPPRESS"] + +# if you want to customize when working locally see https://clang.llvm.org/docs/ClangFormatStyleOptions.html for options. +# See ReformatSource.ps1 for a script to update all source according to the current options in this file. +# e.g. customizations to use Allman bracing and more indenting. +# AccessModifierOffset: -2 +# BreakBeforeBraces: Allman +# CompactNamespaces: false +# IndentCaseLabels: true +# IndentWidth: 4 +# NamespaceIndentation: All ... diff --git a/.github/actions/setup-android-ndk/action.yml b/.github/actions/setup-android-ndk/action.yml new file mode 100644 index 000000000..f7db1cfc7 --- /dev/null +++ b/.github/actions/setup-android-ndk/action.yml @@ -0,0 +1,99 @@ +# .github/actions/setup-android-ndk/action.yml +name: 'Setup Android NDK' +description: 'Installs and configures a specific version of the Android NDK' +inputs: + ndk-version: + description: 'The version of the Android NDK to install (e.g., 27.2.12479018)' + required: true + default: '28.0.13004108' + android-sdk-root: + description: 'The root directory of the Android SDK' + required: true + default: '/usr/local/lib/android/sdk' + +runs: + using: "composite" # Use a composite action for multiple shell commands + steps: + - name: Install coreutils and ninja + shell: bash + run: sudo apt-get update -y && sudo apt-get install -y coreutils ninja-build + + - name: Install Android NDK + shell: bash + run: | + set -e + python -m pip install psutil + "${{ inputs.android-sdk-root }}/cmdline-tools/latest/bin/sdkmanager" --install "ndk;${{ inputs.ndk-version }}" + + NDK_PATH="${{ inputs.android-sdk-root }}/ndk/${{ inputs.ndk-version }}" + if [[ ! -d "${NDK_PATH}" ]]; then + echo "NDK directory is not in expected location: ${NDK_PATH}" + exit 1 + fi + + # Use standard environment variable setting in bash and add to GITHUB_ENV + echo "ANDROID_NDK_HOME=${NDK_PATH}" >> $GITHUB_ENV + echo "ANDROID_NDK_ROOT=${NDK_PATH}" >> $GITHUB_ENV + echo "ANDROID_NDK_HOME: ${NDK_PATH}" + echo "ANDROID_NDK_ROOT: ${NDK_PATH}" + + - name: Check if emulator are installed and add to PATH + shell: bash + run: | + if [[ ":$PATH:" == *":${ANDROID_SDK_ROOT}/emulator:"* ]]; then + echo "${ANDROID_SDK_ROOT}/emulator is in PATH" + else + ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "emulator" + echo "${ANDROID_SDK_ROOT}/emulator" >> $GITHUB_PATH + fi + + - name: Check if platform tools are installed and add to PATH + shell: bash + run: | + if [[ ":$PATH:" == *":${ANDROID_SDK_ROOT}/platform-tools:"* ]]; then + echo "${ANDROID_SDK_ROOT}/platform-tools is in PATH" + else + ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --install "platform-tools" + echo "${ANDROID_SDK_ROOT}/platform-tools" >> $GITHUB_PATH + fi + ls -R "${ANDROID_SDK_ROOT}/platform-tools" + + - name: Create Android Emulator + shell: bash + env: + ANDROID_AVD_HOME: ${{ runner.temp }}/android-avd + run: | + python3 tools/python/run_android_emulator.py \ + --android-sdk-root "${ANDROID_SDK_ROOT}" \ + --create-avd --system-image "system-images;android-31;default;x86_64" + + - name: List Android AVDs + shell: bash + env: + ANDROID_AVD_HOME: ${{ runner.temp }}/android-avd + run: | + "${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/avdmanager" list avd + + - name: Check emulator.pid does not exist + shell: bash + run: | + if test -f ./emulator.pid; then + echo "Emulator PID file was not expected to exist but does and has pid: `cat ./emulator.pid`" + exit 1 + fi + + - name: Start Android Emulator + shell: bash + env: + ANDROID_AVD_HOME: ${{ runner.temp }}/android-avd + run: | + set -e -x + python3 tools/python/run_android_emulator.py \ + --android-sdk-root "${ANDROID_SDK_ROOT}" \ + --start --emulator-extra-args="-partition-size 2047" \ + --emulator-pid-file ./emulator.pid + echo "Emulator PID: `cat ./emulator.pid`" + + - name: View Android ENVs + shell: bash + run: env | grep ANDROID \ No newline at end of file diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 07346b38b..c6004b7fe 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -3,12 +3,20 @@ # This workflow was copied from the link above. name: "Validate Gradle Wrapper" -on: [push, pull_request] +on: + push: + branches: [main, 'rel-*'] + pull_request: + branches: [main, 'rel-*'] + workflow_dispatch: jobs: validation: name: "Validation" - runs-on: ubuntu-latest + runs-on: ["self-hosted", "1ES.Pool=onnxruntime-examples-Ubuntu2204-CPU"] steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1 + - uses: actions/checkout@v5 + - uses: gradle/actions/wrapper-validation@v4 +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.sha }} + cancel-in-progress: true diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..873e6500a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,28 @@ +name: Lint + +on: + push: + branches: [main, 'rel-*'] + pull_request: + branches: [main, 'rel-*'] + workflow_dispatch: + +permissions: + contents: read + packages: write + attestations: write + id-token: write + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.10' + - name: Install dependencies + run: pip install -r requirements.txt + - name: Run lintrunner + run: lintrunner diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml new file mode 100644 index 000000000..ebc8d1111 --- /dev/null +++ b/.github/workflows/mobile.yml @@ -0,0 +1,432 @@ +name: Mobile Examples CI + +on: + push: + branches: [main, 'rel-*'] + pull_request: + branches: [main, 'rel-*'] + workflow_dispatch: + +permissions: + contents: read + packages: write + attestations: write + id-token: write + +jobs: + BasicUsageIos: + name: Basic Usage iOS + runs-on: macos-14 + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Use Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Generate model + shell: bash + run: | + set -e + pip install -r ../model/requirements.txt + ../model/gen_model.sh ./OrtBasicUsage/model + working-directory: mobile/examples/basic_usage/ios + + - name: Update Podfile + run: echo "Using original Podfile" + + - name: Install CocoaPods pods + run: pod install + working-directory: 'mobile/examples/basic_usage/ios' + + - name: Xcode build and test + env: + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + run: | + xcodebuild test \ + -workspace mobile/examples/basic_usage/ios/OrtBasicUsage.xcworkspace \ + -scheme OrtBasicUsage \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' + + WhisperLocalAndroid: + name: Whisper Local Android + runs-on: ["self-hosted", "1ES.Pool=onnxruntime-examples-Ubuntu2204-CPU"] + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Use Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Setup Java JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Android NDK + uses: ./.github/actions/setup-android-ndk + + - name: Build and run tests + run: ./gradlew connectedDebugAndroidTest --no-daemon + working-directory: mobile/examples/whisper/local/android + + - name: Stop Android Emulator + if: always() + run: | + env | grep ANDROID + if test -f ${{ github.workspace }}/emulator.pid; then + echo "Emulator PID:"`cat ${{ github.workspace }}/emulator.pid` + python3 tools/python/run_android_emulator.py \ + --android-sdk-root "${ANDROID_SDK_ROOT}" \ + --stop \ + --emulator-pid-file ${{ github.workspace }}/emulator.pid + rm ${{ github.workspace }}/emulator.pid + else + echo "Emulator PID file was expected to exist but does not." + fi + shell: bash + + WhisperAzureAndroid: + name: Whisper Azure Android + runs-on: ["self-hosted", "1ES.Pool=onnxruntime-examples-Ubuntu2204-CPU"] + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Use Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Setup Java JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Android NDK + uses: ./.github/actions/setup-android-ndk + + - name: Build and run tests + run: ./gradlew connectedDebugAndroidTest --no-daemon + working-directory: mobile/examples/whisper/azure/android + + - name: Stop Android Emulator + if: always() + run: | + env | grep ANDROID + if test -f ${{ github.workspace }}/emulator.pid; then + echo "Emulator PID:"`cat ${{ github.workspace }}/emulator.pid` + python3 tools/python/run_android_emulator.py \ + --android-sdk-root "${ANDROID_SDK_ROOT}" \ + --stop \ + --emulator-pid-file ${{ github.workspace }}/emulator.pid + rm ${{ github.workspace }}/emulator.pid + else + echo "Emulator PID file was expected to exist but does not." + fi + shell: bash + + SpeechRecognitionIos: + name: Speech Recognition iOS + runs-on: macos-14 + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Use Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Generate model + shell: bash + run: | + set -e + pip install -r ../model/requirements.txt + ../model/gen_model.sh ./SpeechRecognition/model + working-directory: mobile/examples/speech_recognition/ios + + - name: Update Podfile + run: echo "Using original Podfile" + + - name: Install CocoaPods pods + run: pod install + working-directory: 'mobile/examples/speech_recognition/ios' + + - name: Xcode build and test + env: + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + run: | + xcodebuild test \ + -workspace mobile/examples/speech_recognition/ios/SpeechRecognition.xcworkspace \ + -scheme SpeechRecognition \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' + + ObjectDetectionIos: + name: Object Detection iOS + runs-on: macos-14 + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Use Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Generate model + shell: bash + run: | + set -e + pip install -r ./prepare_model.requirements.txt + ./prepare_model.sh + working-directory: mobile/examples/object_detection/ios/ORTObjectDetection + + - name: Update Podfile + run: echo "Using original Podfile" + + - name: Install CocoaPods pods + run: pod install + working-directory: 'mobile/examples/object_detection/ios' + + - name: Xcode build and test + env: + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + run: | + xcodebuild test \ + -workspace mobile/examples/object_detection/ios/ORTObjectDetection.xcworkspace \ + -scheme ORTObjectDetection \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' + + ObjectDetectionAndroid: + name: Object Detection Android + runs-on: ["self-hosted", "1ES.Pool=onnxruntime-examples-Ubuntu2204-CPU"] + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Use Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Setup Java JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Android NDK + uses: ./.github/actions/setup-android-ndk + + - name: Build and run tests + run: ./gradlew connectedDebugAndroidTest --no-daemon + working-directory: mobile/examples/object_detection/android + + - name: Stop Android Emulator + if: always() + run: | + env | grep ANDROID + if test -f ${{ github.workspace }}/emulator.pid; then + echo "Emulator PID:"`cat ${{ github.workspace }}/emulator.pid` + python3 tools/python/run_android_emulator.py \ + --android-sdk-root "${ANDROID_SDK_ROOT}" \ + --stop \ + --emulator-pid-file ${{ github.workspace }}/emulator.pid + rm ${{ github.workspace }}/emulator.pid + else + echo "Emulator PID file was expected to exist but does not." + fi + shell: bash + + ImageClassificationAndroid: + name: Image Classification Android + runs-on: ["self-hosted", "1ES.Pool=onnxruntime-examples-Ubuntu2204-CPU"] + strategy: + matrix: + ModelFormat: [onnx, ort] + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Use Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Setup Java JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + + - name: Generate models + shell: bash + run: | + set -e + pip install -r ./requirements.txt + ./prepare_models.py --output_dir ./app/src/main/res/raw --format ${{ matrix.ModelFormat }} + working-directory: mobile/examples/image_classification/android + + - name: Setup Android NDK + uses: ./.github/actions/setup-android-ndk + + - name: Build and run tests + run: ./gradlew connectedDebugAndroidTest --no-daemon + working-directory: mobile/examples/image_classification/android + + - name: Stop Android Emulator + if: always() + run: | + env | grep ANDROID + if test -f ${{ github.workspace }}/emulator.pid; then + echo "Emulator PID:"`cat ${{ github.workspace }}/emulator.pid` + python3 tools/python/run_android_emulator.py \ + --android-sdk-root "${ANDROID_SDK_ROOT}" \ + --stop \ + --emulator-pid-file ${{ github.workspace }}/emulator.pid + rm ${{ github.workspace }}/emulator.pid + else + echo "Emulator PID file was expected to exist but does not." + fi + shell: bash + + SuperResolutionAndroid: + name: Super Resolution Android + runs-on: ["self-hosted", "1ES.Pool=onnxruntime-examples-Ubuntu2204-CPU"] + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Use Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Setup Java JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Android NDK + uses: ./.github/actions/setup-android-ndk + + - name: Build and run tests + run: ./gradlew connectedDebugAndroidTest --no-daemon + working-directory: mobile/examples/super_resolution/android + + - name: Stop Android Emulator + if: always() + run: | + env | grep ANDROID + if test -f ${{ github.workspace }}/emulator.pid; then + echo "Emulator PID:"`cat ${{ github.workspace }}/emulator.pid` + python3 tools/python/run_android_emulator.py \ + --android-sdk-root "${ANDROID_SDK_ROOT}" \ + --stop \ + --emulator-pid-file ${{ github.workspace }}/emulator.pid + rm ${{ github.workspace }}/emulator.pid + else + echo "Emulator PID file was expected to exist but does not." + fi + shell: bash + + SuperResolutionIos: + name: Super Resolution iOS + runs-on: macos-14 + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Install CocoaPods pods + run: pod install + working-directory: 'mobile/examples/super_resolution/ios/ORTSuperResolution' + + - name: Xcode build and test + env: + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + run: | + xcodebuild test \ + -workspace mobile/examples/super_resolution/ios/ORTSuperResolution/ORTSuperResolution.xcworkspace \ + -scheme ORTSuperResolution \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' + + QuestionAnsweringAndroid: + name: Question Answering Android + runs-on: ["self-hosted", "1ES.Pool=onnxruntime-examples-Ubuntu2204-CPU"] + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Use Python 3.10 + uses: actions/setup-python@v6 + with: + python-version: '3.10' + + - name: Generate model + shell: bash + run: | + set -e + bash ./prepare_model.sh + working-directory: 'mobile/examples/question_answering/android' + + - name: Setup Java JDK 17 + uses: actions/setup-java@v5 + with: + java-version: '17' + distribution: 'temurin' + + - name: Setup Android NDK + uses: ./.github/actions/setup-android-ndk + + - name: Build and run tests + run: ./gradlew connectedDebugAndroidTest --no-daemon + working-directory: mobile/examples/question_answering/android + + - name: Stop Android Emulator + if: always() + run: | + env | grep ANDROID + if test -f ${{ github.workspace }}/emulator.pid; then + echo "Emulator PID:"`cat ${{ github.workspace }}/emulator.pid` + python3 tools/python/run_android_emulator.py \ + --android-sdk-root "${ANDROID_SDK_ROOT}" \ + --stop \ + --emulator-pid-file ${{ github.workspace }}/emulator.pid + rm ${{ github.workspace }}/emulator.pid + else + echo "Emulator PID file was expected to exist but does not." + fi + shell: bash + + QuestionAnsweringIos: + name: Question Answering iOS + runs-on: macos-14 + steps: + - name: Checkout repository + uses: actions/checkout@v5 + + - name: Install CocoaPods pods + run: pod install + working-directory: 'mobile/examples/question_answering/ios/ORTQuestionAnswering' + + - name: Xcode build and test + env: + DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer + run: | + xcodebuild test \ + -workspace mobile/examples/question_answering/ios/ORTQuestionAnswering/ORTQuestionAnswering.xcworkspace \ + -scheme ORTQuestionAnswering \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.5' diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 7da1516cc..1ef9bd01c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -1,40 +1,47 @@ name: Windows_CI on: push: - branches: - - main + branches: [main, 'rel-*'] pull_request: + branches: [main, 'rel-*'] + workflow_dispatch: concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true +permissions: + contents: read + packages: write + attestations: write + id-token: write + jobs: Onnxruntime-SCA: runs-on: windows-2022 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 with: submodules: false - - uses: actions/setup-python@v3 + - uses: actions/setup-python@v6 with: python-version: '3.11.x' architecture: 'x64' - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v5 with: node-version: 18 - name: Download ORT shell: powershell run: | - &${{ github.workspace }}\ci_build\download_ort_release 1.14.1 + &${{ github.workspace }}\ci_build\download_ort_release 1.22.1 - name: Config cmake run: npm i -g @microsoft/sarif-multitool && mkdir b && cd b && cmake ../c_cxx -DCMAKE_C_FLAGS="/MP /analyze:external- /external:anglebrackets /DWIN32 /D_WINDOWS /DWINVER=0x0A00 /D_WIN32_WINNT=0x0A00 /DNTDDI_VERSION=0x0A000000 /W4 /Ob0 /Od /RTC1 /analyze:autolog:ext .sarif" -DCMAKE_CXX_FLAGS="/EHsc /MP /analyze:external- /external:anglebrackets /DWIN32 /D_WINDOWS /DWINVER=0x0A00 /D_WIN32_WINNT=0x0A00 /DNTDDI_VERSION=0x0A000000 /W4 /Ob0 /Od /RTC1 /analyze:autolog:ext .sarif" -A x64 -T host=x64 -DONNXRUNTIME_ROOTDIR=${{ github.workspace }}\onnxruntimebin && cmake --build . --config Debug && npx @microsoft/sarif-multitool merge *.sarif --recurse --output-directory=${{ github.workspace }}\output --output-file=MergeResult.sarif --merge-runs - name: Upload SARIF to GitHub - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 continue-on-error: true with: sarif_file: ${{ github.workspace }}\output\MergeResult.sarif diff --git a/.lintrunner.toml b/.lintrunner.toml new file mode 100644 index 000000000..7f6f61df5 --- /dev/null +++ b/.lintrunner.toml @@ -0,0 +1,140 @@ +# Configuration for lintrunner https://github.com/suo/lintrunner +# You can install the dependencies and initialize with +# +# ```sh +# pip install -r requirements-lintrunner.txt +# lintrunner init +# ``` +# +# This will install lintrunner on your system and download all the necessary +# dependencies to run linters locally. +# +# To format local changes: +# +# ```bash +# lintrunner -a +# ``` +# +# To format all files: +# +# ```bash +# lintrunner -a --all-files +# ``` +# +# To read more about lintrunner, see [wiki](https://github.com/pytorch/pytorch/wiki/lintrunner). +# To update an existing linting rule or create a new one, modify this file or create a +# new adapter following examples in https://github.com/justinchuby/lintrunner-adapters. + +merge_base_with = 'origin/main' + +[[linter]] +code = 'RUFF' +include_patterns = [ + '**/*.py', + '**/*.pyi', +] +exclude_patterns = [ + 'cmake/external/**', + # ignore generated flatbuffers code + 'onnxruntime/core/flatbuffers/ort_flatbuffers_py/**', + 'orttraining/orttraining/python/training/optim/_ds_code_store.py', +] +command = [ + 'python', + '-m', + 'lintrunner_adapters', + 'run', + 'ruff_linter', + '--config=pyproject.toml', + '@{{PATHSFILE}}' +] +init_command = [ + 'python', + '-m', + 'lintrunner_adapters', + 'run', + 'pip_init', + '--dry-run={{DRYRUN}}', + '--requirement=requirements-lintrunner.txt', +] +is_formatter = true + + +[[linter]] +code = 'RUFF-FORMAT' +include_patterns = [ + '**/*.py', +] +exclude_patterns = [ + 'cmake/**', + 'orttraining/*', + 'onnxruntime/core/flatbuffers/**', + 'orttraining/orttraining/python/training/optim/_ds_code_store.py', +] +command = [ + 'python', + '-m', + 'lintrunner_adapters', + 'run', + 'ruff_format_linter', + '--', + '@{{PATHSFILE}}' +] +init_command = [ + 'python', + '-m', + 'lintrunner_adapters', + 'run', + 'pip_init', + '--dry-run={{DRYRUN}}', + '--requirement=requirements-lintrunner.txt', +] +is_formatter = true + +[[linter]] +code = 'CLANGFORMAT' +include_patterns = [ + '**/*.h', + '**/*.cc', + '**/*.hpp', + '**/*.cpp', + '**/*.cuh', + '**/*.cu', + '**/*.m', + '**/*.mm', +] +exclude_patterns = [ + 'java/**', # FIXME: Enable clang-format for java + 'onnxruntime/contrib_ops/cuda/bert/tensorrt_fused_multihead_attention/**', # Contains data chunks + 'onnxruntime/contrib_ops/cuda/llm/fpA_intB_gemm/launchers/*.generated.cu', # Generated code + 'onnxruntime/core/flatbuffers/schema/*.fbs.h', # Generated code + 'onnxruntime/test/flatbuffers/*.fbs.h', # Generated code + 'onnxruntime/core/graph/contrib_ops/quantization_defs.cc', + 'onnxruntime/core/mlas/**', # Contains assembly code + 'onnxruntime/core/mickey/cutlass_ext/**', # CUTLASS based libs recommends NO automatic code formatting + 'onnxruntime/core/mickey/gemm/**', # CUTLASS based libs recommends NO automatic code formatting + 'winml/lib/Api.Image/shaders/**', # Contains data chunks + 'onnxruntime/contrib_ops/cuda/bert/flash_attention/flash_fwd_launch_template.h', # Bool Switches hang Clang + 'onnxruntime/core/providers/coreml/mlprogram_test_scripts/**', # test scripts only +] +command = [ + 'python', + '-m', + 'lintrunner_adapters', + 'run', + 'clangformat_linter', + '--binary=clang-format', + '--fallback', + '--', + '@{{PATHSFILE}}' +] +init_command = [ + 'python', + '-m', + 'lintrunner_adapters', + 'run', + 'pip_init', + '--dry-run={{DRYRUN}}', + '--requirement=requirements-lintrunner.txt', +] +is_formatter = true diff --git a/c_cxx/CMakeLists.txt b/c_cxx/CMakeLists.txt index 55176891d..0ee9666f2 100644 --- a/c_cxx/CMakeLists.txt +++ b/c_cxx/CMakeLists.txt @@ -7,6 +7,7 @@ cmake_minimum_required(VERSION 3.13) project(onnxruntime_samples C CXX) if (WIN32) string(APPEND CMAKE_CXX_FLAGS " /W4") + add_compile_definitions(-DNOMINMAX) else() string(APPEND CMAKE_CXX_FLAGS " -Wall -Wextra") string(APPEND CMAKE_C_FLAGS " -Wall -Wextra") @@ -19,7 +20,7 @@ option(LIBPNG_ROOTDIR "libpng root dir") option(ONNXRUNTIME_ROOTDIR "onnxruntime root dir") include(FetchContent) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) if(NOT ONNXRUNTIME_ROOTDIR) if(WIN32) @@ -40,15 +41,15 @@ include_directories("${ONNXRUNTIME_ROOTDIR}/include" # link_directories("${ONNXRUNTIME_ROOTDIR}/lib") if(WIN32) - add_library(wil INTERFACE) - + set(WIL_BUILD_PACKAGING OFF CACHE BOOL "" FORCE) + set(WIL_BUILD_TESTS OFF CACHE BOOL "" FORCE) FetchContent_Declare( microsoft_wil - URL https://github.com/microsoft/wil/archive/refs/tags/v1.0.220914.1.zip + URL https://github.com/microsoft/wil/archive/refs/tags/v1.0.250325.1.zip + EXCLUDE_FROM_ALL ) - FetchContent_Populate(microsoft_wil) - target_include_directories(wil INTERFACE ${microsoft_wil_SOURCE_DIR}/include) - set(WIL_LIB wil) + FetchContent_MakeAvailable(microsoft_wil) + set(WIL_LIB "WIL::WIL") endif() # On Linux the samples use libjpeg and libpng for decoding images. @@ -97,4 +98,4 @@ add_subdirectory(squeezenet) if(WIN32 OR PNG_FOUND) add_subdirectory(fns_candy_style_transfer) endif() -add_subdirectory(model-explorer) +add_subdirectory(model-explorer) \ No newline at end of file diff --git a/c_cxx/MNIST/CMakeLists.txt b/c_cxx/MNIST/CMakeLists.txt index 5e15e73ee..e177dded6 100644 --- a/c_cxx/MNIST/CMakeLists.txt +++ b/c_cxx/MNIST/CMakeLists.txt @@ -1,9 +1,10 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. - -cmake_minimum_required(VERSION 3.13) - add_executable(mnist MNIST.cpp) +if(WIN32) + target_compile_definitions(mnist PRIVATE WIN32_LEAN_AND_MEAN NOMINMAX) +endif() +target_link_libraries(mnist PRIVATE WIL::WIL) target_link_options(mnist PRIVATE "/SUBSYSTEM:WINDOWS") copy_ort_dlls(mnist) diff --git a/c_cxx/MNIST/MNIST.cpp b/c_cxx/MNIST/MNIST.cpp index c346b173b..7c63307ce 100644 --- a/c_cxx/MNIST/MNIST.cpp +++ b/c_cxx/MNIST/MNIST.cpp @@ -1,26 +1,47 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #define UNICODE -#include -#include #include +#include + +// C++20 Standard Library #include #include -#include +#include +#include +#include +#include +#include +#include + +// Microsoft WIL for modern Windows programming +#include +#include #pragma comment(lib, "user32.lib") #pragma comment(lib, "gdi32.lib") #pragma comment(lib, "onnxruntime.lib") +// A C++20 concept to constrain the template to types that are random-access ranges of floats. template -static void softmax(T& input) { - float rowmax = *std::max_element(input.begin(), input.end()); - std::vector y(input.size()); +concept FloatRange = std::ranges::random_access_range && std::is_same_v, float>; + +// Applies the SoftMax function to a container of floats. +// Uses std::ranges::max for conciseness. +static void softmax(FloatRange auto& input) { + const float rowmax = std::ranges::max(input); + + std::vector y; + y.reserve(std::ranges::size(input)); float sum = 0.0f; - for (size_t i = 0; i != input.size(); ++i) { - sum += y[i] = std::exp(input[i] - rowmax); + + for (const float value : input) { + const float exp_val = std::exp(value - rowmax); + y.push_back(exp_val); + sum += exp_val; } - for (size_t i = 0; i != input.size(); ++i) { + + for (size_t i = 0; i < std::ranges::size(input); ++i) { input[i] = y[i] / sum; } } @@ -38,6 +59,7 @@ struct MNIST { output_shape_.data(), output_shape_.size()); } + // Runs the inference and returns the digit with the highest probability. std::ptrdiff_t Run() { const char* input_names[] = {"Input3"}; const char* output_names[] = {"Plus214_Output_0"}; @@ -45,7 +67,9 @@ struct MNIST { Ort::RunOptions run_options; session_.Run(run_options, input_names, &input_tensor_, 1, output_names, &output_tensor_, 1); softmax(results_); - result_ = std::distance(results_.begin(), std::max_element(results_.begin(), results_.end())); + + // Use std::ranges::max_element for a cleaner syntax. + result_ = std::ranges::distance(results_.begin(), std::ranges::max_element(results_)); return result_; } @@ -67,61 +91,70 @@ struct MNIST { std::array output_shape_{1, 10}; }; -const constexpr int drawing_area_inset_{4}; // Number of pixels to inset the top left of the drawing area -const constexpr int drawing_area_scale_{4}; // Number of times larger to make the drawing area compared to the shape inputs +// --- Global Variables and Constants --- +const constexpr int drawing_area_inset_{4}; // Number of pixels to inset the top left of the drawing area +const constexpr int drawing_area_scale_{4}; // Number of times larger to make the drawing area compared to the shape inputs const constexpr int drawing_area_width_{MNIST::width_ * drawing_area_scale_}; const constexpr int drawing_area_height_{MNIST::height_ * drawing_area_scale_}; -std::unique_ptr mnist_; -HBITMAP dib_; -HDC hdc_dib_; -bool painting_{}; +std::unique_ptr g_mnist; +bool g_isPainting{}; -HBRUSH brush_winner_{CreateSolidBrush(RGB(128, 255, 128))}; -HBRUSH brush_bars_{CreateSolidBrush(RGB(128, 128, 255))}; +// Use WIL's RAII wrappers for GDI objects to ensure they are always released. +wil::unique_hbitmap g_dib; +wil::unique_hdc g_hdcDib; +wil::unique_hbrush g_brushWinner{CreateSolidBrush(RGB(128, 255, 128))}; +wil::unique_hbrush g_brushBars{CreateSolidBrush(RGB(128, 128, 255))}; +// Helper struct to safely query DIBSECTION details. struct DIBInfo : DIBSECTION { - DIBInfo(HBITMAP hBitmap) noexcept { ::GetObject(hBitmap, sizeof(DIBSECTION), this); } + DIBInfo(HBITMAP hBitmap) { + // Use WIL to throw an exception on failure, improving robustness. + THROW_IF_WIN32_BOOL_FALSE(::GetObject(hBitmap, sizeof(DIBSECTION), this)); + } int Width() const noexcept { return dsBm.bmWidth; } - int Height() const noexcept { return dsBm.bmHeight; } - + int Height() const noexcept { return abs(dsBm.bmHeight); } void* Bits() const noexcept { return dsBm.bmBits; } - int Pitch() const noexcept { return dsBmih.biSizeImage / abs(dsBmih.biHeight); } + int Pitch() const noexcept { return dsBm.bmWidthBytes; } }; -// We need to convert the true-color data in the DIB into the model's floating point format -// TODO: (also scales down the image and smooths the values, but this is not working properly) +// Converts the 32bpp DIB into the model's required floating-point format. +// This version uses std::span for safe, bounded buffer manipulation. void ConvertDibToMnist() { - DIBInfo info{dib_}; + DIBInfo info(g_dib.get()); + + // Create a span representing the source DIB pixel data. + std::span source_bits(static_cast(info.Bits()), info.Pitch() * info.Height()); - const DWORD* input = reinterpret_cast(info.Bits()); - float* output = mnist_->input_image_.data(); + // Create a span over the destination float array. + std::span dest_floats = g_mnist->input_image_; + std::ranges::fill(dest_floats, 0.0f); - std::fill(mnist_->input_image_.begin(), mnist_->input_image_.end(), 0.f); + constexpr size_t bytes_per_pixel = 4; // 32bpp - for (unsigned y = 0; y < MNIST::height_; y++) { - for (unsigned x = 0; x < MNIST::width_; x++) { - output[x] += input[x] == 0 ? 1.0f : 0.0f; + for (int y = 0; y < MNIST::height_; ++y) { + // Create a view into the current row of the source and destination. + auto source_row = source_bits.subspan(y * info.Pitch(), MNIST::width_ * bytes_per_pixel); + auto dest_row = dest_floats.subspan(y * MNIST::width_, MNIST::width_); + + for (int x = 0; x < MNIST::width_; ++x) { + // Get the 32-bit pixel value (BGRA). + uint32_t pixel = *reinterpret_cast(source_row.data() + x * bytes_per_pixel); + + // The model expects a normalized float [0, 1]. + // We treat black pixels (0x000000) as the drawn digit (1.0f). + dest_row[x] = (pixel == 0) ? 1.0f : 0.0f; } - input = reinterpret_cast(reinterpret_cast(input) + info.Pitch()); - output += MNIST::width_; } } LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); -// The Windows entry point function -int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE /*hPrevInstance*/, _In_ LPTSTR /*lpCmdLine*/, - _In_ int nCmdShow) { +int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE, _In_ LPTSTR, _In_ int nCmdShow) { try { - mnist_ = std::make_unique(); - } catch (const Ort::Exception& exception) { - MessageBoxA(nullptr, exception.what(), "Error:", MB_OK); - return 0; - } + g_mnist = std::make_unique(); - { WNDCLASSEX wc{}; wc.cbSize = sizeof(WNDCLASSEX); wc.style = CS_HREDRAW | CS_VREDRAW; @@ -130,48 +163,53 @@ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE /*hPrevInstan wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wc.lpszClassName = L"ONNXTest"; - RegisterClassEx(&wc); - } - { + THROW_LAST_ERROR_IF(RegisterClassEx(&wc) == 0); + BITMAPINFO bmi{}; bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader); bmi.bmiHeader.biWidth = MNIST::width_; - bmi.bmiHeader.biHeight = -MNIST::height_; + bmi.bmiHeader.biHeight = -MNIST::height_; // Top-down DIB bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 32; - bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biCompression = BI_RGB; - void* bits; - dib_ = CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0); - } - if (dib_ == nullptr) return -1; - hdc_dib_ = CreateCompatibleDC(nullptr); - SelectObject(hdc_dib_, dib_); - SelectObject(hdc_dib_, CreatePen(PS_SOLID, 2, RGB(0, 0, 0))); - RECT rect{0, 0, MNIST::width_, MNIST::height_}; - FillRect(hdc_dib_, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH)); - - HWND hWnd = CreateWindow(L"ONNXTest", L"ONNX Runtime Sample - MNIST", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, - CW_USEDEFAULT, 512, 256, nullptr, nullptr, hInstance, nullptr); - if (!hWnd) - return FALSE; - - ShowWindow(hWnd, nCmdShow); - - MSG msg; - while (GetMessage(&msg, NULL, 0, 0)) { - TranslateMessage(&msg); - DispatchMessage(&msg); - } + void* bits = nullptr; + g_dib.reset(CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0)); + THROW_IF_NULL_ALLOC(g_dib.get()); + + g_hdcDib.reset(CreateCompatibleDC(nullptr)); + THROW_IF_NULL_ALLOC(g_hdcDib.get()); + + auto dibSelection = wil::SelectObject(g_hdcDib.get(), g_dib.get()); + // This pen is created, selected, and automatically destroyed when penSelection goes out of scope. + auto penSelection = wil::SelectObject(g_hdcDib.get(), CreatePen(PS_SOLID, 2, RGB(0, 0, 0))); + + RECT rect{0, 0, MNIST::width_, MNIST::height_}; + FillRect(g_hdcDib.get(), &rect, (HBRUSH)GetStockObject(WHITE_BRUSH)); - DeleteObject(dib_); - DeleteDC(hdc_dib_); + HWND hWnd = CreateWindow(L"ONNXTest", L"ONNX Runtime Sample - MNIST", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, + CW_USEDEFAULT, 512, 256, nullptr, nullptr, hInstance, nullptr); + THROW_IF_WIN32_BOOL_FALSE(IsWindow(hWnd)); - DeleteObject(brush_winner_); - DeleteObject(brush_bars_); + ShowWindow(hWnd, nCmdShow); - return (int)msg.wParam; + MSG msg; + while (GetMessage(&msg, NULL, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + return static_cast(msg.wParam); + } + // Catch specific exception types for better error reporting. + catch (const wil::ResultException& e) { + MessageBoxA(nullptr, e.what(), "WIL Error", MB_OK | MB_ICONERROR); + } catch (const Ort::Exception& e) { + MessageBoxA(nullptr, e.what(), "ONNX Runtime Error", MB_OK | MB_ICONERROR); + } catch (const std::exception& e) { + MessageBoxA(nullptr, e.what(), "Standard C++ Error", MB_OK | MB_ICONERROR); + } + return 0; + // All WIL unique_ handles are automatically cleaned up here. } LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { @@ -180,43 +218,43 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); - // Draw the image - StretchBlt(hdc, drawing_area_inset_, drawing_area_inset_, drawing_area_width_, drawing_area_height_, hdc_dib_, 0, - 0, MNIST::width_, MNIST::height_, SRCCOPY); - SelectObject(hdc, GetStockObject(BLACK_PEN)); - SelectObject(hdc, GetStockObject(NULL_BRUSH)); - Rectangle(hdc, drawing_area_inset_, drawing_area_inset_, drawing_area_inset_ + drawing_area_width_, - drawing_area_inset_ + drawing_area_height_); + StretchBlt(hdc, drawing_area_inset_, drawing_area_inset_, drawing_area_width_, drawing_area_height_, + g_hdcDib.get(), 0, 0, MNIST::width_, MNIST::height_, SRCCOPY); + + { + // This scope-based RAII guard ensures the pen and brush are restored after the Rectangle call. + auto penSelection = wil::SelectObject(hdc, GetStockObject(BLACK_PEN)); + auto brushSelection = wil::SelectObject(hdc, GetStockObject(NULL_BRUSH)); + Rectangle(hdc, drawing_area_inset_, drawing_area_inset_, drawing_area_inset_ + drawing_area_width_, + drawing_area_inset_ + drawing_area_height_); + } constexpr int graphs_left = drawing_area_inset_ + drawing_area_width_ + 5; constexpr int graph_width = 64; - SelectObject(hdc, brush_bars_); - auto least = *std::min_element(mnist_->results_.begin(), mnist_->results_.end()); - auto greatest = mnist_->results_[mnist_->result_]; + auto [least, greatest] = std::ranges::minmax(g_mnist->results_); auto range = greatest - least; + if (range == 0.0f) range = 1.0f; // Avoid division by zero. int graphs_zero = static_cast(graphs_left - least * graph_width / range); - // Hilight the winner - RECT rc{graphs_left, static_cast(mnist_->result_) * 16, graphs_left + graph_width + 128, - static_cast(mnist_->result_ + 1) * 16}; - FillRect(hdc, &rc, brush_winner_); + RECT rcWinner{graphs_left, static_cast(g_mnist->result_) * 16, graphs_left + graph_width + 128, + static_cast(g_mnist->result_ + 1) * 16}; + FillRect(hdc, &rcWinner, g_brushWinner.get()); - // For every entry, draw the odds and the graph for it SetBkMode(hdc, TRANSPARENT); - wchar_t value[80]; - for (unsigned i = 0; i < 10; i++) { - int y = 16 * i; - float result = mnist_->results_[i]; - - auto length = wsprintf(value, L"%2d: %d.%02d", i, int(result), abs(int(result * 100) % 100)); - TextOut(hdc, graphs_left + graph_width + 5, y, value, length); - - Rectangle(hdc, graphs_zero, y + 1, static_cast(graphs_zero + result * graph_width / range), y + 14); + { + // Create a new RAII guard for the bar brush. It will be restored when the scope ends. + auto barBrushSelection = wil::SelectObject(hdc, g_brushBars.get()); + for (unsigned i = 0; i < 10; ++i) { + int y = 16 * i; + float result = g_mnist->results_[i]; + auto value = std::format(L"{:2}: {:.2f}", i, result); + TextOutW(hdc, graphs_left + graph_width + 5, y, value.data(), static_cast(value.length())); + Rectangle(hdc, graphs_zero, y + 1, static_cast(graphs_zero + result * graph_width / range), y + 14); + } } - // Draw the zero line MoveToEx(hdc, graphs_zero, 0, nullptr); LineTo(hdc, graphs_zero, 16 * 10); @@ -226,37 +264,38 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_LBUTTONDOWN: { SetCapture(hWnd); - painting_ = true; - int x = (GET_X_LPARAM(lParam) - drawing_area_inset_) / drawing_area_scale_; - int y = (GET_Y_LPARAM(lParam) - drawing_area_inset_) / drawing_area_scale_; - MoveToEx(hdc_dib_, x, y, nullptr); + g_isPainting = true; + // FIX: Replaced GET_X_LPARAM and GET_Y_LPARAM macros + int x = ((int)(short)LOWORD(lParam) - drawing_area_inset_) / drawing_area_scale_; + int y = ((int)(short)HIWORD(lParam) - drawing_area_inset_) / drawing_area_scale_; + MoveToEx(g_hdcDib.get(), x, y, nullptr); return 0; } case WM_MOUSEMOVE: - if (painting_) { - int x = (GET_X_LPARAM(lParam) - drawing_area_inset_) / drawing_area_scale_; - int y = (GET_Y_LPARAM(lParam) - drawing_area_inset_) / drawing_area_scale_; - LineTo(hdc_dib_, x, y); + if (g_isPainting) { + // FIX: Replaced GET_X_LPARAM and GET_Y_LPARAM macros + int x = ((int)(short)LOWORD(lParam) - drawing_area_inset_) / drawing_area_scale_; + int y = ((int)(short)HIWORD(lParam) - drawing_area_inset_) / drawing_area_scale_; + LineTo(g_hdcDib.get(), x, y); InvalidateRect(hWnd, nullptr, false); } return 0; case WM_CAPTURECHANGED: - painting_ = false; + g_isPainting = false; return 0; case WM_LBUTTONUP: ReleaseCapture(); ConvertDibToMnist(); - mnist_->Run(); + g_mnist->Run(); InvalidateRect(hWnd, nullptr, true); return 0; - case WM_RBUTTONDOWN: // Erase the image - { + case WM_RBUTTONDOWN: { RECT rect{0, 0, MNIST::width_, MNIST::height_}; - FillRect(hdc_dib_, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH)); + FillRect(g_hdcDib.get(), &rect, (HBRUSH)GetStockObject(WHITE_BRUSH)); InvalidateRect(hWnd, nullptr, false); return 0; } diff --git a/c_cxx/README.md b/c_cxx/README.md index 7267fc179..d18c410ff 100644 --- a/c_cxx/README.md +++ b/c_cxx/README.md @@ -21,7 +21,7 @@ Note: These build instructions are for the Windows examples only. ## Prerequisites 1. Visual Studio 2019 or 2022 -2. cmake(version >=3.13) +2. cmake(version >=4.0) 3. (optional) [libpng 1.6](https://libpng.sourceforge.io/) ## Install ONNX Runtime diff --git a/c_cxx/imagenet/controller.cc b/c_cxx/imagenet/controller.cc index 0081334b3..ce29cb6d4 100644 --- a/c_cxx/imagenet/controller.cc +++ b/c_cxx/imagenet/controller.cc @@ -5,7 +5,9 @@ Controller::Controller() : cleanup_group_(CreateThreadpoolCleanupGroup()), event_(CreateOnnxRuntimeEvent()) { InitializeThreadpoolEnvironment(&env_); + #pragma warning(disable : 6387) // The doc didn't say if the default pool could be used as callback pool or not SetThreadpoolCallbackPool(&env_, nullptr); + #pragma warning(default : 6387) SetThreadpoolCallbackCleanupGroup(&env_, cleanup_group_, nullptr); } @@ -37,7 +39,7 @@ void Controller::SetFailBit(_Inout_opt_ ONNXRUNTIME_CALLBACK_INSTANCE pci, _In_ } } -bool Controller::SetEof(ONNXRUNTIME_CALLBACK_INSTANCE pci) { +bool Controller::SetEof(_Inout_opt_ ONNXRUNTIME_CALLBACK_INSTANCE pci) { std::lock_guard g(m_); if (state_ == State::RUNNING) { state_ = State::SHUTDOWN; diff --git a/c_cxx/imagenet/image_loader_wic.cc b/c_cxx/imagenet/image_loader_wic.cc index 42c44965d..452fe9277 100644 --- a/c_cxx/imagenet/image_loader_wic.cc +++ b/c_cxx/imagenet/image_loader_wic.cc @@ -8,6 +8,37 @@ #include #include "image_loader.h" +#include "string_utils.h" +#include "assert.h" + +std::string ToMBString(std::wstring_view s) { + if (s.size() >= static_cast(std::numeric_limits::max())) throw std::runtime_error("length overflow"); + + const int src_len = static_cast(s.size() + 1); + const int len = WideCharToMultiByte(CP_ACP, 0, s.data(), src_len, nullptr, 0, nullptr, nullptr); + assert(len > 0); + std::string ret(static_cast(len) - 1, '\0'); +#pragma warning(disable : 4189) + const int r = WideCharToMultiByte(CP_ACP, 0, s.data(), src_len, (char*)ret.data(), len, nullptr, nullptr); + assert(len == r); +#pragma warning(default : 4189) + return ret; +} + +std::string ToUTF8String(std::wstring_view s) { + if (s.size() >= static_cast(std::numeric_limits::max())) throw std::runtime_error("length overflow"); + + const int src_len = static_cast(s.size() + 1); + const int len = WideCharToMultiByte(CP_UTF8, 0, s.data(), src_len, nullptr, 0, nullptr, nullptr); + assert(len > 0); + std::string ret(static_cast(len) - 1, '\0'); +#pragma warning(disable : 4189) + const int r = WideCharToMultiByte(CP_UTF8, 0, s.data(), src_len, (char*)ret.data(), len, nullptr, nullptr); + assert(len == r); +#pragma warning(default : 4189) + return ret; +} + bool CreateImageLoader(void** out) { IWICImagingFactory* piFactory; @@ -84,24 +115,24 @@ OrtStatus* LoadImageFromFileAndCrop(void* loader, const ORTCHAR_T* filename, dou rect.Width = bbox_w_size; ATLENSURE_SUCCEEDED(ppIFormatConverter->CopyPixels(&rect, stride, static_cast(data.size()), data.data())); - float* float_file_data = new float[data.size()]; + std::unique_ptr float_file_data(new float[data.size()]); size_t len = data.size(); for (size_t i = 0; i != len; ++i) { - float_file_data[i] = static_cast(data[i]) / 255; + float_file_data.get()[i] = static_cast(data[i]) / 255; } - *out = float_file_data; + *out = float_file_data.release(); *out_width = bbox_w_size; *out_height = bbox_h_size; return nullptr; } catch (const std::exception& ex) { - std::ostringstream oss; + std::basic_ostringstream oss; oss << "Load " << filename << " failed:" << ex.what(); - return Ort::GetApi().CreateStatus(ORT_FAIL, oss.str().c_str()); + return Ort::GetApi().CreateStatus(ORT_FAIL, ToUTF8String(oss.str()).c_str()); } catch (const CAtlException& ex) { - std::ostringstream oss; + std::basic_ostringstream oss; oss << "Load " << filename << " failed:"; PrintErrorDescription(ex.m_hr, oss); - return Ort::GetApi().CreateStatus(ORT_FAIL, oss.str().c_str()); + return Ort::GetApi().CreateStatus(ORT_FAIL, ToUTF8String(oss.str()).c_str()); } } diff --git a/c_cxx/imagenet/local_filesystem_win.cc b/c_cxx/imagenet/local_filesystem_win.cc index ccb66120c..197be22e3 100644 --- a/c_cxx/imagenet/local_filesystem_win.cc +++ b/c_cxx/imagenet/local_filesystem_win.cc @@ -4,8 +4,7 @@ #include "local_filesystem.h" #include #include - -static std::mutex m; +#include "string_utils.h" void ReadFileAsString(const ORTCHAR_T* fname, void*& p, size_t& len) { if (!fname) { @@ -15,17 +14,17 @@ void ReadFileAsString(const ORTCHAR_T* fname, void*& p, size_t& len) { HANDLE hFile = CreateFileW(fname, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { int err = GetLastError(); - std::ostringstream oss; + std::basic_ostringstream oss; oss << "open file " << fname << " fail, errcode =" << err; - throw std::runtime_error(oss.str().c_str()); + throw std::runtime_error(ToMBString(oss.str())); } std::unique_ptr handler_holder(hFile, CloseHandle); LARGE_INTEGER filesize; if (!GetFileSizeEx(hFile, &filesize)) { int err = GetLastError(); - std::ostringstream oss; + std::basic_ostringstream oss; oss << "GetFileSizeEx file " << fname << " fail, errcode =" << err; - throw std::runtime_error(oss.str().c_str()); + throw std::runtime_error(ToMBString(oss.str())); } if (static_cast(filesize.QuadPart) > std::numeric_limits::max()) { throw std::runtime_error("ReadFileAsString: File is too large"); @@ -53,16 +52,16 @@ void ReadFileAsString(const ORTCHAR_T* fname, void*& p, size_t& len) { int err = GetLastError(); p = nullptr; len = 0; - std::ostringstream oss; + std::basic_ostringstream oss; oss << "ReadFile " << fname << " fail, errcode =" << err; - throw std::runtime_error(oss.str().c_str()); + throw std::runtime_error(ToMBString(oss.str())); } if (bytes_read != bytes_to_read) { p = nullptr; len = 0; - std::ostringstream oss; + std::basic_ostringstream oss; oss << "ReadFile " << fname << " fail: unexpected end"; - throw std::runtime_error(oss.str().c_str()); + throw std::runtime_error(ToMBString(oss.str())); } } p = buffer.release(); diff --git a/c_cxx/imagenet/main.cc b/c_cxx/imagenet/main.cc index 6af05ed95..fa83fa079 100644 --- a/c_cxx/imagenet/main.cc +++ b/c_cxx/imagenet/main.cc @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include #include #include #include @@ -27,7 +28,7 @@ #ifdef _WIN32 #include #endif - +#include "string_utils.h" using namespace std::chrono; @@ -44,10 +45,10 @@ class Validator : public OutputCollector { if (!line.empty()) labels.push_back(line); } if (labels.size() != expected_line_count) { - std::ostringstream oss; + std::basic_ostringstream oss; oss << "line count mismatch, expect " << expected_line_count << " from " << file_path.c_str() << ", got " << labels.size(); - throw std::runtime_error(oss.str()); + throw std::runtime_error(ToMBString(oss.str())); } return labels; } diff --git a/c_cxx/imagenet/string_utils.h b/c_cxx/imagenet/string_utils.h new file mode 100644 index 000000000..ca7418b20 --- /dev/null +++ b/c_cxx/imagenet/string_utils.h @@ -0,0 +1,6 @@ +#pragma once +#include +#include + +std::string ToMBString(std::wstring_view s); +std::string ToUTF8String(std::wstring_view s); \ No newline at end of file diff --git a/c_cxx/model-explorer/model-explorer.cpp b/c_cxx/model-explorer/model-explorer.cpp index 2f14cc206..a3c180b7f 100644 --- a/c_cxx/model-explorer/model-explorer.cpp +++ b/c_cxx/model-explorer/model-explorer.cpp @@ -39,9 +39,10 @@ std::string print_shape(const std::vector& v) { return ss.str(); } +// XXX: this function does not handle integer overflow int calculate_product(const std::vector& v) { int total = 1; - for (auto& i : v) total *= i; + for (auto& i : v) total *= static_cast(i); return total; } @@ -105,7 +106,7 @@ int main(int argc, ORTCHAR_T* argv[]) { // generate random numbers in the range [0, 255] std::vector input_tensor_values(total_number_elements); - std::generate(input_tensor_values.begin(), input_tensor_values.end(), [&] { return rand() % 255; }); + std::generate(input_tensor_values.begin(), input_tensor_values.end(), [&] { return static_cast(rand() % 255); }); std::vector input_tensors; input_tensors.emplace_back(vec_to_tensor(input_tensor_values, input_shape)); diff --git a/c_cxx/squeezenet/CMakeLists.txt b/c_cxx/squeezenet/CMakeLists.txt index 784564e68..61789945d 100644 --- a/c_cxx/squeezenet/CMakeLists.txt +++ b/c_cxx/squeezenet/CMakeLists.txt @@ -10,7 +10,7 @@ option(ONNXRUNTIME_ROOTDIR "onnxruntime root dir") include(CheckIncludeFileCXX) CHECK_INCLUDE_FILE_CXX(tensorrt_provider_factory.h HAVE_TENSORRT_PROVIDER_FACTORY_H) -set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) include_directories( diff --git a/ci_build/azure_pipelines/mobile-examples-pipeline.yml b/ci_build/azure_pipelines/mobile-examples-pipeline.yml index 338655636..fb1fcded6 100644 --- a/ci_build/azure_pipelines/mobile-examples-pipeline.yml +++ b/ci_build/azure_pipelines/mobile-examples-pipeline.yml @@ -209,9 +209,9 @@ stages: - template: templates/use-python-step.yml - task: JavaToolInstaller@0 - displayName: Use jdk 11 + displayName: Use jdk 17 inputs: - versionSpec: "11" + versionSpec: "17" jdkArchitectureOption: "x64" jdkSourceOption: "PreInstalled" @@ -245,7 +245,7 @@ stages: - bash: | set -e - pip install -r ./prepare_models.requirements.txt + pip install -r ./requirements.txt ./prepare_models.py --output_dir ./app/src/main/res/raw --format $(ModelFormat) workingDirectory: mobile/examples/image_classification/android displayName: "Generate models" diff --git a/ci_build/download_ort_release_and_install_deps.sh b/ci_build/download_ort_release_and_install_deps.sh index b96b7b49c..9c243e088 100755 --- a/ci_build/download_ort_release_and_install_deps.sh +++ b/ci_build/download_ort_release_and_install_deps.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -x -ORT_VER="1.13.1" +ORT_VER="1.22.1" while getopts i: parameter_Option do case "${parameter_Option}" diff --git a/mobile/examples/image_classification/android/README.md b/mobile/examples/image_classification/android/README.md index efa6d5ea3..bd5471be0 100644 --- a/mobile/examples/image_classification/android/README.md +++ b/mobile/examples/image_classification/android/README.md @@ -21,7 +21,7 @@ Run `mobile/examples/image_classification/android/prepare_models.py` to download ```bash cd mobile/examples/image_classification/android # cd to this directory -python -m pip install -r ./prepare_models.requirements.txt +python -m pip install -r ./requirements.txt python ./prepare_models.py --output_dir ./app/src/main/res/raw ``` diff --git a/mobile/examples/image_classification/android/prepare_models.requirements.txt b/mobile/examples/image_classification/android/prepare_models.requirements.txt deleted file mode 100644 index 0339c8fdd..000000000 --- a/mobile/examples/image_classification/android/prepare_models.requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -onnx==1.17.0 -onnxruntime==1.13.1 diff --git a/mobile/examples/image_classification/android/requirements.txt b/mobile/examples/image_classification/android/requirements.txt new file mode 100644 index 000000000..365cbd142 --- /dev/null +++ b/mobile/examples/image_classification/android/requirements.txt @@ -0,0 +1,2 @@ +onnx==1.17.0 +onnxruntime==1.22.1 diff --git a/mobile/examples/object_detection/ios/ORTObjectDetection/prepare_model.requirements.txt b/mobile/examples/object_detection/ios/ORTObjectDetection/prepare_model.requirements.txt index b3c0e65fd..22289d9c1 100644 --- a/mobile/examples/object_detection/ios/ORTObjectDetection/prepare_model.requirements.txt +++ b/mobile/examples/object_detection/ios/ORTObjectDetection/prepare_model.requirements.txt @@ -1,4 +1,4 @@ -onnx==1.13.1 -onnxruntime==1.14.1 -tensorflow==2.12.1 -tf2onnx==1.14.0 +onnx==1.17.0 +onnxruntime==1.22.1 +tensorflow==2.13.0 +tf2onnx==1.16.1 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..a6908407b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +lintrunner +ruff diff --git a/tools/python/__init__.py b/tools/python/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tools/python/run_android_emulator.py b/tools/python/run_android_emulator.py new file mode 100644 index 000000000..6d7c29fc5 --- /dev/null +++ b/tools/python/run_android_emulator.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from __future__ import annotations + +import argparse +import contextlib +import shlex +import sys + +import util.android as android +from util import get_logger + +log = get_logger("run_android_emulator") + + +def parse_args(): + parser = argparse.ArgumentParser( + description="Manages the running of an Android emulator. " + "Supported modes are to create an AVD, and start or stop the emulator. " + "The default is to start the emulator and wait for a keypress to stop it (start and stop)." + ) + + parser.add_argument("--create-avd", action="store_true", help="Whether to create the Android virtual device.") + + parser.add_argument("--start", action="store_true", help="Start the emulator.") + parser.add_argument("--stop", action="store_true", help="Stop the emulator.") + + parser.add_argument("--android-sdk-root", required=True, help="Path to the Android SDK root.") + parser.add_argument( + "--system-image", + default="system-images;android-31;default;x86_64", + help="The Android system image package name.", + ) + parser.add_argument("--avd-name", default="ort_android", help="The Android virtual device name.") + parser.add_argument( + "--emulator-extra-args", default="", help="A string of extra arguments to pass to the Android emulator." + ) + parser.add_argument( + "--emulator-pid-file", + help="Output/input file containing the PID of the emulator process. " + "This is only required if exactly one of --start or --stop is given.", + ) + + args = parser.parse_args() + + if not args.start and not args.stop and not args.create_avd: + # unspecified means start and stop if not creating the AVD + args.start = args.stop = True + + if args.start != args.stop and args.emulator_pid_file is None: + raise ValueError("PID file must be specified if only starting or stopping.") + + return args + + +def main(): + args = parse_args() + + sdk_tool_paths = android.get_sdk_tool_paths(args.android_sdk_root) + + start_emulator_args = { + "sdk_tool_paths": sdk_tool_paths, + "avd_name": args.avd_name, + "extra_args": shlex.split(args.emulator_extra_args), + } + + if args.create_avd: + android.create_virtual_device(sdk_tool_paths, args.system_image, args.avd_name) + + if args.start and args.stop: + with contextlib.ExitStack() as context_stack: + emulator_proc = android.start_emulator(**start_emulator_args) + context_stack.enter_context(emulator_proc) + context_stack.callback(android.stop_emulator, emulator_proc) + + log.info("Press Enter to close.") + sys.stdin.readline() + + elif args.start: + emulator_proc = android.start_emulator(**start_emulator_args) + + with open(args.emulator_pid_file, mode="w") as emulator_pid_file: + print(f"{emulator_proc.pid}", file=emulator_pid_file) + + elif args.stop: + with open(args.emulator_pid_file) as emulator_pid_file: + emulator_pid = int(emulator_pid_file.readline().strip()) + + android.stop_emulator(emulator_pid) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/python/util/__init__.py b/tools/python/util/__init__.py new file mode 100644 index 000000000..8cf584182 --- /dev/null +++ b/tools/python/util/__init__.py @@ -0,0 +1,3 @@ +from .logger import get_logger +from .platform_helpers import is_linux, is_macOS, is_windows # noqa: F401 +from .run import run # noqa: F401 \ No newline at end of file diff --git a/tools/python/util/android/__init__.py b/tools/python/util/android/__init__.py new file mode 100644 index 000000000..104e3738f --- /dev/null +++ b/tools/python/util/android/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from .android import ( # noqa: F401 + SdkToolPaths, + create_virtual_device, + get_sdk_tool_paths, + start_emulator, + stop_emulator, +) diff --git a/tools/python/util/android/android.py b/tools/python/util/android/android.py new file mode 100644 index 000000000..e8dda5cc5 --- /dev/null +++ b/tools/python/util/android/android.py @@ -0,0 +1,359 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from __future__ import annotations + +import collections +import contextlib +import datetime +import os +import signal +import subprocess +import time +import typing +from pathlib import Path + +from ..logger import get_logger +from ..platform_helpers import is_linux, is_windows +from ..run import run + +_log = get_logger("util.android") + + +SdkToolPaths = collections.namedtuple("SdkToolPaths", ["emulator", "adb", "sdkmanager", "avdmanager"]) + + +def get_sdk_tool_paths(sdk_root: str): + def filename(name, windows_extension): + if is_windows(): + return f"{name}.{windows_extension}" + else: + return name + + sdk_root = Path(sdk_root).resolve(strict=True) + + return SdkToolPaths( + # do not use sdk_root/tools/emulator as that is superseded by sdk_root/emulator/emulator + emulator=str((sdk_root / "emulator" / filename("emulator", "exe")).resolve(strict=True)), + adb=str((sdk_root / "platform-tools" / filename("adb", "exe")).resolve(strict=True)), + sdkmanager=str( + (sdk_root / "cmdline-tools" / "latest" / "bin" / filename("sdkmanager", "bat")).resolve(strict=True) + ), + avdmanager=str( + (sdk_root / "cmdline-tools" / "latest" / "bin" / filename("avdmanager", "bat")).resolve(strict=True) + ), + ) + + +def create_virtual_device(sdk_tool_paths: SdkToolPaths, system_image_package_name: str, avd_name: str): + run(sdk_tool_paths.sdkmanager, "--install", system_image_package_name, input=b"y") + android_avd_home = os.environ["ANDROID_AVD_HOME"] + + if android_avd_home is not None: + if not os.path.exists(android_avd_home): + os.makedirs(android_avd_home) + run( + sdk_tool_paths.avdmanager, + "create", + "avd", + "--name", + avd_name, + "--package", + system_image_package_name, + "--force", + "--path", + android_avd_home, + input=b"no", + ) + else: + run( + sdk_tool_paths.avdmanager, + "create", + "avd", + "--name", + avd_name, + "--package", + system_image_package_name, + "--force", + input=b"no", + ) + + +_process_creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if is_windows() else 0 + + +def _start_process(*args) -> subprocess.Popen: + _log.debug(f"Starting process - args: {[*args]}") + return subprocess.Popen([*args], creationflags=_process_creationflags) + + +_stop_signal = signal.CTRL_BREAK_EVENT if is_windows() else signal.SIGTERM + + +def _stop_process(proc: subprocess.Popen): + if proc.returncode is not None: + # process has exited + return + + _log.debug(f"Stopping process - args: {proc.args}") + proc.send_signal(_stop_signal) + + try: + proc.wait(30) + except subprocess.TimeoutExpired: + _log.warning("Timeout expired, forcibly stopping process...") + proc.kill() + + +def _stop_process_with_pid(pid: int): + # minimize scope of external module usage + import psutil # noqa: PLC0415 + + if psutil.pid_exists(pid): + process = psutil.Process(pid) + _log.debug(f"Stopping process - pid={pid}") + process.terminate() + try: + process.wait(60) + except psutil.TimeoutExpired: + print("Process did not terminate within 60 seconds. Killing.") + process.kill() + time.sleep(10) + if psutil.pid_exists(pid): + print(f"Process still exists. State:{process.status()}") + else: + _log.debug(f"No process exists with pid={pid}") + + +def start_emulator( + sdk_tool_paths: SdkToolPaths, + avd_name: str, + extra_args: typing.Sequence[str] | None = None, + timeout_minutes: int = 20, +) -> subprocess.Popen: + if check_emulator_running_using_avd_name(avd_name=avd_name): + raise RuntimeError( + f"An emulator with avd_name{avd_name} is already running. Please close it before starting a new one." + ) + with contextlib.ExitStack() as emulator_stack, contextlib.ExitStack() as waiter_stack: + emulator_args = [ + sdk_tool_paths.emulator, + "-avd", + avd_name, + "-memory", + "4096", + "-timezone", + "America/Los_Angeles", + "-no-snapstorage", + "-no-audio", + "-no-boot-anim", + "-gpu", + "guest", + "-delay-adb", + "-verbose", + ] + + # For Linux CIs we must use "-no-window" otherwise you'll get + # Fatal: This application failed to start because no Qt platform plugin could be initialized + # + # For macOS CIs use a window so that we can potentially capture the desktop and the emulator screen + # and publish screenshot.jpg and emulator.png as artifacts to debug issues. + # screencapture screenshot.jpg + # $(ANDROID_SDK_HOME)/platform-tools/adb exec-out screencap -p > emulator.png + # + # On Windows it doesn't matter (AFAIK) so allow a window which is nicer for local debugging. + if is_linux(): + emulator_args.append("-no-window") + + if extra_args is not None: + emulator_args += extra_args + + emulator_process = emulator_stack.enter_context(_start_process(*emulator_args)) + emulator_stack.callback(_stop_process, emulator_process) + + # we're specifying -delay-adb so use a trivial command to check when adb is available. + waiter_process = waiter_stack.enter_context( + _start_process( + sdk_tool_paths.adb, + "wait-for-device", + "shell", + "ls /data/local/tmp", + ) + ) + + waiter_stack.callback(_stop_process, waiter_process) + + # poll subprocesses. + # allow 20 minutes for startup as some CIs are slow. + sleep_interval_seconds = 10 + end_time = datetime.datetime.now() + datetime.timedelta(minutes=timeout_minutes) + + while True: + waiter_ret, emulator_ret = waiter_process.poll(), emulator_process.poll() + + if emulator_ret is not None: + # emulator exited early + raise RuntimeError(f"Emulator exited early with return code: {emulator_ret}") + + if waiter_ret is not None: + if waiter_ret == 0: + _log.debug("adb wait-for-device process has completed.") + break + raise RuntimeError(f"Waiter process exited with return code: {waiter_ret}") + + if datetime.datetime.now() > end_time: + raise RuntimeError("Emulator startup timeout") + + time.sleep(sleep_interval_seconds) + + # emulator is started + emulator_stack.pop_all() + + # loop to check for sys.boot_completed being set. + # in theory `-delay-adb` should be enough but this extra check seems to be required to be sure. + while True: + # looping on device with `while` seems to be flaky so loop here and call getprop once + args = [ + sdk_tool_paths.adb, + "shell", + # "while [[ -z $(getprop sys.boot_completed) | tr -d '\r' ]]; do sleep 5; done; input keyevent 82", + "getprop sys.boot_completed", + ] + + _log.debug(f"Starting process - args: {args}") + + getprop_output = subprocess.check_output(args, timeout=10) + getprop_value = bytes.decode(getprop_output).strip() + + if getprop_value == "1": + break + + elif datetime.datetime.now() > end_time: + raise RuntimeError("Emulator startup timeout. sys.boot_completed was not set.") + + _log.debug(f"sys.boot_completed='{getprop_value}'. Sleeping for {sleep_interval_seconds} before retrying.") + time.sleep(sleep_interval_seconds) + + # Verify if the emulator is now running + if not check_emulator_running_using_avd_name(avd_name=avd_name): + raise RuntimeError("Emulator failed to start.") + return emulator_process + + +def check_emulator_running_using_avd_name(avd_name: str) -> bool: + """ + Check if an emulator is running based on the provided AVD name. + :param avd_name: Name of the Android Virtual Device (AVD) to check. + :return: True if an emulator with the given AVD name is running, False otherwise. + """ + try: + # Step 1: List running devices + result = subprocess.check_output(["adb", "devices"], text=True).strip() + _log.info(f"adb devices output:\n{result}") + running_emulators = [line.split("\t")[0] for line in result.splitlines()[1:] if "emulator" in line] + + if not running_emulators: + _log.debug("No emulators running.") + return False # No emulators running + + # Step 2: Check each running emulator's AVD name + for emulator in running_emulators: + try: + avd_info = ( + subprocess.check_output(["adb", "-s", emulator, "emu", "avd", "name"], text=True) + .strip() + .split("\n")[0] + ) + _log.debug(f"AVD name for emulator {emulator}: {avd_info}") + if avd_info == avd_name: + return True + except subprocess.SubprocessError: + _log.warning(f"Error checking AVD name for emulator: {emulator}") + continue # Skip if there's an issue querying a specific emulator + + _log.warning(f"No emulator running with AVD name: {avd_name}") + return False # No matching AVD name found + except subprocess.SubprocessError as e: + _log.warning(f"Error checking emulator status: {e}") + return False + + +def check_emulator_running_using_process(emulator_proc: subprocess.Popen) -> bool: + """Check if the emulator process is running based on a Popen instance.""" + return emulator_proc.poll() is None + + +def check_emulator_running_using_pid(emulator_pid: int) -> bool: + """Check if the emulator process is running based on PID.""" + try: + os.kill(emulator_pid, 0) # Signal 0 checks process existence + return True + except OSError: + return False + + +def stop_emulator_by_proc(emulator_proc: subprocess.Popen, timeout_seconds: int = 120): + """ + Stops the emulator process using a subprocess.Popen instance. + :param emulator_proc: The emulator process as a subprocess.Popen instance. + :param timeout_seconds: Maximum time (in seconds) to wait for the emulator to stop. + """ + if not check_emulator_running_using_process(emulator_proc): + _log.warning("The specified emulator process is not running.") + return + + _log.info("Stopping emulator using subprocess.Popen instance.") + _stop_process(emulator_proc) + + # Wait for the process to stop + interval = 5 + end_time = datetime.datetime.now() + datetime.timedelta(seconds=timeout_seconds) + + while check_emulator_running_using_process(emulator_proc): + if datetime.datetime.now() > end_time: + raise RuntimeError(f"Failed to stop the emulator within the specified timeout = {timeout_seconds} seconds.") + _log.debug("Emulator still running. Checking again in 5 seconds...") + time.sleep(interval) + + _log.info("Emulator stopped successfully.") + + +def stop_emulator_by_pid(emulator_pid: int, timeout_seconds: int = 120): + """ + Stops the emulator process using a PID. + :param emulator_pid: The emulator process PID. + :param timeout_seconds: Maximum time (in seconds) to wait for the emulator to stop. + """ + if not check_emulator_running_using_pid(emulator_pid): + _log.warning(f"No emulator process with PID {emulator_pid} is currently running.") + return + + _log.info(f"Stopping emulator with PID: {emulator_pid}") + _stop_process_with_pid(emulator_pid) + + # Wait for the process to stop + interval = 5 + end_time = datetime.datetime.now() + datetime.timedelta(seconds=timeout_seconds) + + while check_emulator_running_using_pid(emulator_pid): + if datetime.datetime.now() > end_time: + raise RuntimeError( + f"Failed to stop the emulator with PID {emulator_pid} within the specified timeout = {timeout_seconds} seconds." + ) + _log.debug("Emulator still running. Checking again in 5 seconds...") + time.sleep(interval) + + _log.info("Emulator stopped successfully.") + + +def stop_emulator(emulator_proc_or_pid: subprocess.Popen | int, timeout_seconds: int = 120): + """ + Stops the emulator process, checking its running status before and after stopping. + :param emulator_proc_or_pid: The emulator process (subprocess.Popen) or PID (int). + :param timeout_seconds: Maximum time (in seconds) to wait for the emulator to stop. + """ + if isinstance(emulator_proc_or_pid, subprocess.Popen): + stop_emulator_by_proc(emulator_proc_or_pid, timeout_seconds) + elif isinstance(emulator_proc_or_pid, int): + stop_emulator_by_pid(emulator_proc_or_pid, timeout_seconds) + else: + raise ValueError("Expected either a PID or subprocess.Popen instance.") diff --git a/tools/python/util/logger.py b/tools/python/util/logger.py new file mode 100644 index 000000000..d6f302695 --- /dev/null +++ b/tools/python/util/logger.py @@ -0,0 +1,11 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import logging + + +def get_logger(name, level=logging.DEBUG): + logging.basicConfig(format="%(asctime)s %(name)s [%(levelname)s] - %(message)s") + logger = logging.getLogger(name) + logger.setLevel(level) + return logger diff --git a/tools/python/util/platform_helpers.py b/tools/python/util/platform_helpers.py new file mode 100644 index 000000000..bd5006cdf --- /dev/null +++ b/tools/python/util/platform_helpers.py @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import sys + + +def is_windows(): + return sys.platform.startswith("win") + + +def is_macOS(): # noqa: N802 + return sys.platform.startswith("darwin") + + +def is_linux(): + return sys.platform.startswith("linux") diff --git a/tools/python/util/run.py b/tools/python/util/run.py new file mode 100644 index 000000000..b1ebd044f --- /dev/null +++ b/tools/python/util/run.py @@ -0,0 +1,63 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +from __future__ import annotations + +import logging +import os +import shlex +import subprocess + +_log = logging.getLogger("util.run") + + +def run( + *args, + cwd=None, + input=None, + capture_stdout=False, + capture_stderr=False, + shell=False, + env=None, + check=True, + quiet=False, +): + """Runs a subprocess. + + Args: + *args: The subprocess arguments. + cwd: The working directory. If None, specifies the current directory. + input: The optional input byte sequence. + capture_stdout: Whether to capture stdout. + capture_stderr: Whether to capture stderr. + shell: Whether to run using the shell. + env: The environment variables as a dict. If None, inherits the current + environment. + check: Whether to raise an error if the return code is not zero. + quiet: If true, do not print output from the subprocess. + + Returns: + A subprocess.CompletedProcess instance. + """ + cmd = [*args] + + _log.info( + "Running subprocess in '{}'\n {}".format(cwd or os.getcwd(), " ".join([shlex.quote(arg) for arg in cmd])) + ) + + def output(is_stream_captured): + return subprocess.PIPE if is_stream_captured else (subprocess.DEVNULL if quiet else None) + + completed_process = subprocess.run( + cmd, + cwd=cwd, + check=check, + input=input, + stdout=output(capture_stdout), + stderr=output(capture_stderr), + env=env, + shell=shell, + ) + + _log.debug(f"Subprocess completed. Return code: {completed_process.returncode}") + + return completed_process