diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml new file mode 100644 index 00000000..da041657 --- /dev/null +++ b/.github/workflows/benchmarks.yml @@ -0,0 +1,53 @@ +name: Benchmarks + +on: + push: + branches: + - master + - devel + pull_request: + +jobs: + benchmarks: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + + - name: Install build dependencies + run: | + sudo apt-get -y install libunwind-dev binutils-dev libiberty-dev + + - name: Install Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Compile Austin + run: | + autoreconf --install + ./configure --enable-debug-symbols true + make + + - name: Install runtime dependencies + run: | + python3.10 -m venv .venv + source .venv/bin/activate + pip install --upgrade pip + pip install -r scripts/requirements-bm.txt + deactivate + + - name: Run benchmarks + run: | + ulimit -c unlimited + + source .venv/bin/activate + python scripts/benchmark.py | tee benchmarks.txt + deactivate + + # Make it a code comment + sed -e $'1i\\\n~~~' -e $'$a\\\n~~~' benchmarks.txt > comment.txt + + - name: Post results on PR + uses: marocchino/sticky-pull-request-comment@v2 + with: + path: comment.txt diff --git a/.github/workflows/build_arch.yml b/.github/workflows/build_arch.yml index fd663ef6..42a0cf05 100644 --- a/.github/workflows/build_arch.yml +++ b/.github/workflows/build_arch.yml @@ -9,7 +9,7 @@ on: jobs: build-linux-archs: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: arch: ["armv7", "aarch64", "ppc64le"] @@ -18,6 +18,7 @@ jobs: steps: - uses: actions/checkout@v2 name: Checkout sources + - uses: uraimo/run-on-arch-action@v2.0.5 name: Build on ${{ matrix.arch }} id: build-on-arch @@ -28,28 +29,7 @@ jobs: dockerRunArgs: --volume "${GITHUB_WORKSPACE}/artifacts:/artifacts" setup: | mkdir -p ./artifacts - run: | - apt-get update - apt-get -y install autoconf build-essential libunwind-dev binutils-dev libiberty-dev musl-tools zlib1g-dev - - # Build austin - autoreconf --install - ./configure - make - - export VERSION=$(cat src/austin.h | sed -r -n "s/^#define VERSION[ ]+\"(.+)\"/\1/p") - - pushd src - tar -Jcf austin-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz austin - tar -Jcf austinp-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz austinp - - musl-gcc -O3 -Os -s -Wall -pthread *.c -o austin -D__MUSL__ - tar -Jcf austin-$VERSION-musl-linux-${{ matrix.arch }}.tar.xz austin - - mv austin-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz /artifacts - mv austinp-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz /artifacts - mv austin-$VERSION-musl-linux-${{ matrix.arch }}.tar.xz /artifacts - popd + run: ARCH=${{ matrix.arch }} bash scripts/build_arch.sh - name: Show artifacts run: | diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 0ee2f724..9e18cb66 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -9,9 +9,7 @@ on: jobs: check-manpage: - runs-on: ubuntu-latest - strategy: - fail-fast: false + runs-on: ubuntu-20.04 name: Check manpage steps: - uses: actions/checkout@v2 @@ -32,9 +30,7 @@ jobs: run: git diff -I".* DO NOT MODIFY.*" -I"[.]TH AUSTIN.*" --exit-code src/austin.1 cppcheck: - runs-on: ubuntu-latest - strategy: - fail-fast: false + runs-on: ubuntu-20.04 name: Static code analysis steps: - uses: actions/checkout@v2 @@ -43,12 +39,10 @@ jobs: run: sudo apt-get -y install cppcheck - name: Check soure code - run: cppcheck -q -f --error-exitcode=1 src + run: cppcheck -q -f --error-exitcode=1 --inline-suppr src codespell: - runs-on: ubuntu-latest - strategy: - fail-fast: false + runs-on: ubuntu-20.04 name: Codespell steps: - uses: actions/checkout@v2 @@ -64,9 +58,7 @@ jobs: run: codespell -I .github/workflows/wordlist.txt -S "src/python/*" src formatting-tests: - runs-on: ubuntu-latest - strategy: - fail-fast: false + runs-on: ubuntu-20.04 name: Formatting (tests) steps: - uses: actions/checkout@v2 @@ -78,13 +70,27 @@ jobs: - name: Install black run: pip install black - - name: Check source code spelling - run: black --check test/ + - name: Check formatting + run: black --check --exclude=test/targets test/ + + linting-tests: + runs-on: ubuntu-20.04 + name: Linting (tests) + steps: + - uses: actions/checkout@v2 + + - uses: "actions/setup-python@v2" + with: + python-version: "3.10" + + - name: Install flake8 + run: pip install flake8 + + - name: Lint code + run: flake8 --config test/.flake8 test/ coverage: - runs-on: ubuntu-latest - strategy: - fail-fast: false + runs-on: ubuntu-20.04 name: Code coverage steps: - uses: actions/checkout@v2 @@ -101,7 +107,7 @@ jobs: valgrind \ python2.7 \ python3.{5..11} \ - python3.10-full + python3.10-full python3.10-dev - name: Compile Austin run: | @@ -131,9 +137,7 @@ jobs: verbose: true check-cog: - runs-on: ubuntu-latest - strategy: - fail-fast: false + runs-on: ubuntu-20.04 name: Check cog output steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5dc18ecc..a05b1761 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,7 +5,7 @@ on: jobs: push_to_registry: name: Push Docker image to Docker Hub - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Check out the repo uses: actions/checkout@v2 diff --git a/.github/workflows/pre_release.yml b/.github/workflows/pre_release.yml index 56549a47..dee1badd 100644 --- a/.github/workflows/pre_release.yml +++ b/.github/workflows/pre_release.yml @@ -5,7 +5,7 @@ on: - 'v*-*' jobs: release-linux: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: fail-fast: false name: Release (Linux) @@ -124,7 +124,7 @@ jobs: sed -i "" "s/$PREV_VERSION/$VERSION/g" src/austin.h echo "::set-output name=version::$VERSION" - gcc -Wall -O3 -Os -o src/austin src/*.c + gcc-11 -Wall -O3 -Os -o src/austin src/*.c pushd src zip -r austin-${VERSION}-mac64.zip austin diff --git a/.github/workflows/pre_release_arch.yml b/.github/workflows/pre_release_arch.yml index 896f6049..493128e3 100644 --- a/.github/workflows/pre_release_arch.yml +++ b/.github/workflows/pre_release_arch.yml @@ -5,7 +5,7 @@ on: - 'v*-*' jobs: release-linux-archs: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: arch: ["armv7", "aarch64", "ppc64le"] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ff0ba4e..fc5a082c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: - 'v[0-9]+.[0-9]+.[0-9]+' jobs: release-linux: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: fail-fast: false name: Release (Linux) @@ -123,7 +123,7 @@ jobs: export VERSION=$(cat src/austin.h | sed -n -E "s/^#define VERSION[ ]+\"(.+)\"/\1/p") echo "::set-output name=version::$VERSION" - gcc -Wall -O3 -Os -o src/austin src/*.c + gcc-11 -Wall -O3 -Os -o src/austin src/*.c pushd src zip -r austin-${VERSION}-mac64.zip austin diff --git a/.github/workflows/release_arch.yml b/.github/workflows/release_arch.yml index 827df602..78ab404f 100644 --- a/.github/workflows/release_arch.yml +++ b/.github/workflows/release_arch.yml @@ -5,7 +5,7 @@ on: - 'v[0-9]+.[0-9]+.[0-9]+' jobs: release-linux-archs: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 strategy: matrix: arch: ["armv7", "aarch64", "ppc64le"] @@ -14,6 +14,7 @@ jobs: steps: - uses: actions/checkout@v2 name: Checkout sources + - uses: uraimo/run-on-arch-action@v2.0.5 name: Run tests on ${{ matrix.arch }} id: run-tests-on-arch @@ -24,28 +25,7 @@ jobs: dockerRunArgs: --volume "${GITHUB_WORKSPACE}/artifacts:/artifacts" setup: | mkdir -p ./artifacts - run: | - apt-get update - apt-get -y install autoconf build-essential libunwind-dev binutils-dev libiberty-dev musl-tools zlib1g-dev - - # Build austin - autoreconf --install - ./configure - make - - export VERSION=$(cat src/austin.h | sed -r -n "s/^#define VERSION[ ]+\"(.+)\"/\1/p") - - pushd src - tar -Jcf austin-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz austin - tar -Jcf austinp-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz austinp - - musl-gcc -O3 -Os -s -Wall -pthread *.c -o austin -D__MUSL__ - tar -Jcf austin-$VERSION-musl-linux-${{ matrix.arch }}.tar.xz austin - - mv austin-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz /artifacts - mv austinp-$VERSION-gnu-linux-${{ matrix.arch }}.tar.xz /artifacts - mv austin-$VERSION-musl-linux-${{ matrix.arch }}.tar.xz /artifacts - popd + run: ARCH=${{ matrix.arch }} bash scripts/build_arch.sh - name: Show artifacts run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 115ea76c..f7308bf4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,10 +10,36 @@ on: concurrency: group: ${{ github.head_ref || github.run_id }} cancel-in-progress: true + jobs: + build-linux: + runs-on: ubuntu-20.04 + name: Build Austin on Linux + steps: + - uses: actions/checkout@v2 + + - name: Install build dependencies + run: | + sudo apt-get -y install libunwind-dev binutils-dev libiberty-dev + + - name: Compile Austin + run: | + autoreconf --install + ./configure --enable-debug-symbols true + make + + - uses: actions/upload-artifact@v3 + with: + name: austin-binaries + path: | + src/austin + src/austinp + tests-linux: - runs-on: ubuntu-latest - + runs-on: ubuntu-20.04 + + needs: build-linux + strategy: fail-fast: false matrix: @@ -26,9 +52,12 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Install build dependencies - run: | - sudo apt-get -y install libunwind-dev binutils-dev libiberty-dev + - uses: actions/download-artifact@v3 + with: + name: austin-binaries + path: src + + - run: chmod +x src/austin && chmod +x src/austinp - name: Install test dependencies run: | @@ -46,12 +75,6 @@ jobs: with: python-version: "3.10" - - name: Compile Austin - run: | - autoreconf --install - ./configure --enable-debug-symbols true - make - - name: Run tests run: | ulimit -c unlimited @@ -63,8 +86,41 @@ jobs: .venv/bin/pytest --pastebin=failed --no-flaky-report -sr a deactivate + build-osx-gcc: + runs-on: macos-latest + + name: Build Austin on macOS (gcc) + steps: + - uses: actions/checkout@v2 + + - name: Compile Austin + run: gcc-11 -Wall -Werror -O3 -g src/*.c -o src/austin + + - uses: actions/upload-artifact@v3 + with: + name: austin-binary + path: | + src/austin + + build-osx-clang: + runs-on: macos-latest + + name: Build Austin on macOS (clang) + steps: + - uses: actions/checkout@v2 + + - name: Install automake + run: brew install automake + + - run: | + autoreconf --install + ./configure + make + tests-osx: runs-on: macos-latest + + needs: [build-osx-gcc, build-osx-clang] strategy: fail-fast: false @@ -77,8 +133,13 @@ jobs: name: Tests on macOS with Python ${{ matrix.python-version }} steps: - uses: actions/checkout@v2 - - name: Compile Austin - run: gcc -Wall -Werror -O3 -g src/*.c -o src/austin + + - uses: actions/download-artifact@v3 + with: + name: austin-binary + path: src + + - run: chmod +x src/austin - name: Install Python uses: actions/setup-python@v4 @@ -105,9 +166,30 @@ jobs: sudo -E pytest --ignore=test/cunit --pastebin=failed --no-flaky-report -sr a deactivate + build-win: + runs-on: windows-latest + + name: Build Austin on Windows + steps: + - uses: actions/checkout@v2 + + - name: Compile Austin + run: | + gcc.exe --version + gcc.exe -O3 -g -o src/austin.exe src/*.c -lpsapi -lntdll -Wall -Werror + src\austin.exe --help + + - uses: actions/upload-artifact@v3 + with: + name: austin-binary + path: | + src/austin.exe + tests-win: runs-on: windows-latest + needs: build-win + strategy: fail-fast: false matrix: @@ -120,11 +202,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Compile Austin - run: | - gcc.exe --version - gcc.exe -O3 -g -o src/austin.exe src/*.c -lpsapi -lntdll -Wall -Werror - src\austin.exe --help + - uses: actions/download-artifact@v3 + with: + name: austin-binary + path: src - name: Install Python uses: actions/setup-python@v4 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7af07a79..00000000 --- a/.travis.yml +++ /dev/null @@ -1,208 +0,0 @@ -language: c -python: - - 2.7 - - 3.3 - - 3.4 - - 3.5 - - 3.6 - - 3.8 - - 3.9 - -compiler: - - gcc - -git: - depth: false - -osx_image: xcode11.4 - -os: linux -dist: bionic - -jobs: - include: - # Linux - - env: TARGET=x86_64-unknown-linux-gnu - arch: amd64 - - env: TARGET=powerpc64le-unknown-linux-gnu - arch: ppc64le - # - env: TARGET=arm64-unknown-linux-gnueabi - # arch: arm64 - - # OSX - - env: TARGET=x86_64-apple-darwin - os: osx - - # Windows - - env: TARGET=x86_64-pc-windows-gnu - os: windows - -before_script: - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; - then - sudo add-apt-repository ppa:deadsnakes/ppa -y; - sudo add-apt-repository ppa:duggan/bats -y; - - sudo apt-get install -y bats valgrind python2.{3..7} python3.{3..10}; - - autoreconf --install; - fi - - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; - then - git clone --depth=1 https://github.com/bats-core/bats-core.git; - fi - # brew install --HEAD valgrind || true; - - - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; - then - powershell Install-WindowsFeature Net-Framework-Core; - cinst -y wixtoolset; - fi - -script: - - echo $TRAVIS_OS_NAME -- $TARGET - - - if [[ "$TARGET" == "x86_64-unknown-linux-gnu" ]]; - then - ./configure && - make && - sudo make check; - - test -f /tmp/austin_tests.log && cat /tmp/austin_tests.log; - fi - - # - if [[ "$TARGET" == "arm64-unknown-linux-gnueabi" ]]; - # then - # gcc -O3 -Os -Wall -pthread src/*.c -o src/austin; - # which bats && sudo bats test/test.bats || src/austin -V; - # fi - - - if [[ "$TARGET" == "powerpc64le-unknown-linux-gnu" ]]; - then - ./configure && - make; - which bats && sudo bats test/test.bats || src/austin -V; - fi - - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; - then - gcc -Wall -O3 -Os -o src/austin src/*.c && - sudo bats-core/bin/bats test/macos/test.bats; - - test -f /tmp/austin_tests.log && cat /tmp/austin_tests.log; - fi - - - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; - then - gcc -s -Wall -O3 -Os -o src/austin src/*.c -lpsapi -lntdll; - fi - -after_success: - ./src/austin -V; - ./src/austin --usage - -after_failure: - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; - then - test -f /var/log/syslog.log && cat /var/log/syslog.log | grep austin; - test -f test-suite.log && cat test-suite.log; - fi - -before_deploy: - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; - then - SED_FLAGS="-n -E"; - else - SED_FLAGS="-r -n"; - fi; - export VERSION=$(cat src/austin.h | sed $SED_FLAGS "s/.*VERSION[ ]+\"(.+)\"/\1/p"); - - # - export TRAVIS_TAG=v$VERSION - - - echo "==== Preparing to create GitHub Release for version $VERSION ====" - - - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; - then - export ZIP_CMD="tar -Jcf"; - export ZIP_SUFFIX="linux-${TARGET%%-*}.tar.xz"; - export AUSTIN_EXE=austin; - fi - - - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; - then - export ZIP_CMD="zip -r"; - export ZIP_SUFFIX="mac-${TARGET%%-*}.zip"; - export AUSTIN_EXE=austin; - fi - - - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; - then - export ZIP_CMD="7z a -tzip"; - export ZIP_SUFFIX="win-${TARGET%%-*}.zip"; - export AUSTIN_EXE=austin.exe; - - export WIN_MSI="austin-$VERSION-win64.msi"; - - git checkout "packaging/msi"; - git checkout master; - git checkout "packaging/msi" -- wix; - - sed -i "s/%VERSION%/$VERSION/g" wix/Austin.wxs; - export PATH="/c/Program Files (x86)/`ls /c/Program\ Files\ \(x86\) | grep \"[wW]i[xX] [tT]oolset\"`/bin:$PATH"; - cd wix; - candle Austin.wxs -out Austin.wixobj; - light -ext WixUIExtension Austin.wixobj -out $WIN_MSI; - cd .. ; - mv wix/$WIN_MSI src/$WIN_MSI; - test -f src/$WIN_MSI && echo ">> Windows MSI installer at src/$WIN_MSI" || echo ">> ERROR No Windows MSI installer generated."; - fi - - - export ARTEFACT="austin-${VERSION}-${ZIP_SUFFIX}" - - - echo ">> Using command $ZIP_CMD to create artefact $ARTEFACT" - - - cd src - - $ZIP_CMD $ARTEFACT $AUSTIN_EXE - - echo ">> Generated artefact" $(ls $ARTEFACT) - - - git config --local user.name "Gabriele N. Tornetta" - - git config --local user.email ${GITHUB_EMAIL} - - git tag -a -f -m "Release $VERSION" $TRAVIS_TAG - -deploy: - - provider: releases - edge: true - token: $GITHUB_TOKEN - file: - - $ARTEFACT - - $WIN_MSI - overwrite: true - on: - tags: true - - # - provider: releases - # edge: true - # token: $GITHUB_TOKEN - # file: $WIN_MSI - # overwrite: true - # on: - # condition: "$TRAVIS_OS_NAME = windows" - -after_deploy: - - cd .. - - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; - then - export WIN_MSI_HASH=$( sha256sum src/$WIN_MSI | head -c 64 ); - - git checkout "packaging/msi" -- choco; - - cd choco; - - sed -i "s/%WIN_MSI_HASH%/$WIN_MSI_HASH/g" tools/chocolateyinstall.ps1; - /bin/find . -type f -exec sed -i "s/%VERSION%/$VERSION/g" {} \; ; - choco apikey --key $CHOCO_APIKEY --source https://push.chocolatey.org/; - choco pack; - choco push; - cd .. ; - fi diff --git a/ChangeLog b/ChangeLog index 0bd6370a..7b182165 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,15 @@ +2023-02-21 v3.5.0 + + Added support for fine-grained, column-level location information when + exporting samples in binary mode. + + Improved multiprocess support. + + Dropped the alternative output format. + + Improved interpreter detection on all supported platforms. + + 2022-10-26 v3.4.1 Fixed a bug with the MOJO binary format. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 6951bc02..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include austin/html/* diff --git a/README.md b/README.md index 7b56c046..bef4d226 100644 --- a/README.md +++ b/README.md @@ -312,7 +312,6 @@ Austin is a frame stack sampler for CPython that is used to extract profiling data out of a running Python process (and all its children, if required) that requires no instrumentation and has practically no impact on the tracee. - -a, --alt-format Alternative collapsed stack sample format. -b, --binary Emit data in the MOJO binary format. See https://github.com/P403n1x87/austin/wiki/The-MOJO-file-format for more details. @@ -378,14 +377,6 @@ In normal mode, the `[frame]` part of each emitted sample has the structure [frame] := :: ~~~ -If you want the flame graph to show the total time spent in each function, plus -the finer detail of the time spent on each line, you can use the alternative -format by passing the `-a` option. In this mode, `[frame]` has the structure - -~~~ -[frame] := :;L -~~~ - Each line then ends with a single `[metric]`, i.e. the sampling time measured in microseconds. @@ -412,6 +403,16 @@ More details about the [MOJO] binary format can be found in the [Wiki]. *Since Austin 3.4.0*. +## Column-level Location Information + +Since Python 3.11, code objects carry finer-grained location information at the +column level. When using the binary MOJO format, Austin can extract this extra +location information when profiling code running with versions of the +interpreter that expose this data. + +*Since Austin 3.5.0*. + + ## Memory and Full Metrics When profiling in memory mode with the `-m` or `--memory` switch, the metric diff --git a/art/cheatsheet.png b/art/cheatsheet.png deleted file mode 100644 index 9f16fb5d..00000000 Binary files a/art/cheatsheet.png and /dev/null differ diff --git a/configure.ac b/configure.ac index 19a8804a..8cda77ff 100644 --- a/configure.ac +++ b/configure.ac @@ -6,7 +6,7 @@ AC_PREREQ([2.69]) # from scripts.utils import get_current_version_from_changelog as version # print(f"AC_INIT([austin], [{version()}], [https://github.com/p403n1x87/austin/issues])") # ]]] -AC_INIT([austin], [3.4.1], [https://github.com/p403n1x87/austin/issues]) +AC_INIT([austin], [3.5.0], [https://github.com/p403n1x87/austin/issues]) # [[[end]]] AC_CONFIG_SRCDIR([config.h.in]) AC_CONFIG_HEADERS([config.h]) diff --git a/doc/cheatsheet.pdf b/doc/cheatsheet.pdf index f99c86ad..ceb0183a 100644 Binary files a/doc/cheatsheet.pdf and b/doc/cheatsheet.pdf differ diff --git a/doc/cheatsheet.png b/doc/cheatsheet.png index 817e4a67..b66f0831 100644 Binary files a/doc/cheatsheet.png and b/doc/cheatsheet.png differ diff --git a/doc/cheatsheet.svg b/doc/cheatsheet.svg index dc80e938..ed54a09a 100644 --- a/doc/cheatsheet.svg +++ b/doc/cheatsheet.svg @@ -2,31 +2,31 @@ + inkscape:export-ydpi="191.99664" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:dc="http://purl.org/dc/elements/1.1/"> @@ -58,9 +58,9 @@ @@ -198,9 +198,9 @@ @@ -220,9 +220,9 @@ inkscape:collect="always" /> @@ -757,16 +757,17 @@ inkscape:pageopacity="1" inkscape:pageshadow="2" inkscape:zoom="1.4" - inkscape:cx="232.52235" - inkscape:cy="1140.005" + inkscape:cx="355.35714" + inkscape:cy="-3878.5714" inkscape:document-units="mm" - inkscape:current-layer="layer3" + inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="2560" inkscape:window-height="1413" inkscape:window-x="0" inkscape:window-y="328" - inkscape:window-maximized="1" /> + inkscape:window-maximized="1" + inkscape:pagecheckerboard="0" /> @@ -775,7 +776,6 @@ image/svg+xml - @@ -783,10 +783,10 @@ inkscape:groupmode="layer" id="layer6" inkscape:label="Background" - style="display:none"> + style="display:inline"> + style="opacity:0.03"> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:none;stroke:#000000;stroke-width:4.23333;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.15888px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + style="opacity:1;fill:#ff6600;fill-opacity:0.0230263;fill-rule:nonzero;stroke:none;stroke-width:2.82222;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> austin + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:213.333px;font-family:'Action Is';-inkscape-font-specification:'Action Is';text-align:start;text-anchor:start;fill:url(#linearGradient1290);fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="flowPara847">austin + austin + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:213.333px;font-family:'Action Is';-inkscape-font-specification:'Action Is';text-align:start;text-anchor:start;fill:url(#linearGradient1290);fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">austin + austin + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:213.333px;font-family:'Action Is';-inkscape-font-specification:'Action Is';text-align:start;text-anchor:start;fill:url(#linearGradient2500);fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">austin + + + + id="text839" /> LAUNCH + x="6.3167839" + y="91.305237" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.35px;font-family:'action is';-inkscape-font-specification:'action is';text-align:start;letter-spacing:0px;text-anchor:start;fill:#5c5c5c;fill-opacity:1;stroke-width:0.264583">LAUNCH OUTPUT - FORMAT - + d="m 110.71987,147.3808 h 95.28158" + style="fill:none;stroke:#236693;stroke-width:1.05833;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> Mode + d="m 110.56476,36.884578 h 95.28158" + style="fill:none;stroke:#236693;stroke-width:1.05833;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> TOOLS - Start a script + d="m 110.77157,231.78465 h 95.28158" + style="fill:none;stroke:#236693;stroke-width:1.05833;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> - + id="g7283" + transform="translate(-0.22647449,8.9958338)"> + + Start a script + + + austin python myscript.py + + + + + + Start an executable script + + + austin ./myscript.py + + + + + + Start a module + + + austin python -m mymodule + + + + + + Attach to a running process* + + + austin -p 123 + + + + + + Attach to child processes + + + austin -C python my_multiproc_script.py + + + + austin -Cp 123 + + + + + + Set sampling interval + + austin -p 123 -i 10ms + + + + Set start-up timeout (on slow machines) + + + austin python myscript.py + id="tspan1542" + style="font-weight:bold;fill:#eeff81;fill-opacity:1;stroke-width:0.264583">-p 123 -t 1s + + + Wall clock time + + austin python myscript.py - Start an executable script + id="g8575" + transform="translate(0.1651062,5.2392316)"> + CPU time + x="110.08945" + y="53.356865" /> austin ./myscript.py + id="tspan1783" + x="111.5863" + y="57.51564" + style="fill:#f0f0f0;fill-opacity:1;stroke-width:0.264583">austin -s python myscript.py - Start a module + id="g8656" + transform="translate(0,-5.7165681)"> + Wall clock time and garbage collector + austin -g python myscript.py + + + All metrics + + id="rect1821" + style="display:inline;opacity:1;fill:#404040;fill-opacity:0.501961;fill-rule:nonzero;stroke:none;stroke-width:0.897307;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> austin python -m mymodule + id="tspan1823" + style="font-weight:bold;fill:#eeff81;fill-opacity:1">-f python -m mymodule - Attach to a running process* + id="g8632" + transform="translate(0.10340131,-2.6458415)"> + All metrics and garbage collector + x="109.94434" + y="129.90771" /> austin -p 123 + id="tspan1837" + x="111.44119" + y="134.06651" + style="fill:#f0f0f0;fill-opacity:1;stroke-width:0.264583">austin -fg -p 123 - Attach to child processes + id="g8664" + transform="translate(0.21680686,3.1750001)"> + Emit to STDOUT (Python STDOUT suppressed) + id="rect1845" + style="display:inline;opacity:1;fill:#404040;fill-opacity:0.501961;fill-rule:nonzero;stroke:none;stroke-width:0.897307;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> austin -C python my_multiproc_script.py + style="fill:#f0f0f0;fill-opacity:1;stroke-width:0.264583" + y="156.37239" + x="111.58628" + id="tspan1849" + sodipodi:role="line">austin python -m mymodule + id="g8683" + transform="translate(0.26850751,5.612626)"> + Pipe to other tools (Python STDOUT suppressed) + x="110.08945" + y="177.61932" /> + austin -P ./myscript.py | ./flamegraph.pl > fg.svg + + + + Exposure + + austin -Cp 123 + y="176.20978" + style="fill:#f0f0f0;fill-opacity:1;stroke-width:0.264583">austin -x 3s ./myscript.py Set sampling interval - - austin -p 123 -i 10ms - Set start-up timeout (on slow machines) - - austin -p 123 -t 1s - Wall clock time - - austin python myscript.py - CPU time - - austin -s python myscript.py - Wall clock time and garbage collector - - austin -g python myscript.py - All metrics - - austin -f python -m mymodule - All metrics and garbage collector - - austin -fg -p 123 - Emit to STDOUT (Python STDOUT suppressed) - - austin python -m mymodule - Pipe to other tools (Python STDOUT suppressed) - - austin -P ./myscript.py | ./flamegraph.pl > fg.svg - Default - - austin ./myscript.py - Alternative - - austin -ap 123 - - - foomodule:foo:42 - foomodule:foo:43 - - barmodule:bar:13 - - - - - - foomodule:foo - - barmodule:bar - - - - - L42 - L43 - - L13 - Exposure - - austin -x 3s ./myscript.py - Supported interpreters + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:3.88056px;font-family:Ubuntu;-inkscape-font-specification:'Ubuntu Bold';fill:#e6f0ff;fill-opacity:1;stroke-width:0.264583">Supported interpreters 2.3 thru 2.7 and 3.3 thru 3.11 INSTALL - - - brew install austin - + d="M 6.4569346,37.336736 H 101.73851" + style="fill:none;stroke:#236693;stroke-width:1.05833;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> - - + + + choco install austin + + - - + id="g2144" + transform="translate(0.37952,5.8208336)"> @@ -6604,14 +6402,14 @@ height="6.4413304" width="95.000885" id="rect9568" - style="display:inline;opacity:1;fill:#404040;fill-opacity:0.50196078;fill-rule:nonzero;stroke:none;stroke-width:0.89730716;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="display:inline;opacity:1;fill:#404040;fill-opacity:0.501961;fill-rule:nonzero;stroke:none;stroke-width:0.897307;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + transform="translate(0.45526937,8.4666709)"> @@ -6692,14 +6490,14 @@ height="6.4413304" width="95.000885" id="rect9582" - style="display:inline;opacity:1;fill:#404040;fill-opacity:0.50196078;fill-rule:nonzero;stroke:none;stroke-width:0.89730716;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + style="display:inline;opacity:1;fill:#404040;fill-opacity:0.501961;fill-rule:nonzero;stroke:none;stroke-width:0.897307;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> - + CPU time and garbage collector - - CPU time and garbage collector + + austin austin -sg python myscript.py - + + Redirect to file Redirect to file (Python STDOUT suppressed) - - (Python STDOUT suppressed) + + austin -p 123 austin -p 123 > /path/to/samples.austin - + + Emit to file Emit to file (Python STDOUT preserved) - - (Python STDOUT preserved) + + austin austin -o /path/to/samples.austin -p 123 - Austin TUI - + + transform="translate(0.53101874,7.4083337)"> conda install -c conda-forge austin @@ -6921,190 +6713,216 @@ inkscape:connector-curvature="0" style="display:inline;opacity:0.5;fill:#e6f0ff;fill-opacity:1;stroke-width:0.00417808" /> - pipx install austin-tui - Austin VS Code - - - - - - - - - - + id="g8603" + transform="translate(0.49115219,-2.6458334)"> + Austin VS Code + + + + + + + + + + + + + - - - + code --install-extension p403n1x87.austin-vscode - - - - - - - - - + style="display:inline;opacity:0.5;fill:#e6f0ff;fill-opacity:1;stroke-width:0.0476616" /> + + + Austin TUI + + pipx install austin-tui + + + + + + + + + + + + + d="m 197.98528,247.45198 c -0.22646,0.21976 -0.4763,0.18549 -0.71445,0.0819 -0.25319,-0.1057 -0.48466,-0.11239 -0.75206,0 -0.33299,0.14372 -0.50972,0.10195 -0.71027,-0.0819 -1.13226,-1.1657 -0.96514,-2.94139 0.32171,-3.00822 0.3121,0.0166 0.53062,0.17255 0.71445,0.1855 0.27325,-0.0556 0.5348,-0.21476 0.82727,-0.19388 0.35137,0.0284 0.61417,0.16713 0.78965,0.41657 -0.72281,0.43452 -0.5515,1.38713 0.11239,1.65451 -0.13287,0.34887 -0.30333,0.69357 -0.58911,0.94843 z m -1.16568,-3.02076 c -0.0339,-0.51808 0.38605,-0.94424 0.86904,-0.98603 0.0664,0.59746 -0.54315,1.04452 -0.86904,0.98603 z" + id="path2076" /> + d="m 199.785,244.0363 1.70092,-0.23165 5.9e-4,1.64064 -1.70008,0.01 z m 1.7001,1.59804 9.2e-4,1.6421 -1.70008,-0.23373 -1e-4,-1.41936 z m 0.20619,-1.85999 2.25526,-0.32915 v 1.97924 l -2.25526,0.0179 z m 2.25579,1.87543 -5.1e-4,1.97032 -2.25525,-0.3183 -0.003,-1.6557 z" + id="path2078" /> - - p403n1x87/austin + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.175px;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono Bold';fill:#e6f0ff;fill-opacity:1;stroke-width:0.264583">p403n1x87/austin Report issues at + + + + brew install austin + + + https://github.com/P403n1x87/austin/issues - https://github.com/P403n1x87/austin/issues + + Set heap size (for more accurate results) - - Set heap size (for more accurate results) + + austin austin -h 512 python -m mymodule - Where?* - - + + + Where?* + + + austin austin -w 123 + * requires superuser capabilities on Linux - Memory - - austin * requires superuser capabilities on Linux + + Memory + + austin -m python myscript.py - -m python myscript.py + + + Emit to file in binary (MOJO) format Emit to file in binary (MOJO) format (STDOUT preserved) - - + austin austin -bo /path/to/samples.austin -p 123 + AUSTIN + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:12.7px;font-family:'action is';-inkscape-font-specification:'action is';text-align:start;letter-spacing:0px;text-anchor:start;fill:#ffe8e1;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1">AUSTIN CHEATSHEET + style="font-weight:bold;font-size:5.57082px;text-align:center;letter-spacing:0px;text-anchor:middle;fill:#ffe8e1;fill-opacity:1;stroke-width:0.232117">CHEATSHEET for version 3.4 + sodipodi:role="line">for version 3.5 + + id="text1402" /> AUSTIN + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.7658px;font-family:'action is';-inkscape-font-specification:'action is';text-align:start;letter-spacing:0px;text-anchor:start;fill:#e6f0ff;fill-opacity:1;stroke:none;stroke-width:0.286788;stroke-opacity:1">AUSTIN + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:3.52778px;line-height:0;font-family:'Ubuntu Mono';-inkscape-font-specification:'Ubuntu Mono';text-align:end;letter-spacing:7.18873px;word-spacing:0px;text-anchor:end;display:inline;fill:#e6f0ff;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-opacity:1" + xml:space="preserve" /> + Frame stack sampler for CPython + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;line-height:1;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;text-anchor:start;fill:#e6f0ff;fill-opacity:1">Frame stack sampler for CPython + + style="opacity:0.142"> dict: - meta = metadata(output) - raw_saturation = meta["saturation"] - _, _, raw_samples = raw_saturation.partition("/") +def get_stats(output: str) -> t.Optional[dict]: + try: + meta = metadata(output) + raw_saturation = meta["saturation"] + _, _, raw_samples = raw_saturation.partition("/") + + duration = float(meta["duration"]) / 1e6 + samples = int(raw_samples) + saturation = eval(raw_saturation) + error_rate = eval(meta["errors"]) + sampling = int(meta["sampling"].split(",")[1]) - duration = float(meta["duration"]) / 1e6 - samples = int(raw_samples) - saturation = eval(raw_saturation) - error_rate = eval(meta["errors"]) - sampling = int(meta["sampling"].split(",")[1]) + return { + "Sample Rate": samples / duration, + "Saturation": saturation, + "Error Rate": error_rate, + "Sampling Speed": sampling, + } - return { - "Sample Rate": samples / duration, - "Saturation": saturation, - "Error Rate": error_rate, - "Sampling Speed": sampling, - } + except Exception: + # Failed to get stats + return None def download_release(version: str, dest: Path, variant_name: str = "austin") -> Variant: @@ -152,12 +167,20 @@ def render(table): def main(): argp = ArgumentParser() + argp.add_argument( "-k", type=re.compile, help="Run benchmark scenarios that match the given regular expression", ) + argp.add_argument( + "-n", + type=int, + default=10, + help="Number of times to run each scenario", + ) + opts = argp.parse_args() print( @@ -180,7 +203,11 @@ def main(): print(f"WARNING: Could not download {variant} {version}") continue - stats = [get_stats(austin(*args).stdout) for _ in range(10)] + stats = [ + _ + for _ in (get_stats(austin(*args).stdout) for _ in range(opts.n)) + if _ is not None + ] table.append( ( version, diff --git a/scripts/build_arch.sh b/scripts/build_arch.sh new file mode 100644 index 00000000..92662475 --- /dev/null +++ b/scripts/build_arch.sh @@ -0,0 +1,34 @@ +#!/bin/bash -eu + +set -e +set -u + +# Install build dependencies +apt-get update +apt-get -y install \ + autoconf \ + build-essential \ + libunwind-dev \ + binutils-dev \ + libiberty-dev \ + musl-tools \ + zlib1g-dev + +# Build Austin +autoreconf --install +./configure +make + +export VERSION=$(cat src/austin.h | sed -r -n "s/^#define VERSION[ ]+\"(.+)\"/\1/p") + +pushd src + tar -Jcf austin-$VERSION-gnu-linux-$ARCH.tar.xz austin + tar -Jcf austinp-$VERSION-gnu-linux-$ARCH.tar.xz austinp + + musl-gcc -O3 -Os -s -Wall -pthread *.c -o austin -D__MUSL__ + tar -Jcf austin-$VERSION-musl-linux-$ARCH.tar.xz austin + + mv austin-$VERSION-gnu-linux-$ARCH.tar.xz /artifacts + mv austinp-$VERSION-gnu-linux-$ARCH.tar.xz /artifacts + mv austin-$VERSION-musl-linux-$ARCH.tar.xz /artifacts +popd diff --git a/scripts/requirements-bm.txt b/scripts/requirements-bm.txt new file mode 100644 index 00000000..edc3cd39 --- /dev/null +++ b/scripts/requirements-bm.txt @@ -0,0 +1 @@ +austin-python~=1.4.1 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index c282ab82..efcb4d79 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -4,7 +4,7 @@ base: core20 # from scripts.utils import get_current_version_from_changelog as version # print(f"version: '{version()}+git'") # ]]] -version: '3.4.1+git' +version: '3.5.0+git' # [[[end]]] summary: A Python frame stack sampler for CPython description: | diff --git a/src/Makefile.am b/src/Makefile.am index 8e0e6d02..380bb12d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -20,7 +20,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -AM_CFLAGS = -I$(srcdir) -Wall -Werror -pthread +AM_CFLAGS = -I$(srcdir) -Wall -Werror -Wno-unused-command-line-argument -pthread OPT_FLAGS = -O3 STRIP_FLAGS = -Os -s diff --git a/src/argparse.c b/src/argparse.c index 53b36399..ebe7cf9f 100644 --- a/src/argparse.c +++ b/src/argparse.c @@ -45,11 +45,10 @@ #else #define DEFAULT_SAMPLING_INTERVAL 100 #endif -#define DEFAULT_INIT_RETRY_CNT 100 -#define DEFAULT_HEAP_SIZE 0 +#define DEFAULT_INIT_TIMEOUT_MS 500 // 0.5 seconds +#define DEFAULT_HEAP_SIZE 0 const char SAMPLE_FORMAT_NORMAL[] = ";%s:%s:%d"; -const char SAMPLE_FORMAT_ALTERNATIVE[] = ";%s:%s;L%d"; const char SAMPLE_FORMAT_WHERE[] = " \033[33;1m%2$s\033[0m (\033[36;1m%1$s\033[0m:\033[32;1m%3$d\033[0m)\n"; #ifdef NATIVE const char SAMPLE_FORMAT_WHERE_NATIVE[]= " \033[38;5;246m%2$s\033[0m (\033[38;5;248;1m%1$s\033[0m:\033[38;5;246m%3$d\033[0m)\n"; @@ -68,7 +67,7 @@ const char HEAD_FORMAT_WHERE[] = "\n\n%3$s%4$s Process \033[35;1m%1$d\03 // Globals for command line arguments parsed_args_t pargs = { /* t_sampling_interval */ DEFAULT_SAMPLING_INTERVAL, - /* timeout */ DEFAULT_INIT_RETRY_CNT * 1000, + /* timeout */ DEFAULT_INIT_TIMEOUT_MS * 1000, /* attach_pid */ 0, /* where */ 0, /* exclude_empty */ 0, @@ -220,10 +219,6 @@ static struct argp_option options[] = { "timeout", 't', "n_ms", 0, "Start up wait time in milliseconds (default is 100). Accepted units: s, ms." }, - { - "alt-format", 'a', NULL, 0, - "Alternative collapsed stack sample format." - }, { "exclude-empty",'e', NULL, 0, "Do not output samples of threads with no frame stacks." @@ -340,13 +335,6 @@ parse_opt (int key, char *arg, struct argp_state *state) pargs.timeout *= 1000; break; - case 'a': - pargs.format = (char *) SAMPLE_FORMAT_ALTERNATIVE; - #ifdef NATIVE - pargs.native_format = pargs.format; - #endif - break; - case 'b': pargs.binary = 1; break; @@ -574,7 +562,6 @@ print(";") "data out of a running Python process (and all its children, if required) that\n" "requires no instrumentation and has practically no impact on the tracee.\n" "\n" -" -a, --alt-format Alternative collapsed stack sample format.\n" " -b, --binary Emit data in the MOJO binary format. See\n" " https://github.com/P403n1x87/austin/wiki/The-MOJO-file-format\n" " for more details.\n" @@ -615,12 +602,12 @@ for line in check_output(["src/austin", "--usage"]).decode().splitlines(): print(f'"{line}\\n"') print(";") ]]]*/ -"Usage: austin [-abCefgmPs?V] [-h n_mb] [-i n_us] [-o FILE] [-p PID] [-t n_ms]\n" -" [-w PID] [-x n_sec] [--alt-format] [--binary] [--children]\n" -" [--exclude-empty] [--full] [--gc] [--heap=n_mb] [--interval=n_us]\n" -" [--memory] [--output=FILE] [--pid=PID] [--pipe] [--sleepless]\n" -" [--timeout=n_ms] [--where=PID] [--exposure=n_sec] [--help]\n" -" [--usage] [--version] command [ARG...]\n" +"Usage: austin [-bCefgmPs?V] [-h n_mb] [-i n_us] [-o FILE] [-p PID] [-t n_ms]\n" +" [-w PID] [-x n_sec] [--binary] [--children] [--exclude-empty]\n" +" [--full] [--gc] [--heap=n_mb] [--interval=n_us] [--memory]\n" +" [--output=FILE] [--pid=PID] [--pipe] [--sleepless] [--timeout=n_ms]\n" +" [--where=PID] [--exposure=n_sec] [--help] [--usage] [--version]\n" +" command [ARG...]\n" ; /*[[[end]]]*/ @@ -694,10 +681,6 @@ cb(const char opt, const char * arg) { } pargs.timeout *= 1000; break; - - case 'a': - pargs.format = (char *) SAMPLE_FORMAT_ALTERNATIVE; - break; case 'b': pargs.binary = 1; diff --git a/src/austin.1 b/src/austin.1 index b12715e9..656fe422 100644 --- a/src/austin.1 +++ b/src/austin.1 @@ -1,5 +1,5 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.49.1. -.TH AUSTIN "1" "October 2022" "austin 3.4.1" "User Commands" +.TH AUSTIN "1" "February 2023" "austin 3.5.0" "User Commands" .SH NAME austin \- Frame stack sampler for CPython .SH SYNOPSIS @@ -10,9 +10,6 @@ Austin is a frame stack sampler for CPython that is used to extract profiling data out of a running Python process (and all its children, if required) that requires no instrumentation and has practically no impact on the tracee. .TP -\fB\-a\fR, \fB\-\-alt\-format\fR -Alternative collapsed stack sample format. -.TP \fB\-b\fR, \fB\-\-binary\fR Emit data in the MOJO binary format. See https://github.com/P403n1x87/austin/wiki/The\-MOJO\-file\-format diff --git a/src/austin.c b/src/austin.c index c1f9d73f..a584243b 100644 --- a/src/austin.c +++ b/src/austin.c @@ -120,8 +120,8 @@ do_single_process(py_proc_t * py_proc) { // ---------------------------------------------------------------------------- void do_child_processes(py_proc_t * py_proc) { - py_proc_list_t * list = py_proc_list_new(py_proc); - if (list == NULL) + cu_py_proc_list_t * list = py_proc_list_new(py_proc); + if (!isvalid(list)) return; // If the parent process is not a Python process, its children might be, so we @@ -150,7 +150,7 @@ do_child_processes(py_proc_t * py_proc) { set_error(EPROCNOCHILDREN); if (pargs.attach_pid == 0) py_proc__terminate(py_proc); - goto release; + return; } } else { @@ -208,9 +208,6 @@ do_child_processes(py_proc_t * py_proc) { py_proc_list__update(list); py_proc_list__wait(list); } - -release: - py_proc_list__destroy(list); } /* do_child_processes */ diff --git a/src/austin.h b/src/austin.h index c1b40672..d26722ce 100644 --- a/src/austin.h +++ b/src/austin.h @@ -32,7 +32,7 @@ from scripts.utils import get_current_version_from_changelog as version print(f'#define VERSION "{version()}"') ]]] */ -#define VERSION "3.4.1" +#define VERSION "3.5.0" // [[[end]]] #endif diff --git a/src/cache.c b/src/cache.c index 5cc8939a..09c0469b 100644 --- a/src/cache.c +++ b/src/cache.c @@ -227,6 +227,7 @@ hash_table_new(int capacity) { hash_table_t *hash = (hash_table_t *) calloc(1, sizeof(hash_table_t)); hash->capacity = capacity; + hash->load_factor = 0.75 * capacity; hash->chains = (chain_t **) calloc(hash->capacity, sizeof(chain_t *)); #ifdef DEBUG @@ -259,7 +260,15 @@ hash_table__get(hash_table_t *self, key_dt key) { } // ---------------------------------------------------------------------------- +int +hash_table__is_full(hash_table_t *self) { + if (!isvalid(self)) + return -1; + + return self->size >= self->load_factor; +} +// ---------------------------------------------------------------------------- void hash_table__set(hash_table_t *self, key_dt key, value_t value) { if (!isvalid(self)) @@ -453,3 +462,81 @@ lru_cache__destroy(lru_cache_t *self) { free(self); } + + +// -- Lookup ------------------------------------------------------------------ + +// ---------------------------------------------------------------------------- +lookup_t * +lookup_new(int size) { + lookup_t *lookup = (lookup_t *)calloc(1, sizeof(lookup_t)); + if (!isvalid(lookup)) + return NULL; + + lookup->hash = hash_table_new(size); + + return lookup; +} + +// ---------------------------------------------------------------------------- +value_t +lookup__get(lookup_t *self, key_dt key) { + if (!isvalid(self)) + return NULL; + + return hash_table__get(self->hash, key); +} + +// ---------------------------------------------------------------------------- +void +lookup__set(lookup_t *self, key_dt key, value_t value) { + if (!isvalid(self)) + return; + + if (hash_table__is_full(self->hash)) { + // Double the hash table and move the items across. + hash_table_t *new_hash = hash_table_new(self->hash->capacity << 1); + + hash_table__iteritems_start(self->hash, key_dt, _key, value_t, _value) { + hash_table__set(new_hash, _key, _value); + } hash_table__iter_stop(self->hash); + + // Destroy the old hash table and replace it with the new one. + hash_table__destroy(self->hash); + self->hash = new_hash; + } + + hash_table__set(self->hash, key, value); +} + +// ---------------------------------------------------------------------------- +void +lookup__del(lookup_t *self, key_dt key) { + if (!isvalid(self)) + return; + + hash_table__del(self->hash, key); +} + +// ---------------------------------------------------------------------------- +void +lookup__clear(lookup_t *self) { + if (!isvalid(self)) + return; + + size_t size = self->hash->capacity; + hash_table__destroy(self->hash); + self->hash = hash_table_new(size); +} + +// ---------------------------------------------------------------------------- +void +lookup__destroy(lookup_t *self) { + if (!isvalid(self)) + return; + + hash_table__destroy(self->hash); + self->hash = NULL; + + free(self); +} diff --git a/src/cache.h b/src/cache.h index 75075bba..fd45deb9 100644 --- a/src/cache.h +++ b/src/cache.h @@ -159,6 +159,7 @@ typedef struct _chain_t { typedef struct hash_table_t { size_t capacity; size_t size; + size_t load_factor; chain_t **chains; #ifdef DEBUG @@ -301,6 +302,18 @@ value_t hash_table__get(hash_table_t *, key_dt); +/** + * Check if the hash table has grown over the load factor. + * + * @param self the hash table + * + * @return TRUE if the hash table has grown over the load factor, FALSE + * otherwise. +*/ +int +hash_table__is_full(hash_table_t *); + + /** * Set a value into the table. * @@ -326,6 +339,18 @@ hash_table__set(hash_table_t *, key_dt, value_t); if (!isvalid(valvar)) \ continue; +#define hash_table__iteritems_start(table, keytype, keyvar, valtype, valvar) \ + for (int __i = 0; __i < table->capacity; __i++) { \ + chain_t * chain = table->chains[__i]; \ + if (!isvalid(chain)) \ + continue; \ + while (isvalid(chain->next)) { \ + chain = chain->next; \ + keytype keyvar = (keytype) chain->key; \ + valtype valvar = (valtype) chain->value; \ + if (!isvalid(valvar)) \ + continue; + #define hash_table__iter_stop(table) }} @@ -425,4 +450,95 @@ lru_cache__store(lru_cache_t *, key_dt, value_t); void lru_cache__destroy(lru_cache_t *); + +// -- Lookup ------------------------------------------------------------------ + +typedef struct { + hash_table_t *hash; + + #ifdef DEBUG + const char * name; + #endif +} lookup_t; + + +/** + * Create a lookup object. + * + * A lookup object is an expandable hash table that can be used to look up + * values by key. No ownership of the values is taken. + * + * @param size the initial size of the underlying hash map + * + * @return a valid reference to a lookup, NULL otherwise. + */ +lookup_t * +lookup_new(int); + + +/** + * Look up a value. + * + * Since this method returns NULL on a lookup failure, this only makes sense if + * all the objects stored within the lookup are non-NULL. + * + * @param self the lookup to use + * @param key the key to search + * + * @return the value associated to the key if the lookup succeeds, NULL + * otherwise. + */ +value_t +lookup__get(lookup_t *, key_dt); + + +/** + * Associated a value with a key in the lookup. + * + * @param self the lookup to set into + * @param key the key associated with the value + * @param value the value to associated with the key + */ +void +lookup__set(lookup_t *, key_dt, value_t); + + +/** + * Delete a value from the lookup. + * + * @param self the lookup to use + * @param key the key to delete + */ +void +lookup__del(lookup_t *, key_dt); + + +/** + * Clear the lookup. + * + * @param self the lookup to clear + */ +void +lookup__clear(lookup_t *); + + +/** + * Iterate over the lookup. +*/ +#define lookup__iteritems_start(lu, keytype, keyvar, valtype, valvar) \ + hash_table__iteritems_start((lu->hash), keytype, keyvar, valtype, valvar) + + +#define lookup__iter_stop(lu) \ + hash_table__iter_stop((lu->hash)) + + +/** + * Deallocate a lookup. + * + * @param self the lookup to deallocate + */ +void +lookup__destroy(lookup_t *); + #endif \ No newline at end of file diff --git a/src/code.h b/src/code.h new file mode 100644 index 00000000..76bbb8b8 --- /dev/null +++ b/src/code.h @@ -0,0 +1,47 @@ +// This file is part of "austin" which is released under GPL. +// +// See file LICENCE or go to http://www.gnu.org/licenses/ for full license +// details. +// +// Austin is a Python frame stack sampler for CPython. +// +// Copyright (c) 2018-2022 Gabriele N. Tornetta . +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + + +#include "py_string.h" + + +#define _code__get_filename(self, pref, py_v) \ + _string_from_raddr( \ + pref, *((void **) ((void *) self + py_v->py_code.o_filename)), py_v \ + ) + +#define _code__get_name(self, pref, py_v) \ + _string_from_raddr( \ + pref, *((void **) ((void *) self + py_v->py_code.o_name)), py_v \ + ) + +#define _code__get_qualname(self, pref, py_v) \ + _string_from_raddr( \ + pref, *((void **) ((void *) self + py_v->py_code.o_qualname)), py_v \ + ) + +#define _code__get_lnotab(self, pref, len, py_v) \ + _bytes_from_raddr( \ + pref, *((void **) ((void *) self + py_v->py_code.o_lnotab)), len, py_v \ + ) diff --git a/src/frame.h b/src/frame.h new file mode 100644 index 00000000..69fafc7e --- /dev/null +++ b/src/frame.h @@ -0,0 +1,282 @@ +// This file is part of "austin" which is released under GPL. +// +// See file LICENCE or go to http://www.gnu.org/licenses/ for full license +// details. +// +// Austin is a Python frame stack sampler for CPython. +// +// Copyright (c) 2018-2022 Gabriele N. Tornetta . +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + + +#include "cache.h" +#include "resources.h" + + +typedef struct { + key_dt key; + char * filename; + char * scope; + unsigned int line; + unsigned int line_end; + unsigned int column; + unsigned int column_end; +} frame_t; + + +typedef struct { + void * origin; + void * code; + int lasti; +} py_frame_t; + + +// ---------------------------------------------------------------------------- +static inline frame_t * +frame_new( + key_dt key, + char * filename, + char * scope, + unsigned int line, + unsigned int line_end, + unsigned int column, + unsigned int column_end +) { + frame_t * frame = (frame_t *) malloc(sizeof(frame_t)); + if (!isvalid(frame)) { + return NULL; + } + + frame->key = key; + frame->filename = filename; + frame->scope = scope; + + frame->line = line; + frame->line_end = line_end; + frame->column = column; + frame->column_end = column_end; + + return frame; +} + + +// ---------------------------------------------------------------------------- +static inline void +frame__destroy(frame_t * self) { + sfree(self); +} + + +#ifdef NATIVE +#define CFRAME_MAGIC ((void*) 0xCF) +#endif + + +#include "code.h" +#include "mojo.h" +#include "py_proc.h" + + +#define py_frame_key(code, lasti) (((key_dt) (((key_dt) code) & MOJO_INT32) << 16) | lasti) +#define py_string_key(code, field) ((key_dt) *((void **) ((void *) &code + py_v->py_code.field))) + + +// ---------------------------------------------------------------------------- +static inline int +_read_varint(unsigned char * lnotab, size_t * i) { + int val = lnotab[++*i] & 63; + int shift = 0; + while (lnotab[*i] & 64) { + shift += 6; + val |= (lnotab[++*i] & 63) << shift; + } + return val; +} + + +// ---------------------------------------------------------------------------- +static inline int +_read_signed_varint(unsigned char * lnotab, size_t * i) { + int val = _read_varint(lnotab, i); + return (val & 1) ? -(val >> 1) : (val >> 1); +} + +// ---------------------------------------------------------------------------- +static inline frame_t * +_frame_from_code_raddr(py_proc_t * py_proc, void * code_raddr, int lasti, python_v * py_v) { + cu_uchar * lnotab = NULL; + proc_ref_t pref = py_proc->proc_ref; + PyCodeObject code; + + if (fail(copy_py(pref, code_raddr, py_code, code))) { + log_ie("Cannot read remote PyCodeObject"); + return NULL; + } + + lru_cache_t * cache = py_proc->string_cache; + + key_dt string_key = py_string_key(code, o_filename); + char * filename = (char *) lru_cache__maybe_hit(cache, string_key); + if (!isvalid(filename)) { + filename = _code__get_filename(&code, pref, py_v); + if (!isvalid(filename)) { + log_ie("Cannot get file name from PyCodeObject"); + return NULL; + } + lru_cache__store(cache, string_key, filename); + if (pargs.binary) { + mojo_string_event(string_key, filename); + } + } + if (pargs.binary) { + filename = (char *) string_key; + } + + string_key = V_MIN(3, 11) ? py_string_key(code, o_qualname) : py_string_key(code, o_name); + char * scope = (char *) lru_cache__maybe_hit(cache, string_key); + if (!isvalid(scope)) { + scope = V_MIN(3, 11) + ? _code__get_qualname(&code, pref, py_v) + : _code__get_name(&code, pref, py_v); + if (!isvalid(scope)) { + log_ie("Cannot get scope name from PyCodeObject"); + return NULL; + } + lru_cache__store(cache, string_key, scope); + if (pargs.binary) { + mojo_string_event(string_key, scope); + } + } + if (pargs.binary) { + scope = (char *) string_key; + } + + ssize_t len = 0; + + unsigned int lineno = V_FIELD(unsigned int, code, py_code, o_firstlineno); + unsigned int line_end = lineno; + unsigned int column = 0; + unsigned int column_end = 0; + + if (V_MIN(3, 11)) { + lnotab = _code__get_lnotab(&code, pref, &len, py_v); + if (!isvalid(lnotab) || len == 0) { + log_ie("Cannot get line information from PyCodeObject"); + return NULL; + } + + lasti >>= 1; + + for (size_t i = 0, bc = 0; i < len; i++) { + bc += (lnotab[i] & 7) + 1; + int code = (lnotab[i] >> 3) & 15; + unsigned char next_byte = 0; + switch (code) { + case 15: + break; + + case 14: // Long form + lineno += _read_signed_varint(lnotab, &i); + line_end = lineno + _read_varint(lnotab, &i); + column = _read_varint(lnotab, &i); + column_end = _read_varint(lnotab, &i); + break; + + case 13: // No column data + lineno += _read_signed_varint(lnotab, &i); + line_end = lineno; + + column = column_end = 0; + break; + + case 12: // New lineno + case 11: + case 10: + lineno += code - 10; + line_end = lineno; + column = 1 + lnotab[++i]; + column_end = 1 + lnotab[++i]; + break; + + default: + next_byte = lnotab[++i]; + line_end = lineno; + column = 1 + (code << 3) + ((next_byte >> 4) & 7); + column_end = column + (next_byte & 15); + } + + if (bc > lasti) + break; + } + } + else { + lnotab = _code__get_lnotab(&code, pref, &len, py_v); + if (!isvalid(lnotab) || len % 2) { + log_ie("Cannot get line information from PyCodeObject"); + return NULL; + } + + if (V_MIN(3, 10)) { + lasti <<= 1; + for (int i = 0, bc = 0; i < len; i++) { + int sdelta = lnotab[i++]; + if (sdelta == 0xff) + break; + + bc += sdelta; + + int ldelta = lnotab[i]; + if (ldelta == 0x80) + ldelta = 0; + else if (ldelta > 0x80) + lineno -= 0x100; + + lineno += ldelta; + if (bc > lasti) + break; + } + } + else { // Python < 3.10 + for (int i = 0, bc = 0; i < len; i++) { + bc += lnotab[i++]; + if (bc > lasti) + break; + + if (lnotab[i] >= 0x80) + lineno -= 0x100; + + lineno += lnotab[i]; + } + } + } + + frame_t * frame = frame_new( + py_frame_key(code_raddr, lasti), + filename, + scope, + lineno, + line_end, + column, + column_end + ); + if (!isvalid(frame)) { + log_e("Failed to create frame object"); + return NULL; + } + + return frame; +} \ No newline at end of file diff --git a/src/hints.h b/src/hints.h index 3a46c755..0aa59137 100644 --- a/src/hints.h +++ b/src/hints.h @@ -45,11 +45,6 @@ #define unlikely(x) __builtin_expect(!!(x), 0) #endif -#define with_resources int retval = 0; -#define OK goto release; -#define NOK {retval = 1; goto release;} -#define released return retval; - #define UNKNOWN_SCOPE ((char *) 1) #endif diff --git a/src/linux/addr2line.h b/src/linux/addr2line.h index ea07c894..bd08ced2 100644 --- a/src/linux/addr2line.h +++ b/src/linux/addr2line.h @@ -23,6 +23,9 @@ // This source has been adapted from // https://github.com/bminor/binutils-gdb/blob/ce230579c65b9e04c830f35cb78ff33206e65db1/binutils/addr2line.c + +#define PACKAGE "austinp" // https://github.com/P403n1x87/austin/issues/152 + #include #include #include @@ -221,8 +224,8 @@ get_native_frame(const char *file_name, bfd_vma addr, key_dt frame_key) syms = NULL; frame_t *frame = isvalid(filename) && isvalid(name) - ? frame_new(frame_key, strdup(filename), strdup(name), line) - : NULL; + ? frame_new(frame_key, strdup(filename), strdup(name), line, 0, 0, 0) + : NULL; bfd_close(abfd); diff --git a/src/linux/common.h b/src/linux/common.h index 850cbdd7..a97cd541 100644 --- a/src/linux/common.h +++ b/src/linux/common.h @@ -20,12 +20,15 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#ifndef COMMON_H -#define COMMON_H +#pragma once + #include +#include +#include #include +#include "../error.h" #include "../stats.h" @@ -41,20 +44,24 @@ #endif -static uintptr_t _pthread_buffer[PTHREAD_BUFFER_ITEMS]; - -#define read_pthread_t(pid, addr) \ - (copy_memory(pid, addr, sizeof(_pthread_buffer), _pthread_buffer)) - - struct _proc_extra_info { unsigned int page_size; char statm_file[24]; pthread_t wait_thread_id; unsigned int pthread_tid_offset; + uintptr_t _pthread_buffer[PTHREAD_BUFFER_ITEMS]; }; +#define read_pthread_t(py_proc, addr) \ + (copy_memory( \ + py_proc->proc_ref, \ + addr, \ + sizeof(py_proc->extra->_pthread_buffer), \ + py_proc->extra->_pthread_buffer \ + )) + + #ifdef NATIVE #include @@ -78,4 +85,27 @@ wait_ptrace(enum __ptrace_request request, pid_t pid, void * addr, void * data) #endif -#endif +// ---------------------------------------------------------------------------- +static inline FILE* +_procfs(pid_t pid, char * file) { + FILE * fp; + char buffer[32]; + + sprintf(buffer, "/proc/%d/%s", pid, file); + + fp = fopen(buffer, "rb"); + if (fp == NULL) { + switch (errno) { + case EACCES: // Needs elevated privileges + set_error(EPROCPERM); + break; + case ENOENT: // Invalid pid + set_error(EPROCNPID); + break; + default: + set_error(EPROCVM); + } + } + + return fp; +} diff --git a/src/linux/py_proc.h b/src/linux/py_proc.h index ed0d0d0f..4f739c64 100644 --- a/src/linux/py_proc.h +++ b/src/linux/py_proc.h @@ -36,6 +36,8 @@ #include #include "common.h" +#include "../mem.h" +#include "../resources.h" #ifdef NATIVE #include "../argparse.h" @@ -64,8 +66,6 @@ #define ELF_SH_OFF(ehdr, i) /* as */ (ehdr->e_shoff + i * ehdr->e_shentsize) - - union { Elf32_Ehdr v32; Elf64_Ehdr v64; @@ -85,7 +85,8 @@ static ssize_t _file_size(char * file) { struct stat statbuf; - stat(file, &statbuf); + if (fail(stat(file, &statbuf))) + return -1; return statbuf.st_size; } @@ -105,7 +106,7 @@ _get_base_64(Elf64_Ehdr * ehdr, void * elf_map) static int -_py_proc__analyze_elf64(py_proc_t * self, void * elf_map) { +_py_proc__analyze_elf64(py_proc_t * self, void * elf_map, void * elf_base) { register int symbols = 0; Elf64_Ehdr * ehdr = elf_map; @@ -120,6 +121,9 @@ _py_proc__analyze_elf64(py_proc_t * self, void * elf_map) { Elf64_Shdr * p_dynsym = NULL; Elf64_Addr base = _get_base_64(ehdr, elf_map); + void * bss_base = NULL; + size_t bss_size = 0; + if (base != UINT64_MAX) { log_d("Base @ %p", base); @@ -146,9 +150,8 @@ _py_proc__analyze_elf64(py_proc_t * self, void * elf_map) { // map_flag |= RODATA_MAP; // } else if (strcmp(sh_name_base + p_shdr->sh_name, ".bss") == 0) { - self->map.bss.base = self->map.elf.base + (p_shdr->sh_addr - base); - self->map.bss.size = p_shdr->sh_size; - log_d("BSS @ %p, (size %x)", self->map.bss.base, self->map.bss.size); + bss_base = elf_base + (p_shdr->sh_addr - base); + bss_size = p_shdr->sh_size; } } @@ -163,7 +166,7 @@ _py_proc__analyze_elf64(py_proc_t * self, void * elf_map) { ) { Elf64_Sym * sym = (Elf64_Sym *) (elf_map + tab_off); char * sym_name = (char *) (elf_map + p_strtabsh->sh_offset + sym->st_name); - void * value = self->map.elf.base + (sym->st_value - base); + void * value = elf_base + (sym->st_value - base); if ((symbols += _py_proc__check_sym(self, sym_name, value)) >= SYMBOLS) break; } @@ -176,6 +179,11 @@ _py_proc__analyze_elf64(py_proc_t * self, void * elf_map) { FAIL; } + // Communicate BSS data back to the caller + self->map.bss.base = bss_base; + self->map.bss.size = bss_size; + log_d("BSS @ %p (size %x, offset %x)", self->map.bss.base, self->map.bss.size, self->map.bss.base - elf_base); + SUCCESS; } /* _py_proc__analyze_elf64 */ @@ -194,7 +202,7 @@ _get_base_32(Elf32_Ehdr * ehdr, void * elf_map) static int -_py_proc__analyze_elf32(py_proc_t * self, void * elf_map) { +_py_proc__analyze_elf32(py_proc_t * self, void * elf_map, void * elf_base) { register int symbols = 0; Elf32_Ehdr * ehdr = elf_map; @@ -209,6 +217,9 @@ _py_proc__analyze_elf32(py_proc_t * self, void * elf_map) { Elf32_Shdr * p_dynsym = NULL; Elf32_Addr base = _get_base_32(ehdr, elf_map); + void * bss_base = NULL; + size_t bss_size = 0; + if (base != UINT32_MAX) { log_d("Base @ %p", base); @@ -234,6 +245,10 @@ _py_proc__analyze_elf32(py_proc_t * self, void * elf_map) { // self->map.rodata.size = p_shdr->sh_size; // map_flag |= RODATA_MAP; // } + else if (strcmp(sh_name_base + p_shdr->sh_name, ".bss") == 0) { + bss_base = elf_base + (p_shdr->sh_addr - base); + bss_size = p_shdr->sh_size; + } } if (p_dynsym != NULL) { @@ -247,7 +262,7 @@ _py_proc__analyze_elf32(py_proc_t * self, void * elf_map) { ) { Elf32_Sym * sym = (Elf32_Sym *) (elf_map + tab_off); char * sym_name = (char *) (elf_map + p_strtabsh->sh_offset + sym->st_name); - void * value = self->map.elf.base + (sym->st_value - base); + void * value = elf_base + (sym->st_value - base); if ((symbols += _py_proc__check_sym(self, sym_name, value)) >= SYMBOLS) break; } @@ -260,103 +275,87 @@ _py_proc__analyze_elf32(py_proc_t * self, void * elf_map) { FAIL; } + // Communicate BSS data back to the caller + self->map.bss.base = bss_base; + self->map.bss.size = bss_size; + log_d("BSS @ %p (size %x, offset %x)", self->map.bss.base, self->map.bss.size, self->map.bss.base - elf_base); + SUCCESS; } /* _py_proc__analyze_elf32 */ // ---------------------------------------------------------------------------- static int -_py_proc__analyze_elf(py_proc_t * self, char * path) { - int fd = open (path, O_RDONLY); +_elf_check(Elf64_Ehdr * ehdr) { + return (ehdr->e_shoff == 0 || ehdr->e_shnum < 2 || memcmp(ehdr->e_ident, ELFMAG, SELFMAG)); +} + + +// ---------------------------------------------------------------------------- +static int +_py_proc__analyze_elf(py_proc_t * self, char * path, void * elf_base) { + cu_fd fd = open(path, O_RDONLY); if (fd == -1) { log_e("Cannot open binary file %s", path); FAIL; } - void * binary_map = MAP_FAILED; + cu_map_t * binary_map = NULL; size_t binary_size = 0; struct stat s; - with_resources; - if (fstat(fd, &s) == -1) { log_ie("Cannot determine size of binary file"); - NOK; + FAIL; } binary_size = s.st_size; - binary_map = mmap(0, binary_size, PROT_READ, MAP_PRIVATE, fd, 0); - if (binary_map == MAP_FAILED) { + binary_map = map_new(fd, binary_size, MAP_PRIVATE); + if (!isvalid(binary_map)) { log_ie("Cannot map binary file to memory"); - NOK; + FAIL; } - Elf64_Ehdr * ehdr = binary_map; + Elf64_Ehdr * ehdr = binary_map->addr; log_t("Analysing ELF"); - if (ehdr->e_shoff == 0 || ehdr->e_shnum < 2 || memcmp(ehdr->e_ident, ELFMAG, SELFMAG)) { - log_d("Bad ELF magic"); - NOK; + if (fail(_elf_check(ehdr))) { + log_e("Bad ELF header"); + FAIL; } // Dispatch switch (ehdr->e_ident[EI_CLASS]) { case ELFCLASS64: - log_d("64-bit ELF detected"); - retval = _py_proc__analyze_elf64(self, binary_map); - break; + log_d("%s is 64-bit ELF", path); + return _py_proc__analyze_elf64(self, binary_map->addr, elf_base); case ELFCLASS32: - retval = _py_proc__analyze_elf32(self, binary_map); - break; + log_d("%s is 32-bit ELF", path); + return _py_proc__analyze_elf32(self, binary_map->addr, elf_base); default: - log_e("Invalid ELF class"); - NOK; + log_e("%s has invalid ELF class", path); + FAIL; } - -release: - if (binary_map != MAP_FAILED) munmap(binary_map, binary_size); - if (fd != -1) close(fd); - - released; } /* _py_proc__analyze_elf */ -// ---------------------------------------------------------------------------- -static int -_elf_is_executable(char * object_file) { - int fd = open(object_file, O_RDONLY); - if (fd == -1) - return FALSE; - - Elf64_Ehdr * ehdr = (Elf64_Ehdr *) mmap(NULL, sizeof(Elf64_Ehdr), PROT_READ, MAP_SHARED, fd, 0); - if (ehdr == MAP_FAILED) { - close(fd); - return FALSE; - } - - int is_exec = ehdr->e_type == ET_EXEC; - - munmap(ehdr, sizeof(Elf64_Ehdr)); - close(fd); - - return is_exec; -} /* _elf_is_executable */ - - // ---------------------------------------------------------------------------- static int _py_proc__parse_maps_file(py_proc_t * self) { char file_name[32]; - FILE * fp = NULL; - char * line = NULL; - size_t len = 0; - int maps_flag = 0; + cu_FILE * fp = NULL; + cu_char * line = NULL; + cu_char * prev_path = NULL; + cu_char * needle_path = NULL; + size_t len = 0; + int maps_flag = 0; - sprintf(file_name, "/proc/%d/maps", self->pid); - fp = fopen(file_name, "r"); + struct vm_map * map = NULL; + + fp = _procfs(self->pid, "maps"); if (fp == NULL) { switch (errno) { case EACCES: // Needs elevated privileges @@ -377,97 +376,179 @@ _py_proc__parse_maps_file(py_proc_t * self) { sfree(self->bin_path); sfree(self->lib_path); - char prev_path[1024] = "\0"; + self->map.elf.base = NULL; + self->map.elf.size = 0; + + sprintf(file_name, "/proc/%d/exe", self->pid); + + cu_void * pd_mem = calloc(1, sizeof(struct proc_desc)); + if (!isvalid(pd_mem)) { + log_ie("Cannot allocate memory for proc_desc"); + FAIL; + } + struct proc_desc * pd = pd_mem; + + if (readlink(file_name, pd->exe_path, sizeof(pd->exe_path)) == -1) { + log_e("Cannot readlink %s", file_name); + FAIL; // cppcheck-suppress [resourceLeak] + } + if (strcmp(pd->exe_path + (strlen(pd->exe_path) - 10), " (deleted)") == 0) { + pd->exe_path[strlen(pd->exe_path) - 10] = '\0'; + } + log_d("Executable path: %s", pd->exe_path); + while (getline(&line, &len, fp) != -1) { ssize_t lower, upper; char pathname[1024]; + char perms[5]; - int field_count = sscanf(line, ADDR_FMT "-" ADDR_FMT " %*s %*x %*x:%*x %*x %s\n", + int field_count = sscanf(line, ADDR_FMT "-" ADDR_FMT " %s %*x %*x:%*x %*x %s\n", &lower, &upper, // Map bounds + perms, // Permissions pathname // Binary path ) - 3; // We expect between 3 and 4 matches. - if (field_count >= 0) { - if (field_count == 0 || strstr(pathname, "[v") == NULL) { - // Skip meaningless addresses like [vsyscall] which would give - // ridiculous values. - if ((void *) lower < self->min_raddr) self->min_raddr = (void *) lower; - if ((void *) upper > self->max_raddr) self->max_raddr = (void *) upper; - } - if ((maps_flag & HEAP_MAP) == 0 && strstr(line, "[heap]\n") != NULL) { - self->map.heap.base = (void *) lower; - self->map.heap.size = upper - lower; + if (field_count == 0 && isvalid(map) && !isvalid(map->bss_base) && strcmp(perms, "rw-p") == 0) { + // The BSS section is not mapped from a file and has rw permissions. + // We find that the map reported by proc fs is rounded to the next page + // boundary, so we need to adjust the values. We might slide into the data + // section, but that should be readable anyway. + size_t page_size = getpagesize(); + map->bss_base = (void *) lower - page_size; + map->bss_size = upper - lower + page_size; + log_d("Inferred BSS for %s: %lx-%lx", map->path, lower, upper); + } - maps_flag |= HEAP_MAP; + if (field_count <= 0) + continue; - log_d("HEAP bounds " ADDR_FMT "-" ADDR_FMT, lower, upper); - continue; - } + if (field_count == 0 || strstr(pathname, "[v") == NULL) { + // Skip meaningless addresses like [vsyscall] which would give + // ridiculous values. + if ((void *) lower < self->min_raddr) self->min_raddr = (void *) lower; + if ((void *) upper > self->max_raddr) self->max_raddr = (void *) upper; + } - // if (strstr(line, "python") == NULL) - // NOTE: The python binary might have a name that doesn't contain python - // but would still be valid. In case of future issues, this - // should be changed so that the binary on the first line is - // checked for, e.g., knownw symbols to determine whether it is a - // valid binary that Austin can handle. - // continue; - - // Check if it is an executable. Only bother if the size is above the - // MB threshold. Anything smaller is probably not a useful binary. - if (pathname[0] == '[') - continue; + if ((maps_flag & HEAP_MAP) == 0 && strstr(line, "[heap]\n") != NULL) { + self->map.heap.base = (void *) lower; + self->map.heap.size = upper - lower; - if (strcmp(pathname, prev_path) == 0) // Avoid analysing a binary multiple times - continue; + maps_flag |= HEAP_MAP; - strncpy(prev_path, pathname, sizeof(prev_path)); + log_d("HEAP bounds " ADDR_FMT "-" ADDR_FMT, lower, upper); + continue; + } - ssize_t file_size = _file_size(pathname); - if (_elf_is_executable(pathname)) { - if (self->bin_path != NULL || (file_size < (1 << 20))) - continue; + if (pathname[0] == '[') + continue; - self->bin_path = strndup(pathname, strlen(pathname)); + if (isvalid(prev_path) && strcmp(pathname, prev_path) == 0) { // Avoid analysing a binary multiple times + continue; + } + + sfree(prev_path); + prev_path = strndup(pathname, strlen(pathname)); + if (!isvalid(prev_path)) { + log_ie("Cannot duplicate path name"); + FAIL; + } - self->map.elf.base = (void *) lower; - self->map.elf.size = upper - lower; + // The first memory map of the executable + if (!isvalid(pd->maps[MAP_BIN].path) && strcmp(pd->exe_path, pathname) == 0) { + map = &(pd->maps[MAP_BIN]); + map->path = strndup(pathname, strlen(pathname)); + if (!isvalid(map->path)) { + log_ie("Cannot duplicate path name"); + FAIL; + } + map->file_size = _file_size(pathname); + map->base = (void *) lower; + map->size = upper - lower; + map->has_symbols = success(_py_proc__analyze_elf(self, pathname, (void *) lower)); + if (map->has_symbols) { + map->bss_base = self->map.bss.base; + map->bss_size = self->map.bss.size; + } + log_d("Binary map: %s (symbols %d)", map->path, map->has_symbols); + continue; + } - if (fail(_py_proc__analyze_elf(self, self->bin_path))) { - log_d("Possibly invalid Python binary at %p: %s", self->map.elf.base, self->bin_path); - sfree(self->bin_path); - self->bin_path = NULL; + // The first memory map of the shared library (if any) + char * needle = strstr(pathname, "libpython"); + if (!isvalid(pd->maps[MAP_LIBSYM].path) && isvalid(needle)) { + int has_symbols = success(_py_proc__analyze_elf(self, pathname, (void *) lower)); + if (has_symbols) { + map = &(pd->maps[MAP_LIBSYM]); + map->path = strndup(pathname, strlen(pathname)); + if (!isvalid(map->path)) { + log_ie("Cannot duplicate path name"); + FAIL; } - else - log_d("Candidate binary: %s (size %d KB)", pathname, file_size >> 10); - - continue; - } else { - if (self->bin_path != NULL || self->lib_path != NULL || (file_size < (1 << 20))) - continue; + map->file_size = _file_size(pathname); + map->base = (void *) lower; + map->size = upper - lower; + map->has_symbols = TRUE; + map->bss_base = self->map.bss.base; + map->bss_size = self->map.bss.size; - self->lib_path = strndup(pathname, strlen(pathname)); + log_d("Library map: %s (with symbols)", map->path); - self->map.elf.base = (void *) lower; - self->map.elf.size = upper - lower; - - if (fail(_py_proc__analyze_elf(self, self->lib_path))) { - log_d("Possibly invalid Python library: %s", self->lib_path); - sfree(self->lib_path); - self->lib_path = NULL; + continue; + } + + // The first memory map of a binary that contains "pythonX.Y" in its name + if (!isvalid(pd->maps[MAP_LIBNEEDLE].path)) { + if (isvalid(needle)) { + unsigned int v; + if (sscanf(needle, "libpython%u.%u", &v, &v) == 2) { + map = &(pd->maps[MAP_LIBNEEDLE]); + map->path = needle_path = strndup(pathname, strlen(pathname)); + if (!isvalid(map->path)) { + log_ie("Cannot duplicate path name"); + FAIL; + } + map->file_size = _file_size(pathname); + map->base = (void *) lower; + map->size = upper - lower; + map->has_symbols = FALSE; + log_d("Library map: %s (needle)", map->path); + continue; + } } - else - log_d("Candidate library: %s (size %d KB)", pathname, file_size >> 10); } } } - sfree(line); - fclose(fp); + // If the library map is not valid, use the needle map + if (!isvalid(pd->maps[MAP_LIBSYM].path)) { + pd->maps[MAP_LIBSYM] = pd->maps[MAP_LIBNEEDLE]; + pd->maps[MAP_LIBNEEDLE].path = needle_path = NULL; + } + + // Work out paths + self->bin_path = pd->maps[MAP_BIN].path; + self->lib_path = pd->maps[MAP_LIBSYM].path; + + // Work out binary map + for (int i = 0; i < MAP_COUNT; i++) { + map = &(pd->maps[i]); + if (map->has_symbols) { + self->map.elf.base = map->base; + self->map.elf.size = map->size; + maps_flag |= BIN_MAP; + break; + } + } - return ( - (self->bin_path == NULL && self->lib_path == NULL) || - maps_flag != HEAP_MAP - ); + // Work out BSS map + int map_index = isvalid(pd->maps[MAP_LIBSYM].path) ? MAP_LIBSYM : MAP_BIN; + self->map.bss.base = pd->maps[map_index].bss_base; + self->map.bss.size = pd->maps[map_index].bss_size; + + log_d("BSS map %d from %s @ %p", map_index, pd->maps[map_index].path, self->map.bss.base); + log_d("VM maps parsing result: bin=%s lib=%s flags=%d", self->bin_path, self->lib_path, maps_flag); + + return maps_flag != (BIN_MAP | HEAP_MAP); } /* _py_proc__parse_maps_file */ @@ -500,12 +581,11 @@ vm_range_t * ranges[256]; static int _py_proc__get_vm_maps(py_proc_t * self) { - FILE * fp = NULL; - char * line = NULL; + cu_FILE * fp = NULL; + cu_char * line = NULL; size_t len = 0; vm_range_tree_t * tree = NULL; hash_table_t * table = NULL; - char file_name[32]; if (pargs.where) { tree = vm_range_tree_new(); @@ -518,19 +598,8 @@ _py_proc__get_vm_maps(py_proc_t * self) { self->base_table = table; } - sprintf(file_name, "/proc/%d/maps", self->pid); - fp = fopen(file_name, "r"); - if (fp == NULL) { - switch (errno) { - case EACCES: // Needs elevated privileges - set_error(EPROCPERM); - break; - case ENOENT: // Invalid pid - set_error(EPROCNPID); - break; - default: - set_error(EPROCVM); - } + fp = _procfs(self->pid, "maps"); + if (!isvalid(fp)) { FAIL; } @@ -564,9 +633,6 @@ _py_proc__get_vm_maps(py_proc_t * self) { for (int i = 0; i < nrange; i++) vm_range_tree__add(tree, (vm_range_t *) ranges[i]); - sfree(line); - fclose(fp); - SUCCESS; } /* _py_proc__get_vm_maps */ #endif @@ -595,6 +661,32 @@ _py_proc__init(py_proc_t * self) { } /* _py_proc__init */ +// ---------------------------------------------------------------------------- +pid_t +_get_nspid(pid_t pid) { + cu_char * line = NULL; + size_t len = 0; + pid_t nspid = 0; + pid_t this = 0; + + cu_FILE * status = _procfs(pid, "status"); + if (!isvalid(status)) { + log_e("Cannot get namespace PID for %d", pid); + return 0; + } + + while (getline(&line, &len, status) != -1) { + if (sscanf(line, "NSpid:\t%d\t%d", &this, &nspid) == 2 && this == pid) { + break; + } + } + + log_d("NS PID for %d: %d", pid, nspid); + + return nspid; +} + + // Support for CPU time on Linux. We need to retrieve the TID from the struct // pthread pointed to by the native thread ID stored by Python. We do not have // the definition of the structure, so we need to "guess" the offset of the tid @@ -603,15 +695,23 @@ _py_proc__init(py_proc_t * self) { // ---------------------------------------------------------------------------- static int _infer_tid_field_offset(py_thread_t * py_thread) { - if (fail(read_pthread_t(py_thread->raddr.pref, (void *) py_thread->tid))) { - log_d("Cannot copy pthread_t structure"); + if (fail(read_pthread_t(py_thread->proc, (void *) py_thread->tid))) { + log_d("> Cannot copy pthread_t structure (pid: %u)", py_thread->raddr.pref); + set_error(EMMAP); FAIL; } log_d("pthread_t at %p", py_thread->tid); + // If the target process is in a different PID namespace, we need to get its + // other PID to be able to determine the offset of the TID field. + pid_t nspid = _get_nspid(py_thread->raddr.pref); + for (register int i = 0; i < PTHREAD_BUFFER_ITEMS; i++) { - if (py_thread->raddr.pref == _pthread_buffer[i]) { + if ( + py_thread->raddr.pref == py_thread->proc->extra->_pthread_buffer[i] + || (nspid && nspid == py_thread->proc->extra->_pthread_buffer[i]) + ) { log_d("TID field offset: %d", i); py_thread->proc->extra->pthread_tid_offset = i; SUCCESS; @@ -620,7 +720,10 @@ _infer_tid_field_offset(py_thread_t * py_thread) { // Fall-back to smaller steps if we failed for (register int i = 0; i < PTHREAD_BUFFER_ITEMS * (sizeof(uintptr_t) / sizeof(pid_t)); i++) { - if (py_thread->raddr.pref == (pid_t) ((pid_t *) _pthread_buffer)[i]) { + if ( + py_thread->raddr.pref == (pid_t) ((pid_t *) py_thread->proc->extra->_pthread_buffer)[i] + || (nspid && nspid == (pid_t) ((pid_t *) py_thread->proc->extra->_pthread_buffer)[i]) + ) { log_d("TID field offset (from fall-back): %d", i); py_thread->proc->extra->pthread_tid_offset = -i; SUCCESS; diff --git a/src/linux/py_thread.h b/src/linux/py_thread.h index 66f87e17..0df973ab 100644 --- a/src/linux/py_thread.h +++ b/src/linux/py_thread.h @@ -30,20 +30,18 @@ #include "../hints.h" #include "../py_thread.h" +#include "../resources.h" // ---------------------------------------------------------------------------- static int _py_thread__is_idle(py_thread_t * self) { - with_resources; - char file_name[64]; char buffer[2048] = ""; - retval = -1; - sprintf(file_name, "/proc/%d/task/" SIZE_FMT "/stat", self->proc->pid, self->tid); - int fd = open(file_name, O_RDONLY); + + cu_fd fd = open(file_name, O_RDONLY); if (fd == -1) { log_d("Cannot open %s", file_name); return -1; @@ -51,22 +49,18 @@ _py_thread__is_idle(py_thread_t * self) { if (read(fd, buffer, 2047) == 0) { log_d("Cannot read %s", file_name); - goto release; + return -1; } char * p = strchr(buffer, ')'); if (!isvalid(p)) { log_d("Invalid format for procfs file %s", file_name); - goto release; + return -1; } p+=2; if (*p == ' ') p++; - retval = (*p != 'R'); - -release: - close(fd); - released; + return (*p != 'R'); } diff --git a/src/logging.h b/src/logging.h index db4cb8d1..1c70aa37 100644 --- a/src/logging.h +++ b/src/logging.h @@ -48,15 +48,15 @@ log_m("\033[1m _ _ \033[0m"); \ log_m("\033[1m __ _ _ _ __| |_(_)_ _ \033[0m"); \ log_m("\033[1m/ _` | || (_-< _| | ' \\ \033[0m"); \ - log_m("\033[1m\\__,_|\\_,_/__/\\__|_|_||_|\033[0m\033[31;1mp\033[0m \033[36;1m%s\033[0m", VERSION); \ + log_m("\033[1m\\__,_|\\_,_/__/\\__|_|_||_|\033[0m\033[31;1mp\033[0m \033[36;1m%s\033[0m [GCC %d.%d.%d]", VERSION, __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__); \ log_i("====[ AUSTINP ]===="); \ } #else #define log_header() { \ - log_m("\033[1m _ _ \033[0m"); \ + log_m("\033[1m _ _ \033[0m "); \ log_m("\033[1m __ _ _ _ __| |_(_)_ _ \033[0m"); \ log_m("\033[1m/ _` | || (_-< _| | ' \\ \033[0m"); \ - log_m("\033[1m\\__,_|\\_,_/__/\\__|_|_||_|\033[0m \033[36;1m%s\033[0m", VERSION); \ + log_m("\033[1m\\__,_|\\_,_/__/\\__|_|_||_|\033[0m \033[36;1m%s\033[0m [GCC %d.%d.%d]", VERSION, __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__); \ log_i("====[ AUSTIN ]===="); \ } #endif diff --git a/src/mac/py_proc.h b/src/mac/py_proc.h index b451bc1c..7ba7402b 100644 --- a/src/mac/py_proc.h +++ b/src/mac/py_proc.h @@ -40,6 +40,8 @@ #include #include "../hints.h" +#include "../mem.h" +#include "../resources.h" #define CHECK_HEAP #define DEREF_SYM @@ -120,15 +122,38 @@ _py_proc__analyze_macho64(py_proc_t * self, void * base, void * map) { int cmd_cnt = 0; struct segment_command_64 * cmd = map + sizeof(struct mach_header_64); + mach_vm_size_t size = 0; + mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t); + mach_vm_address_t address = (mach_vm_address_t) base; + vm_region_basic_info_data_64_t region_info; + mach_port_t object_name; + for (register int i = 0; cmd_cnt < 2 && i < ncmds; i++) { switch (cmd->cmd) { case LC_SEGMENT_64: - if (strcmp(cmd->segname, "__TEXT") == 0) { - // NOTE: This is based on the assumption that we find this segment - // early enough to allow later computations to use the correct value. - base -= cmd->vmaddr; - } - else if (strcmp(cmd->segname, "__DATA") == 0) { + if (strcmp(cmd->segname, "__DATA") == 0) { + // Get the address of the data segment. This way we can compute the base + // address of the binary. + // NOTE: Here we are vulnerable to size collisions. Unfortunately, we + // can't check for the same byte content as the data section is not + // read-only. + while (cmd->filesize != size) { + address += size; + if (mach_vm_region( + self->proc_ref, + &address, + &size, + VM_REGION_BASIC_INFO_64, + (vm_region_info_t) ®ion_info, + &count, + &object_name + ) != KERN_SUCCESS) { + log_e("Cannot get any more VM maps."); + return 0; + } + } + base = (void *) address - cmd->vmaddr; + int nsects = cmd->nsects; struct section_64 * sec = (struct section_64 *) ((void *) cmd + sizeof(struct segment_command_64)); self->map.bss.size = 0; @@ -153,12 +178,13 @@ _py_proc__analyze_macho64(py_proc_t * self, void * base, void * map) { struct nlist_64 * sym_tab = (struct nlist_64 *) (map + sw32(s, ((struct symtab_command *) cmd)->symoff)); void * str_tab = (void *) (map + sw32(s, ((struct symtab_command *) cmd)->stroff)); - // TODO: Assess quality if ((sym_tab[i].n_type & N_EXT) == 0) continue; char * sym_name = (char *) (str_tab + sym_tab[i].n_un.n_strx); - self->sym_loaded += _py_proc__check_sym(self, sym_name, (void *) (base + sym_tab[i].n_value)); + if (_py_proc__check_sym(self, sym_name, (void *) (base + sym_tab[i].n_value))) { + self->sym_loaded++; + } } cmd_cnt++; } // switch @@ -197,15 +223,38 @@ _py_proc__analyze_macho32(py_proc_t * self, void * base, void * map) { int cmd_cnt = 0; struct segment_command * cmd = map + sizeof(struct mach_header); + mach_vm_size_t size = 0; + mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_t); + mach_vm_address_t address = (mach_vm_address_t) base; + vm_region_basic_info_data_t region_info; + mach_port_t object_name; + for (register int i = 0; cmd_cnt < 2 && i < ncmds; i++) { switch (cmd->cmd) { case LC_SEGMENT: - if (strcmp(cmd->segname, "__TEXT") == 0) { - // NOTE: This is based on the assumption that we find this segment - // early enough to allow later computations to use the correct value. - base -= cmd->vmaddr; - } - else if (strcmp(cmd->segname, "__DATA") == 0) { + if (strcmp(cmd->segname, "__DATA") == 0) { + // Get the address of the data segment. This way we can compute the base + // address of the binary. + // NOTE: Here we are vulnerable to size collisions. Unfortunately, we + // can't check for the same byte content as the data section is not + // read-only. + while (cmd->filesize != size) { + address += size; + if (mach_vm_region( + self->proc_ref, + &address, + &size, + VM_REGION_BASIC_INFO, + (vm_region_info_t) ®ion_info, + &count, + &object_name + ) != KERN_SUCCESS) { + log_e("Cannot get any more VM maps."); + return 0; + } + } + base = (void *) address - cmd->vmaddr; + int nsects = cmd->nsects; struct section * sec = (struct section *) ((void *) cmd + sizeof(struct segment_command)); self->map.bss.size = 0; @@ -230,12 +279,13 @@ _py_proc__analyze_macho32(py_proc_t * self, void * base, void * map) { struct nlist * sym_tab = (struct nlist *) (map + sw32(s, ((struct symtab_command *) cmd)->symoff)); void * str_tab = (void *) (map + sw32(s, ((struct symtab_command *) cmd)->stroff)); - // TODO: Assess quality if ((sym_tab[i].n_type & N_EXT) == 0) continue; char * sym_name = (char *) (str_tab + sym_tab[i].n_un.n_strx); - self->sym_loaded += _py_proc__check_sym(self, sym_name, (void *) (base + sym_tab[i].n_value)); + if (_py_proc__check_sym(self, sym_name, (void *) (base + sym_tab[i].n_value))) { + self->sym_loaded++; + } } cmd_cnt++; } // switch @@ -306,9 +356,7 @@ _py_proc__analyze_fat(py_proc_t * self, void * base, void * map) { // ---------------------------------------------------------------------------- static bin_attr_t _py_proc__analyze_macho(py_proc_t * self, char * path, void * base, mach_vm_size_t size) { - bin_attr_t bin_attrs = 0; - - int fd = open(path, O_RDONLY); + cu_fd fd = open(path, O_RDONLY); if (fd == -1) { log_e("Cannot open binary %s", path); return INVALID_ATTR; @@ -316,56 +364,46 @@ _py_proc__analyze_macho(py_proc_t * self, char * path, void * base, mach_vm_size log_d("Analysing binary %s", path); - with_resources; - // This would cause problem if allocated in the stack frame - struct stat * fs = (struct stat *) malloc(sizeof(struct stat)); - void * map = MAP_FAILED; + cu_void * fs_buffer = malloc(sizeof(struct stat)); + struct stat * fs = (struct stat *) fs_buffer; + cu_map_t * map = NULL; if (fstat(fd, fs) == -1) { // Get file size log_e("Cannot get size of binary %s", path); - NOK; + FAIL; // cppcheck-suppress [memleak] } - map = mmap(NULL, fs->st_size, PROT_READ, MAP_SHARED, fd, 0); - if (map == MAP_FAILED) { + map = map_new(fd, fs->st_size, MAP_SHARED); + if (!isvalid(map)) { log_e("Cannot map binary %s", path); - NOK; + FAIL; // cppcheck-suppress [memleak] } - log_t("Local Mach-O file mapping %p-%p\n", map, map+size); + void * map_addr = map->addr; + log_t("Local Mach-O file mapping %p-%p\n", map_addr, map_addr + size); - struct mach_header_64 * hdr = (struct mach_header_64 *) map; + struct mach_header_64 * hdr = (struct mach_header_64 *) map_addr; switch (hdr->magic) { case MH_MAGIC: case MH_CIGAM: log_d("Binary is Mach-O 32"); - bin_attrs = _py_proc__analyze_macho32(self, base, map); - break; + return _py_proc__analyze_macho32(self, base, map_addr); case MH_MAGIC_64: case MH_CIGAM_64: log_d("Binary is Mach-O 64"); - bin_attrs = _py_proc__analyze_macho64(self, base, map); - break; + return _py_proc__analyze_macho64(self, base, map_addr); case FAT_MAGIC: case FAT_CIGAM: log_d("Binary is fat"); - bin_attrs = _py_proc__analyze_fat(self, base, map); - break; + return _py_proc__analyze_fat(self, base, map_addr); default: self->sym_loaded = 0; } -release: - if (map != MAP_FAILED) munmap(map, fs->st_size); - sfree(fs); - close(fd); - - retval = bin_attrs; - - released; + return 0; } // _py_proc__analyze_macho @@ -401,14 +439,14 @@ pid_to_task(pid_t pid) { if (fail(check_pid(pid))) { log_d("No such process: %d", pid); - FAIL; + return 0; } result = task_for_pid(mach_task_self(), pid, &task); if (result != KERN_SUCCESS) { - log_d("Call to task_for_pid failed: %s", mach_error_string(result)); + log_d("Call to task_for_pid failed on PID %d: %s", pid, mach_error_string(result)); set_error(EPROCPERM); - FAIL; + return 0; } return task; } @@ -418,33 +456,55 @@ pid_to_task(pid_t pid) { static int _py_proc__get_maps(py_proc_t * self) { mach_vm_address_t address = 0; - mach_vm_size_t size = 0; + mach_vm_size_t size = 0; + mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t); vm_region_basic_info_data_64_t region_info; - mach_msg_type_number_t count = sizeof(vm_region_basic_info_data_64_t); mach_port_t object_name; - char * path = (char *) calloc(MAXPATHLEN + 1, sizeof(char)); - if (!isvalid(path)) - FAIL; - - with_resources; - + #if defined PL_MACOS // NOTE: Mac OS X kernel bug. This also gives time to the VM maps to // stabilise. - usleep(50000); + usleep(50000); // 50ms + #endif - self->proc_ref = pid_to_task(self->pid); - if (self->proc_ref == 0) - NOK; + cu_char * prev_path = NULL; + cu_char * needle_path = NULL; + cu_char * path = (char *) calloc(MAXPATHLEN + 1, sizeof(char)); + if (!isvalid(path)) + FAIL; self->min_raddr = (void *) -1; self->max_raddr = NULL; - sfree(self->bin_path); - self->bin_path = NULL; + self->map.heap.base = NULL; + self->map.heap.size = 0; + sfree(self->bin_path); sfree(self->lib_path); - self->lib_path = NULL; + + cu_void * pd_mem = calloc(1, sizeof(struct proc_desc)); + if (!isvalid(pd_mem)) { + log_ie("Cannot allocate memory for proc_desc"); + FAIL; // cppcheck-suppress [memleak] + } + struct proc_desc * pd = pd_mem; + + if (proc_pidpath(self->pid, pd->exe_path, sizeof(pd->exe_path)) < 0) { + log_w("Cannot get executable path for process %d", self->pid); + } + if (strlen(pd->exe_path) == 0) { + FAIL; + } + log_d("Executable path: '%s'", pd->exe_path); + + self->proc_ref = pid_to_task(self->pid); + if (self->proc_ref == 0) { + log_ie("Cannot get task for PID"); + FAIL; // cppcheck-suppress [memleak] + } + set_error(EOK); + + struct vm_map * map = NULL; while (mach_vm_region( self->proc_ref, @@ -461,37 +521,90 @@ _py_proc__get_maps(py_proc_t * self) { if ((void *) address + size > self->max_raddr) self->max_raddr = (void *) address + size; - int len = proc_regionfilename(self->pid, address, path, MAXPATHLEN); - int path_len = strlen(path); + int path_len = proc_regionfilename(self->pid, address, path, MAXPATHLEN); + + if (isvalid(prev_path) && strcmp(path, prev_path) == 0) { // Avoid analysing a binary multiple times + goto next; + } + + sfree(prev_path); + prev_path = strndup(path, path_len); + if (!isvalid(prev_path)) { + log_ie("Cannot duplicate path name"); + FAIL; + } - if (size > 0 && len && !self->sym_loaded) { - path[len] = 0; + // The first memory map of the shared library (if any) + char * needle = strstr(path, "ython"); + // The first memory map of the executable + if (!isvalid(pd->maps[MAP_BIN].path) && strcmp(pd->exe_path, path) == 0) { + map = &(pd->maps[MAP_BIN]); + map->path = strndup(path, strlen(path)); + if (!isvalid(map->path)) { + log_ie("Cannot duplicate path name"); + FAIL; + } + + bin_attr_t bin_attrs = _py_proc__analyze_macho(self, path, (void *) address, size); + + map->file_size = size; + map->base = (void *) address; + map->size = size; + map->has_symbols = !!(bin_attrs & B_SYMBOLS); + if (map->has_symbols && bin_attrs & B_BSS) { + map->bss_base = self->map.bss.base; + map->bss_size = self->map.bss.size; + } + + log_d("Binary map: %s (symbols %d)", map->path, map->has_symbols); + } + + else if (!isvalid(pd->maps[MAP_LIBSYM].path) && isvalid(needle)) { bin_attr_t bin_attrs = _py_proc__analyze_macho(self, path, (void *) address, size); if (bin_attrs & B_SYMBOLS) { - if (size < BINARY_MIN_SIZE) { - // We found the symbols in the binary but we are probably going to use the wrong base - // since the map is too small. So pretend we didin't find them. - self->sym_loaded = 0; + map = &(pd->maps[MAP_LIBSYM]); + map->path = strndup(path, strlen(path)); + if (!isvalid(map->path)) { + log_ie("Cannot duplicate path name"); + FAIL; } - else { - switch (BINARY_TYPE(bin_attrs)) { - case BT_EXEC: - if (self->bin_path == NULL) { - self->bin_path = strndup(path, path_len); - log_d("Candidate binary: %s", self->bin_path); - } - break; - case BT_LIB: - if (self->lib_path == NULL && size > BINARY_MIN_SIZE) { - self->lib_path = strndup(path, path_len); - log_d("Candidate library: %s", self->lib_path); + map->file_size = size; + map->base = (void *) address; + map->size = size; + map->has_symbols = TRUE; + map->bss_base = self->map.bss.base; + map->bss_size = self->map.bss.size; + + log_d("Library map: %s (with symbols)", map->path); + } + + // The first memory map of a binary that contains "ythonX.Y" or "ython" + // and "/X.Y"in its name. + else if (!isvalid(pd->maps[MAP_LIBNEEDLE].path)) { + if (isvalid(needle)) { + unsigned int v; + if (sscanf(needle, "ython%u.%u", &v, &v) == 2 + || (isvalid(strstr(path, "/3.")) && sscanf(strstr(path, "/3."), "/3.%u", &v) == 1) + || (isvalid(strstr(path, "/2.")) && sscanf(strstr(path, "/2."), "/2.%u", &v) == 1) + ) { + map = &(pd->maps[MAP_LIBNEEDLE]); + map->path = needle_path = strndup(path, strlen(path)); + if (!isvalid(map->path)) { + log_ie("Cannot duplicate path name"); + FAIL; } + map->file_size = size; + map->base = (void *) address; + map->size = size; + map->has_symbols = FALSE; + log_d("Library map: %s (needle)", map->path); } } } } +next: // Make a best guess for the heap boundary. if (self->map.heap.base == NULL) self->map.heap.base = (void *) address; @@ -503,12 +616,33 @@ _py_proc__get_maps(py_proc_t * self) { log_d("BSS bounds [%p - %p]", self->map.bss.base, self->map.bss.base + self->map.bss.size); log_d("HEAP bounds [%p - %p]", self->map.heap.base, self->map.heap.base + self->map.heap.size); - retval = !self->sym_loaded; + // If the library map is not valid, use the needle map + if (!isvalid(pd->maps[MAP_LIBSYM].path)) { + pd->maps[MAP_LIBSYM] = pd->maps[MAP_LIBNEEDLE]; + pd->maps[MAP_LIBNEEDLE].path = needle_path = NULL; + } + + // Work out paths + self->bin_path = pd->maps[MAP_BIN].path; + self->lib_path = pd->maps[MAP_LIBSYM].path; + + // Work out binary map + for (int i = 0; i < MAP_COUNT; i++) { + map = &(pd->maps[i]); + if (map->has_symbols) { + self->map.elf.base = map->base; + self->map.elf.size = map->size; + self->sym_loaded = TRUE; + break; + } + } -release: - free(path); + // Work out BSS map + int map_index = isvalid(pd->maps[MAP_LIBSYM].path) ? MAP_LIBSYM : MAP_BIN; + self->map.bss.base = pd->maps[map_index].bss_base; + self->map.bss.size = pd->maps[map_index].bss_size; - released; + return !self->sym_loaded; } // _py_proc__get_maps diff --git a/src/mac/py_thread.h b/src/mac/py_thread.h index ad588b27..7fd2b5be 100644 --- a/src/mac/py_thread.h +++ b/src/mac/py_thread.h @@ -26,6 +26,7 @@ #include "../logging.h" #include "../py_thread.h" +#include "../resources.h" // This offset was discovered by looking at the result of PROC_PIDLISTTHREADS. @@ -45,9 +46,10 @@ _infer_thread_id_offset(py_thread_t * py_thread) { // Set the default value, in case we fail to find the actual one. _silly_offset = SILLY_OFFSET; - uint64_t * tids = (uint64_t *) calloc(MAX_THREADS, sizeof(uint64_t)); + cu_void * tids_mem = calloc(MAX_THREADS, sizeof(uint64_t)); + uint64_t * tids = (uint64_t *) tids_mem; if (!isvalid(tids)) { - return; + return; // cppcheck-suppress [memleak] } int n = proc_pidinfo( @@ -62,7 +64,7 @@ _infer_thread_id_offset(py_thread_t * py_thread) { } else if (n <= 0) { log_w("No native threads found. This is weird."); - goto release; + return; // cppcheck-suppress [memleak] } // Find the thread ID offset @@ -75,9 +77,6 @@ _infer_thread_id_offset(py_thread_t * py_thread) { } _silly_offset = min; log_t("Silly thread id offset: %x", _silly_offset); - -release: - sfree(tids); } diff --git a/src/mem.h b/src/mem.h index 500e9560..dd3cebf5 100644 --- a/src/mem.h +++ b/src/mem.h @@ -20,8 +20,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#ifndef MEM_H -#define MEM_H +#pragma once #include @@ -211,4 +210,27 @@ get_total_memory(void) { return 0; } -#endif // MEM_H + +struct vm_map{ + char * path; + ssize_t file_size; + void * base; + size_t size; + void * bss_base; + size_t bss_size; + int has_symbols; +}; + + +enum { + MAP_BIN, + MAP_LIBSYM, + MAP_LIBNEEDLE, + MAP_COUNT, +}; + + +struct proc_desc { + char exe_path[1024]; + struct vm_map maps[MAP_COUNT]; +}; diff --git a/src/mojo.h b/src/mojo.h index 2045bdf3..2a2a7a95 100644 --- a/src/mojo.h +++ b/src/mojo.h @@ -29,7 +29,7 @@ #include "cache.h" #include "platform.h" -#define MOJO_VERSION 1 +#define MOJO_VERSION 2 enum { MOJO_RESERVED, @@ -124,12 +124,15 @@ static inline void mojo_integer(mojo_int_t integer, int sign) { mojo_integer(pid, 0); \ mojo_fstring(FORMAT_TID, tid); -#define mojo_frame(frame) \ - mojo_event(MOJO_FRAME); \ - mojo_integer(frame->key, 0); \ - mojo_ref(frame->filename); \ - mojo_ref(frame->scope); \ - mojo_integer(frame->line, 0); +#define mojo_frame(frame) \ + mojo_event(MOJO_FRAME); \ + mojo_integer(frame->key, 0); \ + mojo_ref(frame->filename); \ + mojo_ref(frame->scope); \ + mojo_integer(frame->line, 0); \ + mojo_integer(frame->line_end, 0); \ + mojo_integer(frame->column, 0); \ + mojo_integer(frame->column_end, 0); #define mojo_frame_ref(frame) \ mojo_event(MOJO_FRAME_REF); \ diff --git a/src/py_proc.c b/src/py_proc.c index d48efb1f..623815b6 100644 --- a/src/py_proc.c +++ b/src/py_proc.c @@ -113,16 +113,11 @@ _py_proc__check_sym(py_proc_t * self, char * name, void * value) { // ---------------------------------------------------------------------------- -#ifdef PL_UNIX -#define _popen popen -#define _pclose pclose -#endif - static int _get_version_from_executable(char * binary, int * major, int * minor, int * patch) { - FILE * fp; - char version[64]; - char cmd[256]; + cu_pipe * fp; + char version[64]; + char cmd[256]; #if defined PL_WIN sprintf(cmd, "\"\"%s\"\" -V 2>&1", binary); @@ -135,36 +130,26 @@ _get_version_from_executable(char * binary, int * major, int * minor, int * patc FAIL; } - with_resources; - while (fgets(version, sizeof(version) - 1, fp) != NULL) { if (sscanf(version, "Python %d.%d.%d", major, minor, patch) == 3) - OK; + SUCCESS; } - NOK; - -release: - _pclose(fp); - - released; + FAIL; } /* _get_version_from_executable */ static int -_get_version_from_filename(char * filename, int * major, int * minor, int * patch) { +_get_version_from_filename(char * filename, const char * needle, int * major, int * minor, int * patch) { #if defined PL_LINUX /* LINUX */ char * base = filename; char * end = base + strlen(base); - const char * needle = "python"; - const size_t needle_len = strlen(needle); while (base < end) { base = strstr(base, needle); if (!isvalid(base)) { break; } - base += needle_len; - if (sscanf(base,"%d.%d", major, minor) == 2) { + if (sscanf(base + strlen(needle), "%u.%u", major, minor) == 2) { SUCCESS; } } @@ -203,40 +188,36 @@ _get_version_from_filename(char * filename, int * major, int * minor, int * patc #if defined PL_MACOS static int _find_version_in_binary(char * path) { + size_t binary_size = 0; + struct stat s; + log_d("Finding version in binary %s", path); - int fd = open (path, O_RDONLY); + cu_fd fd = open(path, O_RDONLY); if (fd == -1) { log_e("Cannot open binary file %s", path); FAIL; } - void * binary_map = MAP_FAILED; - size_t binary_size = 0; - struct stat s; - - with_resources; - if (fstat(fd, &s) == -1) { log_ie("Cannot determine size of binary file"); - NOK; + FAIL; } binary_size = s.st_size; - binary_map = mmap(0, binary_size, PROT_READ, MAP_PRIVATE, fd, 0); - if (binary_map == MAP_FAILED) { + cu_map_t * binary_map = map_new(fd, binary_size, MAP_PRIVATE); + if (!isvalid(binary_map)) { log_ie("Cannot map binary file to memory"); - NOK; + FAIL; } for (char m = '3'; m >= '2'; --m) { char needle[3] = {0x00, m, '.'}; size_t current_size = binary_size; - char * current_pos = binary_map; + char * current_pos = binary_map->addr; int major, minor, patch; major = 0; - retval = NOVERSION; while (TRUE) { char * p = memmem(current_pos, current_size, needle, sizeof(needle)); if (!isvalid(p)) break; @@ -246,20 +227,21 @@ _find_version_in_binary(char * path) { } if (major > 0) { - retval = PYVERSION(major, minor, patch); - break; + return PYVERSION(major, minor, patch); } } -release: - if (binary_map != MAP_FAILED) munmap(binary_map, binary_size); - if (fd != -1) close(fd); - - released; + return NOVERSION; } /* _find_version_in_binary */ #endif +#if defined PL_LINUX +#define LIB_NEEDLE "libpython" +#else +#define LIB_NEEDLE "python" +#endif + static int _py_proc__infer_python_version(py_proc_t * self) { if (self == NULL || (self->bin_path == NULL && self->lib_path == NULL)) @@ -290,7 +272,7 @@ _py_proc__infer_python_version(py_proc_t * self) { // Try to infer the Python version from the library file name. if ( isvalid(self->lib_path) - &&success(_get_version_from_filename(self->lib_path, &major, &minor, &patch)) + &&success(_get_version_from_filename(self->lib_path, LIB_NEEDLE, &major, &minor, &patch)) ) goto from_filename; // On Linux, the actual executable is sometimes picked as a library. Hence we @@ -308,6 +290,12 @@ _py_proc__infer_python_version(py_proc_t * self) { &&(success(_get_version_from_executable(self->bin_path, &major, &minor, &patch))) ) goto from_exe; + // Try to infer the Python version from the executable file name. + if ( + isvalid(self->bin_path) + &&success(_get_version_from_filename(self->bin_path, "python", &major, &minor, &patch)) + ) goto from_filename; + #if defined PL_MACOS if (major == 0) { // We still haven't found a Python version so we look at the binary @@ -373,11 +361,13 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) { PyInterpreterState is; PyThreadState tstate_head; - if (py_proc__get_type(self, raddr, is)) + if (py_proc__get_type(self, raddr, is)) { + log_ie("Cannot get remote interpreter state"); return OUT_OF_BOUND; + } - if (py_proc__get_type(self, V_FIELD(void *, is, py_is, o_tstate_head), tstate_head)) { - log_t( + if (fail(py_proc__get_type(self, V_FIELD(void *, is, py_is, o_tstate_head), tstate_head))) { + log_e( "Cannot copy PyThreadState head at %p from PyInterpreterState instance", V_FIELD(void *, is, py_is, o_tstate_head) ); @@ -386,12 +376,14 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) { log_t("PyThreadState head loaded @ %p", V_FIELD(void *, is, py_is, o_tstate_head)); - if (V_FIELD(void*, tstate_head, py_thread, o_interp) != raddr) + if (V_FIELD(void*, tstate_head, py_thread, o_interp) != raddr) { + log_d("PyThreadState head does not point to interpreter state"); FAIL; + } log_d( "Found possible interpreter state @ %p (offset %p).", - raddr, raddr - self->map.heap.base + raddr, raddr - self->map.elf.base ); log_t( @@ -424,10 +416,16 @@ _py_proc__check_interp_state(py_proc_t * self, void * raddr) { // Try to determine the TID by reading the remote struct pthread structure. // We can then use this information to parse the appropriate procfs file and // determine the native thread's running state. + void * initial_thread_addr = thread.raddr.addr; while (isvalid(thread.raddr.addr)) { if (success(_infer_tid_field_offset(&thread))) SUCCESS; + if (is_fatal(austin_errno)) + FAIL; + py_thread__next(&thread); + if (thread.raddr.addr == initial_thread_addr) + break; } log_d("tid field offset not ready"); FAIL; @@ -490,43 +488,71 @@ _py_proc__scan_heap(py_proc_t * self) { // ---------------------------------------------------------------------------- static int _py_proc__scan_bss(py_proc_t * self) { - if (fail(py_proc__memcpy(self, self->map.bss.base, self->map.bss.size, self->bss))) + // Starting with Python 3.11, BSS scans fail because it seems that the + // interpreter state is stored in the data section. In this case, we shift our + // data queries into the data section. We then take steps of 64KB backwards + // and try to find the interpreter state. This is a bit of a hack for now, but + // it seems to work with decent performance. Note that if we fail the first + // scan, we then look for actual interpreter states rather than pointers to + // it. This make the search a little slower, since we now have to check every + // value in the range. However, the step size we chose seems to get us close + // enough in a few attempts. + if (!isvalid(self) || !isvalid(self->map.bss.base)) { FAIL; + } + + int shift = 0; + size_t step = self->map.bss.size > 0x10000 ? 0x10000 : self->map.bss.size; + + V_DESC(self->py_v); + + while (!(shift && V_MAX(3, 10))) { + void * base = self->map.bss.base - (shift * step); + if (fail(py_proc__memcpy(self, base, self->map.bss.size, self->bss))) { + log_ie("Failed to copy BSS section"); + FAIL; + } - log_d("Scanning the BSS section for PyInterpreterState"); + log_d("Scanning the BSS section @ %p (shift %d)", base, shift); - void * upper_bound = self->bss + self->map.bss.size; - #ifdef CHECK_HEAP - // When the process uses the shared library we need to search in other maps - // other than the heap (at least on Linux). This could be optimised by - // creating a list of all the maps and checking that a value is valid address - // within any of these maps. However, this scan between min and max address - // should still be relatively quick so that the extra complexity of a list is - // not strictly required. - int is_lib = self->lib_path != NULL; - #endif - for ( - register void ** raddr = (void **) self->bss; - (void *) raddr < upper_bound; - raddr++ - ) { - if ( - #ifdef CHECK_HEAP - (is_lib ? _py_proc__is_raddr_within_max_range(self, *raddr) - : _py_proc__is_heap_raddr(self, *raddr)) && - #endif - _py_proc__check_interp_state(self, *raddr) == 0 + void * upper_bound = self->bss + (shift ? step : self->map.bss.size); + #ifdef CHECK_HEAP + // When the process uses the shared library we need to search in other maps + // other than the heap (at least on Linux). This could be optimised by + // creating a list of all the maps and checking that a value is valid address + // within any of these maps. However, this scan between min and max address + // should still be relatively quick so that the extra complexity of a list is + // not strictly required. + #endif + for ( + register void ** raddr = (void **) self->bss; + (void *) raddr < upper_bound; + raddr++ ) { - log_d( - "Possible interpreter state referenced by BSS @ %p (offset %x)", - (void *) raddr - (void *) self->bss + (void *) self->map.bss.base, - (void *) raddr - (void *) self->bss - ); - self->is_raddr = *raddr; - SUCCESS; + if ( + (!shift && + #ifdef CHECK_HEAP + (isvalid(self->lib_path) + ? _py_proc__is_raddr_within_max_range(self, *raddr) + : _py_proc__is_heap_raddr(self, *raddr)) && + #endif + success(_py_proc__check_interp_state(self, *raddr))) + ||(shift && success(_py_proc__check_interp_state(self, (void*) raddr - self->bss + base))) + ) { + log_d( + "Possible interpreter state referenced by BSS @ %p (offset %x)", + (void *) raddr - (void *) self->bss + (void *) base, + (void *) raddr - (void *) self->bss + ); + self->is_raddr = shift ? (void*) raddr - self->bss + base : *raddr; + SUCCESS; + } } + #if defined PL_WIN + break; + #endif + shift++; } - FAIL; } @@ -568,8 +594,10 @@ _py_proc__deref_interp_head(py_proc_t * self) { } else FAIL; - if (_py_proc__check_interp_state(self, interp_head_raddr)) + if (fail(_py_proc__check_interp_state(self, interp_head_raddr))) { + log_d("Interpreter state check failed while dereferencing symbol"); FAIL; + } self->is_raddr = interp_head_raddr; @@ -613,8 +641,8 @@ _py_proc__find_interpreter_state(py_proc_t * self) { void * tstate_current_raddr; // First try to de-reference interpreter head as the most reliable method - if (_py_proc__deref_interp_head(self)) { - log_d("Cannot dereference PyInterpreterState head from symbols"); + if (fail(_py_proc__deref_interp_head(self))) { + log_d("Cannot dereference PyInterpreterState head from symbols (pid: %d)", self->pid); // If that fails try to get the current thread state (can be NULL during idle) tstate_current_raddr = _py_proc__get_current_thread_state_raddr(self); if (tstate_current_raddr == NULL || tstate_current_raddr == (void *) -1) @@ -659,10 +687,17 @@ _py_proc__wait_for_interp_state(py_proc_t * self) { self->is_raddr = NULL; + self->bss = realloc(self->bss, self->map.bss.size); + if (!isvalid(self->bss)) { + log_e("Cannot allocate memory for BSS scan (pid: %d)", self->pid); + FAIL; + } + TIMER_RESET TIMER_START if (!py_proc__is_running(self)) { set_error(EPROCNPID); + log_e("Process %d is not running.", self->pid); FAIL; } @@ -675,16 +710,11 @@ _py_proc__wait_for_interp_state(py_proc_t * self) { #endif if (is_fatal(austin_errno)) { log_d( - "Terminatig _py_proc__wait_for_interp_state loop because of fatal error code %d", + "Terminating _py_proc__wait_for_interp_state loop because of fatal error code %d", austin_errno ); FAIL; } - if (self->bss == NULL) { - self->bss = malloc(self->map.bss.size); - } - if (self->bss == NULL) - FAIL; switch (_py_proc__scan_bss(self)) { case 0: @@ -693,6 +723,11 @@ _py_proc__wait_for_interp_state(py_proc_t * self) { case OUT_OF_BOUND: TIMER_STOP } + + // Try once for child processes. + if (self->child) + TIMER_STOP + #ifdef DEREF_SYM } else { TIMER_STOP @@ -700,10 +735,7 @@ _py_proc__wait_for_interp_state(py_proc_t * self) { #endif TIMER_END - if (self->bss != NULL) { - free(self->bss); - self->bss = NULL; - } + sfree(self->bss); // NOTE: This case should not happen anymore as the addresses have been // corrected. @@ -720,11 +752,14 @@ _py_proc__wait_for_interp_state(py_proc_t * self) { SUCCESS; } + if (self->child) + FAIL; + #ifdef CHECK_HEAP log_w("BSS scan unsuccessful so we scan the heap directly ..."); // TODO: Consider copying heap over and check for pointers - TIMER_SET(10) + TIMER_SET(100) TIMER_START switch (_py_proc__scan_heap(self)) { case 0: @@ -765,12 +800,9 @@ _py_proc__run(py_proc_t * self) { sfree(self->lib_path); self->sym_loaded = 0; - if (success(_py_proc__init(self))) + if (success(_py_proc__init(self))) { + log_d("Process is ready"); break; - - if (is_fatal(austin_errno)) { - log_d("Terminatig _py_proc__run loop because of fatal error code %d", austin_errno); - FAIL; } log_d("Process is not ready"); @@ -901,19 +933,13 @@ py_proc__attach(py_proc_t * self, pid_t pid) { #endif if (fail(_py_proc__run(self))) { - #if defined PL_WIN - if (fail(_py_proc__try_child_proc(self))) { - #endif - if (austin_errno == EPROCNPID) { - set_error(EPROCATTACH); - } - else { - log_ie("Cannot attach to running process."); - } - FAIL; - #if defined PL_WIN + if (austin_errno == EPROCNPID) { + set_error(EPROCATTACH); } - #endif + else { + log_ie("Cannot attach to running process."); + } + FAIL; } SUCCESS; @@ -1042,16 +1068,10 @@ py_proc__start(py_proc_t * self, const char * exec, char * argv[]) { log_d("New process created with PID %d", self->pid); if (fail(_py_proc__run(self))) { - #if defined PL_WIN - if (fail(_py_proc__try_child_proc(self))) { - #endif - if (austin_errno == EPROCNPID) - set_error(EPROCFORK); - log_ie("Cannot start new process"); - FAIL; - #if defined PL_WIN - } - #endif + if (austin_errno == EPROCNPID) + set_error(EPROCFORK); + log_ie("Cannot start new process"); + FAIL; } #ifdef NATIVE diff --git a/src/py_proc_list.c b/src/py_proc_list.c index 3c4b2840..f5643311 100644 --- a/src/py_proc_list.c +++ b/src/py_proc_list.c @@ -24,6 +24,8 @@ #if defined PL_LINUX #include + +#include "linux/common.h" #elif defined PL_MACOS #include #elif defined PL_WIN @@ -37,6 +39,7 @@ #include "hints.h" #include "logging.h" +#include "resources.h" #include "timing.h" #include "py_proc_list.h" @@ -64,7 +67,7 @@ _py_proc_list__add(py_proc_list_t * self, py_proc_t * py_proc) { self->first = item; // Update index table. - self->index[py_proc->pid] = py_proc; + lookup__set(self->py_proc_for_pid, py_proc->pid, py_proc); self->count++; @@ -75,7 +78,7 @@ _py_proc_list__add(py_proc_list_t * self, py_proc_t * py_proc) { // ---------------------------------------------------------------------------- static int _py_proc_list__has_pid(py_proc_list_t * self, pid_t pid) { - return self->index[pid] != NULL; + return isvalid(lookup__get(self->py_proc_for_pid, pid)); } /* _py_proc_list__has_pid */ @@ -86,7 +89,7 @@ _py_proc_list__remove(py_proc_list_t * self, py_proc_item_t * item) { pid_t pid = item->py_proc->pid; #endif - self->index[item->py_proc->pid] = NULL; + lookup__del(self->py_proc_for_pid, item->py_proc->pid); if (item == self->first) self->first = item->next; @@ -113,18 +116,15 @@ py_proc_list_new(py_proc_t * parent_py_proc) { if (!isvalid(list)) return NULL; - list->pids = pid_max(); - log_t("Maximum number of PIDs: %d", list->pids); - list->index = (py_proc_t **) calloc(list->pids, sizeof(py_proc_t *)); - if (list->index == NULL) - goto release; + list->py_proc_for_pid = lookup_new(256); + if (!isvalid(list->py_proc_for_pid)) + goto error; - list->pid_table = (pid_t *) calloc(list->pids, sizeof(pid_t)); - if (list->pid_table == NULL) { - free(list->index); - goto release; + list->ppid_for_pid = lookup_new(1024); + if (!isvalid(list->ppid_for_pid)) { + goto error; } // Add the parent process to the list. @@ -132,17 +132,18 @@ py_proc_list_new(py_proc_t * parent_py_proc) { return list; -release: +error: py_proc_list__destroy(list); + return NULL; } /* py_proc_list_new */ // ---------------------------------------------------------------------------- void -py_proc_list__add_proc_children(py_proc_list_t * self, pid_t ppid) { - for (register pid_t pid = 0; pid <= self->max_pid; pid++) { - if (self->pid_table[pid] == ppid && !_py_proc_list__has_pid(self, pid)) { +py_proc_list__add_proc_children(py_proc_list_t * self, uintptr_t ppid) { + lookup__iteritems_start(self->ppid_for_pid, key_dt, pid, value_t, pid_ppid) { + if (pid_ppid == (value_t) ppid && !_py_proc_list__has_pid(self, pid)) { py_proc_t * child_proc = py_proc_new(TRUE); if (child_proc == NULL) continue; @@ -156,7 +157,7 @@ py_proc_list__add_proc_children(py_proc_list_t * self, pid_t ppid) { py_proc__log_version(child_proc, FALSE); py_proc_list__add_proc_children(self, pid); } - } + } lookup__iter_stop(self->ppid_for_pid); } /* py_proc_list__add_proc_children */ @@ -202,18 +203,18 @@ py_proc_list__update(py_proc_list_t * self) { if (now - self->timestamp < UPDATE_INTERVAL) return; // Do not update too frequently as this is an expensive operation. - memset(self->pid_table, 0, self->pids * sizeof(pid_t)); - self->max_pid = 0; + lookup__clear(self->ppid_for_pid); // Update PID table #if defined PL_LINUX /* LINUX */ - char stat_path[32]; - char buffer[1024]; - struct dirent *ent; + char buffer[1024]; + struct dirent * ent; - DIR * proc_dir = opendir("/proc"); - if (proc_dir == NULL) - goto finally; + cu_DIR * proc_dir = opendir("/proc"); + if (!isvalid(proc_dir)) { + log_e("Failed to open /proc directory"); + return; + } for (;;) { // This code is inspired by the ps util @@ -222,49 +223,61 @@ py_proc_list__update(py_proc_list_t * self) { if ((*ent->d_name <= '0') || (*ent->d_name > '9')) continue; unsigned long pid = strtoul(ent->d_name, NULL, 10); - sprintf(stat_path, "/proc/%ld/stat", pid); - - FILE * stat_file = fopen(stat_path, "rb"); + cu_FILE * stat_file = _procfs(pid, "stat"); if (stat_file == NULL) continue; - char * line = NULL; + cu_char * line = NULL; size_t n = 0; if (getline(&line, &n, stat_file) < 0) { log_w("Failed to read stat file for process %d", pid); - goto release; + return; } char * stat = strchr(line, ')'); - if (!isvalid(stat)) - goto failed; + if (!isvalid(stat)) { + log_e("Failed to parse stat file for process %d", pid); + return; + } stat += 2; if (stat[0] == ' ') stat++; - if (sscanf(stat, "%c %d", (char *)buffer, &(self->pid_table[pid])) != 2) - goto failed; - - if (pid > self->max_pid) - self->max_pid = pid; - - goto release; + + #ifdef __arm__ + #define STAT_FMT "%c %d" + #else + #define STAT_FMT "%c %ld" + #endif + + uintptr_t ppid; + if (sscanf(stat, STAT_FMT, (char *)buffer, &ppid) != 2) { + log_e("Failed to parse stat file for process %d", pid); + return; + } - failed: - log_w("Failed to parse stat file for process %d", pid); + lookup__set(self->ppid_for_pid, pid, (value_t) ppid); - release: - fclose(stat_file); - sfree(line); } - closedir(proc_dir); - #elif defined PL_MACOS /* MACOS */ - int pid_list[PID_MAX]; + cu_int * pid_list = NULL; int n_pids = proc_listallpids(NULL, 0); - if (!n_pids || proc_listallpids(pid_list, sizeof(pid_list)) == -1) - goto finally; + if (n_pids <= 0) { + log_e("Failed to get the number of PIDs"); + return; + } + + pid_list = (int *) calloc(n_pids, sizeof(int)); + if (!isvalid(pid_list)) { + log_e("Failed to allocate memory for PID list"); + return; + } + + if (proc_listallpids(pid_list, n_pids) == -1) { + log_e("Failed to get list of all PIDs"); + return; + } for (register int i = 0; i < n_pids; i++) { struct proc_bsdinfo proc; @@ -272,29 +285,22 @@ py_proc_list__update(py_proc_list_t * self) { if (proc_pidinfo(pid_list[i], PROC_PIDTBSDINFO, 0, &proc, PROC_PIDTBSDINFO_SIZE) == -1) continue; - self->pid_table[pid_list[i]] = proc.pbi_ppid; - if (pid_list[i] > self->max_pid) - self->max_pid = pid_list[i]; + lookup__set(self->ppid_for_pid, pid_list[i], (value_t) (uintptr_t) proc.pbi_ppid); } #elif defined PL_WIN /* WIN */ - HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + cu_HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (h == INVALID_HANDLE_VALUE) - goto finally; + return; PROCESSENTRY32 pe = { 0 }; pe.dwSize = sizeof(PROCESSENTRY32); if (Process32First(h, &pe)) { do { - self->pid_table[pe.th32ProcessID] = pe.th32ParentProcessID; - - if (pe.th32ProcessID > self->max_pid) - self->max_pid = pe.th32ProcessID; + lookup__set(self->ppid_for_pid, pe.th32ProcessID, (value_t) (uintptr_t) pe.th32ParentProcessID); } while (Process32Next(h, &pe)); } - - CloseHandle(h); #endif log_t("PID table populated"); @@ -315,7 +321,6 @@ py_proc_list__update(py_proc_list_t * self) { } } -finally: self->timestamp = now; } /* py_proc_list__update */ @@ -337,7 +342,11 @@ py_proc_list__destroy(py_proc_list_t * self) { while (self->first) _py_proc_list__remove(self, self->first); - sfree(self->index); - sfree(self->pid_table); + lookup__destroy(self->py_proc_for_pid); + self->py_proc_for_pid = NULL; + + lookup__destroy(self->ppid_for_pid); + self->ppid_for_pid = NULL; + free(self); } /* py_proc_list__destroy */ diff --git a/src/py_proc_list.h b/src/py_proc_list.h index 1bae2336..b9da3d84 100644 --- a/src/py_proc_list.h +++ b/src/py_proc_list.h @@ -23,8 +23,9 @@ #ifndef PY_PROC_LIST_H #define PY_PROC_LIST_H - +#include "cache.h" #include "py_proc.h" +#include "resources.h" typedef struct _py_proc_item { @@ -35,13 +36,11 @@ typedef struct _py_proc_item { typedef struct { - int count; // Number of entries in the list - py_proc_item_t * first; // First item in the list - py_proc_t ** index; // Index of PIDs in the list - pid_t * pid_table; // Table of pids with their parents - pid_t max_pid; // Highest seen PID in the index - int pids; // Maximum number of PIDs in the index - ctime_t timestamp; // Timestamp of the last update + int count; // Number of entries in the list + py_proc_item_t * first; // First item in the list + lookup_t * py_proc_for_pid; // PID to py_proc_t lookup table + lookup_t * ppid_for_pid; // PID to PPID lookup table + ctime_t timestamp; // Timestamp of the last update } py_proc_list_t; @@ -72,10 +71,10 @@ py_proc_list__is_empty(py_proc_list_t *); * Add the the children of the given process to the list. * * @param py_proc_list_t the list. - * @param pid_t the PID of the parent process. + * @param uintptr_t the PID of the parent process. */ void -py_proc_list__add_proc_children(py_proc_list_t *, pid_t); +py_proc_list__add_proc_children(py_proc_list_t *, uintptr_t); /** @@ -134,4 +133,7 @@ void py_proc_list__destroy(py_proc_list_t *); +CLEANUP_TYPE(py_proc_list_t, py_proc_list__destroy); +#define cu_py_proc_list_t __attribute__((cleanup(py_proc_list__destroyt))) py_proc_list_t + #endif diff --git a/src/py_thread.c b/src/py_thread.c index 1595a28b..045ffebd 100644 --- a/src/py_thread.c +++ b/src/py_thread.c @@ -173,7 +173,7 @@ _py_thread__resolve_py_stack(py_thread_t * self) { frame_t * frame = lru_cache__maybe_hit(cache, frame_key); if (!isvalid(frame)) { - frame = _frame_from_code_raddr(self, py_frame.code, lasti, self->proc->py_v); + frame = _frame_from_code_raddr(self->proc, py_frame.code, lasti, self->proc->py_v); if (!isvalid(frame)) { log_ie("Failed to get frame from code object"); // Truncate the stack to the point where we have successfully resolved. @@ -513,8 +513,7 @@ py_thread__is_interrupted(py_thread_t * self) { int py_thread__save_kernel_stack(py_thread_t * self) { - char stack_path[48]; - int fd; + char stack_path[48]; if (!isvalid(_kstacks)) FAIL; @@ -522,17 +521,15 @@ py_thread__save_kernel_stack(py_thread_t * self) { sfree(_kstacks[self->tid]); sprintf(stack_path, "/proc/%d/task/" TID_FMT "/stack", self->proc->pid, self->tid); - fd = open(stack_path, O_RDONLY); + cu_fd fd = open(stack_path, O_RDONLY); if (fd == -1) FAIL; _kstacks[self->tid] = (char *) calloc(1, MAX_STACK_FILE_SIZE); if (read(fd, _kstacks[self->tid], MAX_STACK_FILE_SIZE) == -1) { log_e("stack: failed to read %s", stack_path); - close(fd); FAIL; }; - close(fd); SUCCESS; } @@ -686,7 +683,7 @@ _py_thread__unwind_native_frame_stack(py_thread_t * self) { } } - frame = frame_new(frame_key, filename, scope, offset); + frame = frame_new(frame_key, filename, scope, offset, 0, 0, 0); if (!isvalid(frame)) { log_ie("Failed to make native frame"); FAIL; @@ -808,10 +805,12 @@ py_thread__fill_from_raddr(py_thread_t * self, raddr_t * raddr, py_proc_t * proc } else if ( likely(proc->extra->pthread_tid_offset) - && success(read_pthread_t(self->raddr.pref, (void *) self->tid + && success(read_pthread_t(self->proc, (void *) self->tid ))) { int o = proc->extra->pthread_tid_offset; - self->tid = o > 0 ? _pthread_buffer[o] : (pid_t) ((pid_t *) _pthread_buffer)[-o]; + self->tid = o > 0 + ? proc->extra->_pthread_buffer[o] + : (pid_t) ((pid_t *) proc->extra->_pthread_buffer)[-o]; if (self->tid >= max_pid || self->tid == 0) { log_e("Invalid TID detected"); self->tid = 0; diff --git a/src/resources.h b/src/resources.h new file mode 100644 index 00000000..48e08c4c --- /dev/null +++ b/src/resources.h @@ -0,0 +1,152 @@ +// This file is part of "austin" which is released under GPL. +// +// See file LICENCE or go to http://www.gnu.org/licenses/ for full license +// details. +// +// Austin is a Python frame stack sampler for CPython. +// +// Copyright (c) 2023 Gabriele N. Tornetta . +// All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include "hints.h" +#include "platform.h" + +#define CLEANUP_TYPE(type, func) \ + static inline void func##t(type **v) \ + { \ + if (isvalid(*v)) \ + { \ + func(*v); \ + *v = NULL; \ + } \ + } \ + struct __allow_semicolon__ + +#define CLEANUP_FUNC_SENTINEL(type, func, sentinel) \ + static inline void func##type(type *v) \ + { \ + if (*v != sentinel) \ + { \ + func(*v); \ + *v = sentinel; \ + } \ + } \ + struct __allow_semicolon__ + +#define CLEANUP_FUNC(type, func) \ + static inline void func##type(type **v) \ + { \ + if (isvalid(*v)) \ + { \ + func(*v); \ + *v = NULL; \ + } \ + } \ + struct __allow_semicolon__ + +CLEANUP_FUNC(void, free); +#define cu_void __attribute__((cleanup(freevoid))) void + +CLEANUP_FUNC(char, free); +#define cu_char __attribute__((cleanup(freechar))) char + +typedef unsigned char uchar; +CLEANUP_FUNC(uchar, free); +#define cu_uchar __attribute__((cleanup(freeuchar))) uchar + +CLEANUP_FUNC(int, free); +#define cu_int __attribute__((cleanup(freeint))) int + +CLEANUP_FUNC(FILE, fclose); +#define cu_FILE __attribute__((cleanup(fcloseFILE))) FILE + +// ---- Unix ---- + +#if defined PL_UNIX +#include +#include +#include + +typedef struct +{ + void *addr; + size_t size; +} map_t; + +#define _popen popen +#define _pclose pclose + +static inline map_t * +map_new(int fd, size_t size, int flags) +{ + void *addr = mmap(0, size, PROT_READ, flags, fd, 0); + if (!isvalid(addr)) + return NULL; + + map_t *map = malloc(sizeof(map_t)); + if (map == MAP_FAILED) + { + munmap(map, size); + return NULL; + } + + map->size = size; + map->addr = addr; + + return map; +} + +static inline void +map__destroy(map_t *map) +{ + if (isvalid(map)) + { + munmap(map->addr, map->size); + free(map); + } +} + +CLEANUP_FUNC(map_t, map__destroy); +#define cu_map_t __attribute__((cleanup(map__destroymap_t))) map_t + +CLEANUP_FUNC_SENTINEL(int, close, -1); +#define cu_fd __attribute__((cleanup(closeint))) int + +#endif // PL_UNIX + +CLEANUP_FUNC(FILE, _pclose); +#define cu_pipe __attribute__((cleanup(_pcloseFILE))) FILE + +// ---- Linux resources ---- + +#if defined PL_LINUX +#include + +CLEANUP_FUNC(DIR, closedir); +#define cu_DIR __attribute__((cleanup(closedirDIR))) DIR + +#endif // PL_LINUX + +// ---- Windows resources ---- + +#if defined PL_WIN +CLEANUP_FUNC_SENTINEL(HANDLE, CloseHandle, INVALID_HANDLE_VALUE); +#define cu_HANDLE __attribute__((cleanup(CloseHandleHANDLE))) HANDLE + +CLEANUP_FUNC_SENTINEL(LPVOID, UnmapViewOfFile, NULL); +#define cu_VOF __attribute__((cleanup(UnmapViewOfFileLPVOID))) LPVOID +#endif // PL_WIN diff --git a/src/stack.h b/src/stack.h index 134bc22d..036dbf9d 100644 --- a/src/stack.h +++ b/src/stack.h @@ -27,6 +27,7 @@ #include #include "cache.h" +#include "frame.h" #include "hints.h" #include "mojo.h" #include "platform.h" @@ -35,23 +36,6 @@ #include "py_thread.h" #include "version.h" -typedef struct { - key_dt key; - char * filename; - char * scope; - unsigned int line; -} frame_t; - -#define py_frame_key(code, lasti) (((key_dt) (((key_dt) code) & MOJO_INT32) << 16) | lasti) -#define py_string_key(code, field) ((key_dt) *((void **) ((void *) &code + py_v->py_code.field))) - -#ifdef PY_THREAD_C - -typedef struct { - void * origin; - void * code; - int lasti; -} py_frame_t; typedef struct { size_t size; @@ -69,21 +53,6 @@ typedef struct { static stack_dt * _stack; -static inline frame_t * -frame_new(key_dt key, char * filename, char * scope, unsigned int line) { - frame_t * frame = (frame_t *) malloc(sizeof(frame_t)); - if (!isvalid(frame)) { - return NULL; - } - - frame->key = key; - frame->filename = filename; - frame->scope = scope; - frame->line = line; - - return frame; -} - static inline int stack_allocate(size_t size) { if (isvalid(_stack)) @@ -119,9 +88,7 @@ stack_deallocate(void) { free(_stack); } -#ifdef NATIVE -#define CFRAME_MAGIC ((void*) 0xCF) -#endif + static inline int stack_has_cycle(void) { @@ -180,210 +147,4 @@ stack_py_push(void * origin, void * code, int lasti) { #define stack_kernel_reset() {_stack->kernel_pointer = 0;} #endif - -// ---------------------------------------------------------------------------- -#define _code__get_filename(self, pref, py_v) \ - _string_from_raddr( \ - pref, *((void **) ((void *) self + py_v->py_code.o_filename)), py_v \ - ) - -#define _code__get_name(self, pref, py_v) \ - _string_from_raddr( \ - pref, *((void **) ((void *) self + py_v->py_code.o_name)), py_v \ - ) - -#define _code__get_qualname(self, pref, py_v) \ - _string_from_raddr( \ - pref, *((void **) ((void *) self + py_v->py_code.o_qualname)), py_v \ - ) - -#define _code__get_lnotab(self, pref, len, py_v) \ - _bytes_from_raddr( \ - pref, *((void **) ((void *) self + py_v->py_code.o_lnotab)), len, py_v \ - ) - - -// ---------------------------------------------------------------------------- -static inline int -_read_varint(unsigned char * lnotab, size_t * i) { - int val = lnotab[++*i] & 63; - int shift = 0; - while (lnotab[*i] & 64) { - shift += 6; - val |= (lnotab[++*i] & 63) << shift; - } - return val; -} - - -// ---------------------------------------------------------------------------- -static inline int -_read_signed_varint(unsigned char * lnotab, size_t * i) { - int val = _read_varint(lnotab, i); - return (val & 1) ? -(val >> 1) : (val >> 1); -} - -// ---------------------------------------------------------------------------- -static inline frame_t * -_frame_from_code_raddr(py_thread_t * py_thread, void * code_raddr, int lasti, python_v * py_v) { - PyCodeObject code; - unsigned char * lnotab = NULL; - py_proc_t * py_proc = py_thread->proc; - proc_ref_t pref = py_thread->raddr.pref; - - if (fail(copy_py(pref, code_raddr, py_code, code))) { - log_ie("Cannot read remote PyCodeObject"); - return NULL; - } - - lru_cache_t * cache = py_proc->string_cache; - - key_dt string_key = py_string_key(code, o_filename); - char * filename = lru_cache__maybe_hit(cache, string_key); - if (!isvalid(filename)) { - filename = _code__get_filename(&code, pref, py_v); - if (!isvalid(filename)) { - log_ie("Cannot get file name from PyCodeObject"); - return NULL; - } - lru_cache__store(cache, string_key, filename); - if (pargs.binary) { - mojo_string_event(string_key, filename); - } - } - if (pargs.binary) { - filename = (char *) string_key; - } - - string_key = V_MIN(3, 11) ? py_string_key(code, o_qualname) : py_string_key(code, o_name); - char * scope = lru_cache__maybe_hit(cache, string_key); - if (!isvalid(scope)) { - scope = V_MIN(3, 11) - ? _code__get_qualname(&code, pref, py_v) - : _code__get_name(&code, pref, py_v); - if (!isvalid(scope)) { - log_ie("Cannot get scope name from PyCodeObject"); - return NULL; - } - lru_cache__store(cache, string_key, scope); - if (pargs.binary) { - mojo_string_event(string_key, scope); - } - } - if (pargs.binary) { - scope = (char *) string_key; - } - - ssize_t len = 0; - int lineno = V_FIELD(unsigned int, code, py_code, o_firstlineno); - - if (V_MIN(3, 11)) { - lnotab = _code__get_lnotab(&code, pref, &len, py_v); - if (!isvalid(lnotab) || len == 0) { - log_ie("Cannot get line information from PyCodeObject"); - goto failed; - } - - lasti >>= 1; - - for (size_t i = 0, bc = 0; i < len; i++) { - bc += (lnotab[i] & 7) + 1; - int code = (lnotab[i] >> 3) & 15; - switch (code) { - case 15: - break; - - case 14: // Long form - lineno += _read_signed_varint(lnotab, &i); - _read_varint(lnotab, &i); // end line - _read_varint(lnotab, &i); // column - _read_varint(lnotab, &i); // end column - break; - - case 13: // No column data - lineno += _read_signed_varint(lnotab, &i); - break; - - case 12: // New lineno - case 11: - case 10: - lineno += code - 10; - i += 2; // skip column + end column - break; - - default: - i++; // skip column - } - - if (bc > lasti) - break; - } - } - else { - lnotab = _code__get_lnotab(&code, pref, &len, py_v); - if (!isvalid(lnotab) || len % 2) { - log_ie("Cannot get line information from PyCodeObject"); - goto failed; - } - - if (V_MIN(3, 10)) { - lasti <<= 1; - for (register int i = 0, bc = 0; i < len; i++) { - int sdelta = lnotab[i++]; - if (sdelta == 0xff) - break; - - bc += sdelta; - - int ldelta = lnotab[i]; - if (ldelta == 0x80) - ldelta = 0; - else if (ldelta > 0x80) - lineno -= 0x100; - - lineno += ldelta; - if (bc > lasti) - break; - } - } - else { // Python < 3.10 - for (register int i = 0, bc = 0; i < len; i++) { - bc += lnotab[i++]; - if (bc > lasti) - break; - - if (lnotab[i] >= 0x80) - lineno -= 0x100; - - lineno += lnotab[i]; - } - } - } - - free(lnotab); - - frame_t * frame = frame_new(py_frame_key(code_raddr, lasti), filename, scope, lineno); - if (!isvalid(frame)) { - log_e("Failed to create frame object"); - goto failed; - } - - return frame; - -failed: - sfree(lnotab); - - return NULL; -} - - -#endif // PY_THREAD_C - - -// ---------------------------------------------------------------------------- -static inline void -frame__destroy(frame_t * self) { - sfree(self); -} - #endif // STACK_H diff --git a/src/win/py_proc.h b/src/win/py_proc.h index e6119fae..2454f237 100644 --- a/src/win/py_proc.h +++ b/src/win/py_proc.h @@ -23,9 +23,11 @@ #ifdef PY_PROC_C #include +#include #include #include "../py_proc.h" +#include "../resources.h" #define CHECK_HEAP @@ -59,64 +61,147 @@ map_addr_from_rva(void * bin, DWORD rva) { // ---------------------------------------------------------------------------- +// Helper to compare file paths that might differ in e.g. the case. Here we use +// the file information to reliably determine if two paths refer to the same +// file. static int -_py_proc__analyze_pe(py_proc_t * self, char * path) { - HANDLE hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0); - LPVOID pMapping = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); +pathcmp(char * a, char * b) { + cu_HANDLE ha = CreateFile(a, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + cu_HANDLE hb = CreateFile(b, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + + BY_HANDLE_FILE_INFORMATION ia, ib; + GetFileInformationByHandle(ha, &ia); + GetFileInformationByHandle(hb, &ib); + + return !( + ia.dwVolumeSerialNumber == ib.dwVolumeSerialNumber + && ia.nFileIndexLow == ib.nFileIndexLow + && ia.nFileIndexHigh == ib.nFileIndexHigh + ); +} + + +// ---------------------------------------------------------------------------- +static int +_py_proc__analyze_pe(py_proc_t * self, char * path, void * base) { + cu_HANDLE hFile = CreateFile(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + cu_HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, 0); + cu_VOF pMapping = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); IMAGE_DOS_HEADER * dos_hdr = (IMAGE_DOS_HEADER *) pMapping; IMAGE_NT_HEADERS * nt_hdr = (IMAGE_NT_HEADERS *) (pMapping + dos_hdr->e_lfanew); IMAGE_SECTION_HEADER * s_hdr = (IMAGE_SECTION_HEADER *) (pMapping + dos_hdr->e_lfanew + sizeof(IMAGE_NT_HEADERS)); if (nt_hdr->Signature != IMAGE_NT_SIGNATURE) - self->sym_loaded = 0; + FAIL; - else { - void * base = self->map.bss.base; + // ---- Find the .data section ---- + for (register int i = 0; i < nt_hdr->FileHeader.NumberOfSections; i++) { + if (strcmp(".data", (const char *) s_hdr[i].Name) == 0) { + self->map.bss.base = base + s_hdr[i].VirtualAddress; + self->map.bss.size = s_hdr[i].Misc.VirtualSize; + break; + } + } - // ---- Find the .data section ---- - for (register int i = 0; i < nt_hdr->FileHeader.NumberOfSections; i++) { - if (strcmp(".data", (const char *) s_hdr[i].Name) == 0) { - self->map.bss.base += s_hdr[i].VirtualAddress; - self->map.bss.size = s_hdr[i].Misc.VirtualSize; - break; - } + // ---- Search for exports ---- + self->sym_loaded = 0; + + IMAGE_EXPORT_DIRECTORY * e_dir = (IMAGE_EXPORT_DIRECTORY *) map_addr_from_rva( + pMapping, nt_hdr->OptionalHeader.DataDirectory[0].VirtualAddress + ); + if (e_dir != NULL) { + DWORD * names = (DWORD *) map_addr_from_rva(pMapping, e_dir->AddressOfNames); + WORD * idx_tab = (WORD *) map_addr_from_rva(pMapping, e_dir->AddressOfNameOrdinals); + DWORD * addrs = (DWORD *) map_addr_from_rva(pMapping, e_dir->AddressOfFunctions); + for ( + register int i = 0; + self->sym_loaded < SYMBOLS && i < e_dir->NumberOfNames; + i++ + ) { + char * sym_name = (char *) map_addr_from_rva(pMapping, names[i]); + self->sym_loaded += _py_proc__check_sym(self, sym_name, addrs[idx_tab[i]] + base); } + } - // ---- Search for exports ---- - self->sym_loaded = 0; + return !self->sym_loaded; +} - IMAGE_EXPORT_DIRECTORY * e_dir = (IMAGE_EXPORT_DIRECTORY *) map_addr_from_rva( - pMapping, nt_hdr->OptionalHeader.DataDirectory[0].VirtualAddress - ); - if (e_dir != NULL) { - DWORD * names = (DWORD *) map_addr_from_rva(pMapping, e_dir->AddressOfNames); - WORD * idx_tab = (WORD *) map_addr_from_rva(pMapping, e_dir->AddressOfNameOrdinals); - DWORD * addrs = (DWORD *) map_addr_from_rva(pMapping, e_dir->AddressOfFunctions); - for ( - register int i = 0; - self->sym_loaded < SYMBOLS && i < e_dir->NumberOfNames; - i++ - ) { - char * sym_name = (char *) map_addr_from_rva(pMapping, names[i]); - self->sym_loaded += _py_proc__check_sym(self, sym_name, addrs[idx_tab[i]] + base); + +// ---------------------------------------------------------------------------- +// Forward declaration. +static int +_py_proc__run(py_proc_t *); + + +// On Windows, if we fail with the parent process we look if it has a single +// child and try to attach to that instead. We keep going until we either find +// a single Python process or more or less than a single child. +static int +_py_proc__try_child_proc(py_proc_t * self) { + cu_HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (h == INVALID_HANDLE_VALUE) { + log_e("Cannot inspect processes details"); + FAIL; + } + + HANDLE orig_hproc = self->proc_ref; + pid_t orig_pid = self->pid; + + for(;;) { + pid_t parent_pid = self->pid; + + PROCESSENTRY32 pe = { 0 }; + pe.dwSize = sizeof(PROCESSENTRY32); + + if (Process32First(h, &pe)) { + pid_t child_pid = 0; + do { + if (pe.th32ParentProcessID == parent_pid) { + if (child_pid) { + log_d("Process has more than one child"); + goto rollback; + } + child_pid = pe.th32ProcessID; + } + } while (Process32Next(h, &pe)); + + if (!child_pid) { + log_d("Process has no children"); + goto rollback; + } + + self->pid = child_pid; + self->proc_ref = OpenProcess( + PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, child_pid + ); + if (self->proc_ref == INVALID_HANDLE_VALUE) { + log_e("Cannot open child process handle"); + goto rollback; + } + if (success(_py_proc__run(self))) { + log_d("Process has a single Python child with PID %d. We will attach to that", child_pid); + SUCCESS; + } + else { + log_d("Process has a single non-Python child with PID %d. Taking it as new parent", child_pid); + CloseHandle(self->proc_ref); } } } - UnmapViewOfFile(pMapping); - CloseHandle(hMapping); - CloseHandle(hFile); - - return !self->sym_loaded; +rollback: + self->pid = orig_pid; + self->proc_ref = orig_hproc; + + FAIL; } // ---------------------------------------------------------------------------- static int _py_proc__get_modules(py_proc_t * self) { - HANDLE mod_hdl; + cu_HANDLE mod_hdl; mod_hdl = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, self->pid); if (mod_hdl == INVALID_HANDLE_VALUE) FAIL; @@ -130,43 +215,143 @@ _py_proc__get_modules(py_proc_t * self) { sfree(self->bin_path); sfree(self->lib_path); - BOOL success = Module32First(mod_hdl, &module); - while (success) { + cu_void * pd_mem = calloc(1, sizeof(struct proc_desc)); + if (!isvalid(pd_mem)) { + log_ie("Cannot allocate memory for proc_desc"); + FAIL; + } + struct proc_desc * pd = pd_mem; + cu_char * prev_path = NULL; + cu_char * needle_path = NULL; + struct vm_map * map = NULL; + + if (GetModuleFileNameEx(self->proc_ref, NULL, pd->exe_path, sizeof(pd->exe_path)) == 0) { + log_ie("Cannot get executable path"); + FAIL; + } + log_d("Executable path: %s", pd->exe_path); + + if (!Module32First(mod_hdl, &module)) { + log_ie("Cannot get first module"); + FAIL; + } + do { if ((void *) module.modBaseAddr < self->min_raddr) self->min_raddr = module.modBaseAddr; if ((void *) module.modBaseAddr + module.modBaseSize > self->max_raddr) self->max_raddr = module.modBaseAddr + module.modBaseSize; + if (isvalid(prev_path) && strcmp(module.szExePath, prev_path) == 0) { // Avoid analysing a binary multiple times + continue; + } + + sfree(prev_path); + prev_path = strdup(module.szExePath); + if (!isvalid(prev_path)) { + log_ie("Cannot duplicate path name"); + FAIL; + } + log_t( "%p-%p: Module %s", module.modBaseAddr, module.modBaseAddr + module.modBaseSize, - module.szModule + module.szExePath ); - if ( - self->bin_path == NULL \ - && strcmp(module.szModule, "py.exe") \ - && strstr(module.szModule, ".exe") \ - ) { - log_d("Candidate binary: %s (size %d KB)", module.szModule, module.modBaseSize >> 10); - self->bin_path = strdup(module.szExePath); + + // The first memory map of the executable + if (!isvalid(pd->maps[MAP_BIN].path) && pathcmp(pd->exe_path, module.szExePath) == 0) { + map = &(pd->maps[MAP_BIN]); + map->path = strdup(module.szExePath); + if (!isvalid(map->path)) { + log_ie("Cannot duplicate path name"); + FAIL; + } + map->file_size = module.modBaseSize; + map->base = (void *) module.modBaseAddr; + map->size = module.modBaseSize; + map->has_symbols = success(_py_proc__analyze_pe(self, module.szExePath, (void *) module.modBaseAddr)); + if (map->has_symbols) { + map->bss_base = self->map.bss.base; + map->bss_size = self->map.bss.size; + } + log_d("Binary map: %s (symbols %d)", map->path, map->has_symbols); + continue; } - if (!self->sym_loaded && strstr(module.szModule, ".dll") && module.modBaseSize > (1 << 20)) { - self->map.bss.base = module.modBaseAddr; // WARNING: Not the BSS base yet! - if (success(_py_proc__analyze_pe(self, module.szExePath))) { - log_d("Candidate library: %s (size %d KB)", module.szModule, module.modBaseSize >> 10); - sfree(self->lib_path); - self->lib_path = strdup(module.szExePath); + + // The first memory map of the shared library (if any) + char * needle = strstr(module.szExePath, "python"); + if (!isvalid(pd->maps[MAP_LIBSYM].path) && isvalid(needle)) { + int has_symbols = success(_py_proc__analyze_pe(self, module.szExePath, (void *) module.modBaseAddr)); + if (has_symbols) { + map = &(pd->maps[MAP_LIBSYM]); + map->path = strdup(module.szExePath); + if (!isvalid(map->path)) { + log_ie("Cannot duplicate path name"); + FAIL; + } + map->file_size = module.modBaseSize; + map->base = (void *) module.modBaseAddr; + map->size = module.modBaseSize; + map->has_symbols = TRUE; + map->bss_base = self->map.bss.base; + map->bss_size = self->map.bss.size; + + log_d("Library map: %s (with symbols)", map->path); + + continue; + } + + // The first memory map of a binary that contains "pythonX.Y" in its name + if (!isvalid(pd->maps[MAP_LIBNEEDLE].path)) { + if (isvalid(needle)) { + unsigned int v; + if (sscanf(needle, "python%u.%u", &v, &v) == 2) { + map = &(pd->maps[MAP_LIBNEEDLE]); + map->path = needle_path = strdup(module.szExePath); + if (!isvalid(map->path)) { + log_ie("Cannot duplicate path name"); + FAIL; + } + map->file_size = module.modBaseSize; + map->base = (void *) module.modBaseAddr; + map->size = module.modBaseSize; + map->has_symbols = FALSE; + log_d("Library map: %s (needle)", map->path); + continue; + } + } } } + } while (Module32Next(mod_hdl, &module)); - if (self->bin_path != NULL && self->lib_path != NULL && self->sym_loaded) - break; + // If the library map is not valid, use the needle map + if (!isvalid(pd->maps[MAP_LIBSYM].path)) { + pd->maps[MAP_LIBSYM] = pd->maps[MAP_LIBNEEDLE]; + pd->maps[MAP_LIBNEEDLE].path = needle_path = NULL; + } - success = Module32Next(mod_hdl, &module); + // Work out paths + self->bin_path = pd->maps[MAP_BIN].path; + self->lib_path = pd->maps[MAP_LIBSYM].path; + + // Work out binary map + for (int i = 0; i < MAP_COUNT; i++) { + map = &(pd->maps[i]); + if (map->has_symbols) { + self->map.elf.base = map->base; + self->map.elf.size = map->size; + break; + } } - CloseHandle(mod_hdl); + // Work out BSS map + int map_index = isvalid(pd->maps[MAP_LIBSYM].path) ? MAP_LIBSYM : MAP_BIN; + self->map.bss.base = pd->maps[map_index].bss_base; + self->map.bss.size = pd->maps[map_index].bss_size; + + log_d("BSS map %d from %s @ %p", map_index, pd->maps[map_index].path, self->map.bss.base); + log_d("VM maps parsing result: bin=%s lib=%s symbols=%d", self->bin_path, self->lib_path, self->sym_loaded); return !self->sym_loaded; } @@ -188,7 +373,12 @@ _py_proc__init(py_proc_t * self) { if (!isvalid(self)) FAIL; - return _py_proc__get_modules(self); + if (fail(_py_proc__get_modules(self))) { + log_d("Process does not seem to be Python; look for single child process"); + return _py_proc__try_child_proc(self); + } + + SUCCESS; } @@ -204,80 +394,4 @@ reader_thread(LPVOID lpParam) { return 0; } - -// ---------------------------------------------------------------------------- -// Forward declaration. -static int -_py_proc__run(py_proc_t *); - - -// On Windows, if we fail with the parent process we look if it has a single -// child and try to attach to that instead. We keep going until we either find -// a single Python process or more or less than a single child. -static int -_py_proc__try_child_proc(py_proc_t * self) { - log_d("Process is not Python so we look for a single child Python process"); - - HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (h == INVALID_HANDLE_VALUE) { - log_e("Cannot inspect processes details"); - FAIL; - } - -with_resources; - - HANDLE orig_hproc = self->proc_ref; - pid_t orig_pid = self->pid; - while (TRUE) { - pid_t parent_pid = self->pid; - - PROCESSENTRY32 pe = { 0 }; - pe.dwSize = sizeof(PROCESSENTRY32); - - if (Process32First(h, &pe)) { - pid_t child_pid = 0; - do { - if (pe.th32ParentProcessID == parent_pid) { - if (child_pid) { - log_d("Process has more than one child"); - NOK; - } - child_pid = pe.th32ProcessID; - } - } while (Process32Next(h, &pe)); - - if (!child_pid) { - log_d("Process has no children"); - NOK; - } - - self->pid = child_pid; - self->proc_ref = OpenProcess( - PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, child_pid - ); - if (self->proc_ref == INVALID_HANDLE_VALUE) { - log_e("Cannot open child process handle"); - NOK; - } - if (success(_py_proc__run(self))) { - log_d("Process has a single Python child with PID %d. We will attach to that", child_pid); - OK; - } - else { - log_d("Process has a single non-Python child with PID %d. Taking it as new parent", child_pid); - CloseHandle(self->proc_ref); - } - } - } - -release: - CloseHandle(h); - if (retval) { - self->pid = orig_pid; - self->proc_ref = orig_hproc; - } - - released; -} - #endif diff --git a/test/.flake8 b/test/.flake8 new file mode 100644 index 00000000..767d5dd6 --- /dev/null +++ b/test/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 88 +exclude = targets/* diff --git a/test/.isort.cfg b/test/.isort.cfg new file mode 100644 index 00000000..34ad4f39 --- /dev/null +++ b/test/.isort.cfg @@ -0,0 +1,4 @@ +[settings] +force_single_line = true +lines_after_imports = 2 +profile = black diff --git a/test/__init__.py b/test/__init__.py index d95a2aa6..f1543e76 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,6 +1,7 @@ import os import platform + PY3_LATEST = 11 try: diff --git a/test/bm/rsa_key_generator.py b/test/bm/rsa_key_generator.py index da111426..c4a54a3d 100644 --- a/test/bm/rsa_key_generator.py +++ b/test/bm/rsa_key_generator.py @@ -1,8 +1,7 @@ # https://github.com/TheAlgorithms/Python/blob/98a4c2487814cdfe0822526e05c4e63ff6aef7d0/ciphers/rsa_key_generator.py -import os + import random -import sys from . import cryptomath_module as cryptoMath # noqa: N812 from . import rabin_miller as rabinMiller # noqa: N812 diff --git a/test/cunit/__init__.py b/test/cunit/__init__.py index 9458b48f..10d18e97 100644 --- a/test/cunit/__init__.py +++ b/test/cunit/__init__.py @@ -1,14 +1,27 @@ import ctypes import re -from ctypes import CDLL, POINTER, Structure, c_char_p, c_long, c_void_p, cast +from ctypes import CDLL +from ctypes import POINTER +from ctypes import Structure +from ctypes import c_char_p +from ctypes import c_long +from ctypes import c_void_p +from ctypes import cast from pathlib import Path -from subprocess import PIPE, STDOUT, run +from subprocess import PIPE +from subprocess import STDOUT +from subprocess import run from types import ModuleType -from typing import Any, Callable, Optional, Type, Union +from typing import Any +from typing import Callable +from typing import Optional +from typing import Type -from pycparser import c_ast, c_parser +from pycparser import c_ast +from pycparser import c_parser from pycparser.plyparser import ParseError + HERE = Path(__file__).resolve().parent TEST = HERE.parent ROOT = TEST.parent diff --git a/test/cunit/argparse.py b/test/cunit/argparse.py index 61788b31..4cc53e09 100644 --- a/test/cunit/argparse.py +++ b/test/cunit/argparse.py @@ -1,6 +1,8 @@ import sys from pathlib import Path -from test.cunit import SRC, CModule +from test.cunit import SRC +from test.cunit import CModule + CFLAGS = ["-g", "-fprofile-arcs", "-ftest-coverage", "-fPIC"] diff --git a/test/cunit/cache.py b/test/cunit/cache.py index d6631789..f8e9d3f2 100644 --- a/test/cunit/cache.py +++ b/test/cunit/cache.py @@ -1,6 +1,8 @@ import sys from pathlib import Path -from test.cunit import SRC, CModule +from test.cunit import SRC +from test.cunit import CModule + CFLAGS = ["-g", "-fprofile-arcs", "-ftest-coverage"] diff --git a/test/cunit/conftest.py b/test/cunit/conftest.py index a7aa0694..7e7d02d4 100644 --- a/test/cunit/conftest.py +++ b/test/cunit/conftest.py @@ -2,7 +2,8 @@ import sys import typing as t from pathlib import Path -from subprocess import PIPE, run +from subprocess import PIPE +from subprocess import run from test.cunit import SRC from test.utils import bt from types import FunctionType @@ -58,7 +59,8 @@ def _(*_, **__): raise CUnitTestFailure( f"\n{result.stdout.decode()}\n" - f"Process terminated with exit code {result.returncode} (expected {exit_code})" + f"Process terminated with exit code {result.returncode} " + "(expected {exit_code})" ) return _ diff --git a/test/cunit/error.py b/test/cunit/error.py index 61788b31..4cc53e09 100644 --- a/test/cunit/error.py +++ b/test/cunit/error.py @@ -1,6 +1,8 @@ import sys from pathlib import Path -from test.cunit import SRC, CModule +from test.cunit import SRC +from test.cunit import CModule + CFLAGS = ["-g", "-fprofile-arcs", "-ftest-coverage", "-fPIC"] diff --git a/test/cunit/platform.py b/test/cunit/platform.py index 61788b31..4cc53e09 100644 --- a/test/cunit/platform.py +++ b/test/cunit/platform.py @@ -1,6 +1,8 @@ import sys from pathlib import Path -from test.cunit import SRC, CModule +from test.cunit import SRC +from test.cunit import CModule + CFLAGS = ["-g", "-fprofile-arcs", "-ftest-coverage", "-fPIC"] diff --git a/test/cunit/stats.py b/test/cunit/stats.py index 1b3fbcf5..e9fb26bf 100644 --- a/test/cunit/stats.py +++ b/test/cunit/stats.py @@ -1,6 +1,8 @@ import sys from pathlib import Path -from test.cunit import SRC, CModule +from test.cunit import SRC +from test.cunit import CModule + CFLAGS = ["-g", "-fprofile-arcs", "-ftest-coverage", "-fPIC"] diff --git a/test/cunit/test_cache.py b/test/cunit/test_cache.py index 7db7d024..fe72b949 100644 --- a/test/cunit/test_cache.py +++ b/test/cunit/test_cache.py @@ -1,9 +1,15 @@ from ctypes import c_void_p from test.cunit import C -from test.cunit.cache import Chain, HashTable, LruCache, Queue, QueueItem +from test.cunit.cache import Chain +from test.cunit.cache import HashTable +from test.cunit.cache import Lookup +from test.cunit.cache import LruCache +from test.cunit.cache import Queue +from test.cunit.cache import QueueItem import pytest + NULL = 0 C.free.argtypes = [c_void_p] C.malloc.restype = c_void_p @@ -128,3 +134,23 @@ def test_lru_cache_expand(): # Check that we still have all the items for k, v in values: assert c.maybe_hit(k) == v + + +def test_lookup(): + lu = Lookup(8) + + assert not lu.get(42) + + for i in range(1000): + lu.set(42 + i, i + 1) + + for i in range(1000): + assert lu.get(42 + i) == i + 1 + + getattr(lu, "del")(42) + assert not lu.get(42) + + lu.clear() + + for i in range(1000): + assert not lu.get(42 + i) diff --git a/test/cunit/test_error.py b/test/cunit/test_error.py index a821e999..6aebb9ff 100644 --- a/test/cunit/test_error.py +++ b/test/cunit/test_error.py @@ -1,4 +1,6 @@ -from test.cunit.error import cglobal, error_get_msg, is_fatal +from test.cunit.error import cglobal +from test.cunit.error import error_get_msg +from test.cunit.error import is_fatal import pytest diff --git a/test/requirements.txt b/test/requirements.txt index 18872ae0..42b3356a 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,7 +1,12 @@ -austin-python~=1.4 +austin-python~=1.5 flaky pytest pytest-xdist # C unit tests pycparser + +# Support tests +psutil +requests +uwsgi; sys_platform != 'win32' diff --git a/test/support/test_uwsgi.py b/test/support/test_uwsgi.py new file mode 100644 index 00000000..80c85a07 --- /dev/null +++ b/test/support/test_uwsgi.py @@ -0,0 +1,89 @@ +import sys +from contextlib import contextmanager +from pathlib import Path +from subprocess import PIPE +from subprocess import Popen +from test.utils import austin +from test.utils import compress +from test.utils import has_pattern +from test.utils import requires_sudo +from test.utils import threads +from threading import Thread +from time import sleep + +import psutil +import pytest +from requests import get + + +pytestmark = pytest.mark.skipif( + sys.platform == "win32", reason="Not supported on Windows" +) + +UWSGI = Path(__file__).parent / "uwsgi" + + +@contextmanager +def uwsgi(app="app.py", port=9090, args=[]): + with Popen( + [ + "uwsgi", + "--http", + f":{port}", + "--wsgi-file", + UWSGI / app, + *args, + ], + stdout=PIPE, + stderr=PIPE, + ) as uw: + sleep(0.5) + + assert uw.poll() is None, uw.stderr.read().decode() + + pid = uw.pid + assert pid is not None, uw.stderr.read().decode() + + try: + yield uw + finally: + uw.kill() + # Sledgehammer + for proc in psutil.process_iter(): + if "uwsgi" in proc.name(): + proc.terminate() + proc.wait() + + +@requires_sudo +def test_uwsgi(): + request_thread = Thread(target=get, args=("http://localhost:9090",)) + + with uwsgi() as uw: + request_thread.start() + + result = austin("-x", "2", "-Cp", str(uw.pid)) + assert has_pattern(result.stdout, "app.py:application:5"), compress( + result.stdout + ) + + request_thread.join() + + +@requires_sudo +def test_uwsgi_multiprocess(): + request_thread = Thread(target=get, args=("http://localhost:9091",)) + + with uwsgi(port=9091, args=["--processes", "2", "--threads", "2"]) as uw: + request_thread.start() + + result = austin("-x", "2", "-Cp", str(uw.pid)) + assert has_pattern(result.stdout, "app.py:application:5"), compress( + result.stdout + ) + + ts = threads(result.stdout) + assert len(ts) >= 4, ts + assert len({p for p, _ in ts}) >= 2, ts + + request_thread.join() diff --git a/test/support/uwsgi/app.py b/test/support/uwsgi/app.py new file mode 100644 index 00000000..effe1b46 --- /dev/null +++ b/test/support/uwsgi/app.py @@ -0,0 +1,9 @@ +from time import sleep + + +def application(env, start_response): + sleep(2) + + start_response("200 OK", [("Content-Type", "text/html")]) + + return [b"Hello World"] diff --git a/test/targets/column.py b/test/targets/column.py new file mode 100644 index 00000000..f4bfc095 --- /dev/null +++ b/test/targets/column.py @@ -0,0 +1,21 @@ +from time import sleep + + +def lazy(n): + for _ in range(n): + sleep(0.1) + yield _ + + +def fib(n): + a, b = 0, 1 + for _ in range(n): + yield a + a, b = b, a + b + +a = [ + list(fib(_)) + for _ in lazy(30) +] + +print(a) diff --git a/test/targets/recursive.py b/test/targets/recursive.py index cf43a8a1..d9cd5f6e 100644 --- a/test/targets/recursive.py +++ b/test/targets/recursive.py @@ -7,6 +7,6 @@ def sum_up_to(n): return result -for _ in range(200000): +for _ in range(300000): N = 16 assert sum_up_to(N) == (N * (N + 1)) >> 1 diff --git a/test/targets/target_gc.py b/test/targets/target_gc.py index 0f478e7a..8e556d8b 100644 --- a/test/targets/target_gc.py +++ b/test/targets/target_gc.py @@ -25,6 +25,7 @@ import gc import os + if os.getenv("GC_DISABLED"): gc.disable() diff --git a/test/targets/target_mp.py b/test/targets/target_mp.py index eedaf6fa..a37c94ef 100644 --- a/test/targets/target_mp.py +++ b/test/targets/target_mp.py @@ -40,8 +40,15 @@ def do(N): if __name__ == "__main__": + import sys + + try: + nproc = int(sys.argv[1]) + except Exception: + nproc = 2 + processes = [] - for _ in range(2): + for _ in range(nproc): process = multiprocessing.Process(target=do, args=(3000,)) process.start() processes.append(process) diff --git a/test/test_accuracy.py b/test/test_accuracy.py index 7118bd7c..ca10e3ac 100644 --- a/test/test_accuracy.py +++ b/test/test_accuracy.py @@ -20,15 +20,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from test.utils import ( - allpythons, - austin, - compress, - has_pattern, - python, - samples, - target, -) +from test.utils import allpythons +from test.utils import austin +from test.utils import compress +from test.utils import has_pattern +from test.utils import python +from test.utils import samples +from test.utils import target import pytest from flaky import flaky diff --git a/test/test_attach.py b/test/test_attach.py index 2bc0ab07..a8c1b013 100644 --- a/test/test_attach.py +++ b/test/test_attach.py @@ -20,23 +20,23 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os import platform from collections import Counter +from shutil import rmtree from test.utils import allpythons as _allpythons -from test.utils import ( - austin, - austinp, - compress, - has_pattern, - metadata, - mojo, - requires_sudo, - run_python, - sum_metric, - target, - threads, - variants, -) +from test.utils import austin +from test.utils import austinp +from test.utils import compress +from test.utils import has_pattern +from test.utils import metadata +from test.utils import mojo +from test.utils import requires_sudo +from test.utils import run_python +from test.utils import sum_metric +from test.utils import target +from test.utils import threads +from test.utils import variants from time import sleep import pytest @@ -62,7 +62,7 @@ def allpythons(): @allpythons() @variants def test_attach_wall_time(austin, py, mode, mode_meta, heap): - with run_python(py, target("sleepy.py")) as p: + with run_python(py, target("sleepy.py"), "1") as p: sleep(0.4) result = austin(mode, "2ms", *heap, "-p", str(p.pid)) @@ -160,3 +160,47 @@ def test_where_kernel(py): assert "" in result.stdout, compress(result.stdout) assert "libc" in result.stdout, compress(result.stdout) assert "do_syscall" in result.stdout, compress(result.stdout) + + +@pytest.mark.parametrize("prefix", [[], ["unshare", "-p", "-f", "-r"]]) +@pytest.mark.skipif(platform.system() != "Linux", reason="Linux only") +@requires_sudo +@_allpythons(min=(3,)) +def test_attach_container_like(py, tmp_path, prefix): + """Test in container-like conditions. + + We test that we can still attach Austin to a running process even if we + don't have access to the binary files to determine the location of the + symbols. We also test against an interpreter started in a different PID + namespace to emulate a container as closely as possible. + """ + venv_path = tmp_path / ".venv" + p = run_python(py, "-m", "venv", "--copies", str(venv_path)) + p.wait(30) + assert 0 == p.returncode, "Virtual environment was created successfully" + + env = os.environ.copy() + env["LD_LIBRARY_PATH"] = str(venv_path / "lib") + env["PATH"] = str(venv_path / "bin") + os.pathsep + env["PATH"] + with run_python( + py, target("sleepy.py"), "3", env=env, prefix=prefix, sleep_after=0.5 + ) as p: + rmtree(venv_path) + sleep(0.5) + + result = austin("-Cp", str(p.pid)) + assert result.returncode == 0 + + ts = threads(result.stdout) + assert len(ts) == 1, compress(result.stdout) + + assert has_pattern(result.stdout, "sleepy.py::"), compress( + result.stdout + ) + + meta = metadata(result.stdout) + + a = sum_metric(result.stdout) + d = int(meta["duration"]) + + assert a <= d diff --git a/test/test_cli.py b/test/test_cli.py index ff84a08d..733ceded 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -21,7 +21,10 @@ # along with this program. If not, see . import platform -from test.utils import austin, no_sudo, run_python, target +from test.utils import austin +from test.utils import no_sudo +from test.utils import run_python +from test.utils import target import pytest @@ -40,18 +43,13 @@ def test_cli_no_python(): "-c", "sleep 1", ) - if platform.system() == "Darwin": - # Darwin CI gives a different result than manual tests. We are accepting - # this for now. - assert result.returncode in (37, 39) - assert "Insufficient permissions" in result.stderr, result.stderr - else: - assert result.returncode == 39 - assert "not a Python" in result.stderr or "Cannot launch" in result.stderr + assert result.returncode == 39 + assert "not a Python" in result.stderr or "Cannot launch" in result.stderr def test_cli_invalid_command(): result = austin("snafubar") + assert "[GCC" in result.stderr assert result.returncode == 33 assert "Cannot launch" in (result.stderr or result.stdout) diff --git a/test/test_fork.py b/test/test_fork.py index 5e4d25ac..65af5e44 100644 --- a/test/test_fork.py +++ b/test/test_fork.py @@ -22,24 +22,22 @@ import platform from pathlib import Path -from test.utils import ( - allpythons, - austin, - compress, - demojo, - has_pattern, - maps, - metadata, - mojo, - processes, - python, - samples, - sum_metric, - sum_metrics, - target, - threads, - variants, -) +from test.utils import allpythons +from test.utils import austin +from test.utils import compress +from test.utils import demojo +from test.utils import has_pattern +from test.utils import maps +from test.utils import metadata +from test.utils import mojo +from test.utils import processes +from test.utils import python +from test.utils import samples +from test.utils import sum_metric +from test.utils import sum_metrics +from test.utils import target +from test.utils import threads +from test.utils import variants import pytest from flaky import flaky diff --git a/test/test_gc.py b/test/test_gc.py index bfce521b..f085c439 100644 --- a/test/test_gc.py +++ b/test/test_gc.py @@ -21,16 +21,14 @@ # along with this program. If not, see . import platform -from test.utils import ( - allpythons, - austin, - has_pattern, - metadata, - mojo, - python, - samples, - target, -) +from test.utils import allpythons +from test.utils import austin +from test.utils import has_pattern +from test.utils import metadata +from test.utils import mojo +from test.utils import python +from test.utils import samples +from test.utils import target import pytest diff --git a/test/test_mojo.py b/test/test_mojo.py new file mode 100644 index 00000000..c0a51431 --- /dev/null +++ b/test/test_mojo.py @@ -0,0 +1,59 @@ +# This file is part of "austin" which is released under GPL. +# +# See file LICENCE or go to http://www.gnu.org/licenses/ for full license +# details. +# +# Austin is a Python frame stack sampler for CPython. +# +# Copyright (c) 2023 Gabriele N. Tornetta . +# All rights reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from pathlib import Path +from test.utils import allpythons +from test.utils import austin +from test.utils import python +from test.utils import target + +from austin.format.mojo import MojoFile, MojoFrame +from flaky import flaky + + +@flaky +@allpythons(min=(3, 11)) +def test_mojo_column_data(py, tmp_path: Path): + datafile = tmp_path / "test_mojo_column.austin" + + result = austin( + "-i", "100", "-o", str(datafile), *python(py), target("column.py"), mojo=True + ) + assert result.returncode == 0, result.stderr or result.stdout + + def strip(f): + return (f.scope.string.value, f.line, f.line_end, f.column, f.column_end) + + with datafile.open("rb") as f: + frames = { + strip(_) + for _ in MojoFile(f).parse() + if isinstance(_, MojoFrame) + and _.filename.string.value.endswith("column.py") + } + + assert frames & { + ("", 16, 19, 5, 2), + ("", 16, 19, 5, 2), + ("lazy", 6, 6, 9, 19), + ("", 17, 17, 5, 17), + } diff --git a/test/test_pipe.py b/test/test_pipe.py index 30d460fd..2105ecdb 100644 --- a/test/test_pipe.py +++ b/test/test_pipe.py @@ -20,19 +20,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from test.utils import ( - allpythons, - austin, - compress, - has_pattern, - metadata, - processes, - python, - samples, - sum_metric, - target, - threads, -) +from test.utils import allpythons +from test.utils import austin +from test.utils import compress +from test.utils import has_pattern +from test.utils import metadata +from test.utils import processes +from test.utils import python +from test.utils import samples +from test.utils import sum_metric +from test.utils import target +from test.utils import threads from flaky import flaky diff --git a/test/test_valgrind.py b/test/test_valgrind.py index dbde8975..e90ef19d 100644 --- a/test/test_valgrind.py +++ b/test/test_valgrind.py @@ -21,7 +21,12 @@ # along with this program. If not, see . from subprocess import run -from test.utils import allpythons, austin, python, requires_sudo, run_python, target +from test.utils import allpythons +from test.utils import austin +from test.utils import python +from test.utils import requires_sudo +from test.utils import run_python +from test.utils import target import pytest diff --git a/test/utils.py b/test/utils.py index db926367..b1901bda 100644 --- a/test/utils.py +++ b/test/utils.py @@ -24,19 +24,32 @@ import os import platform from asyncio.subprocess import STDOUT -from collections import Counter, defaultdict -from io import BytesIO, StringIO +from collections import Counter +from collections import defaultdict +from io import BytesIO +from io import StringIO from pathlib import Path from shutil import rmtree -from subprocess import PIPE, CompletedProcess, Popen, check_output, run +from subprocess import PIPE +from subprocess import CompletedProcess +from subprocess import Popen +from subprocess import check_output +from subprocess import run from test import PYTHON_VERSIONS from time import sleep from types import ModuleType -from typing import Iterator, TypeVar +from typing import Iterator +from typing import TypeVar + + +try: + import pytest +except ImportError: + pytest = None -import pytest from austin.format.mojo import MojoFile + HERE = Path(__file__).parent @@ -63,7 +76,7 @@ def _(f): def python(version: str) -> list[str]: - match pl := platform.system(): + match platform.system(): case "Windows": py = ["py", f"-{version}"] case "Darwin" | "Linux": @@ -122,7 +135,6 @@ def bt(binary: Path) -> str: class Variant(str): - ALL: list["Variant"] = [] def __init__(self, name: str) -> None: @@ -167,15 +179,19 @@ def __call__( austin = Variant("austin") austinp = Variant("austinp") -variants = pytest.mark.parametrize("austin", Variant.ALL) +def run_async(command: list[str], *args: tuple[str], env: dict | None = None) -> Popen: + return Popen(command + list(args), stdout=PIPE, stderr=PIPE, env=env) -def run_async(command: list[str], *args: tuple[str]) -> Popen: - return Popen(command + list(args), stdout=PIPE, stderr=PIPE) - -def run_python(version, *args: tuple[str], sleep_after: float | None = None) -> Popen: - result = run_async(python(version), *args) +def run_python( + version, + *args: tuple[str], + env: dict | None = None, + prefix: list[str] = [], + sleep_after: float | None = None, +) -> Popen: + result = run_async(prefix + python(version), *args, env=env) if sleep_after is not None: sleep(sleep_after) @@ -218,7 +234,7 @@ def metadata(data: str) -> dict[str, str]: for v in ("austin", "python"): if v in meta: - meta[v] = tuple(int(_) for _ in meta[v].split(".")) + meta[v] = tuple(int(_.replace("?", "-1")) for _ in meta[v].split(".")[:3]) return meta @@ -284,18 +300,6 @@ def compress(data: str) -> str: return output -match platform.system(): - case "Windows": - requires_sudo = no_sudo = lambda f: f - case _: - requires_sudo = pytest.mark.skipif( - os.geteuid() != 0, reason="Requires superuser privileges" - ) - no_sudo = pytest.mark.skipif( - os.geteuid() == 0, reason="Must not have superuser privileges" - ) - - def demojo(data: bytes) -> str: result = StringIO() @@ -305,9 +309,6 @@ def demojo(data: bytes) -> str: return result.getvalue() -mojo = pytest.mark.parametrize("mojo", [False, True]) - - # Load from the utils scripts def load_util(name: str) -> ModuleType: module_path = (Path(__file__).parent.parent / "utils" / name).with_suffix(".py") @@ -319,3 +320,20 @@ def load_util(name: str) -> ModuleType: spec.loader.exec_module(module) return module + + +if pytest is not None: + variants = pytest.mark.parametrize("austin", Variant.ALL) + + match platform.system(): + case "Windows": + requires_sudo = no_sudo = lambda f: f + case _: + requires_sudo = pytest.mark.skipif( + os.geteuid() != 0, reason="Requires superuser privileges" + ) + no_sudo = pytest.mark.skipif( + os.geteuid() == 0, reason="Must not have superuser privileges" + ) + + mojo = pytest.mark.parametrize("mojo", [False, True])