diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000..187ae8ceda --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,316 @@ +name: Build + +on: + schedule: + - cron: '10 20 * * *' + + workflow_dispatch: + inputs: + publish: + description: 'Publish on GitHub.' + required: true + type: boolean + default: true + release: + description: 'Release Build (removes -SNAPSHOT from source version).' + required: true + type: boolean + default: false + useGitRefAsGithubVersion: + description: 'Use the Git ref field as GitHub version, e.g. if you build a release tag. If false, the GitHub version is derived from the JMC source version.' + required: true + type: boolean + default: false + forceBuild: + description: 'Force build (even if already published). EA releases will be updated/overwritten.' + required: true + type: boolean + default: false + skipTests: + description: 'Skip testing.' + required: true + type: boolean + default: false + gitRef: + description: 'Git ref (branch/tag/commit). If not set, the head of the branch selected above is used.' + required: false + +jobs: + parameters: + name: Gather Parameters + runs-on: ubuntu-latest + outputs: + publish: ${{ github.event.inputs.publish || true }} + release: ${{ github.event.inputs.release || false }} + skipTests: ${{ github.event.inputs.skipTests || false }} + githubRelease: ${{ steps.versions.outputs.githubRelease }} + jmcVersion: ${{ steps.versions.outputs.jmcVersion }} + agentVersion: ${{ steps.versions.outputs.agentVersion }} + doBuild: ${{ steps.buildcheck.outputs.doBuild }} + commit: ${{ steps.buildcheck.outputs.commit }} + steps: + - name: Checkout JMC + uses: actions/checkout@v7 + with: + ref: ${{ github.event.inputs.gitRef }} + + - name: Determine JMC version + id: versions + run: | + JMC_REV=$(grep -i \ pom.xml | sed 's/^[ \t]*//g;s/<\/revision>[ \t]*$//g') + if [[ '${{ github.event.inputs.release || false }}' == true ]]; then + JMC_VERSION=${JMC_REV} + else + JMC_VERSION=${JMC_REV}-SNAPSHOT + fi + echo JMC version: $JMC_VERSION + echo "jmcVersion=$(echo $JMC_VERSION)" >> $GITHUB_OUTPUT + + if [[ '${{ github.event.inputs.useGitRefAsGithubVersion || false }}' == true ]]; then + RELEASE_TAG=${{ github.event.inputs.gitRef }} + else + RELEASE_TAG=${JMC_VERSION}-sap + fi + echo GitHub Release Tag: $RELEASE_TAG + echo "githubRelease=$(echo $RELEASE_TAG)" >> $GITHUB_OUTPUT + + AGENT_REV=$(cd agent && grep -i \ pom.xml | sed 's/^[ \t]*//g;s/<\/revision>[ \t]*$//g' | head -n 1) + if [[ '${{ github.event.inputs.release || false }}' == true ]]; then + AGENT_VERSION=${AGENT_REV} + else + AGENT_VERSION=${AGENT_REV}-SNAPSHOT + fi + echo Agent version: $AGENT_VERSION + echo "agentVersion=$(echo $AGENT_VERSION)" >> $GITHUB_OUTPUT + + - name: Download existing release file + uses: robinraju/release-downloader@v1.13 + continue-on-error: true + with: + tag: ${{ steps.versions.outputs.githubRelease }} + repository: ${{ github.repository }} + fileName: buildRef-${{ steps.versions.outputs.jmcVersion }}.txt + + - name: Determine whether build is needed + id: buildcheck + run: | + function check_run() { + FORCE_BUILD='${{ github.event.inputs.forceBuild || false }}' + + if [[ $FORCE_BUILD == true ]]; then + echo 'true' + exit 0 + fi + + if [[ $MY_REV != $REL_REV ]]; then + echo 'true' + else + echo 'false' + fi + } + + MY_REV=$(git rev-parse HEAD) + echo My Rev: $MY_REV + REL_REV=$(cat buildRef-${{ steps.versions.outputs.jmcVersion }}.txt) || true + echo Released Rev: $REL_REV + echo Should run: $(check_run) + echo "doBuild=$(check_run)" >> $GITHUB_OUTPUT + echo "commit=$(echo $MY_REV)" >> $GITHUB_OUTPUT + + printparameters: + name: Print Parameters + needs: [parameters] + runs-on: ubuntu-latest + steps: + - name: Print parameters + run: | + echo publish: ${{ needs.parameters.outputs.publish }} + echo release: ${{ needs.parameters.outputs.release }} + echo skipTests: ${{ needs.parameters.outputs.skipTests }} + echo githubRelease: ${{ needs.parameters.outputs.githubRelease }} + echo jmcVersion: ${{ needs.parameters.outputs.jmcVersion }} + echo agentVersion: ${{ needs.parameters.outputs.agentVersion }} + echo doBuild: ${{ needs.parameters.outputs.doBuild }} + echo commit: ${{ needs.parameters.outputs.commit }} + + build: + name: Build JMC + needs: [parameters] + if: needs.parameters.outputs.doBuild == 'true' + runs-on: ubuntu-latest + env: + MAVEN_OPTS: -Xmx2048m + MAVEN_CALL: mvn --batch-mode --no-transfer-progress + permissions: + contents: write + steps: + - name: Delete old GitHub release ${{ needs.parameters.outputs.githubRelease }} + if: ${{ needs.parameters.outputs.publish == 'true' && needs.parameters.outputs.release != 'true' }} + continue-on-error: true + uses: dev-drprasad/delete-tag-and-release@v1.1 + with: + tag_name: ${{ needs.parameters.outputs.githubRelease }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout JMC + uses: actions/checkout@v7 + with: + ref: ${{ needs.parameters.outputs.commit }} + + - name: Set up JDK 17 + uses: actions/setup-java@v5 + with: + distribution: sapmachine + java-version: '17' + java-package: jdk + mvn-toolchain-id: 'JavaSE-17' + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + distribution: sapmachine + java-version: '21' + java-package: jdk + mvn-toolchain-id: 'JavaSE-21' + + - name: Set up Maven + uses: stCarolas/setup-maven@v5 + with: + maven-version: 3.9.8 + + - name: Cache local Maven repository + uses: actions/cache@v6 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven + + - name: Set up GA build + if: needs.parameters.outputs.release == 'true' + run: | + find . ! -path "*/.git/**" -type f -name "pom.xml" -exec sed -i s/"${{ needs.versions.outputs.jmcVersion }}-SNAPSHOT"/"${{ needs.versions.outputs.jmcVersion }}"/ {} \; + find . ! -path "*/.git/**" -type f \( -name "feature.xml" -o -name "MANIFEST.MF" \) -exec sed -i s/"${{ needs.versions.outputs.jmcVersion }}.qualifier"/"${{ needs.versions.outputs.jmcVersion }}"/ {} \; + echo "MAVEN_OPTS=$MAVEN_OPTS -Dchangelist=" >> $GITHUB_ENV + + - name: Write build information + run: | + echo '${{ needs.parameters.outputs.commit }}' > buildRef.txt + + - name: Build & test core libraries + run: | + $MAVEN_CALL install + working-directory: core + + - name: Build & test agent + # we build the agent only in snapshots + if: needs.parameters.outputs.release != 'true' + run: | + $MAVEN_CALL install + working-directory: agent + + - name: Build JMC + run: | + $MAVEN_CALL p2:site + $MAVEN_CALL jetty:run & + cd ../../ + $MAVEN_CALL package + working-directory: releng/third-party + + - name: Run unit tests + if: needs.parameters.outputs.skipTests != 'true' + run: | + $MAVEN_CALL verify + + #- name: Run UI tests + # if: needs.parameters.outputs.skipTests != 'true' + # # Ignore UI failures for now + # continue-on-error: true + # uses: GabrielBB/xvfb-action@v1 + # with: + # run: ${{ env.MAVEN_CALL }} verify -P uitests + + - name: Create/Update GitHub release ${{ needs.parameters.outputs.githubRelease }} + if: needs.parameters.outputs.publish == 'true' + uses: ncipollo/release-action@v1 + with: + tag: ${{ needs.parameters.outputs.githubRelease }} + commit: ${{ needs.parameters.outputs.commit }} + allowUpdates: ${{ needs.parameters.outputs.release != 'true' }} + prerelease: ${{ needs.parameters.outputs.release != 'true' }} + + - name: Upload buildRef-${{ needs.parameters.outputs.jmcVersion }}.txt + if: needs.parameters.outputs.publish == 'true' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ needs.parameters.outputs.githubRelease }} + file: buildRef.txt + asset_name: buildRef-${{ needs.parameters.outputs.jmcVersion }}.txt + overwrite: ${{ needs.parameters.outputs.release != 'true' }} + + - name: Upload sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-linux.gtk.aarch64.tar.gz + if: needs.parameters.outputs.publish == 'true' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ needs.parameters.outputs.githubRelease }} + file: target/products/org.openjdk.jmc-${{ needs.parameters.outputs.jmcVersion }}-linux.gtk.aarch64.tar.gz + asset_name: sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-linux.gtk.aarch64.tar.gz + overwrite: ${{ needs.parameters.outputs.release != 'true' }} + + - name: Upload sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-linux.gtk.x86_64.tar.gz + if: needs.parameters.outputs.publish == 'true' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + tag: ${{ needs.parameters.outputs.githubRelease }} + file: target/products/org.openjdk.jmc-${{ needs.parameters.outputs.jmcVersion }}-linux.gtk.x86_64.tar.gz + asset_name: sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-linux.gtk.x86_64.tar.gz + overwrite: ${{ needs.parameters.outputs.release != 'true' }} + + - name: Upload sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-macosx.cocoa.aarch64.tar.gz + if: needs.parameters.outputs.publish == 'true' + uses: svenstaro/upload-release-action@v2 + with: + tag: ${{ needs.parameters.outputs.githubRelease }} + file: target/products/org.openjdk.jmc-${{ needs.parameters.outputs.jmcVersion }}-macosx.cocoa.aarch64.tar.gz + asset_name: sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-macosx.cocoa.aarch64.tar.gz + overwrite: ${{ needs.parameters.outputs.release != 'true' }} + + - name: Upload sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-macosx.cocoa.x86_64.tar.gz + if: needs.parameters.outputs.publish == 'true' + uses: svenstaro/upload-release-action@v2 + with: + tag: ${{ needs.parameters.outputs.githubRelease }} + file: target/products/org.openjdk.jmc-${{ needs.parameters.outputs.jmcVersion }}-macosx.cocoa.x86_64.tar.gz + asset_name: sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-macosx.cocoa.x86_64.tar.gz + overwrite: ${{ needs.parameters.outputs.release != 'true' }} + + - name: Upload sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-win32.win32.x86_64.zip + if: needs.parameters.outputs.publish == 'true' + uses: svenstaro/upload-release-action@v2 + with: + tag: ${{ needs.parameters.outputs.githubRelease }} + file: target/products/org.openjdk.jmc-${{ needs.parameters.outputs.jmcVersion }}-win32.win32.x86_64.zip + asset_name: sap.jmc-${{ needs.parameters.outputs.jmcVersion }}-win32.win32.x86_64.zip + overwrite: ${{ needs.parameters.outputs.release != 'true' }} + + - name: Upload sap.jmc.updatesite.ide-${{ needs.parameters.outputs.jmcVersion }}.zip + if: needs.parameters.outputs.publish == 'true' + uses: svenstaro/upload-release-action@v2 + with: + tag: ${{ needs.parameters.outputs.githubRelease }} + file: application/org.openjdk.jmc.updatesite.ide/target/org.openjdk.jmc.updatesite.ide-${{ needs.parameters.outputs.jmcVersion }}.zip + asset_name: sap.jmc.updatesite.ide-${{ needs.parameters.outputs.jmcVersion }}.zip + overwrite: ${{ needs.parameters.outputs.release != 'true' }} + + - name: Upload agent-${{ needs.parameters.outputs.agentVersion }}.jar + # we only build the agent in snapshots + if: ${{ needs.parameters.outputs.publish == 'true' && needs.parameters.outputs.release != 'true' }} + uses: svenstaro/upload-release-action@v2 + with: + tag: ${{ needs.parameters.outputs.githubRelease }} + file: agent/target/agent-${{ needs.parameters.outputs.agentVersion }}.jar + asset_name: agent-${{ needs.parameters.outputs.agentVersion }}.jar + overwrite: ${{ needs.parameters.outputs.release != 'true' }} diff --git a/.github/workflows/sync-fork-checks.yml b/.github/workflows/sync-fork-checks.yml new file mode 100644 index 0000000000..d1ce858d73 --- /dev/null +++ b/.github/workflows/sync-fork-checks.yml @@ -0,0 +1,275 @@ +# +# Copyright (c) 2026 SAP SE. All rights reserved. +# +# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +# +# The contents of this file are subject to the terms of either the Universal Permissive License +# v 1.0 as shown at https://oss.oracle.com/licenses/upl +# +# or the following license: +# +# Redistribution and use in source and binary forms, with or without modification, are permitted +# provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of conditions +# and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be used to +# endorse or promote products derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +# FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +# WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# + +# Fires in the upstream repo context when a PR from a fork is opened or updated. +# Polls the fork repo's Actions API for the "Validation" +# run on the PR head commit, then mirrors every individual job as a native +# Check Run on the upstream PR — matching the appearance of the fork's own +# checks tab. No submit branches are created; no CI is re-run upstream. +name: 'Mirror Checks from Source Fork' + +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + checks: write + pull-requests: read + +jobs: + mirror: + name: 'Sync Actions from Fork' + runs-on: ubuntu-24.04 + # Only act on fork PRs — same-repo PRs get CI directly from main.yml. + if: github.event.pull_request.head.repo.full_name != github.repository + steps: + - name: 'Poll fork CI and mirror per-job results' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + HEAD_REF: ${{ github.event.pull_request.head.ref }} + PR_NUMBER: ${{ github.event.pull_request.number }} + FORK_REPO: ${{ github.event.pull_request.head.repo.full_name }} + UPSTREAM_REPO: ${{ github.repository }} + run: | + # ── Phase 1: Wait for the fork workflow run to appear ─────────────── + # The fork may not have triggered its CI yet immediately after push. + MAX_WAIT=20 + ATTEMPT=0 + RUN_ID="" + + while [[ $ATTEMPT -lt $MAX_WAIT ]]; do + ATTEMPT=$((ATTEMPT + 1)) + echo "Waiting for workflow run on fork (attempt ${ATTEMPT}/${MAX_WAIT})..." + + RUN_ID=$(gh api \ + "repos/${FORK_REPO}/actions/runs?head_sha=${HEAD_SHA}&per_page=10" \ + --jq '.workflow_runs[] | select(.name == "Validation") | .id' \ + 2>/dev/null | head -1) + + if [[ -n "$RUN_ID" && "$RUN_ID" != "null" ]]; then + echo "Found workflow run on fork: ${RUN_ID}" + break + fi + + # Fallback for reopened PRs: no new run may exist for HEAD_SHA, + # so locate the latest pull_request run for this PR number/branch. + RUN_ID=$(gh api \ + "repos/${FORK_REPO}/actions/runs?event=pull_request&branch=${HEAD_REF}&per_page=50" \ + --jq '.workflow_runs[] + | select(.name == "Validation") + | select(any(.pull_requests[]?; .number == ('"${PR_NUMBER}"' | tonumber))) + | .id' \ + 2>/dev/null | head -1) + + if [[ -n "$RUN_ID" && "$RUN_ID" != "null" ]]; then + echo "Found workflow run on fork via PR fallback: ${RUN_ID}" + break + fi + + sleep 60 + done + + if [[ -z "$RUN_ID" || "$RUN_ID" == "null" ]]; then + echo "No workflow run found on fork within ${MAX_WAIT} minutes — giving up." + exit 1 + fi + + # ── Phase 2: Mirror each fork job as an upstream Check Run ────────── + # job name → upstream check run ID + declare -A JOB_CHECK_RUN_IDS + # job name → '1' once the check run has been finalized + declare -A JOB_DONE + # job name → fork job HTML URL + declare -A JOB_URLS + + MAX_POLL=180 + POLL=0 + OVERALL_CONCLUSION="" + + while [[ $POLL -lt $MAX_POLL ]]; do + POLL=$((POLL + 1)) + echo "Poll ${POLL}/${MAX_POLL} — syncing fork job statuses..." + + JOBS_JSON=$(gh api \ + "repos/${FORK_REPO}/actions/runs/${RUN_ID}/jobs?per_page=100" \ + 2>/dev/null) + + if [[ -z "$JOBS_JSON" ]]; then + sleep 60 + continue + fi + + JOB_COUNT=$(echo "$JOBS_JSON" | jq '.jobs | length') + + for i in $(seq 0 $((JOB_COUNT - 1))); do + JOB_NAME=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].name") + JOB_STATUS=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].status") + JOB_CONCLUSION=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].conclusion") + JOB_URL=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].html_url") + + # Create a Check Run for this job the first time we see it. + JOB_URLS[$JOB_NAME]="$JOB_URL" + if [[ -z "${JOB_CHECK_RUN_IDS[$JOB_NAME]}" ]]; then + echo " Creating check run: ${JOB_NAME}" + CR_ID=$(gh api \ + -X POST \ + "repos/${UPSTREAM_REPO}/check-runs" \ + -f name="${JOB_NAME}" \ + -f head_sha="${HEAD_SHA}" \ + -f status="in_progress" \ + -f output[title]="Waiting for fork job: ${JOB_NAME}" \ + -f output[summary]="Polling fork repository for job '${JOB_NAME}' on commit ${HEAD_SHA}. [View job](${JOB_URL})" \ + --jq '.id') + JOB_CHECK_RUN_IDS[$JOB_NAME]=$CR_ID + echo " → check run ID: ${CR_ID}" + fi + + # Finalize any job that has completed since the last poll. + if [[ "$JOB_STATUS" == "completed" && -z "${JOB_DONE[$JOB_NAME]}" ]]; then + CR_ID="${JOB_CHECK_RUN_IDS[$JOB_NAME]}" + # Guard against null conclusion (e.g. cancelled mid-queue). + [[ -z "$JOB_CONCLUSION" || "$JOB_CONCLUSION" == "null" ]] && JOB_CONCLUSION="cancelled" + + if [[ "$JOB_CONCLUSION" == "success" ]]; then + TITLE="Passed: ${JOB_NAME}" + SUMMARY="Job **${JOB_NAME}** passed on the fork. [View job](${JOB_URL})" + else + TITLE="${JOB_CONCLUSION^}: ${JOB_NAME}" + SUMMARY="Job **${JOB_NAME}** reported **${JOB_CONCLUSION}** on the fork. [View job](${JOB_URL})" + fi + + gh api \ + -X PATCH \ + "repos/${UPSTREAM_REPO}/check-runs/${CR_ID}" \ + -f status="completed" \ + -f conclusion="${JOB_CONCLUSION}" \ + -f output[title]="${TITLE}" \ + -f output[summary]="${SUMMARY}" \ + -f details_url="${JOB_URL}" + + JOB_DONE[$JOB_NAME]=1 + echo " ✓ Finalized ${JOB_NAME}: ${JOB_CONCLUSION}" + fi + done + + # Check whether the overall run has finished. + RUN_JSON=$(gh api \ + "repos/${FORK_REPO}/actions/runs/${RUN_ID}" \ + --jq '{status: .status, conclusion: .conclusion}' \ + 2>/dev/null) + + RUN_STATUS=$(echo "$RUN_JSON" | jq -r '.status') + OVERALL_CONCLUSION=$(echo "$RUN_JSON" | jq -r '.conclusion') + + if [[ "$RUN_STATUS" == "completed" ]]; then + echo "Workflow run on fork completed with conclusion: ${OVERALL_CONCLUSION}" + # Do one final job sync to catch any jobs that finished in this last window. + JOBS_JSON=$(gh api \ + "repos/${FORK_REPO}/actions/runs/${RUN_ID}/jobs?per_page=100" \ + 2>/dev/null) + JOB_COUNT=$(echo "$JOBS_JSON" | jq '.jobs | length') + for i in $(seq 0 $((JOB_COUNT - 1))); do + JOB_NAME=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].name") + JOB_STATUS=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].status") + JOB_CONCLUSION=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].conclusion") + JOB_URL=$(echo "$JOBS_JSON" | jq -r ".jobs[$i].html_url") + if [[ -z "${JOB_CHECK_RUN_IDS[$JOB_NAME]}" ]]; then + CR_ID=$(gh api \ + -X POST \ + "repos/${UPSTREAM_REPO}/check-runs" \ + -f name="${JOB_NAME}" \ + -f head_sha="${HEAD_SHA}" \ + -f status="in_progress" \ + -f output[title]="Waiting for fork job: ${JOB_NAME}" \ + -f output[summary]="Polling fork repository for job '${JOB_NAME}' on commit ${HEAD_SHA}. [View job](${JOB_URL})" \ + --jq '.id') + JOB_CHECK_RUN_IDS[$JOB_NAME]=$CR_ID + fi + if [[ "$JOB_STATUS" == "completed" && -z "${JOB_DONE[$JOB_NAME]}" ]]; then + CR_ID="${JOB_CHECK_RUN_IDS[$JOB_NAME]}" + [[ -z "$JOB_CONCLUSION" || "$JOB_CONCLUSION" == "null" ]] && JOB_CONCLUSION="cancelled" + if [[ "$JOB_CONCLUSION" == "success" ]]; then + TITLE="Passed: ${JOB_NAME}" + SUMMARY="Job **${JOB_NAME}** passed on the fork. [View job](${JOB_URL})" + else + TITLE="${JOB_CONCLUSION^}: ${JOB_NAME}" + SUMMARY="Job **${JOB_NAME}** reported **${JOB_CONCLUSION}** on the fork. [View job](${JOB_URL})" + fi + gh api \ + -X PATCH \ + "repos/${UPSTREAM_REPO}/check-runs/${CR_ID}" \ + -f status="completed" \ + -f conclusion="${JOB_CONCLUSION}" \ + -f output[title]="${TITLE}" \ + -f output[summary]="${SUMMARY}" \ + -f details_url="${JOB_URL}" + JOB_DONE[$JOB_NAME]=1 + echo " ✓ Finalized ${JOB_NAME}: ${JOB_CONCLUSION}" + fi + done + break + fi + + sleep 60 + done + + # ── Phase 3: Handle timeout ───────────────────────────────────────── + if [[ -z "$OVERALL_CONCLUSION" || "$OVERALL_CONCLUSION" == "null" ]]; then + OVERALL_CONCLUSION="timed_out" + echo "Timed out — marking remaining open check runs." + for JOB_NAME in "${!JOB_CHECK_RUN_IDS[@]}"; do + if [[ -z "${JOB_DONE[$JOB_NAME]}" ]]; then + CR_ID="${JOB_CHECK_RUN_IDS[$JOB_NAME]}" + gh api \ + -X PATCH \ + "repos/${UPSTREAM_REPO}/check-runs/${CR_ID}" \ + -f status="completed" \ + -f conclusion="timed_out" \ + -f output[title]="Timed out: ${JOB_NAME}" \ + -f output[summary]="The polling window expired before job '${JOB_NAME}' completed. [View job](${JOB_URLS[$JOB_NAME]})" + fi + done + fi + + # Exit non-zero so the upstream PR shows a red check if CI did not pass. + if [[ "$OVERALL_CONCLUSION" != "success" ]]; then + exit 1 + fi diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 84eadd610d..f50a1eb851 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -48,7 +48,7 @@ jobs: shell: bash steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name : Check latest copyright year @@ -68,18 +68,18 @@ jobs: MAVENPARAMS: --batch-mode --no-transfer-progress steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Java 17 uses: actions/setup-java@v4 with: - distribution: temurin + distribution: sapmachine java-version: '17' java-package: jdk mvn-toolchain-id: 'JavaSE-17' - name: Set up Java 21 uses: actions/setup-java@v4 with: - distribution: temurin + distribution: sapmachine java-version: '21' java-package: jdk mvn-toolchain-id: 'JavaSE-21' diff --git a/README.md b/README.md index c3227b9783..5109d3d345 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Mission Control +*This is a friendly fork of [openjdk/jmc](https://github.com/openjdk/jmc). It aims to integrate changes and improvements for the specific needs of SAP customers.* + Mission Control is an open source production time profiling and diagnostics tool for Java. Builds of Mission Control can currently be found in the Oracle JDK on supported platforms and in the Eclipse Marketplace. diff --git a/agent/pom.xml b/agent/pom.xml index d6397dff05..b5f7224c66 100644 --- a/agent/pom.xml +++ b/agent/pom.xml @@ -93,7 +93,84 @@ 9.10.1 4.13.2 + Agent + + + + release + + + + + + SapAgent + + SapAgent + 0.9.3 + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven.jar.version} + + + boot-jar + + jar + + + boot + + org/openjdk/jmc/agent/sap/boot/** + + + + + + test-jar + + + + org/openjdk/jmc/agent/sap/** + org/openjdk/jmc/agent/test/sap/** + + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven.failsafe.version} + + + test-sap-extensions + + integration-test + verify + + + + ${fullTest} + ${dumpOutputToFile} + ${debugPort} + ${singleTest} + + + -cp target/test-classes/ + + SapIntegrationTest.java + + + + + + + + ${scmConnection} @@ -174,6 +251,7 @@ --add-opens java.base/jdk.internal.misc=ALL-UNNAMED -XX:+FlightRecorder + SapIntegrationTest.java TestDefineEventProbes.java TestCustomClassloader.java TestPermissionChecks.java @@ -183,11 +261,6 @@ - - org.apache.maven.plugins - maven-jar-plugin - ${maven.jar.version} - org.apache.maven.plugins maven-shade-plugin @@ -210,6 +283,7 @@ module-info.class META-INF/MANIFEST.MF + org/openjdk/jmc/agent/sap/boot/** @@ -217,8 +291,8 @@ org.openjdk.jmc.agent - org.openjdk.jmc.agent.Agent - org.openjdk.jmc.agent.Agent + org.openjdk.jmc.agent.${agent.name} + org.openjdk.jmc.agent.${agent.name} true JavaSE-17 @@ -383,6 +457,7 @@ ${spotless.version} + ${jmc.config.path}/ide/eclipse/formatting/formatting.xml 4.8.0 diff --git a/agent/src/main/java/org/openjdk/jmc/agent/SapAgent.java b/agent/src/main/java/org/openjdk/jmc/agent/SapAgent.java new file mode 100644 index 0000000000..9db2ac9385 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/SapAgent.java @@ -0,0 +1,349 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.jar.JarFile; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.openjdk.jmc.agent.impl.DefaultTransformRegistry; +import org.openjdk.jmc.agent.jfr.JFRTransformDescriptor; +import org.openjdk.jmc.agent.jmx.AgentManagementFactory; +import org.openjdk.jmc.agent.sap.boot.converters.GenericLogger; +import org.openjdk.jmc.agent.sap.boot.util.Command; +import org.openjdk.jmc.agent.sap.boot.util.Commands; +import org.openjdk.jmc.agent.sap.boot.util.Dumps; +import org.openjdk.jmc.agent.util.ModuleUtils; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +public class SapAgent { + + private static final String CONFIGS_PATH = "org/openjdk/jmc/agent/sap/"; + private static final String CONVERTERS_PREFIX = "org.openjdk.jmc.agent.sap.boot.converters."; + private static Logger logger = Logger.getLogger(SapAgent.class.getName()); + private static Instrumentation instr; + private static boolean addedBootJar = false; + private static HashMap seenCommands = new HashMap<>(); + + public static void premain(String agentArguments, Instrumentation instrumentation) throws Exception { + agentmain(agentArguments, instrumentation); + } + + public static void agentmain(String agentArguments, Instrumentation instrumentation) throws Exception { + ModuleUtils.openUnsafePackage(instrumentation); + instr = instrumentation; + + if (agentArguments == null || agentArguments.trim().length() == 0) { + initializeAgent(null, instrumentation); + } else { + if (agentArguments.equals("help")) { + ensureBootJarAdded(); + Commands.printAllCommands(); + System.exit(0); + } + + if (agentArguments.startsWith("dump=")) { + ensureBootJarAdded(); + Dumps.performDump(agentArguments.substring(5)); + return; + } + + try (InputStream stream = new ByteArrayInputStream(getXmlConfig(agentArguments))) { + initializeAgent(stream, instrumentation); + } catch (XMLStreamException | IOException | XMLValidationException e) { + logger.log(Level.SEVERE, "Failed to read jfr probe definitions from " + agentArguments, e); + } + } + } + + private static void addBootJarIfNeeded(TransformRegistry registry) throws IOException { + // Check if we need converters from the boot jar. + boolean needsBootJar = false; + + outer: for (String className : registry.getClassNames()) { + for (TransformDescriptor descriptor : registry.getTransformData(className)) { + JFRTransformDescriptor impl = (JFRTransformDescriptor) descriptor; + + for (Field field : impl.getFields()) { + if (field.hasConverter() && field.getConverterDefinition().startsWith(CONVERTERS_PREFIX)) { + needsBootJar = true; + break outer; + } + } + + for (Parameter param : impl.getParameters()) { + if (param.hasConverter() && param.getConverterDefinition().startsWith(CONVERTERS_PREFIX)) { + needsBootJar = true; + break outer; + } + } + } + } + + if (needsBootJar) { + ensureBootJarAdded(); + } + } + + private static void ensureBootJarAdded() throws IOException { + if (addedBootJar) { + return; + } + + ClassLoader cl = SapAgent.class.getClassLoader(); + + // Find out where the agent jar is, since the boot jar should live + // there too. + URL url = cl.getResource(SapAgent.class.getName().replace('.', '/') + ".class"); + String file = url.getFile(); + + if (!file.startsWith("file:/")) { + throw new IOException("Could not determine agent jar from " + file); + } + + System.err.println("Agent base file: " + file); + + String os = AccessController.doPrivileged((PrivilegedAction) () -> System.getProperty("os.name")); + int skip = os.toLowerCase().startsWith("win") ? 6 : 5; + String jar = file.substring(skip, file.indexOf(".jar!")) + "-boot.jar"; + + if (!new File(jar).canRead()) { + throw new IOException("Could not find boot jar at " + jar); + } + + instr.appendToBootstrapClassLoaderSearch(new JarFile(jar)); + addedBootJar = true; + } + + private static InputStream getStreamForConfig(String config) throws Exception { + try { + return new FileInputStream(config); + } catch (FileNotFoundException e) { + ClassLoader cl = SapAgent.class.getClassLoader(); + InputStream is = cl.getResourceAsStream(CONFIGS_PATH + config + ".xml"); + + if (is != null) { + return is; + } + + throw e; + } + } + + private static void preInitCommands() throws IOException { + if (seenCommands.size() > 0) { + ensureBootJarAdded(); + } + + for (String commandName : seenCommands.keySet()) { + Command command = Commands.getCommand(commandName); + + if (command != null) { + command.addCommandArgs(seenCommands.get(commandName)); + } + } + } + + private static StringBuilder addCommandOptions(String commandName, StringBuilder options) { + if (commandName != null) { + seenCommands.put(commandName, options.toString()); + } + + return new StringBuilder(); + } + + private static byte[] getXmlConfig(String agentArguments) throws Exception { + String[] parts = agentArguments.split("(?\n" + + " \n" + + " __JFREvent\n" + + " false\n" + + " false\n" + + " \n" + + " \n" + + " \n" + + "\n"; + // spotless:on + + Document base = factory.newDocumentBuilder() + .parse(new ByteArrayInputStream(DUMMY.getBytes(StandardCharsets.UTF_8))); + Node events = base.getElementsByTagName("events").item(0); + String requestedPrefix = null; + String configName = null; + + for (String part : parts) { + if (part.equals("help")) { + configProp.append("help=true,"); + } else if (part.startsWith(GenericLogger.GENERIC_COMMAND_PREFIX)) { + // Do nothing, just pick up the options and make sure the converter is accessible. + configProp = addCommandOptions(configName, configProp); + configName = part; + } else if (part.indexOf('=') > 0) { + configProp.append(part).append(','); + } else { + InputStream is = getStreamForConfig(part); + Document doc = factory.newDocumentBuilder().parse(is); + configProp = addCommandOptions(configName, configProp); + + if (is instanceof FileInputStream) { + // No configuration for direct XML file. + configName = null; + } else { + configName = part; + } + + if (getBool(doc, "allowtostring", false)) { + setText(base, "allowtostring", "true"); + } + + if (getBool(doc, "allowconverter", false)) { + setText(base, "allowconverter", "true"); + } + + String prefix = getString(doc, "classprefix"); + + if (requestedPrefix == null) { + requestedPrefix = prefix; + setText(base, "classprefix", prefix); + } else if ((prefix != null) && !requestedPrefix.equals(prefix)) { + System.out.println("Conflicting class prefixes " + prefix + " vs. " + requestedPrefix); + } + + NodeList list = doc.getElementsByTagName("event"); + + for (int j = 0; j < list.getLength(); ++j) { + Node toAdd = list.item(j).cloneNode(true); + events.getOwnerDocument().adoptNode(toAdd); + events.appendChild(toAdd); + } + } + } + + addCommandOptions(configName, configProp); + preInitCommands(); + + // If we added our boot jar, check the options now. + if (addedBootJar && !Commands.checkCommands()) { + System.exit(1); + } + + TransformerFactory tf = TransformerFactory.newInstance(); + javax.xml.transform.Transformer trans = tf.newTransformer(); + StringWriter sw = new StringWriter(); + trans.transform(new DOMSource(base), new StreamResult(sw)); + + return sw.toString().getBytes(); + } + + private static void setText(Document doc, String tag, String text) { + NodeList list = doc.getElementsByTagName(tag); + + if (list.getLength() == 1) { + list.item(0).setTextContent(text); + } + } + + private static String getString(Document doc, String tag) { + NodeList list = doc.getElementsByTagName(tag); + + if (list.getLength() != 1) { + return null; + } + + return list.item(0).getTextContent(); + } + + private static boolean getBool(Document doc, String tag, boolean fallback) { + String text = getString(doc, tag); + + return text == null ? fallback : Boolean.parseBoolean(text); + } + + public static void initializeAgent(InputStream configuration, Instrumentation instrumentation) + throws XMLStreamException, XMLValidationException, IOException { + TransformRegistry registry = configuration != null ? DefaultTransformRegistry.from(configuration) + : DefaultTransformRegistry.empty(); + addBootJarIfNeeded(registry); + instrumentation.addTransformer(new SapTransformer(registry), true); + AgentManagementFactory.createAndRegisterAgentControllerMBean(instrumentation, + new SapTransformRegistry(registry)); + + List> classesToRetransform = new ArrayList<>(); + Set clazzes = registry.getClassNames().stream().map((name) -> name.replace('/', '.')) + .collect(Collectors.toSet()); + + for (Class clazz : instrumentation.getAllLoadedClasses()) { + if (clazzes.contains(clazz.getName())) { + classesToRetransform.add(clazz); + System.out.println("Class to retransform: " + clazz); + } + } + + try { + instrumentation.retransformClasses(classesToRetransform.toArray(new Class[0])); + } catch (UnmodifiableClassException e) { + logger.log(Level.SEVERE, "Unable to retransform classes", e); + } + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/SapTransformRegistry.java b/agent/src/main/java/org/openjdk/jmc/agent/SapTransformRegistry.java new file mode 100644 index 0000000000..3081b953c7 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/SapTransformRegistry.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +import org.openjdk.jmc.agent.jfr.JFRTransformDescriptor; + +public class SapTransformRegistry implements TransformRegistry { + + private TransformRegistry registry; + private final HashMap> modifiedTransforms = new HashMap<>(); + + private void modifyTransformations() { + modifiedTransforms.clear(); + + for (String className : getClassNames()) { + String simpleName = className.substring(className.lastIndexOf('/') + 1); + List descs = getTransformData(className); + List modifiedDescs = null; + + for (int i = 0; i < descs.size(); ++i) { + JFRTransformDescriptor desc = (JFRTransformDescriptor) descs.get(i); + JFRTransformDescriptor modified = null; + + if (simpleName.equals(desc.getMethod().getName())) { + Method modifiedMethod = new Method("", desc.getMethod().getSignature()); + modified = new JFRTransformDescriptor(desc.getId(), desc.getClassName(), modifiedMethod, + desc.getTransformationAttributes(), desc.getParameters(), desc.getReturnValue(), + desc.getFields()); + } + + if (modified != null) { + if (modifiedDescs == null) { + modifiedDescs = new ArrayList(descs); + } + + modifiedDescs.set(i, modified); + } + } + + if (modifiedDescs != null) { + modifiedTransforms.put(className, modifiedDescs); + } + } + } + + public SapTransformRegistry(TransformRegistry registry) { + this.registry = registry; + modifyTransformations(); + } + + @Override + public boolean hasPendingTransforms(String className) { + return registry.hasPendingTransforms(className); + } + + @Override + public List getTransformData(String className) { + List modified = modifiedTransforms.get(className); + + if (modified != null) { + return modified; + } + + return registry.getTransformData(className); + } + + @Override + public Set getClassNames() { + return registry.getClassNames(); + } + + @Override + public String getCurrentConfiguration() { + return registry.getCurrentConfiguration(); + } + + @Override + public void setCurrentConfiguration(String xmlDescription) { + registry.setCurrentConfiguration(xmlDescription); + modifyTransformations(); + } + + @Override + public Set modify(String xmlDescription) throws XMLValidationException { + Set result = registry.modify(xmlDescription); + modifyTransformations(); + + return result; + } + + @Override + public Set clearAllTransformData() { + Set result = registry.clearAllTransformData(); + modifyTransformations(); + + return result; + } + + @Override + public void setRevertInstrumentation(boolean shouldRevert) { + registry.setRevertInstrumentation(shouldRevert); + } + + @Override + public boolean isRevertIntrumentation() { + return registry.isRevertIntrumentation(); + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/SapTransformer.java b/agent/src/main/java/org/openjdk/jmc/agent/SapTransformer.java new file mode 100644 index 0000000000..44f4ee4f51 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/SapTransformer.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.security.ProtectionDomain; + +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.openjdk.jmc.agent.util.TypeUtils; + +public class SapTransformer implements ClassFileTransformer { + + private final Transformer impl; + private final Module jfrModule; + private TransformRegistry registry; + + public SapTransformer(TransformRegistry registry) { + this.registry = registry; + jfrModule = ModuleLayer.boot().findModule("jdk.jfr").get(); + impl = new Transformer(new SapTransformRegistry(registry)); + } + + private void grantJfrAccessToModule( + Module module, ClassLoader loader, String className, ProtectionDomain protectionDomain) + throws IllegalClassFormatException { + // We need to access the jfr module. + if (!module.canRead(jfrModule)) { + // Create a class in the module which grants the access. + ClassWriter cw = new ClassWriter(0); + String name = className + "_$MakeJFRModuleReadable"; + cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, name, null, "java/lang/Object", null); + + MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "", "()V", null, null); + mv.visitCode(); + mv.visitLdcInsn(Type.getObjectType(name)); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getModule", "()Ljava/lang/Module;", false); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/ModuleLayer", "boot", "()Ljava/lang/ModuleLayer;", + false); + mv.visitLdcInsn("jdk.jfr"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/ModuleLayer", "findModule", + "(Ljava/lang/String;)Ljava/util/Optional;", false); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/Optional", "get", "()Ljava/lang/Object;", false); + mv.visitTypeInsn(Opcodes.CHECKCAST, "java/lang/Module"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Module", "addReads", + "(Ljava/lang/Module;)Ljava/lang/Module;", false); + mv.visitInsn(Opcodes.RETURN); + mv.visitMaxs(2, 0); + mv.visitEnd(); + + cw.visitEnd(); + byte[] bytes = cw.toByteArray(); + + try { + TypeUtils.defineClass(name.replace('/', '.'), bytes, 0, bytes.length, loader, protectionDomain); + // Trigger clinit to invoke the code. Needs no special permissions. + Class.forName(name.replace('/', '.'), true, loader); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + } + + @Override + public byte[] transform( + ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, + byte[] classfileBuffer) throws IllegalClassFormatException { + if (registry.getTransformData(className).isEmpty()) { + return null; + } + + if (classBeingRedefined != null) { + grantJfrAccessToModule(classBeingRedefined.getModule(), loader, className, protectionDomain); + } + + return impl.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + } + + @Override + public byte[] transform( + Module module, ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + if (registry.getTransformData(className).isEmpty()) { + return null; + } + + grantJfrAccessToModule(module, loader, className, protectionDomain); + + return impl.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer); + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/jfr/impl/JFRClassVisitor.java b/agent/src/main/java/org/openjdk/jmc/agent/jfr/impl/JFRClassVisitor.java index 67ff2d9be8..9b4827933d 100644 --- a/agent/src/main/java/org/openjdk/jmc/agent/jfr/impl/JFRClassVisitor.java +++ b/agent/src/main/java/org/openjdk/jmc/agent/jfr/impl/JFRClassVisitor.java @@ -106,8 +106,14 @@ private void reflectiveRegister(Class generateEventClass) throws Exception { } private Class generateEventClass() throws Exception { - byte[] eventClass = JFREventClassGenerator.generateEventClass(transformDescriptor, inspectionClass); - return TypeUtils.defineClass(transformDescriptor.getEventClassName(), eventClass, 0, eventClass.length, - definingClassLoader, protectionDomain); + try { + // Might have already been defined earlier. + return Class.forName(TypeUtils.getCanonicalName(transformDescriptor.getEventClassName()), false, + definingClassLoader); + } catch (ClassNotFoundException e) { + byte[] eventClass = JFREventClassGenerator.generateEventClass(transformDescriptor, inspectionClass); + return TypeUtils.defineClass(transformDescriptor.getEventClassName(), eventClass, 0, eventClass.length, + definingClassLoader, protectionDomain); + } } } diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/AllocationSite.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/AllocationSite.java new file mode 100644 index 0000000000..1f2f9e5f54 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/AllocationSite.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.converters; + +import java.io.PrintStream; +import java.util.Date; + +public class AllocationSite { + public final Exception stack; + public final long timestamp; + public final long size; + + public AllocationSite(long size) { + this.stack = new Exception(); + this.timestamp = System.currentTimeMillis(); + this.size = size; + } + + public boolean printOn(long address, PrintStream ps, AllocationStatisticDumpFilter filter) { + if (size < filter.minStackSize) { + return false; + } + + long age = System.currentTimeMillis() - timestamp; + + if (age > filter.maxAge) { + return false; + } + + if (age < filter.minAge) { + return false; + } + + StackTraceElement[] frames = stack.getStackTrace(); + int framesToSkip = 3; + int maxFrames = Math.min(filter.maxFrames + framesToSkip, frames.length); + + if (filter.mustContain != null) { + boolean matchFound = false; + + for (int i = framesToSkip; i < maxFrames; ++i) { + if (filter.mustContain.matcher(frames[i].toString()).find()) { + matchFound = true; + + break; + } + } + + if (!matchFound) { + return false; + } + } + + if (filter.mustNotContain != null) { + for (int i = framesToSkip; i < maxFrames; ++i) { + if (filter.mustNotContain.matcher(frames[i].toString()).find()) { + return false; + } + } + } + + ps.println("Allocated " + size + " bytes at 0x" + Long.toUnsignedString(address, 16)); + ps.println("Timestamp: " + new Date(timestamp).toString()); + ps.println("Allocated at:"); + + for (int i = framesToSkip; i < maxFrames; ++i) { + ps.println("\t" + frames[i]); + } + + return true; + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/AllocationStatistic.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/AllocationStatistic.java new file mode 100644 index 0000000000..16db113cd3 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/AllocationStatistic.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.converters; + +import java.io.PrintStream; +import java.util.HashMap; +import java.util.Map; + +import org.openjdk.jmc.agent.sap.boot.util.Arguments; +import org.openjdk.jmc.agent.sap.boot.util.LoggingUtils; + +public class AllocationStatistic { + private HashMap activeAllocations = new HashMap<>(); + private long totalSize = 0; + private static long lastDumpSize = 0; + + public AllocationStatistic copy() { + AllocationStatistic result = new AllocationStatistic(); + + synchronized (activeAllocations) { + result.activeAllocations = new HashMap<>(activeAllocations); + result.totalSize = totalSize; + } + + return result; + } + + public void addAllocation(long addr, long size) { + AllocationSite site = new AllocationSite(size); + + synchronized (activeAllocations) { + assert !activeAllocations.containsKey(addr); + totalSize += size; + activeAllocations.put(addr, site); + } + } + + public void removeAllocation(long addr) { + synchronized (activeAllocations) { + assert activeAllocations.containsKey(addr); + AllocationSite site = activeAllocations.remove(addr); + + if (site != null) { + assert totalSize > site.size; + totalSize -= site.size; + } + } + } + + public boolean printActiveAllocations(Arguments args) { + PrintStream ps = LoggingUtils.getStream(args); + AllocationStatisticDumpFilter filter = new AllocationStatisticDumpFilter(args); + + synchronized (AllocationStatistic.class) { + if (totalSize < filter.minSize) { + return false; + } + + if (totalSize < lastDumpSize * filter.minPercentageIncrease) { + return false; + } + + if ((filter.minIncrease >= 0) && (totalSize < lastDumpSize + filter.minIncrease)) { + return false; + } + + long printedSize = 0; + long printedCount = 0; + boolean dumped = false; + + for (Map.Entry entry : activeAllocations.entrySet()) { + if (entry.getValue().printOn(entry.getKey(), ps, filter)) { + printedSize += entry.getValue().size; + printedCount += 1; + } + } + + if (printedCount > 0) { + ps.println("Printed " + printedCount + " of " + activeAllocations.size() + " allocations with " + + printedSize + " bytes (of " + totalSize + " bytes allocated in total)."); + lastDumpSize = totalSize; + dumped = true; + } + + return dumped; + } + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/AllocationStatisticDumpFilter.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/AllocationStatisticDumpFilter.java new file mode 100644 index 0000000000..84cfcf753c --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/AllocationStatisticDumpFilter.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.converters; + +import java.util.regex.Pattern; + +import org.openjdk.jmc.agent.sap.boot.util.Arguments; + +public class AllocationStatisticDumpFilter { + public final int maxFrames; + public final long minSize; + public final long minStackSize; + public final long minIncrease; + public final double minPercentageIncrease; + public final long minAge; + public final long maxAge; + public final Pattern mustContain; + public final Pattern mustNotContain; + + public AllocationStatisticDumpFilter(Arguments args) { + this.maxFrames = args.getInt(UnsafeMemoryAllocationLogger.MAX_FRAMES, 16); + this.minSize = args.getSize(UnsafeMemoryAllocationLogger.MIN_SIZE, 0); + this.minStackSize = args.getSize(UnsafeMemoryAllocationLogger.MIN_STACK_SIZE, 0); + this.minIncrease = args.getSize(UnsafeMemoryAllocationLogger.MIN_INCREASE, -1); + this.minPercentageIncrease = 0.01 * args.getLong(UnsafeMemoryAllocationLogger.MIN_PERCENTAGE, 0); + this.minAge = 1000 * args.getDurationInSeconds(UnsafeMemoryAllocationLogger.MIN_AGE, 0); + this.maxAge = 1000 * args.getDurationInSeconds(UnsafeMemoryAllocationLogger.MAX_AGE, 365 * 24 * 3600); + this.mustContain = args.getPattern(UnsafeMemoryAllocationLogger.MUST_CONTAIN, null); + this.mustNotContain = args.getPattern(UnsafeMemoryAllocationLogger.MUST_NOT_CONTAIN, null); + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/FileOpenCloseLogger.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/FileOpenCloseLogger.java new file mode 100644 index 0000000000..d925fd09d6 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/FileOpenCloseLogger.java @@ -0,0 +1,304 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.converters; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.regex.Pattern; + +import org.openjdk.jmc.agent.sap.boot.util.Command; +import org.openjdk.jmc.agent.sap.boot.util.Arguments; +import org.openjdk.jmc.agent.sap.boot.util.Dumps; +import org.openjdk.jmc.agent.sap.boot.util.LoggingUtils; + +public class FileOpenCloseLogger { + public static final String MUST_CONTAIN = "mustContain"; + public static final String MUST_NOT_CONTAIN = "mustNotContain"; + public static final Command dumpCommand; + public static final Command command; + + private static HashMap mapping = new HashMap<>(); + private static final ThreadLocal pathKey = new ThreadLocal(); + private static final ThreadLocal modeKey = new ThreadLocal(); + private static final String UNKNOWN_FILE = ""; + + static { + // spotless:off + dumpCommand = new Command( + "openFiles", "Dump the currently files opened by Java code.", + MUST_CONTAIN, "A regexp which must match the file name to be printed.", + MUST_NOT_CONTAIN, "A regexp which must not match the file name to be printed."); + command = new Command(dumpCommand, + "traceOpenFiles", "Traces files opened by Java code.") { + public void preTraceInit() { + Dumps.registerPeriodicDump(command, "Open files", (Arguments args) -> printOpenFiles(args)); + } + }; + // spotless:on + + LoggingUtils.addOptions(command); + Dumps.addOptions(command); + Dumps.registerOnDemandDump(dumpCommand, (Arguments args) -> printOpenFiles(args)); + } + + public static synchronized boolean openFileInputStream(FileInputStream stream) { + if (pathKey.get() != null) { + mapping.put(new Key(stream), new Entry(pathKey.get(), "r", new Exception())); + pathKey.remove(); + } + + return true; + } + + public static synchronized String openFileInputStream(File file) { + String result = file.getAbsolutePath(); + pathKey.set(result); + + return result; + } + + public static synchronized String closeFileInputStream(FileInputStream stream) { + Key key = new Key(stream); + Entry entry = mapping.get(key); + + if (entry != null) { + mapping.remove(key); + + return entry.path; + } + + return UNKNOWN_FILE; + } + + public static synchronized boolean openFileOutputStream(FileOutputStream stream) { + if ((pathKey.get() != null) && (modeKey.get() != null)) { + mapping.put(new Key(stream), new Entry(pathKey.get(), modeKey.get(), new Exception())); + } + + pathKey.remove(); + modeKey.remove(); + + return true; + } + + public static synchronized boolean openFileOutputStream(boolean append) { + modeKey.set(append ? "wa" : "w"); + + return append; + } + + public static synchronized String openFileOutputStream(File file) { + String result = file.getAbsolutePath(); + pathKey.set(result); + + return result; + } + + public static synchronized String closeFileOutputStream(FileOutputStream stream) { + Key key = new Key(stream); + Entry entry = mapping.get(key); + + if (entry != null) { + mapping.remove(key); + + return entry.path; + } + + return UNKNOWN_FILE; + } + + public static synchronized String openRandomAccessFile(File file) { + String result = file.getAbsolutePath(); + pathKey.set(result); + + return result; + } + + public static synchronized String openRandomAccessFile(String file) { + return openRandomAccessFile(new File(file)); + } + + public static synchronized String openRandomAccessFileMode(String mode) { + modeKey.set(mode); + + return mode; + } + + public static synchronized boolean openRandomAccessFile(RandomAccessFile file) { + if ((pathKey.get() != null) && (modeKey.get() != null)) { + mapping.put(new Key(file), new Entry(pathKey.get(), modeKey.get(), new Exception())); + } + + return true; + } + + public static synchronized String closeRandomAccessFile(RandomAccessFile file) { + Key key = new Key(file); + Entry entry = mapping.get(key); + + if (entry != null) { + mapping.remove(key); + + return entry.path; + } + + return UNKNOWN_FILE; + } + + public static boolean printOpenFiles(Arguments args) { + HashMap copy; + + // Make a copy first, since logging might open new files. + synchronized (FileOpenCloseLogger.class) { + copy = new HashMap(mapping); + } + + for (Key key : new ArrayList(copy.keySet())) { + Object obj = key.getObject(); + + // If the ref is dead, remove it from the original map. + if (obj == null) { + synchronized (FileOpenCloseLogger.class) { + mapping.remove(key); + } + // Fall through to remove from copy as well. + } + + boolean remove = true; + + try { + FileDescriptor fd = null; + + if (obj instanceof FileInputStream) { + fd = ((FileInputStream) obj).getFD(); + } else if (obj instanceof FileOutputStream) { + fd = ((FileOutputStream) obj).getFD(); + } else if (obj instanceof RandomAccessFile) { + fd = ((RandomAccessFile) obj).getFD(); + } + + remove = (fd == null) || !fd.valid(); + } catch (IOException e) { + // Remove too. + } + + if (remove) { + copy.remove(key); + } + } + + Pattern mustContain = args.getPattern(MUST_CONTAIN, null); + Pattern mustNotContain = args.getPattern(MUST_NOT_CONTAIN, null); + int printed = 0; + + for (Entry entry : copy.values()) { + if (mustContain != null) { + if (!mustContain.matcher(entry.path).find()) { + continue; + } + } + + if (mustNotContain != null) { + if (mustNotContain.matcher(entry.path).find()) { + continue; + } + } + + LoggingUtils.logWithStack(args, "File '" + entry.path + "', mode '" + entry.mode + "'", entry.stack, 1); + printed += 1; + } + + if (printed > 0) { + LoggingUtils.log(args, "Printed " + printed + " of " + copy.size() + " file(s) currently opened."); + } + + return printed > 0; + } + + private static class Key { + private final WeakReference ref; + private final int hashCode; + + public Key(Object obj) { + ref = new WeakReference(obj); + hashCode = System.identityHashCode(obj); + } + + public Object getObject() { + return ref.get(); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object other) { + if (other instanceof Key) { + if (this == other) { + // Needed so we can remove a dead key from the hash map. + return true; + } + + Object thisKey = ref.get(); + Object otherKey = ((Key) other).ref.get(); + + if ((thisKey != null) && (otherKey != null)) { + return thisKey == otherKey; + } + } + + return false; + } + } + + private static class Entry { + public final String path; + public final String mode; + public final Exception stack; + + public Entry(String path, String mode, Exception stack) { + this.path = path; + this.mode = mode; + this.stack = stack; + } + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/GenericLogger.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/GenericLogger.java new file mode 100644 index 0000000000..2fa1fc2c2a --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/GenericLogger.java @@ -0,0 +1,1545 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.converters; + +import java.lang.reflect.Array; +import java.nio.CharBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +import org.openjdk.jmc.agent.sap.boot.util.Arguments; +import org.openjdk.jmc.agent.sap.boot.util.ArgumentsHolder; +import org.openjdk.jmc.agent.sap.boot.util.Command; +import org.openjdk.jmc.agent.sap.boot.util.LoggingUtils; + +public class GenericLogger { + + public static final String GENERIC_COMMAND_PREFIX = "logGeneric"; + public static final int MAX_FORMATS = 6; + public static final Command[] commands = new Command[MAX_FORMATS]; + + private static final int DEFAULT_MAX_PRINT_LENGTH = 256; + private static final int DEFAULT_MAX_ARRAY_PRINT_LENGTH = 5; + private static final String ONCE_PER_STACK = "oncePerStack"; + private static final String FORMAT = "format"; + private static final String PRINT_COLLECTION_CONTENT = "printCollectionContent"; + private static final String MAX_PRINT_LENGTH = "maxPrintLength"; + private static final String MAX_ARRAY_PRINT_LENGTH = "maxArrayPrintLength"; + private static final String MAX_LONG = "maxLongValue"; + private static final String MIN_LONG = "minLongValue"; + private static final String MAX_DOUBLE = "maxDoubleValue"; + private static final String MIN_DOUBLE = "minDoubleValue"; + private static final String MIN_LENGTH = "valueMinLength"; + private static final String MAX_LENGTH = "valueMaxLength"; + private static final String EQUALS = "valueEquals"; + private static final String STARTS_WITH = "valueStartsWith"; + private static final String ENDS_WITH = "valueEndsWith"; + private static final String CONTAINS = "valueContains"; + private static final String MATCHES_REGEXP = "valueMatchesRegexp"; + private static final String INSTANCEOF = "valueInstanceof"; + private static final String IS_TYPE = "valueIsType"; + private static final String NOT_EQUALS = "valueNotEquals"; + private static final String NOT_STARTS_WITH = "valueNotStartsWith"; + private static final String NOT_ENDS_WITH = "valueNotEndsWith"; + private static final String NOT_CONTAINS = "valueNotContains"; + private static final String NOT_MATCHES_REGEXP = "valueNotMatchesRegexp"; + private static final String NOT_INSTANCEOF = "valueNotInstanceof"; + private static final String IS_NOT_TYPE = "valueIsNotType"; + private static final ArrayList>> locals = new ArrayList<>(); + private static final int[] parameterIndices = new int[MAX_FORMATS]; + private static final HashSet seenStacks = new HashSet<>(); + private static final ArgumentsHolder[] argsHolder = new ArgumentsHolder[MAX_FORMATS]; + + static { + for (int i = 0; i < MAX_FORMATS; ++i) { + commands[i] = new Command(GENERIC_COMMAND_PREFIX + (i + 1), + "Used to specify the logging options for generic logger " + (i + 1), FORMAT, + "Used to specify the output of the generic logging format " + (i + 1) + ".", ONCE_PER_STACK, + "If true we only log once per unique call stack.", MAX_PRINT_LENGTH, + "The maximum number of characters to print.", MAX_ARRAY_PRINT_LENGTH, + "The maximum number of elements in an array to print.", PRINT_COLLECTION_CONTENT, + "If true we dump part of the contents of a supported Java collection type. " + + "This is disabled by default because of potential multi-threading issues."); + LoggingUtils.addOptionsWithStack(commands[i]); + addFilterOptions(commands[i]); + argsHolder[i] = commands[i].getArguments(); + } + } + + private static StringBuilder sanitize(CharSequence cs) { + StringBuilder sb = new StringBuilder(cs.length()); + + for (int i = 0; i < cs.length(); ++i) { + char c = cs.charAt(i); + + switch (c) { + case '\n': + sb.append("\\n"); + break; + + case '\t': + sb.append("\\t"); + break; + + case '"': + sb.append("\\\""); + break; + + case '\'': + sb.append("\\'"); + break; + + case '\\': + sb.append("\\\\"); + break; + + default: + if (c < 32 || c >= 127) { + sb.append('.'); + } else { + sb.append(c); + } + } + } + + return sb; + } + + private static StringBuilder convertToString(int format, boolean allowInspection, Object o) { + if (o == null) { + return new StringBuilder("null"); + } + + int maxPrintLen = argsHolder[format].get().getInt(MAX_PRINT_LENGTH, DEFAULT_MAX_PRINT_LENGTH); + boolean isCollection = o instanceof Collection || o instanceof Map; + + if (o.getClass().isArray() || (isCollection && allowInspection)) { + int maxArrayLen = argsHolder[format].get().getInt(MAX_ARRAY_PRINT_LENGTH, DEFAULT_MAX_ARRAY_PRINT_LENGTH); + int len; + Iterator it = null; + + StringBuilder sb = new StringBuilder("{"); + boolean success = true; + + if (o.getClass().isArray()) { + len = Array.getLength(o); + } else if (o instanceof Map) { + len = ((Map) o).size(); + it = ((Map) o).entrySet().iterator(); + } else { + len = ((Collection) o).size(); + it = ((Collection) o).iterator(); + } + + for (int i = 0; i < len && i < maxArrayLen; ++i) { + if (i > 0) { + sb.append(", "); + } + + if (o instanceof Collection) { + if (it.hasNext()) { + try { + Object v = it.next(); + sb.append(convertToString(format, allowInspection, v)); + } catch (ConcurrentModificationException | NoSuchElementException e) { + success = false; + break; + } + } + } else if (o instanceof Map) { + if (it.hasNext()) { + try { + Map.Entry v = (Map.Entry) it.next(); + sb.append(convertToString(format, allowInspection, v.getKey()) + ": " + + convertToString(format, allowInspection, v.getValue())); + } catch (ConcurrentModificationException | NoSuchElementException e) { + success = false; + break; + } + } + } else if (o instanceof boolean[]) { + sb.append(Boolean.toString(((boolean[]) o)[i])); + } else if (o instanceof byte[]) { + sb.append(Byte.toString(((byte[]) o)[i])); + } else if (o instanceof short[]) { + sb.append(Short.toString(((short[]) o)[i])); + } else if (o instanceof char[]) { + sb.append("'" + sanitize(Character.toString(((char[]) o)[i])) + "'"); + } else if (o instanceof int[]) { + sb.append(Integer.toString(((int[]) o)[i])); + } else if (o instanceof long[]) { + sb.append(Long.toString(((long[]) o)[i])); + } else if (o instanceof float[]) { + sb.append(Float.toString(((float[]) o)[i])); + } else if (o instanceof double[]) { + sb.append(Double.toString(((double[]) o)[i])); + } else { + Class compType = o.getClass().getComponentType(); + Object oi = ((Object[]) o)[i]; + + if (oi == null) { + sb.append("null"); + } else if (oi.getClass().isArray()) { + int depth = 0; + compType = oi.getClass().getComponentType(); + + while (compType.isArray()) { + depth += 1; + compType = compType.getComponentType(); + } + + sb.append(compType.getName() + "[" + Array.getLength(oi) + "]"); + + for (int j = 0; j < depth; ++j) { + sb.append("[]"); + } + } else { + sb.append(convertToString(format, allowInspection, ((Object[]) o)[i])); + } + } + } + + if (len > maxArrayLen) { + sb.append(", " + (len - maxArrayLen) + " skipped ..."); + } + + sb.append("}"); + + if (success) { + return sb; + } + } + + StringBuilder sb = new StringBuilder(); + + if (isCollection) { + if (o instanceof Collection) { + sb.append(o.getClass().getName() + "(size " + ((Collection) o).size() + ")"); + + return sb; + } + + if (o instanceof Map) { + sb.append(o.getClass().getName() + "(size " + ((Map) o).size() + ")"); + + return sb; + } + } + + if (o instanceof CharSequence) { + StringBuilder cs = sanitize(new StringBuilder((CharSequence) o)); + sb = new StringBuilder(cs.length() + 2); + sb.append('"'); + sb.append(cs); + sb.append('"'); + } else { + sb = new StringBuilder(sanitize(o.toString())); + + if (sb.length() > maxPrintLen) { + return new StringBuilder(sb.substring(0, maxPrintLen)); + } + } + + return sb; + } + + @SuppressWarnings("deprecation") + private static void logValue(Object value, int index, boolean isLast) { + int paramenterIndex; + ArrayList> params; + + synchronized (GenericLogger.class) { + while (index >= locals.size()) { + locals.add(new ArrayList<>()); + } + + params = locals.get(index); + paramenterIndex = parameterIndices[index]++; + + if (isLast) { + parameterIndices[index] = 0; + } + + while (paramenterIndex >= params.size()) { + params.add(new ThreadLocal()); + } + } + + params.get(paramenterIndex).set(value); + + if (isLast) { + Arguments args = argsHolder[index].get(); + + if (args.getBoolean(ONCE_PER_STACK, false)) { + SeenStack stack = new SeenStack(); + + synchronized (GenericLogger.class) { + if (seenStacks.contains(stack)) { + return; + } + + seenStacks.add(stack); + } + } + + Object[] values = new Object[paramenterIndex + 1]; + + for (int i = 0; i <= paramenterIndex; ++i) { + values[i] = params.get(i).get(); + + // Make sure we have a standard format for dates. + if (values[i] instanceof Date) { + values[i] = ((Date) values[i]).toGMTString(); + } + + params.get(i).remove(); + } + + @SuppressWarnings("unchecked") + ArrayList> filters = (ArrayList>) args.getCustomData(); + + if (filters == null) { + filters = new ArrayList<>(values.length); + + for (int i = 0; i < values.length; ++i) { + filters.add(getFilter(args, i + 1)); + } + + args.setCustomData(filters); + } + + for (int i = 0; i < values.length; ++i) { + Predicate filter = filters.get(i); + + if ((filter != null) && !filter.test(values[i])) { + return; + } + } + + StringBuilder sb = new StringBuilder("Values for generic logger " + (index + 1) + ":"); + + for (Object o : values) { + sb.append(' '); + sb.append(convertToString(index, args.getBoolean(PRINT_COLLECTION_CONTENT, false), o)); + } + + LoggingUtils.log(args, sb.toString()); + } + } + + private static void log(boolean v, int index, boolean isLast) { + logValue(Boolean.valueOf(v), index, isLast); + } + + private static void log(byte v, int index, boolean isLast) { + logValue(Byte.valueOf(v), index, isLast); + } + + private static void log(short v, int index, boolean isLast) { + logValue(Short.valueOf(v), index, isLast); + } + + private static void log(char v, int index, boolean isLast) { + logValue(Character.valueOf(v), index, isLast); + } + + private static void log(int v, int index, boolean isLast) { + logValue(Integer.valueOf(v), index, isLast); + } + + private static void log(long v, int index, boolean isLast) { + logValue(Long.valueOf(v), index, isLast); + } + + private static void log(float v, int index, boolean isLast) { + logValue(Float.valueOf(v), index, isLast); + } + + private static void log(double v, int index, boolean isLast) { + logValue(Double.valueOf(v), index, isLast); + } + + private static void log(Object v, int index, boolean isLast) { + logValue(v, index, isLast); + } + + private static String stringify(Object v) { + return v == null ? "null" : v.toString(); + } + + public static boolean logFormat1(boolean v) { + log(v, 0, false); + return v; + } + + public static byte logFormat1(byte v) { + log(v, 0, false); + return v; + } + + public static short logFormat1(short v) { + log(v, 0, false); + return v; + } + + public static char logFormat1(char v) { + log(v, 0, false); + return v; + } + + public static int logFormat1(int v) { + log(v, 0, false); + return v; + } + + public static long logFormat1(long v) { + log(v, 0, false); + return v; + } + + public static float logFormat1(float v) { + log(v, 0, false); + return v; + } + + public static double logFormat1(double v) { + log(v, 0, false); + return v; + } + + public static String logFormat1(Object v) { + log(v, 0, false); + return stringify(v); + } + + public static boolean logLastFormat1(boolean v) { + log(v, 0, true); + return v; + } + + public static byte logLastFormat1(byte v) { + log(v, 0, true); + return v; + } + + public static short logLastFormat1(short v) { + log(v, 0, true); + return v; + } + + public static char logLastFormat1(char v) { + log(v, 0, true); + return v; + } + + public static int logLastFormat1(int v) { + log(v, 0, true); + return v; + } + + public static long logLastFormat1(long v) { + log(v, 0, true); + return v; + } + + public static float logLastFormat1(float v) { + log(v, 0, true); + return v; + } + + public static double logLastFormat1(double v) { + log(v, 0, true); + return v; + } + + public static String logLastFormat1(Object v) { + log(v, 0, true); + return stringify(v); + } + + public static boolean logFormat2(boolean v) { + log(v, 1, false); + return v; + } + + public static byte logFormat2(byte v) { + log(v, 1, false); + return v; + } + + public static short logFormat2(short v) { + log(v, 1, false); + return v; + } + + public static char logFormat2(char v) { + log(v, 1, false); + return v; + } + + public static int logFormat2(int v) { + log(v, 1, false); + return v; + } + + public static long logFormat2(long v) { + log(v, 1, false); + return v; + } + + public static float logFormat2(float v) { + log(v, 1, false); + return v; + } + + public static double logFormat2(double v) { + log(v, 1, false); + return v; + } + + public static String logFormat2(Object v) { + log(v, 1, false); + return stringify(v); + } + + public static boolean logLastFormat2(boolean v) { + log(v, 1, true); + return v; + } + + public static byte logLastFormat2(byte v) { + log(v, 1, true); + return v; + } + + public static short logLastFormat2(short v) { + log(v, 1, true); + return v; + } + + public static char logLastFormat2(char v) { + log(v, 1, true); + return v; + } + + public static int logLastFormat2(int v) { + log(v, 1, true); + return v; + } + + public static long logLastFormat2(long v) { + log(v, 1, true); + return v; + } + + public static float logLastFormat2(float v) { + log(v, 1, true); + return v; + } + + public static double logLastFormat2(double v) { + log(v, 1, true); + return v; + } + + public static String logLastFormat2(Object v) { + log(v, 1, true); + return stringify(v); + } + + public static boolean logFormat3(boolean v) { + log(v, 2, false); + return v; + } + + public static byte logFormat3(byte v) { + log(v, 2, false); + return v; + } + + public static short logFormat3(short v) { + log(v, 2, false); + return v; + } + + public static char logFormat3(char v) { + log(v, 2, false); + return v; + } + + public static int logFormat3(int v) { + log(v, 2, false); + return v; + } + + public static long logFormat3(long v) { + log(v, 2, false); + return v; + } + + public static float logFormat3(float v) { + log(v, 2, false); + return v; + } + + public static double logFormat3(double v) { + log(v, 2, false); + return v; + } + + public static String logFormat3(Object v) { + log(v, 2, false); + return stringify(v); + } + + public static boolean logLastFormat3(boolean v) { + log(v, 2, true); + return v; + } + + public static byte logLastFormat3(byte v) { + log(v, 2, true); + return v; + } + + public static short logLastFormat3(short v) { + log(v, 2, true); + return v; + } + + public static char logLastFormat3(char v) { + log(v, 2, true); + return v; + } + + public static int logLastFormat3(int v) { + log(v, 2, true); + return v; + } + + public static long logLastFormat3(long v) { + log(v, 2, true); + return v; + } + + public static float logLastFormat3(float v) { + log(v, 2, true); + return v; + } + + public static double logLastFormat3(double v) { + log(v, 2, true); + return v; + } + + public static String logLastFormat3(Object v) { + log(v, 2, true); + return stringify(v); + } + + public static boolean logFormat4(boolean v) { + log(v, 3, false); + return v; + } + + public static byte logFormat4(byte v) { + log(v, 3, false); + return v; + } + + public static short logFormat4(short v) { + log(v, 3, false); + return v; + } + + public static char logFormat4(char v) { + log(v, 3, false); + return v; + } + + public static int logFormat4(int v) { + log(v, 3, false); + return v; + } + + public static long logFormat4(long v) { + log(v, 3, false); + return v; + } + + public static float logFormat4(float v) { + log(v, 3, false); + return v; + } + + public static double logFormat4(double v) { + log(v, 3, false); + return v; + } + + public static String logFormat4(Object v) { + log(v, 3, false); + return stringify(v); + } + + public static boolean logLastFormat4(boolean v) { + log(v, 3, true); + return v; + } + + public static byte logLastFormat4(byte v) { + log(v, 3, true); + return v; + } + + public static short logLastFormat4(short v) { + log(v, 3, true); + return v; + } + + public static char logLastFormat4(char v) { + log(v, 3, true); + return v; + } + + public static int logLastFormat4(int v) { + log(v, 3, true); + return v; + } + + public static long logLastFormat4(long v) { + log(v, 3, true); + return v; + } + + public static float logLastFormat4(float v) { + log(v, 3, true); + return v; + } + + public static double logLastFormat4(double v) { + log(v, 3, true); + return v; + } + + public static String logLastFormat4(Object v) { + log(v, 3, true); + return stringify(v); + } + + public static boolean logFormat5(boolean v) { + log(v, 4, false); + return v; + } + + public static byte logFormat5(byte v) { + log(v, 4, false); + return v; + } + + public static short logFormat5(short v) { + log(v, 4, false); + return v; + } + + public static char logFormat5(char v) { + log(v, 4, false); + return v; + } + + public static int logFormat5(int v) { + log(v, 4, false); + return v; + } + + public static long logFormat5(long v) { + log(v, 4, false); + return v; + } + + public static float logFormat5(float v) { + log(v, 4, false); + return v; + } + + public static double logFormat5(double v) { + log(v, 4, false); + return v; + } + + public static String logFormat5(Object v) { + log(v, 4, false); + return stringify(v); + } + + public static boolean logLastFormat5(boolean v) { + log(v, 4, true); + return v; + } + + public static byte logLastFormat5(byte v) { + log(v, 4, true); + return v; + } + + public static short logLastFormat5(short v) { + log(v, 4, true); + return v; + } + + public static char logLastFormat5(char v) { + log(v, 4, true); + return v; + } + + public static int logLastFormat5(int v) { + log(v, 4, true); + return v; + } + + public static long logLastFormat5(long v) { + log(v, 4, true); + return v; + } + + public static float logLastFormat5(float v) { + log(v, 4, true); + return v; + } + + public static double logLastFormat5(double v) { + log(v, 4, true); + return v; + } + + public static String logLastFormat5(Object v) { + log(v, 4, true); + return stringify(v); + } + + public static boolean logFormat6(boolean v) { + log(v, 5, false); + return v; + } + + public static byte logFormat6(byte v) { + log(v, 5, false); + return v; + } + + public static short logFormat6(short v) { + log(v, 5, false); + return v; + } + + public static char logFormat6(char v) { + log(v, 5, false); + return v; + } + + public static int logFormat6(int v) { + log(v, 5, false); + return v; + } + + public static long logFormat6(long v) { + log(v, 5, false); + return v; + } + + public static float logFormat6(float v) { + log(v, 5, false); + return v; + } + + public static double logFormat6(double v) { + log(v, 5, false); + return v; + } + + public static String logFormat6(Object v) { + log(v, 5, false); + return stringify(v); + } + + public static boolean logLastFormat6(boolean v) { + log(v, 5, true); + return v; + } + + public static byte logLastFormat6(byte v) { + log(v, 5, true); + return v; + } + + public static short logLastFormat6(short v) { + log(v, 5, true); + return v; + } + + public static char logLastFormat6(char v) { + log(v, 5, true); + return v; + } + + public static int logLastFormat6(int v) { + log(v, 5, true); + return v; + } + + public static long logLastFormat6(long v) { + log(v, 5, true); + return v; + } + + public static float logLastFormat6(float v) { + log(v, 5, true); + return v; + } + + public static double logLastFormat6(double v) { + log(v, 5, true); + return v; + } + + public static String logLastFormat6(Object v) { + log(v, 5, true); + return stringify(v); + } + + private static void addFilterOptions(Command cmd) { + cmd.addOption(suffixValue(MAX_LONG, ""), "Traces only if value has the given maximum long value."); + cmd.addOption(suffixValue(MIN_LONG, ""), "Traces only if value has the given minimum long value."); + cmd.addOption(suffixValue(MAX_DOUBLE, ""), + "Traces only if value has the given maximum double value."); + cmd.addOption(suffixValue(MIN_DOUBLE, ""), + "Traces only if value has the given minimum double value."); + cmd.addOption(suffixValue(MIN_LENGTH, ""), + "Traces only if array or similar type has the given minimum length."); + cmd.addOption(suffixValue(MAX_LENGTH, ""), + "Traces only if array or similar type has the given minimum length."); + cmd.addOption(suffixValue(EQUALS, ""), "Traces only if value equals the given value."); + cmd.addOption(suffixValue(CONTAINS, ""), + "Traces only if the string representation of value contains the given string."); + cmd.addOption(suffixValue(STARTS_WITH, ""), + "Traces only if the string representation of value starts with the given string."); + cmd.addOption(suffixValue(ENDS_WITH, ""), + "Traces only if the string representation of value ends with the given string."); + cmd.addOption(suffixValue(MATCHES_REGEXP, ""), + "Traces only if the string representation of value matches the given regexp."); + cmd.addOption(suffixValue(INSTANCEOF, ""), + "Traces only if value is a class and is an instance of the given type."); + cmd.addOption(suffixValue(IS_TYPE, ""), + "Traces only if the value is of the given type (null, array, primitive_array or object_array)."); + cmd.addOption(suffixValue(NOT_EQUALS, ""), "Traces only if value does NOT equals the given value."); + cmd.addOption(suffixValue(NOT_CONTAINS, ""), + "Traces only if the string representation of value does NOT contains the given string."); + cmd.addOption(suffixValue(NOT_STARTS_WITH, ""), + "Traces only if the string representation of value does NOT starts with the given string."); + cmd.addOption(suffixValue(NOT_ENDS_WITH, ""), + "Traces only if the string representation of value does NOT ends with the given string."); + cmd.addOption(suffixValue(NOT_MATCHES_REGEXP, ""), + "Traces only if the string representation of value does NOT matches the given regexp."); + cmd.addOption(suffixValue(NOT_INSTANCEOF, ""), + "Traces only if value is a class and is NOT an instance of the given type."); + cmd.addOption(suffixValue(IS_NOT_TYPE, ""), + "Traces only if the value is not of the given type (null, array, primitive_array or object_array)."); + } + + private static String suffixValue(String option, String suffix) { + if (option.startsWith("value")) { + return "value" + suffix + option.substring(5); + } + + int pos = option.indexOf("Value"); + + return option.substring(0, pos) + "Value" + suffix + option.substring(pos + 5); + } + + private static String getValueOption(String option, int idx) { + return suffixValue(option, Integer.toString(idx)); + } + + private static Predicate addPredicate(Predicate predicate, Predicate toAdd) { + if (predicate == null) { + return toAdd; + } else if (toAdd == null) { + return predicate; + } else { + return predicate.and(toAdd); + } + } + + private static Predicate getFilter(Arguments args, int idx) { + Predicate result = null; + String maxLong = getValueOption(MAX_LONG, idx); + String maxDouble = getValueOption(MAX_DOUBLE, idx); + String minLong = getValueOption(MIN_LONG, idx); + String minDouble = getValueOption(MIN_DOUBLE, idx); + String minLength = getValueOption(MIN_LENGTH, idx); + String maxLength = getValueOption(MAX_LENGTH, idx); + String equals = getValueOption(EQUALS, idx); + String startsWith = getValueOption(STARTS_WITH, idx); + String endsWith = getValueOption(ENDS_WITH, idx); + String contains = getValueOption(CONTAINS, idx); + String matchesRegexp = getValueOption(MATCHES_REGEXP, idx); + String instanceOf = getValueOption(INSTANCEOF, idx); + String isType = getValueOption(IS_TYPE, idx); + String notEquals = getValueOption(NOT_EQUALS, idx); + String notStartsWith = getValueOption(NOT_STARTS_WITH, idx); + String notEndsWith = getValueOption(NOT_ENDS_WITH, idx); + String notContains = getValueOption(NOT_CONTAINS, idx); + String notMatchesRegexp = getValueOption(NOT_MATCHES_REGEXP, idx); + String notInstanceOf = getValueOption(NOT_INSTANCEOF, idx); + String isNotType = getValueOption(IS_NOT_TYPE, idx); + + if (args.hasOption(maxLong)) { + result = addPredicate(result, new MaxLongValueFilter(args.getLong(maxLong, 0))); + } + + if (args.hasOption(maxDouble)) { + result = addPredicate(result, new MaxDoubleValueFilter(args.getDouble(maxDouble, 0))); + } + + if (args.hasOption(minLong)) { + result = addPredicate(result, new MinLongValueFilter(args.getLong(minLong, 0))); + } + + if (args.hasOption(minDouble)) { + result = addPredicate(result, new MinDoubleValueFilter(args.getDouble(minDouble, 0))); + } + + if (args.hasOption(minLength)) { + result = addPredicate(result, new MinLengthFilter(args.getInt(minLength, 0))); + } + + if (args.hasOption(maxLength)) { + result = addPredicate(result, new MaxLengthFilter(args.getInt(maxLength, 0))); + } + + if (args.hasOption(equals)) { + result = addPredicate(result, new EqualsValueFilter(args.getString(equals, ""))); + } + + if (args.hasOption(startsWith)) { + result = addPredicate(result, new StartsWithValueFilter(args.getString(startsWith, ""))); + } + + if (args.hasOption(endsWith)) { + result = addPredicate(result, new EndsWithValueFilter(args.getString(endsWith, ""))); + } + + if (args.hasOption(contains)) { + for (String str : args.getStrings(contains)) { + result = addPredicate(result, new ContainsValueFilter(str)); + } + } + + if (args.hasOption(matchesRegexp)) { + for (String str : args.getStrings(matchesRegexp)) { + result = addPredicate(result, new MatchesRegexpValueFilter(str)); + } + } + + if (args.hasOption(instanceOf)) { + for (String str : args.getStrings(instanceOf)) { + result = addPredicate(result, new InstanceofValueFilter(str)); + } + } + + if (args.hasOption(isType)) { + for (String str : args.getStrings(isType)) { + result = addPredicate(result, new IsTypeFilter(str)); + } + } + + if (args.hasOption(notEquals)) { + for (String str : args.getStrings(notEquals)) { + result = addPredicate(result, new EqualsValueFilter(str).negate()); + } + } + + if (args.hasOption(notStartsWith)) { + for (String str : args.getStrings(notStartsWith)) { + result = addPredicate(result, new StartsWithValueFilter(str).negate()); + } + } + + if (args.hasOption(notEndsWith)) { + for (String str : args.getStrings(notEndsWith)) { + result = addPredicate(result, new EndsWithValueFilter(str).negate()); + } + } + + if (args.hasOption(notContains)) { + for (String str : args.getStrings(notContains)) { + result = addPredicate(result, new ContainsValueFilter(str).negate()); + } + } + + if (args.hasOption(notMatchesRegexp)) { + for (String str : args.getStrings(notMatchesRegexp)) { + result = addPredicate(result, new MatchesRegexpValueFilter(str).negate()); + } + } + + if (args.hasOption(notInstanceOf)) { + for (String str : args.getStrings(notInstanceOf)) { + result = addPredicate(result, new InstanceofValueFilter(str).negate()); + } + } + + if (args.hasOption(isNotType)) { + for (String str : args.getStrings(isNotType)) { + result = addPredicate(result, new IsTypeFilter(str).negate()); + } + } + + return result; + } + + private static final class IsTypeFilter implements Predicate { + private final String type; + + public IsTypeFilter(String type) { + this.type = type; + } + + @Override + public boolean test(Object t) { + if (t == null) { + return "null".equals(type); + } + + if (t.getClass().isArray()) { + if ("array".equals(type)) { + return true; + } + + if (t.getClass().getComponentType().isPrimitive()) { + return "primitive_array".equals(type); + } + + return "object_array".equals(type); + } + + return false; + } + } + + private static final class MaxLongValueFilter implements Predicate { + + private final long max; + + MaxLongValueFilter(long max) { + this.max = max; + } + + @Override + public boolean test(Object t) { + // Handle double and float separately, since we want to avoid treating 0.3 <= 0. + if (t instanceof Double) { + return ((Double) t).doubleValue() <= max; + } + + if (t instanceof Float) { + return ((Float) t).floatValue() <= max; + } + + if (t instanceof Number) { + return ((Number) t).longValue() <= max; + } + + if (t instanceof Character) { + return ((Character) t).charValue() <= max; + } + + return false; + } + } + + private static final class MaxDoubleValueFilter implements Predicate { + + private final double max; + + MaxDoubleValueFilter(double max) { + this.max = max; + } + + @Override + public boolean test(Object t) { + if (t instanceof Number) { + return ((Number) t).doubleValue() <= max; + } + + if (t instanceof Character) { + return ((Character) t).charValue() <= max; + } + + return false; + } + } + + private static final class MinLongValueFilter implements Predicate { + + private final long min; + + MinLongValueFilter(long min) { + this.min = min; + } + + @Override + public boolean test(Object t) { + // Handle double and float separately, since we want to avoid treating -0.3 >= 0. + if (t instanceof Double) { + return ((Double) t).doubleValue() >= min; + } + + if (t instanceof Float) { + return ((Float) t).floatValue() >= min; + } + + if (t instanceof Number) { + return ((Number) t).longValue() >= min; + } + + if (t instanceof Character) { + return ((Character) t).charValue() >= min; + } + + return false; + } + } + + private static final class MinDoubleValueFilter implements Predicate { + + private final double min; + + MinDoubleValueFilter(double min) { + this.min = min; + } + + @Override + public boolean test(Object t) { + if (t instanceof Number) { + return ((Number) t).doubleValue() >= min; + } + + if (t instanceof Character) { + return ((Character) t).charValue() >= min; + } + + return false; + } + } + + private static int getLength(Object o) { + if (o == null) { + return -1; + } + + if (o.getClass().isArray()) { + return Array.getLength(o); + } + + // Support some of the common types with lengths. + if (o instanceof CharSequence) { + return ((CharSequence) o).length(); + } + + if (o instanceof Collection) { + return ((Collection) o).size(); + } + + if (o instanceof Map) { + return ((Map) o).size(); + } + + return -1; + } + + private static final class MinLengthFilter implements Predicate { + + private final int min; + + MinLengthFilter(int min) { + this.min = min; + } + + @Override + public boolean test(Object t) { + int len = getLength(t); + + if (len < 0) { + return false; + } + + return len >= min; + } + } + + private static final class MaxLengthFilter implements Predicate { + + private final int max; + + MaxLengthFilter(int max) { + this.max = max; + } + + @Override + public boolean test(Object t) { + int len = getLength(t); + + if (len < 0) { + return false; + } + + return len <= max; + } + } + + private static final class EqualsValueFilter implements Predicate { + + private final String val; + + EqualsValueFilter(String val) { + this.val = val; + } + + @Override + public boolean test(Object t) { + if (t == null) { + return false; + } + + try { + if (t instanceof Number) { + if ((t instanceof Double) || (t instanceof Float)) { + return Double.parseDouble(val) == ((Number) t).doubleValue(); + } else { + return Long.parseLong(val) == ((Number) t).longValue(); + } + } else if (t instanceof CharSequence) { + return val.equals((CharSequence) t); + } else if (t instanceof Class) { + return val.equals(((Class) t).getName()); + } else { + return val.equals(t.toString()); + } + } catch (NumberFormatException e) { + // Ignore and return false. + } + + return false; + } + } + + private static final class StartsWithValueFilter implements Predicate { + + private final String prefix; + + StartsWithValueFilter(String prefix) { + this.prefix = prefix; + } + + @Override + public boolean test(Object t) { + if (t != null) { + if (t instanceof Class) { + return ((Class) t).getName().startsWith(prefix); + } + + if (t instanceof CharBuffer) { + return t.toString().startsWith(prefix); + } + + return t.toString().startsWith(prefix); + } + + return false; + } + } + + private static final class EndsWithValueFilter implements Predicate { + + private final String suffix; + + EndsWithValueFilter(String suffix) { + this.suffix = suffix; + } + + @Override + public boolean test(Object t) { + if (t != null) { + if (t instanceof Class) { + return ((Class) t).getName().endsWith(suffix); + } + + return t.toString().endsWith(suffix); + } + + return false; + } + } + + private static final class ContainsValueFilter implements Predicate { + + private final String part; + + ContainsValueFilter(String part) { + this.part = part; + } + + @Override + public boolean test(Object t) { + if (t != null) { + if (t instanceof Class) { + return ((Class) t).getName().contains(part); + } + + return t.toString().contains(part); + } + + return false; + } + } + + private static final class MatchesRegexpValueFilter implements Predicate { + + private final Pattern pattern; + + MatchesRegexpValueFilter(String pattern) { + this.pattern = Pattern.compile(pattern); + } + + @Override + public boolean test(Object t) { + if (t != null) { + if (t instanceof Class) { + return pattern.matcher(((Class) t).getName()).find(); + } else if (t instanceof CharSequence) { + return pattern.matcher((CharSequence) t).find(); + } + + return pattern.matcher(t.toString()).find(); + } + + return false; + } + } + + private static final class InstanceofValueFilter implements Predicate { + + private final String clazz; + + InstanceofValueFilter(String clazz) { + this.clazz = clazz; + } + + private boolean instanceofImpl(Class base) { + if (base == null) { + return false; + } + + if (base.getName().equals(clazz)) { + return true; + } + + if (instanceofImpl(base.getSuperclass())) { + return true; + } + + for (Class interf : base.getInterfaces()) { + if (instanceofImpl(interf)) { + return true; + } + } + + return false; + } + + @Override + public boolean test(Object t) { + if (t instanceof Class) { + if ("java.lang.Class".equals(clazz)) { + return true; + } + + return instanceofImpl((Class) t); + } else if (t != null) { + return instanceofImpl(t.getClass()); + } + + return false; + } + } + + private static class SeenStack { + private final int hashCode; + private final StackTraceElement[] stack; + + public SeenStack() { + stack = new Throwable().fillInStackTrace().getStackTrace(); + hashCode = Arrays.hashCode(stack); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object other) { + if (other instanceof SeenStack) { + SeenStack otherStack = (SeenStack) other; + + if (otherStack.hashCode != hashCode) { + return false; + } + + return Arrays.equals(stack, otherStack.stack); + } + + return false; + } + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/LocaleChangeLogger.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/LocaleChangeLogger.java new file mode 100644 index 0000000000..ec5ad085db --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/LocaleChangeLogger.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.converters; + +import java.util.Locale; + +import org.openjdk.jmc.agent.sap.boot.util.ArgumentsHolder; +import org.openjdk.jmc.agent.sap.boot.util.Command; +import org.openjdk.jmc.agent.sap.boot.util.LoggingUtils; +import org.openjdk.jmc.agent.sap.boot.util.OutputCommand; + +public class LocaleChangeLogger { + + private static final ThreadLocal categoryKey = new ThreadLocal(); + + public static Command command = new OutputCommand("traceLocaleChange", + "Traces when the default locale is changed."); + + private static final ArgumentsHolder holder = command.getArguments(); + + public static String logDefaultLocaleCategoryChange(Locale.Category newCategory) { + assert categoryKey.get() == null; + categoryKey.set(newCategory); + + return newCategory.name(); + } + + public static String logDefaultLocale(Locale.Category newCategory) { + assert categoryKey.get() != null; + + return Locale.getDefault(newCategory).getDisplayName(Locale.ENGLISH); + } + + public static String logDefaultLocalChange(Locale newLocale) { + assert categoryKey.get() != null; + + return newLocale.getDisplayName(Locale.ENGLISH); + } + + public static boolean changesDefaultLocale(Locale newLocale) { + assert categoryKey.get() != null; + Locale oldLocale = Locale.getDefault(categoryKey.get()); + boolean result = !oldLocale.equals(newLocale); + + if (result) { + LoggingUtils.logWithStack(holder.get(), + "Changed default locale for category '" + categoryKey.get().name() + "' from '" + + oldLocale.getDisplayName(Locale.ENGLISH) + "' to '" + + newLocale.getDisplayName(Locale.ENGLISH) + "'.", + 2); + } + + categoryKey.remove(); + + return result; + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/SystemPropChangeLogger.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/SystemPropChangeLogger.java new file mode 100644 index 0000000000..aa04c2f42f --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/SystemPropChangeLogger.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.converters; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Properties; + +import org.openjdk.jmc.agent.sap.boot.util.ArgumentsHolder; +import org.openjdk.jmc.agent.sap.boot.util.Arguments; +import org.openjdk.jmc.agent.sap.boot.util.LoggingUtils; +import org.openjdk.jmc.agent.sap.boot.util.OutputCommand; + +public class SystemPropChangeLogger { + private static final Properties systemProps = AccessController + .doPrivileged((PrivilegedAction) () -> System.getProperties()); + + public static final OutputCommand command = new OutputCommand("traceSysPropsChange", + "Traces changes to the system properties."); + + private static final ArgumentsHolder holder = command.getArguments(); + + private static final ThreadLocal usedKey = new ThreadLocal(); + private static final ThreadLocal usedValue = new ThreadLocal(); + + // This is just used to get the old value in the JFR event too. + public static String logOldValue(Properties props) { + String key = usedKey.get(); + + return props.getProperty(key); + } + + public static boolean logProperties(Properties props) { + Arguments args = holder.get(); + String key = usedKey.get(); + assert key == null; + String val = usedValue.get(); + + usedKey.remove(); + usedValue.remove(); + + if (props == systemProps) { + String oldVal = props.getProperty(key); + + if (val == null) { + LoggingUtils.logWithStack(args, "System properties '" + key + "' with value '" + oldVal + "' removed", + 2); + } else { + LoggingUtils.logWithStack(args, + "System property '" + key + "' changed from '" + oldVal + "' to '" + val + "'", 2); + } + + return true; + } + + return false; + } + + public static String logKey(Object key) { + assert usedKey.get() == null; + + if (key instanceof String) { + usedKey.set((String) key); + + return (String) key; + } + + return ""; + } + + public static String logValue(Object value) { + assert usedValue.get() == null; + + if ((value instanceof String) || (value == null)) { + usedValue.set((String) value); + + return (String) value; + } + + return ""; + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/TimeZoneChangeLogger.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/TimeZoneChangeLogger.java new file mode 100644 index 0000000000..4cc0051193 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/TimeZoneChangeLogger.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.converters; + +import java.util.TimeZone; + +import org.openjdk.jmc.agent.sap.boot.util.ArgumentsHolder; +import org.openjdk.jmc.agent.sap.boot.util.Command; +import org.openjdk.jmc.agent.sap.boot.util.LoggingUtils; +import org.openjdk.jmc.agent.sap.boot.util.OutputCommand; + +public class TimeZoneChangeLogger { + + public static Command command = new OutputCommand("traceTimeZoneChange", + "Traces when the default time zone is changed.") { + public void preTraceInit() { + TimeZone.getDefault(); // Trigger loading before we trace to avoid circularity errors. + } + }; + + private static final ArgumentsHolder holder = command.getArguments(); + + public static String logDefaultTimeZoneChange(TimeZone newZone) { + String result = newZone.getDisplayName(); + + // If this doesn't changes the default time zone, don't log it. + if (changesDefaultTimeZone(newZone)) { + LoggingUtils.logWithStack(holder.get(), + "Changed default time zone to " + result + " (" + newZone.toZoneId().toString() + ").", 2); + } + + return result; + } + + public static String logDefaultTimeZoneIdChange(TimeZone newZone) { + return newZone.toZoneId().getId(); + } + + public static String logDefaultTimeZone(TimeZone newZone) { + return TimeZone.getDefault().getDisplayName(); + } + + public static String logDefaultTimeZoneId(TimeZone newZone) { + return TimeZone.getDefault().toZoneId().getId(); + } + + public static boolean changesDefaultTimeZone(TimeZone newZone) { + return !newZone.toZoneId().equals(TimeZone.getDefault().toZoneId()); + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/UnsafeMemoryAllocationLogger.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/UnsafeMemoryAllocationLogger.java new file mode 100644 index 0000000000..6d823b501f --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/converters/UnsafeMemoryAllocationLogger.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.converters; + +import org.openjdk.jmc.agent.sap.boot.util.ArgumentsHolder; +import org.openjdk.jmc.agent.sap.boot.util.Command; +import org.openjdk.jmc.agent.sap.boot.util.Arguments; +import org.openjdk.jmc.agent.sap.boot.util.Dumps; +import org.openjdk.jmc.agent.sap.boot.util.LoggingUtils; + +public class UnsafeMemoryAllocationLogger { + public static final String MAX_FRAMES = "maxFrames"; + public static final String MIN_STACK_SIZE = "minStackSize"; + public static final String MIN_SIZE = "minSize"; + public static final String MIN_INCREASE = "minIncrease"; + public static final String MIN_PERCENTAGE = "minPercentage"; + public static final String MIN_AGE = "minAge"; + public static final String MAX_AGE = "maxAge"; + public static final String MUST_CONTAIN = "mustContain"; + public static final String MUST_NOT_CONTAIN = "mustNotContain"; + public static final Command dumpCommand; + public static final Command command; + + private static final ThreadLocal sizeKey = new ThreadLocal(); + private static final ThreadLocal ptrKey = new ThreadLocal(); + private static final AllocationStatistic allocations = new AllocationStatistic(); + private static final ArgumentsHolder holder; + + static { + // spotless:off + dumpCommand = new Command( + "unsafeAllocations", "Dump the currently active jdk.internal.misc.Unsafe allocatios.", + MAX_FRAMES, "The maximum number of frame to use for stack traces.", + MIN_SIZE, "The minimum size of the live allocations to dump the result.", + MIN_STACK_SIZE, "The minimum size of a stack to be included in a dump.", + MIN_PERCENTAGE, "The minimum percentage compared to the last dump to print a dump.", + MIN_AGE, "The minimum age to include an allocation in the output.", + MAX_AGE, "The maximum age to include an allocation in the output.", + MUST_CONTAIN, "A regexp which must match at least one frame to be printed.", + MUST_NOT_CONTAIN, "A regexp which must not match any frame to be printed."); + command = new Command(dumpCommand, + "traceUnsafeAllocations", "Traces native memory allocation by jdk.internal.misc.Unsafe") { + public void preTraceInit() { + Dumps.registerPeriodicDump(command, "Unsafe native memory allocation", + (Arguments args) -> printActiveAllocations(args)); + } + }; + // spotless:on + + LoggingUtils.addOptions(command); + Dumps.addOptions(command); + Dumps.registerOnDemandDump(dumpCommand, (Arguments args) -> printActiveAllocations(args)); + holder = command.getArguments(); + } + + public static long logSize(long size) { + assert sizeKey.get() == null; + sizeKey.set(size); + + return size; + } + + public static long logPtr(long ptr) { + assert ptrKey.get() == null; + assert sizeKey.get() != null; + ptrKey.set(ptr); + + return ptr; + } + + public static long logResult(long result) { + Long toAdd = Long.valueOf(result); + Long oldPtr = ptrKey.get(); + + if ((oldPtr != null) && (oldPtr.longValue() != 0)) { + // This is realloc. + Long newSize = sizeKey.get(); + + if (newSize != null) { + allocations.removeAllocation(oldPtr); + + // Realloc with size 0 is a free. + if (newSize > 0) { + allocations.addAllocation(toAdd, newSize); + } + } + } else { + // This is malloc. + Long size = sizeKey.get(); + + if (size != null) { + allocations.addAllocation(toAdd, size); + } + } + + sizeKey.remove(); + ptrKey.remove(); + + return result; + } + + public static long logFree(long ptr) { + assert sizeKey.get() == null; + assert ptrKey.get() == null; + + Long toRemove = Long.valueOf(ptr); + + if (toRemove != null) { + allocations.removeAllocation(toRemove); + } + + return ptr; + } + + public static boolean printActiveAllocations() { + return printActiveAllocations(holder.get()); + } + + public static boolean printActiveAllocations(Arguments args) { + if (LoggingUtils.doesOutput(args)) { + return allocations.copy().printActiveAllocations(args); + } + + return false; + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/Arguments.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/Arguments.java new file mode 100644 index 0000000000..acd4c86cf0 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/Arguments.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.util; + +import java.util.HashMap; +import java.util.regex.Pattern; + +public class Arguments { + + private final HashMap args; + private final Command command; + private volatile Object customData; + + Arguments(String optionsLine) { + this.command = null; + this.args = getOptions(optionsLine); + } + + Arguments(String line, Command command) { + this.command = command; + this.args = getOptions(line); + } + + public Command getCommand() { + return command; + } + + public Object getCustomData() { + return customData; + } + + public void setCustomData(Object customData) { + this.customData = customData; + } + + public boolean hasOption(String option) { + return args.containsKey(option); + } + + public boolean hasHelpOption() { + return args.containsKey("help"); + } + + public String getString(String option, String defaultResult) { + if (args.containsKey(option)) { + String[] opts = args.get(option); + + if (opts.length == 0) { + return null; + } + + if (opts.length == 1) { + return opts[0]; + } + + reportOptionError(option, "Found " + opts.length + " values for option. Expected only one."); + } + + return defaultResult; + } + + public String[] getStrings(String option) { + if (args.containsKey(option)) { + return args.get(option); + } + + return new String[0]; + } + + public boolean getBoolean(String option, boolean defaultResult) { + if (args.containsKey(option)) { + return Boolean.parseBoolean(getString(option, "")); + } + + return defaultResult; + } + + public Pattern getPattern(String option, Pattern defaultResult) { + if (args.containsKey(option)) { + return Pattern.compile(getString(option, "")); + } + + return defaultResult; + } + + public int getInt(String option, int defaultRersult) { + if (args.containsKey(option)) { + try { + return Integer.parseInt(getString(option, "")); + } catch (NumberFormatException e) { + reportOptionError(option, "Could not parse integer value"); + } + } + + return defaultRersult; + } + + public String getUnknownArgument() { + for (String key : args.keySet()) { + if (!command.hasOption(key)) { + return key; + } + } + + return null; + } + + public long getLong(String option, long defaultRersult) { + if (args.containsKey(option)) { + try { + return Long.parseLong(getString(option, "")); + } catch (NumberFormatException e) { + reportOptionError(option, "Could not parse integer value"); + } + } + + return defaultRersult; + } + + public double getDouble(String option, double defaultRersult) { + if (args.containsKey(option)) { + try { + return Double.parseDouble(getString(option, "")); + } catch (NumberFormatException e) { + reportOptionError(option, "Could not parse floating point value"); + } + } + + return defaultRersult; + } + + private long parseUnits(String option, long defaultResult, char[] suffixes, long[] scale) { + if (!args.containsKey(option)) { + return defaultResult; + } + + String rest = getString(option, ""); + long result = 0; + + while (!rest.isEmpty()) { + long part = 0; + boolean isNeg = false; + + if (rest.startsWith("-")) { + isNeg = true; + rest = rest.substring(1); + } + + while (!rest.isEmpty()) { + int c = rest.charAt(0); + + if ((c < '0') || (c > '9')) { + break; + } + + part = part * 10 + (c - '0'); + rest = rest.substring(1); + } + + if (rest.isEmpty()) { + result += part * (isNeg ? -1 : 1); + break; + } + + boolean found = false; + + for (int i = 0; i < suffixes.length; ++i) { + if (rest.charAt(0) == suffixes[i]) { + result += part * scale[i] * (isNeg ? -1 : 1); + found = true; + break; + } + } + + if (!found) { + reportOptionError(option, "Unknown unit '" + rest.charAt(0) + "'."); + } + + rest = rest.substring(1); + } + + return result; + } + + public long getSize(String option, long defaultRersult) { + return parseUnits(option, defaultRersult, new char[] {'k', 'M', 'G'}, + new long[] {1024, 1024 * 1024, 1024 * 1024 * 1024}); + } + + public long getDurationInSeconds(String option, long defaultRersult) { + return parseUnits(option, defaultRersult, new char[] {'s', 'm', 'h', 'd'}, new long[] {1, 60, 3600, 3600 * 24}); + } + + private static String dequote(String str) { + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < str.length(); ++i) { + char c = str.charAt(i); + + if (c == '\\') { + if (i + 1 < str.length()) { + result.append(str.charAt(i + 1)); + i += 1; + } else { + // Trailing backslash is treated just as a backslash. + result.append(c); + } + } else { + result.append(c); + } + } + + return result.toString(); + } + + private static HashMap getOptions(String line) { + HashMap result = new HashMap<>(); + String[] keysAndValues = line.split("(? 0) { + String[] parts = keyAndValue.split("(? 0) { + String key = dequote(parts[0]); + + if (parts.length > 1) { + String[] oldOpts = result.containsKey(key) ? result.get(key) : new String[0]; + String[] newOpts = new String[oldOpts.length + 1]; + System.arraycopy(oldOpts, 0, newOpts, 0, oldOpts.length); + newOpts[oldOpts.length] = dequote(parts[1]); + result.put(key, newOpts); + } + } + } + } + + return result; + } + + private void reportOptionError(String option, String msg) { + System.err.println("Error in option " + option + "=" + getString(option, "") + " for command '" + + command.getName() + "'"); + System.err.println(msg); + System.exit(1); + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/ArgumentsHolder.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/ArgumentsHolder.java new file mode 100644 index 0000000000..05fca1b3f1 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/ArgumentsHolder.java @@ -0,0 +1,49 @@ +/* + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.util; + +public class ArgumentsHolder { + + private volatile Arguments args; + + public ArgumentsHolder(Arguments args) { + this.args = args; + } + + public void set(Arguments args) { + this.args = args; + } + + public Arguments get() { + return args; + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/Command.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/Command.java new file mode 100644 index 0000000000..64e583c993 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/Command.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.util; + +import java.io.PrintStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Objects; + +public class Command { + private final String name; + private final String description; + private final HashMap optionsWithHelp; + public static final HashMap holders = new HashMap<>(); + + public Command(String name, String description, String ... optionsWithHelp) { + this.name = name; + this.description = description; + this.optionsWithHelp = new HashMap<>(); + + for (int i = 0; i < optionsWithHelp.length; i += 2) { + this.optionsWithHelp.put(optionsWithHelp[i], optionsWithHelp[i + 1]); + } + } + + public Command(Command parentCommand, String name, String description, String ... optionsWithHelp) { + this.name = name; + this.description = description; + this.optionsWithHelp = new HashMap<>(parentCommand.optionsWithHelp); + + for (int i = 0; i < optionsWithHelp.length; i += 2) { + this.optionsWithHelp.put(optionsWithHelp[i], optionsWithHelp[i + 1]); + } + } + + public void addOption(String option, String description) { + optionsWithHelp.put(option, description); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String[] getOptions() { + return optionsWithHelp.keySet().toArray(new String[optionsWithHelp.size()]); + } + + public boolean hasOption(String name) { + if (optionsWithHelp.containsKey(name)) { + return true; + } + + for (String option : optionsWithHelp.keySet()) { + if (option.contains("")) { + if (name.matches(option.replace("", "[0-9]+"))) { + return true; + } + } + } + + return false; + } + + public String getOptionHelp(String name) { + return optionsWithHelp.get(name); + } + + public void preTraceInit() { + // Nothing to do by default + } + + public void addCommandArgs(String options) { + Arguments args = new Arguments(options, this); + boolean seenFirst = false; + + synchronized (Command.class) { + ArgumentsHolder holder = holders.get(name); + + if (holder == null) { + holder = new ArgumentsHolder(args); + holders.put(name, holder); + seenFirst = true; + } else { + if (holder.get() == null) { + seenFirst = true; + } + + holder.set(args); + } + } + + if (seenFirst) { + preTraceInit(); + } + } + + public ArgumentsHolder getArguments() { + synchronized (Command.class) { + ArgumentsHolder holder = holders.get(name); + + if (holder == null) { + holder = new ArgumentsHolder(null); + holders.put(name, holder); + } + + return holder; + } + } + + public void printHelp(PrintStream str) { + str.println("Help for command '" + getName() + "':"); + str.println("Description: " + getDescription()); + String[] options = getOptions(); + Arrays.sort(options, String.CASE_INSENSITIVE_ORDER); + + if (options.length > 0) { + str.println(); + str.println("The following options are supported:"); + + for (String option : options) { + str.println(option + ": " + getOptionHelp(option)); + } + + str.println(); + str.println("In order to specify options, add them separated by commas after the command:"); + str.println(getName() + "[,[,,help to get further help for a specific command."); + } + + public static Command getCommand(String name) { + for (Command command : commands) { + if (command.getName().equals(name)) { + return command; + } + } + + return null; + } + + public static boolean checkCommands() { + for (Command command : commands) { + ArgumentsHolder holder = command.getArguments(); + + if (holder.get() == null) { + continue; // No arguments. + } + + Arguments args = holder.get(); + + if (args.hasHelpOption()) { + printHelp(command); + + return false; + } + + String unknownArgument = args.getUnknownArgument(); + + if (unknownArgument != null) { + // spotless:off + System.err.println("Unknown argument '" + unknownArgument + "' for command '" + command.getName() + "'."); + // spotless:on + printHelp(command); + + return false; + } + } + + return true; + } + + private static void printHelp(Command command) { + command.printHelp(System.err); + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/Dumps.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/Dumps.java new file mode 100644 index 0000000000..ac0620ccab --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/Dumps.java @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.util; + +import java.io.IOException; +import java.util.HashMap; +import java.util.function.Predicate; + +public class Dumps { + public static final String DUMP_COUNT = "dumpCount"; + public static final String DUMP_INTERVAL = "dumpInterval"; + public static final String DUMP_DELAY = "dumpDelay"; + public static final String EXIT_AFTER_LAST_DUMP = "exitAfterLastDump"; + + private static final HashMap> registeredDumps = new HashMap<>(); + + public static void performDump(String arguments) throws IOException { + if (arguments.equals("help")) { + System.out.println("The following dumps are supported:"); + + for (Command cmd : registeredDumps.keySet()) { + if (registeredDumps.get(cmd) != null) { + System.out.println(cmd.getName() + ": " + cmd.getDescription()); + } + } + + System.out.println("Use dump=help: for all options for a specific dump."); + return; + } else if (arguments.startsWith("help:")) { + String name = arguments.substring(5); + + for (Command cmd : registeredDumps.keySet()) { + if (cmd.getName().equals(name)) { + cmd.printHelp(System.out); + return; + } + } + + System.err.println("Could not find dump type '" + name + "'. Use dump=help for a list of supported types."); + return; + } + + String[] parts = arguments.split(","); + String type = parts[0]; + + if (parts.length > 1) { + arguments = arguments.substring(type.length() + 1); + } else { + arguments = ""; + } + + Arguments args = new Arguments(arguments); + Predicate callback = null; + + synchronized (Dumps.class) { + callback = registeredDumps.get(new Command(type, "")); + } + + if (callback == null) { + System.err.println("No dump registered for dump type '" + type + "'"); + return; + } + + callback.test(args); + } + + public static void registerPeriodicDump(Command command, String name, Predicate callback) { + Arguments args = command.getArguments().get(); + long dumpCount = args.getLong(DUMP_COUNT, 0); + + if (dumpCount != 0) { + long interval = args.getDurationInSeconds(DUMP_INTERVAL, 3600); + long delay = args.getDurationInSeconds(DUMP_DELAY, interval); + boolean exitAfterLastDump = args.getBoolean(EXIT_AFTER_LAST_DUMP, false); + + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + try { + Thread.sleep(delay * 1000); + + long dumpsLeft = dumpCount; + + while (dumpsLeft > 0) { + if (callback.test(args)) { + dumpsLeft -= 1; + LoggingUtils.log(args, + name + " dump " + (dumpCount - dumpsLeft) + " of " + dumpCount + "."); + } + + if (dumpsLeft > 0) { + Thread.sleep(interval * 1000); + } + } + + if (exitAfterLastDump) { + LoggingUtils.log(args, name + " dumps finished. Exiting VM."); + System.exit(0); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }, "Dumper Thread"); + + t.setDaemon(true); + t.start(); + } + } + + public static void registerOnDemandDump(Command command, Predicate callback) { + synchronized (Dumps.class) { + registeredDumps.put(command, callback); + } + } + + public static void addOptions(Command command) { + command.addOption(DUMP_COUNT, "The maximum number of dumps to perform."); + command.addOption(DUMP_INTERVAL, + "The interval between successive dumps. Supports s, m, h and d (e.g. 10s or 6m)."); + command.addOption(DUMP_DELAY, "The delay until the first dump is triggered. Supports s, m, h and d."); + command.addOption(EXIT_AFTER_LAST_DUMP, "If true, the VM will be exited after the last dump is triggered."); + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/LoggingUtils.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/LoggingUtils.java new file mode 100644 index 0000000000..9184320348 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/LoggingUtils.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.util; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Formatter; +import java.util.HashMap; +import java.util.IdentityHashMap; + +public class LoggingUtils { + public static final String LOG_DEST = "logDest"; + public static final String LOG_WITH_STACK = "logWithStack"; + private static HashMap outputs = new HashMap<>(); + private static IdentityHashMap formatters = new IdentityHashMap<>(); + + public static void addOptions(Command command) { + command.addOption(LOG_DEST, + "Specifies the output destination. Can be 'stdout', 'stderr', 'none' or a file name. " + + "Prepend the filename with a '+' to append to the file instead of overwriting it."); + command.addOption(LOG_WITH_STACK, "If not false, print a stack trace for every log output."); + } + + public static void addOptionsWithStack(Command command) { + addOptions(command); + command.addOption(LOG_WITH_STACK, "Print a stack trace for every log output."); + } + + public static boolean doesOutput(Arguments args) { + return !"none".equals(args.getString(LOG_DEST, "stderr")); + } + + public static Formatter getFormatter(Arguments args) { + PrintStream stream = getStream(args); + Formatter formatter; + + synchronized (formatters) { + formatter = formatters.get(stream); + + if (formatter == null) { + formatter = new Formatter(stream); + formatters.put(stream, formatter); + } + } + + return formatter; + } + + public static PrintStream getStream(Arguments args) { + String dest = args.getString(LOG_DEST, "stderr"); + + if ("none".equals(dest)) { + return new PrintStream(new OutputStream() { + + @Override + public void write(byte[] b) throws IOException { + // Just throw everything away. + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + // Just throw everything away. + } + + @Override + public void write(int b) throws IOException { + // Just throw everything away. + } + }); + } + + if ("stdout".equals(dest)) { + return System.out; + } + + if ("stderr".equals(dest)) { + return System.err; + } + + synchronized (outputs) { + PrintStream result = outputs.get(dest); + + if (result != null) { + return result; + } + + try { + if (dest.startsWith("+")) { + // Append if the file name starts with a +. + result = new PrintStream(new FileOutputStream(dest.substring(1), true), true); + } else { + result = new PrintStream(new FileOutputStream(dest, false), true); + } + } catch (FileNotFoundException e) { + System.err.println("Could not open file '" + dest + "' for output. Using stderr instead."); + // Don't try this again. + result = System.err; + } + + outputs.put(dest, result); + return result; + } + } + + public static void log(Arguments args, String msg) { + getStream(args).println(msg); + + if (args.getBoolean(LOG_WITH_STACK, false)) { + logCurrentStack(args, 1); + } + } + + public static void log(Arguments args, Object[] parts) { + PrintStream stream = getStream(args); + + for (Object part : parts) { + stream.print(part); + } + } + + public static void logWithFormat(Arguments args, String format, Object[] values) { + Formatter formatter = getFormatter(args); + formatter.format(format + "\n", values); + + if (args.getBoolean(LOG_WITH_STACK, false)) { + logCurrentStack(args, 1); + } + } + + private static void logCurrentStack(Arguments args, int toSkip) { + logWithStack(args, "", new Exception(), toSkip); + } + + public static void logWithStack(Arguments args, String msg, int toSkip) { + logWithStack(args, msg, new Exception(), toSkip); + } + + public static void logWithStack(Arguments args, String msg, Exception stack, int toSkip) { + PrintStream stream = getStream(args); + + if (msg.length() > 0) { + stream.println(msg); + } + + if (args.getBoolean(LOG_WITH_STACK, true)) { + StackTraceElement[] frames = stack.getStackTrace(); + + for (int i = toSkip; i < frames.length; ++i) { + stream.println("\t" + frames[i]); + } + } + } +} diff --git a/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/OutputCommand.java b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/OutputCommand.java new file mode 100644 index 0000000000..48a39095c6 --- /dev/null +++ b/agent/src/main/java/org/openjdk/jmc/agent/sap/boot/util/OutputCommand.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.boot.util; + +public class OutputCommand extends Command { + + public OutputCommand(String name, String description) { + super(name, description); + LoggingUtils.addOptions(this); + } +} diff --git a/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceLocaleChange.xml b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceLocaleChange.xml new file mode 100644 index 0000000000..feb8433b9e --- /dev/null +++ b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceLocaleChange.xml @@ -0,0 +1,43 @@ + + + __JFREvent + false + true + + + + + Change of the default locale + java.util.Locale + true + false + jdk/log/locale/changed + + setDefault + (Ljava/util/Locale$Category;Ljava/util/Locale;)V + + + newCategory + The category of the new locale + org.openjdk.jmc.agent.sap.boot.converters.LocaleChangeLogger.logDefaultLocaleCategoryChange()V + + + oldLocale + The current locale for the category + org.openjdk.jmc.agent.sap.boot.converters.LocaleChangeLogger.logDefaultLocale()V + + + newLocale + The new default locale + org.openjdk.jmc.agent.sap.boot.converters.LocaleChangeLogger.logDefaultLocalChange()V + + + hasLocaleChanged + Has the locale for the category really changed + org.openjdk.jmc.agent.sap.boot.converters.LocaleChangeLogger.changesDefaultLocale()V + + + + + + diff --git a/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceOpenFiles.xml b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceOpenFiles.xml new file mode 100644 index 0000000000..cbdce0044c --- /dev/null +++ b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceOpenFiles.xml @@ -0,0 +1,166 @@ + + + __JFREvent + false + true + + + + + Traces the FileInputStream constructor taking a File + java.io.FileInputStream + true + false + EXIT + jdk/log/file/statistic + + FileInputStream + (Ljava/io/File;)V + + + fileName + The file name + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.openFileInputStream()V + + + + + + hasOpened + this + has the file been opened + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.openFileInputStream()V + + + + + + Traces the FileInputStream close calls + java.io.FileInputStream + true + false + ENTRY + jdk/log/file/statistic + + close + ()V + + + + closedFile + this + The closed file + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.closeFileInputStream()V + + + + + + Traces the FileOutputStream constructor taking a File + java.io.FileOutputStream + true + false + EXIT + jdk/log/file/statistic + + FileOutputStream + (Ljava/io/File;Z)V + + + fileName + The file name + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.openFileOutputStream()V + + + append + The append flag + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.openFileOutputStream()V + + + + + + hasOpened + this + has the file been opened + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.openFileOutputStream()V + + + + + + Traces the FileOutputStream close calls + java.io.FileOutputStream + true + false + ENTRY + jdk/log/file/statistic + + close + ()V + + + + closedFile + this + The closed file + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.closeFileOutputStream()V + + + + + + Traces the RandomAccessFile constructor taking a File + java.io.RandomAccessFile + true + false + EXIT + jdk/log/file/statistic + + RandomAccessFile + (Ljava/io/File;Ljava/lang/String;)V + + + fileName + The file name + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.openRandomAccessFile()V + + + mode + The file mode + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.openRandomAccessFileMode()V + + + + + + hasOpened + this + has the file been opened + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.openRandomAccessFile()V + + + + + + Traces the RandomAccessFile close call + java.io.RandomAccessFile + true + false + EXIT + jdk/log/file/statistic + + close + ()V + + + + closedFile + this + The closed file + org.openjdk.jmc.agent.sap.boot.converters.FileOpenCloseLogger.closeRandomAccessFile()V + + + + + diff --git a/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceSysPropsChange.xml b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceSysPropsChange.xml new file mode 100644 index 0000000000..64fd0297be --- /dev/null +++ b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceSysPropsChange.xml @@ -0,0 +1,80 @@ + + + __JFREvent + false + true + + + + + Change of system property + java.util.Properties + true + false + jdk/log/sysProps/changed + + put + (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; + + + key + The key + org.openjdk.jmc.agent.sap.boot.converters.SystemPropChangeLogger.logKey()V + + + value + The changed value + org.openjdk.jmc.agent.sap.boot.converters.SystemPropChangeLogger.logValue()V + + + + + + oldValue + this + The old value + org.openjdk.jmc.agent.sap.boot.converters.SystemPropChangeLogger.logOldValue()V + + + isSystemProperty + this + Is this change for a System property + org.openjdk.jmc.agent.sap.boot.converters.SystemPropChangeLogger.logProperties()V + + + + + + Removing of system property + java.util.Properties + true + false + jdk/log/sysProps/changed + + remove + (Ljava/lang/Object;)Ljava/lang/Object; + + + key + The key + org.openjdk.jmc.agent.sap.boot.converters.SystemPropChangeLogger.logKey()V + + + + + + removedValue + this + The value of the removed property + org.openjdk.jmc.agent.sap.boot.converters.SystemPropChangeLogger.logOldValue()V + + + isSystemProperty + this + Is this change for a System property + org.openjdk.jmc.agent.sap.boot.converters.SystemPropChangeLogger.logProperties()V + + + + + diff --git a/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceTimeZoneChange.xml b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceTimeZoneChange.xml new file mode 100644 index 0000000000..d9b64d17fc --- /dev/null +++ b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceTimeZoneChange.xml @@ -0,0 +1,48 @@ + + + __JFREvent + false + true + + + + + Change of the default time zone + java.util.TimeZone + true + false + jdk/log/timeZone/changed + + setDefault + (Ljava/util/TimeZone;)V + + + newTimeZone + The new default time zone + org.openjdk.jmc.agent.sap.boot.converters.TimeZoneChangeLogger.logDefaultTimeZoneChange()V + + + newTimeZoneId + The new default time zone id + org.openjdk.jmc.agent.sap.boot.converters.TimeZoneChangeLogger.logDefaultTimeZoneIdChange()V + + + oldTimeZone + The old default time zone + org.openjdk.jmc.agent.sap.boot.converters.TimeZoneChangeLogger.logDefaultTimeZone()V + + + oldTimeZoneId + The old default time zone id + org.openjdk.jmc.agent.sap.boot.converters.TimeZoneChangeLogger.logDefaultTimeZoneId()V + + + changesDefault + Does this changes the default time zone, because it's not already the default + org.openjdk.jmc.agent.sap.boot.converters.TimeZoneChangeLogger.changesDefaultTimeZone()V + + + + + + diff --git a/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceUnsafeAllocations.xml b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceUnsafeAllocations.xml new file mode 100644 index 0000000000..bacccb255d --- /dev/null +++ b/agent/src/main/resources/org/openjdk/jmc/agent/sap/traceUnsafeAllocations.xml @@ -0,0 +1,81 @@ + + + __JFREvent + false + true + + + + + Memory trace of Unsafe allocations + jdk.internal.misc.Unsafe + true + false + jdk/log/Unsafe/memoryTrace + + allocateMemory + (J)J + + + size + The allocated size + org.openjdk.jmc.agent.sap.boot.converters.UnsafeMemoryAllocationLogger.logSize(J)J + + + + address + the allocated address + org.openjdk.jmc.agent.sap.boot.converters.UnsafeMemoryAllocationLogger.logResult(J)J + + + + + + Memory trace of Unsafe reallocations + jdk.internal.misc.Unsafe + true + false + jdk/log/Unsafe/memoryTrace + + reallocateMemory + (JJ)J + + + oldAddress + The old memory address + org.openjdk.jmc.agent.sap.boot.converters.UnsafeMemoryAllocationLogger.logPtr()V + + + size + The new allocated size + org.openjdk.jmc.agent.sap.boot.converters.UnsafeMemoryAllocationLogger.logSize()V + + + + address + the allocated address + org.openjdk.jmc.agent.sap.boot.converters.UnsafeMemoryAllocationLogger.logResult()V + + + + + + Memory trace of Unsafe frees + jdk.internal.misc.Unsafe + true + false + jdk/log/Unsafe/memoryTrace + + freeMemory + (J)V + + + address + The freed memory address + org.openjdk.jmc.agent.sap.boot.converters.UnsafeMemoryAllocationLogger.logFree()V + + + + + + diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/CloneClassLoader.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/CloneClassLoader.java new file mode 100644 index 0000000000..4fdd131e9e --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/CloneClassLoader.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.SecureClassLoader; +import java.util.Arrays; +import java.util.HashSet; + +/** + * This is a class loader which can load the same classes as another class loader. + *

+ * This is mainly useful for tests when you want to load a class, but do it with a class loader you + * can dispose. The clone loader just asks the loader to be cloned to get the bytecodes, but defines + * the class itself. + *

+ * Additionally you can specify a set of classes the loader should not be able to load. + * + * @author Ralf Schmelter + */ +public class CloneClassLoader extends SecureClassLoader { + + /** + * The class loaded to clone. + */ + private final ClassLoader toClone; + + /** + * The strings we cannot load. + */ + private final HashSet notLoadable; + + /** + * The strings we just delegate. + */ + private final HashSet simpleDelegate; + + /** + * Creates a class loader which can load the same classes as the loader which loaded the + * CloneClassLoader class itself. + *

+ * Only the bootstrap classes are delegated to the bootstrap class loader. + * + * @param toClone + * the class loader to mimic. The clone class loader will be able to load the same + * classes as the 'toClone' loader. + */ + public CloneClassLoader(ClassLoader toClone) { + this(null, toClone, new String[0], new String[0]); + } + + /** + * Creates a class loader which can load the same classes as the loader which loaded the + * CloneClassLoader class itself. + *

+ * Only the bootstrap classes are delegated to the bootstrap class loader. + * + * @param toClone + * the class loader to mimic. The clone class loader will be able to load the same + * classes as the 'toClone' loader. + * @param notLoadable + * The classes we should not be able to load via this loader. + */ + public CloneClassLoader(ClassLoader toClone, String[] notLoadable) { + this(null, toClone, notLoadable, new String[0]); + } + + /** + * Creates a class loader which can load the same classes as the loader which loaded the + * CloneClassLoader class itself. + *

+ * Only the classes which are loadable by the 'parent' loader are delegated to that loader (to + * make it possible mix classes). + * + * @param parent + * the parent loader which is first asked to load a class. + * @param toClone + * the class loader to mimic. The clone class loader will be able to load the same + * classes as the 'toClone' loader. + */ + public CloneClassLoader(ClassLoader parent, ClassLoader toClone) { + this(parent, toClone, new String[0], new String[0]); + } + + /** + * Creates a class loader which can load the same classes as the loader which loaded the + * CloneClassLoader class itself. + *

+ * Only the classes which are loadable by the 'parent' loader are delegated to that loader (to + * make it possible mix classes). + * + * @param parent + * the parent loader which is first asked to load a class. + * @param toClone + * the class loader to mimic. The clone class loader will be able to load the same + * classes as the 'toClone' loader. + * @param notLoadable + * The classes we should not be able to load via this loader. + * @param simpleDelegate + * The names of the classes for which we simply delegate. + */ + public CloneClassLoader(ClassLoader parent, ClassLoader toClone, String[] notLoadable, String[] simpleDelegate) { + super(parent); + + this.toClone = toClone; + this.notLoadable = new HashSet<>(Arrays.asList(notLoadable)); + this.simpleDelegate = new HashSet<>(Arrays.asList(simpleDelegate)); + } + + /** + * @see java.lang.ClassLoader#findClass(java.lang.String) + */ + @Override + protected Class findClass(String name) throws ClassNotFoundException { + if (notLoadable.contains(name)) { + throw new ClassNotFoundException("The clone class loader explicitly didn't find the class '" + name + "'"); + } + + if (simpleDelegate.contains(name)) { + return toClone.loadClass(name); + } + + // We just ask the wrapper class loader to find the resource for us + URL res = toClone.getResource(name.replace('.', '/') + ".class"); + + if (res == null) { + throw new ClassNotFoundException(name); + } + + try { + InputStream is = res.openStream(); + byte[] code = readStreamIntoBuffer(is, 8192); + is.close(); + return defineClass(name, code, 0, code.length); + } catch (IOException e) { + throw new ClassNotFoundException(name, e); + } + } + + /** + * Reads all data of a stream into a byte array. The method allocates as much memory as + * necessary to put the whole data into that byte array. The data is read in chunks of + * chunkSize chunks.
+ *
+ * Implementation Note: The data is read in chunks of chunkSize bytes. The + * data is copied to the result array. The memory consumption at the end of the reading is + * 2 x [size of resulting array] + chunkSize. + * + * @param is + * the stream to read the data from + * @param chunkSize + * the size of the chunks the data should be read in + * @return the whole data of the stream read into an byte array + * @throws IllegalArgumentException + * if chunkSize <= 0 + * @throws NullPointerException + * if is == null + * @throws IOException + * thrown if the provided stream encounters IO problems + */ + public static byte[] readStreamIntoBuffer(InputStream is, int chunkSize) throws IOException { + + // check preconditions + if (chunkSize <= 0) { + throw new IllegalArgumentException("chunkSize <= 0"); + } else if (is == null) { + throw new NullPointerException("is is null"); + } + + // temporary buffer for read operations and result buffer + byte[] tempBuffer = new byte[chunkSize]; + byte[] buffer = new byte[0]; + + int bytesRead = 0; // bytes actual read + int oldSize = 0; // size of the resulting buffer + + while ((bytesRead = is.read(tempBuffer)) > 0) { + + // temporary reference to the buffer for the copy operation + byte[] oldBuffer = buffer; + + // create a new buffer with the size needed and copy data + buffer = new byte[oldSize + bytesRead]; + System.arraycopy(oldBuffer, 0, buffer, 0, oldBuffer.length); + + // copy the new data + System.arraycopy(tempBuffer, 0, buffer, oldSize, bytesRead); + oldSize += bytesRead; + } + + return buffer; + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/DuplicateTracedClasses.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/DuplicateTracedClasses.java new file mode 100644 index 0000000000..bb53ab949b --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/DuplicateTracedClasses.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.lang.reflect.Method; + +public class DuplicateTracedClasses { + + public static void main(String[] args) throws Exception { + CloneClassLoader cl1 = new CloneClassLoader(DuplicateTracedClasses.class.getClassLoader()); + CloneClassLoader cl2 = new CloneClassLoader(DuplicateTracedClasses.class.getClassLoader()); + Class clz1 = cl1.loadClass(DuplicateTracedClasses.class.getName()); + Class clz2 = cl2.loadClass(DuplicateTracedClasses.class.getName()); + Method m1 = clz1.getDeclaredMethod("test", String.class); + Method m2 = clz2.getDeclaredMethod("test", String.class); + m1.invoke(null, "Loader 1"); + m2.invoke(null, "Loader 2"); + test("Default loader"); + } + + public static void test(String msg) { + // Nothing to do. + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/GenericLoggingTest.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/GenericLoggingTest.java new file mode 100644 index 0000000000..3b282ef72e --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/GenericLoggingTest.java @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.io.File; +import java.io.InputStream; +import java.io.Serializable; +import java.nio.CharBuffer; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; + +public class GenericLoggingTest extends TestBase implements VmAgnostic { + + public static void main(String[] args) { + new GenericLoggingTest().dispatch(args); + } + + public static void genericLogging4() { + Object[][] oa1 = new Object[][] {new Object[1], new Serializable[2][], new String[3]}; + String[] sa = new String[] {"just", "a", "test", null, "with", "many", "strings", "!"}; + boolean[] za = new boolean[] {true, false, true, false, true}; + boolean[] za1 = new boolean[] {true}; + HashMap map1 = new HashMap<>(); + map1.put("test", new C1()); + LinkedHashMap map2 = new LinkedHashMap<>(); // Must be linked to stable order. + map2.put("test1", new C1()); + map2.put("test2", new C2()); + map2.put("test3", new C3()); + map2.put(null, new C1()); + map2.put("test5", new C2()); + map2.put("test6", new C3()); + DummyList dl1 = new DummyList(map2.values(), true); + DummyList dl2 = new DummyList(map2.values(), false); + LinkedHashMap map3 = new LinkedHashMap<>(); + map3.put("map1", map1); + map3.put("map2", map2); + Object[] oa2 = new Object[] {oa1, sa, map1, map3, za, za1}; + + // The calls we expect to be logged + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], new C1(), new C1(), new C3(), "Hehe!"); + tracePrimitives1(false, (byte) -1, (short) -7, 'C', 128, -7, 0.2f, -2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'C', -2, 7, -0.2f, 2.2); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 77132, -0.22387f, 2.1332); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), + new StringBuilder("Hehe!")); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), + new StringBuffer("Hehe!")); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), + CharBuffer.allocate(5).append("Hele!").position(0)); + traceObjects2(map1, map2, za, null, dl1, dl2, map3, oa2); + traceObjects3(dl2, sa, za, map1, map2, oa1, Class.class, new StringBuilder("Jeje")); + + // The calls we expect to be filtered out. + tracePrimitives1(true, (byte) -1, (short) -7, 'C', 128, -7, 0.2f, -2.2); + tracePrimitives1(false, (byte) -2, (short) -7, 'C', 128, -7, 0.2f, -2.2); + tracePrimitives1(false, (byte) 3, (short) -7, 'C', 128, -7, 0.2f, -2.2); + tracePrimitives1(false, (byte) -1, (short) -8, 'C', 128, -7, 0.2f, -2.2); + tracePrimitives1(false, (byte) -1, (short) 2, 'C', 128, -7, 0.2f, -2.2); + tracePrimitives1(false, (byte) -1, (short) -7, 'B', 128, -7, 0.2f, -2.2); + tracePrimitives1(false, (byte) -1, (short) -7, 'D', 128, -7, 0.2f, -2.2); + tracePrimitives1(false, (byte) -1, (short) -7, 'C', 127, -7, 0.2f, -2.2); + tracePrimitives1(false, (byte) -1, (short) -7, 'C', 129, -7, 0.2f, -2.2); + tracePrimitives1(false, (byte) -1, (short) -7, 'C', 128, -8, 0.2f, -2.2); + tracePrimitives1(false, (byte) -1, (short) -7, 'C', 128, -6, 0.2f, -2.2); + tracePrimitives1(false, (byte) -1, (short) -7, 'C', 128, -7, 0.1f, -2.2); + tracePrimitives1(false, (byte) -1, (short) -7, 'C', 128, -7, 0.3f, -2.2); + tracePrimitives2('D', (byte) 1, (short) 7, 'C', -2, 7, -0.2f, 2.2); + tracePrimitives2('C', (byte) 0, (short) 7, 'C', -2, 7, -0.2f, 2.2); + tracePrimitives2('C', (byte) 2, (short) 7, 'C', -2, 7, -0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 6, 'C', -2, 7, -0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 8, 'C', -2, 7, -0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'B', -2, 7, -0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'D', -2, 7, -0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'C', -4, 7, -0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'C', 0, 7, -0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'C', -2, 6, -0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'C', -2, 8, -0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'C', -2, 7, -1.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'C', -2, 7, 0.2f, 2.2); + tracePrimitives2('C', (byte) 1, (short) 7, 'C', -2, 7, -0.2f, 2.1); + tracePrimitives2('C', (byte) 1, (short) 7, 'C', -2, 7, -0.2f, 2.3); + tracePrimitives3('E', (byte) 127, (short) 15, 'E', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 126, (short) 15, 'E', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 107, (short) 15, 'E', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 27, (short) 15, 'E', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 215, 'E', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 151, 'E', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15315, 'E', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15715, 'E', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'e', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'f', -222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', 222, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -223, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -232, 77132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 67132, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 77133, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 77232, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 76232, -0.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 77132, -1.22387f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 77132, -0.22367f, 2.1332); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 77132, -0.22387f, 2.1432); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 77132, -0.22387f, 2.1335); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 77132, -0.22387f, 2.1533); + tracePrimitives3('e', (byte) 127, (short) 15, 'E', -222, 77132, -0.22387f, -2.1332); + traceObjects1("", new byte[3], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new Byte[3], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[2], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[5], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4], new Integer[10], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[3][], new Integer[10], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[6][], new Integer[10], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4][], new byte[10], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4][], new Integer[9], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4][], new Integer[11], C1.class, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], null, new C1(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], C1.class, C1.class, new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], C1.class, new C2(), new C3(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], C1.class, new C1(), new C2(), "Hehe!"); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), "Hehe!!"); + traceObjects1(null, new byte[3], new byte[4][], new Integer[10], C1.class, new C1(), new C3(), "Heha!"); + traceObjects2(za1, map2, za, null, dl1, dl2, map3, oa2); + traceObjects2(map2, map2, za, null, dl1, dl2, map3, oa2); + traceObjects2(map1, map1, za, null, dl1, dl2, map3, oa2); + traceObjects2(map1, map2, new Object[0], "null", dl1, dl2, map3, oa2); + traceObjects2(map1, map2, za, "null", dl1, dl2, map3, oa2); + traceObjects2(map1, map2, za, null, new int[10], dl2, map3, oa2); + traceObjects2(map1, map2, za, null, map1, dl2, map3, oa2); + traceObjects2(map1, map2, za, null, dl1, dl1, map3, oa2); + traceObjects2(map1, map2, za, null, dl1, dl2, map2, oa2); + } + + protected void runAllTests() throws Exception { + File xmlFile = new File("generic_logger.xml").getAbsoluteFile(); + + try (InputStream is = GenericLoggingTest.class.getClassLoader() + .getResourceAsStream("org/openjdk/jmc/agent/test/sap/generic.xml")) { + Files.copy(is, xmlFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + + String filter1 = "value1Equals=false,value1NotEquals=true,value1StartsWith=fal,value1EndsWith=lse," + + "value1NotStartsWith=tr,value1NotEndsWith=ue,value1Contains=ls," + + "value1NotContains=ru,value1MatchesRegexp=a.s,value1NotMatchesRegexp=r.e," + + "minLongValue2=-1,maxLongValue2=2,minDoubleValue3=-7.2,maxDoubleValue3=1.1," + + "minLongValue4=67,maxLongValue4=67,minDoubleValue5=127.2,maxDoubleValue5=128.1," + + "minLongValue6=-7,maxLongValue6=-7,minDoubleValue7=0.15,maxDoubleValue7=0.25"; + String filter2 = "value1Equals=C,value1StartsWith=C,value1EndsWith=C,value1Contains=C," + + "value1MatchesRegexp=^[C]$,minDoubleValue2=0.9,maxDoubleValue2=1.1," + + "minLongValue3=7,maxLongValue3=7,minDoubleValue4=66.3,maxDoubleValue4=67.1," + + "minLongValue5=-3,maxLongValue5=-1,minDoubleValue6=6.9,maxDoubleValue6=7.1," + + "minLongValue7=-1,maxLongValue7=0,minDoubleValue8=2.15,maxDoubleValue8=2.25"; + String filter3 = "value1NotMatchesRegexp=E,value2MatchesRegexp=1?[12][76],value2EndsWith=7," + + "value2Contains=1,value3StartsWith=15,value3EndsWith=15,value3NotContains=3," + + "value3NotMatchesRegexp=7,value3Instanceof=java.lang.Short," + + "value3Instanceof=java.lang.Number,value3Instanceof=java.lang.Object," + + "value4Instanceof=java.lang.Character,value4NotInstanceof=java.lang.Number," + + "value4NotContains=e,value4MatchesRegexp=[eE],value5StartsWith=-," + + "value5EndsWith=2,value5MatchesRegexp=22,value6NotStartsWith=6,value6NotEndsWith=3," + + "value6NotContains=23,value6MatchesRegexp=.7.3,value7NotContains=6,value7MatchesRegexp=0[.]," + + "value7Instanceof=java.lang.Float,value8Instanceof=java.lang.Number," + + "value8Instanceof=java.lang.Double,value8NotInstanceof=java.lang.Float," + + "value8NotEquals=2.1432,value8NotContains=35,value8NotEndsWith=3,value8NotStartsWith=-"; + String filter4 = "value1IsType=null,value2IsNotType=object_array,value2IsType=array," + + "value2IsType=primitive_array,value2MinLength=3,value2MaxLength=4,value3IsType=array," + + "value3IsNotType=primitive_array,value3IsType=object_array,value3MinLength=4," + + "value3MaxLength=5,value4IsType=array,value4IsNotType=primitive_array," + + "value4IsType=object_array,value4MinLength=10,value4MaxLength=10,value5IsNotType=null," + + "value5Instanceof=org.openjdk.jmc.agent.sap.test.C1,value5Instanceof=java.lang.Object," + + "value5Instanceof=java.lang.Class,value5Instanceof=java.lang.Comparable," + + "value6Instanceof=org.openjdk.jmc.agent.sap.test.C1,value6Instanceof=java.lang.Object," + + "value6NotInstanceof=java.lang.Class,value6NotInstanceof=org.openjdk.jmc.agent.sap.test.C2," + + "value7Instanceof=org.openjdk.jmc.agent.sap.test.I3,value7Instanceof=org.openjdk.jmc.agent.sap.test.C2," + + "value7Instanceof=org.openjdk.jmc.agent.sap.test.C1,value7Instanceof=org.openjdk.jmc.agent.sap.test.I2," + + "value7Instanceof=org.openjdk.jmc.agent.sap.test.I1,value8StartsWith=He,value8EndsWith=!," + + "value8MatchesRegexp=.e.e,value8MinLength=5,value8MaxLength=5,value8Contains=e!"; + String filter5 = "printCollectionContent=true,value1MaxLength=1,value1MinLength=1," + + "value1IsNotType=array,value2MinLength=6,value2IsNotType=primitive_array," + + "value2IsNotType=object_array,value2IsNotType=null,value3IsType=primitive_array," + + "value4IsType=null,value4IsNotType=object_array,value4IsNotType=array," + + "value5MinLength=6,value5IsNotType=array,value6StartsWith=JustA,value6EndsWith=Dummy," + + "value6MatchesRegexp=.ust.Du[m-n].y,value6Contains=ADum,value6Equals=JustADummy," + + "value7MaxLength=2"; + String filter6 = "printCollectionContent=false"; + JavaAgentRunner runner = getRunner(xmlFile.getPath() + ",logGeneric1,logDest=stdout," + filter1 + + ",logGeneric2,logDest=stdout," + filter2 + ",logGeneric3,logDest=stdout," + filter3 + + ",logGeneric4,logDest=stdout," + filter4 + ",logGeneric5,logDest=stdout," + filter5 + + ",logGeneric6,logDest=stdout," + filter6); + runner.start("genericLogging4"); + runner.waitForEnd(); + assertLinesContains(runner.getStdoutLines(), "Values for generic logger 1: false -1 -7 C 128 -7 0.2 -2.2", + "Values for generic logger 2: C 1 7 C -2 7 -0.2 2.2", + "Values for generic logger 3: e 127 15 E -222 77132 -0.22387 2.1332", + "Values for generic logger 4: null {0, 0, 0} {null, null, null, null} " + + "{null, null, null, null, null, 5 skipped ...} " + + "class org.openjdk.jmc.agent.sap.test.C1 C1 C3 \"Hehe!\"", + "Values for generic logger 4: null {0, 0, 0} {null, null, null, null} " + + "{null, null, null, null, null, 5 skipped ...} " + + "class org.openjdk.jmc.agent.sap.test.C1 C1 C3 \"Hele!\"", + "Values for generic logger 5: {\"test\": C1} {\"test1\": C1, \"test2\": C2, " + + "\"test3\": C3, null: C1, \"test5\": C2, 1 skipped ...} " + + "{true, false, true, false, true} null {C1, C2, C3, C1, C2, 1 skipped ...} " + + "org.openjdk.jmc.agent.sap.test.DummyList(size 6) " + + "{\"map1\": {\"test\": C1}, \"map2\": {\"test1\": C1, \"test2\": C2, " + + "\"test3\": C3, null: C1, \"test5\": C2, 1 skipped ...}} " + + "{java.lang.Object[3][], java.lang.String[8], {\"test\": C1}, " + + "{\"map1\": {\"test\": C1}, \"map2\": {\"test1\": C1, \"test2\": C2, " + + "\"test3\": C3, null: C1, \"test5\": C2, 1 skipped ...}}, boolean[5], 1 skipped ...}", + "Values for generic logger 6: org.openjdk.jmc.agent.sap.test.DummyList(size 6) " + + "{\"just\", \"a\", \"test\", null, \"with\", 3 skipped ...} " + + "{true, false, true, false, true} java.util.HashMap(size 1) " + + "java.util.LinkedHashMap(size 6) {java.lang.Object[1], java.io.Serializable[2][], " + + "java.lang.String[3]} class java.lang.Class \"Jeje\""); + assertNrOfLines(runner.getStdoutLines(), 9); // Should contain no other than the lines check above. + } finally { + while (xmlFile.exists()) { + xmlFile.delete(); + } + } + } + + public static int tracePrimitives1(boolean v1, byte v2, short v3, char c4, int v5, long v6, float v7, double v8) { + return 0; + } + + public static int tracePrimitives2(char v1, byte v2, short v3, char c4, int v5, long v6, float v7, double v8) { + return 0; + } + + public static int tracePrimitives3(char v1, byte v2, short v3, char c4, int v5, long v6, float v7, double v8) { + return 0; + } + + public static int traceObjects1( + Object o1, Object o2, Object o3, Object o4, Object o5, Object o6, Object o7, Object o8) { + if (o2 != null) { + return 1; + } + + return 0; + } + + public static int traceObjects2( + Object o1, Object o2, Object o3, Object o4, Object o5, Object o6, Object o7, Object o8) { + return 0; + } + + public static int traceObjects3( + Object o1, Object o2, Object o3, Object o4, Object o5, Object o6, Object o7, Object o8) { + return 0; + } +} + +class C1 implements Comparable { + + @Override + public int compareTo(Object o) { + return 0; + } + + @Override + public String toString() { + return "C1"; + } +} + +interface I1 { + public void if1(); +} + +interface I2 extends I1 { + public void if2(); +} + +interface I3 { + public void if3(); +} + +class C2 extends C1 implements I2, I1 { + + @Override + public void if1() { + } + + @Override + public void if2() { + } + + @Override + public String toString() { + return "C2"; + } +} + +class C3 extends C2 implements I3 { + + @Override + public void if3() { + } + + @Override + public String toString() { + return "C3"; + } +} + +@SuppressWarnings("serial") +class DummyList extends ArrayList { + + private final boolean allowInspection; + + public DummyList(Collection toCopy, boolean allowInspection) { + super(toCopy); + this.allowInspection = allowInspection; + } + + @Override + public Iterator iterator() { + if (allowInspection) { + return super.iterator(); + } + + return new Iterator() { + + @Override + public boolean hasNext() { + return true; + } + + @Override + public E next() { + throw new ConcurrentModificationException(); + } + }; + } + + @SuppressWarnings("unchecked") + @Override + public E[] toArray() { + if (allowInspection) { + return (E[]) super.toArray(); + } + + throw new RuntimeException("Chould not be called."); + } + + @Override + public T[] toArray(T[] a) { + if (allowInspection) { + return super.toArray(a); + } + + throw new RuntimeException("Chould not be called."); + } + + @Override + public String toString() { + if (allowInspection) { + return super.toString(); + } + + return "JustADummy"; + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/JavaAgentRunner.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/JavaAgentRunner.java new file mode 100644 index 0000000000..7fa2551ddc --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/JavaAgentRunner.java @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.ProcessBuilder.Redirect; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +public class JavaAgentRunner { + + private final String classToRun; + private final String options; + private final String[] vmArgs; + private final StringBuilder stdout; + private final StringBuilder stderr; + private String commandLine = "java"; + private Thread stdoutWorker; + private Thread stderrWorker; + private Process process; + private static boolean dumpOnExit; + private static int debugPort = -1; + private static int MAX_WAIT_TIME = 60; + private static String[] additionalVmArgs = new String[0]; + + private static final boolean dumpOutputToFile = Boolean.getBoolean("dumpOutputToFile"); + private static final boolean useJmcAgentOption = Boolean.getBoolean("useJmcAgentOption"); + private static final boolean traceExecs = Boolean.getBoolean("traceExecs"); + + public JavaAgentRunner(Class classToRun, String options, String ... vmArgs) { + this.classToRun = classToRun.getName(); + this.options = options; + this.vmArgs = vmArgs; + this.stdout = new StringBuilder(); + this.stderr = new StringBuilder(); + } + + public static void setAdditionalVmArgs(String[] args) { + additionalVmArgs = args; + } + + private ArrayList getArgs(String[] javaArgs) { + ArrayList args = new ArrayList<>(); + args.add(getExe("java")); + args.add("-cp"); + args.add(System.getProperty("java.class.path")); + + if (useJmcAgentOption) { + args.add("-jmcagent:" + options); + } else { + args.add("-javaagent:" + getAgent() + "=" + options); + } + + for (String vmArg : additionalVmArgs) { + args.add(vmArg); + } + + args.add("-cp"); + args.add(System.getProperty("java.class.path")); + + for (String vmArg : vmArgs) { + args.add(vmArg); + } + + if (debugPort >= 0) { + args.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=" + debugPort); + System.out.println("Waiting for debugger on port " + debugPort + "."); + } + + args.add(classToRun); + + for (String javaArg : javaArgs) { + args.add(javaArg); + } + + commandLine = String.join(" ", args); + + return args; + } + + public String getCommandLine() { + return commandLine; + } + + private static String getAgent() { + Pattern pattern = Pattern.compile("^agent-[0-9]+[.][0-9]+[.][0-9]+(-SNAPSHOT)?[.]jar$"); + String javaHome = System.getProperty("java.home"); + String[] toSearch = new String[] {".", "target", javaHome + File.separator + "lib"}; + FilenameFilter filter = (File dir, String name) -> pattern.matcher(name).matches(); + + for (String dir : toSearch) { + File[] files = new File(dir).listFiles(filter); + + if (files.length == 1) { + return files[0].getAbsolutePath(); + } else if (files.length > 1) { + throw new RuntimeException("Found more than one agent: " + Arrays.toString(files)); + } + } + + throw new RuntimeException("Could not find agent in " + Arrays.toString(toSearch)); + } + + private static String getExe(String name) { + String javaHome = System.getProperty("java.home"); + File java = new File(javaHome + File.separator + "bin" + File.separator + name); + + if (java.exists() && java.canExecute()) { + return java.getAbsolutePath(); + } + + java = new File(java.getAbsolutePath() + ".exe"); + + if (java.exists() && java.canExecute()) { + return java.getAbsolutePath(); + } + + throw new RuntimeException("Could not locate '" + name + "'"); + } + + public void start(String ... javaArgs) throws IOException { + stdout.setLength(0); + stderr.setLength(0); + + ProcessBuilder pb = new ProcessBuilder(getArgs(javaArgs)); + commandLine = String.join(" ", pb.command()); + dumpToAll("------------------------", commandLine); + + if (traceExecs) { + System.out.println("Starting " + commandLine); + } + + process = pb.start(); + stdoutWorker = new Thread(new OutputReader(process.getInputStream(), stdout)); + stdoutWorker.setDaemon(true); + stdoutWorker.start(); + stderrWorker = new Thread(new OutputReader(process.getErrorStream(), stderr)); + stderrWorker.setDaemon(true); + stderrWorker.start(); + } + + public void waitForStdout(String tag) { + waitFor(stdout, tag); + } + + public void waitForStderr(String tag) { + waitFor(stderr, tag); + } + + public void loadAgent(String options) throws IOException { + if (process == null) { + throw new IOException("No process"); + } + + if (options.indexOf('=') >= 0) { + options = "'" + options + "'"; // jcmd can remove everything after the equals. + } + + long pid = process.pid(); + ArrayList args = new ArrayList<>(); + args.add(getExe("jcmd")); + args.add(Long.toString(pid)); + + if (useJmcAgentOption) { + args.add("JVMTI.jmc_agent_load"); + } else { + args.add("JVMTI.agent_load"); + args.add(getAgent()); + } + + args.add(options); + ProcessBuilder pb = new ProcessBuilder(args); + pb.redirectError(Redirect.DISCARD); + pb.redirectOutput(Redirect.DISCARD); + + if (traceExecs) { + System.out.println("Starting " + String.join(" ", args)); + } + + Process p = pb.start(); + + while (true) { + try { + int result = p.waitFor(); + + if (result != 0) { + kill(); + dumpOnExit(result); + throw new RuntimeException(pb.command().toString() + " return with exit code " + result); + } + + return; + } catch (InterruptedException e) { + // retry + } + } + } + + public int waitForEnd() { + long t1 = System.currentTimeMillis(); + + while (true) { + try { + boolean exited = process.waitFor(5, TimeUnit.SECONDS); + + if (!exited && !checkWaitTimeout(t1)) { + dumpOnExit(-1); + throw new RuntimeException("Waited over one minute for the process to end."); + } + + if (!exited) { + continue; + } + + int result = process.exitValue(); + dumpOnExit(result); + + return result; + } catch (InterruptedException e) { + // Retry + } + } + } + + private void waitForOutput() { + try { + stdoutWorker.join(); + stderrWorker.join(); + } catch (InterruptedException e) { + // Ignore + } + } + + public String[] getStdoutLines() { + return OutputReader.getLines(stdout); + } + + public String[] getStderrLines() { + return OutputReader.getLines(stderr); + } + + private void dumpLines(StringBuilder sb) { + String raw; + + synchronized (sb) { + raw = sb.toString(); + } + + System.out.println(raw); + } + + public static void setDumpOnExit(boolean dumpOnExit) { + JavaAgentRunner.dumpOnExit = dumpOnExit; + } + + public static void setDebugPort(int port) { + debugPort = port; + } + + private void dumpOnExit(int result) { + if (dumpOnExit && (result != 0)) { + waitForOutput(); + System.out.println("Command line: " + commandLine); + System.out.println("Output on stdout:"); + dumpLines(stdout); + System.out.println("Output on stderr:"); + dumpLines(stderr); + } + + if (dumpOutputToFile) { + waitForOutput(); + dumpToFile(getStdoutLines(), false); + dumpToFile(getStderrLines(), true); + dumpToAll("------------------------", getCommandLine() + " end with result " + result); + } + } + + private void dumpToAll(String ... lines) { + if (dumpOutputToFile) { + dumpToFile(lines, false); + dumpToFile(lines, true); + } + } + + private void dumpToFile(String[] lines, boolean stderr) { + File outputDir = new File("target", "output"); + + if (!outputDir.exists()) { + outputDir.mkdir(); + } + + String filename = classToRun.substring(classToRun.lastIndexOf('.') + 1) + (stderr ? ".stderr" : ".stdout"); + + try (PrintStream ps = new PrintStream(new FileOutputStream(new File(outputDir, filename), true))) { + for (String line : lines) { + ps.println(line); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + public void kill() { + int result = 0; + + try { + if (process.waitFor(10, TimeUnit.MILLISECONDS)) { + result = process.exitValue(); + } else { + process.destroyForcibly(); + result = -1; + } + + } catch (InterruptedException e) { + // Ignore. + } + + dumpOnExit(result); + } + + private boolean checkWaitTimeout(long t1) { + if (debugPort >= 0) { + return true; // Don't terminate if we are debugging. + } + + long elapsed = (System.currentTimeMillis() - t1) / 1000; + + // We should never wait this long. + if (elapsed > MAX_WAIT_TIME) { + kill(); + + return false; + } + + return true; + } + + private void waitFor(StringBuilder output, String tag) { + long t1 = System.currentTimeMillis(); + + while (true) { + synchronized (output) { + if (output.indexOf(tag) >= 0) { + return; + } + + if (!checkWaitTimeout(t1)) { + System.out.println("stdout:"); + synchronized (stdout) { + System.out.println(stdout); + } + System.out.println("stderr:"); + synchronized (stderr) { + System.out.println(stderr); + } + throw new RuntimeException("Waited over one minute for '" + tag + "'"); + } + + try { + output.wait(10000); + } catch (InterruptedException e) { + // Ignore. + } + } + } + } + + public void waitForDone() { + waitForStdout(TestBase.DONE + "*"); + } + + public void waitForDone(int index) { + waitForStdout(TestBase.DONE + index); + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/LocaleChangeTest.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/LocaleChangeTest.java new file mode 100644 index 0000000000..de89344d98 --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/LocaleChangeTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.util.Locale; +import java.util.Locale.Category; + +public class LocaleChangeTest extends TestBase { + + public static void main(String[] args) { + new LocaleChangeTest().dispatch(args); + } + + @Override + protected void runAllTests() throws Exception { + JavaAgentRunner runner = getRunner("traceLocaleChange,logDest=stdout"); + runner.start("changeLocale"); + runner.waitForEnd(); + assertLinesContainsRegExp(runner.getStdoutLines(), + "Changed default locale for category 'DISPLAY' from .+ to 'English [(]Canada[)]'", + "Changed default locale for category 'FORMAT' from .* to 'English [(]Canada[)]'"); + assertLinesContains(runner.getStdoutLines(), + "Changed default locale for category 'DISPLAY' from 'English (Canada)' to 'Chinese (Taiwan)'.", + "Changed default locale for category 'FORMAT' from 'English (Canada)' to 'Chinese (Taiwan)'.", + "Changed default locale for category 'DISPLAY' from 'Chinese (Taiwan)' to 'English (Canada)'", + "Changed default locale for category 'FORMAT' from 'Chinese (Taiwan)' to 'English (Canada)'.", + "Changed default locale for category 'DISPLAY' from 'English (Canada)' to 'Chinese (China)'.", + "Changed default locale for category 'DISPLAY' from 'Chinese (China)' to 'French (France)'.", + "Changed default locale for category 'FORMAT' from 'English (Canada)' to 'Italian'.", + "Changed default locale for category 'FORMAT' from 'Italian' to 'French (Canada)'."); + assertLinesNotContains(runner.getStdoutLines(), + "Changed default locale for category 'DISPLAY' from 'Chinese (Taiwan)' to 'Chinese (Taiwan)'.", + "Changed default locale for category 'FORMAT' from 'Chinese (Taiwan)' to 'Chinese (Taiwan)'.", + "Changed default locale for category 'DISPLAY' from 'Chinese (China)' to 'Chinese (China)'.", + "Changed default locale for category 'FORMAT' from 'Italian' to 'Italian'."); + } + + public static void changeLocale() { + Locale.setDefault(Locale.CANADA); + Locale.setDefault(Locale.TAIWAN); + Locale.setDefault(Locale.TAIWAN); + Locale.setDefault(Locale.CANADA); + Locale.setDefault(Category.DISPLAY, Locale.CHINA); + Locale.setDefault(Category.DISPLAY, Locale.CHINA); + Locale.setDefault(Category.DISPLAY, Locale.FRANCE); + Locale.setDefault(Category.FORMAT, Locale.ITALIAN); + Locale.setDefault(Category.FORMAT, Locale.ITALIAN); + Locale.setDefault(Category.FORMAT, Locale.CANADA_FRENCH); + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/OpenFileStatisticTest.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/OpenFileStatisticTest.java new file mode 100644 index 0000000000..52b35e5de8 --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/OpenFileStatisticTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +public class OpenFileStatisticTest extends TestBase { + + public static void main(String[] args) { + new OpenFileStatisticTest().dispatch(args); + } + + private static void deleteTestFiles() { + for (int i = 1; i <= 6; ++i) { + File file = getFile(i); + + while (file.exists()) { + file.delete(); + } + } + } + + @Override + protected void runAllTests() throws Exception { + JavaAgentRunner runner = getRunner("traceOpenFiles,logDest=stdout"); + runner.start("test"); + runner.waitForDone(1); + runner.loadAgent("dump=openFiles,logDest=stderr"); + + if (!smokeTestsOnly()) { + runner.waitForDone(); + runner.loadAgent("dump=openFiles,logDest=stdout"); + } + + runner.kill(); + + String[] stderr = runner.getStderrLines(); + assertLinesContains(stderr, getFileName(1) + "', mode 'w'"); + assertLinesContains(stderr, getFileName(2) + "', mode 'wa'"); + assertLinesContains(stderr, getFileName(1) + "', mode 'r'"); + assertLinesContains(stderr, getFileName(3) + "', mode 'w'"); + assertLinesContains(stderr, getFileName(4) + "', mode 'wa'"); + assertLinesContains(stderr, getFileName(2) + "', mode 'r'"); + assertLinesContains(stderr, getFileName(5) + "', mode 'rw'"); + assertLinesContains(stderr, getFileName(5) + "', mode 'r'"); + assertLinesContains(stderr, getFileName(6) + "', mode 'rw'"); + assertLinesContains(stderr, getFileName(6) + "', mode 'r'"); + assertLinesContainsRegExp(stderr, "Printed [0-9]+ of [0-9][0-9]+ file.* currently opened"); + + if (!smokeTestsOnly()) { + assertLinesNotContains(runner.getStdoutLines(), getFileName(1)); + } + + deleteTestFiles(); + } + + public static String getFileName(int index) { + return "testopen" + index + ".txt"; + } + + public static File getFile(int index) { + return new File(getFileName(index)); + } + + @SuppressWarnings("resource") + public void test() throws IOException { + FileOutputStream fos1 = new FileOutputStream(getFileName(1)); + FileOutputStream fos2 = new FileOutputStream(getFileName(2), true); + FileInputStream fis1 = new FileInputStream(getFileName(1)); + FileOutputStream fos3 = new FileOutputStream(getFile(3)); + FileOutputStream fos4 = new FileOutputStream(getFile(4), true); + FileInputStream fis2 = new FileInputStream(getFile(2)); + RandomAccessFile raf1 = new RandomAccessFile(getFileName(5), "rw"); + RandomAccessFile raf2 = new RandomAccessFile(getFileName(5), "r"); + RandomAccessFile raf3 = new RandomAccessFile(getFile(6), "rw"); + RandomAccessFile raf4 = new RandomAccessFile(getFile(6), "r"); + FileChannel fc = FileChannel.open(getFile(1).toPath()); + fc.read(ByteBuffer.allocate(10)); + + done(1, 3000); + + fos1.close(); + fos2.close(); + fos3.close(); + fos4.close(); + fis1.close(); + fis2.close(); + raf1.close(); + raf2.close(); + raf3.close(); + raf4.close(); + fc.close(); + + FileInputStream dummy = null; + deleteTestFiles(); + + try { + dummy = new FileInputStream(getFileName(1)); + throw new RuntimeException("Should not be able to open the file"); + } catch (FileNotFoundException e) { + // This is what we expect. + } + + done(); + assertNotNull(dummy); + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/OutputReader.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/OutputReader.java new file mode 100644 index 0000000000..0b9c96b462 --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/OutputReader.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class OutputReader implements Runnable { + private final StringBuilder out; + private final InputStream is; + + public OutputReader(InputStream is, StringBuilder out) { + this.is = is; + this.out = out; + } + + public static String[] getLines(CharSequence cs) { + String raw; + + synchronized (cs) { + raw = cs.toString(); + } + + return raw.split("[\r\n]+"); + } + + public String[] getLines() { + return getLines(out); + } + + public void run() { + try { + byte[] buf = new byte[8192]; + int read; + + while ((read = is.read(buf)) > 0) { + + if (read > 0) { + byte[] part = new byte[read]; + System.arraycopy(buf, 0, part, 0, read); + String toAppend = new String(part, StandardCharsets.ISO_8859_1); + + synchronized (out) { + out.append(toAppend); + out.notifyAll(); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/SapIntegrationTest.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/SapIntegrationTest.java new file mode 100644 index 0000000000..b7ae4b7b3e --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/SapIntegrationTest.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.util.ArrayList; + +import org.junit.Test; + +public class SapIntegrationTest { + + @Test + public void smokeTest() throws Exception { + ArrayList args = new ArrayList<>(); + + if (!System.getProperty("fullTest", "false").equals("true")) { + args.add("-smoke"); + } + + String port = System.getProperty("debugPort", ""); + + if (port.length() > 0) { + args.add("-debug"); + args.add(port); + } + + String singleTest = System.getProperty("singleTest", ""); + + if (singleTest.length() > 0) { + args.add(singleTest); + } + + TestRunner.main(args.toArray(new String[args.size()])); + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/SysPropsChangeTest.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/SysPropsChangeTest.java new file mode 100644 index 0000000000..21595b59b9 --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/SysPropsChangeTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.io.File; +import java.util.Properties; + +// You can run it via (if the cwd is the agent directory): +// java -cp target/test-classes org.openjdk.jmc.agent.sap.test.SysPropsChangeTest +public class SysPropsChangeTest extends TestBase { + + public static void main(String[] args) { + new SysPropsChangeTest().dispatch(args); + } + + @Override + protected void runAllTests() throws Exception { + JavaAgentRunner runner = getRunnerWithJFR("traceSysPropsChange,logDest=stdout"); + runner.start("changeSystemProps"); + runner.waitForEnd(); + assertLinesContains(runner.getStdoutLines(), "System property 'TEST_KEY' changed from 'null' to 'TEST_VAL'", + "System properties 'TEST_KEY' with value 'TEST_VAL' removed", SysPropsChangeTest.class.getName()); + assertLinesNotContains(runner.getStdoutLines(), "TEST_KEY_NO_SYS"); + assertLinesContainsInOrder(getJfrOutput("jdk.log.*"), "fieldValue = \"TEST_VAL\"", "OldValue = N/A", + "IsSystemProperty = true", "RemovedValue = \"TEST_VAL\"", "IsSystemProperty = true", "OldValue = N/A", + "IsSystemProperty = false", "OldValue = \"TEST_ADD_VALUE\"", "RemovedValue = \"TEST_CHANGE_VALUE\"", + "RemovedValue = N/A"); + + // Check if we can omit the stack. + runner = getRunner("traceSysPropsChange,logDest=stderr,logWithStack=false"); + runner.start("changeSystemProps"); + runner.waitForEnd(); + assertLinesContains(runner.getStderrLines(), "System property 'TEST_KEY' changed from 'null' to 'TEST_VAL'", + "System properties 'TEST_KEY' with value 'TEST_VAL' removed"); + assertLinesNotContains(runner.getStderrLines(), SysPropsChangeTest.class.getName()); + + // Check if we get help + runner = getRunner("traceSysPropsChange,help"); + runner.start("changeSystemProps"); + runner.waitForEnd(); + assertLinesContains(runner.getStderrLines(), "Help for command 'traceSysPropsChange'", + "Traces changes to the system properties", "logWithStack"); + } + + public static void changeSystemProps() { + new File("testfile"); + System.setProperty("TEST_KEY", "TEST_VAL"); + System.getProperties().remove("TEST_KEY"); + Properties props = new Properties(); + props.put("TEST_KEY_NO_SYS", "TEST_ADD_VALUE"); + props.setProperty("TEST_KEY_NO_SYS", "TEST_CHANGE_VALUE"); + props.remove("TEST_KEY_NO_SYS"); + props.remove("TEST_KEY_NO_SYS"); + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/TestBase.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/TestBase.java new file mode 100644 index 0000000000..71e20f67d2 --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/TestBase.java @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.regex.Pattern; + +public abstract class TestBase { + + private int jfrFileIndex = 1; + private File jfrFile = null; + + private static boolean smokeTestsOnly; + public static String DONE = "DONE"; + public static long MAX_TEST_CASE_DURATION = 5 * 60; + + public void dispatch(String[] args) { + try { + if (args.length == 0) { + runAllTests(); + } else { + Thread killer = new Thread(() -> { + while (true) { + try { + Thread.sleep(MAX_TEST_CASE_DURATION * 1000); + System.err.println("Test run in timeout."); + System.exit(1); + } catch (InterruptedException e) { + // Ignore + } + } + }, "Timeout Thread"); + killer.setDaemon(true); + killer.start(); + + try { + Method m = this.getClass().getDeclaredMethod(args[0]); + m.invoke(this); + } catch (NoSuchMethodException e) { + throw new RuntimeException("Undefined test '" + args[0] + "'"); + } + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Test failed", e); + } + } + + public JavaAgentRunner getRunner(String options, String ... vmArgs) { + return new JavaAgentRunner(getClass(), options, vmArgs); + } + + private File getNewJfrFile() { + File outputDir = new File("target", "output"); + + if (!outputDir.exists()) { + outputDir.mkdirs(); + } + + jfrFile = new File(outputDir, getClass().getName().replace('.', '_') + jfrFileIndex + ".jfr").getAbsoluteFile(); + jfrFileIndex += 1; + + return jfrFile; + } + + public JavaAgentRunner getRunnerWithJFR(String options, String ... vmArgs) { + File jfrFile = getNewJfrFile(); + jfrFile.delete(); + + String[] newVmArgs = new String[vmArgs.length + 1]; + System.arraycopy(vmArgs, 0, newVmArgs, 0, vmArgs.length); + newVmArgs[vmArgs.length] = "-XX:StartFlightRecording=filename=" + jfrFile.getPath(); + + return new JavaAgentRunner(getClass(), options, newVmArgs); + } + + public String[] getJfrOutput(String idFilter) throws IOException, InterruptedException { + return getJfrOutput(idFilter, 8); + } + + public String[] getJfrOutput(String idFilter, int stackDepth) throws IOException, InterruptedException { + if (!jfrFile.exists()) { + throw new FileNotFoundException(jfrFile.getPath()); + } + + ProcessBuilder pb = new ProcessBuilder("jfr", "print", "--stack-depth", "" + stackDepth, "--events", idFilter, + jfrFile.getPath()); + Process process = pb.start(); + StringBuilder output = new StringBuilder(); + OutputReader reader = new OutputReader(process.getInputStream(), output); + Thread worker = new Thread(reader); + worker.setDaemon(true); + worker.start(); + process.waitFor(); + worker.join(); + + return reader.getLines(); + } + + protected abstract void runAllTests() throws Exception; + + private static void failLines(String[] lines, String msg) { + failLines(lines, msg, -1); + } + + private static void failLines(String[] lines, String msg, int markedLine) { + System.err.println(msg + ":"); + System.err.println("---- START"); + + for (int i = 0; i < lines.length; ++i) { + if (i == markedLine) { + System.err.println("=> " + lines[i]); + } else { + System.err.println(" " + lines[i]); + } + } + + System.err.println("---- END"); + throw new AssertionError(msg); + } + + public static void assertNrOfLines(String[] lines, int expectedNrOfLines) { + if (lines.length != expectedNrOfLines) { + failLines(lines, "Expected " + expectedNrOfLines + " lines but got " + lines.length, -1); + } + } + + public static void assertLinesContains(String[] lines, String ... substrings) { + outer: for (String substring : substrings) { + for (String line : lines) { + if (line.indexOf(substring) >= 0) { + continue outer; + } + } + + failLines(lines, "Could not find '" + substring + "' in the lines"); + } + } + + public static void assertLinesContainsInOrder(String[] lines, String ... substrings) { + int index = 0; + for (String line : lines) { + String substring = substrings[index]; + + if (line.indexOf(substring) >= 0) { + ++index; + + if (index == substrings.length) { + return; + } + } + } + + failLines(lines, "Could not find '" + substrings[index] + "' in the lines"); + } + + public static void assertLinesContainsRegExp(String[] lines, String ... regexps) { + outer: for (String regexp : regexps) { + Pattern pattern = Pattern.compile(regexp); + + for (String line : lines) { + if (pattern.matcher(line).find()) { + continue outer; + } + } + + failLines(lines, "Could not find regexp '" + regexp + "' in the lines"); + } + } + + public static void assertLinesNotContains(String[] lines, String ... substrings) { + for (String substring : substrings) { + for (String line : lines) { + if (line.indexOf(substring) >= 0) { + failLines(lines, "Unexpectedly found '" + substring + "' in the lines"); + } + } + } + } + + public static void assertLinesNotContainsRegExp(String[] lines, String ... regexps) { + for (String regexp : regexps) { + Pattern pattern = Pattern.compile(regexp); + + for (int i = 0; i < lines.length; ++i) { + if (pattern.matcher(lines[i]).find()) { + failLines(lines, "Unexpectedly found regexp '" + regexp + "' in the lines", i); + return; + } + } + } + } + + public static void assertNotNull(Object obj) { + if (obj == null) { + throw new AssertionError("Object is null"); + } + } + + public static void setSmokeTestOnly() { + smokeTestsOnly = true; + } + + public static boolean smokeTestsOnly() { + return smokeTestsOnly; + } + + public void assertRunnerFinished(JavaAgentRunner runner) { + int result = runner.waitForEnd(); + + if (result != 0) { + throw new AssertionError("Exit code " + result + " for " + runner.getCommandLine()); + } + } + + public static void sleep(long seconds) { + try { + Thread.sleep(seconds * 1000); + } catch (InterruptedException e) { + + } + } + + protected static void done(int index, long waitTime) { + System.out.println(DONE + index); + + try { + Thread.sleep(waitTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + protected static void done() { + System.out.println(DONE + "*"); + + while (true) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/TestRunner.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/TestRunner.java new file mode 100644 index 0000000000..481b61ad0f --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/TestRunner.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; + +public class TestRunner { + private static Class[] testClasses = new Class[] {UnsafeAllocationTest.class, SysPropsChangeTest.class, + TimeZoneChangeTest.class, LocaleChangeTest.class, OpenFileStatisticTest.class, GenericLoggingTest.class}; + + public static void main(String[] args, String[] additionalVmArgs) throws Exception { + JavaAgentRunner.setAdditionalVmArgs(additionalVmArgs); + main(args); + } + + public static void main(String[] args) throws Exception { + ArrayList leftArgs = new ArrayList<>(Arrays.asList(args)); + boolean vmAgnosticTests = false; + + while (leftArgs.size() > 0) { + String first = leftArgs.get(0); + + if (first.equals("-dump")) { + JavaAgentRunner.setDumpOnExit(true); + leftArgs.remove(0); + } else if (first.equals("-debug")) { + if (leftArgs.size() < 2) { + throw new Exception("Missing port to '-debug'"); + } + + JavaAgentRunner.setDebugPort(Integer.parseInt(leftArgs.get(1))); + leftArgs.remove(0); + leftArgs.remove(0); + } else if (first.equals("-smoke")) { + TestBase.setSmokeTestOnly(); + leftArgs.remove(0); + } else if (first.endsWith("-vm-agnostic-tests")) { + vmAgnosticTests = true; + leftArgs.remove(0); + } else { + break; + } + } + + HashSet toRun = new HashSet<>(leftArgs); + + for (Class testClass : testClasses) { + boolean run = false; + + if (leftArgs.size() == 0) { + run = true; + } else { + for (String arg : leftArgs) { + if (testClass.getName().endsWith("." + arg)) { + run = true; + toRun.remove(arg); + } + } + } + + if (vmAgnosticTests) { + if (!VmAgnostic.class.isAssignableFrom(testClass)) { + continue; + } + } + + if (run) { + Method mainMethod = testClass.getDeclaredMethod("main", String[].class); + mainMethod.invoke(null, new Object[] {new String[0]}); + } + } + + if (toRun.size() > 0) { + throw new RuntimeException("Could not find test " + toRun.iterator().next()); + } + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/TimeZoneChangeTest.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/TimeZoneChangeTest.java new file mode 100644 index 0000000000..89b349b759 --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/TimeZoneChangeTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.util.TimeZone; + +public class TimeZoneChangeTest extends TestBase { + private static final TimeZone zone0 = TimeZone.getDefault(); + private static final TimeZone zone1a = TimeZone.getTimeZone("Europe/Berlin"); + private static final TimeZone zone1b = TimeZone.getTimeZone("America/Los_Angeles"); + private static final boolean zone0is1a = zone0.toZoneId().equals(zone1a.toZoneId()); + private static final TimeZone zone1 = zone0is1a ? zone1b : zone1a; + + public static void main(String[] args) { + new TimeZoneChangeTest().dispatch(args); + } + + @Override + protected void runAllTests() throws Exception { + JavaAgentRunner runner = getRunner("traceTimeZoneChange,logDest=stdout"); + runner.start("changeTimeZones"); + runner.waitForEnd(); + assertLinesContainsRegExp(runner.getStdoutLines(), + "Changed default time zone to Central European.* Time [(]CET[)]", + "Changed default time zone to Greenwich Mean Time [(]Etc/GMT+0[)]", + "Changed default time zone to Central European Standard Time [(]Europe/Berlin[)]."); + runner = getRunnerWithJFR("traceTimeZoneChange,logDest=stdout"); + runner.start("changeForJFR"); + runner.waitForEnd(); + String[] lines = getJfrOutput("jdk.log.*"); + assertLinesContainsInOrder(lines, "fieldNewTimeZone = \"" + zone0.getDisplayName(), + "fieldNewTimeZoneId = \"" + zone0.toZoneId().getId(), "fieldOldTimeZone = \"" + zone0.getDisplayName(), + "fieldOldTimeZoneId = \"" + zone0.toZoneId().getId(), "fieldChangesDefault = false", + "fieldNewTimeZone = \"" + zone1.getDisplayName(), "fieldNewTimeZoneId = \"" + zone1.toZoneId().getId(), + "fieldOldTimeZone = \"" + zone0.getDisplayName(), "fieldOldTimeZoneId = \"" + zone0.toZoneId().getId(), + "fieldChangesDefault = true", "fieldNewTimeZone = \"" + zone0.getDisplayName(), + "fieldNewTimeZoneId = \"" + zone0.toZoneId().getId(), "fieldOldTimeZone = \"" + zone1.getDisplayName(), + "fieldOldTimeZoneId = \"" + zone1.toZoneId().getId(), "fieldChangesDefault = true"); + + } + + public void changeTimeZones() { + TimeZone.setDefault(TimeZone.getDefault()); + + for (int i = 0; i < 2; ++i) { + for (String id : TimeZone.getAvailableIDs()) { + TimeZone.setDefault(TimeZone.getTimeZone(id)); + } + } + } + + public void changeForJFR() { + TimeZone.setDefault(zone0); + TimeZone.setDefault(zone1); + TimeZone.setDefault(zone0); + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/UnsafeAllocationTest.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/UnsafeAllocationTest.java new file mode 100644 index 0000000000..c5fe739cd5 --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/UnsafeAllocationTest.java @@ -0,0 +1,337 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +// You can run it via (if the cwd is the agent directory): +// java -cp target/test-classes org.openjdk.jmc.agent.sap.test.UnsafeAllocationTest +public class UnsafeAllocationTest extends TestBase { + + private static Method allocateMemoryMethod; + private static Method reallocateMemoryMethod; + private static Method freeMemoryMethod; + private static Object theUnsafe; + private static String DO_ALLOCS = "runRandomAllocs"; + private static String DO_NATIVE_ALLOCS = "doNativeAllocs"; + private static String DO_DELAYED_ALLOCS = "doDelayedAllocs"; + private static String DO_NATIVE_ALLOCS_FOR_JFR = "doNativeAllocsForJfr"; + private static long DELAY = 10; + + private static void initUnsafe() { + try { + Class unsafeClass = Class.forName("jdk.internal.misc.Unsafe"); + Field theUnsafeField = unsafeClass.getDeclaredField("theUnsafe"); + theUnsafeField.setAccessible(true); + theUnsafe = theUnsafeField.get(null); + allocateMemoryMethod = unsafeClass.getDeclaredMethod("allocateMemory", long.class); + reallocateMemoryMethod = unsafeClass.getDeclaredMethod("reallocateMemory", long.class, long.class); + freeMemoryMethod = unsafeClass.getDeclaredMethod("freeMemory", long.class); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void main(String[] args) { + new UnsafeAllocationTest().dispatch(args); + } + + protected void runAllTests() throws Exception { + testNativeAllocs(); + testNativeAllocsForJfr(); + + if (smokeTestsOnly()) { + return; + } + + testDelayedDumping(); + testAgeFiltering(); + } + + public void testNativeAllocsForJfr() throws Exception { + JavaAgentRunner runner = getRunnerWithJFR("traceUnsafeAllocations", "--add-opens", + "java.base/jdk.internal.misc=ALL-UNNAMED", "-XX:NativeMemoryTracking=off"); + runner.start(DO_NATIVE_ALLOCS_FOR_JFR); + runner.waitForEnd(); + String[] lines = getJfrOutput("jdk.log.*", 16); + + // Find the allocated addresses. + long m1 = -1; + long m2 = -1; + + for (int i = 0; i < lines.length; ++i) { + String line = lines[i]; + + if (m1 == -1) { + if (line.endsWith("fieldSize = 12345")) { + m1 = Long.parseLong(lines[i + 1].split(" = ")[1]); + } + } else if (line.endsWith("fieldSize = 54321")) { + m2 = Long.parseLong(lines[i + 1].split(" = ")[1]); + break; + } + } + + assertLinesContainsInOrder(lines, "jdk.log.unsafeMemoryAlloc", "fieldSize = 72057594037927936", + "fieldAddress = 0", "jdk.log.unsafeMemoryAlloc", "fieldSize = 12345", "fieldAddress = " + m1, + "jdk.log.unsafeMemoryRealloc", "fieldOldAddress = " + m1, "fieldSize = 144115188075855872", + "fieldAddress = 0", "jdk.log.unsafeMemoryRealloc", "fieldOldAddress = " + m1, "fieldSize = 54321", + "fieldAddress = " + m2, "jdk.log.unsafeMemoryFree", "fieldAddress = " + m2); + } + + public void testNativeAllocs() throws IOException { + JavaAgentRunner runner = getRunner("traceUnsafeAllocations,logDest=stdout", "--add-opens", + "java.base/jdk.internal.misc=ALL-UNNAMED", "-XX:NativeMemoryTracking=off"); + runner.start(DO_NATIVE_ALLOCS); + runner.waitForDone(); + runner.loadAgent("dump=unsafeAllocations,logDest=stderr,mustContain=doNativeAllocs"); + runner.kill(); + assertLinesContainsRegExp(runner.getStderrLines(), "Allocated 570 bytes at"); + assertLinesContainsRegExp(runner.getStderrLines(), "Allocated 750 bytes at"); + assertLinesContainsRegExp(runner.getStderrLines(), "Printed 2 of 2 allocations with 1320 bytes"); + runner.start(DO_NATIVE_ALLOCS); + runner.waitForDone(); + runner.loadAgent("dump=unsafeAllocations,logDest=stderr,mustNotContain=reallocateMemory"); + runner.kill(); + assertLinesNotContainsRegExp(runner.getStderrLines(), "Allocated 570 bytes at"); + assertLinesContainsRegExp(runner.getStderrLines(), "Allocated 750 bytes at", + "Printed 1 of 2 allocations with 750 bytes"); + runner.start(DO_NATIVE_ALLOCS); + runner.waitForDone(); + runner.loadAgent("dump=unsafeAllocations,logDest=stderr,minStackSize=571"); + runner.kill(); + assertLinesNotContainsRegExp(runner.getStderrLines(), "Allocated 570 bytes at"); + assertLinesContainsRegExp(runner.getStderrLines(), "Allocated 750 bytes at", + "Printed 1 of 2 allocations with 750 bytes"); + runner.start(DO_NATIVE_ALLOCS); + runner.waitForDone(); + runner.loadAgent("dump=unsafeAllocations,logDest=stderr,minAge=1m"); + runner.kill(); + assertLinesNotContainsRegExp(runner.getStderrLines(), "Allocated 570 bytes at", "Allocated 750 bytes at", + "Printed"); + } + + public void testRandomAllocs() throws IOException { + JavaAgentRunner runner = getRunner("traceUnsafeAllocations,dumpCount=1,dumpInterval=3s,logDest=stdout", + "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED"); + runner.start(DO_ALLOCS); + runner.waitForDone(); + runner.loadAgent("dump=unsafeAllocations,logDest=stderr,maxFrames=8"); + runner.kill(); + assertLinesContainsRegExp(runner.getStdoutLines(), "^Printed [0-9]+ of [0-9] allocations with [0-9]+ bytes", + "^Printed [0-9]+ of [0-9] allocations with [0-9]+ bytes"); + } + + public static void doNativeAllocsForJfr() { + initUnsafe(); + + try { + allocateMemory(1L << 56); + } catch (OutOfMemoryError e) { + // Expected + } + + long m1 = allocateMemory(12345); + + try { + reallocateMemory(m1, 1L << 57); + } catch (OutOfMemoryError e) { + // Expected + } + + m1 = reallocateMemory(m1, 54321); + freeMemory(m1); + } + + public static void doNativeAllocs() { + initUnsafe(); + long a1 = 0; + + try { + a1 = allocateMemory(1L << 56); // Should fail. + } catch (OutOfMemoryError e) { + // Expected. + } + + try { + a1 = reallocateMemory(a1, 1L << 56); // Should fail. + } catch (OutOfMemoryError e) { + // Expected. + } + + a1 = allocateMemory(4027); + a1 = reallocateMemory(a1, 0); // Should act like a free + a1 = reallocateMemory(0, 570); // Should act like a malloc. + long a2 = allocateMemory(128); + freeMemory(a2); + a2 = allocateMemory(750); + done(); + } + + public void testDelayedDumping() throws IOException { + JavaAgentRunner runner = getRunner( + "traceUnsafeAllocations,dumpCount=2,minSize=7M,dumpInterval=1s," + + "minPercentage=300,logDest=stdout,exitAfterLastDump=true", + "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED"); + runner.start(DO_DELAYED_ALLOCS); + runner.waitForStdout("Printed 2 of 2 allocations"); // Should be the last dump we see. + runner.waitForEnd(); + assertLinesNotContainsRegExp(runner.getStdoutLines(), "Printed 1 of 1 allocations"); + assertLinesContainsRegExp(runner.getStdoutLines(), "Printed 2 of 2 allocations with 8388608 bytes"); + assertLinesNotContainsRegExp(runner.getStdoutLines(), "Printed 3 of 3 allocations"); + assertLinesContainsRegExp(runner.getStdoutLines(), "Printed 4 of 4 allocations with 38797312 bytes"); + assertLinesNotContainsRegExp(runner.getStdoutLines(), DONE); + runner = getRunner( + "traceUnsafeAllocations,dumpCount=4,minSize=1M,dumpInterval=1s," + + "minPercentage=101,logDest=stdout,exitAfterLastDump=false", + "--add-opens", "java.base/jdk.internal.misc=ALL-UNNAMED"); + runner.start(DO_DELAYED_ALLOCS); + runner.waitForDone(); + runner.kill(); + assertLinesContainsRegExp(runner.getStdoutLines(), "Printed 1 of 1 allocations with 1048576 bytes", + "Printed 2 of 2 allocations with 8388608 bytes", "Printed 3 of 3 allocations with 17825792 bytes", + "Printed 4 of 4 allocations with 38797312 bytes"); + } + + public void testAgeFiltering() throws IOException { + JavaAgentRunner runner = getRunner("traceUnsafeAllocations,logDest=stdout", "--add-opens", + "java.base/jdk.internal.misc=ALL-UNNAMED"); + runner.start(DO_DELAYED_ALLOCS); + runner.waitForDone(); + runner.loadAgent("dump=unsafeAllocations,maxAge=27s,minAge=13s"); + runner.kill(); + assertLinesContainsRegExp(runner.getStderrLines(), "Printed 1 of 4 allocations with 9437184 bytes"); + runner.start(DO_DELAYED_ALLOCS); + runner.waitForDone(); + runner.loadAgent("dump=unsafeAllocations,minAge=13s"); + runner.kill(); + assertLinesContainsRegExp(runner.getStderrLines(), "Printed 3 of 4 allocations with 17825792"); + runner.start(DO_DELAYED_ALLOCS); + runner.waitForDone(); + runner.loadAgent("dump=unsafeAllocations,maxAge=27s"); + runner.kill(); + assertLinesContainsRegExp(runner.getStderrLines(), "Printed 2 of 4 allocations with 30408704 bytes"); + } + + public static void doDelayedAllocs() throws InterruptedException { + initUnsafe(); + allocateMemory(1 * 1024 * 1024); + sleep(DELAY); + allocateMemory(7 * 1024 * 1024); + sleep(DELAY); + allocateMemory(9 * 1024 * 1024); + sleep(DELAY); + allocateMemory(20 * 1024 * 1024); + sleep(DELAY); + done(); + } + + public static void runRandomAllocs(String[] args) { + initUnsafe(); + int increasePerAlloc = args.length > 1 ? Integer.parseInt(args[1]) : 256; + int maxDepth = args.length > 2 ? Integer.parseInt(args[2]) : 10; + long addr = 0; + long allocSize = increasePerAlloc; + + while (true) { + addr = doAlloc1(addr, allocSize, Math.max(2, (int) (Math.random() * maxDepth)), + (long) (Math.random() * Integer.MAX_VALUE)); + allocSize += increasePerAlloc; + } + } + + public static long doAllocImpl(long addr, long allocSize) { + long dummy = allocateMemory(1024); + addr = reallocateMemory(addr, Math.max(0, allocSize - 1024)); + addr = reallocateMemory(addr, allocSize); + freeMemory(dummy); + + return addr; + } + + public static long doAlloc1(long addr, long allocSize, int depth, long seed) { + if (depth == 0) { + return doAllocImpl(addr, allocSize); + } else if ((seed & 1) == 0) { + return doAlloc1(addr, allocSize, depth - 1, seed / 2); + } else { + return doAlloc2(addr, allocSize, depth - 1, seed / 2); + } + } + + public static long doAlloc2(long addr, long allocSize, int depth, long seed) { + if (depth == 0) { + return doAllocImpl(addr, allocSize); + } else if ((seed & 1) == 0) { + return doAlloc1(addr, allocSize, depth - 1, seed / 2); + } else { + return doAlloc2(addr, allocSize, depth - 1, seed / 2); + } + } + + private static long allocateMemory(long size) { + try { + return (Long) allocateMemoryMethod.invoke(theUnsafe, Long.valueOf(size)); + } catch (Exception e) { + if (e.getCause() instanceof OutOfMemoryError) { + throw (OutOfMemoryError) e.getCause(); + } + + e.printStackTrace(); + return 0; + } + } + + private static long reallocateMemory(long addr, long size) { + try { + return (Long) reallocateMemoryMethod.invoke(theUnsafe, Long.valueOf(addr), Long.valueOf(size)); + } catch (Exception e) { + if (e.getCause() instanceof OutOfMemoryError) { + throw (OutOfMemoryError) e.getCause(); + } + + e.printStackTrace(); + return 0; + } + } + + private static void freeMemory(long addr) { + try { + freeMemoryMethod.invoke(theUnsafe, Long.valueOf(addr)); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/agent/src/test/java/org/openjdk/jmc/agent/sap/test/VmAgnostic.java b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/VmAgnostic.java new file mode 100644 index 0000000000..cc30165fb7 --- /dev/null +++ b/agent/src/test/java/org/openjdk/jmc/agent/sap/test/VmAgnostic.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 SAP SE. All rights reserved. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * The contents of this file are subject to the terms of either the Universal Permissive License + * v 1.0 as shown at https://oss.oracle.com/licenses/upl + * + * or the following license: + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be used to + * endorse or promote products derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY + * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.openjdk.jmc.agent.sap.test; + +// Marker interface for tests which run on any VM (mostly because they don*t +// try to instrument classes of the VM). +public interface VmAgnostic { + +} diff --git a/agent/src/test/resources/org/openjdk/jmc/agent/test/sap/duplicateClasses.xml b/agent/src/test/resources/org/openjdk/jmc/agent/test/sap/duplicateClasses.xml new file mode 100644 index 0000000000..fe40ae362c --- /dev/null +++ b/agent/src/test/resources/org/openjdk/jmc/agent/test/sap/duplicateClasses.xml @@ -0,0 +1,55 @@ + + + + + + + Tracks two classes with the same name. + test/duplicateClasses + true + org.openjdk.jmc.agent.sap.test.DuplicateTracedClasses + + test + (Ljava/lang/String;)V + + + String Attribute + The first parameter + + + + ENTRY + + + diff --git a/agent/src/test/resources/org/openjdk/jmc/agent/test/sap/generic.xml b/agent/src/test/resources/org/openjdk/jmc/agent/test/sap/generic.xml new file mode 100644 index 0000000000..7b44fb2b95 --- /dev/null +++ b/agent/src/test/resources/org/openjdk/jmc/agent/test/sap/generic.xml @@ -0,0 +1,366 @@ + + + + + __JFREvent + false + true + + + + + generic + org.openjdk.jmc.agent.sap.test.GenericLoggingTest + true + false + jdk/test/generic + + tracePrimitives1 + (ZBSCIJFD)I + + + v1 + v1 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat1()V + + + v2 + v2 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat1()V + + + v3 + v3 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat1()V + + + v4 + v4 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat1()V + + + v5 + v5 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat1()V + + + v6 + v6 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat1()V + + + v7 + v7 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat1()V + + + v8 + v8 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logLastFormat1()V + + + + + + + generic + org.openjdk.jmc.agent.sap.test.GenericLoggingTest + true + false + jdk/test/generic + + tracePrimitives2 + (CBSCIJFD)I + + + v1 + v1 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat2()V + + + v2 + v2 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat2()V + + + v3 + v3 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat2()V + + + v4 + v4 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat2()V + + + v5 + v5 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat2()V + + + v6 + v6 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat2()V + + + v7 + v7 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat2()V + + + v8 + v8 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logLastFormat2()V + + + + + + + generic + org.openjdk.jmc.agent.sap.test.GenericLoggingTest + true + false + jdk/test/generic + + tracePrimitives3 + (CBSCIJFD)I + + + v1 + v1 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat3()V + + + v2 + v2 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat3()V + + + v3 + v3 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat3()V + + + v4 + v4 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat3()V + + + v5 + v5 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat3()V + + + v6 + v6 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat3()V + + + v7 + v7 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat3()V + + + v8 + v8 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logLastFormat3()V + + + + + + + generic + org.openjdk.jmc.agent.sap.test.GenericLoggingTest + true + false + jdk/test/generic + + traceObjects1 + (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)I + + + v1 + v1 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat4()V + + + v2 + v2 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat4()V + + + v3 + v3 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat4()V + + + v4 + v4 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat4()V + + + v5 + v5 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat4()V + + + v6 + v6 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat4()V + + + v7 + v7 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat4()V + + + v8 + v8 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logLastFormat4()V + + + + + + + generic + org.openjdk.jmc.agent.sap.test.GenericLoggingTest + true + false + jdk/test/generic + + traceObjects2 + (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)I + + + v1 + v1 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat5()V + + + v2 + v2 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat5()V + + + v3 + v3 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat5()V + + + v4 + v4 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat5()V + + + v5 + v5 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat5()V + + + v6 + v6 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat5()V + + + v7 + v7 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat5()V + + + v8 + v8 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logLastFormat5()V + + + + + + + generic + org.openjdk.jmc.agent.sap.test.GenericLoggingTest + true + false + jdk/test/generic + + traceObjects3 + (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)I + + + v1 + v1 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat6()V + + + v2 + v2 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat6()V + + + v3 + v3 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat6()V + + + v4 + v4 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat6()V + + + v5 + v5 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat6()V + + + v6 + v6 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat6()V + + + v7 + v7 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logFormat6()V + + + v8 + v8 + org.openjdk.jmc.agent.sap.boot.converters.GenericLogger.logLastFormat6()V + + + + + + diff --git a/agent/src/test/resources/org/openjdk/jmc/agent/test/sap/jconsoleMain.xml b/agent/src/test/resources/org/openjdk/jmc/agent/test/sap/jconsoleMain.xml new file mode 100644 index 0000000000..42f1de83af --- /dev/null +++ b/agent/src/test/resources/org/openjdk/jmc/agent/test/sap/jconsoleMain.xml @@ -0,0 +1,55 @@ + + + + + + + JConsole main method + test/jconsole + true + sun.tools.jconsole.JConsole + + main + ([Ljava/lang/String;)V + + + parameters + The parameters + + + + ENTRY + + + diff --git a/application/org.openjdk.jmc.rcp.application/splash.bmp b/application/org.openjdk.jmc.rcp.application/splash.bmp index 91c8518e1c..9f5a79f8f3 100644 Binary files a/application/org.openjdk.jmc.rcp.application/splash.bmp and b/application/org.openjdk.jmc.rcp.application/splash.bmp differ diff --git a/scripts/checkcopyrightyear.sh b/scripts/checkcopyrightyear.sh index d10a0d4313..cc4c8700c7 100755 --- a/scripts/checkcopyrightyear.sh +++ b/scripts/checkcopyrightyear.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # set remote for upstream repository -git remote -v | grep -w upstream || git remote add upstream https://github.com/openjdk/jmc.git +git remote -v | grep -w upstream || git remote add upstream https://github.com/SAP/jmc.git git fetch upstream CURRENT_YEAR=$(date +'%Y') -COMMITTED_FILES=$(git diff --name-only --diff-filter=d upstream/master...HEAD) +COMMITTED_FILES=$(git diff --name-only --diff-filter=d upstream/sap...HEAD) UNCOMMITTED_FILES=$(git diff --name-only --diff-filter=d) MODIFIED_FILES=$(echo -e "$COMMITTED_FILES\n$UNCOMMITTED_FILES" | sort -u | grep -v '^$') counter=0 @@ -26,13 +26,13 @@ do done if [ $counter -ne 0 ] then - # check if the PR branch is up-to-date with upstream/master + # check if the PR branch is up-to-date with upstream/sap # borrowed from: https://stackoverflow.com/a/39402294 - if git merge-base --is-ancestor upstream/master @ + if git merge-base --is-ancestor upstream/sap @ then echo "Branch is up-to-date." else - echo "Branch is out of date with upstream/master. Please rebase your branch and try again." + echo "Branch is out of date with upstream/sap. Please rebase your branch and try again." exit 1 fi echo "There is a total of $counter copyright year(s) that require updating." diff --git a/scripts/runagenttests.bat b/scripts/runagenttests.bat index 5703cb4d3e..3f0ab95005 100755 --- a/scripts/runagenttests.bat +++ b/scripts/runagenttests.bat @@ -3,4 +3,5 @@ echo "======== Building and testing agent =========" cd agent call mvn %MAVENPARAMS% verify || EXIT /B 4 +call mvn -P SapAgent -DfullTest=true %MAVENPARAMS% clean verify || EXIT /B 4 echo "======== Finished ===========================" diff --git a/scripts/runagenttests.sh b/scripts/runagenttests.sh index 3a990fbf45..2e69166286 100755 --- a/scripts/runagenttests.sh +++ b/scripts/runagenttests.sh @@ -4,4 +4,5 @@ set -e echo "======== Building and testing agent =========" cd agent sh -c "mvn ${MAVENPARAMS} verify" +sh -c "mvn -P SapAgent -DfullTest=true ${MAVENPARAMS} clean verify" echo "======== Finished ==========================="