diff --git a/.azure-pipelines/publish-to-maven.yml b/.azure-pipelines/publish-to-maven.yml new file mode 100644 index 000000000..a1ef0a204 --- /dev/null +++ b/.azure-pipelines/publish-to-maven.yml @@ -0,0 +1,104 @@ +name: $(Date:yyyyMMdd).$(Rev:r) +resources: + repositories: + - repository: MicroBuildTemplate + type: git + name: 1ESPipelineTemplates/MicroBuildTemplate + ref: refs/tags/release +trigger: none +extends: + template: azure-pipelines/1ES.Official.Publish.yml@MicroBuildTemplate + parameters: + pool: + os: linux + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Ubuntu-2004 + sdl: + sourceAnalysisPool: + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Windows_2022 + os: windows + stages: + - stage: PublishToMaven + jobs: + - job: PublishToMaven + displayName: Maven Release job + templateContext: + type: releaseJob + isProduction: true + steps: + - task: DownloadBuildArtifacts@1 + displayName: 'Download Jar Artifacts' + inputs: + buildType: specific + project: 'a4d27ce2-a42d-4b71-8eef-78cee9a9728e' + pipeline: 16486 + downloadType: specific + extractTars: false + itemPattern: 'm2/**' + - script: | + echo "import public key" + echo $GPG_PUBLIC_B64 | base64 -d | gpg --import + + echo "import secret key" + echo $GPG_SECRET_B64 | base64 -d | gpg --batch --passphrase $GPGPASS --import + displayName: 'import GPG keys' + env: + GPG_PUBLIC_B64: $(GPG_PUBLIC_B64) + GPG_SECRET_B64: $(GPG_SECRET_B64) + GPGPASS: $(GPGPASS) + - task: NodeTool@0 + displayName: 'Use Node 20.x' + inputs: + versionSpec: 20.x + - script: | + cd $(System.ArtifactsDirectory)/m2 + pluginJarFile=$(basename -- java-debug-parent/*.pom) + + # remove .* from end + noExt=${pluginJarFile%.*} + + # remove *- from start + export releaseVersion=${noExt##*-} + echo $releaseVersion + + export artifactFolder=$(pwd .) + wget https://raw.githubusercontent.com/microsoft/java-debug/master/scripts/publishMaven.js + + export GPG_TTY=$(tty) + node publishMaven.js -task gpg + displayName: 'sign artifacts' + env: + GPG_PUBLIC_B64: $(GPG_PUBLIC_B64) + GPG_SECRET_B64: $(GPG_SECRET_B64) + GPGPASS: $(GPGPASS) + NEXUS_OSSRHPASS: $(NEXUS_OSSRHPASS) + NEXUS_OSSRHUSER: $(NEXUS_OSSRHUSER) + NEXUS_STAGINGPROFILEID: $(NEXUS_STAGINGPROFILEID) + - template: MicroBuild.Publish.yml@MicroBuildTemplate + parameters: + intent: 'PackageDistribution' + contentType: 'Maven' + contentSource: 'Folder' + folderLocation: '$(System.ArtifactsDirectory)/m2/java-debug-parent' + waitForReleaseCompletion: true + owners: 'jinbwan@microsoft.com' + approvers: 'roml@microsoft.com' + - template: MicroBuild.Publish.yml@MicroBuildTemplate + parameters: + intent: 'PackageDistribution' + contentType: 'Maven' + contentSource: 'Folder' + folderLocation: '$(System.ArtifactsDirectory)/m2/com.microsoft.java.debug.core' + waitForReleaseCompletion: true + owners: 'jinbwan@microsoft.com' + approvers: 'roml@microsoft.com' + - template: MicroBuild.Publish.yml@MicroBuildTemplate + parameters: + intent: 'PackageDistribution' + contentType: 'Maven' + contentSource: 'Folder' + folderLocation: '$(System.ArtifactsDirectory)/m2/com.microsoft.java.debug.plugin' + waitForReleaseCompletion: true + owners: 'jinbwan@microsoft.com' + approvers: 'roml@microsoft.com' \ No newline at end of file diff --git a/.azure-pipelines/signjars-nightly.yml b/.azure-pipelines/signjars-nightly.yml new file mode 100644 index 000000000..8b1e8d12a --- /dev/null +++ b/.azure-pipelines/signjars-nightly.yml @@ -0,0 +1,138 @@ +name: $(Date:yyyyMMdd).$(Rev:r) +variables: + - name: Codeql.Enabled + value: true +schedules: + - cron: 0 5 * * 1,2,3,4,5 + branches: + include: + - refs/heads/main +resources: + repositories: + - repository: self + type: git + ref: refs/heads/main + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release +trigger: none +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + pool: + os: linux + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Ubuntu-2004 + sdl: + sourceAnalysisPool: + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Windows_2022 + os: windows + customBuildTags: + - MigrationTooling-mseng-VSJava-13474-Tool + stages: + - stage: Build + jobs: + - job: Job_1 + displayName: Sign-Jars-Nightly + templateContext: + outputs: + - output: pipelineArtifact + artifactName: plugin + targetPath: $(Build.ArtifactStagingDirectory) + displayName: "Publish Artifact: plugin" + steps: + - checkout: self + fetchTags: true + - task: UseDotNet@2 + displayName: 'Use .NET Core 3.1.x' + inputs: + packageType: 'sdk' + version: '3.1.x' + - task: UseDotNet@2 + displayName: 'Use .NET Core 8.0.x' + inputs: + packageType: 'sdk' + version: '8.0.x' + - task: MicroBuildSigningPlugin@4 + displayName: 'Install Signing Plugin' + inputs: + signType: real + azureSubscription: 'MicroBuild Signing Task (MSEng)' + useEsrpCli: true + ConnectedPMEServiceName: 0e38ce24-f885-4c86-b997-5887b97a1899 + feedSource: 'https://mseng.pkgs.visualstudio.com/DefaultCollection/_packaging/MicroBuildToolset/nuget/v3/index.json' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + MicroBuildOutputFolderOverride: '$(Agent.TempDirectory)' + - task: JavaToolInstaller@0 + displayName: Use Java 21 + inputs: + versionSpec: "21" + jdkArchitectureOption: x64 + jdkSourceOption: PreInstalled + - task: CmdLine@2 + displayName: Parse the release version from pom.xml + inputs: + script: |- + #!/bin/bash + + sudo apt-get install xmlstarlet + xmlstarlet --version + RELEASE_VERSION=$(xmlstarlet sel -t -v "/_:project/_:version" pom.xml) + echo $RELEASE_VERSION + echo "##vso[task.setvariable variable=RELEASE_VERSION]$RELEASE_VERSION" + - task: CmdLine@2 + displayName: Build core.jar + inputs: + script: | + ./mvnw clean install -f com.microsoft.java.debug.core/pom.xml -Dmaven.repo.local=./.repository + + mkdir -p jars + mv .repository/com/microsoft/java/com.microsoft.java.debug.core/$RELEASE_VERSION/com.microsoft.java.debug.core*.jar jars/ + - task: CmdLine@2 + displayName: Sign core jars + inputs: + script: | + files=$(find . -type f -name "com.microsoft.java.debug.core*.jar") + for file in $files; do + fileName=$(basename "$file") + dotnet "$MBSIGN_APPFOLDER/DDSignFiles.dll" -- /file:"$fileName" /certs:100010171 + done + workingDirectory: 'jars' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - task: CmdLine@2 + displayName: install signed core.jar + inputs: + script: cp jars/com.microsoft.java.debug.core*.jar .repository/com/microsoft/java/com.microsoft.java.debug.core/$RELEASE_VERSION/ + - task: CmdLine@2 + displayName: Build plugin.jar + inputs: + script: |- + ./mvnw clean install -N -f pom.xml -Dmaven.repo.local=./.repository + ./mvnw clean install -f com.microsoft.java.debug.target/pom.xml -Dmaven.repo.local=./.repository + ./mvnw clean install -f com.microsoft.java.debug.plugin/pom.xml -Dmaven.repo.local=./.repository + + mkdir -p jars + mv .repository/com/microsoft/java/com.microsoft.java.debug.plugin/$RELEASE_VERSION/com.microsoft.java.debug.plugin*.jar jars/ + - task: CmdLine@2 + displayName: Sign plugin jars + inputs: + script: | + files=$(find . -type f -name "com.microsoft.java.debug.plugin*.jar") + for file in $files; do + fileName=$(basename "$file") + dotnet "$MBSIGN_APPFOLDER/DDSignFiles.dll" -- /file:"$fileName" /certs:100010171 + done + workingDirectory: 'jars' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - task: CopyFiles@2 + displayName: "Copy plugin.jar to: $(Build.ArtifactStagingDirectory)" + inputs: + Contents: |+ + jars/com.microsoft.java.debug.plugin*.jar + + TargetFolder: $(Build.ArtifactStagingDirectory) diff --git a/.azure-pipelines/signjars-rc.yml b/.azure-pipelines/signjars-rc.yml new file mode 100644 index 000000000..e87444603 --- /dev/null +++ b/.azure-pipelines/signjars-rc.yml @@ -0,0 +1,169 @@ +name: $(Date:yyyyMMdd).$(Rev:r) +variables: + - name: Codeql.Enabled + value: true +resources: + repositories: + - repository: self + type: git + ref: refs/heads/main + - repository: 1esPipelines + type: git + name: 1ESPipelineTemplates/1ESPipelineTemplates + ref: refs/tags/release +trigger: none +extends: + template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines + parameters: + pool: + os: linux + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Ubuntu-2004 + sdl: + sourceAnalysisPool: + name: 1ES_JavaTooling_Pool + image: 1ES_JavaTooling_Windows_2022 + os: windows + customBuildTags: + - MigrationTooling-mseng-VSJava-9151-Tool + stages: + - stage: Build + jobs: + - job: Job_1 + displayName: Sign-Jars-RC + templateContext: + outputs: + - output: pipelineArtifact + artifactName: m2 + targetPath: $(Build.ArtifactStagingDirectory)/m2 + displayName: "Publish Artifact: m2" + steps: + - checkout: self + fetchTags: true + - task: UseDotNet@2 + displayName: 'Use .NET Core 3.1.x' + inputs: + packageType: 'sdk' + version: '3.1.x' + - task: UseDotNet@2 + displayName: 'Use .NET Core 8.0.x' + inputs: + packageType: 'sdk' + version: '8.0.x' + - task: MicroBuildSigningPlugin@4 + displayName: 'Install Signing Plugin' + inputs: + signType: real + azureSubscription: 'MicroBuild Signing Task (MSEng)' + useEsrpCli: true + ConnectedPMEServiceName: 0e38ce24-f885-4c86-b997-5887b97a1899 + feedSource: 'https://mseng.pkgs.visualstudio.com/DefaultCollection/_packaging/MicroBuildToolset/nuget/v3/index.json' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + MicroBuildOutputFolderOverride: '$(Agent.TempDirectory)' + - task: JavaToolInstaller@0 + displayName: Use Java 21 + inputs: + versionSpec: "21" + jdkArchitectureOption: x64 + jdkSourceOption: PreInstalled + - task: CmdLine@2 + displayName: Parse the release version from pom.xml + inputs: + script: |- + #!/bin/bash + + sudo apt-get install xmlstarlet + xmlstarlet --version + RELEASE_VERSION=$(xmlstarlet sel -t -v "/_:project/_:version" pom.xml) + echo $RELEASE_VERSION + echo "##vso[task.setvariable variable=RELEASE_VERSION]$RELEASE_VERSION" + - task: CmdLine@2 + displayName: Build core.jar + inputs: + script: | + ./mvnw -N clean install -Dmaven.repo.local=./.repository + + ./mvnw clean install -f com.microsoft.java.debug.core/pom.xml -Dmaven.repo.local=./.repository + + mkdir -p jars + mv .repository/com/microsoft/java/com.microsoft.java.debug.core/$RELEASE_VERSION/com.microsoft.java.debug.core*.jar jars/ + - task: CmdLine@2 + displayName: Sign core jars + inputs: + script: | + files=$(find . -type f -name "com.microsoft.java.debug.core*.jar") + for file in $files; do + fileName=$(basename "$file") + dotnet "$MBSIGN_APPFOLDER/DDSignFiles.dll" -- /file:"$fileName" /certs:100010171 + done + workingDirectory: 'jars' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - task: CmdLine@2 + displayName: install signed core.jar + inputs: + script: cp jars/com.microsoft.java.debug.core*.jar .repository/com/microsoft/java/com.microsoft.java.debug.core/$RELEASE_VERSION/ + - task: CmdLine@2 + displayName: Build plugin.jar + inputs: + script: |- + ./mvnw clean install -f com.microsoft.java.debug.target/pom.xml -Dmaven.repo.local=./.repository + ./mvnw clean install -f com.microsoft.java.debug.plugin/pom.xml -Dmaven.repo.local=./.repository + + mkdir -p jars + mv .repository/com/microsoft/java/com.microsoft.java.debug.plugin/$RELEASE_VERSION/com.microsoft.java.debug.plugin*.jar jars/ + - task: CmdLine@2 + displayName: Sign plugin jars + inputs: + script: | + files=$(find . -type f -name "com.microsoft.java.debug.plugin*.jar") + for file in $files; do + fileName=$(basename "$file") + dotnet "$MBSIGN_APPFOLDER/DDSignFiles.dll" -- /file:"$fileName" /certs:100010171 + done + workingDirectory: 'jars' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - task: CmdLine@2 + displayName: install signed plugin.jar + inputs: + script: cp jars/com.microsoft.java.debug.plugin*.jar .repository/com/microsoft/java/com.microsoft.java.debug.plugin/$RELEASE_VERSION/ + - task: CmdLine@2 + displayName: build m2 artifacts + inputs: + script: | + ./mvnw source:jar -f com.microsoft.java.debug.core/pom.xml -Dmaven.repo.local=./.repository + ./mvnw javadoc:jar -f com.microsoft.java.debug.core/pom.xml -Ddoclint=none -Dmaven.repo.local=./.repository + + ./mvnw source:jar -f com.microsoft.java.debug.plugin/pom.xml -Dmaven.repo.local=./.repository + ./mvnw javadoc:jar -f com.microsoft.java.debug.plugin/pom.xml -Ddoclint=none -Dmaven.repo.local=./.repository + + mkdir -p m2/java-debug-parent + cp pom.xml m2/java-debug-parent/java-debug-parent-$RELEASE_VERSION.pom + + mkdir -p m2/com.microsoft.java.debug.core + cp com.microsoft.java.debug.core/target/com.microsoft.java.debug.core*.jar m2/com.microsoft.java.debug.core + cp com.microsoft.java.debug.core/pom.xml m2/com.microsoft.java.debug.core/com.microsoft.java.debug.core-$RELEASE_VERSION.pom + + mkdir -p m2/com.microsoft.java.debug.plugin + cp com.microsoft.java.debug.plugin/target/com.microsoft.java.debug.plugin*.jar m2/com.microsoft.java.debug.plugin + cp com.microsoft.java.debug.plugin/pom.xml m2/com.microsoft.java.debug.plugin/com.microsoft.java.debug.plugin-$RELEASE_VERSION.pom + - task: CmdLine@2 + displayName: Sign m2 jars + inputs: + script: | + files=$(find . -type f -name "*.jar") + for file in $files; do + # fileName=$(basename "$file") + dotnet "$MBSIGN_APPFOLDER/DDSignFiles.dll" -- /file:"$file" /certs:100010171 + done + workingDirectory: 'm2' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - task: CopyFiles@2 + displayName: "Copy m2 to: $(Build.ArtifactStagingDirectory)" + inputs: + Contents: |+ + m2/** + TargetFolder: $(Build.ArtifactStagingDirectory) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..9d7579633 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @testforstephen @jdneo @chagong @wenytang-ms diff --git a/.github/llms.md b/.github/llms.md new file mode 100644 index 000000000..55d69b238 --- /dev/null +++ b/.github/llms.md @@ -0,0 +1,38 @@ +# Extension Pack for Java +Extension Pack for Java is a collection of popular extensions that can help write, test and debug Java applications in Visual Studio Code. By installing Extension Pack for Java, the following extensions are installed: + +- [📦 Language Support for Java™ by Red Hat ](https://marketplace.visualstudio.com/items?itemName=redhat.java) + - Code Navigation + - Auto Completion + - Refactoring + - Code Snippets +- [📦 Debugger for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug) + - Debugging +- [📦 Test Runner for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-test) + - Run & Debug JUnit/TestNG Test Cases +- [📦 Maven for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-maven) + - Project Scaffolding + - Custom Goals +- [📦 Gradle for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-gradle) + - View Gradle tasks and project dependencies + - Gradle file authoring + - Import Gradle projects via [Gradle Build Server](https://github.com/microsoft/build-server-for-gradle) +- [📦 Project Manager for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-dependency) + - Manage Java projects, referenced libraries, resource files, packages, classes, and class members +- [📦 Visual Studio IntelliCode](https://marketplace.visualstudio.com/items?itemName=VisualStudioExptTeam.vscodeintellicode) + - AI-assisted development + - Completion list ranked by AI + +## Label +When labeling an issue, follow the rules below per label category: +### General Rules +- Analyze if the issue is related with the scope of using extensions for Java development. If not, STOP labelling IMMEDIATELY. +- Assign label per category. +- If a category is not applicable or you're unsure, you may skip it. +- Do not assign multiple labels within the same category, unless explicitly allowed as an exception. + +### Issue Type Labels +- [bug]: Primary label for real bug issues +- [enhancement]: Primary label for enhancement issues +- [documentation]: Primary label for documentation issues +- [question]: Primary label for question issues \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58381df99..553d95a9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,80 +12,83 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: '11' + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' - - name: Cache local Maven repository - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- - - name: Verify - run: ./mvnw clean verify + - name: Verify + run: ./mvnw clean verify -U - - name: Checkstyle - run: ./mvnw checkstyle:check + - name: Checkstyle + run: ./mvnw checkstyle:check windows: name: Windows runs-on: windows-latest timeout-minutes: 30 steps: - - name: Set git to use LF - run: | - git config --global core.autocrlf false - git config --global core.eol lf + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: '11' + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' - - name: Cache local Maven repository - uses: actions/cache@v2 - with: - path: $HOME/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: $HOME/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- - - name: Verify - run: ./mvnw.cmd clean verify + - name: Verify + run: ./mvnw.cmd clean verify - - name: Checkstyle - run: ./mvnw.cmd checkstyle:check + - name: Checkstyle + run: ./mvnw.cmd checkstyle:check darwin: name: macOS runs-on: macos-latest timeout-minutes: 30 steps: - - uses: actions/checkout@v2 - - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: '11' - - - name: Cache local Maven repository - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - - name: Verify - run: ./mvnw clean verify - - - name: Checkstyle - run: ./mvnw checkstyle:check + - uses: actions/checkout@v5 + + - name: Set up JDK 21 + uses: actions/setup-java@v5 + with: + java-version: '21' + distribution: 'temurin' + + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Verify + run: ./mvnw clean verify -U + + - name: Checkstyle + run: ./mvnw checkstyle:check diff --git a/.github/workflows/triage-agent.yml b/.github/workflows/triage-agent.yml new file mode 100644 index 000000000..f1df6e117 --- /dev/null +++ b/.github/workflows/triage-agent.yml @@ -0,0 +1,125 @@ +name: AI Triage +on: + issues: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: 'Issue number to triage (manual run). e.g. 123' + required: true + +run-name: >- + AI Triage for Issue #${{ github.event.issue.number || github.event.inputs.issue_number }} + +permissions: + issues: write + contents: read + +jobs: + label_and_comment: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Get issue data + id: get_issue + uses: actions/github-script@v6 + with: + script: | + const eventName = context.eventName; + let issue; + if (eventName === 'workflow_dispatch') { + const inputs = context.payload.inputs || {}; + const issueNumber = inputs.issue_number || inputs.issueNumber; + if (!issueNumber) core.setFailed('Input issue_number is required for manual run.'); + const { data } = await github.rest.issues.get({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: parseInt(issueNumber, 10), + }); + issue = data; + } else if (context.payload.issue) { + issue = context.payload.issue; + } else { + core.setFailed('No issue information found in the event payload.'); + } + core.setOutput('id', String(issue.number)); + core.setOutput('user', String((issue.user && issue.user.login) || '')); + core.setOutput('title', String(issue.title || '')); + core.setOutput('body', String(issue.body || '')); + const labelNames = (issue.labels || []).map(label => label.name); + core.setOutput('labels', JSON.stringify(labelNames)); + + - name: Call Azure Function + id: call_azure_function + env: + PAYLOAD: >- + { + "authToken": "${{ secrets.GITHUB_TOKEN }}", + "repoId": "microsoft/java-debug", + "issueData": { + "id": ${{ steps.get_issue.outputs.id }}, + "user": ${{ toJson(steps.get_issue.outputs.user) }}, + "title": ${{ toJson(steps.get_issue.outputs.title) }}, + "body": ${{ toJson(steps.get_issue.outputs.body) }}, + "labels": ${{ steps.get_issue.outputs.labels }} + }, + "mode": "DirectUpdate" + } + + run: | + # Make the HTTP request with improved error handling and timeouts + echo "Making request to triage agent..." + + # Add timeout handling and better error detection + set +e # Don't exit on curl failure + response=$(timeout ${{ vars.TRIAGE_AGENT_TIMEOUT }} curl \ + --max-time 0 \ + --connect-timeout 30 \ + --fail-with-body \ + --silent \ + --show-error \ + --write-out "HTTPSTATUS:%{http_code}" \ + --header "Content-Type: application/json" \ + --request POST \ + --data "$PAYLOAD" \ + ${{ secrets.TRIAGE_FUNCTION_LINK }} 2>&1) + + curl_exit_code=$? + set -e # Re-enable exit on error + + echo "Curl exit code: $curl_exit_code" + + # Check if curl command timed out or failed + if [ $curl_exit_code -eq 124 ]; then + echo "❌ Request timed out after 650 seconds" + exit 1 + elif [ $curl_exit_code -ne 0 ]; then + echo "❌ Curl command failed with exit code: $curl_exit_code" + echo "Response: $response" + exit 1 + fi + + # Extract HTTP status code and response body + http_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2) + response_body=$(echo "$response" | sed 's/HTTPSTATUS:[0-9]*$//') + + echo "HTTP Status Code: $http_code" + + # Validate HTTP status code + if [ -z "$http_code" ]; then + echo "❌ Failed to extract HTTP status code from response" + echo "Raw response: $response" + exit 1 + fi + + # Check if the request was successful + if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then + echo "✅ Azure Function call succeeded" + else + echo "❌ Azure Function call failed with status code: $http_code" + echo "Response: $response_body" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/triage-all-open-issues.yml b/.github/workflows/triage-all-open-issues.yml new file mode 100644 index 000000000..092f4ef70 --- /dev/null +++ b/.github/workflows/triage-all-open-issues.yml @@ -0,0 +1,145 @@ +name: AI Triage - Process All Open Issues +on: + workflow_dispatch: + inputs: + dry_run: + description: 'Dry run mode - only list issues without processing' + required: false + default: false + type: boolean + max_issues: + description: 'Maximum number of issues to process (0 = all)' + required: false + default: '0' + type: string + +permissions: + issues: write + contents: read + actions: write + +jobs: + get_open_issues: + runs-on: ubuntu-latest + outputs: + issue_numbers: ${{ steps.get_issues.outputs.issue_numbers }} + total_count: ${{ steps.get_issues.outputs.total_count }} + + steps: + - name: Get all open issues + id: get_issues + uses: actions/github-script@v6 + with: + script: | + // Use Search API to filter issues at API level + const { data } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${context.repo.owner}/${context.repo.repo} is:issue is:open -label:ai-triaged -label:invalid`, + sort: 'created', + order: 'asc', + per_page: 100 + }); + + const actualIssues = data.items; + + let issuesToProcess = actualIssues; + const maxIssues = parseInt('${{ inputs.max_issues }}' || '0'); + + if (maxIssues > 0 && actualIssues.length > maxIssues) { + issuesToProcess = actualIssues.slice(0, maxIssues); + console.log(`Limiting to first ${maxIssues} issues out of ${actualIssues.length} total`); + } + + const issueNumbers = issuesToProcess.map(issue => issue.number); + const totalCount = issuesToProcess.length; + + console.log(`Found ${actualIssues.length} open issues, processing ${totalCount}:`); + issuesToProcess.forEach(issue => { + console.log(` #${issue.number}: ${issue.title}`); + }); + + core.setOutput('issue_numbers', JSON.stringify(issueNumbers)); + core.setOutput('total_count', totalCount); + + process_issues: + runs-on: ubuntu-latest + needs: get_open_issues + if: needs.get_open_issues.outputs.total_count > 0 + + strategy: + # Process issues one by one (max-parallel: 1) + max-parallel: 1 + matrix: + issue_number: ${{ fromJSON(needs.get_open_issues.outputs.issue_numbers) }} + + steps: + - name: Log current issue being processed + run: | + echo "🔄 Processing issue #${{ matrix.issue_number }}" + echo "Total issues to process: ${{ needs.get_open_issues.outputs.total_count }}" + + - name: Check if dry run mode + if: inputs.dry_run == true + run: | + echo "🔍 DRY RUN MODE: Would process issue #${{ matrix.issue_number }}" + echo "Skipping actual triage processing" + + - name: Trigger triage workflow for issue + if: inputs.dry_run != true + uses: actions/github-script@v6 + with: + script: | + const issueNumber = '${{ matrix.issue_number }}'; + + try { + console.log(`Triggering triage workflow for issue #${issueNumber}`); + + const response = await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'triage-agent.yml', + ref: 'main', + inputs: { + issue_number: issueNumber + } + }); + + console.log(`✅ Successfully triggered triage workflow for issue #${issueNumber}`); + + } catch (error) { + console.error(`❌ Failed to trigger triage workflow for issue #${issueNumber}:`, error); + core.setFailed(`Failed to process issue #${issueNumber}: ${error.message}`); + } + + - name: Wait for workflow completion + if: inputs.dry_run != true + run: | + echo "⏳ Waiting for triage workflow to complete for issue #${{ matrix.issue_number }}..." + echo "Timeout: ${{ vars.TRIAGE_AGENT_TIMEOUT }} seconds" + sleep ${{ vars.TRIAGE_AGENT_TIMEOUT }} # Wait for triage workflow completion + + summary: + runs-on: ubuntu-latest + needs: [get_open_issues, process_issues] + if: always() + + steps: + - name: Print summary + run: | + echo "## Triage Processing Summary" + echo "Total open issues found: ${{ needs.get_open_issues.outputs.total_count }}" + + if [ "${{ inputs.dry_run }}" == "true" ]; then + echo "Mode: DRY RUN (no actual processing performed)" + else + echo "Mode: FULL PROCESSING" + fi + + if [ "${{ needs.process_issues.result }}" == "success" ]; then + echo "✅ All issues processed successfully" + elif [ "${{ needs.process_issues.result }}" == "failure" ]; then + echo "❌ Some issues failed to process" + elif [ "${{ needs.process_issues.result }}" == "skipped" ]; then + echo "⏭️ Processing was skipped (no open issues found)" + else + echo "⚠️ Processing completed with status: ${{ needs.process_issues.result }}" + fi diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 41c70a7e0..000000000 Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 56bb0164e..44f3cf2c1 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1,2 @@ -distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip \ No newline at end of file +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/.project b/.project index d34865de3..f00ccfc41 100644 --- a/.project +++ b/.project @@ -16,12 +16,12 @@ - 1600224298170 + 1665543654766 30 org.eclipse.core.resources.regexFilterMatcher - node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..e138ec5d6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). + + diff --git a/com.microsoft.java.debug.core/.classpath b/com.microsoft.java.debug.core/.classpath index 9ba41a249..b6fe6b96c 100644 --- a/com.microsoft.java.debug.core/.classpath +++ b/com.microsoft.java.debug.core/.classpath @@ -13,7 +13,7 @@ - + diff --git a/com.microsoft.java.debug.core/.project b/com.microsoft.java.debug.core/.project index a7480133e..353c44be5 100644 --- a/com.microsoft.java.debug.core/.project +++ b/com.microsoft.java.debug.core/.project @@ -28,12 +28,12 @@ - 1599036548523 + 1665543654702 30 org.eclipse.core.resources.regexFilterMatcher - node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ diff --git a/com.microsoft.java.debug.core/pom.xml b/com.microsoft.java.debug.core/pom.xml index 4c67c37f4..8bfdb7292 100644 --- a/com.microsoft.java.debug.core/pom.xml +++ b/com.microsoft.java.debug.core/pom.xml @@ -5,7 +5,7 @@ com.microsoft.java java-debug-parent - 0.36.0 + 0.53.2 com.microsoft.java.debug.core jar @@ -42,7 +42,7 @@ org.apache.commons commons-lang3 - 3.6 + 3.18.0 com.google.code.gson @@ -57,12 +57,12 @@ org.reactivestreams reactive-streams - 1.0.3 + 1.0.4 commons-io commons-io - 2.11.0 + 2.14.0 diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/AsyncJdwpUtils.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/AsyncJdwpUtils.java new file mode 100644 index 000000000..fad2ac223 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/AsyncJdwpUtils.java @@ -0,0 +1,143 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core; + +import static java.util.concurrent.CompletableFuture.allOf; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Supplier; + +public class AsyncJdwpUtils { + /** + * Create a the thread pool to process JDWP tasks. + * JDWP tasks are IO-bounded, so use a relatively large thread pool for JDWP tasks. + */ + public static ExecutorService jdwpThreadPool = Executors.newWorkStealingPool(100); + // public static ExecutorService jdwpThreadPool = Executors.newCachedThreadPool(); + + public static CompletableFuture runAsync(List tasks) { + return runAsync(jdwpThreadPool, tasks.toArray(new Runnable[0])); + } + + public static CompletableFuture runAsync(Runnable... tasks) { + return runAsync(jdwpThreadPool, tasks); + } + + public static CompletableFuture runAsync(Executor executor, List tasks) { + return runAsync(executor, tasks.toArray(new Runnable[0])); + } + + public static CompletableFuture runAsync(Executor executor, Runnable... tasks) { + List> promises = new ArrayList<>(); + for (Runnable task : tasks) { + if (task == null) { + continue; + } + + promises.add(CompletableFuture.runAsync(task, executor)); + } + + return CompletableFuture.allOf(promises.toArray(new CompletableFuture[0])); + } + + public static CompletableFuture supplyAsync(Supplier supplier) { + return supplyAsync(jdwpThreadPool, supplier); + } + + public static CompletableFuture supplyAsync(Executor executor, Supplier supplier) { + return CompletableFuture.supplyAsync(supplier, executor); + } + + public static U await(CompletableFuture future) { + try { + return future.join(); + } catch (CompletionException ex) { + if (ex.getCause() instanceof RuntimeException) { + throw (RuntimeException) ex.getCause(); + } + + throw ex; + } + } + + public static List await(CompletableFuture[] futures) { + List results = new ArrayList<>(); + try { + allOf(futures).join(); + for (CompletableFuture future : futures) { + results.add(await(future)); + } + } catch (CompletionException ex) { + if (ex.getCause() instanceof RuntimeException) { + throw (RuntimeException) ex.getCause(); + } + + throw ex; + } + + return results; + } + + public static List await(List> futures) { + return await((CompletableFuture[]) futures.toArray(new CompletableFuture[0])); + } + + public static CompletableFuture> all(CompletableFuture... futures) { + return allOf(futures).thenApply((res) -> { + List results = new ArrayList<>(); + for (CompletableFuture future : futures) { + results.add(future.join()); + } + + return results; + }); + } + + public static CompletableFuture> all(List> futures) { + return allOf(futures.toArray(new CompletableFuture[0])).thenApply((res) -> { + List results = new ArrayList<>(); + for (CompletableFuture future : futures) { + results.add(future.join()); + } + + return results; + }); + } + + public static CompletableFuture> flatAll(CompletableFuture>... futures) { + return allOf(futures).thenApply((res) -> { + List results = new ArrayList<>(); + for (CompletableFuture> future : futures) { + results.addAll(future.join()); + } + + return results; + }); + } + + public static CompletableFuture> flatAll(List>> futures) { + return allOf(futures.toArray(new CompletableFuture[0])).thenApply((res) -> { + List results = new ArrayList<>(); + for (CompletableFuture> future : futures) { + results.addAll(future.join()); + } + + return results; + }); + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java index 790eff801..d8316c2e3 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Breakpoint.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,12 +12,17 @@ package com.microsoft.java.debug.core; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; +import com.sun.jdi.AbsentInformationException; import com.sun.jdi.Location; +import com.sun.jdi.Method; import com.sun.jdi.ReferenceType; import com.sun.jdi.VMDisconnectedException; import com.sun.jdi.VirtualMachine; @@ -32,37 +37,65 @@ public class Breakpoint implements IBreakpoint { private VirtualMachine vm = null; private IEventHub eventHub = null; - private String className = null; - private int lineNumber = 0; + private JavaBreakpointLocation sourceLocation = null; private int hitCount = 0; private String condition = null; private String logMessage = null; private HashMap propertyMap = new HashMap<>(); + private final boolean suspendAllThreads; - Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber) { - this(vm, eventHub, className, lineNumber, 0, null); + private boolean async = false; + + Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, boolean suspendAllThreads) { + this(vm, eventHub, className, lineNumber, 0, null, suspendAllThreads); + } + + Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, boolean suspendAllThreads) { + this(vm, eventHub, className, lineNumber, hitCount, null, suspendAllThreads); } - Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount) { - this(vm, eventHub, className, lineNumber, hitCount, null); + Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, + String condition, boolean suspendAllThreads) { + this(vm, eventHub, className, lineNumber, hitCount, condition, null, suspendAllThreads); } - Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, String condition) { + Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, + String condition, String logMessage, boolean suspendAllThreads) { this.vm = vm; this.eventHub = eventHub; - this.className = className; - this.lineNumber = lineNumber; + this.suspendAllThreads = suspendAllThreads; + String contextClass = className; + String methodName = null; + String methodSignature = null; + if (className != null && className.contains("#")) { + contextClass = className.substring(0, className.indexOf("#")); + String[] methodInfo = className.substring(className.indexOf("#") + 1).split("#"); + methodName = methodInfo[0]; + methodSignature = methodInfo[1]; + } + + this.sourceLocation = new JavaBreakpointLocation(lineNumber, -1); + this.sourceLocation.setClassName(contextClass); + this.sourceLocation.setMethodName(methodName); + this.sourceLocation.setMethodSignature(methodSignature); this.hitCount = hitCount; this.condition = condition; + this.logMessage = logMessage; } - Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, String condition, String logMessage) { - this(vm, eventHub, className, lineNumber, hitCount, condition); + Breakpoint(VirtualMachine vm, IEventHub eventHub, JavaBreakpointLocation sourceLocation, int hitCount, + String condition, String logMessage, boolean suspendAllThreads) { + this.vm = vm; + this.eventHub = eventHub; + this.sourceLocation = sourceLocation; + this.hitCount = hitCount; + this.condition = condition; this.logMessage = logMessage; + this.suspendAllThreads = suspendAllThreads; } // IDebugResource - private List requests = new ArrayList<>(); + private List requests = Collections.synchronizedList(new ArrayList<>()); private List subscriptions = new ArrayList<>(); @Override @@ -91,14 +124,24 @@ public void close() throws Exception { } // IBreakpoint + @Override + public JavaBreakpointLocation sourceLocation() { + return this.sourceLocation; + } + @Override public String className() { - return className; + return this.sourceLocation.className(); } @Override public int getLineNumber() { - return lineNumber; + return this.sourceLocation.lineNumber(); + } + + @Override + public int getColumnNumber() { + return this.sourceLocation.columnNumber(); } @Override @@ -106,14 +149,21 @@ public String getCondition() { return condition; } + @Override + public int hashCode() { + return Objects.hash(sourceLocation); + } + @Override public boolean equals(Object obj) { - if (!(obj instanceof IBreakpoint)) { - return super.equals(obj); + if (this == obj) { + return true; } - - IBreakpoint breakpoint = (IBreakpoint) obj; - return Objects.equals(this.className(), breakpoint.className()) && this.getLineNumber() == breakpoint.getLineNumber(); + if (!(obj instanceof Breakpoint)) { + return false; + } + Breakpoint other = (Breakpoint) obj; + return Objects.equals(sourceLocation, other.sourceLocation); } @Override @@ -129,6 +179,7 @@ public void setHitCount(int hitCount) { .filter(request -> request instanceof BreakpointRequest) .subscribe(request -> { request.addCountFilter(hitCount); + request.disable(); request.enable(); }); } @@ -148,18 +199,41 @@ public String getLogMessage() { return this.logMessage; } + @Override + public boolean async() { + return this.async; + } + + @Override + public void setAsync(boolean async) { + this.async = async; + } + + @Override + public void setSuspendPolicy(String policy) { + } + + @Override + public String getSuspendPolicy() { + return suspendAllThreads ? "SUSPEND_ALL" : "SUSPEND_EVENT_THREAD"; + } + + protected boolean suspendAllThreads() { + return suspendAllThreads; + } + @Override public CompletableFuture install() { // It's possible that different class loaders create new class with the same name. // Here to listen to future class prepare events to handle such case. ClassPrepareRequest classPrepareRequest = vm.eventRequestManager().createClassPrepareRequest(); - classPrepareRequest.addClassFilter(className); + classPrepareRequest.addClassFilter(className()); classPrepareRequest.enable(); requests.add(classPrepareRequest); // Local types also needs to be handled ClassPrepareRequest localClassPrepareRequest = vm.eventRequestManager().createClassPrepareRequest(); - localClassPrepareRequest.addClassFilter(className + "$*"); + localClassPrepareRequest.addClassFilter(className() + "$*"); localClassPrepareRequest.enable(); requests.add(localClassPrepareRequest); @@ -171,8 +245,9 @@ public CompletableFuture install() { || localClassPrepareRequest.equals(debugEvent.event.request()))) .subscribe(debugEvent -> { ClassPrepareEvent event = (ClassPrepareEvent) debugEvent.event; - List newRequests = createBreakpointRequests(event.referenceType(), lineNumber, - hitCount, false); + List newRequests = AsyncJdwpUtils.await( + createBreakpointRequests(event.referenceType(), getLineNumber(), hitCount, false) + ); requests.addAll(newRequests); if (!newRequests.isEmpty() && !future.isDone()) { this.putProperty("verified", true); @@ -181,102 +256,230 @@ public CompletableFuture install() { }); subscriptions.add(subscription); - List refTypes = vm.classesByName(className); - List newRequests = createBreakpointRequests(refTypes, lineNumber, hitCount, true); - requests.addAll(newRequests); + Runnable resolveRequestsFromExistingClasses = () -> { + List refTypes = vm.classesByName(className()); + createBreakpointRequests(refTypes, getLineNumber(), hitCount, true) + .whenComplete((newRequests, ex) -> { + if (ex != null) { + return; + } + + requests.addAll(newRequests); + if (!newRequests.isEmpty() && !future.isDone()) { + this.putProperty("verified", true); + future.complete(this); + } + }); + }; - if (!newRequests.isEmpty() && !future.isDone()) { - this.putProperty("verified", true); - future.complete(this); + if (async()) { + AsyncJdwpUtils.runAsync(resolveRequestsFromExistingClasses); + } else { + resolveRequestsFromExistingClasses.run(); } return future; } - private static List collectLocations(ReferenceType refType, int lineNumber) { - List locations = new ArrayList<>(); - - try { - locations.addAll(refType.locationsOfLine(lineNumber)); - } catch (Exception e) { - // could be AbsentInformationException or ClassNotPreparedException - // but both are expected so no need to further handle + private CompletableFuture> collectLocations(ReferenceType refType, int lineNumber) { + List>> futures = new ArrayList<>(); + Iterator iter = refType.methods().iterator(); + while (iter.hasNext()) { + Method method = iter.next(); + if (async()) { + futures.add(AsyncJdwpUtils.supplyAsync(() -> findLocaitonsOfLine(method, lineNumber))); + } else { + futures.add(CompletableFuture.completedFuture(findLocaitonsOfLine(method, lineNumber))); + } } - return locations; + return AsyncJdwpUtils.flatAll(futures); } - private static List collectLocations(List refTypes, int lineNumber, boolean includeNestedTypes) { - List locations = new ArrayList<>(); - try { - refTypes.forEach(refType -> { - List newLocations = collectLocations(refType, lineNumber); - if (!newLocations.isEmpty()) { - locations.addAll(newLocations); - } else if (includeNestedTypes) { - // ReferenceType.nestedTypes() will invoke vm.allClasses() to list all loaded classes, - // should avoid using nestedTypes for performance. - for (ReferenceType nestedType : refType.nestedTypes()) { - List nestedLocations = collectLocations(nestedType, lineNumber); - if (!nestedLocations.isEmpty()) { - locations.addAll(nestedLocations); - break; - } + private CompletableFuture> collectLocations(List refTypes, int lineNumber, boolean includeNestedTypes) { + List>> futures = new ArrayList<>(); + refTypes.forEach(refType -> { + futures.add(collectLocations(refType, lineNumber, includeNestedTypes)); + }); + + return AsyncJdwpUtils.flatAll(futures); + } + + private CompletableFuture> collectLocations(ReferenceType refType, int lineNumber, boolean includeNestedTypes) { + return collectLocations(refType, lineNumber).thenCompose((newLocations) -> { + if (!newLocations.isEmpty()) { + return CompletableFuture.completedFuture(newLocations); + } else if (includeNestedTypes) { + // ReferenceType.nestedTypes() will invoke vm.allClasses() to list all loaded classes, + // should avoid using nestedTypes for performance. + for (ReferenceType nestedType : refType.nestedTypes()) { + CompletableFuture> nestedLocationsFuture = collectLocations(nestedType, lineNumber); + List nestedLocations = nestedLocationsFuture.join(); + if (!nestedLocations.isEmpty()) { + return CompletableFuture.completedFuture(nestedLocations); } } - }); - } catch (VMDisconnectedException ex) { - // collect locations operation may be executing while JVM is terminating, thus the VMDisconnectedException may be - // possible, in case of VMDisconnectedException, this method will return an empty array which turns out a valid - // response in vscode, causing no error log in trace. + } + + return CompletableFuture.completedFuture(Collections.emptyList()); + }); + } + + private CompletableFuture> collectLocations(List refTypes, String methodName, String methodSiguature) { + List> futures = new ArrayList<>(); + for (ReferenceType refType : refTypes) { + if (async()) { + futures.add(AsyncJdwpUtils.supplyAsync(() -> findMethodLocaiton(refType, methodName, methodSiguature))); + } else { + futures.add(CompletableFuture.completedFuture(findMethodLocaiton(refType, methodName, methodSiguature))); + } } - return locations; + return AsyncJdwpUtils.all(futures); } - private List createBreakpointRequests(ReferenceType refType, int lineNumber, int hitCount, + private Location findMethodLocaiton(ReferenceType refType, String methodName, String methodSiguature) { + List methods = refType.methods(); + Location location = null; + for (Method method : methods) { + if (!method.isAbstract() && !method.isNative() + && methodName.equals(method.name()) + && (methodSiguature.equals(method.genericSignature()) || methodSiguature.equals(method.signature()) + || toNoneGeneric(methodSiguature).equals(method.signature()))) { + location = method.location(); + break; + } + } + + return location; + } + + static String toNoneGeneric(String genericSig) { + StringBuilder builder = new StringBuilder(); + boolean append = true; + int depth = 0; + char[] chars = genericSig.toCharArray(); + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (c == '<') { + depth++; + append = (depth == 0); + } + if (append) { + builder.append(c); + } + if (c == '>') { + depth--; + append = (depth == 0); + } + } + return builder.toString(); + } + + private List findLocaitonsOfLine(Method method, int lineNumber) { + try { + return method.locationsOfLine(lineNumber); + } catch (AbsentInformationException e) { + // could be AbsentInformationException or ClassNotPreparedException + // but both are expected so no need to further handle + } + + return Collections.emptyList(); + } + + private CompletableFuture> createBreakpointRequests(ReferenceType refType, int lineNumber, int hitCount, boolean includeNestedTypes) { - List refTypes = new ArrayList<>(); - refTypes.add(refType); - return createBreakpointRequests(refTypes, lineNumber, hitCount, includeNestedTypes); + return createBreakpointRequests(Arrays.asList(refType), lineNumber, hitCount, includeNestedTypes); } - private List createBreakpointRequests(List refTypes, int lineNumber, + private CompletableFuture> createBreakpointRequests(List refTypes, int lineNumber, int hitCount, boolean includeNestedTypes) { - List locations = collectLocations(refTypes, lineNumber, includeNestedTypes); + CompletableFuture> locationsFuture; + if (this.sourceLocation.methodName() != null) { + locationsFuture = collectLocations(refTypes, this.sourceLocation.methodName(), this.sourceLocation.methodSignature()); + } else { + locationsFuture = collectLocations(refTypes, lineNumber, includeNestedTypes).thenApply((locations) -> { + if (locations.isEmpty()) { + return locations; + } - // find out the existing breakpoint locations - List existingLocations = new ArrayList<>(requests.size()); - Observable.fromIterable(requests).filter(request -> request instanceof BreakpointRequest) - .map(request -> ((BreakpointRequest) request).location()).toList().subscribe(list -> { - existingLocations.addAll(list); - }); + /** + * For a line breakpoint, we default to breaking at the first location + * of the line. If you want to break at other locations on the same line, + * you can add an inline breakpoint based on the locations returned by + * the BreakpointLocation request. + */ + return Arrays.asList(locations.get(0)); + }); + } - // remove duplicated locations - List newLocations = new ArrayList<>(locations.size()); - Observable.fromIterable(locations).filter(location -> !existingLocations.contains(location)).toList().subscribe(list -> { - newLocations.addAll(list); - }); + return locationsFuture.thenCompose((locations) -> { + // find out the existing breakpoint locations + List existingLocations = new ArrayList<>(requests.size()); + Observable.fromIterable(requests).filter(request -> request instanceof BreakpointRequest) + .map(request -> ((BreakpointRequest) request).location()).toList().subscribe(list -> { + existingLocations.addAll(list); + }); + + // remove duplicated locations + List newLocations = new ArrayList<>(locations.size()); + Observable.fromIterable(locations).filter(location -> !existingLocations.contains(location)).toList().subscribe(list -> { + newLocations.addAll(list); + }); - List newRequests = new ArrayList<>(newLocations.size()); + List newRequests = new ArrayList<>(newLocations.size()); - newLocations.forEach(location -> { - try { + newLocations.forEach(location -> { BreakpointRequest request = vm.eventRequestManager().createBreakpointRequest(location); - request.setSuspendPolicy(BreakpointRequest.SUSPEND_EVENT_THREAD); + if ("SUSPEND_ALL".equals(getSuspendPolicy())) { + request.setSuspendPolicy(BreakpointRequest.SUSPEND_ALL); + } else { + request.setSuspendPolicy(BreakpointRequest.SUSPEND_EVENT_THREAD); + } if (hitCount > 0) { request.addCountFilter(hitCount); } - request.enable(); + request.putProperty(IBreakpoint.REQUEST_TYPE, computeRequestType()); newRequests.add(request); - } catch (VMDisconnectedException ex) { - // enable breakpoint operation may be executing while JVM is terminating, thus the VMDisconnectedException may be - // possible, in case of VMDisconnectedException, this method will return an empty array which turns out a valid - // response in vscode, causing no error log in trace. + }); + + List> futures = new ArrayList<>(); + for (BreakpointRequest request : newRequests) { + if (async()) { + futures.add(AsyncJdwpUtils.runAsync(() -> { + try { + request.enable(); + } catch (VMDisconnectedException ex) { + // enable breakpoint operation may be executing while JVM is terminating, thus the VMDisconnectedException may be + // possible, in case of VMDisconnectedException, this method will return an empty array which turns out a valid + // response in vscode, causing no error log in trace. + } + })); + } else { + try { + request.enable(); + } catch (VMDisconnectedException ex) { + // enable breakpoint operation may be executing while JVM is terminating, thus the VMDisconnectedException may be + // possible, in case of VMDisconnectedException, this method will return an empty array which turns out a valid + // response in vscode, causing no error log in trace. + } + } } + + return AsyncJdwpUtils.all(futures).thenApply((res) -> newRequests); }); + } - return newRequests; + private Object computeRequestType() { + if (this.sourceLocation.methodName() == null) { + return IBreakpoint.REQUEST_TYPE_LINE; + } + + if (this.sourceLocation.methodName().startsWith("lambda$")) { + return IBreakpoint.REQUEST_TYPE_LAMBDA; + } else { + return IBreakpoint.REQUEST_TYPE_METHOD; + } } @Override diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java index a56eaf695..d5b8ceb47 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSession.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,37 +11,84 @@ package com.microsoft.java.debug.core; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.StringUtils; + +import com.sun.jdi.ObjectCollectedException; +import com.sun.jdi.ReferenceType; import com.sun.jdi.ThreadReference; +import com.sun.jdi.VMDisconnectedException; import com.sun.jdi.VirtualMachine; +import com.sun.jdi.event.ClassPrepareEvent; +import com.sun.jdi.request.ClassPrepareRequest; import com.sun.jdi.request.EventRequest; import com.sun.jdi.request.EventRequestManager; import com.sun.jdi.request.ExceptionRequest; +import io.reactivex.disposables.Disposable; + public class DebugSession implements IDebugSession { private VirtualMachine vm; private EventHub eventHub = new EventHub(); + private List eventRequests = new ArrayList<>(); + private List subscriptions = new ArrayList<>(); + private final boolean suspendAllThreads; public DebugSession(VirtualMachine virtualMachine) { vm = virtualMachine; + // Capture suspend policy at session start - this persists for the session lifetime + this.suspendAllThreads = DebugSettings.getCurrent().suspendAllThreads; } @Override public void start() { + boolean supportsVirtualThreads = mayCreateVirtualThreads(); + // request thread events by default EventRequest threadStartRequest = vm.eventRequestManager().createThreadStartRequest(); threadStartRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE); + if (supportsVirtualThreads) { + addPlatformThreadsOnlyFilter(threadStartRequest); + } threadStartRequest.enable(); EventRequest threadDeathRequest = vm.eventRequestManager().createThreadDeathRequest(); threadDeathRequest.setSuspendPolicy(EventRequest.SUSPEND_NONE); + if (supportsVirtualThreads) { + addPlatformThreadsOnlyFilter(threadDeathRequest); + } threadDeathRequest.enable(); eventHub.start(vm); } + private boolean mayCreateVirtualThreads() { + try { + Method method = vm.getClass().getMethod("mayCreateVirtualThreads"); + return (boolean) method.invoke(vm); + } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + // ignore + } + + return false; + } + + /** + * For thread start and thread death events, restrict the events so they are only sent for platform threads. + */ + private void addPlatformThreadsOnlyFilter(EventRequest threadLifecycleRequest) { + try { + Method method = threadLifecycleRequest.getClass().getMethod("addPlatformThreadsOnlyFilter"); + method.invoke(threadLifecycleRequest); + } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + // ignore + } + } + @Override public void suspend() { vm.suspend(); @@ -57,8 +104,12 @@ public void resume() { * all threads fully. */ for (ThreadReference tr : DebugUtility.getAllThreadsSafely(this)) { - while (!tr.isCollected() && tr.suspendCount() > 1) { - tr.resume(); + try { + while (tr.suspendCount() > 1) { + tr.resume(); + } + } catch (ObjectCollectedException ex) { + // Skip it if the thread is garbage collected. } } vm.resume(); @@ -71,19 +122,26 @@ public void detach() { @Override public void terminate() { - if (vm.process() == null || vm.process().isAlive()) { + if (vm.process() != null && vm.process().isAlive()) { + vm.process().destroy(); + } else if (vm.process() == null || vm.process().isAlive()) { vm.exit(0); } } + @Override + public IBreakpoint createBreakpoint(JavaBreakpointLocation sourceLocation, int hitCount, String condition, String logMessage) { + return new EvaluatableBreakpoint(vm, this.getEventHub(), sourceLocation, hitCount, condition, logMessage, suspendAllThreads); + } + @Override public IBreakpoint createBreakpoint(String className, int lineNumber, int hitCount, String condition, String logMessage) { - return new EvaluatableBreakpoint(vm, this.getEventHub(), className, lineNumber, hitCount, condition, logMessage); + return new EvaluatableBreakpoint(vm, this.getEventHub(), className, lineNumber, hitCount, condition, logMessage, suspendAllThreads); } @Override public IWatchpoint createWatchPoint(String className, String fieldName, String accessType, String condition, int hitCount) { - return new Watchpoint(vm, this.getEventHub(), className, fieldName, accessType, condition, hitCount); + return new Watchpoint(vm, this.getEventHub(), className, fieldName, accessType, condition, hitCount, suspendAllThreads); } @Override @@ -93,9 +151,27 @@ public void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught @Override public void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught, String[] classFilters, String[] classExclusionFilters) { + setExceptionBreakpoints(notifyCaught, notifyUncaught, null, classFilters, classExclusionFilters); + } + + @Override + public void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught, String[] exceptionTypes, + String[] classFilters, String[] classExclusionFilters) { EventRequestManager manager = vm.eventRequestManager(); - ArrayList legacy = new ArrayList<>(manager.exceptionRequests()); - manager.deleteEventRequests(legacy); + + try { + ArrayList legacy = new ArrayList<>(manager.exceptionRequests()); + manager.deleteEventRequests(legacy); + manager.deleteEventRequests(eventRequests); + } catch (VMDisconnectedException ex) { + // ignore since removing breakpoints is meaningless when JVM is terminated. + } + subscriptions.forEach(subscription -> { + subscription.dispose(); + }); + subscriptions.clear(); + eventRequests.clear(); + // When no exception breakpoints are requested, no need to create an empty exception request. if (notifyCaught || notifyUncaught) { // from: https://www.javatips.net/api/REPLmode-master/src/jm/mode/replmode/REPLRunner.java @@ -110,20 +186,60 @@ public void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught // a thread to be available, and queries it by calling allThreads(). // See org.eclipse.debug.jdi.tests.AbstractJDITest for the example. - // get only the uncaught exceptions - ExceptionRequest request = manager.createExceptionRequest(null, notifyCaught, notifyUncaught); - request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); - if (classFilters != null) { - for (String classFilter : classFilters) { - request.addClassFilter(classFilter); + if (exceptionTypes == null || exceptionTypes.length == 0) { + ExceptionRequest request = manager.createExceptionRequest(null, notifyCaught, notifyUncaught); + request.setSuspendPolicy(suspendAllThreads ? EventRequest.SUSPEND_ALL : EventRequest.SUSPEND_EVENT_THREAD); + if (classFilters != null) { + for (String classFilter : classFilters) { + request.addClassFilter(classFilter); + } + } + if (classExclusionFilters != null) { + for (String exclusionFilter : classExclusionFilters) { + request.addClassExclusionFilter(exclusionFilter); + } } + request.enable(); + return; } - if (classExclusionFilters != null) { - for (String exclusionFilter : classExclusionFilters) { - request.addClassExclusionFilter(exclusionFilter); + + for (String exceptionType : exceptionTypes) { + if (StringUtils.isBlank(exceptionType)) { + continue; + } + + // register exception breakpoint in the future loaded classes. + ClassPrepareRequest classPrepareRequest = manager.createClassPrepareRequest(); + classPrepareRequest.addClassFilter(exceptionType); + classPrepareRequest.enable(); + eventRequests.add(classPrepareRequest); + + Disposable subscription = eventHub.events() + .filter(debugEvent -> debugEvent.event instanceof ClassPrepareEvent + && eventRequests.contains(debugEvent.event.request())) + .subscribe(debugEvent -> { + ClassPrepareEvent event = (ClassPrepareEvent) debugEvent.event; + createExceptionBreakpoint(event.referenceType(), notifyCaught, notifyUncaught, classFilters, classExclusionFilters); + }); + subscriptions.add(subscription); + + // register exception breakpoint in the loaded classes. + for (ReferenceType refType : vm.classesByName(exceptionType)) { + createExceptionBreakpoint(refType, notifyCaught, notifyUncaught, classFilters, classExclusionFilters); } } - request.enable(); + } + } + + @Override + public void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught, String[] exceptionTypes, + String[] classFilters, String[] classExclusionFilters, boolean async) { + if (async) { + AsyncJdwpUtils.runAsync(() -> { + setExceptionBreakpoints(notifyCaught, notifyUncaught, exceptionTypes, classFilters, classExclusionFilters); + }); + } else { + setExceptionBreakpoints(notifyCaught, notifyUncaught, exceptionTypes, classFilters, classExclusionFilters); } } @@ -146,4 +262,33 @@ public IEventHub getEventHub() { public VirtualMachine getVM() { return vm; } + + @Override + public boolean shouldSuspendAllThreads() { + return suspendAllThreads; + } + + @Override + public IMethodBreakpoint createFunctionBreakpoint(String className, String functionName, String condition, + int hitCount) { + return new MethodBreakpoint(vm, this.getEventHub(), className, functionName, condition, hitCount, suspendAllThreads); + } + + private void createExceptionBreakpoint(ReferenceType refType, boolean notifyCaught, boolean notifyUncaught, + String[] classFilters, String[] classExclusionFilters) { + EventRequestManager manager = vm.eventRequestManager(); + ExceptionRequest request = manager.createExceptionRequest(refType, notifyCaught, notifyUncaught); + request.setSuspendPolicy(suspendAllThreads ? EventRequest.SUSPEND_ALL : EventRequest.SUSPEND_EVENT_THREAD); + if (classFilters != null) { + for (String classFilter : classFilters) { + request.addClassFilter(classFilter); + } + } + if (classExclusionFilters != null) { + for (String exclusionFilter : classExclusionFilters) { + request.addClassExclusionFilter(exclusionFilter); + } + } + request.enable(); + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java index e3c928332..b422f6801 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugSettings.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2020 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,7 +19,7 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.annotations.SerializedName; import com.microsoft.java.debug.core.protocol.JsonUtils; -import com.microsoft.java.debug.core.protocol.Requests.ClassFilters; +import com.microsoft.java.debug.core.protocol.Requests.ExceptionFilters; import com.microsoft.java.debug.core.protocol.Requests.StepFilters; public final class DebugSettings { @@ -39,10 +39,13 @@ public final class DebugSettings { public String javaHome; public HotCodeReplace hotCodeReplace = HotCodeReplace.MANUAL; public StepFilters stepFilters = new StepFilters(); - public ClassFilters exceptionFilters = new ClassFilters(); + public ExceptionFilters exceptionFilters = new ExceptionFilters(); public boolean exceptionFiltersUpdated = false; public int limitOfVariablesPerJdwpRequest = 100; public int jdwpRequestTimeout = 3000; + public AsyncMode asyncJDWP = AsyncMode.OFF; + public Switch debugSupportOnDecompiledSource = Switch.OFF; + public boolean suspendAllThreads = false; public static DebugSettings getCurrent() { return current; @@ -87,6 +90,22 @@ public static enum HotCodeReplace { NEVER } + public static enum AsyncMode { + @SerializedName("auto") + AUTO, + @SerializedName("on") + ON, + @SerializedName("off") + OFF + } + + public static enum Switch { + @SerializedName("on") + ON, + @SerializedName("off") + OFF + } + public static interface IDebugSettingChangeListener { public void update(DebugSettings oldSettings, DebugSettings newSettings); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java index 1202a30f3..8a31792f0 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/DebugUtility.java @@ -394,6 +394,7 @@ private static StepRequest createStepRequest(ThreadReference thread, int stepSiz request.addClassExclusionFilter(exclusionFilter); } } + // Note: suspend policy will be set by the caller (StepRequestHandler) based on session settings request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); request.addCountFilter(1); @@ -415,7 +416,7 @@ public static CompletableFuture stopOnEntry(IDebugSession debugSession, St EventRequestManager manager = debugSession.getVM().eventRequestManager(); MethodEntryRequest request = manager.createMethodEntryRequest(); request.addClassFilter(mainClass); - request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); + request.setSuspendPolicy(debugSession.shouldSuspendAllThreads() ? EventRequest.SUSPEND_ALL : EventRequest.SUSPEND_EVENT_THREAD); debugSession.getEventHub().events().filter(debugEvent -> { return debugEvent.event instanceof MethodEntryEvent && request.equals(debugEvent.event.request()); @@ -444,7 +445,7 @@ public static CompletableFuture stopOnEntry(IDebugSession debugSession, St */ public static ThreadReference getThread(IDebugSession debugSession, long threadId) { for (ThreadReference thread : getAllThreadsSafely(debugSession)) { - if (thread.uniqueID() == threadId && !thread.isCollected()) { + if (thread.uniqueID() == threadId) { return thread; } } @@ -477,7 +478,7 @@ public static List getAllThreadsSafely(IDebugSession debugSessi */ public static void resumeThread(ThreadReference thread) { // if thread is not found or is garbage collected, do nothing - if (thread == null || thread.isCollected()) { + if (thread == null) { return; } try { @@ -495,6 +496,25 @@ public static void resumeThread(ThreadReference thread) { } } + public static void resumeThread(ThreadReference thread, int resumeCount) { + if (thread == null) { + return; + } + + try { + for (int i = 0; i < resumeCount; i++) { + /** + * Invoking this method will decrement the count of pending suspends on this thread. + * If it is decremented to 0, the thread will continue to execute. + */ + thread.resume(); + } + } catch (ObjectCollectedException ex) { + // ObjectCollectionException can be thrown if the thread has already completed (exited) in the VM when calling suspendCount, + // the resume operation to this thread is meanness. + } + } + /** * Remove the event request from the vm. If the vm has terminated, do nothing. * @param eventManager diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/EvaluatableBreakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/EvaluatableBreakpoint.java index 1a0647411..d5a909f0b 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/EvaluatableBreakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/EvaluatableBreakpoint.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2018 Microsoft Corporation and others. +* Copyright (c) 2018-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,15 +11,15 @@ package com.microsoft.java.debug.core; -import org.apache.commons.lang3.StringUtils; - import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import com.sun.jdi.event.ThreadDeathEvent; +import org.apache.commons.lang3.StringUtils; + import com.sun.jdi.ThreadReference; import com.sun.jdi.VirtualMachine; +import com.sun.jdi.event.ThreadDeathEvent; import io.reactivex.disposables.Disposable; @@ -29,22 +29,28 @@ public class EvaluatableBreakpoint extends Breakpoint implements IEvaluatableBre private Object compiledLogpointExpression = null; private Map compiledExpressions = new ConcurrentHashMap<>(); - EvaluatableBreakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber) { - this(vm, eventHub, className, lineNumber, 0, null); + EvaluatableBreakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, boolean suspendAllThreads) { + this(vm, eventHub, className, lineNumber, 0, null, suspendAllThreads); } - EvaluatableBreakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount) { - this(vm, eventHub, className, lineNumber, hitCount, null); + EvaluatableBreakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, boolean suspendAllThreads) { + this(vm, eventHub, className, lineNumber, hitCount, null, suspendAllThreads); } EvaluatableBreakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, - String condition) { - this(vm, eventHub, className, lineNumber, hitCount, condition, null); + String condition, boolean suspendAllThreads) { + this(vm, eventHub, className, lineNumber, hitCount, condition, null, suspendAllThreads); } EvaluatableBreakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, - String condition, String logMessage) { - super(vm, eventHub, className, lineNumber, hitCount, condition, logMessage); + String condition, String logMessage, boolean suspendAllThreads) { + super(vm, eventHub, className, lineNumber, hitCount, condition, logMessage, suspendAllThreads); + this.eventHub = eventHub; + } + + EvaluatableBreakpoint(VirtualMachine vm, IEventHub eventHub, JavaBreakpointLocation sourceLocation, int hitCount, + String condition, String logMessage, boolean suspendAllThreads) { + super(vm, eventHub, sourceLocation, hitCount, condition, logMessage, suspendAllThreads); this.eventHub = eventHub; } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java index bb9549635..ec3ea818a 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IBreakpoint.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -14,10 +14,23 @@ import java.util.concurrent.CompletableFuture; public interface IBreakpoint extends IDebugResource { + + String REQUEST_TYPE = "request_type"; + + int REQUEST_TYPE_LINE = 0; + + int REQUEST_TYPE_METHOD = 1; + + int REQUEST_TYPE_LAMBDA = 2; + + JavaBreakpointLocation sourceLocation(); + String className(); int getLineNumber(); + int getColumnNumber(); + int getHitCount(); void setHitCount(int hitCount); @@ -35,4 +48,18 @@ public interface IBreakpoint extends IDebugResource { String getLogMessage(); void setLogMessage(String logMessage); + + default void setAsync(boolean async) { + } + + default boolean async() { + return false; + } + + default void setSuspendPolicy(String policy) { + } + + default String getSuspendPolicy() { + return null; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java index 24138fef1..afc2283f0 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -30,13 +30,20 @@ public interface IDebugSession { // breakpoints IBreakpoint createBreakpoint(String className, int lineNumber, int hitCount, String condition, String logMessage); + IBreakpoint createBreakpoint(JavaBreakpointLocation sourceLocation, int hitCount, String condition, String logMessage); + IWatchpoint createWatchPoint(String className, String fieldName, String accessType, String condition, int hitCount); void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught); void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught, String[] classFilters, String[] classExclusionFilters); - // TODO: createFunctionBreakpoint + void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught, String[] exceptionTypes, String[] classFilters, String[] classExclusionFilters); + + void setExceptionBreakpoints(boolean notifyCaught, boolean notifyUncaught, String[] exceptionTypes, String[] classFilters, String[] classExclusionFilters, + boolean async); + + IMethodBreakpoint createFunctionBreakpoint(String className, String functionName, String condition, int hitCount); Process process(); @@ -45,4 +52,10 @@ public interface IDebugSession { IEventHub getEventHub(); VirtualMachine getVM(); + + /** + * Returns whether breakpoints should suspend all threads or just the event thread. + * This value is captured at session start and persists for the session lifetime. + */ + boolean shouldSuspendAllThreads(); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IMethodBreakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IMethodBreakpoint.java new file mode 100644 index 000000000..68f9539c8 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IMethodBreakpoint.java @@ -0,0 +1,40 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Gayan Perera - initial API and implementation +*******************************************************************************/ +package com.microsoft.java.debug.core; + +import java.util.concurrent.CompletableFuture; + +public interface IMethodBreakpoint extends IDebugResource { + String methodName(); + + String className(); + + int getHitCount(); + + String getCondition(); + + void setHitCount(int hitCount); + + void setCondition(String condition); + + CompletableFuture install(); + + Object getProperty(Object key); + + void putProperty(Object key, Object value); + + default void setAsync(boolean async) { + } + + default boolean async() { + return false; + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/JavaBreakpointLocation.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/JavaBreakpointLocation.java new file mode 100644 index 000000000..3dbc1a24d --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/JavaBreakpointLocation.java @@ -0,0 +1,125 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core; + +import java.util.Objects; + +import com.microsoft.java.debug.core.protocol.Types; + +public class JavaBreakpointLocation { + /** + * The line number in the source file. + */ + private int lineNumberInSourceFile = Integer.MIN_VALUE; + /** + * The line number in the class file. + */ + private int lineNumber; + /** + * The source column of the breakpoint. + */ + private int columnNumber = -1; + /** + * The declaring class name that encloses the target position. + */ + private String className; + /** + * The method name and signature when the target position + * points to a method declaration. + */ + private String methodName; + private String methodSignature; + /** + * All possible locations for source breakpoints in a given range. + */ + private Types.BreakpointLocation[] availableBreakpointLocations = new Types.BreakpointLocation[0]; + + public JavaBreakpointLocation(int lineNumber, int columnNumber) { + this.lineNumber = lineNumber; + this.columnNumber = columnNumber; + } + + @Override + public int hashCode() { + return Objects.hash(lineNumber, columnNumber, className, methodName, methodSignature); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof JavaBreakpointLocation)) { + return false; + } + JavaBreakpointLocation other = (JavaBreakpointLocation) obj; + return lineNumber == other.lineNumber && columnNumber == other.columnNumber + && Objects.equals(className, other.className) && Objects.equals(methodName, other.methodName) + && Objects.equals(methodSignature, other.methodSignature); + } + + public int lineNumber() { + return lineNumber; + } + + public void setLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + } + + public int columnNumber() { + return columnNumber; + } + + public void setColumnNumber(int columnNumber) { + this.columnNumber = columnNumber; + } + + public String className() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String methodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public String methodSignature() { + return methodSignature; + } + + public void setMethodSignature(String methodSignature) { + this.methodSignature = methodSignature; + } + + public Types.BreakpointLocation[] availableBreakpointLocations() { + return availableBreakpointLocations; + } + + public void setAvailableBreakpointLocations(Types.BreakpointLocation[] availableBreakpointLocations) { + this.availableBreakpointLocations = availableBreakpointLocations; + } + + public int lineNumberInSourceFile() { + return lineNumberInSourceFile == Integer.MIN_VALUE ? lineNumber : lineNumberInSourceFile; + } + + public void setLineNumberInSourceFile(int lineNumberInSourceFile) { + this.lineNumberInSourceFile = lineNumberInSourceFile; + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/MethodBreakpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/MethodBreakpoint.java new file mode 100644 index 000000000..bc46fd96f --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/MethodBreakpoint.java @@ -0,0 +1,296 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Gayan Perera - initial API and implementation +*******************************************************************************/ +package com.microsoft.java.debug.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.lang3.StringUtils; + +import com.sun.jdi.ReferenceType; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.VMDisconnectedException; +import com.sun.jdi.VirtualMachine; +import com.sun.jdi.event.ClassPrepareEvent; +import com.sun.jdi.event.ThreadDeathEvent; +import com.sun.jdi.request.ClassPrepareRequest; +import com.sun.jdi.request.EventRequest; +import com.sun.jdi.request.MethodEntryRequest; + +import io.reactivex.Observable; +import io.reactivex.disposables.Disposable; + +public class MethodBreakpoint implements IMethodBreakpoint, IEvaluatableBreakpoint { + + private VirtualMachine vm; + private IEventHub eventHub; + private String className; + private String functionName; + private String condition; + private int hitCount; + private boolean async = false; + private final boolean suspendAllThreads; + + private HashMap propertyMap = new HashMap<>(); + private Object compiledConditionalExpression = null; + private Map compiledExpressions = new ConcurrentHashMap<>(); + + private List requests = Collections.synchronizedList(new ArrayList<>()); + private List subscriptions = new ArrayList<>(); + + public MethodBreakpoint(VirtualMachine vm, IEventHub eventHub, String className, String functionName, + String condition, int hitCount, boolean suspendAllThreads) { + Objects.requireNonNull(vm); + Objects.requireNonNull(eventHub); + Objects.requireNonNull(className); + Objects.requireNonNull(functionName); + this.vm = vm; + this.eventHub = eventHub; + this.className = className; + this.functionName = functionName; + this.condition = condition; + this.hitCount = hitCount; + this.suspendAllThreads = suspendAllThreads; + } + + @Override + public List requests() { + return requests; + } + + @Override + public List subscriptions() { + return subscriptions; + } + + @Override + public void close() throws Exception { + try { + vm.eventRequestManager().deleteEventRequests(requests()); + } catch (VMDisconnectedException ex) { + // ignore since removing breakpoints is meaningless when JVM is terminated. + } + subscriptions().forEach(Disposable::dispose); + requests.clear(); + subscriptions.clear(); + } + + @Override + public boolean containsEvaluatableExpression() { + return containsConditionalExpression() || containsLogpointExpression(); + } + + @Override + public boolean containsConditionalExpression() { + return StringUtils.isNotBlank(getCondition()); + } + + @Override + public boolean containsLogpointExpression() { + return false; + } + + @Override + public String getCondition() { + return condition; + } + + @Override + public void setCondition(String condition) { + this.condition = condition; + setCompiledConditionalExpression(null); + compiledExpressions.clear(); + } + + @Override + public String getLogMessage() { + return null; + } + + @Override + public void setLogMessage(String logMessage) { + // for future implementation + } + + @Override + public void setCompiledConditionalExpression(Object compiledExpression) { + this.compiledConditionalExpression = compiledExpression; + } + + @Override + public Object getCompiledConditionalExpression() { + return compiledConditionalExpression; + } + + @Override + public void setCompiledLogpointExpression(Object compiledExpression) { + // for future implementation + } + + @Override + public Object getCompiledLogpointExpression() { + return null; + } + + @Override + public void setCompiledExpression(long threadId, Object compiledExpression) { + compiledExpressions.put(threadId, compiledExpression); + } + + @Override + public Object getCompiledExpression(long threadId) { + return compiledExpressions.get(threadId); + } + + @Override + public int getHitCount() { + return hitCount; + } + + @Override + public void setHitCount(int hitCount) { + this.hitCount = hitCount; + Observable.fromIterable(this.requests()) + .filter(request -> request instanceof MethodEntryRequest) + .subscribe(request -> { + request.addCountFilter(hitCount); + request.enable(); + }); + } + + @Override + public boolean async() { + return this.async; + } + + @Override + public void setAsync(boolean async) { + this.async = async; + } + + @Override + public CompletableFuture install() { + Disposable subscription = eventHub.events() + .filter(debugEvent -> debugEvent.event instanceof ThreadDeathEvent) + .subscribe(debugEvent -> { + ThreadReference deathThread = ((ThreadDeathEvent) debugEvent.event).thread(); + compiledExpressions.remove(deathThread.uniqueID()); + }); + + subscriptions.add(subscription); + + // It's possible that different class loaders create new class with the same + // name. + // Here to listen to future class prepare events to handle such case. + ClassPrepareRequest classPrepareRequest = vm.eventRequestManager().createClassPrepareRequest(); + classPrepareRequest.addClassFilter(className); + classPrepareRequest.enable(); + requests.add(classPrepareRequest); + + CompletableFuture future = new CompletableFuture<>(); + subscription = eventHub.events() + .filter(debugEvent -> debugEvent.event instanceof ClassPrepareEvent + && (classPrepareRequest.equals(debugEvent.event.request()))) + .subscribe(debugEvent -> { + ClassPrepareEvent event = (ClassPrepareEvent) debugEvent.event; + Optional createdRequest = AsyncJdwpUtils.await( + createMethodEntryRequest(event.referenceType()) + ); + if (createdRequest.isPresent()) { + MethodEntryRequest methodEntryRequest = createdRequest.get(); + requests.add(methodEntryRequest); + if (!future.isDone()) { + this.putProperty("verified", true); + future.complete(this); + } + } + }); + subscriptions.add(subscription); + + Runnable createRequestsFromLoadedClasses = () -> { + List types = vm.classesByName(className); + for (ReferenceType type : types) { + createMethodEntryRequest(type).whenComplete((createdRequest, ex) -> { + if (ex != null) { + return; + } + + if (createdRequest.isPresent()) { + MethodEntryRequest methodEntryRequest = createdRequest.get(); + requests.add(methodEntryRequest); + if (!future.isDone()) { + this.putProperty("verified", true); + future.complete(this); + } + } + }); + } + }; + + if (async()) { + AsyncJdwpUtils.runAsync(createRequestsFromLoadedClasses); + } else { + createRequestsFromLoadedClasses.run(); + } + + return future; + } + + private CompletableFuture> createMethodEntryRequest(ReferenceType type) { + if (async()) { + return CompletableFuture.supplyAsync(() -> createMethodEntryRequest0(type)); + } else { + return CompletableFuture.completedFuture(createMethodEntryRequest0(type)); + } + } + + private Optional createMethodEntryRequest0(ReferenceType type) { + return type.methodsByName(functionName).stream().findFirst().map(method -> { + MethodEntryRequest request = vm.eventRequestManager().createMethodEntryRequest(); + + request.addClassFilter(type); + request.setSuspendPolicy(suspendAllThreads ? EventRequest.SUSPEND_ALL : EventRequest.SUSPEND_EVENT_THREAD); + if (hitCount > 0) { + request.addCountFilter(hitCount); + } + request.enable(); + return request; + }); + } + + @Override + public Object getProperty(Object key) { + return propertyMap.get(key); + } + + @Override + public void putProperty(Object key, Object value) { + propertyMap.put(key, value); + } + + @Override + public String methodName() { + return functionName; + } + + @Override + public String className() { + return className; + } + +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/UsageDataSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/UsageDataSession.java index e54727b66..b645e2843 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/UsageDataSession.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/UsageDataSession.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,6 +12,7 @@ package com.microsoft.java.debug.core; import java.util.ArrayList; +import java.util.Formatter; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,6 +33,7 @@ public class UsageDataSession { private static final Logger usageDataLogger = Logger.getLogger(Configuration.USAGE_DATA_LOGGER_NAME); private static final long RESPONSE_MAX_DELAY_MS = 1000; private static final ThreadLocal threadLocal = new InheritableThreadLocal<>(); + private static final boolean TRACE_DAP_PERF = Boolean.getBoolean("debug.dap.perf"); private final String sessionGuid = UUID.randomUUID().toString(); private boolean jdiEventSequenceEnabled = false; @@ -43,6 +45,7 @@ public class UsageDataSession { private Map userErrorCount = new HashMap<>(); private Map commandPerfCountMap = new HashMap<>(); private List eventList = new ArrayList<>(); + private List dapPerf = new ArrayList<>(); public static String getSessionGuid() { return threadLocal.get() == null ? "" : threadLocal.get().sessionGuid; @@ -117,6 +120,12 @@ public void recordResponse(Response response) { long duration = responseMillis - requestMillis; commandPerfCountMap.compute(command, (k, v) -> (v == null ? 0 : v.intValue()) + (int) duration); + if (TRACE_DAP_PERF) { + synchronized (dapPerf) { + dapPerf.add(new String[]{command, String.valueOf(duration)}); + } + } + if (!response.success || duration > RESPONSE_MAX_DELAY_MS) { Map props = new HashMap<>(); props.put("duration", duration); @@ -148,6 +157,18 @@ public void submitUsageData() { } } usageDataLogger.log(Level.INFO, "session usage data summary", props); + + if (TRACE_DAP_PERF) { + Formatter fmt = new Formatter(); + fmt.format("\nDAP Performance Metrics:\n"); + fmt.format("%30s %10s(ms)\n", "Request", "Duration"); + synchronized (dapPerf) { + dapPerf.forEach((event) -> { + fmt.format("%30s %14s\n", event[0], event[1]); + }); + } + logger.info(String.valueOf(fmt)); + } } /** @@ -169,6 +190,16 @@ public static void recordEvent(Event event) { } } + public static void recordInfo(String key, Object value) { + Map map = new HashMap<>(); + map.put(key, value); + usageDataLogger.log(Level.INFO, "session info", map); + } + + public static void recordInfo(String description, Map data) { + usageDataLogger.log(Level.INFO, description, data); + } + /** * Record counts for each user errors encountered. */ diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Watchpoint.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Watchpoint.java index 3de321ec8..fdb2354a9 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Watchpoint.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/Watchpoint.java @@ -46,20 +46,22 @@ public class Watchpoint implements IWatchpoint, IEvaluatableBreakpoint { private HashMap propertyMap = new HashMap<>(); private Object compiledConditionalExpression = null; private Map compiledExpressions = new ConcurrentHashMap<>(); + private final boolean suspendAllThreads; // IDebugResource private List requests = new ArrayList<>(); private List subscriptions = new ArrayList<>(); - Watchpoint(VirtualMachine vm, IEventHub eventHub, String className, String fieldName) { - this(vm, eventHub, className, fieldName, "write"); + Watchpoint(VirtualMachine vm, IEventHub eventHub, String className, String fieldName, boolean suspendAllThreads) { + this(vm, eventHub, className, fieldName, "write", suspendAllThreads); } - Watchpoint(VirtualMachine vm, IEventHub eventHub, String className, String fieldName, String accessType) { - this(vm, eventHub, className, fieldName, accessType, null, 0); + Watchpoint(VirtualMachine vm, IEventHub eventHub, String className, String fieldName, String accessType, boolean suspendAllThreads) { + this(vm, eventHub, className, fieldName, accessType, null, 0, suspendAllThreads); } - Watchpoint(VirtualMachine vm, IEventHub eventHub, String className, String fieldName, String accessType, String condition, int hitCount) { + Watchpoint(VirtualMachine vm, IEventHub eventHub, String className, String fieldName, String accessType, + String condition, int hitCount, boolean suspendAllThreads) { Objects.requireNonNull(vm); Objects.requireNonNull(eventHub); Objects.requireNonNull(className); @@ -71,6 +73,7 @@ public class Watchpoint implements IWatchpoint, IEvaluatableBreakpoint { this.accessType = accessType; this.condition = condition; this.hitCount = hitCount; + this.suspendAllThreads = suspendAllThreads; } @Override @@ -212,7 +215,7 @@ private List createWatchpointRequests(ReferenceType type) { } watchpointRequests.forEach(request -> { - request.setSuspendPolicy(WatchpointRequest.SUSPEND_EVENT_THREAD); + request.setSuspendPolicy(suspendAllThreads ? EventRequest.SUSPEND_ALL : EventRequest.SUSPEND_EVENT_THREAD); if (hitCount > 0) { request.addCountFilter(hitCount); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/AdapterUtils.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/AdapterUtils.java index 74d3fb292..c30aa8742 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/AdapterUtils.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/AdapterUtils.java @@ -111,6 +111,26 @@ public static int convertLineNumber(int line, boolean sourceLinesStartAt1, boole } } + /** + * Convert the source platform's column number to the target platform's column + * number. + * + * @param column + * the column number from the source platform + * @param sourceColumnsStartAt1 + * the source platform's column starts at 1 or not + * @param targetColumnStartAt1 + * the target platform's column starts at 1 or not + * @return the new column number + */ + public static int convertColumnNumber(int column, boolean sourceColumnsStartAt1, boolean targetColumnStartAt1) { + if (sourceColumnsStartAt1) { + return targetColumnStartAt1 ? column : column - 1; + } else { + return targetColumnStartAt1 ? column + 1 : column; + } + } + /** * Convert the source platform's path format to the target platform's path format. * @@ -290,4 +310,58 @@ public static String decodeURIComponent(String uri) { return uri; } } + + /** + * Find the mapped lines based on the given line number. + * + * The line mappings format is as follows: + * - [i]: key + * - [i+1]: value + */ + public static int[] binarySearchMappedLines(int[] lineMappings, int targetLine) { + if (lineMappings == null || lineMappings.length == 0 || lineMappings.length % 2 != 0) { + return null; + } + + final int MAX = lineMappings.length / 2 - 1; + int low = 0; + int high = MAX; + int found = -1; + while (low <= high) { + int mid = low + (high - low) / 2; + int actualMid = mid * 2; + if (lineMappings[actualMid] == targetLine) { + found = mid; + break; + } + + if (lineMappings[actualMid] < targetLine) { + low = mid + 1; + } else { + high = mid - 1; + } + } + + if (found == -1) { + return null; + } + + // Find the duplicates in the sorted array + int left = found; + while ((left - 1) >= 0 && lineMappings[(left - 1) * 2] == targetLine) { + left--; + } + + int right = found; + while ((right + 1) <= MAX && lineMappings[(right + 1) * 2] == targetLine) { + right++; + } + + int[] values = new int[right - left + 1]; + for (int i = 0; i < values.length; i++) { + values[i] = lineMappings[(left + i) * 2 + 1]; + } + + return values; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/BreakpointManager.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/BreakpointManager.java index 78898df73..eaf1bb56f 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/BreakpointManager.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/BreakpointManager.java @@ -25,6 +25,7 @@ import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.IBreakpoint; +import com.microsoft.java.debug.core.IMethodBreakpoint; import com.microsoft.java.debug.core.IWatchpoint; public class BreakpointManager implements IBreakpointManager { @@ -35,6 +36,7 @@ public class BreakpointManager implements IBreakpointManager { private List breakpoints; private Map> sourceToBreakpoints; private Map watchpoints; + private Map methodBreakpoints; private AtomicInteger nextBreakpointId = new AtomicInteger(1); /** @@ -44,6 +46,7 @@ public BreakpointManager() { this.breakpoints = Collections.synchronizedList(new ArrayList<>(5)); this.sourceToBreakpoints = new HashMap<>(); this.watchpoints = new HashMap<>(); + this.methodBreakpoints = new HashMap<>(); } @Override @@ -76,12 +79,12 @@ public IBreakpoint[] setBreakpoints(String source, IBreakpoint[] breakpoints, bo // Compute the breakpoints that are newly added. List toAdd = new ArrayList<>(); - List visitedLineNumbers = new ArrayList<>(); + List visitedBreakpoints = new ArrayList<>(); for (IBreakpoint breakpoint : breakpoints) { - IBreakpoint existed = breakpointMap.get(String.valueOf(breakpoint.getLineNumber())); + IBreakpoint existed = breakpointMap.get(String.valueOf(breakpoint.hashCode())); if (existed != null) { result.add(existed); - visitedLineNumbers.add(existed.getLineNumber()); + visitedBreakpoints.add(existed.hashCode()); continue; } else { result.add(breakpoint); @@ -92,7 +95,7 @@ public IBreakpoint[] setBreakpoints(String source, IBreakpoint[] breakpoints, bo // Compute the breakpoints that are no longer listed. List toRemove = new ArrayList<>(); for (IBreakpoint breakpoint : breakpointMap.values()) { - if (!visitedLineNumbers.contains(breakpoint.getLineNumber())) { + if (!visitedBreakpoints.contains(breakpoint.hashCode())) { toRemove.add(breakpoint); } } @@ -110,7 +113,7 @@ private void addBreakpointsInternally(String source, IBreakpoint[] breakpoints) for (IBreakpoint breakpoint : breakpoints) { breakpoint.putProperty("id", this.nextBreakpointId.getAndIncrement()); this.breakpoints.add(breakpoint); - breakpointMap.put(String.valueOf(breakpoint.getLineNumber()), breakpoint); + breakpointMap.put(String.valueOf(breakpoint.hashCode()), breakpoint); } } } @@ -130,7 +133,7 @@ private void removeBreakpointsInternally(String source, IBreakpoint[] breakpoint // Destroy the breakpoint on the debugee VM. breakpoint.close(); this.breakpoints.remove(breakpoint); - breakpointMap.remove(String.valueOf(breakpoint.getLineNumber())); + breakpointMap.remove(String.valueOf(breakpoint.hashCode())); } catch (Exception e) { logger.log(Level.SEVERE, String.format("Remove breakpoint exception: %s", e.toString()), e); } @@ -208,4 +211,61 @@ private String getWatchpointKey(IWatchpoint watchpoint) { public IWatchpoint[] getWatchpoints() { return this.watchpoints.values().stream().filter(wp -> wp != null).toArray(IWatchpoint[]::new); } + + @Override + public IMethodBreakpoint[] getMethodBreakpoints() { + return this.methodBreakpoints.values().stream().filter(Objects::nonNull).toArray(IMethodBreakpoint[]::new); + } + + @Override + public IMethodBreakpoint[] setMethodBreakpoints(IMethodBreakpoint[] breakpoints) { + List result = new ArrayList<>(); + List toAdds = new ArrayList<>(); + List toRemoves = new ArrayList<>(); + + Set visitedKeys = new HashSet<>(); + for (IMethodBreakpoint change : breakpoints) { + if (change == null) { + result.add(change); + continue; + } + + String key = getMethodBreakpointKey(change); + IMethodBreakpoint cache = methodBreakpoints.get(key); + if (cache != null) { + visitedKeys.add(key); + result.add(cache); + } else { + toAdds.add(change); + result.add(change); + } + } + + for (IMethodBreakpoint cache : methodBreakpoints.values()) { + if (!visitedKeys.contains(getMethodBreakpointKey(cache))) { + toRemoves.add(cache); + } + } + + for (IMethodBreakpoint toRemove : toRemoves) { + try { + // Destroy the method breakpoint on the debugee VM. + toRemove.close(); + this.methodBreakpoints.remove(getMethodBreakpointKey(toRemove)); + } catch (Exception e) { + logger.log(Level.SEVERE, String.format("Remove the method breakpoint exception: %s", e.toString()), e); + } + } + + for (IMethodBreakpoint toAdd : toAdds) { + toAdd.putProperty("id", this.nextBreakpointId.getAndIncrement()); + this.methodBreakpoints.put(getMethodBreakpointKey(toAdd), toAdd); + } + + return result.toArray(new IMethodBreakpoint[0]); + } + + private String getMethodBreakpointKey(IMethodBreakpoint breakpoint) { + return breakpoint.className() + "#" + breakpoint.methodName(); + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java index afdb5759c..8f595151f 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapter.java @@ -21,6 +21,7 @@ import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.adapter.handler.AttachRequestHandler; +import com.microsoft.java.debug.core.adapter.handler.BreakpointLocationsRequestHander; import com.microsoft.java.debug.core.adapter.handler.CompletionsHandler; import com.microsoft.java.debug.core.adapter.handler.ConfigurationDoneRequestHandler; import com.microsoft.java.debug.core.adapter.handler.DataBreakpointInfoRequestHandler; @@ -33,15 +34,18 @@ import com.microsoft.java.debug.core.adapter.handler.InlineValuesRequestHandler; import com.microsoft.java.debug.core.adapter.handler.LaunchRequestHandler; import com.microsoft.java.debug.core.adapter.handler.ProcessIdHandler; +import com.microsoft.java.debug.core.adapter.handler.RefreshFramesHandler; import com.microsoft.java.debug.core.adapter.handler.RefreshVariablesHandler; import com.microsoft.java.debug.core.adapter.handler.RestartFrameHandler; import com.microsoft.java.debug.core.adapter.handler.ScopesRequestHandler; import com.microsoft.java.debug.core.adapter.handler.SetBreakpointsRequestHandler; import com.microsoft.java.debug.core.adapter.handler.SetDataBreakpointsRequestHandler; import com.microsoft.java.debug.core.adapter.handler.SetExceptionBreakpointsRequestHandler; +import com.microsoft.java.debug.core.adapter.handler.SetFunctionBreakpointsRequestHandler; import com.microsoft.java.debug.core.adapter.handler.SetVariableRequestHandler; import com.microsoft.java.debug.core.adapter.handler.SourceRequestHandler; import com.microsoft.java.debug.core.adapter.handler.StackTraceRequestHandler; +import com.microsoft.java.debug.core.adapter.handler.StepInTargetsRequestHandler; import com.microsoft.java.debug.core.adapter.handler.StepRequestHandler; import com.microsoft.java.debug.core.adapter.handler.ThreadsRequestHandler; import com.microsoft.java.debug.core.adapter.handler.VariablesRequestHandler; @@ -98,13 +102,13 @@ public CompletableFuture dispatchRequest(Messages.Request req } } - private void initialize() { + protected void initialize() { // Register request handlers. // When there are multiple handlers registered for the same request, follow the rule "first register, first execute". registerHandler(new InitializeRequestHandler()); registerHandler(new LaunchRequestHandler()); - // DEBUG node only + // DEBUG mode only registerHandlerForDebug(new AttachRequestHandler()); registerHandlerForDebug(new ConfigurationDoneRequestHandler()); registerHandlerForDebug(new DisconnectRequestHandler()); @@ -127,21 +131,25 @@ private void initialize() { registerHandlerForDebug(new InlineValuesRequestHandler()); registerHandlerForDebug(new RefreshVariablesHandler()); registerHandlerForDebug(new ProcessIdHandler()); + registerHandlerForDebug(new SetFunctionBreakpointsRequestHandler()); + registerHandlerForDebug(new BreakpointLocationsRequestHander()); + registerHandlerForDebug(new StepInTargetsRequestHandler()); + registerHandlerForDebug(new RefreshFramesHandler()); // NO_DEBUG mode only registerHandlerForNoDebug(new DisconnectRequestWithoutDebuggingHandler()); registerHandlerForNoDebug(new ProcessIdHandler()); } - private void registerHandlerForDebug(IDebugRequestHandler handler) { + protected void registerHandlerForDebug(IDebugRequestHandler handler) { registerHandler(requestHandlersForDebug, handler); } - private void registerHandlerForNoDebug(IDebugRequestHandler handler) { + protected void registerHandlerForNoDebug(IDebugRequestHandler handler) { registerHandler(requestHandlersForNoDebug, handler); } - private void registerHandler(IDebugRequestHandler handler) { + protected void registerHandler(IDebugRequestHandler handler) { registerHandler(requestHandlersForDebug, handler); registerHandler(requestHandlersForNoDebug, handler); } @@ -157,4 +165,5 @@ private void registerHandler(Map> requestHan handlerList.add(handler); } } + } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java index 30fb12200..bbf215c67 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/DebugAdapterContext.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2020 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -21,6 +21,7 @@ import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.IDebugSession; +import com.microsoft.java.debug.core.DebugSettings.AsyncMode; import com.microsoft.java.debug.core.adapter.variables.IVariableFormatter; import com.microsoft.java.debug.core.adapter.variables.VariableFormatterFactory; import com.microsoft.java.debug.core.protocol.IProtocolServer; @@ -31,12 +32,14 @@ public class DebugAdapterContext implements IDebugAdapterContext { private static final int MAX_CACHE_ITEMS = 10000; private final StepFilters defaultFilters = new StepFilters(); - private Map sourceMappingCache = Collections.synchronizedMap(new LRUCache<>(MAX_CACHE_ITEMS)); + private Map sourceMappingCache = Collections.synchronizedMap(new LRUCache<>(MAX_CACHE_ITEMS)); private IProviderContext providerContext; private IProtocolServer server; private IDebugSession debugSession; private boolean debuggerLinesStartAt1 = true; + // The Java model on debugger uses 0-based column number. + private boolean debuggerColumnStartAt1 = false; private boolean debuggerPathsAreUri = true; private boolean clientLinesStartAt1 = true; private boolean clientColumnsStartAt1 = true; @@ -53,10 +56,14 @@ public class DebugAdapterContext implements IDebugAdapterContext { private StepFilters stepFilters; private Path classpathJar = null; private Path argsfile = null; + private boolean isInitialized = false; private long shellProcessId = -1; private long processId = -1; + private boolean localDebugging = true; + private long jdwpLatency = 0; + private IdCollection sourceReferences = new IdCollection<>(); private RecyclableObjectPool recyclableIdPool = new RecyclableObjectPool<>(); private IVariableFormatter variableFormatter = VariableFormatterFactory.createVariableFormatter(); @@ -65,6 +72,7 @@ public class DebugAdapterContext implements IDebugAdapterContext { private IExceptionManager exceptionManager = new ExceptionManager(); private IBreakpointManager breakpointManager = new BreakpointManager(); private IStepResultManager stepResultManager = new StepResultManager(); + private ThreadCache threadCache = new ThreadCache(); public DebugAdapterContext(IProtocolServer server, IProviderContext providerContext) { this.providerContext = providerContext; @@ -101,6 +109,10 @@ public void setDebuggerLinesStartAt1(boolean debuggerLinesStartAt1) { this.debuggerLinesStartAt1 = debuggerLinesStartAt1; } + public boolean isDebuggerColumnsStartAt1() { + return debuggerColumnStartAt1; + } + @Override public boolean isDebuggerPathsAreUri() { return debuggerPathsAreUri; @@ -200,7 +212,7 @@ public void setVariableFormatter(IVariableFormatter variableFormatter) { } @Override - public Map getSourceLookupCache() { + public Map getSourceLookupCache() { return sourceMappingCache; } @@ -349,4 +361,61 @@ public void setProcessId(long processId) { public void setShellProcessId(long shellProcessId) { this.shellProcessId = shellProcessId; } + + @Override + public void setThreadCache(ThreadCache cache) { + this.threadCache = cache; + } + + @Override + public ThreadCache getThreadCache() { + return this.threadCache; + } + + @Override + public boolean asyncJDWP() { + /** + * If we take 1 second as the acceptable latency for DAP requests, + * With a single-threaded strategy for handling JDWP requests, + * a latency of about 15ms per JDWP request can ensure the responsiveness + * for most DAPs. It allows sending 66 JDWP requests within 1 seconds, + * which can cover most DAP operations such as breakpoint, threads, + * call stack, step and continue. + */ + return asyncJDWP(15); + } + + @Override + public boolean asyncJDWP(long usableLatency) { + return DebugSettings.getCurrent().asyncJDWP == AsyncMode.ON + || (DebugSettings.getCurrent().asyncJDWP == AsyncMode.AUTO && this.jdwpLatency > usableLatency); + } + + public boolean isLocalDebugging() { + return localDebugging; + } + + public void setLocalDebugging(boolean local) { + this.localDebugging = local; + } + + @Override + public long getJDWPLatency() { + return this.jdwpLatency; + } + + @Override + public void setJDWPLatency(long baseLatency) { + this.jdwpLatency = baseLatency; + } + + @Override + public boolean isInitialized() { + return isInitialized; + } + + @Override + public void setInitialized(boolean isInitialized) { + this.isInitialized = isInitialized; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ErrorCode.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ErrorCode.java index 1cdc4f9ce..6cfe523cf 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ErrorCode.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ErrorCode.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -35,7 +35,8 @@ public enum ErrorCode { EXCEPTION_INFO_FAILURE(1018), EVALUATION_COMPILE_ERROR(2001), EVALUATE_NOT_SUSPENDED_THREAD(2002), - HCR_FAILURE(3001); + HCR_FAILURE(3001), + INVALID_DAP_HEADER(3002); private int id; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IBreakpointManager.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IBreakpointManager.java index 9ba609221..196714d52 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IBreakpointManager.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IBreakpointManager.java @@ -12,6 +12,7 @@ package com.microsoft.java.debug.core.adapter; import com.microsoft.java.debug.core.IBreakpoint; +import com.microsoft.java.debug.core.IMethodBreakpoint; import com.microsoft.java.debug.core.IWatchpoint; public interface IBreakpointManager { @@ -69,4 +70,23 @@ public interface IBreakpointManager { * Returns all registered watchpoints. */ IWatchpoint[] getWatchpoints(); + + /** + * Returns all the registered method breakpoints. + */ + IMethodBreakpoint[] getMethodBreakpoints(); + + /** + * Update the method breakpoints list. If the requested method breakpoints + * already registered in the breakpoint + * manager, reuse the cached one. Otherwise register the requested method + * breakpoints as a new method breakpoints. + * Besides, delete those not existed any more. + * + * @param methodBreakpoints + * the method breakpoints requested by client + * @return the full registered method breakpoints list + */ + IMethodBreakpoint[] setMethodBreakpoints(IMethodBreakpoint[] methodBreakpoints); + } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java index a1b21ecee..e65270eef 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterContext.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2020 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -55,6 +55,8 @@ public interface IDebugAdapterContext { void setClientColumnsStartAt1(boolean clientColumnsStartAt1); + boolean isDebuggerColumnsStartAt1(); + boolean isClientPathsAreUri(); void setClientPathsAreUri(boolean clientPathsAreUri); @@ -83,7 +85,7 @@ public interface IDebugAdapterContext { void setVariableFormatter(IVariableFormatter variableFormatter); - Map getSourceLookupCache(); + Map getSourceLookupCache(); void setDebuggeeEncoding(Charset encoding); @@ -136,4 +138,24 @@ public interface IDebugAdapterContext { void setProcessId(long processId); long getProcessId(); + + void setThreadCache(ThreadCache cache); + + ThreadCache getThreadCache(); + + boolean asyncJDWP(); + + boolean asyncJDWP(long usableLatency/**ms*/); + + boolean isLocalDebugging(); + + void setLocalDebugging(boolean local); + + long getJDWPLatency(); + + void setJDWPLatency(long baseLatency); + + boolean isInitialized(); + + void setInitialized(boolean isInitialized); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterFactory.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterFactory.java new file mode 100644 index 000000000..d392d3b48 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IDebugAdapterFactory.java @@ -0,0 +1,19 @@ +/******************************************************************************* + * Copyright (c) 2023 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core.adapter; + +import com.microsoft.java.debug.core.protocol.IProtocolServer; + +@FunctionalInterface +public interface IDebugAdapterFactory { + public IDebugAdapter create(IProtocolServer server); +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java index dd3e4dbde..f33742852 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ISourceLookUpProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -8,29 +8,64 @@ * Contributors: * Microsoft Corporation - initial API and implementation *******************************************************************************/ - package com.microsoft.java.debug.core.adapter; +import java.util.List; +import java.util.Objects; + import com.microsoft.java.debug.core.DebugException; +import com.microsoft.java.debug.core.JavaBreakpointLocation; +import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint; public interface ISourceLookUpProvider extends IProvider { boolean supportsRealtimeBreakpointVerification(); + /** + * Deprecated, please use {@link #getBreakpointLocations(String, SourceBreakpoint[])} instead. + */ + @Deprecated String[] getFullyQualifiedName(String uri, int[] lines, int[] columns) throws DebugException; /** - * Given a fully qualified class name and source file path, search the associated disk source file. + * Given a set of source breakpoint locations with line and column numbers, + * verify if they are valid breakpoint locations. If it's a valid location, + * resolve its enclosing class name, method name and signature (for method + * breakpoint) and all possible inline breakpoint locations in that line. * - * @param fullyQualifiedName - * the fully qualified class name (e.g. com.microsoft.java.debug.core.adapter.ISourceLookUpProvider). - * @param sourcePath - * the qualified source file path (e.g. com\microsoft\java\debug\core\adapter\ISourceLookupProvider.java). - * @return the associated source file uri. + * @param sourceUri + * the source file uri + * @param sourceBreakpoints + * the source breakpoints with line and column numbers + * @return Locations of Breakpoints containing context class and method information. */ + JavaBreakpointLocation[] getBreakpointLocations(String sourceUri, SourceBreakpoint[] sourceBreakpoints) throws DebugException; + + /** + * Deprecated, please use {@link #getSource(String, String)} instead. + */ + @Deprecated String getSourceFileURI(String fullyQualifiedName, String sourcePath); String getSourceContents(String uri); + /** + * Retrieves a {@link Source} object representing the source code associated with the given fully qualified class name and source file path. + * The implementation of this interface can determine a source is "local" or "remote". + * In case of "remote" a follow up "source" request will be issued by the client + * + * @param fullyQualifiedName + * the fully qualified class name, + * e.g., "com.microsoft.java.debug.core.adapter.ISourceLookUpProvider". + * @param sourcePath + * the qualified source file path, + * e.g., "com/microsoft/java/debug/core/adapter/ISourceLookupProvider.java". + * @return A {@link Source} object encapsulating the source file URI obtained from + * {@link #getSourceFileURI(String, String)} and the source type as {@link SourceType#LOCAL}. + */ + default Source getSource(String fullyQualifiedName, String sourcePath) { + return new Source(getSourceFileURI(fullyQualifiedName, sourcePath), SourceType.LOCAL); + } + /** * Returns the Java runtime that the specified project's build path used. * @param projectName @@ -40,4 +75,70 @@ public interface ISourceLookUpProvider extends IProvider { default String getJavaRuntimeVersion(String projectName) { return null; } + + /** + * Return method invocation found in the statement as the given line number of + * the source file. + * + * @param uri The source file where the invocation must be searched. + * @param line The line number where the invocation must be searched. + * + * @return List of found method invocation or empty if not method invocations + * can be found. + */ + List findMethodInvocations(String uri, int line); + + /** + * Return the line mappings from the original line to the decompiled line. + * + * @param uri The uri + * @return the line mappings from the original line to the decompiled line. + */ + default int[] getOriginalLineMappings(String uri) { + return null; + } + + /** + * Return the line mappings from the decompiled line to the original line. + * + * @param uri The uri + * @return the line mappings from the decompiled line to the original line. + */ + default int[] getDecompiledLineMappings(String uri) { + return null; + } + + public static class MethodInvocation { + public String expression; + public String methodName; + public String methodSignature; + public String methodGenericSignature; + public String declaringTypeName; + public int lineStart; + public int lineEnd; + public int columnStart; + public int columnEnd; + + @Override + public int hashCode() { + return Objects.hash(expression, methodName, methodSignature, methodGenericSignature, declaringTypeName, + lineStart, lineEnd, columnStart, columnEnd); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof MethodInvocation)) { + return false; + } + MethodInvocation other = (MethodInvocation) obj; + return Objects.equals(expression, other.expression) && Objects.equals(methodName, other.methodName) + && Objects.equals(methodSignature, other.methodSignature) + && Objects.equals(methodGenericSignature, other.methodGenericSignature) + && Objects.equals(declaringTypeName, other.declaringTypeName) && lineStart == other.lineStart + && lineEnd == other.lineEnd && columnStart == other.columnStart && columnEnd == other.columnEnd; + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IStackFrameManager.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IStackFrameManager.java index de10319f6..8d6c74486 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IStackFrameManager.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/IStackFrameManager.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -31,4 +31,35 @@ public interface IStackFrameManager { * @return all the stackframes in the specified thread */ StackFrame[] reloadStackFrames(ThreadReference thread); + + /** + * Refresh all stackframes from jdi thread. + * + * @param thread the jdi thread + * @param force Whether to load the whole frames if the thread's stackframes haven't been cached. + * @return all the stackframes in the specified thread + */ + StackFrame[] reloadStackFrames(ThreadReference thread, boolean force); + + /** + * Refersh the stackframes starting from the specified depth and length. + * + * @param thread the jdi thread + * @param start the index of the first frame to refresh. Index 0 represents the current frame. + * @param length the number of frames to refersh + * @return the refreshed stackframes + */ + StackFrame[] reloadStackFrames(ThreadReference thread, int start, int length); + + /** + * Clear the stackframes cache from the specified thread. + * + * @param thread the jdi thread + */ + void clearStackFrames(ThreadReference thread); + + /** + * Clear the whole stackframes cache. + */ + void clearStackFrames(); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java index 0526293b9..9e503e0af 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProtocolServer.java @@ -52,6 +52,20 @@ public ProtocolServer(InputStream input, OutputStream output, IProviderContext c debugAdapter = new DebugAdapter(this, context); } + /** + * Constructs a protocol server instance based on the given input stream and output stream. + * @param input + * the input stream + * @param output + * the output stream + * @param debugAdapterFactory + * factory to create debug adapter that implements DAP communication + */ + public ProtocolServer(InputStream input, OutputStream output, IDebugAdapterFactory debugAdapterFactory) { + super(input, output); + debugAdapter = debugAdapterFactory.create(this); + } + /** * A while-loop to parse input data and send output data constantly. */ diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/Source.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/Source.java new file mode 100644 index 000000000..d00b4cb4c --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/Source.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2017 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package com.microsoft.java.debug.core.adapter; + +public class Source { + public final String uri; + public final SourceType type; + + public Source(String uri, SourceType type) { + this.uri = uri; + this.type = type; + } + + public String getUri() { + return this.uri; + } + + public SourceType getType() { + return this.type; + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/SourceType.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/SourceType.java new file mode 100644 index 000000000..724bf3bda --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/SourceType.java @@ -0,0 +1,16 @@ +/******************************************************************************* + * Copyright (c) 2017 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ +package com.microsoft.java.debug.core.adapter; + +public enum SourceType { + REMOTE, + LOCAL +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/StackFrameManager.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/StackFrameManager.java index 9e1a86970..518cc7761 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/StackFrameManager.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/StackFrameManager.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,7 +11,6 @@ package com.microsoft.java.debug.core.adapter; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -21,10 +20,10 @@ import com.sun.jdi.ThreadReference; public class StackFrameManager implements IStackFrameManager { - private Map threadStackFrameMap = Collections.synchronizedMap(new HashMap<>()); + private Map threadStackFrameMap = new HashMap<>(); @Override - public StackFrame getStackFrame(StackFrameReference ref) { + public synchronized StackFrame getStackFrame(StackFrameReference ref) { ThreadReference thread = ref.getThread(); int depth = ref.getDepth(); StackFrame[] frames = threadStackFrameMap.get(thread.uniqueID()); @@ -32,13 +31,58 @@ public StackFrame getStackFrame(StackFrameReference ref) { } @Override - public StackFrame[] reloadStackFrames(ThreadReference thread) { + public synchronized StackFrame[] reloadStackFrames(ThreadReference thread) { + return reloadStackFrames(thread, true); + } + + @Override + public synchronized StackFrame[] reloadStackFrames(ThreadReference thread, boolean force) { return threadStackFrameMap.compute(thread.uniqueID(), (key, old) -> { try { - return thread.frames().toArray(new StackFrame[0]); + if (old == null || old.length == 0) { + if (force) { + return thread.frames().toArray(new StackFrame[0]); + } else { + return new StackFrame[0]; + } + } else { + return thread.frames(0, old.length).toArray(new StackFrame[0]); + } } catch (IncompatibleThreadStateException e) { return new StackFrame[0]; } }); } + + @Override + public synchronized StackFrame[] reloadStackFrames(ThreadReference thread, int start, int length) { + long threadId = thread.uniqueID(); + StackFrame[] old = threadStackFrameMap.get(threadId); + try { + StackFrame[] newFrames = thread.frames(start, length).toArray(new StackFrame[0]); + if (old == null || (start == 0 && length == old.length)) { + threadStackFrameMap.put(threadId, newFrames); + } else { + int maxLength = Math.max(old.length, start + length); + StackFrame[] totalFrames = new StackFrame[maxLength]; + System.arraycopy(old, 0, totalFrames, 0, old.length); + System.arraycopy(newFrames, 0, totalFrames, start, length); + threadStackFrameMap.put(threadId, totalFrames); + } + + return newFrames; + } catch (IncompatibleThreadStateException | IndexOutOfBoundsException e) { + return new StackFrame[0]; + } + } + + @Override + public synchronized void clearStackFrames(ThreadReference thread) { + threadStackFrameMap.remove(thread.uniqueID()); + } + + @Override + public synchronized void clearStackFrames() { + threadStackFrameMap.clear(); + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ThreadCache.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ThreadCache.java new file mode 100644 index 000000000..57d39154e --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ThreadCache.java @@ -0,0 +1,162 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core.adapter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import com.sun.jdi.ThreadReference; + +public class ThreadCache { + private List allThreads = new ArrayList<>(); + private Map threadNameMap = new ConcurrentHashMap<>(); + private Map deathThreads = Collections.synchronizedMap(new LinkedHashMap<>() { + @Override + protected boolean removeEldestEntry(java.util.Map.Entry eldest) { + return this.size() > 100; + } + }); + private Map eventThreads = new ConcurrentHashMap<>(); + private Map> decompiledClassesByThread = new HashMap<>(); + private Map threadStoppedReasons = new HashMap<>(); + + public synchronized void resetThreads(List threads) { + allThreads.clear(); + allThreads.addAll(threads); + } + + public synchronized List getThreads() { + return allThreads; + } + + public synchronized ThreadReference getThread(long threadId) { + for (ThreadReference thread : allThreads) { + if (threadId == thread.uniqueID()) { + return thread; + } + } + + for (ThreadReference thread : eventThreads.values()) { + if (threadId == thread.uniqueID()) { + return thread; + } + } + + return null; + } + + public void setThreadName(long threadId, String name) { + threadNameMap.put(threadId, name); + } + + public String getThreadName(long threadId) { + return threadNameMap.get(threadId); + } + + public void addDeathThread(long threadId) { + threadNameMap.remove(threadId); + eventThreads.remove(threadId); + deathThreads.put(threadId, true); + } + + public boolean isDeathThread(long threadId) { + return deathThreads.containsKey(threadId); + } + + public void addEventThread(ThreadReference thread) { + eventThreads.put(thread.uniqueID(), thread); + } + + public void addEventThread(ThreadReference thread, String reason) { + eventThreads.put(thread.uniqueID(), thread); + if (reason != null) { + threadStoppedReasons.put(thread.uniqueID(), reason); + } + } + + public void removeEventThread(long threadId) { + eventThreads.remove(threadId); + } + + public void clearEventThread() { + eventThreads.clear(); + } + + /** + * The visible threads includes: + * 1. The currently running threads returned by the JDI API + * VirtualMachine.allThreads(). + * 2. The threads suspended by events such as Breakpoint, Step, Exception etc. + * + * The part 2 is mainly for virtual threads, since VirtualMachine.allThreads() + * does not include virtual threads by default. For those virtual threads + * that are suspended, we need to show their call stacks in CALL STACK view. + */ + public List visibleThreads(IDebugAdapterContext context) { + List visibleThreads = new ArrayList<>(context.getDebugSession().getAllThreads()); + Set idSet = new HashSet<>(); + visibleThreads.forEach(thread -> idSet.add(thread.uniqueID())); + for (ThreadReference thread : eventThreads.values()) { + if (idSet.contains(thread.uniqueID())) { + continue; + } + + idSet.add(thread.uniqueID()); + visibleThreads.add(thread); + } + + return visibleThreads; + } + + public Set getDecompiledClassesByThread(long threadId) { + return decompiledClassesByThread.get(threadId); + } + + public void setDecompiledClassesByThread(long threadId, Set decompiledClasses) { + if (decompiledClasses == null || decompiledClasses.isEmpty()) { + decompiledClassesByThread.remove(threadId); + return; + } + + decompiledClassesByThread.put(threadId, decompiledClasses); + } + + public String getThreadStoppedReason(long threadId) { + return threadStoppedReasons.get(threadId); + } + + public void setThreadStoppedReason(long threadId, String reason) { + if (reason == null) { + threadStoppedReasons.remove(threadId); + return; + } + + threadStoppedReasons.put(threadId, reason); + } + + public void clearThreadStoppedState(long threadId) { + threadStoppedReasons.remove(threadId); + decompiledClassesByThread.remove(threadId); + } + + public void clearAllThreadStoppedState() { + threadStoppedReasons.clear(); + decompiledClassesByThread.clear(); + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/AttachRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/AttachRequestHandler.java index 7069bc39e..5ab848c8a 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/AttachRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/AttachRequestHandler.java @@ -23,6 +23,7 @@ import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugUtility; import com.microsoft.java.debug.core.IDebugSession; +import com.microsoft.java.debug.core.UsageDataSession; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.Constants; import com.microsoft.java.debug.core.adapter.ErrorCode; @@ -39,6 +40,7 @@ import com.microsoft.java.debug.core.protocol.Requests.AttachArguments; import com.microsoft.java.debug.core.protocol.Requests.Command; import com.sun.jdi.connect.IllegalConnectorArgumentsException; +import com.sun.jdi.request.EventRequest; import org.apache.commons.lang3.StringUtils; @@ -58,52 +60,71 @@ public CompletableFuture handle(Command command, Arguments arguments, context.setSourcePaths(attachArguments.sourcePaths); context.setDebuggeeEncoding(StandardCharsets.UTF_8); // Use UTF-8 as debuggee's default encoding format. context.setStepFilters(attachArguments.stepFilters); + context.setLocalDebugging(isLocalHost(attachArguments.hostName)); + + Map traceInfo = new HashMap<>(); + traceInfo.put("localAttach", context.isLocalDebugging()); + traceInfo.put("asyncJDWP", context.asyncJDWP()); IVirtualMachineManagerProvider vmProvider = context.getProvider(IVirtualMachineManagerProvider.class); vmHandler.setVmProvider(vmProvider); IDebugSession debugSession = null; try { - logger.info(String.format("Trying to attach to remote debuggee VM %s:%d .", attachArguments.hostName, attachArguments.port)); - debugSession = DebugUtility.attach(vmProvider.getVirtualMachineManager(), attachArguments.hostName, attachArguments.port, - attachArguments.timeout); - context.setDebugSession(debugSession); - vmHandler.connectVirtualMachine(debugSession.getVM()); - logger.info("Attaching to debuggee VM succeeded."); - } catch (IOException | IllegalConnectorArgumentsException e) { - throw AdapterUtils.createCompletionException( - String.format("Failed to attach to remote debuggee VM. Reason: %s", e.toString()), - ErrorCode.ATTACH_FAILURE, - e); - } + try { + logger.info(String.format("Trying to attach to remote debuggee VM %s:%d .", attachArguments.hostName, attachArguments.port)); + debugSession = DebugUtility.attach(vmProvider.getVirtualMachineManager(), attachArguments.hostName, attachArguments.port, + attachArguments.timeout); + context.setDebugSession(debugSession); + vmHandler.connectVirtualMachine(debugSession.getVM()); + logger.info("Attaching to debuggee VM succeeded."); + } catch (IOException | IllegalConnectorArgumentsException e) { + throw AdapterUtils.createCompletionException( + String.format("Failed to attach to remote debuggee VM. Reason: %s", e.toString()), + ErrorCode.ATTACH_FAILURE, + e); + } - Map options = new HashMap<>(); - options.put(Constants.DEBUGGEE_ENCODING, context.getDebuggeeEncoding()); - if (attachArguments.projectName != null) { - options.put(Constants.PROJECT_NAME, attachArguments.projectName); - } - // TODO: Clean up the initialize mechanism - ISourceLookUpProvider sourceProvider = context.getProvider(ISourceLookUpProvider.class); - sourceProvider.initialize(context, options); - // If the debugger and debuggee run at the different JVM platforms, show a warning message. - if (debugSession != null) { - String debuggeeVersion = debugSession.getVM().version(); - String debuggerVersion = sourceProvider.getJavaRuntimeVersion(attachArguments.projectName); - if (StringUtils.isNotBlank(debuggerVersion) && !debuggerVersion.equals(debuggeeVersion)) { - String warnMessage = String.format("[Warn] The debugger and the debuggee are running in different versions of JVMs. " - + "You could see wrong source mapping results.\n" - + "Debugger JVM version: %s\n" - + "Debuggee JVM version: %s", debuggerVersion, debuggeeVersion); - logger.warning(warnMessage); - context.getProtocolServer().sendEvent(Events.OutputEvent.createConsoleOutput(warnMessage)); + Map options = new HashMap<>(); + options.put(Constants.DEBUGGEE_ENCODING, context.getDebuggeeEncoding()); + if (attachArguments.projectName != null) { + options.put(Constants.PROJECT_NAME, attachArguments.projectName); + } + // TODO: Clean up the initialize mechanism + ISourceLookUpProvider sourceProvider = context.getProvider(ISourceLookUpProvider.class); + sourceProvider.initialize(context, options); + // If the debugger and debuggee run at the different JVM platforms, show a warning message. + if (debugSession != null) { + String debuggeeVersion = debugSession.getVM().version(); + String debuggerVersion = sourceProvider.getJavaRuntimeVersion(attachArguments.projectName); + if (StringUtils.isNotBlank(debuggerVersion) && !debuggerVersion.equals(debuggeeVersion)) { + String warnMessage = String.format("[Warn] The debugger and the debuggee are running in different versions of JVMs. " + + "You could see wrong source mapping results.\n" + + "Debugger JVM version: %s\n" + + "Debuggee JVM version: %s", debuggerVersion, debuggeeVersion); + logger.warning(warnMessage); + context.getProtocolServer().sendEvent(Events.OutputEvent.createConsoleOutput(warnMessage)); + } + + EventRequest request = debugSession.getVM().eventRequestManager().createVMDeathRequest(); + request.setSuspendPolicy(EventRequest.SUSPEND_NONE); + long sent = System.currentTimeMillis(); + request.enable(); + long received = System.currentTimeMillis(); + long latency = received - sent; + context.setJDWPLatency(latency); + logger.info("Network latency for JDWP command: " + latency + "ms"); + traceInfo.put("networkLatency", latency); } - } - IEvaluationProvider evaluationProvider = context.getProvider(IEvaluationProvider.class); - evaluationProvider.initialize(context, options); - IHotCodeReplaceProvider hcrProvider = context.getProvider(IHotCodeReplaceProvider.class); - hcrProvider.initialize(context, options); - ICompletionsProvider completionsProvider = context.getProvider(ICompletionsProvider.class); - completionsProvider.initialize(context, options); + IEvaluationProvider evaluationProvider = context.getProvider(IEvaluationProvider.class); + evaluationProvider.initialize(context, options); + IHotCodeReplaceProvider hcrProvider = context.getProvider(IHotCodeReplaceProvider.class); + hcrProvider.initialize(context, options); + ICompletionsProvider completionsProvider = context.getProvider(ICompletionsProvider.class); + completionsProvider.initialize(context, options); + } finally { + UsageDataSession.recordInfo("attach debug info", traceInfo); + } // Send an InitializedEvent to indicate that the debugger is ready to accept configuration requests // (e.g. SetBreakpointsRequest, SetExceptionBreakpointsRequest). @@ -111,4 +132,13 @@ public CompletableFuture handle(Command command, Arguments arguments, return CompletableFuture.completedFuture(response); } + private boolean isLocalHost(String hostName) { + if (hostName == null || "localhost".equals(hostName) || "127.0.0.1".equals(hostName)) { + return true; + } + + // TODO: Check the host name of current computer as well. + return false; + } + } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/BreakpointLocationsRequestHander.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/BreakpointLocationsRequestHander.java new file mode 100644 index 000000000..57c7ea15e --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/BreakpointLocationsRequestHander.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2022 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package com.microsoft.java.debug.core.adapter.handler; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; + +import com.microsoft.java.debug.core.IBreakpoint; +import com.microsoft.java.debug.core.adapter.AdapterUtils; +import com.microsoft.java.debug.core.adapter.ErrorCode; +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; +import com.microsoft.java.debug.core.protocol.Requests; +import com.microsoft.java.debug.core.protocol.Responses; +import com.microsoft.java.debug.core.protocol.Messages.Response; +import com.microsoft.java.debug.core.protocol.Requests.Arguments; +import com.microsoft.java.debug.core.protocol.Requests.BreakpointLocationsArguments; +import com.microsoft.java.debug.core.protocol.Requests.Command; +import com.microsoft.java.debug.core.protocol.Types.BreakpointLocation; + +/** + * The breakpointLocations request returns all possible locations for source breakpoints in a given range. + * Clients should only call this request if the corresponding capability supportsBreakpointLocationsRequest is true. + */ +public class BreakpointLocationsRequestHander implements IDebugRequestHandler { + + @Override + public List getTargetCommands() { + return Arrays.asList(Requests.Command.BREAKPOINTLOCATIONS); + } + + @Override + public CompletableFuture handle(Command command, Arguments arguments, Response response, + IDebugAdapterContext context) { + BreakpointLocationsArguments bpArgs = (BreakpointLocationsArguments) arguments; + String sourceUri = SetBreakpointsRequestHandler.normalizeSourcePath(bpArgs.source, context); + // When breakpoint source path is null or an invalid file path, send an ErrorResponse back. + if (StringUtils.isBlank(sourceUri)) { + throw AdapterUtils.createCompletionException( + String.format("Failed to get BreakpointLocations. Reason: '%s' is an invalid path.", bpArgs.source.path), + ErrorCode.SET_BREAKPOINT_FAILURE); + } + + int debuggerLine = AdapterUtils.convertLineNumber(bpArgs.line, context.isClientLinesStartAt1(), context.isDebuggerLinesStartAt1()); + IBreakpoint[] breakpoints = context.getBreakpointManager().getBreakpoints(sourceUri); + BreakpointLocation[] locations = new BreakpointLocation[0]; + for (int i = 0; i < breakpoints.length; i++) { + if (breakpoints[i].getLineNumber() == debuggerLine && ArrayUtils.isNotEmpty( + breakpoints[i].sourceLocation().availableBreakpointLocations())) { + locations = Stream.of(breakpoints[i].sourceLocation().availableBreakpointLocations()).map(location -> { + BreakpointLocation newLocaiton = new BreakpointLocation(); + newLocaiton.line = AdapterUtils.convertLineNumber(location.line, + context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + newLocaiton.column = AdapterUtils.convertColumnNumber(location.column, + context.isDebuggerColumnsStartAt1(), context.isClientColumnsStartAt1()); + newLocaiton.endLine = AdapterUtils.convertLineNumber(location.endLine, + context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + newLocaiton.endColumn = AdapterUtils.convertColumnNumber(location.endColumn, + context.isDebuggerColumnsStartAt1(), context.isClientColumnsStartAt1()); + return newLocaiton; + }).toArray(BreakpointLocation[]::new); + break; + } + } + + response.body = new Responses.BreakpointLocationsResponseBody(locations); + return CompletableFuture.completedFuture(response); + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java index 9cd3aa446..308adddb6 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ConfigurationDoneRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2020 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -40,6 +40,7 @@ import com.sun.jdi.event.ThreadStartEvent; import com.sun.jdi.event.VMDeathEvent; import com.sun.jdi.event.VMDisconnectEvent; +import com.sun.jdi.request.EventRequest; import com.sun.jdi.event.VMStartEvent; public class ConfigurationDoneRequestHandler implements IDebugRequestHandler { @@ -76,6 +77,7 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, if (context.isVmStopOnEntry()) { DebugUtility.stopOnEntry(debugSession, context.getMainClass()).thenAccept(threadId -> { context.getProtocolServer().sendEvent(new Events.StoppedEvent("entry", threadId)); + context.getThreadCache().setThreadStoppedReason(threadId, "entry"); }); } } else if (event instanceof VMDeathEvent) { @@ -104,20 +106,23 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, ThreadReference deathThread = ((ThreadDeathEvent) event).thread(); Events.ThreadEvent threadDeathEvent = new Events.ThreadEvent("exited", deathThread.uniqueID()); context.getProtocolServer().sendEvent(threadDeathEvent); + context.getThreadCache().addDeathThread(deathThread.uniqueID()); } else if (event instanceof BreakpointEvent) { // ignore since SetBreakpointsRequestHandler has already handled } else if (event instanceof ExceptionEvent) { ThreadReference thread = ((ExceptionEvent) event).thread(); - ThreadReference bpThread = ((ExceptionEvent) event).thread(); IEvaluationProvider engine = context.getProvider(IEvaluationProvider.class); - if (engine.isInEvaluation(bpThread)) { + if (engine.isInEvaluation(thread)) { return; } JdiExceptionReference jdiException = new JdiExceptionReference(((ExceptionEvent) event).exception(), ((ExceptionEvent) event).catchLocation() == null); context.getExceptionManager().setException(thread.uniqueID(), jdiException); - context.getProtocolServer().sendEvent(new Events.StoppedEvent("exception", thread.uniqueID())); + context.getThreadCache().addEventThread(thread, "exception"); + boolean allThreadsStopped = event.request() != null + && event.request().suspendPolicy() == EventRequest.SUSPEND_ALL; + context.getProtocolServer().sendEvent(new Events.StoppedEvent("exception", thread.uniqueID(), allThreadsStopped)); debugEvent.shouldResume = false; } else { isImportantEvent = false; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DisconnectRequestWithoutDebuggingHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DisconnectRequestWithoutDebuggingHandler.java index bb56d4052..59d750bcd 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DisconnectRequestWithoutDebuggingHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/DisconnectRequestWithoutDebuggingHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2018 Microsoft Corporation and others. +* Copyright (c) 2018-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,6 +11,8 @@ package com.microsoft.java.debug.core.adapter.handler; +import java.util.Optional; + import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.Arguments; @@ -25,6 +27,11 @@ public void destroyDebugSession(Command command, Arguments arguments, Response r Process debuggeeProcess = context.getDebuggeeProcess(); if (debuggeeProcess != null && disconnectArguments.terminateDebuggee) { debuggeeProcess.destroy(); + } else if (context.getProcessId() > 0 && disconnectArguments.terminateDebuggee) { + Optional debuggeeHandle = ProcessHandle.of(context.getProcessId()); + if (debuggeeHandle.isPresent()) { + debuggeeHandle.get().destroy(); + } } } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/EvaluateRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/EvaluateRequestHandler.java index 9cf99742d..d135ee5b2 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/EvaluateRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/EvaluateRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2021 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -64,6 +64,13 @@ public CompletableFuture handle(Command command, Arguments arguments, VariableUtils.applyFormatterOptions(options, evalArguments.format != null && evalArguments.format.hex); String expression = evalArguments.expression; + // Async mode is supposed to be performant, then disable the advanced features like hover evaluation. + if (context.asyncJDWP(VariablesRequestHandler.USABLE_JDWP_LATENCY) + && context.getJDWPLatency() > VariablesRequestHandler.USABLE_JDWP_LATENCY + && "hover".equals(evalArguments.context)) { + return CompletableFuture.completedFuture(response); + } + if (StringUtils.isBlank(expression)) { throw new CompletionException(AdapterUtils.createUserErrorDebugException( "Failed to evaluate. Reason: Empty expression cannot be evaluated.", @@ -93,7 +100,7 @@ public CompletableFuture handle(Command command, Arguments arguments, Value sizeValue = null; if (value instanceof ArrayReference) { indexedVariables = ((ArrayReference) value).length(); - } else if (value instanceof ObjectReference && DebugSettings.getCurrent().showLogicalStructure && engine != null) { + } else if (value instanceof ObjectReference && supportsLogicStructureView(context, evalArguments.context) && engine != null) { try { JavaLogicalStructure structure = JavaLogicalStructureManager.getLogicalStructure((ObjectReference) value); if (structure != null && structure.getSizeExpression() != null) { @@ -130,7 +137,7 @@ public CompletableFuture handle(Command command, Arguments arguments, // If failed to resolve the variable value, skip the details info as well. } else if (sizeValue != null) { detailsString = "size=" + variableFormatter.valueToString(sizeValue, options); - } else if (DebugSettings.getCurrent().showToString) { + } else if (supportsToStringView(context, evalArguments.context)) { try { detailsString = VariableDetailUtils.formatDetailsValue(value, stackFrameReference.getThread(), variableFormatter, options, engine); } catch (OutOfMemoryError e) { @@ -177,4 +184,24 @@ public CompletableFuture handle(Command command, Arguments arguments, } }); } + + private boolean supportsLogicStructureView(IDebugAdapterContext context, String evalContext) { + if (!"watch".equals(evalContext)) { + return true; + } + + return (!context.asyncJDWP(VariablesRequestHandler.USABLE_JDWP_LATENCY) + || context.getJDWPLatency() <= VariablesRequestHandler.USABLE_JDWP_LATENCY) + && DebugSettings.getCurrent().showLogicalStructure; + } + + private boolean supportsToStringView(IDebugAdapterContext context, String evalContext) { + if (!"watch".equals(evalContext)) { + return true; + } + + return (!context.asyncJDWP(VariablesRequestHandler.USABLE_JDWP_LATENCY) + || context.getJDWPLatency() <= VariablesRequestHandler.USABLE_JDWP_LATENCY) + && DebugSettings.getCurrent().showToString; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ExceptionInfoRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ExceptionInfoRequestHandler.java index 13456029d..5e065edd0 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ExceptionInfoRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ExceptionInfoRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2019 Microsoft Corporation and others. +* Copyright (c) 2019-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -53,7 +53,11 @@ public List getTargetCommands() { public CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { ExceptionInfoArguments exceptionInfoArgs = (ExceptionInfoArguments) arguments; - ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), exceptionInfoArgs.threadId); + ThreadReference thread = context.getThreadCache().getThread(exceptionInfoArgs.threadId); + if (thread == null) { + thread = DebugUtility.getThread(context.getDebugSession(), exceptionInfoArgs.threadId); + } + if (thread == null) { throw AdapterUtils.createCompletionException("Thread " + exceptionInfoArgs.threadId + " doesn't exist.", ErrorCode.EXCEPTION_INFO_FAILURE); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java index 4733170eb..6b9245166 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InitializeRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -62,8 +62,12 @@ public CompletableFuture handle(Requests.Command command, Req caps.exceptionBreakpointFilters = exceptionFilters; caps.supportsExceptionInfoRequest = true; caps.supportsDataBreakpoints = true; + caps.supportsFunctionBreakpoints = true; caps.supportsClipboardContext = true; + caps.supportsBreakpointLocationsRequest = true; + caps.supportsStepInTargetsRequest = true; response.body = caps; + context.setInitialized(true); return CompletableFuture.completedFuture(response); } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InlineValuesRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InlineValuesRequestHandler.java index e9697f899..21f77ee5a 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InlineValuesRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InlineValuesRequestHandler.java @@ -72,7 +72,7 @@ public List getTargetCommands() { public CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { InlineValuesArguments inlineValuesArgs = (InlineValuesArguments) arguments; - int variableCount = inlineValuesArgs == null || inlineValuesArgs.variables == null ? 0 : inlineValuesArgs.variables.length; + final int variableCount = inlineValuesArgs == null || inlineValuesArgs.variables == null ? 0 : inlineValuesArgs.variables.length; InlineVariable[] inlineVariables = inlineValuesArgs.variables; StackFrameReference stackFrameReference = (StackFrameReference) context.getRecyclableIdPool().getObjectById(inlineValuesArgs.frameId); if (stackFrameReference == null) { @@ -81,6 +81,13 @@ public CompletableFuture handle(Command command, Arguments arguments, return CompletableFuture.completedFuture(response); } + // Async mode is supposed to be performant, then disable the advanced features like inline values. + if (context.getJDWPLatency() > VariablesRequestHandler.USABLE_JDWP_LATENCY + && context.asyncJDWP(VariablesRequestHandler.USABLE_JDWP_LATENCY)) { + response.body = new Responses.InlineValuesResponse(null); + return CompletableFuture.completedFuture(response); + } + IStackFrameManager stackFrameManager = context.getStackFrameManager(); StackFrame frame = stackFrameManager.getStackFrame(stackFrameReference); if (frame == null) { diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchRequestHandler.java index fec967b84..e5662f936 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2018-2021 Microsoft Corporation and others. +* Copyright (c) 2018-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -16,6 +16,7 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -42,6 +43,7 @@ import com.microsoft.java.debug.core.DebugUtility; import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.LaunchException; +import com.microsoft.java.debug.core.UsageDataSession; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; @@ -75,7 +77,19 @@ public List getTargetCommands() { @Override public CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { + if (!context.isInitialized()) { + final String errorMessage = "'launch' request is rejected since the debug session has not been initialized yet."; + logger.log(Level.SEVERE, errorMessage); + return CompletableFuture.completedFuture( + AdapterUtils.setErrorResponse(response, ErrorCode.LAUNCH_FAILURE, errorMessage)); + } LaunchArguments launchArguments = (LaunchArguments) arguments; + Map traceInfo = new HashMap<>(); + traceInfo.put("asyncJDWP", context.asyncJDWP()); + traceInfo.put("noDebug", launchArguments.noDebug); + traceInfo.put("console", launchArguments.console); + UsageDataSession.recordInfo("launch debug info", traceInfo); + activeLaunchHandler = launchArguments.noDebug ? new LaunchWithoutDebuggingDelegate((daContext) -> handleTerminatedEvent(daContext)) : new LaunchWithDebuggingDelegate(); return handleLaunchCommand(arguments, response, context); @@ -127,11 +141,50 @@ protected CompletableFuture handleLaunchCommand(Arguments arguments, R } } else if (launchArguments.shortenCommandLine == ShortenApproach.ARGFILE) { try { - Path tempfile = LaunchUtils.generateArgfile(launchArguments.classPaths, launchArguments.modulePaths); - launchArguments.vmArgs += " \"@" + tempfile.toAbsolutePath().toString() + "\""; - launchArguments.classPaths = new String[0]; - launchArguments.modulePaths = new String[0]; - context.setArgsfile(tempfile); + /** + * See the JDK spec https://docs.oracle.com/en/java/javase/18/docs/specs/man/java.html#java-command-line-argument-files. + * The argument file must contain only ASCII characters or characters in system default encoding that's ASCII friendly. + */ + Charset systemCharset = LaunchUtils.getSystemCharset(); + CharsetEncoder encoder = systemCharset.newEncoder(); + String vmArgsForShorten = null; + String[] classPathsForShorten = null; + String[] modulePathsForShorten = null; + if (StringUtils.isNotBlank(launchArguments.vmArgs)) { + if (!encoder.canEncode(launchArguments.vmArgs)) { + logger.warning(String.format("Cannot generate the 'vmArgs' argument into the argfile because it contains characters " + + "that cannot be encoded in the system charset '%s'.", systemCharset.displayName())); + } else { + vmArgsForShorten = launchArguments.vmArgs; + } + } + + if (ArrayUtils.isNotEmpty(launchArguments.classPaths)) { + if (!encoder.canEncode(String.join(File.pathSeparator, launchArguments.classPaths))) { + logger.warning(String.format("Cannot generate the '-cp' argument into the argfile because it contains characters " + + "that cannot be encoded in the system charset '%s'.", systemCharset.displayName())); + } else { + classPathsForShorten = launchArguments.classPaths; + } + } + + if (ArrayUtils.isNotEmpty(launchArguments.modulePaths)) { + if (!encoder.canEncode(String.join(File.pathSeparator, launchArguments.modulePaths))) { + logger.warning(String.format("Cannot generate the '--module-path' argument into the argfile because it contains characters " + + "that cannot be encoded in the system charset '%s'.", systemCharset.displayName())); + } else { + modulePathsForShorten = launchArguments.modulePaths; + } + } + + if (vmArgsForShorten != null || classPathsForShorten != null || modulePathsForShorten != null) { + Path tempfile = LaunchUtils.generateArgfile(vmArgsForShorten, classPathsForShorten, modulePathsForShorten, systemCharset); + launchArguments.vmArgs = (vmArgsForShorten == null ? launchArguments.vmArgs : "") + + " \"@" + tempfile.toAbsolutePath().toString() + "\""; + launchArguments.classPaths = (classPathsForShorten == null ? launchArguments.classPaths : new String[0]); + launchArguments.modulePaths = (modulePathsForShorten == null ? launchArguments.modulePaths : new String[0]); + context.setArgsfile(tempfile); + } } catch (IOException e) { logger.log(Level.SEVERE, String.format("Failed to create a temp argfile: %s", e.toString()), e); } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchUtils.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchUtils.java index d0d99af40..7370328b2 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchUtils.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2021 Microsoft Corporation and others. +* Copyright (c) 2021-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,29 +11,71 @@ package com.microsoft.java.debug.core.adapter.handler; +import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStreamReader; import java.math.BigInteger; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.jar.Attributes; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; - -import com.microsoft.java.debug.core.adapter.AdapterUtils; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; + +import com.microsoft.java.debug.core.Configuration; +import com.microsoft.java.debug.core.adapter.AdapterUtils; public class LaunchUtils { + private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); private static Set tempFilesInUse = new HashSet<>(); + private static final Charset SYSTEM_CHARSET; + + static { + Charset result = null; + try { + // JEP 400: Java 17+ populates this system property. + String encoding = System.getProperty("native.encoding"); //$NON-NLS-1$ + if (encoding != null && !encoding.isBlank()) { + result = Charset.forName(encoding); + } else { + // JVM internal property, works on older JVM's too + encoding = System.getProperty("sun.jnu.encoding"); //$NON-NLS-1$ + if (encoding != null && !encoding.isBlank()) { + result = Charset.forName(encoding); + } + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Error occurs during resolving system encoding", e); + } + if (result == null) { + // This is always UTF-8 on Java >= 18. + result = Charset.defaultCharset(); + } + SYSTEM_CHARSET = result; + } + + public static Charset getSystemCharset() { + return SYSTEM_CHARSET; + } /** * Generate the classpath parameters to a temporary classpath.jar. @@ -53,7 +95,7 @@ public static synchronized Path generateClasspathJar(String[] classPaths) throws // In jar manifest, the absolute path C:\a.jar should be converted to the url style file:///C:/a.jar String classpathValue = String.join(" ", classpathUrls); attributes.put(Attributes.Name.CLASS_PATH, classpathValue); - String baseName = "cp_" + getMd5(classpathValue); + String baseName = "cp_" + getSha256(classpathValue); cleanupTempFiles(baseName, ".jar"); Path tempfile = createTempFile(baseName, ".jar"); JarOutputStream jar = new JarOutputStream(new FileOutputStream(tempfile.toFile()), manifest); @@ -70,10 +112,14 @@ public static synchronized Path generateClasspathJar(String[] classPaths) throws * @return the file path of the generated argfile * @throws IOException Some errors occur during generating the argfile */ - public static synchronized Path generateArgfile(String[] classPaths, String[] modulePaths) throws IOException { + public static synchronized Path generateArgfile(String vmArgs, String[] classPaths, String[] modulePaths, Charset encoding) throws IOException { String argfile = ""; + if (StringUtils.isNotBlank(vmArgs)) { + argfile += vmArgs; + } + if (ArrayUtils.isNotEmpty(classPaths)) { - argfile = "-cp \"" + String.join(File.pathSeparator, classPaths) + "\""; + argfile += " -cp \"" + String.join(File.pathSeparator, classPaths) + "\""; } if (ArrayUtils.isNotEmpty(modulePaths)) { @@ -81,10 +127,10 @@ public static synchronized Path generateArgfile(String[] classPaths, String[] mo } argfile = argfile.replace("\\", "\\\\"); - String baseName = "cp_" + getMd5(argfile); + String baseName = "cp_" + getSha256(argfile); cleanupTempFiles(baseName, ".argfile"); Path tempfile = createTempFile(baseName, ".argfile"); - Files.write(tempfile, argfile.getBytes()); + Files.writeString(tempfile, argfile, encoding); lockTempLaunchFile(tempfile); return tempfile; @@ -102,6 +148,178 @@ public static void releaseTempLaunchFile(Path tempFile) { } } + public static ProcessHandle findJavaProcessInTerminalShell(long shellPid, String javaCommand, int timeout/*ms*/) { + ProcessHandle shellProcess = ProcessHandle.of(shellPid).orElse(null); + if (shellProcess != null) { + int retry = 0; + final int INTERVAL = 20/*ms*/; + final int maxRetries = timeout / INTERVAL; + final boolean isCygwinShell = isCygwinShell(shellProcess.info().command().orElse(null)); + while (retry <= maxRetries) { + Optional subProcessHandle = shellProcess.descendants().filter(proc -> { + String command = proc.info().command().orElse(""); + return Objects.equals(command, javaCommand) || command.endsWith("\\java.exe") || command.endsWith("/java"); + }).findFirst(); + + if (subProcessHandle.isPresent()) { + if (retry > 0) { + logger.info("Retried " + retry + " times to find Java subProcess."); + } + logger.info("shellPid: " + shellPid + ", javaPid: " + subProcessHandle.get().pid()); + return subProcessHandle.get(); + } else if (isCygwinShell) { + long javaPid = findJavaProcessByCygwinPsCommand(shellProcess, javaCommand); + if (javaPid > 0) { + if (retry > 0) { + logger.info("Retried " + retry + " times to find Java subProcess."); + } + logger.info("[Cygwin Shell] shellPid: " + shellPid + ", javaPid: " + javaPid); + return ProcessHandle.of(javaPid).orElse(null); + } + } + + retry++; + if (retry > maxRetries) { + break; + } + + try { + Thread.sleep(INTERVAL); + } catch (InterruptedException e) { + // do nothing + } + } + + logger.info("Retried " + retry + " times but failed to find Java subProcess of shell pid " + shellPid); + } + + return null; + } + + private static long findJavaProcessByCygwinPsCommand(ProcessHandle shellProcess, String javaCommand) { + String psCommand = detectPsCommandPath(shellProcess.info().command().orElse(null)); + if (psCommand == null) { + return -1; + } + + BufferedReader psReader = null; + List psProcs = new ArrayList<>(); + List javaCandidates = new ArrayList<>(); + try { + String[] headers = null; + int pidIndex = -1; + int ppidIndex = -1; + int winpidIndex = -1; + String line; + String javaExeName = Paths.get(javaCommand).toFile().getName().replaceFirst("\\.exe$", ""); + + Process p = Runtime.getRuntime().exec(new String[] {psCommand, "-l"}); + psReader = new BufferedReader(new InputStreamReader(p.getInputStream())); + /** + * Here is a sample output when running ps command in Cygwin/MINGW64 shell. + * PID PPID PGID WINPID TTY UID STIME COMMAND + * 1869 1 1869 7852 cons2 4096 15:29:27 /usr/bin/bash + * 2271 1 2271 30820 cons4 4096 19:38:30 /usr/bin/bash + * 1812 1 1812 21540 cons1 4096 15:05:03 /usr/bin/bash + * 2216 1 2216 11328 cons3 4096 19:38:18 /usr/bin/bash + * 1720 1 1720 5404 cons0 4096 13:46:42 /usr/bin/bash + * 2269 2216 2269 6676 cons3 4096 19:38:21 /c/Program Files/Microsoft/jdk-11.0.14.9-hotspot/bin/java + * 1911 1869 1869 29708 cons2 4096 15:29:31 /c/Program Files/nodejs/node + * 2315 2271 2315 18064 cons4 4096 19:38:34 /usr/bin/ps + */ + while ((line = psReader.readLine()) != null) { + String[] cols = line.strip().split("\\s+"); + if (headers == null) { + headers = cols; + pidIndex = ArrayUtils.indexOf(headers, "PID"); + ppidIndex = ArrayUtils.indexOf(headers, "PPID"); + winpidIndex = ArrayUtils.indexOf(headers, "WINPID"); + if (pidIndex < 0 || ppidIndex < 0 || winpidIndex < 0) { + logger.warning("Failed to find Java process because ps command is not the standard Cygwin ps command."); + return -1; + } + } else if (cols.length >= headers.length) { + long pid = Long.parseLong(cols[pidIndex]); + long ppid = Long.parseLong(cols[ppidIndex]); + long winpid = Long.parseLong(cols[winpidIndex]); + PsProcess process = new PsProcess(pid, ppid, winpid); + psProcs.add(process); + if (cols[cols.length - 1].endsWith("/" + javaExeName) || cols[cols.length - 1].endsWith("/java")) { + javaCandidates.add(process); + } + } + } + } catch (Exception err) { + logger.log(Level.WARNING, "Failed to find Java process by Cygwin ps command.", err); + } finally { + if (psReader != null) { + try { + psReader.close(); + } catch (IOException e) { + // ignore + } + } + } + + if (!javaCandidates.isEmpty()) { + Set descendantWinpids = shellProcess.descendants().map(proc -> proc.pid()).collect(Collectors.toSet()); + long shellWinpid = shellProcess.pid(); + for (PsProcess javaCandidate: javaCandidates) { + if (descendantWinpids.contains(javaCandidate.winpid)) { + return javaCandidate.winpid; + } + + for (PsProcess psProc : psProcs) { + if (javaCandidate.ppid != psProc.pid) { + continue; + } + + if (descendantWinpids.contains(psProc.winpid) || psProc.winpid == shellWinpid) { + return javaCandidate.winpid; + } + + break; + } + } + } + + return -1; + } + + private static boolean isCygwinShell(String shellPath) { + if (!SystemUtils.IS_OS_WINDOWS || shellPath == null) { + return false; + } + + String lowerShellPath = shellPath.toLowerCase(); + return lowerShellPath.endsWith("git\\bin\\bash.exe") + || lowerShellPath.endsWith("git\\usr\\bin\\bash.exe") + || lowerShellPath.endsWith("mintty.exe") + || lowerShellPath.endsWith("cygwin64\\bin\\bash.exe") + || (lowerShellPath.endsWith("bash.exe") && detectPsCommandPath(shellPath) != null) + || (lowerShellPath.endsWith("sh.exe") && detectPsCommandPath(shellPath) != null); + } + + private static String detectPsCommandPath(String shellPath) { + if (shellPath == null) { + return null; + } + + Path psPath = Paths.get(shellPath, "..\\ps.exe"); + if (!Files.exists(psPath)) { + psPath = Paths.get(shellPath, "..\\..\\usr\\bin\\ps.exe"); + if (!Files.exists(psPath)) { + psPath = null; + } + } + + if (psPath == null) { + return null; + } + + return psPath.normalize().toString(); + } + private static Path tmpdir = null; private static synchronized Path getTmpDir() throws IOException { @@ -146,14 +364,29 @@ private static Path createTempFile(String baseName, String suffix) throws IOExce } } - private static String getMd5(String input) { + private static String getSha256(String input) { try { - MessageDigest md = MessageDigest.getInstance("MD5"); + MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] messageDigest = md.digest(input.getBytes()); - BigInteger md5 = new BigInteger(1, messageDigest); - return md5.toString(Character.MAX_RADIX); + // Use only first 16 bytes to keep filename shorter + byte[] truncated = new byte[16]; + System.arraycopy(messageDigest, 0, truncated, 0, 16); + BigInteger hash = new BigInteger(1, truncated); + return hash.toString(Character.MAX_RADIX); } catch (NoSuchAlgorithmException e) { return Integer.toString(input.hashCode(), Character.MAX_RADIX); } } + + private static class PsProcess { + long pid; + long ppid; + long winpid; + + public PsProcess(long pid, long ppid, long winpid) { + this.pid = pid; + this.ppid = ppid; + this.winpid = winpid; + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java index 11935fcbe..2962294a0 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithDebuggingDelegate.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -58,7 +58,6 @@ public class LaunchWithDebuggingDelegate implements ILaunchDelegate { protected static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); private static final int ATTACH_TERMINAL_TIMEOUT = 20 * 1000; - private static final String TERMINAL_TITLE = "Java Debug Console"; protected static final long RUNINTERMINAL_TIMEOUT = 10 * 1000; private VMHandler vmHandler = new VMHandler(); @@ -77,6 +76,8 @@ public CompletableFuture launchInTerminal(LaunchArguments launchArgume ((Connector.IntegerArgument) args.get("timeout")).setValue(ATTACH_TERMINAL_TIMEOUT); String address = listenConnector.startListening(args); + final String[] names = launchArguments.mainClass.split("[/\\.]"); + final String terminalName = "Debug: " + names[names.length - 1]; String[] cmds = LaunchRequestHandler.constructLaunchCommands(launchArguments, false, address); RunInTerminalRequestArguments requestArgs = null; if (launchArguments.console == CONSOLE.integratedTerminal) { @@ -84,13 +85,13 @@ public CompletableFuture launchInTerminal(LaunchArguments launchArgume cmds, launchArguments.cwd, launchArguments.env, - TERMINAL_TITLE); + terminalName); } else { requestArgs = RunInTerminalRequestArguments.createExternalTerminal( cmds, launchArguments.cwd, launchArguments.env, - TERMINAL_TITLE); + terminalName); } Request request = new Request(Command.RUNINTERMINAL.getName(), (JsonObject) JsonUtils.toJsonTree(requestArgs, RunInTerminalRequestArguments.class)); @@ -116,6 +117,12 @@ public CompletableFuture launchInTerminal(LaunchArguments launchArgume vmHandler.connectVirtualMachine(vm); context.setDebugSession(new DebugSession(vm)); logger.info("Launching debuggee in terminal console succeeded."); + if (context.getShellProcessId() > 0) { + ProcessHandle debuggeeProcess = LaunchUtils.findJavaProcessInTerminalShell(context.getShellProcessId(), cmds[0], 0); + if (debuggeeProcess != null) { + context.setProcessId(debuggeeProcess.pid()); + } + } resultFuture.complete(response); } catch (TransportTimeoutException e) { int commandLength = StringUtils.length(launchArguments.cwd) + 1; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java index 7b9a5be10..82a481718 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/LaunchWithoutDebuggingDelegate.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2018 Microsoft Corporation and others. +* Copyright (c) 2018-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -40,7 +40,6 @@ public class LaunchWithoutDebuggingDelegate implements ILaunchDelegate { protected static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); - protected static final String TERMINAL_TITLE = "Java Process Console"; protected static final long RUNINTERMINAL_TIMEOUT = 10 * 1000; private Consumer terminateHandler; @@ -92,14 +91,16 @@ public CompletableFuture launchInTerminal(LaunchArguments launchArgume final String launchInTerminalErrorFormat = "Failed to launch debuggee in terminal. Reason: %s"; + final String[] names = launchArguments.mainClass.split("[/\\.]"); + final String terminalName = "Run: " + names[names.length - 1]; String[] cmds = LaunchRequestHandler.constructLaunchCommands(launchArguments, false, null); RunInTerminalRequestArguments requestArgs = null; if (launchArguments.console == CONSOLE.integratedTerminal) { requestArgs = RunInTerminalRequestArguments.createIntegratedTerminal(cmds, launchArguments.cwd, - launchArguments.env, TERMINAL_TITLE); + launchArguments.env, terminalName); } else { requestArgs = RunInTerminalRequestArguments.createExternalTerminal(cmds, launchArguments.cwd, - launchArguments.env, TERMINAL_TITLE); + launchArguments.env, terminalName); } Request request = new Request(Command.RUNINTERMINAL.getName(), (JsonObject) JsonUtils.toJsonTree(requestArgs, RunInTerminalRequestArguments.class)); @@ -114,20 +115,32 @@ public CompletableFuture launchInTerminal(LaunchArguments launchArgume context.getProtocolServer().sendRequest(request, RUNINTERMINAL_TIMEOUT).whenComplete((runResponse, ex) -> { if (runResponse != null) { if (runResponse.success) { + ProcessHandle debuggeeProcess = null; try { RunInTerminalResponseBody terminalResponse = JsonUtils.fromJson( JsonUtils.toJson(runResponse.body), RunInTerminalResponseBody.class); context.setProcessId(terminalResponse.processId); context.setShellProcessId(terminalResponse.shellProcessId); + + if (terminalResponse.processId > 0) { + debuggeeProcess = ProcessHandle.of(terminalResponse.processId).orElse(null); + } else if (terminalResponse.shellProcessId > 0) { + debuggeeProcess = LaunchUtils.findJavaProcessInTerminalShell(terminalResponse.shellProcessId, cmds[0], 3000); + } + + if (debuggeeProcess != null) { + context.setProcessId(debuggeeProcess.pid()); + debuggeeProcess.onExit().thenAcceptAsync(proc -> { + context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); + }); + } } catch (JsonSyntaxException e) { logger.severe("Failed to resolve runInTerminal response: " + e.toString()); } - // TODO: Since the RunInTerminal request will return the pid or parent shell - // pid now, the debugger is able to use this pid to monitor the lifecycle - // of the running Java process. There is no need to terminate the debug - // session early here. - context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); + if (debuggeeProcess == null || !debuggeeProcess.isAlive()) { + context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); + } resultFuture.complete(response); } else { resultFuture.completeExceptionally( diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RefreshFramesHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RefreshFramesHandler.java new file mode 100644 index 000000000..b91daaf80 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RefreshFramesHandler.java @@ -0,0 +1,136 @@ +/******************************************************************************* +* Copyright (c) 2023 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core.adapter.handler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import com.microsoft.java.debug.core.AsyncJdwpUtils; +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; +import com.microsoft.java.debug.core.protocol.Events.StoppedEvent; +import com.microsoft.java.debug.core.protocol.Messages.Response; +import com.microsoft.java.debug.core.protocol.Requests.Arguments; +import com.microsoft.java.debug.core.protocol.Requests.Command; +import com.microsoft.java.debug.core.protocol.Requests.RefreshFramesArguments; +import com.sun.jdi.ObjectCollectedException; +import com.sun.jdi.ThreadReference; + +public class RefreshFramesHandler implements IDebugRequestHandler { + + @Override + public List getTargetCommands() { + return Arrays.asList(Command.REFRESHFRAMES); + } + + @Override + public CompletableFuture handle(Command command, Arguments arguments, Response response, + IDebugAdapterContext context) { + RefreshFramesArguments refreshArgs = (RefreshFramesArguments) arguments; + String[] affectedRootPaths = refreshArgs == null ? null : refreshArgs.affectedRootPaths; + List pausedThreads = getPausedThreads(context); + for (long threadId : pausedThreads) { + if (affectedRootPaths == null || affectedRootPaths.length == 0) { + refreshFrames(threadId, context); + continue; + } + + Set decompiledClasses = context.getThreadCache().getDecompiledClassesByThread(threadId); + if (decompiledClasses == null || decompiledClasses.isEmpty()) { + continue; + } + + if (anyInAffectedRootPaths(decompiledClasses, affectedRootPaths)) { + refreshFrames(threadId, context); + } + } + + return CompletableFuture.completedFuture(response); + } + + List getPausedThreads(IDebugAdapterContext context) { + List results = new ArrayList<>(); + List> futures = new ArrayList<>(); + List threads = context.getThreadCache().visibleThreads(context); + for (ThreadReference thread : threads) { + if (context.asyncJDWP()) { + futures.add(AsyncJdwpUtils.supplyAsync(() -> { + try { + if (thread.isSuspended()) { + return thread.uniqueID(); + } + } catch (ObjectCollectedException ex) { + // Ignore it if the thread is garbage collected. + } + + return -1L; + })); + } else { + try { + if (thread.isSuspended()) { + results.add(thread.uniqueID()); + } + } catch (ObjectCollectedException ex) { + // Ignore it if the thread is garbage collected. + } + } + } + + List awaitedResutls = AsyncJdwpUtils.await(futures); + for (Long threadId : awaitedResutls) { + if (threadId > 0) { + results.add(threadId); + } + } + + return results; + } + + /** + * See https://github.com/microsoft/vscode/issues/188606, + * VS Code doesn't provide a simple way to refetch the stack frames. + * We're going to resend a thread stopped event to trick the client + * into refetching the thread stack frames. + */ + void refreshFrames(long threadId, IDebugAdapterContext context) { + StoppedEvent stoppedEvent = new StoppedEvent(context.getThreadCache().getThreadStoppedReason(threadId), threadId); + stoppedEvent.preserveFocusHint = true; + context.getProtocolServer().sendEvent(stoppedEvent); + } + + boolean anyInAffectedRootPaths(Collection classes, String[] affectedRootPaths) { + if (affectedRootPaths == null || affectedRootPaths.length == 0) { + return true; + } + + for (String classUri : classes) { + // decompiled class uri is like 'jdt://contents/rt.jar/java.io/PrintStream.class?=1.helloworld/%5C/usr%5C/lib%5C/jvm%5C/ + // java-8-oracle%5C/jre%5C/lib%5C/rt.jar%3Cjava.io(PrintStream.class'. + if (classUri.startsWith("jdt://contents/")) { + String jarName = classUri.substring("jdt://contents/".length()); + int sep = jarName.indexOf("/"); + jarName = sep >= 0 ? jarName.substring(0, sep) : jarName; + for (String affectedRootPath : affectedRootPaths) { + if (affectedRootPath.endsWith("/" + jarName) || affectedRootPath.endsWith("\\" + jarName)) { + return true; + } + } + } + } + + return false; + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java index 3479c9a8a..26ebefaee 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RestartFrameHandler.java @@ -30,8 +30,10 @@ import com.microsoft.java.debug.core.protocol.Requests.Arguments; import com.microsoft.java.debug.core.protocol.Requests.Command; import com.microsoft.java.debug.core.protocol.Requests.RestartFrameArguments; +import com.sun.jdi.IncompatibleThreadStateException; import com.sun.jdi.StackFrame; import com.sun.jdi.ThreadReference; +import com.sun.jdi.request.EventRequest; import com.sun.jdi.request.StepRequest; /** @@ -59,7 +61,7 @@ public CompletableFuture handle(Command command, Arguments arguments, if (canRestartFrame(context, stackFrameReference)) { try { ThreadReference reference = stackFrameReference.getThread(); - popStackFrames(context, reference, stackFrameReference.getDepth()); + popStackFrames(context, stackFrameReference); stepInto(context, reference); } catch (DebugException de) { context.getProtocolServer().sendEvent(new Events.UserNotificationEvent(NotificationType.ERROR, de.getMessage())); @@ -80,10 +82,20 @@ private boolean canRestartFrame(IDebugAdapterContext context, StackFrameReferenc return false; } ThreadReference reference = frameReference.getThread(); - StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(reference); + int totalFrames; + try { + totalFrames = reference.frameCount(); + } catch (IncompatibleThreadStateException e) { + return false; + } // The frame cannot be the bottom one of the call stack: - if (frames.length <= frameReference.getDepth() + 1) { + if (totalFrames <= frameReference.getDepth() + 1) { + return false; + } + + StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(reference, 0, frameReference.getDepth() + 2); + if (frames.length == 0) { return false; } @@ -96,9 +108,12 @@ private boolean canRestartFrame(IDebugAdapterContext context, StackFrameReferenc return true; } - private void popStackFrames(IDebugAdapterContext context, ThreadReference thread, int depth) throws DebugException { - StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(thread); - StackFrameUtility.pop(frames[depth]); + private void popStackFrames(IDebugAdapterContext context, StackFrameReference stackFrameRef) throws DebugException { + StackFrame frame = context.getStackFrameManager().getStackFrame(stackFrameRef); + if (frame == null) { + return; + } + StackFrameUtility.pop(frame); } private void stepInto(IDebugAdapterContext context, ThreadReference thread) { @@ -107,7 +122,9 @@ private void stepInto(IDebugAdapterContext context, ThreadReference thread) { debugEvent.shouldResume = false; // Have to send two events to keep the UI sync with the step in operations: context.getProtocolServer().sendEvent(new Events.ContinuedEvent(thread.uniqueID())); - context.getProtocolServer().sendEvent(new Events.StoppedEvent("restartframe", thread.uniqueID())); + boolean allThreadsStopped = request.suspendPolicy() == EventRequest.SUSPEND_ALL; + context.getProtocolServer().sendEvent(new Events.StoppedEvent("restartframe", thread.uniqueID(), allThreadsStopped)); + context.getThreadCache().setThreadStoppedReason(thread.uniqueID(), "restartframe"); }); request.enable(); thread.resume(); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java index 2bc2b5f05..0f171486e 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetBreakpointsRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -17,15 +17,19 @@ import java.util.concurrent.CompletableFuture; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Stream; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugException; +import com.microsoft.java.debug.core.DebugSettings; +import com.microsoft.java.debug.core.DebugSettings.Switch; import com.microsoft.java.debug.core.IBreakpoint; import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.IEvaluatableBreakpoint; +import com.microsoft.java.debug.core.JavaBreakpointLocation; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.HotCodeReplaceEvent.EventType; @@ -44,14 +48,15 @@ import com.sun.jdi.BooleanValue; import com.sun.jdi.Field; import com.sun.jdi.ObjectReference; -import com.sun.jdi.StringReference; import com.sun.jdi.ReferenceType; +import com.sun.jdi.StringReference; import com.sun.jdi.ThreadReference; -import com.sun.jdi.Value; import com.sun.jdi.VMDisconnectedException; +import com.sun.jdi.Value; import com.sun.jdi.event.BreakpointEvent; import com.sun.jdi.event.Event; import com.sun.jdi.event.StepEvent; +import com.sun.jdi.request.EventRequest; public class SetBreakpointsRequestHandler implements IDebugRequestHandler { @@ -92,28 +97,7 @@ public CompletableFuture handle(Command command, Arguments arguments, } SetBreakpointArguments bpArguments = (SetBreakpointArguments) arguments; - String clientPath = bpArguments.source.path; - if (AdapterUtils.isWindows()) { - // VSCode may send drive letters with inconsistent casing which will mess up the key - // in the BreakpointManager. See https://github.com/Microsoft/vscode/issues/6268 - // Normalize the drive letter casing. Note that drive letters - // are not localized so invariant is safe here. - String drivePrefix = FilenameUtils.getPrefix(clientPath); - if (drivePrefix != null && drivePrefix.length() >= 2 - && Character.isLowerCase(drivePrefix.charAt(0)) && drivePrefix.charAt(1) == ':') { - drivePrefix = drivePrefix.substring(0, 2); // d:\ is an illegal regex string, convert it to d: - clientPath = clientPath.replaceFirst(drivePrefix, drivePrefix.toUpperCase()); - } - } - String sourcePath = clientPath; - if (bpArguments.source.sourceReference != 0 && context.getSourceUri(bpArguments.source.sourceReference) != null) { - sourcePath = context.getSourceUri(bpArguments.source.sourceReference); - } else if (StringUtils.isNotBlank(clientPath)) { - // See the bug https://github.com/Microsoft/vscode/issues/30996 - // Source.path in the SetBreakpointArguments could be a file system path or uri. - sourcePath = AdapterUtils.convertPath(clientPath, AdapterUtils.isUri(clientPath), context.isDebuggerPathsAreUri()); - } - + String sourcePath = normalizeSourcePath(bpArguments.source, context); // When breakpoint source path is null or an invalid file path, send an ErrorResponse back. if (StringUtils.isBlank(sourcePath)) { throw AdapterUtils.createCompletionException( @@ -129,10 +113,11 @@ public CompletableFuture handle(Command command, Arguments arguments, IBreakpoint[] added = context.getBreakpointManager() .setBreakpoints(AdapterUtils.decodeURIComponent(sourcePath), toAdds, bpArguments.sourceModified); for (int i = 0; i < bpArguments.breakpoints.length; i++) { + added[i].setAsync(context.asyncJDWP()); // For newly added breakpoint, should install it to debuggee first. if (toAdds[i] == added[i] && added[i].className() != null) { added[i].install().thenAccept(bp -> { - Events.BreakpointEvent bpEvent = new Events.BreakpointEvent("new", this.convertDebuggerBreakpointToClient(bp, context)); + Events.BreakpointEvent bpEvent = new Events.BreakpointEvent("changed", this.convertDebuggerBreakpointToClient(bp, context)); context.getProtocolServer().sendEvent(bpEvent); }); } else if (added[i].className() != null) { @@ -161,6 +146,32 @@ public CompletableFuture handle(Command command, Arguments arguments, } } + public static String normalizeSourcePath(Types.Source source, IDebugAdapterContext context) { + String clientPath = source.path; + if (AdapterUtils.isWindows()) { + // VSCode may send drive letters with inconsistent casing which will mess up the key + // in the BreakpointManager. See https://github.com/Microsoft/vscode/issues/6268 + // Normalize the drive letter casing. Note that drive letters + // are not localized so invariant is safe here. + String drivePrefix = FilenameUtils.getPrefix(clientPath); + if (drivePrefix != null && drivePrefix.length() >= 2 + && Character.isLowerCase(drivePrefix.charAt(0)) && drivePrefix.charAt(1) == ':') { + drivePrefix = drivePrefix.substring(0, 2); // d:\ is an illegal regex string, convert it to d: + clientPath = clientPath.replaceFirst(drivePrefix, drivePrefix.toUpperCase()); + } + } + String sourcePath = clientPath; + if (source.sourceReference != 0 && context.getSourceUri(source.sourceReference) != null) { + sourcePath = context.getSourceUri(source.sourceReference); + } else if (StringUtils.isNotBlank(clientPath)) { + // See the bug https://github.com/Microsoft/vscode/issues/30996 + // Source.path in the SetBreakpointArguments could be a file system path or uri. + sourcePath = AdapterUtils.convertPath(clientPath, AdapterUtils.isUri(clientPath), context.isDebuggerPathsAreUri()); + } + + return sourcePath; + } + private IBreakpoint getAssociatedEvaluatableBreakpoint(IDebugAdapterContext context, BreakpointEvent event) { return Arrays.asList(context.getBreakpointManager().getBreakpoints()).stream().filter( bp -> { @@ -188,6 +199,7 @@ private void registerBreakpointHandler(IDebugAdapterContext context) { // find the breakpoint related to this breakpoint event IBreakpoint expressionBP = getAssociatedEvaluatableBreakpoint(context, (BreakpointEvent) event); + String breakpointName = computeBreakpointName(event.request()); if (expressionBP != null) { CompletableFuture.runAsync(() -> { @@ -199,12 +211,20 @@ private void registerBreakpointHandler(IDebugAdapterContext context) { if (resume) { debugEvent.eventSet.resume(); } else { - context.getProtocolServer().sendEvent(new Events.StoppedEvent("breakpoint", bpThread.uniqueID())); + context.getThreadCache().addEventThread(bpThread, breakpointName); + boolean allThreadsStopped = event.request() != null + && event.request().suspendPolicy() == EventRequest.SUSPEND_ALL; + context.getProtocolServer().sendEvent(new Events.StoppedEvent( + breakpointName, bpThread.uniqueID(), allThreadsStopped)); } }); }); } else { - context.getProtocolServer().sendEvent(new Events.StoppedEvent("breakpoint", bpThread.uniqueID())); + context.getThreadCache().addEventThread(bpThread, breakpointName); + boolean allThreadsStopped = event.request() != null + && event.request().suspendPolicy() == EventRequest.SUSPEND_ALL; + context.getProtocolServer().sendEvent(new Events.StoppedEvent( + breakpointName, bpThread.uniqueID(), allThreadsStopped)); } debugEvent.shouldResume = false; } @@ -212,6 +232,17 @@ private void registerBreakpointHandler(IDebugAdapterContext context) { } } + private String computeBreakpointName(EventRequest request) { + switch ((int) request.getProperty(IBreakpoint.REQUEST_TYPE)) { + case IBreakpoint.REQUEST_TYPE_LAMBDA: + return "lambda breakpoint"; + case IBreakpoint.REQUEST_TYPE_METHOD: + return "function breakpoint"; + default: + return "breakpoint"; + } + } + /** * Check whether the condition expression is satisfied, and return a boolean value to determine to resume the thread or not. */ @@ -271,28 +302,45 @@ public static boolean handleEvaluationResult(IDebugAdapterContext context, Threa private Types.Breakpoint convertDebuggerBreakpointToClient(IBreakpoint breakpoint, IDebugAdapterContext context) { int id = (int) breakpoint.getProperty("id"); boolean verified = breakpoint.getProperty("verified") != null && (boolean) breakpoint.getProperty("verified"); - int lineNumber = AdapterUtils.convertLineNumber(breakpoint.getLineNumber(), context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + int lineNumber = AdapterUtils.convertLineNumber(breakpoint.sourceLocation().lineNumberInSourceFile(), + context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); return new Types.Breakpoint(id, verified, lineNumber, ""); } private IBreakpoint[] convertClientBreakpointsToDebugger(String sourceFile, Types.SourceBreakpoint[] sourceBreakpoints, IDebugAdapterContext context) throws DebugException { - int[] lines = Arrays.asList(sourceBreakpoints).stream().map(sourceBreakpoint -> { - return AdapterUtils.convertLineNumber(sourceBreakpoint.line, context.isClientLinesStartAt1(), context.isDebuggerLinesStartAt1()); - }).mapToInt(line -> line).toArray(); + Types.SourceBreakpoint[] debugSourceBreakpoints = Stream.of(sourceBreakpoints).map(sourceBreakpoint -> { + int line = AdapterUtils.convertLineNumber(sourceBreakpoint.line, context.isClientLinesStartAt1(), context.isDebuggerLinesStartAt1()); + int column = AdapterUtils.convertColumnNumber(sourceBreakpoint.column, context.isClientColumnsStartAt1(), context.isDebuggerColumnsStartAt1()); + return new Types.SourceBreakpoint(line, column); + }).toArray(Types.SourceBreakpoint[]::new); + ISourceLookUpProvider sourceProvider = context.getProvider(ISourceLookUpProvider.class); - String[] fqns = sourceProvider.getFullyQualifiedName(sourceFile, lines, null); - IBreakpoint[] breakpoints = new IBreakpoint[lines.length]; - for (int i = 0; i < lines.length; i++) { + JavaBreakpointLocation[] locations = sourceProvider.getBreakpointLocations(sourceFile, debugSourceBreakpoints); + IBreakpoint[] breakpoints = new IBreakpoint[locations.length]; + for (int i = 0; i < locations.length; i++) { int hitCount = 0; try { hitCount = Integer.parseInt(sourceBreakpoints[i].hitCondition); } catch (NumberFormatException e) { hitCount = 0; // If hitCount is an illegal number, ignore hitCount condition. } - breakpoints[i] = context.getDebugSession().createBreakpoint(fqns[i], lines[i], hitCount, sourceBreakpoints[i].condition, + + if (DebugSettings.getCurrent().debugSupportOnDecompiledSource == Switch.ON) { + // Align the decompiled line with the original line. + int[] lineMappings = sourceProvider.getDecompiledLineMappings(sourceFile); + if (locations[i] != null && lineMappings != null) { + int lineNumberInSourceFile = locations[i].lineNumber(); + int[] originalLines = AdapterUtils.binarySearchMappedLines(lineMappings, lineNumberInSourceFile); + if (originalLines != null && originalLines.length > 0) { + locations[i].setLineNumberInSourceFile(lineNumberInSourceFile); + locations[i].setLineNumber(originalLines[0]); + } + } + } + breakpoints[i] = context.getDebugSession().createBreakpoint(locations[i], hitCount, sourceBreakpoints[i].condition, sourceBreakpoints[i].logMessage); - if (sourceProvider.supportsRealtimeBreakpointVerification() && StringUtils.isNotBlank(fqns[i])) { + if (sourceProvider.supportsRealtimeBreakpointVerification() && StringUtils.isNotBlank(locations[i].className())) { breakpoints[i].putProperty("verified", true); } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetDataBreakpointsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetDataBreakpointsRequestHandler.java index be15852e4..c7c0995fe 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetDataBreakpointsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetDataBreakpointsRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2019 Microsoft Corporation and others. +* Copyright (c) 2019-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -41,6 +41,7 @@ import com.sun.jdi.ThreadReference; import com.sun.jdi.event.Event; import com.sun.jdi.event.WatchpointEvent; +import com.sun.jdi.request.EventRequest; public class SetDataBreakpointsRequestHandler implements IDebugRequestHandler { private boolean registered = false; @@ -151,12 +152,18 @@ private void registerWatchpointHandler(IDebugAdapterContext context) { if (resume) { debugEvent.eventSet.resume(); } else { - context.getProtocolServer().sendEvent(new Events.StoppedEvent("data breakpoint", bpThread.uniqueID())); + context.getThreadCache().addEventThread(bpThread, "data breakpoint"); + boolean allThreadsStopped = event.request() != null + && event.request().suspendPolicy() == EventRequest.SUSPEND_ALL; + context.getProtocolServer().sendEvent(new Events.StoppedEvent("data breakpoint", bpThread.uniqueID(), allThreadsStopped)); } }); }); } else { - context.getProtocolServer().sendEvent(new Events.StoppedEvent("data breakpoint", bpThread.uniqueID())); + context.getThreadCache().addEventThread(bpThread, "data breakpoint"); + boolean allThreadsStopped = event.request() != null + && event.request().suspendPolicy() == EventRequest.SUSPEND_ALL; + context.getProtocolServer().sendEvent(new Events.StoppedEvent("data breakpoint", bpThread.uniqueID(), allThreadsStopped)); } debugEvent.shouldResume = false; }); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetExceptionBreakpointsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetExceptionBreakpointsRequestHandler.java index 3a4e642c8..b51c5fe27 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetExceptionBreakpointsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetExceptionBreakpointsRequestHandler.java @@ -26,8 +26,8 @@ import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.Arguments; -import com.microsoft.java.debug.core.protocol.Requests.ClassFilters; import com.microsoft.java.debug.core.protocol.Requests.Command; +import com.microsoft.java.debug.core.protocol.Requests.ExceptionFilters; import com.microsoft.java.debug.core.protocol.Requests.SetExceptionBreakpointsArguments; import com.microsoft.java.debug.core.protocol.Types; import com.sun.jdi.event.VMDeathEvent; @@ -38,6 +38,7 @@ public class SetExceptionBreakpointsRequestHandler implements IDebugRequestHandl private boolean isInitialized = false; private boolean notifyCaught = false; private boolean notifyUncaught = false; + private boolean asyncJDWP = false; @Override public List getTargetCommands() { @@ -53,6 +54,7 @@ public synchronized CompletableFuture handle(Command command, Argument if (!isInitialized) { isInitialized = true; debugSession = context.getDebugSession(); + asyncJDWP = context.asyncJDWP(); DebugSettings.addDebugSettingChangeListener(this); debugSession.getEventHub().events().subscribe(debugEvent -> { if (debugEvent.event instanceof VMDeathEvent @@ -77,10 +79,11 @@ public synchronized CompletableFuture handle(Command command, Argument } private void setExceptionBreakpoints(IDebugSession debugSession, boolean notifyCaught, boolean notifyUncaught) { - ClassFilters exceptionFilters = DebugSettings.getCurrent().exceptionFilters; + ExceptionFilters exceptionFilters = DebugSettings.getCurrent().exceptionFilters; + String[] exceptionTypes = (exceptionFilters == null ? null : exceptionFilters.exceptionTypes); String[] classFilters = (exceptionFilters == null ? null : exceptionFilters.allowClasses); String[] classExclusionFilters = (exceptionFilters == null ? null : exceptionFilters.skipClasses); - debugSession.setExceptionBreakpoints(notifyCaught, notifyUncaught, classFilters, classExclusionFilters); + debugSession.setExceptionBreakpoints(notifyCaught, notifyUncaught, exceptionTypes, classFilters, classExclusionFilters, this.asyncJDWP); } @Override diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetFunctionBreakpointsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetFunctionBreakpointsRequestHandler.java new file mode 100644 index 000000000..01b5e9619 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetFunctionBreakpointsRequestHandler.java @@ -0,0 +1,197 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Gayan Perera - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug.core.adapter.handler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; + +import com.microsoft.java.debug.core.IDebugSession; +import com.microsoft.java.debug.core.IEvaluatableBreakpoint; +import com.microsoft.java.debug.core.IMethodBreakpoint; +import com.microsoft.java.debug.core.MethodBreakpoint; +import com.microsoft.java.debug.core.adapter.AdapterUtils; +import com.microsoft.java.debug.core.adapter.ErrorCode; +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; +import com.microsoft.java.debug.core.adapter.IEvaluationProvider; +import com.microsoft.java.debug.core.protocol.Events; +import com.microsoft.java.debug.core.protocol.Events.BreakpointEvent; +import com.microsoft.java.debug.core.protocol.Messages.Response; +import com.microsoft.java.debug.core.protocol.Requests.Arguments; +import com.microsoft.java.debug.core.protocol.Requests.Command; +import com.microsoft.java.debug.core.protocol.Requests.SetFunctionBreakpointsArguments; +import com.microsoft.java.debug.core.protocol.Responses; +import com.microsoft.java.debug.core.protocol.Types.Breakpoint; +import com.microsoft.java.debug.core.protocol.Types.FunctionBreakpoint; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.event.MethodEntryEvent; +import com.sun.jdi.request.EventRequest; + +public class SetFunctionBreakpointsRequestHandler implements IDebugRequestHandler { + private boolean registered = false; + + @Override + public List getTargetCommands() { + return Arrays.asList(Command.SETFUNCTIONBREAKPOINTS); + } + + @Override + public CompletableFuture handle(Command command, Arguments arguments, Response response, + IDebugAdapterContext context) { + if (context.getDebugSession() == null) { + return AdapterUtils.createAsyncErrorResponse(response, ErrorCode.EMPTY_DEBUG_SESSION, + "Empty debug session."); + } + + if (!registered) { + registered = true; + registerMethodBreakpointHandler(context); + } + + SetFunctionBreakpointsArguments funcBpArgs = (SetFunctionBreakpointsArguments) arguments; + IMethodBreakpoint[] requestedMethodBreakpoints = (funcBpArgs.breakpoints == null) ? new IMethodBreakpoint[0] + : new MethodBreakpoint[funcBpArgs.breakpoints.length]; + for (int i = 0; i < requestedMethodBreakpoints.length; i++) { + FunctionBreakpoint funcBreakpoint = funcBpArgs.breakpoints[i]; + if (funcBreakpoint.name != null) { + String[] segments = funcBreakpoint.name.split("#"); + if (segments.length == 2 && StringUtils.isNotBlank(segments[0]) + && StringUtils.isNotBlank(segments[1])) { + int hitCount = 0; + try { + hitCount = Integer.parseInt(funcBreakpoint.hitCondition); + } catch (NumberFormatException e) { + hitCount = 0; // If hitCount is an illegal number, ignore hitCount condition. + } + requestedMethodBreakpoints[i] = context.getDebugSession().createFunctionBreakpoint(segments[0], + segments[1], + funcBreakpoint.condition, hitCount); + } + } + } + + IMethodBreakpoint[] currentMethodBreakpoints = context.getBreakpointManager() + .setMethodBreakpoints(requestedMethodBreakpoints); + List breakpoints = new ArrayList<>(); + for (int i = 0; i < currentMethodBreakpoints.length; i++) { + if (currentMethodBreakpoints[i] == null) { + breakpoints.add(new Breakpoint(false)); + continue; + } + + currentMethodBreakpoints[i].setAsync(context.asyncJDWP()); + // If the requested method breakpoint exists in the manager, it will reuse + // the cached breakpoint exists object. + // Otherwise add the requested method breakpoint to the cache. + // So if the returned method breakpoint from the manager is same as the + // requested method breakpoint, this means it's a new method breakpoint, need + // install it. + if (currentMethodBreakpoints[i] == requestedMethodBreakpoints[i]) { + currentMethodBreakpoints[i].install().thenAccept(wp -> { + BreakpointEvent bpEvent = new BreakpointEvent("changed", convertDebuggerMethodToClient(wp)); + context.getProtocolServer().sendEvent(bpEvent); + }); + } else { + if (currentMethodBreakpoints[i].getHitCount() != requestedMethodBreakpoints[i].getHitCount()) { + currentMethodBreakpoints[i].setHitCount(requestedMethodBreakpoints[i].getHitCount()); + } + + if (!Objects.equals(currentMethodBreakpoints[i].getCondition(), + requestedMethodBreakpoints[i].getCondition())) { + currentMethodBreakpoints[i].setCondition(requestedMethodBreakpoints[i].getCondition()); + } + } + + breakpoints.add(convertDebuggerMethodToClient(currentMethodBreakpoints[i])); + } + + response.body = new Responses.SetDataBreakpointsResponseBody(breakpoints); + return CompletableFuture.completedFuture(response); + } + + private Breakpoint convertDebuggerMethodToClient(IMethodBreakpoint methodBreakpoint) { + return new Breakpoint((int) methodBreakpoint.getProperty("id"), + methodBreakpoint.getProperty("verified") != null && (boolean) methodBreakpoint.getProperty("verified")); + } + + private void registerMethodBreakpointHandler(IDebugAdapterContext context) { + IDebugSession debugSession = context.getDebugSession(); + if (debugSession != null) { + debugSession.getEventHub().events().filter(debugEvent -> debugEvent.event instanceof MethodEntryEvent) + .subscribe(debugEvent -> { + MethodEntryEvent methodEntryEvent = (MethodEntryEvent) debugEvent.event; + ThreadReference bpThread = methodEntryEvent.thread(); + IEvaluationProvider engine = context.getProvider(IEvaluationProvider.class); + + // Find the method breakpoint related to this method entry event + IMethodBreakpoint methodBreakpoint = Stream + .of(context.getBreakpointManager().getMethodBreakpoints()) + .filter(mp -> { + return mp.requests().contains(methodEntryEvent.request()) + && matches(methodEntryEvent, mp); + }) + .findFirst().orElse(null); + + if (methodBreakpoint != null) { + if (methodBreakpoint instanceof IEvaluatableBreakpoint + && ((IEvaluatableBreakpoint) methodBreakpoint).containsConditionalExpression()) { + if (engine.isInEvaluation(bpThread)) { + return; + } + CompletableFuture.runAsync(() -> { + engine.evaluateForBreakpoint((IEvaluatableBreakpoint) methodBreakpoint, bpThread) + .whenComplete((value, ex) -> { + boolean resume = SetBreakpointsRequestHandler.handleEvaluationResult( + context, bpThread, (IEvaluatableBreakpoint) methodBreakpoint, + value, + ex); + // Clear the evaluation environment caused by above evaluation. + engine.clearState(bpThread); + + if (resume) { + debugEvent.eventSet.resume(); + } else { + context.getThreadCache().addEventThread(bpThread, "function breakpoint"); + boolean allThreadsStopped = methodEntryEvent.request() != null + && methodEntryEvent.request().suspendPolicy() == EventRequest.SUSPEND_ALL; + context.getProtocolServer().sendEvent(new Events.StoppedEvent( + "function breakpoint", bpThread.uniqueID(), allThreadsStopped)); + } + }); + }); + + } else { + context.getThreadCache().addEventThread(bpThread, "function breakpoint"); + boolean allThreadsStopped = methodEntryEvent.request() != null + && methodEntryEvent.request().suspendPolicy() == EventRequest.SUSPEND_ALL; + context.getProtocolServer() + .sendEvent(new Events.StoppedEvent("function breakpoint", bpThread.uniqueID(), allThreadsStopped)); + } + + debugEvent.shouldResume = false; + } + }); + } + } + + private boolean matches(MethodEntryEvent methodEntryEvent, IMethodBreakpoint breakpoint) { + return breakpoint.className().equals(methodEntryEvent.location().declaringType().name()) + && breakpoint.methodName().equals(methodEntryEvent.method().name()); + } + +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java index 089a55b9d..6f54bcedb 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StackTraceRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -15,19 +15,32 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import com.google.gson.JsonObject; +import com.microsoft.java.debug.core.AsyncJdwpUtils; +import com.microsoft.java.debug.core.DebugSettings; +import com.microsoft.java.debug.core.DebugSettings.Switch; import com.microsoft.java.debug.core.DebugUtility; +import com.microsoft.java.debug.core.IBreakpoint; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider; +import com.microsoft.java.debug.core.adapter.Source; +import com.microsoft.java.debug.core.adapter.SourceType; import com.microsoft.java.debug.core.adapter.formatter.SimpleTypeFormatter; import com.microsoft.java.debug.core.adapter.variables.StackFrameReference; +import com.microsoft.java.debug.core.protocol.Events.TelemetryEvent; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.Arguments; import com.microsoft.java.debug.core.protocol.Requests.Command; @@ -36,13 +49,18 @@ import com.microsoft.java.debug.core.protocol.Types; import com.sun.jdi.AbsentInformationException; import com.sun.jdi.IncompatibleThreadStateException; +import com.sun.jdi.LocalVariable; import com.sun.jdi.Location; import com.sun.jdi.Method; import com.sun.jdi.ObjectCollectedException; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; import com.sun.jdi.StackFrame; import com.sun.jdi.ThreadReference; +import com.sun.jdi.request.BreakpointRequest; public class StackTraceRequestHandler implements IDebugRequestHandler { + private ThreadLocal isDecompilerInvoked = new ThreadLocal<>(); @Override public List getTargetCommands() { @@ -51,52 +69,152 @@ public List getTargetCommands() { @Override public CompletableFuture handle(Command command, Arguments arguments, Response response, IDebugAdapterContext context) { + final long startAt = System.currentTimeMillis(); + isDecompilerInvoked.set(false); StackTraceArguments stacktraceArgs = (StackTraceArguments) arguments; List result = new ArrayList<>(); if (stacktraceArgs.startFrame < 0 || stacktraceArgs.levels < 0) { response.body = new Responses.StackTraceResponseBody(result, 0); return CompletableFuture.completedFuture(response); } - ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), stacktraceArgs.threadId); + long threadId = stacktraceArgs.threadId; + ThreadReference thread = context.getThreadCache().getThread(threadId); + if (thread == null) { + thread = DebugUtility.getThread(context.getDebugSession(), threadId); + } int totalFrames = 0; if (thread != null) { + Set decompiledClasses = new LinkedHashSet<>(); try { + // Thread state has changed and then invalidate the stack frame cache. + if (stacktraceArgs.startFrame == 0) { + context.getStackFrameManager().clearStackFrames(thread); + } else { + Set existing = context.getThreadCache().getDecompiledClassesByThread(threadId); + if (existing != null) { + decompiledClasses.addAll(existing); + } + } + totalFrames = thread.frameCount(); + int count = stacktraceArgs.levels == 0 ? totalFrames - stacktraceArgs.startFrame + : Math.min(totalFrames - stacktraceArgs.startFrame, stacktraceArgs.levels); if (totalFrames <= stacktraceArgs.startFrame) { response.body = new Responses.StackTraceResponseBody(result, totalFrames); return CompletableFuture.completedFuture(response); } - StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(thread); - int count = stacktraceArgs.levels == 0 ? totalFrames - stacktraceArgs.startFrame - : Math.min(totalFrames - stacktraceArgs.startFrame, stacktraceArgs.levels); - for (int i = stacktraceArgs.startFrame; i < frames.length && count-- > 0; i++) { - StackFrameReference stackframe = new StackFrameReference(thread, i); - int frameId = context.getRecyclableIdPool().addObject(thread.uniqueID(), stackframe); - result.add(convertDebuggerStackFrameToClient(frames[i], frameId, context)); + StackFrame[] frames = context.getStackFrameManager().reloadStackFrames(thread, stacktraceArgs.startFrame, count); + List jdiFrames = resolveStackFrameInfos(frames, context.asyncJDWP()); + for (int i = 0; i < count; i++) { + StackFrameReference frameReference = new StackFrameReference(thread, stacktraceArgs.startFrame + i); + int frameId = context.getRecyclableIdPool().addObject(stacktraceArgs.threadId, frameReference); + StackFrameInfo jdiFrame = jdiFrames.get(i); + Types.StackFrame lspFrame = convertDebuggerStackFrameToClient(jdiFrame, frameId, i == 0, context); + result.add(lspFrame); + frameReference.setSource(lspFrame.source); + int jdiLineNumber = AdapterUtils.convertLineNumber(jdiFrame.lineNumber, context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + if (jdiLineNumber != lspFrame.line && lspFrame.source != null && lspFrame.source.path != null) { + decompiledClasses.add(lspFrame.source.path); + } } } catch (IncompatibleThreadStateException | IndexOutOfBoundsException | URISyntaxException - | AbsentInformationException | ObjectCollectedException e) { + | AbsentInformationException | ObjectCollectedException + | CancellationException | CompletionException e) { // when error happens, the possible reason is: // 1. the vscode has wrong parameter/wrong uri // 2. the thread actually terminates // TODO: should record a error log here. + } finally { + context.getThreadCache().setDecompiledClassesByThread(threadId, decompiledClasses); } } response.body = new Responses.StackTraceResponseBody(result, totalFrames); + long duration = System.currentTimeMillis() - startAt; + JsonObject properties = new JsonObject(); + properties.addProperty("command", "stackTrace"); + properties.addProperty("duration", duration); + properties.addProperty("decompileSupport", DebugSettings.getCurrent().debugSupportOnDecompiledSource.toString()); + if (isDecompilerInvoked.get() != null) { + properties.addProperty("isDecompilerInvoked", Boolean.toString(isDecompilerInvoked.get())); + } + context.getProtocolServer().sendEvent(new TelemetryEvent("dap", properties)); return CompletableFuture.completedFuture(response); } - private Types.StackFrame convertDebuggerStackFrameToClient(StackFrame stackFrame, int frameId, IDebugAdapterContext context) + private static List resolveStackFrameInfos(StackFrame[] frames, boolean async) + throws AbsentInformationException, IncompatibleThreadStateException { + List jdiFrames = new ArrayList<>(); + List> futures = new ArrayList<>(); + for (StackFrame frame : frames) { + StackFrameInfo jdiFrame = new StackFrameInfo(frame); + jdiFrame.location = jdiFrame.frame.location(); + jdiFrame.method = jdiFrame.location.method(); + jdiFrame.methodName = jdiFrame.method.name(); + jdiFrame.isNative = jdiFrame.method.isNative(); + jdiFrame.declaringType = jdiFrame.location.declaringType(); + if (async) { + // JDWP Command: M_LINE_TABLE + futures.add(AsyncJdwpUtils.runAsync(() -> { + jdiFrame.lineNumber = jdiFrame.location.lineNumber(); + })); + + // JDWP Commands: RT_SOURCE_DEBUG_EXTENSION, RT_SOURCE_FILE + futures.add(AsyncJdwpUtils.runAsync(() -> { + try { + // When the .class file doesn't contain source information in meta data, + // invoking Location#sourceName() would throw AbsentInformationException. + jdiFrame.sourceName = jdiFrame.declaringType.sourceName(); + } catch (AbsentInformationException e) { + jdiFrame.sourceName = null; + } + })); + + // JDWP Command: RT_SIGNATURE + futures.add(AsyncJdwpUtils.runAsync(() -> { + jdiFrame.typeSignature = jdiFrame.declaringType.signature(); + })); + } else { + jdiFrame.lineNumber = jdiFrame.location.lineNumber(); + jdiFrame.typeSignature = jdiFrame.declaringType.signature(); + try { + // When the .class file doesn't contain source information in meta data, + // invoking Location#sourceName() would throw AbsentInformationException. + jdiFrame.sourceName = jdiFrame.declaringType.sourceName(); + } catch (AbsentInformationException e) { + jdiFrame.sourceName = null; + } + } + + jdiFrames.add(jdiFrame); + } + + AsyncJdwpUtils.await(futures); + for (StackFrameInfo jdiFrame : jdiFrames) { + jdiFrame.typeName = jdiFrame.declaringType.name(); + jdiFrame.argumentTypeNames = jdiFrame.method.argumentTypeNames(); + if (jdiFrame.sourceName == null) { + String enclosingType = AdapterUtils.parseEnclosingType(jdiFrame.typeName); + jdiFrame.sourceName = enclosingType.substring(enclosingType.lastIndexOf('.') + 1) + ".java"; + jdiFrame.sourcePath = enclosingType.replace('.', File.separatorChar) + ".java"; + } else { + jdiFrame.sourcePath = jdiFrame.declaringType.sourcePaths(null).get(0); + } + } + + return jdiFrames; + } + + private Types.StackFrame convertDebuggerStackFrameToClient(StackFrameInfo jdiFrame, int frameId, boolean isTopFrame, IDebugAdapterContext context) throws URISyntaxException, AbsentInformationException { - Location location = stackFrame.location(); - Method method = location.method(); - Types.Source clientSource = this.convertDebuggerSourceToClient(location, context); - String methodName = formatMethodName(method, true, true); - int lineNumber = AdapterUtils.convertLineNumber(location.lineNumber(), context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + Types.Source clientSource = convertDebuggerSourceToClient(jdiFrame.typeName, jdiFrame.sourceName, jdiFrame.sourcePath, context); + String methodName = formatMethodName(jdiFrame.methodName, jdiFrame.argumentTypeNames, jdiFrame.typeName, true, true); + int clientLineNumber = AdapterUtils.convertLineNumber(jdiFrame.lineNumber, context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); // Line number returns -1 if the information is not available; specifically, always returns -1 for native methods. - if (lineNumber < 0) { - if (method.isNative()) { + String presentationHint = null; + if (clientLineNumber < 0) { + presentationHint = "subtle"; + if (jdiFrame.isNative) { // For native method, display a tip text "native method" in the Call Stack View. methodName += "[native method]"; } else { @@ -104,78 +222,124 @@ private Types.StackFrame convertDebuggerStackFrameToClient(StackFrame stackFrame // display "Unknown Source" in the Call Stack View. clientSource = null; } + // DAP specifies lineNumber to be set to 0 when unavailable + clientLineNumber = 0; + } else if (DebugSettings.getCurrent().debugSupportOnDecompiledSource == Switch.ON + && clientSource != null && clientSource.path != null) { + // Align the original line with the decompiled line. + int[] lineMappings = context.getProvider(ISourceLookUpProvider.class).getOriginalLineMappings(clientSource.path); + int[] renderLines = AdapterUtils.binarySearchMappedLines(lineMappings, clientLineNumber); + if (renderLines != null && renderLines.length > 0) { + clientLineNumber = renderLines[0]; + isDecompilerInvoked.set(true); + } } - return new Types.StackFrame(frameId, methodName, clientSource, lineNumber, context.isClientColumnsStartAt1() ? 1 : 0); - } - private Types.Source convertDebuggerSourceToClient(Location location, IDebugAdapterContext context) throws URISyntaxException { - final String fullyQualifiedName = location.declaringType().name(); - String sourceName = ""; - String relativeSourcePath = ""; - try { - // When the .class file doesn't contain source information in meta data, - // invoking Location#sourceName() would throw AbsentInformationException. - sourceName = location.sourceName(); - relativeSourcePath = location.sourcePath(); - } catch (AbsentInformationException e) { - String enclosingType = AdapterUtils.parseEnclosingType(fullyQualifiedName); - sourceName = enclosingType.substring(enclosingType.lastIndexOf('.') + 1) + ".java"; - relativeSourcePath = enclosingType.replace('.', File.separatorChar) + ".java"; + int clientColumnNumber = context.isClientColumnsStartAt1() ? 1 : 0; + // If the top-level frame is a lambda method, it might be paused on a lambda breakpoint. + // We can associate its column number with the target lambda breakpoint. + if (isTopFrame && jdiFrame.methodName.startsWith("lambda$")) { + for (IBreakpoint breakpoint : context.getBreakpointManager().getBreakpoints()) { + if (breakpoint.getColumnNumber() > 0 && breakpoint.getLineNumber() == jdiFrame.lineNumber + && Objects.equals(jdiFrame.typeName, breakpoint.className())) { + boolean match = breakpoint.requests().stream().anyMatch(request -> { + return request instanceof BreakpointRequest + && Objects.equals(((BreakpointRequest) request).location(), jdiFrame.location); + }); + if (match) { + clientColumnNumber = AdapterUtils.convertColumnNumber(breakpoint.getColumnNumber(), + context.isDebuggerColumnsStartAt1(), context.isClientColumnsStartAt1()); + } + } + } } - return convertDebuggerSourceToClient(fullyQualifiedName, sourceName, relativeSourcePath, context); + return new Types.StackFrame(frameId, methodName, clientSource, clientLineNumber, clientColumnNumber, presentationHint); } /** * Find the source mapping for the specified source file name. */ - public static Types.Source convertDebuggerSourceToClient(String fullyQualifiedName, String sourceName, String relativeSourcePath, + public static Types.Source convertDebuggerSourceToClient(String fullyQualifiedName, String sourceName, + String relativeSourcePath, IDebugAdapterContext context) throws URISyntaxException { + // use a lru cache for better performance - String uri = context.getSourceLookupCache().computeIfAbsent(fullyQualifiedName, key -> { - String fromProvider = context.getProvider(ISourceLookUpProvider.class).getSourceFileURI(key, relativeSourcePath); - // avoid return null which will cause the compute function executed again - return StringUtils.isBlank(fromProvider) ? "" : fromProvider; + Source source = context.getSourceLookupCache().computeIfAbsent(fullyQualifiedName, key -> { + Source result = context.getProvider(ISourceLookUpProvider.class).getSource(key, relativeSourcePath); + if (result == null) { + return new Source("", SourceType.LOCAL); + } + return result; }); + Integer sourceReference = 0; + String uri = source.getUri(); + + if (source.getType().equals(SourceType.REMOTE)) { + sourceReference = context.createSourceReference(source.getUri()); + } + if (!StringUtils.isBlank(uri)) { // The Source.path could be a file system path or uri string. if (uri.startsWith("file:")) { String clientPath = AdapterUtils.convertPath(uri, context.isDebuggerPathsAreUri(), context.isClientPathsAreUri()); - return new Types.Source(sourceName, clientPath, 0); + return new Types.Source(sourceName, clientPath, sourceReference); } else { // If the debugger returns uri in the Source.path for the StackTrace response, VSCode client will try to find a TextDocumentContentProvider // to render the contents. // Language Support for Java by Red Hat extension has already registered a jdt TextDocumentContentProvider to parse the jdt-based uri. // The jdt uri looks like 'jdt://contents/rt.jar/java.io/PrintStream.class?=1.helloworld/%5C/usr%5C/lib%5C/jvm%5C/java-8-oracle%5C/jre%5C/ // lib%5C/rt.jar%3Cjava.io(PrintStream.class'. - return new Types.Source(sourceName, uri, 0); + return new Types.Source(sourceName, uri, sourceReference); } } else { // If the source lookup engine cannot find the source file, then lookup it in the source directories specified by user. String absoluteSourcepath = AdapterUtils.sourceLookup(context.getSourcePaths(), relativeSourcePath); if (absoluteSourcepath != null) { - return new Types.Source(sourceName, absoluteSourcepath, 0); + return new Types.Source(sourceName, absoluteSourcepath, sourceReference); } else { return null; } } } - private String formatMethodName(Method method, boolean showContextClass, boolean showParameter) { + private String formatMethodName(String methodName, List argumentTypeNames, String fqn, boolean showContextClass, boolean showParameter) { StringBuilder formattedName = new StringBuilder(); if (showContextClass) { - String fullyQualifiedClassName = method.declaringType().name(); - formattedName.append(SimpleTypeFormatter.trimTypeName(fullyQualifiedClassName)); + formattedName.append(SimpleTypeFormatter.trimTypeName(fqn)); formattedName.append("."); } - formattedName.append(method.name()); + formattedName.append(methodName); if (showParameter) { - List argumentTypeNames = method.argumentTypeNames().stream().map(SimpleTypeFormatter::trimTypeName).collect(Collectors.toList()); + argumentTypeNames = argumentTypeNames.stream().map(SimpleTypeFormatter::trimTypeName).collect(Collectors.toList()); formattedName.append("("); formattedName.append(String.join(",", argumentTypeNames)); formattedName.append(")"); } return formattedName.toString(); } + + static class StackFrameInfo { + public StackFrame frame; + public Location location; + public Method method; + public String methodName; + public List argumentTypeNames = new ArrayList<>(); + public boolean isNative = false; + public int lineNumber; + public ReferenceType declaringType = null; + public String typeName; + public String typeSignature; + public String sourceName = ""; + public String sourcePath = ""; + + // variables + public List visibleVariables = null; + public ObjectReference thisObject; + + public StackFrameInfo(StackFrame frame) { + this.frame = frame; + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepInTargetsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepInTargetsRequestHandler.java new file mode 100644 index 000000000..5b3b735fe --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepInTargetsRequestHandler.java @@ -0,0 +1,138 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Gayan Perera - initial API and implementation +*******************************************************************************/ +package com.microsoft.java.debug.core.adapter.handler; + +import java.io.File; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.microsoft.java.debug.core.Configuration; +import com.microsoft.java.debug.core.adapter.AdapterUtils; +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; +import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider; +import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider.MethodInvocation; +import com.microsoft.java.debug.core.adapter.variables.StackFrameReference; +import com.microsoft.java.debug.core.protocol.Messages.Response; +import com.microsoft.java.debug.core.protocol.Requests.Arguments; +import com.microsoft.java.debug.core.protocol.Requests.Command; +import com.microsoft.java.debug.core.protocol.Requests.StepInTargetsArguments; +import com.microsoft.java.debug.core.protocol.Responses.StepInTargetsResponse; +import com.microsoft.java.debug.core.protocol.Types.Source; +import com.microsoft.java.debug.core.protocol.Types.StepInTarget; +import com.sun.jdi.AbsentInformationException; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.StackFrame; + +public class StepInTargetsRequestHandler implements IDebugRequestHandler { + private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); + + @Override + public List getTargetCommands() { + return Arrays.asList(Command.STEPIN_TARGETS); + } + + @Override + public CompletableFuture handle(Command command, Arguments arguments, Response response, + IDebugAdapterContext context) { + final StepInTargetsArguments stepInTargetsArguments = (StepInTargetsArguments) arguments; + + final int frameId = stepInTargetsArguments.frameId; + return CompletableFuture.supplyAsync(() -> { + response.body = new StepInTargetsResponse( + findFrame(frameId, context).map(f -> findTargets(f, context)) + .orElse(Collections.emptyList()).toArray(StepInTarget[]::new)); + return response; + }); + } + + private Optional findFrame(int frameId, IDebugAdapterContext context) { + Object object = context.getRecyclableIdPool().getObjectById(frameId); + if (object instanceof StackFrameReference) { + return Optional.of((StackFrameReference) object); + } + return Optional.empty(); + } + + private List findTargets(StackFrameReference frameReference, IDebugAdapterContext context) { + StackFrame stackframe = context.getStackFrameManager().getStackFrame(frameReference); + if (stackframe == null) { + return Collections.emptyList(); + } + + Source source = frameReference.getSource() == null ? findSource(stackframe, context) : frameReference.getSource(); + if (source == null) { + return Collections.emptyList(); + } + + String sourceUri = AdapterUtils.convertPath(source.path, AdapterUtils.isUri(source.path), true); + if (sourceUri == null) { + return Collections.emptyList(); + } + + ISourceLookUpProvider sourceLookUpProvider = context.getProvider(ISourceLookUpProvider.class); + List invocations = sourceLookUpProvider.findMethodInvocations(sourceUri, stackframe.location().lineNumber()); + if (invocations.isEmpty()) { + return Collections.emptyList(); + } + + long threadId = stackframe.thread().uniqueID(); + List targets = new ArrayList<>(invocations.size()); + for (MethodInvocation methodInvocation : invocations) { + int id = context.getRecyclableIdPool().addObject(threadId, methodInvocation); + StepInTarget target = new StepInTarget(id, methodInvocation.expression); + target.column = AdapterUtils.convertColumnNumber(methodInvocation.columnStart, + context.isDebuggerColumnsStartAt1(), context.isClientColumnsStartAt1()); + target.endColumn = AdapterUtils.convertColumnNumber(methodInvocation.columnEnd, + context.isDebuggerColumnsStartAt1(), context.isClientColumnsStartAt1()); + target.line = AdapterUtils.convertLineNumber(methodInvocation.lineStart, + context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + target.endLine = AdapterUtils.convertLineNumber(methodInvocation.lineEnd, + context.isDebuggerLinesStartAt1(), context.isClientLinesStartAt1()); + targets.add(target); + } + + // TODO remove the executed method calls. + return targets; + } + + private Source findSource(StackFrame frame, IDebugAdapterContext context) { + ReferenceType declaringType = frame.location().declaringType(); + String typeName = declaringType.name(); + String sourceName = null; + String sourcePath = null; + try { + // When the .class file doesn't contain source information in meta data, + // invoking ReferenceType#sourceName() would throw AbsentInformationException. + sourceName = declaringType.sourceName(); + sourcePath = declaringType.sourcePaths(null).get(0); + } catch (AbsentInformationException e) { + String enclosingType = AdapterUtils.parseEnclosingType(typeName); + sourceName = enclosingType.substring(enclosingType.lastIndexOf('.') + 1) + ".java"; + sourcePath = enclosingType.replace('.', File.separatorChar) + ".java"; + } + + try { + return StackTraceRequestHandler.convertDebuggerSourceToClient(typeName, sourceName, sourcePath, context); + } catch (URISyntaxException e) { + logger.log(Level.SEVERE, "Failed to resolve the source info of the stack frame.", e); + } + + return null; + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java index 72d14eb5d..e8f782668 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/StepRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2020 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,12 +11,16 @@ package com.microsoft.java.debug.core.adapter.handler; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import org.apache.commons.lang3.ArrayUtils; +import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.DebugEvent; import com.microsoft.java.debug.core.DebugUtility; import com.microsoft.java.debug.core.IDebugSession; @@ -26,16 +30,21 @@ import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; +import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider.MethodInvocation; import com.microsoft.java.debug.core.protocol.Events; import com.microsoft.java.debug.core.protocol.Messages.Response; import com.microsoft.java.debug.core.protocol.Requests.Arguments; import com.microsoft.java.debug.core.protocol.Requests.Command; import com.microsoft.java.debug.core.protocol.Requests.StepArguments; import com.microsoft.java.debug.core.protocol.Requests.StepFilters; +import com.microsoft.java.debug.core.protocol.Requests.StepInArguments; +import com.sun.jdi.ClassType; import com.sun.jdi.IncompatibleThreadStateException; +import com.sun.jdi.InterfaceType; import com.sun.jdi.Location; import com.sun.jdi.Method; import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; import com.sun.jdi.StackFrame; import com.sun.jdi.ThreadReference; import com.sun.jdi.Value; @@ -67,17 +76,21 @@ public CompletableFuture handle(Command command, Arguments arguments, return AdapterUtils.createAsyncErrorResponse(response, ErrorCode.EMPTY_DEBUG_SESSION, "Debug Session doesn't exist."); } - long threadId = ((StepArguments) arguments).threadId; - ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), threadId); + StepArguments stepArguments = (StepArguments) arguments; + long threadId = stepArguments.threadId; + int targetId = (stepArguments instanceof StepInArguments) ? ((StepInArguments) stepArguments).targetId : 0; + ThreadReference thread = context.getThreadCache().getThread(threadId); + if (thread == null) { + thread = DebugUtility.getThread(context.getDebugSession(), threadId); + } if (thread != null) { JdiExceptionReference exception = context.getExceptionManager().removeException(threadId); context.getStepResultManager().removeMethodResult(threadId); try { + final ThreadReference targetThread = thread; ThreadState threadState = new ThreadState(); threadState.threadId = threadId; threadState.pendingStepType = command; - threadState.stackDepth = thread.frameCount(); - threadState.stepLocation = getTopFrame(thread).location(); threadState.eventSubscription = context.getDebugSession().getEventHub().events() .filter(debugEvent -> (debugEvent.event instanceof StepEvent && debugEvent.event.request().equals(threadState.pendingStepRequest)) || (debugEvent.event instanceof MethodExitEvent && debugEvent.event.request().equals(threadState.pendingMethodExitRequest)) @@ -98,27 +111,85 @@ public CompletableFuture handle(Command command, Arguments arguments, } else { threadState.pendingStepRequest = DebugUtility.createStepOverRequest(thread, null); } - threadState.pendingStepRequest.enable(); - MethodExitRequest methodExitRequest = thread.virtualMachine().eventRequestManager().createMethodExitRequest(); - methodExitRequest.addThreadFilter(thread); - methodExitRequest.addClassFilter(threadState.stepLocation.declaringType()); - if (thread.virtualMachine().canUseInstanceFilters()) { + threadState.pendingMethodExitRequest = thread.virtualMachine().eventRequestManager().createMethodExitRequest(); + threadState.pendingMethodExitRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); + + threadState.targetStepIn = targetId > 0 + ? (MethodInvocation) context.getRecyclableIdPool().getObjectById(targetId) : null; + if (context.asyncJDWP()) { + List> futures = new ArrayList<>(); + futures.add(AsyncJdwpUtils.runAsync(() -> { + // JDWP Command: TR_FRAMES + try { + threadState.topFrame = getTopFrame(targetThread); + threadState.stepLocation = threadState.topFrame.location(); + threadState.pendingMethodExitRequest.addClassFilter(threadState.stepLocation.declaringType()); + if (targetThread.virtualMachine().canUseInstanceFilters()) { + try { + // JDWP Command: SF_THIS_OBJECT + ObjectReference thisObject = threadState.topFrame.thisObject(); + if (thisObject != null) { + threadState.pendingMethodExitRequest.addInstanceFilter(thisObject); + } + } catch (Exception e) { + // ignore + } + } + } catch (IncompatibleThreadStateException e1) { + throw new CompletionException(e1); + } + })); + futures.add(AsyncJdwpUtils.runAsync( + // JDWP Command: OR_IS_COLLECTED + () -> threadState.pendingMethodExitRequest.addThreadFilter(targetThread) + )); + futures.add(AsyncJdwpUtils.runAsync(() -> { + try { + // JDWP Command: TR_FRAME_COUNT + threadState.stackDepth = targetThread.frameCount(); + } catch (IncompatibleThreadStateException e) { + throw new CompletionException(e); + } + })); + futures.add( + // JDWP Command: ER_SET + AsyncJdwpUtils.runAsync(() -> threadState.pendingStepRequest.enable()) + ); + try { - ObjectReference thisObject = getTopFrame(thread).thisObject(); - if (thisObject != null) { - methodExitRequest.addInstanceFilter(thisObject); + AsyncJdwpUtils.await(futures); + } catch (CompletionException ex) { + if (ex.getCause() instanceof IncompatibleThreadStateException) { + throw (IncompatibleThreadStateException) ex.getCause(); + } + throw ex; + } + + // JDWP Command: ER_SET + threadState.pendingMethodExitRequest.enable(); + } else { + threadState.topFrame = getTopFrame(targetThread); + threadState.stackDepth = targetThread.frameCount(); + threadState.stepLocation = threadState.topFrame.location(); + threadState.pendingMethodExitRequest.addThreadFilter(thread); + threadState.pendingMethodExitRequest.addClassFilter(threadState.stepLocation.declaringType()); + if (targetThread.virtualMachine().canUseInstanceFilters()) { + try { + ObjectReference thisObject = threadState.topFrame.thisObject(); + if (thisObject != null) { + threadState.pendingMethodExitRequest.addInstanceFilter(thisObject); + } + } catch (Exception e) { + // ignore } - } catch (Exception e) { - // ignore } + threadState.pendingStepRequest.enable(); + threadState.pendingMethodExitRequest.enable(); } - methodExitRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD); - threadState.pendingMethodExitRequest = methodExitRequest; - methodExitRequest.enable(); + context.getThreadCache().removeEventThread(thread.uniqueID()); DebugUtility.resumeThread(thread); - ThreadsRequestHandler.checkThreadRunningAndRecycleIds(thread, context); } catch (IncompatibleThreadStateException ex) { // Roll back the Exception info if stepping fails. @@ -136,6 +207,14 @@ public CompletableFuture handle(Command command, Arguments arguments, failureMessage, ErrorCode.STEP_FAILURE, ex); + } catch (Exception ex) { + // Roll back the Exception info if stepping fails. + context.getExceptionManager().setException(threadId, exception); + final String failureMessage = String.format("Failed to step because of the error '%s'", ex.getMessage()); + throw AdapterUtils.createCompletionException( + failureMessage, + ErrorCode.STEP_FAILURE, + ex.getCause() != null ? ex.getCause() : ex); } } @@ -149,6 +228,12 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, // When a breakpoint occurs, abort any pending step requests from the same thread. if (event instanceof BreakpointEvent || event instanceof ExceptionEvent) { + // if we have a pending target step in then ignore and continue. + if (threadState.targetStepIn != null) { + debugEvent.shouldResume = true; + return; + } + long threadId = ((LocatableEvent) event).thread().uniqueID(); if (threadId == threadState.threadId && threadState.pendingStepRequest != null) { threadState.deleteStepRequest(eventRequestManager); @@ -160,20 +245,57 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, } } else if (event instanceof StepEvent) { ThreadReference thread = ((StepEvent) event).thread(); + long threadId = thread.uniqueID(); threadState.deleteStepRequest(eventRequestManager); - if (isStepFiltersConfigured(context.getStepFilters())) { + if (isStepFiltersConfigured(context.getStepFilters()) || threadState.targetStepIn != null) { try { - if (threadState.pendingStepType == Command.STEPIN) { + if (threadState.pendingStepType == Command.STEPIN || threadState.targetStepIn != null) { int currentStackDepth = thread.frameCount(); - Location currentStepLocation = getTopFrame(thread).location(); - - // If the ending step location is filtered, or same as the original location where the step into operation is originated, - // do another step of the same kind. - if (shouldFilterLocation(threadState.stepLocation, currentStepLocation, context) - || shouldDoExtraStepInto(threadState.stackDepth, threadState.stepLocation, currentStackDepth, currentStepLocation)) { - threadState.pendingStepRequest = DebugUtility.createStepIntoRequest(thread, - context.getStepFilters().allowClasses, - context.getStepFilters().skipClasses); + StackFrame topFrame = getTopFrame(thread); + Location currentStepLocation = topFrame.location(); + if (threadState.targetStepIn != null) { + if (isStoppedAtSelectedMethod(topFrame, threadState.targetStepIn)) { + // hit: send StoppedEvent + } else { + if (currentStackDepth > threadState.stackDepth) { + context.getStepResultManager().removeMethodResult(threadId); + threadState.pendingStepRequest = DebugUtility.createStepOutRequest(thread, + context.getStepFilters().allowClasses, + context.getStepFilters().skipClasses); + threadState.pendingStepRequest.enable(); + debugEvent.shouldResume = true; + return; + } else if (currentStackDepth == threadState.stackDepth) { + // If the ending step location is same as the original location where the step into operation is originated, + // do another step of the same kind. + if (isSameLocation(currentStepLocation, threadState.stepLocation, + threadState.targetStepIn)) { + context.getStepResultManager().removeMethodResult(threadId); + threadState.pendingStepRequest = DebugUtility.createStepIntoRequest(thread, + context.getStepFilters().allowClasses, + context.getStepFilters().skipClasses); + threadState.pendingStepRequest.enable(); + debugEvent.shouldResume = true; + return; + } + } + } + } else if (shouldFilterLocation(threadState.stepLocation, currentStepLocation, context) + || shouldDoExtraStepInto(threadState.stackDepth, threadState.stepLocation, + currentStackDepth, currentStepLocation)) { + // If the ending step location is filtered, or same as the original location where the step into operation is originated, + // do another step of the same kind. + context.getStepResultManager().removeMethodResult(threadId); + String[] allowedClasses = context.getStepFilters().allowClasses; + if (currentStackDepth > threadState.stackDepth) { + threadState.pendingStepRequest = DebugUtility.createStepOutRequest(thread, + allowedClasses, + context.getStepFilters().skipClasses); + } else { + threadState.pendingStepRequest = DebugUtility.createStepIntoRequest(thread, + allowedClasses, + context.getStepFilters().skipClasses); + } threadState.pendingStepRequest.enable(); debugEvent.shouldResume = true; return; @@ -187,6 +309,7 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, if (threadState.eventSubscription != null) { threadState.eventSubscription.dispose(); } + context.getThreadCache().addEventThread(thread, "step"); context.getProtocolServer().sendEvent(new Events.StoppedEvent("step", thread.uniqueID())); debugEvent.shouldResume = false; } else if (event instanceof MethodExitEvent) { @@ -205,6 +328,56 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, } } + private boolean isStoppedAtSelectedMethod(StackFrame frame, MethodInvocation selectedMethod) { + Method method = frame.location().method(); + if (method != null + && Objects.equals(method.name(), selectedMethod.methodName) + && (Objects.equals(method.signature(), selectedMethod.methodSignature) + || Objects.equals(method.genericSignature(), selectedMethod.methodGenericSignature))) { + ObjectReference thisObject = frame.thisObject(); + ReferenceType currentType = (thisObject == null) ? method.declaringType() : thisObject.referenceType(); + if ("java.lang.Object".equals(selectedMethod.declaringTypeName)) { + return true; + } + + return isSubType(currentType, selectedMethod.declaringTypeName); + } + + return false; + } + + private boolean isSubType(ReferenceType currentType, String baseType) { + if (baseType.equals(currentType.name())) { + return true; + } + + if (currentType instanceof ClassType) { + ClassType classType = (ClassType) currentType; + ClassType superClassType = classType.superclass(); + if (superClassType != null && isSubType(superClassType, baseType)) { + return true; + } + + List interfaces = classType.allInterfaces(); + for (InterfaceType iface : interfaces) { + if (isSubType(iface, baseType)) { + return true; + } + } + } + + if (currentType instanceof InterfaceType) { + List superInterfaces = ((InterfaceType) currentType).superinterfaces(); + for (InterfaceType superInterface : superInterfaces) { + if (isSubType(superInterface, baseType)) { + return true; + } + } + } + + return false; + } + private boolean isStepFiltersConfigured(StepFilters filters) { if (filters == null) { return false; @@ -235,12 +408,17 @@ private boolean shouldFilterMethod(Method method, IDebugAdapterContext context) } /** - * Check if the current top stack is same as the original top stack. + * Check if the current top stack is same as the original top stack and if we + * are not in target step in we should not request an extra step in. But if we + * are processing a target step in, we only check if the original and current + * location are same. If they are not same we request a extra step in. * * @throws IncompatibleThreadStateException - * if the thread is not suspended in the target VM. + * if the thread is not suspended in + * the target VM. */ - private boolean shouldDoExtraStepInto(int originalStackDepth, Location originalLocation, int currentStackDepth, Location currentLocation) + private boolean shouldDoExtraStepInto(int originalStackDepth, Location originalLocation, int currentStackDepth, + Location currentLocation) throws IncompatibleThreadStateException { if (originalStackDepth != currentStackDepth) { return false; @@ -248,6 +426,7 @@ private boolean shouldDoExtraStepInto(int originalStackDepth, Location originalL if (originalLocation == null) { return false; } + Method originalMethod = originalLocation.method(); Method currentMethod = currentLocation.method(); if (!originalMethod.equals(currentMethod)) { @@ -256,9 +435,25 @@ private boolean shouldDoExtraStepInto(int originalStackDepth, Location originalL if (originalLocation.lineNumber() != currentLocation.lineNumber()) { return false; } + return true; } + private boolean isSameLocation(Location current, Location original, MethodInvocation targetStepIn) { + if (original == null || current == null) { + return false; + } + + Method originalMethod = original.method(); + Method currentMethod = current.method(); + // if the lines doesn't match, check if the current line is still behind the + // target if a target exist. This handles where the target is part of a + // expression which is wrapped. + return originalMethod.equals(currentMethod) + && (original.lineNumber() == current.lineNumber() + || (targetStepIn != null && targetStepIn.lineEnd >= current.lineNumber())); + } + /** * Return the top stack frame of the target thread. * @@ -280,8 +475,10 @@ class ThreadState { StepRequest pendingStepRequest = null; MethodExitRequest pendingMethodExitRequest = null; int stackDepth = -1; + StackFrame topFrame = null; Location stepLocation = null; Disposable eventSubscription = null; + MethodInvocation targetStepIn = null; public void deleteMethodExitRequest(EventRequestManager manager) { DebugUtility.deleteEventRequestSafely(manager, this.pendingMethodExitRequest); diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java index 3e4b481d5..5c4770391 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ThreadsRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -14,8 +14,15 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; + +import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.DebugUtility; import com.microsoft.java.debug.core.adapter.AdapterUtils; import com.microsoft.java.debug.core.adapter.ErrorCode; @@ -77,14 +84,15 @@ public CompletableFuture handle(Command command, Arguments arguments, private CompletableFuture threads(Requests.ThreadsArguments arguments, Response response, IDebugAdapterContext context) { ArrayList threads = new ArrayList<>(); try { - for (ThreadReference thread : context.getDebugSession().getAllThreads()) { - if (thread.isCollected()) { - continue; - } - Types.Thread clientThread = new Types.Thread(thread.uniqueID(), "Thread [" + thread.name() + "]"); - threads.add(clientThread); + List allThreads = context.getThreadCache().visibleThreads(context); + context.getThreadCache().resetThreads(allThreads); + allThreads = allThreads.stream().filter((thread) -> !context.getThreadCache().isDeathThread(thread.uniqueID())).collect(Collectors.toList()); + List jdiThreads = resolveThreadInfos(allThreads, context); + for (ThreadInfo jdiThread : jdiThreads) { + String name = StringUtils.isBlank(jdiThread.name) ? String.valueOf(jdiThread.thread.uniqueID()) : jdiThread.name; + threads.add(new Types.Thread(jdiThread.thread.uniqueID(), "Thread [" + name + "]")); } - } catch (ObjectCollectedException ex) { + } catch (ObjectCollectedException | CancellationException | CompletionException ex) { // allThreads may throw VMDisconnectedException when VM terminates and thread.name() may throw ObjectCollectedException // when the thread is exiting. } @@ -92,38 +100,80 @@ private CompletableFuture threads(Requests.ThreadsArguments arguments, return CompletableFuture.completedFuture(response); } + private static List resolveThreadInfos(List allThreads, IDebugAdapterContext context) { + List threadInfos = new ArrayList<>(allThreads.size()); + List> futures = new ArrayList<>(); + for (ThreadReference thread : allThreads) { + ThreadInfo threadInfo = new ThreadInfo(thread); + long threadId = thread.uniqueID(); + if (context.getThreadCache().getThreadName(threadId) != null) { + threadInfo.name = context.getThreadCache().getThreadName(threadId); + } else { + if (context.asyncJDWP()) { + futures.add(AsyncJdwpUtils.runAsync(() -> { + threadInfo.name = threadInfo.thread.name(); + context.getThreadCache().setThreadName(threadId, threadInfo.name); + })); + } else { + threadInfo.name = threadInfo.thread.name(); + context.getThreadCache().setThreadName(threadId, threadInfo.name); + } + } + + threadInfos.add(threadInfo); + } + + AsyncJdwpUtils.await(futures); + return threadInfos; + } + private CompletableFuture pause(Requests.PauseArguments arguments, Response response, IDebugAdapterContext context) { - ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), arguments.threadId); + ThreadReference thread = context.getThreadCache().getThread(arguments.threadId); + if (thread == null) { + thread = DebugUtility.getThread(context.getDebugSession(), arguments.threadId); + } if (thread != null) { - context.getStepResultManager().removeMethodResult(arguments.threadId); - thread.suspend(); - context.getProtocolServer().sendEvent(new Events.StoppedEvent("pause", arguments.threadId)); + pauseThread(thread, context); } else { context.getStepResultManager().removeAllMethodResults(); context.getDebugSession().suspend(); context.getProtocolServer().sendEvent(new Events.StoppedEvent("pause", arguments.threadId, true)); + context.getThreadCache().setThreadStoppedReason(arguments.threadId, "pause"); } return CompletableFuture.completedFuture(response); } private CompletableFuture resume(Requests.ContinueArguments arguments, Response response, IDebugAdapterContext context) { boolean allThreadsContinued = true; - ThreadReference thread = DebugUtility.getThread(context.getDebugSession(), arguments.threadId); + ThreadReference thread = context.getThreadCache().getThread(arguments.threadId); + if (thread == null) { + thread = DebugUtility.getThread(context.getDebugSession(), arguments.threadId); + } + + if (context.getDebugSession().shouldSuspendAllThreads()) { + thread = null; + } + /** * See the jdi doc https://docs.oracle.com/javase/7/docs/jdk/api/jpda/jdi/com/sun/jdi/ThreadReference.html#resume(), * suspends of both the virtual machine and individual threads are counted. Before a thread will run again, it must * be resumed (through ThreadReference#resume() or VirtualMachine#resume()) the same number of times it has been suspended. */ if (thread != null) { + context.getThreadCache().removeEventThread(arguments.threadId); context.getStepResultManager().removeMethodResult(arguments.threadId); context.getExceptionManager().removeException(arguments.threadId); allThreadsContinued = false; + context.getThreadCache().clearThreadStoppedState(arguments.threadId); DebugUtility.resumeThread(thread); + context.getStackFrameManager().clearStackFrames(thread); checkThreadRunningAndRecycleIds(thread, context); } else { context.getStepResultManager().removeAllMethodResults(); context.getExceptionManager().removeAllExceptions(); - context.getDebugSession().resume(); + context.getThreadCache().clearAllThreadStoppedState(); + resumeVM(context); + context.getStackFrameManager().clearStackFrames(); context.getRecyclableIdPool().removeAllObjects(); } response.body = new Responses.ContinueResponseBody(allThreadsContinued); @@ -131,44 +181,57 @@ private CompletableFuture resume(Requests.ContinueArguments arguments, } private CompletableFuture resumeAll(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) { + context.getStepResultManager().removeAllMethodResults(); context.getExceptionManager().removeAllExceptions(); - context.getDebugSession().resume(); + context.getThreadCache().clearAllThreadStoppedState(); + resumeVM(context); context.getProtocolServer().sendEvent(new Events.ContinuedEvent(arguments.threadId, true)); + context.getStackFrameManager().clearStackFrames(); context.getRecyclableIdPool().removeAllObjects(); return CompletableFuture.completedFuture(response); } private CompletableFuture resumeOthers(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) { - List threads = DebugUtility.getAllThreadsSafely(context.getDebugSession()); + List threads = context.getThreadCache().visibleThreads(context); + List> futures = new ArrayList<>(); for (ThreadReference thread : threads) { - long threadId = thread.uniqueID(); - if (threadId != arguments.threadId && thread.isSuspended()) { - context.getExceptionManager().removeException(threadId); - DebugUtility.resumeThread(thread); - context.getProtocolServer().sendEvent(new Events.ContinuedEvent(threadId)); - checkThreadRunningAndRecycleIds(thread, context); + if (thread.uniqueID() == arguments.threadId) { + continue; } - } + context.getThreadCache().clearThreadStoppedState(thread.uniqueID()); + if (context.asyncJDWP()) { + futures.add(AsyncJdwpUtils.runAsync(() -> resumeThread(thread, context))); + } else { + resumeThread(thread, context); + } + } + AsyncJdwpUtils.await(futures); return CompletableFuture.completedFuture(response); } private CompletableFuture pauseAll(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) { context.getDebugSession().suspend(); context.getProtocolServer().sendEvent(new Events.StoppedEvent("pause", arguments.threadId, true)); + context.getThreadCache().setThreadStoppedReason(arguments.threadId, "pause"); return CompletableFuture.completedFuture(response); } private CompletableFuture pauseOthers(Requests.ThreadOperationArguments arguments, Response response, IDebugAdapterContext context) { - List threads = DebugUtility.getAllThreadsSafely(context.getDebugSession()); + List threads = context.getThreadCache().visibleThreads(context); + List> futures = new ArrayList<>(); for (ThreadReference thread : threads) { - long threadId = thread.uniqueID(); - if (threadId != arguments.threadId && !thread.isCollected() && !thread.isSuspended()) { - thread.suspend(); - context.getProtocolServer().sendEvent(new Events.StoppedEvent("pause", threadId)); + if (thread.uniqueID() == arguments.threadId) { + continue; } - } + if (context.asyncJDWP()) { + futures.add(AsyncJdwpUtils.runAsync(() -> pauseThread(thread, context))); + } else { + pauseThread(thread, context); + } + } + AsyncJdwpUtils.await(futures); return CompletableFuture.completedFuture(response); } @@ -179,13 +242,7 @@ public static void checkThreadRunningAndRecycleIds(ThreadReference thread, IDebu try { IEvaluationProvider engine = context.getProvider(IEvaluationProvider.class); engine.clearState(thread); - boolean allThreadsRunning = !DebugUtility.getAllThreadsSafely(context.getDebugSession()).stream() - .anyMatch(ThreadReference::isSuspended); - if (allThreadsRunning) { - context.getRecyclableIdPool().removeAllObjects(); - } else { - context.getRecyclableIdPool().removeObjectsByOwner(thread.uniqueID()); - } + context.getRecyclableIdPool().removeObjectsByOwner(thread.uniqueID()); } catch (VMDisconnectedException ex) { // isSuspended may throw VMDisconnectedException when the VM terminates context.getRecyclableIdPool().removeAllObjects(); @@ -194,4 +251,80 @@ public static void checkThreadRunningAndRecycleIds(ThreadReference thread, IDebu context.getRecyclableIdPool().removeObjectsByOwner(thread.uniqueID()); } } + + private void resumeVM(IDebugAdapterContext context) { + List visibleThreads = context.getThreadCache().visibleThreads(context); + context.getThreadCache().clearEventThread(); + + List> futures = new ArrayList<>(); + /** + * To ensure that all threads are fully resumed when the VM is resumed, make sure the suspend count + * of each thread is no larger than 1. + * Notes: Decrementing the thread' suspend count to 1 is on purpose, because it doesn't break the + * the thread's suspend state, and also make sure the next instruction vm.resume() is able to resume + * all threads fully. + */ + Consumer resumeThread = (ThreadReference tr) -> { + try { + while (tr.suspendCount() > 1) { + tr.resume(); + } + } catch (ObjectCollectedException ex) { + // Ignore it if the thread is garbage collected. + } + }; + for (ThreadReference tr : visibleThreads) { + if (context.asyncJDWP()) { + futures.add(AsyncJdwpUtils.runAsync(() -> resumeThread.accept(tr))); + } else { + resumeThread.accept(tr); + } + } + + AsyncJdwpUtils.await(futures); + context.getDebugSession().getVM().resume(); + } + + private void resumeThread(ThreadReference thread, IDebugAdapterContext context) { + try { + context.getThreadCache().removeEventThread(thread.uniqueID()); + int suspends = thread.suspendCount(); + if (suspends > 0) { + long threadId = thread.uniqueID(); + context.getExceptionManager().removeException(threadId); + DebugUtility.resumeThread(thread, suspends); + context.getProtocolServer().sendEvent(new Events.ContinuedEvent(threadId)); + context.getStackFrameManager().clearStackFrames(thread); + checkThreadRunningAndRecycleIds(thread, context); + } + } catch (ObjectCollectedException ex) { + // the thread is garbage collected. + context.getThreadCache().addDeathThread(thread.uniqueID()); + } + } + + private void pauseThread(ThreadReference thread, IDebugAdapterContext context) { + try { + // Ignore it if the thread status is unknown or zombie + if (!thread.isSuspended() && thread.status() > 0) { + long threadId = thread.uniqueID(); + context.getStepResultManager().removeMethodResult(threadId); + thread.suspend(); + context.getProtocolServer().sendEvent(new Events.StoppedEvent("pause", threadId)); + context.getThreadCache().setThreadStoppedReason(threadId, "pause"); + } + } catch (ObjectCollectedException ex) { + // the thread is garbage collected. + context.getThreadCache().addDeathThread(thread.uniqueID()); + } + } + + static class ThreadInfo { + public ThreadReference thread; + public String name; + + public ThreadInfo(ThreadReference thread) { + this.thread = thread; + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/VariablesRequestHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/VariablesRequestHandler.java index 8f37ef574..11a6391de 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/VariablesRequestHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/VariablesRequestHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2021 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,11 +19,14 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.JdiMethodResult; @@ -39,6 +42,7 @@ import com.microsoft.java.debug.core.adapter.variables.JavaLogicalStructure.LogicalVariable; import com.microsoft.java.debug.core.adapter.variables.JavaLogicalStructureManager; import com.microsoft.java.debug.core.adapter.variables.StackFrameReference; +import com.microsoft.java.debug.core.adapter.variables.StringReferenceProxy; import com.microsoft.java.debug.core.adapter.variables.Variable; import com.microsoft.java.debug.core.adapter.variables.VariableDetailUtils; import com.microsoft.java.debug.core.adapter.variables.VariableProxy; @@ -56,12 +60,29 @@ import com.sun.jdi.InternalException; import com.sun.jdi.InvalidStackFrameException; import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; import com.sun.jdi.StackFrame; +import com.sun.jdi.StringReference; import com.sun.jdi.Type; import com.sun.jdi.Value; public class VariablesRequestHandler implements IDebugRequestHandler { protected static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); + /** + * When the debugger enables logical structures and + * toString settings, for each Object variable in the + * variable list, the debugger needs to check its + * superclass and interface to find out if it inherits + * from Collection or overrides the toString method. + * This will cause the debugger to send a lot of JDWP + * requests for them. For a test case with 4 object + * variables, the debug adapter may need to send more + * than 100 JDWP requests to handle these variable + * requests. To achieve a DAP latency of 1s with a + * single-threaded JDWP request processing strategy, + * a single JDWP latency is about 10ms. + */ + static final long USABLE_JDWP_LATENCY = 10/**ms*/; @Override public List getTargetCommands() { @@ -96,7 +117,7 @@ public CompletableFuture handle(Command command, Arguments arguments, VariableProxy containerNode = (VariableProxy) container; - if (containerNode.isLazyVariable() && DebugSettings.getCurrent().showToString) { + if (supportsToStringView(context) && containerNode.isLazyVariable()) { Types.Variable typedVariable = this.resolveLazyVariable(context, containerNode, variableFormatter, options, evaluationEngine); if (typedVariable != null) { list.add(typedVariable); @@ -123,24 +144,29 @@ public CompletableFuture handle(Command command, Arguments arguments, String returnIcon = (AdapterUtils.isWin || AdapterUtils.isMac) ? "⎯►" : "->"; childrenList.add(new Variable(returnIcon + result.method.name() + "()", result.value, null)); } - childrenList.addAll(VariableUtils.listLocalVariables(frame)); - Variable thisVariable = VariableUtils.getThisVariable(frame); - if (thisVariable != null) { - childrenList.add(thisVariable); - } - if (showStaticVariables && frame.location().method().isStatic()) { - childrenList.addAll(VariableUtils.listStaticVariables(frame)); + + if (useAsyncJDWP(context)) { + childrenList.addAll(getVariablesOfFrameAsync(frame, showStaticVariables)); + } else { + childrenList.addAll(VariableUtils.listLocalVariables(frame)); + Variable thisVariable = VariableUtils.getThisVariable(frame); + if (thisVariable != null) { + childrenList.add(thisVariable); + } + if (showStaticVariables && frame.location().method().isStatic()) { + childrenList.addAll(VariableUtils.listStaticVariables(frame)); + } } - } catch (AbsentInformationException | InternalException | InvalidStackFrameException e) { + } catch (CompletionException | InternalException | InvalidStackFrameException | CancellationException | AbsentInformationException e) { throw AdapterUtils.createCompletionException( String.format("Failed to get variables. Reason: %s", e.toString()), ErrorCode.GET_VARIABLE_FAILURE, - e); + e.getCause() != null ? e.getCause() : e); } } else { try { ObjectReference containerObj = (ObjectReference) containerNode.getProxiedVariable(); - if (DebugSettings.getCurrent().showLogicalStructure && evaluationEngine != null) { + if (supportsLogicStructureView(context) && evaluationEngine != null) { JavaLogicalStructure logicalStructure = null; try { logicalStructure = JavaLogicalStructureManager.getLogicalStructure(containerObj); @@ -187,7 +213,7 @@ public CompletableFuture handle(Command command, Arguments arguments, if (varArgs.count > 0) { childrenList = VariableUtils.listFieldVariables(containerObj, varArgs.start, varArgs.count); } else { - childrenList = VariableUtils.listFieldVariables(containerObj, showStaticVariables); + childrenList = VariableUtils.listFieldVariables(containerObj, showStaticVariables, useAsyncJDWP(context)); } } } catch (AbsentInformationException e) { @@ -200,12 +226,24 @@ public CompletableFuture handle(Command command, Arguments arguments, // Find variable name duplicates Set duplicateNames = getDuplicateNames(childrenList.stream().map(var -> var.name).collect(Collectors.toList())); - Map variableNameMap = new HashMap<>(); - if (!duplicateNames.isEmpty()) { - Map> duplicateVars = childrenList.stream() - .filter(var -> duplicateNames.contains(var.name)).collect(Collectors.groupingBy(var -> var.name, Collectors.toList())); + List duplicateVars = childrenList.stream() + .filter(var -> duplicateNames.contains(var.name)) + .collect(Collectors.toList()); + // Since JDI caches the fetched properties locally, in async mode we can warm up the JDI cache in advance. + if (useAsyncJDWP(context)) { + try { + AsyncJdwpUtils.await(warmUpJDICache(childrenList, duplicateVars)); + } catch (CompletionException | CancellationException e) { + response.body = new Responses.VariablesResponseBody(list); + return CompletableFuture.completedFuture(response); + } + } - duplicateVars.forEach((k, duplicateVariables) -> { + Map variableNameMap = new HashMap<>(); + if (!duplicateVars.isEmpty()) { + Map> duplicateVarGroups = duplicateVars.stream() + .collect(Collectors.groupingBy(var -> var.name, Collectors.toList())); + duplicateVarGroups.forEach((k, duplicateVariables) -> { Set declarationTypeNames = new HashSet<>(); boolean declarationTypeNameConflict = false; // try use type formatter to resolve name conflict @@ -232,6 +270,7 @@ public CompletableFuture handle(Command command, Arguments arguments, } }); } + for (Variable javaVariable : childrenList) { Value value = javaVariable.value; String name = javaVariable.name; @@ -242,7 +281,7 @@ public CompletableFuture handle(Command command, Arguments arguments, Value sizeValue = null; if (value instanceof ArrayReference) { indexedVariables = ((ArrayReference) value).length(); - } else if (value instanceof ObjectReference && DebugSettings.getCurrent().showLogicalStructure && evaluationEngine != null) { + } else if (supportsLogicStructureView(context) && value instanceof ObjectReference && evaluationEngine != null) { try { JavaLogicalStructure structure = JavaLogicalStructureManager.getLogicalStructure((ObjectReference) value); if (structure != null && structure.getSizeExpression() != null) { @@ -310,7 +349,7 @@ public CompletableFuture handle(Command command, Arguments arguments, // If failed to resolve the variable value, skip the details info as well. } else if (sizeValue != null) { detailsValue = "size=" + variableFormatter.valueToString(sizeValue, options); - } else if (DebugSettings.getCurrent().showToString) { + } else if (supportsToStringView(context)) { if (VariableDetailUtils.isLazyLoadingSupported(value) && varProxy != null) { varProxy.setLazyVariable(true); } else { @@ -352,6 +391,20 @@ public CompletableFuture handle(Command command, Arguments arguments, return CompletableFuture.completedFuture(response); } + private boolean supportsLogicStructureView(IDebugAdapterContext context) { + return (!useAsyncJDWP(context) || context.getJDWPLatency() <= USABLE_JDWP_LATENCY) + && DebugSettings.getCurrent().showLogicalStructure; + } + + private boolean supportsToStringView(IDebugAdapterContext context) { + return (!useAsyncJDWP(context) || context.getJDWPLatency() <= USABLE_JDWP_LATENCY) + && DebugSettings.getCurrent().showToString; + } + + private boolean useAsyncJDWP(IDebugAdapterContext context) { + return context.asyncJDWP(USABLE_JDWP_LATENCY); + } + private Types.Variable resolveLazyVariable(IDebugAdapterContext context, VariableProxy containerNode, IVariableFormatter variableFormatter, Map options, IEvaluationProvider evaluationEngine) { VariableProxy valueReferenceProxy = new VariableProxy(containerNode.getThread(), containerNode.getScope(), @@ -384,4 +437,74 @@ private Set getDuplicateNames(Collection list) { } return result; } + + private List getVariablesOfFrameAsync(StackFrame frame, boolean showStaticVariables) { + CompletableFuture> localVariables = VariableUtils.listLocalVariablesAsync(frame); + CompletableFuture thisVariable = VariableUtils.getThisVariableAsync(frame); + CompletableFuture>[] staticVariables = new CompletableFuture[1]; + if (showStaticVariables && frame.location().method().isStatic()) { + staticVariables[0] = VariableUtils.listStaticVariablesAsync(frame); + } + + CompletableFuture futures = staticVariables[0] == null ? CompletableFuture.allOf(localVariables, thisVariable) + : CompletableFuture.allOf(localVariables, thisVariable, staticVariables[0]); + + AsyncJdwpUtils.await(futures); + + List result = new ArrayList<>(); + result.addAll(localVariables.join()); + Variable thisVar = thisVariable.join(); + if (thisVar != null) { + result.add(thisVar); + } + + if (staticVariables[0] != null) { + result.addAll(staticVariables[0].join()); + } + + return result; + } + + private CompletableFuture warmUpJDICache(List variables, List duplicatedVars) { + List> futures = new ArrayList<>(); + if (duplicatedVars != null && !duplicatedVars.isEmpty()) { + Set declaringTypes = new HashSet<>(); + duplicatedVars.forEach((var) -> { + Type declarationType = var.getDeclaringType(); + if (declarationType != null) { + declaringTypes.add(declarationType); + } + }); + + for (Type type : declaringTypes) { + if (type instanceof ReferenceType) { + // JDWP Command: RT_SIGNATURE + futures.add(AsyncJdwpUtils.runAsync(() -> type.signature())); + } + } + } + + for (Variable javaVariable : variables) { + Value value = javaVariable.value; + if (value instanceof ArrayReference) { + // JDWP Command: AR_LENGTH + futures.add(AsyncJdwpUtils.runAsync(() -> ((ArrayReference) value).length())); + } else if (value instanceof StringReference) { + // JDWP Command: SR_VALUE + futures.add(AsyncJdwpUtils.runAsync(() -> { + String strValue = ((StringReference) value).value(); + javaVariable.value = new StringReferenceProxy((StringReference) value, strValue); + })); + } + + if (value instanceof ObjectReference) { + // JDWP Command: OR_REFERENCE_TYPE, RT_SIGNATURE + futures.add(AsyncJdwpUtils.runAsync(() -> { + value.type().signature(); + })); + } + } + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StackFrameReference.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StackFrameReference.java index 4dc895007..46726cde8 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StackFrameReference.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StackFrameReference.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -11,12 +11,14 @@ package com.microsoft.java.debug.core.adapter.variables; +import com.microsoft.java.debug.core.protocol.Types.Source; import com.sun.jdi.ThreadReference; public class StackFrameReference { private final int depth; private final int hash; private final ThreadReference thread; + private Source source; /** * Create a wrapper of JDI stackframe to keep the immutable properties of a stackframe, IStackFrameManager will use @@ -48,6 +50,14 @@ public ThreadReference getThread() { return thread; } + public Source getSource() { + return source; + } + + public void setSource(Source source) { + this.source = source; + } + @Override public int hashCode() { return hash; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StringReferenceProxy.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StringReferenceProxy.java new file mode 100644 index 000000000..82c4da56e --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/StringReferenceProxy.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2022 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package com.microsoft.java.debug.core.adapter.variables; + +import java.util.List; +import java.util.Map; + +import com.sun.jdi.ClassNotLoadedException; +import com.sun.jdi.Field; +import com.sun.jdi.IncompatibleThreadStateException; +import com.sun.jdi.InvalidTypeException; +import com.sun.jdi.InvocationException; +import com.sun.jdi.Method; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.StringReference; +import com.sun.jdi.ThreadReference; +import com.sun.jdi.Type; +import com.sun.jdi.Value; +import com.sun.jdi.VirtualMachine; + +public class StringReferenceProxy implements StringReference { + private StringReference delegateStringRef; + private String value = null; + + public StringReferenceProxy(StringReference sr, String value) { + this.delegateStringRef = sr; + this.value = value; + } + + public String value() { + if (value != null) { + return value; + } + + return delegateStringRef.value(); + } + + public ReferenceType referenceType() { + return delegateStringRef.referenceType(); + } + + public VirtualMachine virtualMachine() { + return delegateStringRef.virtualMachine(); + } + + public String toString() { + return delegateStringRef.toString(); + } + + public Value getValue(Field sig) { + return delegateStringRef.getValue(sig); + } + + public Map getValues(List fields) { + return delegateStringRef.getValues(fields); + } + + public void setValue(Field field, Value value) throws InvalidTypeException, ClassNotLoadedException { + delegateStringRef.setValue(field, value); + } + + public Value invokeMethod(ThreadReference thread, Method method, List arguments, int options) + throws InvalidTypeException, ClassNotLoadedException, IncompatibleThreadStateException, + InvocationException { + return delegateStringRef.invokeMethod(thread, method, arguments, options); + } + + public Type type() { + return delegateStringRef.type(); + } + + public void disableCollection() { + delegateStringRef.disableCollection(); + } + + public void enableCollection() { + delegateStringRef.enableCollection(); + } + + public boolean isCollected() { + return delegateStringRef.isCollected(); + } + + public long uniqueID() { + return delegateStringRef.uniqueID(); + } + + public List waitingThreads() throws IncompatibleThreadStateException { + return delegateStringRef.waitingThreads(); + } + + public ThreadReference owningThread() throws IncompatibleThreadStateException { + return delegateStringRef.owningThread(); + } + + public int entryCount() throws IncompatibleThreadStateException { + return delegateStringRef.entryCount(); + } + + public List referringObjects(long maxReferrers) { + return delegateStringRef.referringObjects(maxReferrers); + } + + public boolean equals(Object obj) { + return delegateStringRef.equals(obj); + } + + public int hashCode() { + return delegateStringRef.hashCode(); + } +} diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableUtils.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableUtils.java index d53e2705c..1a8139fa9 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableUtils.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2020 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -12,14 +12,21 @@ package com.microsoft.java.debug.core.adapter.variables; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.function.Consumer; +import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import com.microsoft.java.debug.core.AsyncJdwpUtils; import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugSettings; import com.microsoft.java.debug.core.adapter.formatter.NumericFormatEnum; @@ -29,6 +36,8 @@ import com.sun.jdi.AbsentInformationException; import com.sun.jdi.ArrayReference; import com.sun.jdi.ArrayType; +import com.sun.jdi.ClassType; +import com.sun.jdi.InterfaceType; import com.sun.jdi.ClassNotLoadedException; import com.sun.jdi.Field; import com.sun.jdi.InternalException; @@ -73,6 +82,10 @@ public static boolean hasChildren(Value value, boolean includeStatic) { * when there is any error in retrieving information */ public static List listFieldVariables(ObjectReference obj, boolean includeStatic) throws AbsentInformationException { + return listFieldVariables(obj, includeStatic, false); + } + + public static List listFieldVariables(ObjectReference obj, boolean includeStatic, boolean async) throws AbsentInformationException { List res = new ArrayList<>(); ReferenceType type = obj.referenceType(); if (type instanceof ArrayType) { @@ -85,7 +98,7 @@ public static List listFieldVariables(ObjectReference obj, boolean inc } return res; } - List fields = type.allFields().stream().filter(t -> includeStatic || !t.isStatic()) + List fields = resolveAllFields(type, async).stream().filter(t -> includeStatic || !t.isStatic()) .sorted((a, b) -> { try { boolean v1isStatic = a.isStatic(); @@ -213,6 +226,91 @@ public static List listLocalVariables(StackFrame stackFrame) throws Ab return res; } + public static CompletableFuture> listLocalVariablesAsync(StackFrame stackFrame) { + CompletableFuture> future = new CompletableFuture<>(); + if (stackFrame.location().method().isNative()) { + return CompletableFuture.completedFuture(new ArrayList<>()); + } + + AsyncJdwpUtils.supplyAsync(() -> { + try { + return stackFrame.visibleVariables(); + } catch (AbsentInformationException ex) { + throw new CompletionException(ex); + } + }).thenCompose((visibleVariables) -> { + // When using the API StackFrame.getValues() to batch fetch the variable values, the JDI + // probably throws timeout exception if the variables to be passed at one time are large. + // So use paging to fetch the values in chunks. + return bulkFetchValuesAsync(visibleVariables, DebugSettings.getCurrent().limitOfVariablesPerJdwpRequest, (currentPage) -> { + Map values = stackFrame.getValues(currentPage); + List result = new ArrayList<>(); + for (LocalVariable localVariable : currentPage) { + Variable var = new Variable(localVariable.name(), values.get(localVariable)); + var.local = localVariable; + result.add(var); + } + + return result; + }); + }).whenComplete((res, ex) -> { + if (ex instanceof CompletionException && ex.getCause() != null) { + ex = ex.getCause(); + } + + if (ex instanceof AbsentInformationException) { + // avoid listing variable on native methods + try { + if (stackFrame.location().method().argumentTypes().size() == 0) { + future.complete(new ArrayList<>()); + return; + } + } catch (ClassNotLoadedException ex2) { + // ignore since the method is hit. + } + // 1. in oracle implementations, when there is no debug information, the AbsentInformationException will be + // thrown, then we need to retrieve arguments from stackFrame#getArgumentValues. + // 2. in eclipse jdt implementations, when there is no debug information, stackFrame#visibleVariables will + // return some generated variables like arg0, arg1, and the stackFrame#getArgumentValues will return null + + // for both scenarios, we need to handle the possible null returned by stackFrame#getArgumentValues and + // we need to call stackFrame.getArgumentValues get the arguments if AbsentInformationException is thrown + int argId = 0; + try { + List arguments = stackFrame.getArgumentValues(); + if (arguments == null) { + future.complete(new ArrayList<>()); + return; + } + + List variables = new ArrayList<>(); + for (Value argValue : arguments) { + Variable var = new Variable("arg" + argId, argValue); + var.argumentIndex = argId++; + variables.add(var); + } + future.complete(variables); + } catch (InternalException ex2) { + // From Oracle's forums: + // This could be a JPDA bug. Unexpected JDWP Error: 32 means that an 'opaque' frame was + // detected at the lower JPDA levels, + // typically a native frame. + if (ex2.errorCode() != 32) { + throw ex2; + } + } + } else if (ex != null) { + future.complete(new ArrayList<>()); + } else { + future.complete(res.stream() + .flatMap(List::stream) + .collect(Collectors.toList())); + } + }); + + return future; + } + /** * Get the this variable of an stack frame. * @@ -228,6 +326,16 @@ public static Variable getThisVariable(StackFrame stackFrame) { return new Variable("this", thisObject); } + public static CompletableFuture getThisVariableAsync(StackFrame stackFrame) { + return AsyncJdwpUtils.supplyAsync(() -> { + ObjectReference thisObject = stackFrame.thisObject(); + if (thisObject == null) { + return null; + } + return new Variable("this", thisObject); + }); + } + /** * Get the static variable of an stack frame. * @@ -251,6 +359,40 @@ public static List listStaticVariables(StackFrame stackFrame) { return res; } + public static CompletableFuture> listStaticVariablesAsync(StackFrame stackFrame) { + CompletableFuture> future = new CompletableFuture<>(); + ReferenceType type = stackFrame.location().declaringType(); + AsyncJdwpUtils.supplyAsync(() -> { + return type.allFields().stream().filter(TypeComponent::isStatic).collect(Collectors.toList()); + }).thenCompose((fields) -> { + return bulkFetchValuesAsync(fields, DebugSettings.getCurrent().limitOfVariablesPerJdwpRequest, (currentPage) -> { + List variables = new ArrayList<>(); + Map fieldValues = type.getValues(currentPage); + for (Field currentField : currentPage) { + Variable var = new Variable(currentField.name(), fieldValues.get(currentField)); + var.field = currentField; + variables.add(var); + } + + return variables; + }); + }).whenComplete((res, ex) -> { + if (ex instanceof CompletionException && ex.getCause() != null) { + ex = ex.getCause(); + } + + if (ex != null) { + future.complete(new ArrayList<>()); + } else { + future.complete(res.stream() + .flatMap(List::stream) + .collect(Collectors.toList())); + } + }); + + return future; + } + /** * Apply the display options for variable formatter, it is used in variable and evaluate requests, controls the display content in * variable view/debug console. @@ -316,6 +458,84 @@ private static void bulkFetchValues(List elements, int numberPerPage, Con } } + private static CompletableFuture> bulkFetchValuesAsync(List elements, int numberPerPage, Function, R> function) { + int size = elements.size(); + numberPerPage = numberPerPage < 1 ? 1 : numberPerPage; + int page = size / numberPerPage + Math.min(size % numberPerPage, 1); + List> futures = new ArrayList<>(); + for (int i = 0; i < page; i++) { + int pageStart = i * numberPerPage; + int pageEnd = Math.min(pageStart + numberPerPage, size); + final List currentPage = elements.subList(pageStart, pageEnd); + futures.add(AsyncJdwpUtils.supplyAsync(() -> { + return function.apply(currentPage); + })); + } + + return AsyncJdwpUtils.all(futures); + } + + private static List resolveAllFields(ReferenceType type, boolean async) { + if (async) { + return resolveAllFieldsAsync(type); + } + + return type.allFields(); + } + + private static List resolveAllFieldsAsync(ReferenceType type) { + Set result = Collections.synchronizedSet(new HashSet<>()); + AsyncJdwpUtils.await(resolveAllFieldsAsync(type, result)); + List fields = new ArrayList<>(); + fields.addAll(result); + return fields; + } + + private static CompletableFuture resolveAllFieldsAsync(ReferenceType type, Set result) { + List> futures = new ArrayList<>(); + // JDWP Command: RT_FIELDS_WITH_GENERIC + futures.add( + AsyncJdwpUtils.runAsync(() -> result.addAll(type.fields())) + ); + + if (type instanceof ClassType) { + ClassType classType = (ClassType) type; + // JDWP Command: RT_INTERFACES + futures.add(AsyncJdwpUtils.supplyAsync(() -> classType.interfaces()) + .thenCompose((its) -> { + List> itFutures = new ArrayList<>(); + for (InterfaceType it : its) { + itFutures.add(resolveAllFieldsAsync(it, result)); + } + + return CompletableFuture.allOf(itFutures.toArray(new CompletableFuture[0])); + })); + + // JDWP Command: CT_SUPERCLASS + AsyncJdwpUtils.supplyAsync(() -> classType.superclass()) + .thenCompose((superclass) -> { + if (superclass != null) { + return resolveAllFieldsAsync(superclass, result); + } + return CompletableFuture.completedFuture(null); + }); + } else if (type instanceof InterfaceType) { + InterfaceType interfaceType = (InterfaceType) type; + // JDWP Command: RT_INTERFACES + futures.add(AsyncJdwpUtils.supplyAsync(() -> interfaceType.superinterfaces()) + .thenCompose((its) -> { + List> itFutures = new ArrayList<>(); + for (InterfaceType it : its) { + itFutures.add(resolveAllFieldsAsync(it, result)); + } + + return CompletableFuture.allOf(itFutures.toArray(new CompletableFuture[0])); + })); + } + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + } + private VariableUtils() { } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/AbstractProtocolServer.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/AbstractProtocolServer.java index 9bec3c228..61d57bd85 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/AbstractProtocolServer.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/AbstractProtocolServer.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -33,6 +33,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.microsoft.java.debug.core.adapter.AdapterUtils; +import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.protocol.Events.DebugEvent; import io.reactivex.disposables.Disposable; @@ -54,6 +56,7 @@ public abstract class AbstractProtocolServer implements IProtocolServer { private ByteBuffer rawData; private int contentLength = -1; private AtomicInteger sequenceNumber = new AtomicInteger(1); + private boolean isValidDAPRequest = true; private PublishSubject responseSubject = PublishSubject.create(); private PublishSubject requestSubject = PublishSubject.create(); @@ -217,7 +220,14 @@ private void processData() { if (message.type.equals("request")) { Messages.Request request = JsonUtils.fromJson(messageData, Messages.Request.class); - requestSubject.onNext(request); + if (this.isValidDAPRequest) { + requestSubject.onNext(request); + } else { + Messages.Response response = new Messages.Response(request.seq, request.command); + sendResponse(AdapterUtils.setErrorResponse(response, + ErrorCode.INVALID_DAP_HEADER, + String.format("'%s' request is rejected due to not being a valid DAP message.", request.command))); + } } else if (message.type.equals("response")) { Messages.Response response = JsonUtils.fromJson(messageData, Messages.Response.class); responseSubject.onNext(response); @@ -235,10 +245,20 @@ private void processData() { if (idx != -1) { Matcher matcher = CONTENT_LENGTH_MATCHER.matcher(rawMessage); if (matcher.find()) { - this.contentLength = Integer.parseInt(matcher.group(1)); - int headerByteLength = rawMessage.substring(0, idx + TWO_CRLF.length()) - .getBytes(PROTOCOL_ENCODING).length; + final String contentLengthText = matcher.group(1); + this.contentLength = Integer.parseInt(contentLengthText); + final String headerMessage = rawMessage.substring(0, idx + TWO_CRLF.length()); + final int headerByteLength = headerMessage.getBytes(PROTOCOL_ENCODING).length; this.rawData.removeFirst(headerByteLength); // Remove the header from the raw message. + + int expectedHeaderLength = 16 /*"Content-Length: ".length()*/ + contentLengthText.length(); + int actualHeaderLength = idx; + if (expectedHeaderLength != actualHeaderLength) { + this.isValidDAPRequest = false; + logger.log(Level.SEVERE, String.format("Illegal DAP request is detected: %s", headerMessage)); + } else { + this.isValidDAPRequest = true; + } continue; } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java index 681ec543d..e692e9b8d 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Events.java @@ -38,6 +38,11 @@ public static class StoppedEvent extends DebugEvent { public String description; public String text; public boolean allThreadsStopped; + /** + * A value of true hints to the client that this event should not change the + * focus. + */ + public Boolean preserveFocusHint; /** * Constructor. @@ -246,6 +251,30 @@ public UserNotificationEvent(NotificationType notifyType, String message) { } } + public static class TelemetryEvent extends DebugEvent { + /** + * The telemetry event name. + */ + public String name; + + /** + * The properties is an object as below. + * { + * [key: string]: string | number; + * } + */ + public Object properties; + + /** + * Constructor. + */ + public TelemetryEvent(String name, Object data) { + super("telemetry"); + this.name = name; + this.properties = data; + } + } + public static enum InvalidatedAreas { @SerializedName("all") ALL, diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java index 9b444155c..1129d230e 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Requests.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -17,6 +17,7 @@ import com.google.gson.annotations.SerializedName; import com.microsoft.java.debug.core.protocol.Types.DataBreakpoint; +import com.microsoft.java.debug.core.protocol.Types.Source; /** * The request arguments types defined by VSCode Debug Protocol. @@ -66,6 +67,15 @@ public static class ClassFilters { public String[] skipClasses = new String[0]; } + public static class ExceptionFilters extends ClassFilters { + /** + * Specifies that exceptions which are instances of refType will be reported. + * Note: this will include instances of sub-types. If null, all instances + * will be reported. + */ + public String[] exceptionTypes = new String[0]; + } + public static class StepFilters extends ClassFilters { /** * Deprecated - please use {@link ClassFilters#skipClasses } instead. @@ -270,6 +280,10 @@ public static class StepOutArguments extends StepArguments { } + public static class StepInTargetsArguments extends Arguments { + public int frameId; + } + public static class PauseArguments extends Arguments { public long threadId; } @@ -376,6 +390,51 @@ public boolean equals(Object obj) { } } + /** + * Arguments for breakpointLocations request. + */ + public static class BreakpointLocationsArguments extends Arguments { + /** + * The source location of the breakpoints; either `source.path` or + * `source.reference` must be specified. + */ + public Source source; + + /** + * Start line of range to search possible breakpoint locations in. If only the + * line is specified, the request returns all possible locations in that line. + */ + public int line; + + /** + * Start column of range to search possible breakpoint locations in. If no + * start column is given, the first column in the start line is assumed. + */ + public int column; + + /** + * End line of range to search possible breakpoint locations in. If no end + * line is given, then the end line is assumed to be the start line. + */ + public int endLine; + + /** + * End column of range to search possible breakpoint locations in. If no end + * column is given, then it is assumed to be in the last column of the end + * line. + */ + public int endColumn; + } + + public static class RefreshFramesArguments extends Arguments { + /** + * If provided, refresh the stack frames of the paused threads that previously + * requested decompiled sources for classes in the affected root paths. + * Otherwise, refresh all paused threads. + */ + public String[] affectedRootPaths; + } + public static enum Command { INITIALIZE("initialize", InitializeArguments.class), LAUNCH("launch", LaunchArguments.class), @@ -386,6 +445,8 @@ public static enum Command { CONTINUE("continue", ContinueArguments.class), STEPIN("stepIn", StepInArguments.class), STEPOUT("stepOut", StepOutArguments.class), + STEPIN_TARGETS("stepInTargets", + StepInTargetsArguments.class), PAUSE("pause", PauseArguments.class), STACKTRACE("stackTrace", StackTraceArguments.class), RESTARTFRAME("restartFrame", RestartFrameArguments.class), @@ -411,6 +472,8 @@ public static enum Command { INLINEVALUES("inlineValues", InlineValuesArguments.class), REFRESHVARIABLES("refreshVariables", RefreshVariablesArguments.class), PROCESSID("processId", Arguments.class), + BREAKPOINTLOCATIONS("breakpointLocations", BreakpointLocationsArguments.class), + REFRESHFRAMES("refreshFrames", RefreshFramesArguments.class), UNSUPPORTED("", Arguments.class); private String command; diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java index 2210c8e93..af701552e 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Responses.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2019 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -13,9 +13,11 @@ import java.util.List; +import com.microsoft.java.debug.core.protocol.Types.BreakpointLocation; import com.microsoft.java.debug.core.protocol.Types.DataBreakpointAccessType; import com.microsoft.java.debug.core.protocol.Types.ExceptionBreakMode; import com.microsoft.java.debug.core.protocol.Types.ExceptionDetails; +import com.microsoft.java.debug.core.protocol.Types.StepInTarget; import com.microsoft.java.debug.core.protocol.Types.Variable; /** @@ -282,6 +284,21 @@ public DataBreakpointInfoResponseBody(String dataId, String description, DataBre } } + /** + * Response to breakpointLocations request. + * Contains possible locations for source breakpoints. + */ + public static class BreakpointLocationsResponseBody extends ResponseBody { + /** + * Sorted set of possible breakpoint locations. + */ + public BreakpointLocation[] breakpoints; + + public BreakpointLocationsResponseBody(BreakpointLocation[] breakpoints) { + this.breakpoints = breakpoints; + } + } + public static class ContinueResponseBody extends ResponseBody { public boolean allThreadsContinued; @@ -348,4 +365,12 @@ public InlineValuesResponse(Variable[] variables) { this.variables = variables; } } + + public static class StepInTargetsResponse extends ResponseBody { + public StepInTarget[] targets; + + public StepInTargetsResponse(StepInTarget[] targets) { + this.targets = targets; + } + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java index f0a91ce88..33308af6d 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/protocol/Types.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2019 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -16,7 +16,7 @@ import com.google.gson.annotations.SerializedName; /** - * The data types defined by VSCode Debug Protocol. + * The data types defined by Debug Adapter Protocol. */ public class Types { public static class Message { @@ -43,6 +43,8 @@ public static class StackFrame { public int line; public int column; public String name; + public String presentationHint; + /** * Constructs a StackFrame with the given information. @@ -57,13 +59,17 @@ public static class StackFrame { * line number of the stack frame * @param col * column number of the stack frame + * @param presentationHint + * An optional hint for how to present this frame in the UI. + * Values: 'normal', 'label', 'subtle' */ - public StackFrame(int id, String name, Source src, int ln, int col) { + public StackFrame(int id, String name, Source src, int ln, int col, String presentationHint) { this.id = id; this.name = name; this.source = src; this.line = ln; this.column = col; + this.presentationHint = presentationHint; } } @@ -194,13 +200,19 @@ public Breakpoint(int id, boolean verified, int line, String message) { } } + /** + * Properties of a breakpoint or logpoint passed to the setBreakpoints request. + */ public static class SourceBreakpoint { public int line; + public int column; public String hitCondition; public String condition; public String logMessage; - public SourceBreakpoint() { + public SourceBreakpoint(int line, int column) { + this.line = line; + this.column = column; } /** @@ -211,6 +223,16 @@ public SourceBreakpoint(int line, String condition, String hitCondition) { this.condition = condition; this.hitCondition = hitCondition; } + + /** + * Constructor. + */ + public SourceBreakpoint(int line, String condition, String hitCondition, int column) { + this.line = line; + this.column = column; + this.condition = condition; + this.hitCondition = hitCondition; + } } public static class FunctionBreakpoint { @@ -283,6 +305,46 @@ public DataBreakpoint(String dataId, DataBreakpointAccessType accessType, String } } + /** + * Properties of a breakpoint location returned from the breakpointLocations request. + */ + public static class BreakpointLocation { + /** + * Start line of breakpoint location. + */ + public int line; + + /** + * The start column of breakpoint location. + */ + public int column; + + /** + * The end line of breakpoint location if the location covers a range. + */ + public int endLine; + + /** + * The end column of breakpoint location if the location covers a range. + */ + public int endColumn; + + public BreakpointLocation() { + } + + public BreakpointLocation(int line, int column) { + this.line = line; + this.column = column; + } + + public BreakpointLocation(int line, int column, int endLine, int endColumn) { + this.line = line; + this.column = column; + this.endLine = endLine; + this.endColumn = endColumn; + } + } + public static class CompletionItem { public String label; public String text; @@ -368,5 +430,24 @@ public static class Capabilities { public ExceptionBreakpointFilter[] exceptionBreakpointFilters = new ExceptionBreakpointFilter[0]; public boolean supportsDataBreakpoints; public boolean supportsClipboardContext; + public boolean supportsFunctionBreakpoints; + // https://microsoft.github.io/debug-adapter-protocol/specification#Requests_BreakpointLocations + public boolean supportsBreakpointLocationsRequest; + public boolean supportsStepInTargetsRequest; } + + public static class StepInTarget { + public int id; + public String label; + public int line; + public int column; + public int endLine; + public int endColumn; + + public StepInTarget(int id, String label) { + this.id = id; + this.label = label; + } + } + } diff --git a/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/BreakpointTest.java b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/BreakpointTest.java new file mode 100644 index 000000000..5cc6fa3d7 --- /dev/null +++ b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/BreakpointTest.java @@ -0,0 +1,17 @@ +package com.microsoft.java.debug.core; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class BreakpointTest { + @Test + public void testToNoneGeneric() { + assertEquals("Ljava.util.List;", Breakpoint.toNoneGeneric("Ljava.util.List;")); + assertEquals("(Ljava/util/Map;)Ljava/util/Map;", Breakpoint.toNoneGeneric( + "(Ljava/util/Map;>;)Ljava/util/Map;>;")); + assertEquals("(Ljava/util/Map;)Ljava/util/Map;", + Breakpoint.toNoneGeneric( + "(Ljava/util/Map;Ljava/util/List;>;)Ljava/util/Map;Ljava/util/List;>;")); + } +} diff --git a/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/formatter/NumericFormatterTest.java b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/formatter/NumericFormatterTest.java index 001b928e9..372eb8163 100644 --- a/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/formatter/NumericFormatterTest.java +++ b/com.microsoft.java.debug.core/src/test/java/com/microsoft/java/debug/core/adapter/formatter/NumericFormatterTest.java @@ -29,6 +29,8 @@ import com.sun.jdi.Value; import com.sun.jdi.VirtualMachine; +import java.util.Locale; + import static com.microsoft.java.debug.core.adapter.formatter.NumericFormatter.NUMERIC_FORMAT_OPTION; import static com.microsoft.java.debug.core.adapter.formatter.NumericFormatter.NUMERIC_PRECISION_OPTION; import static org.junit.Assert.*; @@ -38,6 +40,7 @@ public class NumericFormatterTest extends BaseJdiTestCase { @Before public void setup() throws Exception { super.setup(); + Locale.setDefault(Locale.US); formatter = new NumericFormatter(); } diff --git a/com.microsoft.java.debug.plugin/.classpath b/com.microsoft.java.debug.plugin/.classpath index 94b396327..b2be945c8 100644 --- a/com.microsoft.java.debug.plugin/.classpath +++ b/com.microsoft.java.debug.plugin/.classpath @@ -1,16 +1,16 @@ - + - + - - + + diff --git a/com.microsoft.java.debug.plugin/.project b/com.microsoft.java.debug.plugin/.project index 72ba17ff7..529a656dd 100644 --- a/com.microsoft.java.debug.plugin/.project +++ b/com.microsoft.java.debug.plugin/.project @@ -39,12 +39,12 @@ - 1599036548577 + 1665543654719 30 org.eclipse.core.resources.regexFilterMatcher - node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ diff --git a/com.microsoft.java.debug.plugin/META-INF/MANIFEST.MF b/com.microsoft.java.debug.plugin/META-INF/MANIFEST.MF index 0f59e7eb9..f2ceffb53 100644 --- a/com.microsoft.java.debug.plugin/META-INF/MANIFEST.MF +++ b/com.microsoft.java.debug.plugin/META-INF/MANIFEST.MF @@ -2,8 +2,8 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Java Debug Server Plugin Bundle-SymbolicName: com.microsoft.java.debug.plugin;singleton:=true -Bundle-Version: 0.36.0 -Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Version: 0.53.2 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy Bundle-Activator: com.microsoft.java.debug.plugin.internal.JavaDebuggerServerPlugin Bundle-Vendor: Microsoft @@ -21,8 +21,8 @@ Require-Bundle: org.eclipse.core.runtime, org.apache.commons.lang3, org.eclipse.lsp4j, com.google.guava -Bundle-ClassPath: lib/commons-io-2.11.0.jar, +Bundle-ClassPath: lib/commons-io-2.19.0.jar, ., lib/rxjava-2.2.21.jar, - lib/reactive-streams-1.0.3.jar, - lib/com.microsoft.java.debug.core-0.36.0.jar + lib/reactive-streams-1.0.4.jar, + lib/com.microsoft.java.debug.core-0.53.2.jar diff --git a/com.microsoft.java.debug.plugin/pom.xml b/com.microsoft.java.debug.plugin/pom.xml index 1e8e8c5d0..2e2c0e6a8 100644 --- a/com.microsoft.java.debug.plugin/pom.xml +++ b/com.microsoft.java.debug.plugin/pom.xml @@ -5,7 +5,7 @@ com.microsoft.java java-debug-parent - 0.36.0 + 0.53.2 com.microsoft.java.debug.plugin eclipse-plugin @@ -46,17 +46,17 @@ org.reactivestreams reactive-streams - 1.0.3 + 1.0.4 commons-io commons-io - 2.11.0 + 2.19.0 com.microsoft.java com.microsoft.java.debug.core - 0.36.0 + 0.53.2 diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/BindingUtils.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/BindingUtils.java new file mode 100644 index 000000000..085bf0d4c --- /dev/null +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/BindingUtils.java @@ -0,0 +1,63 @@ +/******************************************************************************* + * Copyright (c) 2017-2022 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation +*******************************************************************************/ + +package com.microsoft.java.debug; + +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.internal.debug.core.breakpoints.LambdaLocationLocatorHelper; + +/** + * Utility methods around working with JDT Bindings. + */ +@SuppressWarnings("restriction") +public final class BindingUtils { + private BindingUtils() { + + } + + /** + * Return the method name from the binding using either the + * {@link IMethodBinding#getKey()} or {@link IMethodBinding#getName()}. The key + * can be used to find the name of a generated lambda method if the minding + * represents a lambda method. + * + * @param binding the binding to extract the name from. + * @param fromKey use binging key to resolve the method name. + * @return the name of the method. + */ + public static String getMethodName(IMethodBinding binding, boolean fromKey) { + if (fromKey) { + String key = binding.getKey(); + int dotAt = key.indexOf('.'); + int end = key.indexOf('<', dotAt); + if (end == -1) { + end = key.indexOf('('); + } else { + end = Math.min(end, key.indexOf('(')); + } + return key.substring(dotAt + 1, end); + } else { + return binding.getName(); + } + } + + /** + * Returns the method signature of the method represented by the binding + * including the synthetic outer locals. + * + * @param binding the binding which the signature must be resolved for. + * @return the signature or null if the signature could not be resolved. + */ + public static String toSignature(IMethodBinding binding) { + return LambdaLocationLocatorHelper.toMethodSignature(binding); + } + +} diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/BreakpointLocationLocator.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/BreakpointLocationLocator.java new file mode 100644 index 000000000..97581242e --- /dev/null +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/BreakpointLocationLocator.java @@ -0,0 +1,73 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Gayan Perera (gayanper@gmail.com) - initial API and implementation +*******************************************************************************/ +package com.microsoft.java.debug; + +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; + +@SuppressWarnings("restriction") +public class BreakpointLocationLocator + extends org.eclipse.jdt.internal.debug.core.breakpoints.ValidBreakpointLocationLocator { + + private IMethodBinding methodBinding; + + public BreakpointLocationLocator(CompilationUnit compilationUnit, int lineNumber, + boolean bindingsResolved, + boolean bestMatch, int offset, int end) { + super(compilationUnit, lineNumber, bindingsResolved, bestMatch, offset, end); + } + + @Override + public boolean visit(MethodDeclaration node) { + boolean result = super.visit(node); + if (methodBinding == null && getLocationType() == LOCATION_METHOD) { + this.methodBinding = node.resolveBinding(); + } + return result; + } + + /** + * Returns the signature of method found if the + * {@link org.eclipse.jdt.internal.debug.core.breakpoints.ValidBreakpointLocationLocator#getLocationType()} + * is + * {@link org.eclipse.jdt.internal.debug.core.breakpoints.ValidBreakpointLocationLocator#LOCATION_METHOD}. + * Otherwise return null. + */ + public String getMethodSignature() { + if (this.methodBinding == null) { + return null; + } + return BindingUtils.toSignature(this.methodBinding); + } + + /** + * Returns the name of method found if the + * {@link org.eclipse.jdt.internal.debug.core.breakpoints.ValidBreakpointLocationLocator#getLocationType()} + * is + * {@link org.eclipse.jdt.internal.debug.core.breakpoints.ValidBreakpointLocationLocator#LOCATION_METHOD}. + * Otherwise return null. + */ + public String getMethodName() { + if (this.methodBinding == null) { + return null; + } + return this.methodBinding.getName(); + } + + @Override + public String getFullyQualifiedTypeName() { + if (this.methodBinding != null) { + return this.methodBinding.getDeclaringClass().getQualifiedName(); + } + return super.getFullyQualifiedTypeName(); + } +} diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/LambdaExpressionLocator.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/LambdaExpressionLocator.java new file mode 100644 index 000000000..a5456809a --- /dev/null +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/LambdaExpressionLocator.java @@ -0,0 +1,103 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Gayan Perera (gayanper@gmail.com) - initial API and implementation +*******************************************************************************/ +package com.microsoft.java.debug; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.AbstractTypeDeclaration; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.LambdaExpression; + +public class LambdaExpressionLocator extends ASTVisitor { + private CompilationUnit compilationUnit; + private int line; + private int column; + private boolean found; + + private IMethodBinding lambdaMethodBinding; + private LambdaExpression lambdaExpression; + + public LambdaExpressionLocator(CompilationUnit compilationUnit, int line, int column) { + this.compilationUnit = compilationUnit; + this.line = line; + this.column = column; + } + + @Override + public boolean visit(LambdaExpression node) { + if (column > -1) { + int startPosition = node.getStartPosition(); + int breakOffset = this.compilationUnit.getPosition(line, column); + // lambda on same line: + // list.stream().map(i -> i + 1); + // + // lambda on multiple lines: + // list.stream().map(user + // -> user.isSystem() ? new SystemUser(user) : new EndUser(user)); + + // Since the debugger supports BreakpointLocations Request to hint user + // about possible inline breakpoint locations, we will only support + // inline breakpoints added where a lambda expression begins. + if (breakOffset == startPosition) { + this.lambdaMethodBinding = node.resolveMethodBinding(); + this.found = true; + this.lambdaExpression = node; + return false; + } + } + return super.visit(node); + } + + /** + * Returns true if a lambda is found at given location. + */ + public boolean isFound() { + return found; + } + + /** + * Returns the signature of lambda method otherwise return null. + */ + public String getMethodSignature() { + if (!this.found) { + return null; + } + return BindingUtils.toSignature(this.lambdaMethodBinding); + } + + /** + * Returns the name of lambda method otherwise return null. + */ + public String getMethodName() { + if (!this.found) { + return null; + } + return BindingUtils.getMethodName(lambdaMethodBinding, true); + } + + /** + * Returns the name of the type which the lambda method is found. + */ + public String getFullyQualifiedTypeName() { + if (this.found) { + ASTNode parent = lambdaExpression.getParent(); + while (parent != null) { + if (parent instanceof AbstractTypeDeclaration) { + AbstractTypeDeclaration declaration = (AbstractTypeDeclaration) parent; + return declaration.resolveBinding().getBinaryName(); + } + parent = parent.getParent(); + } + } + return null; + } +} diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/AdvancedLaunchingConnector.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/AdvancedLaunchingConnector.java index 3551ab2e6..24c65785d 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/AdvancedLaunchingConnector.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/AdvancedLaunchingConnector.java @@ -195,7 +195,6 @@ private static String[] constructLaunchCommand(Map l StringBuilder execString = new StringBuilder(); execString.append("\"" + javaHome + slash + "bin" + slash + javaExec + "\""); - execString.append(" -Xdebug -Xnoagent -Djava.compiler=NONE"); execString.append(" -Xrunjdwp:transport=dt_socket,address=" + address + ",server=n,suspend=" + (suspend ? "y" : "n")); if (javaOptions != null) { execString.append(" " + javaOptions); diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/Compile.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/Compile.java new file mode 100644 index 000000000..245281816 --- /dev/null +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/Compile.java @@ -0,0 +1,190 @@ +/******************************************************************************* + * Copyright (c) 2022 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ + +package com.microsoft.java.debug.plugin.internal; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +import org.apache.commons.lang3.StringUtils; +import org.eclipse.core.resources.IBuildConfiguration; +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceStatus; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.IncrementalProjectBuilder; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.ls.core.internal.BuildWorkspaceStatus; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; +import org.eclipse.jdt.ls.core.internal.ResourceUtils; +import org.eclipse.jdt.ls.core.internal.handlers.BuildWorkspaceHandler; +import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.extended.ProjectBuildParams; + +import com.microsoft.java.debug.core.Configuration; + +public class Compile { + private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); + + private static final int GRADLE_BS_COMPILATION_ERROR = 100; + + public static Object compile(CompileParams params, IProgressMonitor monitor) { + if (params == null) { + throw new IllegalArgumentException("The compile parameters should not be null."); + } + + IProject mainProject = JdtUtils.getMainProject(params.getProjectName(), params.getMainClass()); + if (JdtUtils.isBspProject(mainProject) && !ProjectUtils.isGradleProject(mainProject)) { + // Just need to trigger a build for the target project, the Gradle build server will + // handle the build dependencies for us. + try { + ResourcesPlugin.getWorkspace().build( + new IBuildConfiguration[]{mainProject.getActiveBuildConfig()}, + IncrementalProjectBuilder.INCREMENTAL_BUILD, + false /*buildReference*/, + monitor + ); + } catch (CoreException e) { + if (e.getStatus().getCode() == IResourceStatus.BUILD_FAILED) { + return GRADLE_BS_COMPILATION_ERROR; + } else { + return BuildWorkspaceStatus.FAILED; + } + } + return BuildWorkspaceStatus.SUCCEED; + } + + if (monitor.isCanceled()) { + return BuildWorkspaceStatus.CANCELLED; + } + + ProjectBuildParams buildParams = new ProjectBuildParams(); + List identifiers = new LinkedList<>(); + buildParams.setFullBuild(params.isFullBuild); + for (IJavaProject javaProject : ProjectUtils.getJavaProjects()) { + if (ProjectsManager.getDefaultProject().equals(javaProject.getProject())) { + continue; + } + // we only build project which is not a BSP project, in case that the compile request is triggered by + // HCR with auto-build disabled, the build for BSP projects will be triggered by JavaHotCodeReplaceProvider. + if (!JdtUtils.isBspProject(javaProject.getProject())) { + identifiers.add(new TextDocumentIdentifier(javaProject.getProject().getLocationURI().toString())); + } + } + if (identifiers.size() == 0) { + return BuildWorkspaceStatus.SUCCEED; + } + + buildParams.setIdentifiers(identifiers); + long compileAt = System.currentTimeMillis(); + BuildWorkspaceHandler buildWorkspaceHandler = new BuildWorkspaceHandler(JavaLanguageServerPlugin.getProjectsManager()); + BuildWorkspaceStatus status = buildWorkspaceHandler.buildProjects(buildParams, monitor); + logger.info("Time cost for ECJ: " + (System.currentTimeMillis() - compileAt) + "ms"); + if (status == BuildWorkspaceStatus.FAILED || status == BuildWorkspaceStatus.CANCELLED) { + return status; + } + + try { + IResource currentResource = mainProject; + if (isUnmanagedFolder(mainProject) && StringUtils.isNotBlank(params.getMainClass())) { + IType mainType = ProjectUtils.getJavaProject(mainProject).findType(params.getMainClass()); + if (mainType != null && mainType.getResource() != null) { + currentResource = mainType.getResource(); + } + } + + List problemMarkers = new ArrayList<>(); + if (currentResource != null) { + List markers = ResourceUtils.getErrorMarkers(currentResource); + if (markers != null) { + problemMarkers.addAll(markers); + } + + // Check if the referenced projects contain compilation errors. + if (currentResource instanceof IProject && ProjectUtils.isJavaProject((IProject) currentResource)) { + IJavaProject currentJavaProject = ProjectUtils.getJavaProject((IProject) currentResource); + IJavaProject[] javaProjects = ProjectUtils.getJavaProjects(); + for (IJavaProject otherJavaProject : javaProjects) { + IProject other = otherJavaProject.getProject(); + if (!other.equals(getDefaultProject()) && !other.equals((IProject) currentResource) + && currentJavaProject.isOnClasspath(otherJavaProject)) { + markers = ResourceUtils.getErrorMarkers(other); + if (markers != null) { + problemMarkers.addAll(markers); + } + } + } + } + } else { + IJavaProject[] javaProjects = ProjectUtils.getJavaProjects(); + for (IJavaProject javaProject : javaProjects) { + IProject project = javaProject.getProject(); + if (!project.equals(getDefaultProject())) { + List markers = ResourceUtils.getErrorMarkers(project); + if (markers != null) { + problemMarkers.addAll(markers); + } + } + } + } + + if (!problemMarkers.isEmpty()) { + return BuildWorkspaceStatus.WITH_ERROR; + } + } catch (CoreException e) { + JavaLanguageServerPlugin.log(e); + } + + return BuildWorkspaceStatus.SUCCEED; + } + + private static boolean isUnmanagedFolder(IProject project) { + return project != null && ProjectUtils.isUnmanagedFolder(project) + && ProjectUtils.isJavaProject(project); + } + + private static IProject getDefaultProject() { + return getWorkspaceRoot().getProject(ProjectsManager.DEFAULT_PROJECT_NAME); + } + + private static IWorkspaceRoot getWorkspaceRoot() { + return ResourcesPlugin.getWorkspace().getRoot(); + } + + class CompileParams { + String projectName; + String mainClass; + boolean isFullBuild = false; + + public String getMainClass() { + return mainClass; + } + + public boolean isFullBuild() { + return isFullBuild; + } + + public String getProjectName() { + return projectName; + } + } +} diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/CompletionProposalRequestor.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/CompletionProposalRequestor.java index be7932992..431c283f5 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/CompletionProposalRequestor.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/CompletionProposalRequestor.java @@ -28,6 +28,7 @@ import org.eclipse.jdt.core.CompletionContext; import org.eclipse.jdt.core.CompletionProposal; import org.eclipse.jdt.core.CompletionRequestor; +import org.eclipse.jdt.core.Flags; import org.eclipse.jdt.core.IClassFile; import org.eclipse.jdt.core.IClasspathEntry; import org.eclipse.jdt.core.IJavaElement; @@ -45,6 +46,7 @@ import org.eclipse.jdt.ls.core.internal.handlers.CompletionResponses; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.CompletionItemLabelDetails; import com.google.common.collect.ImmutableSet; import com.microsoft.java.debug.core.Configuration; @@ -70,6 +72,8 @@ public final class CompletionProposalRequestor extends CompletionRequestor { CompletionItemKind.Text); // @formatter:on + private static boolean isFilterFailed = false; + /** * Constructor. * @param typeRoot ITypeRoot @@ -153,19 +157,40 @@ public List getCompletionItems() { */ public CompletionItem toCompletionItem(CompletionProposal proposal, int index) { final CompletionItem $ = new CompletionItem(); - $.setKind(mapKind(proposal.getKind())); + $.setKind(mapKind(proposal.getKind(), proposal.getFlags())); Map data = new HashMap<>(); data.put(CompletionResolveHandler.DATA_FIELD_REQUEST_ID, String.valueOf(response.getId())); data.put(CompletionResolveHandler.DATA_FIELD_PROPOSAL_ID, String.valueOf(index)); $.setData(data); this.descriptionProvider.updateDescription(proposal, $); + // Use fully qualified name as needed. + $.setInsertText(String.valueOf(proposal.getCompletion())); adjustCompleteItem($); $.setSortText(SortTextHelper.computeSortText(proposal)); return $; } private void adjustCompleteItem(CompletionItem item) { - if (item.getKind() == CompletionItemKind.Function) { + CompletionItemKind itemKind = item.getKind(); + if (itemKind == CompletionItemKind.Class || itemKind == CompletionItemKind.Interface + || itemKind == CompletionItemKind.Enum) { + // Display the package name in the label property. + CompletionItemLabelDetails labelDetails = item.getLabelDetails(); + if (labelDetails != null && StringUtils.isNotBlank(labelDetails.getDescription())) { + item.setLabel(item.getLabel() + " - " + labelDetails.getDescription()); + } + } else if (itemKind == CompletionItemKind.Function) { + // Merge the label details into the label property + // because the completion provider in DEBUG CONSOLE + // doesn't support the label details. + CompletionItemLabelDetails labelDetails = item.getLabelDetails(); + if (labelDetails != null && StringUtils.isNotBlank(labelDetails.getDetail())) { + item.setLabel(item.getLabel() + labelDetails.getDetail()); + } + if (labelDetails != null && StringUtils.isNotBlank(labelDetails.getDescription())) { + item.setLabel(item.getLabel() + " : " + labelDetails.getDescription()); + } + String text = item.getInsertText(); if (StringUtils.isNotBlank(text) && !text.endsWith(")")) { item.setInsertText(text + "()"); @@ -181,7 +206,7 @@ public void acceptContext(CompletionContext context) { this.descriptionProvider = new CompletionProposalDescriptionProvider(context); } - private CompletionItemKind mapKind(final int kind) { + private CompletionItemKind mapKind(final int kind, final int flags) { // When a new CompletionItemKind is added, don't forget to update // SUPPORTED_KINDS switch (kind) { @@ -190,6 +215,11 @@ private CompletionItemKind mapKind(final int kind) { return CompletionItemKind.Constructor; case CompletionProposal.ANONYMOUS_CLASS_DECLARATION: case CompletionProposal.TYPE_REF: + if (Flags.isInterface(flags)) { + return CompletionItemKind.Interface; + } else if (Flags.isEnum(flags)) { + return CompletionItemKind.Enum; + } return CompletionItemKind.Class; case CompletionProposal.FIELD_IMPORT: case CompletionProposal.METHOD_IMPORT: @@ -199,6 +229,9 @@ private CompletionItemKind mapKind(final int kind) { return CompletionItemKind.Module; case CompletionProposal.FIELD_REF: case CompletionProposal.FIELD_REF_WITH_CASTED_RECEIVER: + if (Flags.isStatic(flags) && Flags.isFinal(flags)) { + return CompletionItemKind.Constant; + } return CompletionItemKind.Field; case CompletionProposal.KEYWORD: return CompletionItemKind.Keyword; @@ -290,7 +323,7 @@ private boolean isFiltered(CompletionProposal proposal) { case CompletionProposal.JAVADOC_TYPE_REF: case CompletionProposal.TYPE_REF: { char[] declaringType = getDeclaringType(proposal); - return declaringType != null && org.eclipse.jdt.ls.core.internal.contentassist.TypeFilter.isFiltered(declaringType); + return declaringType != null && isFiltered(declaringType); } default: // do nothing } @@ -301,6 +334,22 @@ private boolean isFiltered(CompletionProposal proposal) { return false; } + // Temp workaround for the completion error https://github.com/microsoft/java-debug/issues/534 + private static boolean isFiltered(char[] fullTypeName) { + if (isFilterFailed) { + return false; + } + + try { + return JavaLanguageServerPlugin.getInstance().getTypeFilter().filter(new String(fullTypeName)); + } catch (NoSuchMethodError ex) { + isFilterFailed = true; + JavaLanguageServerPlugin.logException("isFiltered for the completion failed.", ex); + } + + return false; + } + /** * copied from * org.eclipse.jdt.ui.text.java.CompletionProposalCollector.getDeclaringType(CompletionProposal) diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/InlineValueHandler.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/InlineValueHandler.java index 5c2cf8834..15aeeb2b4 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/InlineValueHandler.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/InlineValueHandler.java @@ -166,7 +166,7 @@ private static IMethod findMethodInLocalTypes(IMethod enclosingMethod, int stopp */ private static Position getPosition(IBuffer buffer, int offset) { int[] result = JsonRpcHelpers.toLine(buffer, offset); - if (result == null && result.length < 1) { + if (result == null || result.length < 1) { return new Position(-1, -1); } diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java index 1875d514e..f4e7b0ba3 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebugDelegateCommandHandler.java @@ -1,5 +1,5 @@ /******************************************************************************* -* Copyright (c) 2017-2019 Microsoft Corporation and others. +* Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -64,8 +64,12 @@ public Object executeCommand(String commandId, List arguments, IProgress ResolveMainClassHandler resolveMainClassHandler = new ResolveMainClassHandler(); return resolveMainClassHandler.resolveMainClass(arguments); case BUILD_WORKSPACE: - // TODO - break; + Compile.CompileParams params = null; + if (arguments != null && !arguments.isEmpty()) { + params = JsonUtils.fromJson((String) arguments.get(0), Compile.CompileParams.class); + } + + return Compile.compile(params, progress); case FETCH_USER_DATA: return UsageDataStore.getInstance().fetchAll(); case UPDATE_DEBUG_SETTINGS: diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebuggerServerPlugin.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebuggerServerPlugin.java index 975338371..d3307a5b2 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebuggerServerPlugin.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaDebuggerServerPlugin.java @@ -34,7 +34,6 @@ public void start(BundleContext context) throws Exception { @Override public void stop(BundleContext context) throws Exception { - logger.info("Stopping " + PLUGIN_ID); LogUtils.cleanupHandlers(); } diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaHotCodeReplaceProvider.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaHotCodeReplaceProvider.java index 2bf905c89..3a2b2f3df 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaHotCodeReplaceProvider.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JavaHotCodeReplaceProvider.java @@ -34,16 +34,20 @@ import java.util.logging.Level; import java.util.logging.Logger; +import org.eclipse.core.resources.IBuildConfiguration; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; +import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaElement; @@ -55,6 +59,7 @@ import org.eclipse.jdt.core.util.ISourceAttribute; import org.eclipse.jdt.internal.core.util.Util; import org.eclipse.jdt.ls.core.internal.JobHelpers; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugException; @@ -63,6 +68,7 @@ import com.microsoft.java.debug.core.IDebugSession; import com.microsoft.java.debug.core.StackFrameUtility; import com.microsoft.java.debug.core.adapter.AdapterUtils; +import com.microsoft.java.debug.core.adapter.Constants; import com.microsoft.java.debug.core.adapter.ErrorCode; import com.microsoft.java.debug.core.adapter.HotCodeReplaceEvent; import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; @@ -104,6 +110,8 @@ public class JavaHotCodeReplaceProvider implements IHotCodeReplaceProvider, IRes private List deltaClassNames = new ArrayList<>(); + private String mainProjectName = ""; + /** * Visitor for resource deltas. */ @@ -269,6 +277,7 @@ public void initialize(IDebugAdapterContext context, Map options } this.context = context; currentDebugSession = context.getDebugSession(); + this.mainProjectName = ((String) options.get(Constants.PROJECT_NAME)); } @Override @@ -319,6 +328,7 @@ public void onClassRedefined(Consumer> consumer) { @Override public CompletableFuture> redefineClasses() { + triggerBuildForBspProject(); JobHelpers.waitForBuildJobs(10 * 1000); return CompletableFuture.supplyAsync(() -> { List classNames = new ArrayList<>(); @@ -737,4 +747,39 @@ private List getStackFrames(ThreadReference thread, boolean refresh) } }); } + + /** + * Trigger build separately if the main project is a BSP project. + * This is because auto build for BSP project will not update the class files to disk. + */ + private void triggerBuildForBspProject() { + // check if the workspace contains BSP project first. This is for performance consideration. + // Due to that getJavaProjectFromType() is a heavy operation. + if (!containsBspProjects()) { + return; + } + + IProject mainProject = JdtUtils.getMainProject(this.mainProjectName, context.getMainClass()); + if (mainProject != null && JdtUtils.isBspProject(mainProject)) { + try { + ResourcesPlugin.getWorkspace().build( + new IBuildConfiguration[]{mainProject.getActiveBuildConfig()}, + IncrementalProjectBuilder.INCREMENTAL_BUILD, + false /*buildReference*/, + new NullProgressMonitor() + ); + } catch (CoreException e) { + // ignore compilation errors + } + } + } + + private boolean containsBspProjects() { + for (IJavaProject javaProject : ProjectUtils.getJavaProjects()) { + if (JdtUtils.isBspProject(javaProject.getProject())) { + return true; + } + } + return false; + } } diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java index efc7ce20c..5652b922e 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017-2021 Microsoft Corporation and others. + * Copyright (c) 2017-2022 Microsoft Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -19,22 +19,29 @@ import java.net.URISyntaxException; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; -import com.microsoft.java.debug.core.Configuration; -import com.microsoft.java.debug.core.DebugException; -import com.microsoft.java.debug.core.adapter.AdapterUtils; -import com.microsoft.java.debug.core.adapter.Constants; -import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; -import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider; - +import org.apache.commons.lang3.StringUtils; +import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.core.runtime.URIUtil; import org.eclipse.debug.core.sourcelookup.ISourceContainer; import org.eclipse.jdt.core.IBuffer; import org.eclipse.jdt.core.IClassFile; @@ -44,19 +51,47 @@ import org.eclipse.jdt.core.ITypeRoot; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.Signature; import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.LambdaExpression; +import org.eclipse.jdt.core.manipulation.CoreASTProvider; import org.eclipse.jdt.internal.core.JarPackageFragmentRoot; -import org.eclipse.jdt.internal.debug.core.breakpoints.ValidBreakpointLocationLocator; import org.eclipse.jdt.launching.IVMInstall; import org.eclipse.jdt.launching.JavaRuntime; import org.eclipse.jdt.launching.LibraryLocation; +import org.eclipse.jdt.ls.core.internal.DecompilerResult; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.managers.ContentProviderManager; + +import com.microsoft.java.debug.BindingUtils; +import com.microsoft.java.debug.BreakpointLocationLocator; +import com.microsoft.java.debug.LambdaExpressionLocator; +import com.microsoft.java.debug.core.Configuration; +import com.microsoft.java.debug.core.DebugException; +import com.microsoft.java.debug.core.DebugSettings; +import com.microsoft.java.debug.core.DebugSettings.Switch; +import com.microsoft.java.debug.core.JavaBreakpointLocation; +import com.microsoft.java.debug.core.adapter.AdapterUtils; +import com.microsoft.java.debug.core.adapter.Constants; +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider; +import com.microsoft.java.debug.core.protocol.Types.BreakpointLocation; +import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint; public class JdtSourceLookUpProvider implements ISourceLookUpProvider { private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); private static final String JDT_SCHEME = "jdt"; private static final String PATH_SEPARATOR = "/"; + private static final Set IMPLICITLY_DECLARED_CLASSES = new HashSet<>( + Arrays.asList("org.eclipse.jdt.core.dom.UnnamedClass", + "org.eclipse.jdt.core.dom.ImplicitTypeDeclaration")); private ISourceContainer[] sourceContainers = null; private HashMap options = new HashMap(); @@ -78,7 +113,8 @@ public void initialize(IDebugAdapterContext context, Map props) throw new IllegalArgumentException("argument is null"); } options.putAll(props); - // During initialization, trigger a background job to load the source containers to improve the perf. + // During initialization, trigger a background job to load the source containers + // to improve the perf. new Thread(() -> { getSourceContainers(); }).start(); @@ -111,67 +147,246 @@ public String[] getFullyQualifiedName(String uri, int[] lines, int[] columns) th return new String[0]; } + SourceBreakpoint[] sourceBreakpoints = new SourceBreakpoint[lines.length]; + for (int i = 0; i < lines.length; i++) { + sourceBreakpoints[i] = new SourceBreakpoint(lines[i], columns[i]); + } + + JavaBreakpointLocation[] locations = getBreakpointLocations(uri, sourceBreakpoints); + return Stream.of(locations).map(location -> { + if (location.className() != null && location.methodName() != null) { + return location.className() + .concat("#").concat(location.methodName()) + .concat("#").concat(location.methodSignature()); + } + return location.className(); + }).toArray(String[]::new); + } + + @Override + public JavaBreakpointLocation[] getBreakpointLocations(String sourceUri, SourceBreakpoint[] sourceBreakpoints) + throws DebugException { + if (sourceUri == null) { + throw new IllegalArgumentException("sourceUri is null"); + } + + if (sourceBreakpoints == null || sourceBreakpoints.length == 0) { + return new JavaBreakpointLocation[0]; + } + + CompilationUnit astUnit = asCompilationUnit(sourceUri); + JavaBreakpointLocation[] sourceLocations = Stream.of(sourceBreakpoints) + .map(sourceBreakpoint -> new JavaBreakpointLocation(sourceBreakpoint.line, sourceBreakpoint.column)) + .toArray(JavaBreakpointLocation[]::new); + if (astUnit != null) { + List types = astUnit.types(); + String unnamedClass = null; + // See https://github.com/eclipse-jdt/eclipse.jdt.core/pull/2220 + // Given that the JDT plans to rename UnamedClass to ImplicitTypeDeclaration, we will check + // the class name of the ASTNode to prevent the potential breaking in the future. + if (types.size() == 1 && IMPLICITLY_DECLARED_CLASSES.contains(types.get(0).getClass().getName())) { + unnamedClass = inferPrimaryTypeName(sourceUri, astUnit); + } + Map resolvedLocations = new HashMap<>(); + for (JavaBreakpointLocation sourceLocation : sourceLocations) { + int sourceLine = sourceLocation.lineNumber(); + int sourceColumn = sourceLocation.columnNumber(); + if (sourceColumn > -1) { + // if we have a column, try to find the lambda expression at that column + LambdaExpressionLocator lambdaExpressionLocator = new LambdaExpressionLocator(astUnit, + sourceLine, sourceColumn); + astUnit.accept(lambdaExpressionLocator); + if (lambdaExpressionLocator.isFound()) { + sourceLocation.setClassName(lambdaExpressionLocator.getFullyQualifiedTypeName()); + sourceLocation.setMethodName(lambdaExpressionLocator.getMethodName()); + sourceLocation.setMethodSignature(lambdaExpressionLocator.getMethodSignature()); + } + + if (resolvedLocations.containsKey(sourceLine)) { + sourceLocation.setAvailableBreakpointLocations(resolvedLocations.get(sourceLine)); + } else { + BreakpointLocation[] inlineLocations = getInlineBreakpointLocations(astUnit, sourceLine); + sourceLocation.setAvailableBreakpointLocations(inlineLocations); + resolvedLocations.put(sourceLine, inlineLocations); + } + + continue; + } + + // TODO + // The ValidBreakpointLocationLocator will verify if the current line is a valid + // location or not. + // If so, it will return the fully qualified name of the class type that + // contains the current line. + // Otherwise, it will try to find a valid location from the next lines and + // return it's fully qualified name. + // In current stage, we don't support to move the invalid breakpoint down to the + // next valid location, and just + // mark it as "unverified". + // In future, we could consider supporting to update the breakpoint to a valid + // location. + + // passing the offset to the constructor, it can recognize the multiline lambda + // expression well + BreakpointLocationLocator locator = new BreakpointLocationLocator(astUnit, + sourceLine, true, true, astUnit.getPosition(sourceLine, 0), 0); + astUnit.accept(locator); + // When the final valid line location is same as the original line, that + // represents it's a valid breakpoint. + // Add location type check to avoid breakpoint on method/field which will never + // be hit in current implementation. + if (sourceLine == locator.getLineLocation() + && locator.getLocationType() == BreakpointLocationLocator.LOCATION_LINE) { + sourceLocation.setClassName(StringUtils.isBlank(unnamedClass) ? locator.getFullyQualifiedTypeName() : unnamedClass); + if (resolvedLocations.containsKey(sourceLine)) { + sourceLocation.setAvailableBreakpointLocations(resolvedLocations.get(sourceLine)); + } else { + BreakpointLocation[] inlineLocations = getInlineBreakpointLocations(astUnit, sourceLine); + sourceLocation.setAvailableBreakpointLocations(inlineLocations); + resolvedLocations.put(sourceLine, inlineLocations); + } + } else if (locator.getLocationType() == BreakpointLocationLocator.LOCATION_METHOD) { + sourceLocation.setClassName(StringUtils.isBlank(unnamedClass) ? locator.getFullyQualifiedTypeName() : unnamedClass); + sourceLocation.setMethodName(locator.getMethodName()); + sourceLocation.setMethodSignature(locator.getMethodSignature()); + } + } + } + + return sourceLocations; + } + + private String inferPrimaryTypeName(String uri, CompilationUnit astUnit) { + String fileName = ""; + String filePath = AdapterUtils.toPath(uri); + if (filePath != null && Files.isRegularFile(Paths.get(filePath))) { + fileName = Paths.get(filePath).getFileName().toString(); + } else if (astUnit.getTypeRoot() != null) { + fileName = astUnit.getTypeRoot().getElementName(); + } + + if (StringUtils.isNotBlank(fileName)) { + String[] extensions = JavaCore.getJavaLikeExtensions(); + for (String extension : extensions) { + if (fileName.endsWith("." + extension)) { + return fileName.substring(0, fileName.length() - 1 - extension.length()); + } + } + } + + return fileName; + } + + private BreakpointLocation[] getInlineBreakpointLocations(final CompilationUnit astUnit, int sourceLine) { + List locations = new ArrayList<>(); + // The starting position of each line is the default breakpoint location for + // that line. + locations.add(new BreakpointLocation(sourceLine, 0)); + astUnit.accept(new ASTVisitor() { + @Override + public boolean visit(LambdaExpression node) { + int lambdaStart = node.getStartPosition(); + int startLine = astUnit.getLineNumber(lambdaStart); + if (startLine == sourceLine) { + int startColumn = astUnit.getColumnNumber(lambdaStart); + int lambdaEnd = lambdaStart + node.getLength(); + int endLine = astUnit.getLineNumber(lambdaEnd); + int endColumn = astUnit.getColumnNumber(lambdaEnd); + BreakpointLocation location = new BreakpointLocation(startLine, startColumn, endLine, endColumn); + locations.add(location); + } + return super.visit(node); + } + }); + + return locations.toArray(BreakpointLocation[]::new); + } + + private CompilationUnit asCompilationUnit(String uri) { final ASTParser parser = ASTParser.newParser(this.latestASTLevel); parser.setResolveBindings(true); parser.setBindingsRecovery(true); parser.setStatementsRecovery(true); CompilationUnit astUnit = null; String filePath = AdapterUtils.toPath(uri); - // For file uri, read the file contents directly and pass them to the ast parser. + // For file uri, read the file contents directly and pass them to the ast + // parser. if (filePath != null && Files.isRegularFile(Paths.get(filePath))) { String source = readFile(filePath); parser.setSource(source.toCharArray()); /** * See the java doc for { @link ASTParser#setResolveBindings(boolean) }. - * Binding information is obtained from the Java model. This means that the compilation unit must be located relative to the Java model. - * This happens automatically when the source code comes from either setSource(ICompilationUnit) or setSource(IClassFile). - * When source is supplied by setSource(char[]), the location must be established explicitly - * by setting an environment using setProject(IJavaProject) or setEnvironment(String [], String [], String [], boolean) + * Binding information is obtained from the Java model. This means that the + * compilation unit must be located relative to the Java model. + * This happens automatically when the source code comes from either + * setSource(ICompilationUnit) or setSource(IClassFile). + * When source is supplied by setSource(char[]), the location must be + * established explicitly + * by setting an environment using setProject(IJavaProject) or + * setEnvironment(String [], String [], String [], boolean) * and a unit name setUnitName(String). */ - parser.setEnvironment(new String[0], new String[0], null, true); + IFile resource = (IFile) JDTUtils.findResource(JDTUtils.toURI(uri), + ResourcesPlugin.getWorkspace().getRoot()::findFilesForLocationURI); + if (resource != null && JdtUtils.isJavaProject(resource.getProject())) { + parser.setProject(JavaCore.create(resource.getProject())); + } else { + parser.setEnvironment(new String[0], new String[0], null, true); + /** + * See the java doc for { @link ASTParser#setSource(char[]) }, + * the user need specify the compiler options explicitly. + */ + Map javaOptions = JavaCore.getOptions(); + javaOptions.put(JavaCore.COMPILER_SOURCE, this.latestJavaVersion); + javaOptions.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, this.latestJavaVersion); + javaOptions.put(JavaCore.COMPILER_COMPLIANCE, this.latestJavaVersion); + javaOptions.put(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED); + parser.setCompilerOptions(javaOptions); + } parser.setUnitName(Paths.get(filePath).getFileName().toString()); - /** - * See the java doc for { @link ASTParser#setSource(char[]) }, - * the user need specify the compiler options explicitly. - */ - Map javaOptions = JavaCore.getOptions(); - javaOptions.put(JavaCore.COMPILER_SOURCE, this.latestJavaVersion); - javaOptions.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, this.latestJavaVersion); - javaOptions.put(JavaCore.COMPILER_COMPLIANCE, this.latestJavaVersion); - javaOptions.put(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED); - parser.setCompilerOptions(javaOptions); astUnit = (CompilationUnit) parser.createAST(null); } else { // For non-file uri (e.g. jdt://contents/rt.jar/java.io/PrintStream.class), // leverage jdt to load the source contents. - ITypeRoot typeRoot = resolveClassFile(uri); - if (typeRoot != null) { - parser.setSource(typeRoot); - astUnit = (CompilationUnit) parser.createAST(null); - } - } - - String[] fqns = new String[lines.length]; - if (astUnit != null) { - for (int i = 0; i < lines.length; i++) { - // TODO - // The ValidBreakpointLocationLocator will verify if the current line is a valid location or not. - // If so, it will return the fully qualified name of the class type that contains the current line. - // Otherwise, it will try to find a valid location from the next lines and return it's fully qualified name. - // In current stage, we don't support to move the invalid breakpoint down to the next valid location, and just - // mark it as "unverified". - // In future, we could consider supporting to update the breakpoint to a valid location. - ValidBreakpointLocationLocator locator = new ValidBreakpointLocationLocator(astUnit, lines[i], true, true); - astUnit.accept(locator); - // When the final valid line location is same as the original line, that represents it's a valid breakpoint. - // Add location type check to avoid breakpoint on method/field which will never be hit in current implementation. - if (lines[i] == locator.getLineLocation() && locator.getLocationType() == ValidBreakpointLocationLocator.LOCATION_LINE) { - fqns[i] = locator.getFullyQualifiedTypeName(); + IClassFile typeRoot = resolveClassFile(uri); + try { + if (typeRoot != null && typeRoot.getSourceRange() != null) { + parser.setSource(typeRoot); + astUnit = (CompilationUnit) parser.createAST(null); + } else if (typeRoot != null && DebugSettings.getCurrent().debugSupportOnDecompiledSource == Switch.ON) { + ContentProviderManager contentProvider = JavaLanguageServerPlugin.getContentProviderManager(); + try { + String contents = contentProvider.getSource(typeRoot, new NullProgressMonitor()); + if (contents != null && !contents.isBlank()) { + IJavaProject javaProject = typeRoot.getJavaProject(); + if (javaProject != null) { + parser.setProject(javaProject); + } else { + parser.setEnvironment(new String[0], new String[0], null, true); + /** + * See the java doc for { @link ASTParser#setSource(char[]) }, + * the user need specify the compiler options explicitly. + */ + Map javaOptions = JavaCore.getOptions(); + javaOptions.put(JavaCore.COMPILER_SOURCE, this.latestJavaVersion); + javaOptions.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, this.latestJavaVersion); + javaOptions.put(JavaCore.COMPILER_COMPLIANCE, this.latestJavaVersion); + javaOptions.put(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, JavaCore.ENABLED); + parser.setCompilerOptions(javaOptions); + } + parser.setUnitName(typeRoot.getElementName()); + parser.setSource(contents.toCharArray()); + astUnit = (CompilationUnit) parser.createAST(null); + } + } catch (Exception e) { + JavaLanguageServerPlugin.logException(e.getMessage(), e); + } } + } catch (JavaModelException e) { + // ignore } } - return fqns; + return astUnit; } @Override @@ -201,7 +416,8 @@ public String getJavaRuntimeVersion(String projectName) { return resolveSystemLibraryVersion(project, vmInstall); } catch (CoreException e) { - logger.log(Level.SEVERE, "Failed to get Java runtime version for project '" + projectName + "': " + e.getMessage(), e); + logger.log(Level.SEVERE, + "Failed to get Java runtime version for project '" + projectName + "': " + e.getMessage(), e); } } @@ -210,6 +426,7 @@ public String getJavaRuntimeVersion(String projectName) { /** * Get the project associated source containers. + * * @return the initialized source container list */ public synchronized ISourceContainer[] getSourceContainers() { @@ -238,7 +455,8 @@ private String getContents(IClassFile cf) { source = buffer.getContents(); } } catch (JavaModelException e) { - logger.log(Level.SEVERE, String.format("Failed to parse the source contents of the class file: %s", e.toString()), e); + logger.log(Level.SEVERE, + String.format("Failed to parse the source contents of the class file: %s", e.toString()), e); } if (source == null) { source = ""; @@ -253,7 +471,7 @@ private static String getFileURI(IClassFile classFile) { try { return new URI(JDT_SCHEME, "contents", PATH_SEPARATOR + jarName + PATH_SEPARATOR + packageName + PATH_SEPARATOR + classFile.getElementName(), classFile.getHandleIdentifier(), null) - .toASCIIString(); + .toASCIIString(); } catch (URISyntaxException e) { return null; } @@ -290,8 +508,7 @@ private static IClassFile resolveClassFile(String uriString) { private static String readFile(String filePath) { StringBuilder builder = new StringBuilder(); - try (BufferedReader bufferReader = - new BufferedReader(new InputStreamReader(new FileInputStream(filePath)))) { + try (BufferedReader bufferReader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)))) { final int BUFFER_SIZE = 4096; char[] buffer = new char[BUFFER_SIZE]; while (true) { @@ -307,7 +524,8 @@ private static String readFile(String filePath) { return builder.toString(); } - private static String resolveSystemLibraryVersion(IJavaProject project, IVMInstall vmInstall) throws JavaModelException { + private static String resolveSystemLibraryVersion(IJavaProject project, IVMInstall vmInstall) + throws JavaModelException { LibraryLocation[] libraries = JavaRuntime.getLibraryLocations(vmInstall); if (libraries != null && libraries.length > 0) { IPackageFragmentRoot root = project.findPackageFragmentRoot(libraries[0].getSystemLibraryPath()); @@ -324,4 +542,126 @@ private static String resolveSystemLibraryVersion(IJavaProject project, IVMInsta return null; } + + public List findMethodInvocations(String uri, int line) { + if (uri == null) { + return Collections.emptyList(); + } + + boolean useCache = false; + CompilationUnit cachedUnit = CoreASTProvider.getInstance().getCachedAST(); + if (cachedUnit != null) { + ITypeRoot cachedElement = cachedUnit.getTypeRoot(); + if (cachedElement != null && isSameURI(JDTUtils.toUri(cachedElement), uri)) { + useCache = true; + } + } + + final CompilationUnit astUnit = useCache ? cachedUnit : asCompilationUnit(uri); + if (astUnit == null) { + return Collections.emptyList(); + } + + MethodInvocationLocator locator = new MethodInvocationLocator(line, astUnit); + astUnit.accept(locator); + + return locator.getTargets().entrySet().stream().map(entry -> { + MethodInvocation invocation = new MethodInvocation(); + ASTNode astNode = entry.getKey(); + invocation.expression = astNode.toString(); + IMethodBinding binding = entry.getValue().getMethodDeclaration(); + invocation.methodName = binding.isConstructor() ? "" : binding.getName(); + if (binding.getDeclaringClass().isAnonymous()) { + ITypeBinding superclass = binding.getDeclaringClass().getSuperclass(); + if (superclass != null + && !superclass.isEqualTo(astUnit.getAST().resolveWellKnownType("java.lang.Object"))) { + invocation.declaringTypeName = superclass.getBinaryName(); + } else { + return null; + } + } else { + // Keep consistent with JDI since JDI uses binary class name + invocation.declaringTypeName = binding.getDeclaringClass().getBinaryName(); + } + invocation.methodGenericSignature = BindingUtils.toSignature(binding); + invocation.methodSignature = Signature.getTypeErasure(invocation.methodGenericSignature); + int startOffset = astNode.getStartPosition(); + if (astNode instanceof org.eclipse.jdt.core.dom.MethodInvocation) { + // The range covered by the stepIn target should start with the method name. + startOffset = ((org.eclipse.jdt.core.dom.MethodInvocation) astNode).getName().getStartPosition(); + } + invocation.lineStart = astUnit.getLineNumber(startOffset); + invocation.columnStart = astUnit.getColumnNumber(startOffset); + int endOffset = astNode.getStartPosition() + astNode.getLength(); + invocation.lineEnd = astUnit.getLineNumber(endOffset); + invocation.columnEnd = astUnit.getColumnNumber(endOffset); + return invocation; + }).filter(Objects::nonNull).collect(Collectors.toList()); + } + + private boolean isSameURI(String uri1, String uri2) { + if (Objects.equals(uri1, uri2)) { + return true; + } + + try { + return URIUtil.sameURI(new URI(uri1), new URI(uri2)); + } catch (URISyntaxException e) { + return false; + } + } + + public int[] getOriginalLineMappings(String uri) { + IClassFile classFile = resolveClassFile(uri); + try { + if (classFile == null) { + return null; + } + + IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) classFile.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); + if (packageRoot != null && packageRoot.getSourceAttachmentPath() != null) { + return null; + } + + if (classFile.getSourceRange() == null) { + ContentProviderManager contentProvider = JavaLanguageServerPlugin.getContentProviderManager(); + try { + DecompilerResult result = contentProvider.getSourceResult(classFile, new NullProgressMonitor()); + if (result != null) { + return result.getOriginalLineMappings(); + } + } catch (NoSuchMethodError e) { + // ignore it if old language server version is installed. + } catch (Exception e) { + JavaLanguageServerPlugin.logException(e.getMessage(), e); + } + } + } catch (JavaModelException e) { + // ignore + } + return null; + } + + public int[] getDecompiledLineMappings(String uri) { + IClassFile classFile = resolveClassFile(uri); + try { + if (classFile != null && classFile.getSourceRange() == null) { + ContentProviderManager contentProvider = JavaLanguageServerPlugin.getContentProviderManager(); + try { + DecompilerResult result = contentProvider.getSourceResult(classFile, new NullProgressMonitor()); + if (result != null) { + return result.getDecompiledLineMappings(); + } + } catch (NoSuchMethodError e) { + // ignore it if old Java language server version is installed. + } catch (Exception e) { + JavaLanguageServerPlugin.logException(e.getMessage(), e); + } + } + } catch (JavaModelException e) { + // ignore + } + + return null; + } } diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtUtils.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtUtils.java index 9a918112b..66562ae74 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtUtils.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtUtils.java @@ -40,6 +40,8 @@ import org.eclipse.jdt.launching.JavaRuntime; import org.eclipse.jdt.launching.sourcelookup.containers.JavaProjectSourceContainer; import org.eclipse.jdt.launching.sourcelookup.containers.PackageFragmentRootSourceContainer; +import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; import com.microsoft.java.debug.core.DebugException; import com.microsoft.java.debug.core.StackFrameUtility; @@ -213,10 +215,10 @@ public static ISourceContainer[] getSourceContainers(String projectName) { projects.stream().distinct().map(project -> JdtUtils.getJavaProject(project)) .filter(javaProject -> javaProject != null && javaProject.exists()) .forEach(javaProject -> { - // Add source containers associated with the project's runtime classpath entries. - containers.addAll(Arrays.asList(getSourceContainers(javaProject, calculated))); // Add source containers associated with the project's source folders. containers.add(new JavaProjectSourceContainer(javaProject)); + // Add source containers associated with the project's runtime classpath entries. + containers.addAll(Arrays.asList(getSourceContainers(javaProject, calculated))); }); return containers.toArray(new ISourceContainer[0]); @@ -415,4 +417,36 @@ public static boolean isSameFile(IResource resource1, IResource resource2) { return Objects.equals(resource1.getLocation(), resource2.getLocation()); } + + /** + * Check if the project is managed by Gradle Build Server. + */ + public static boolean isBspProject(IProject project) { + return project != null && ProjectUtils.isJavaProject(project) + && ProjectUtils.hasNature(project, "com.microsoft.gradle.bs.importer.GradleBuildServerProjectNature"); + } + + /** + * Get main project according to the main project name or main class name, + * or return null if the main project cannot be resolved. + */ + public static IProject getMainProject(String mainProjectName, String mainClassName) { + IProject mainProject = null; + if (StringUtils.isNotBlank(mainProjectName)) { + mainProject = ProjectUtils.getProject(mainProjectName); + } + + if (mainProject == null && StringUtils.isNotBlank(mainClassName)) { + try { + List javaProjects = ResolveClasspathsHandler.getJavaProjectFromType(mainClassName); + if (javaProjects.size() == 1) { + mainProject = javaProjects.get(0).getProject(); + } + } catch (CoreException e) { + JavaLanguageServerPlugin.logException("Failed to resolve project from main class name.", e); + } + } + + return mainProject; + } } diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/MethodInvocationLocator.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/MethodInvocationLocator.java new file mode 100644 index 000000000..654cbb00b --- /dev/null +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/MethodInvocationLocator.java @@ -0,0 +1,232 @@ +/******************************************************************************* +* Copyright (c) 2022 Microsoft Corporation and others. +* All rights reserved. This program and the accompanying materials +* are made available under the terms of the Eclipse Public License v1.0 +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v10.html +* +* Contributors: +* Gayan Perera (gayanper@gmail.com) - initial API and implementation +*******************************************************************************/ +package com.microsoft.java.debug.plugin.internal; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration; +import org.eclipse.jdt.core.dom.AnonymousClassDeclaration; +import org.eclipse.jdt.core.dom.AssertStatement; +import org.eclipse.jdt.core.dom.BreakStatement; +import org.eclipse.jdt.core.dom.ClassInstanceCreation; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ConstructorInvocation; +import org.eclipse.jdt.core.dom.ContinueStatement; +import org.eclipse.jdt.core.dom.DoStatement; +import org.eclipse.jdt.core.dom.EmptyStatement; +import org.eclipse.jdt.core.dom.EnhancedForStatement; +import org.eclipse.jdt.core.dom.EnumDeclaration; +import org.eclipse.jdt.core.dom.ExpressionStatement; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.ForStatement; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.IfStatement; +import org.eclipse.jdt.core.dom.LabeledStatement; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.ReturnStatement; +import org.eclipse.jdt.core.dom.SuperConstructorInvocation; +import org.eclipse.jdt.core.dom.SwitchStatement; +import org.eclipse.jdt.core.dom.SynchronizedStatement; +import org.eclipse.jdt.core.dom.ThrowStatement; +import org.eclipse.jdt.core.dom.TryStatement; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.TypeDeclarationStatement; +import org.eclipse.jdt.core.dom.VariableDeclarationStatement; +import org.eclipse.jdt.core.dom.WhileStatement; +import org.eclipse.jdt.core.dom.YieldStatement; + +public class MethodInvocationLocator extends ASTVisitor { + private int line; + private CompilationUnit unit; + private Map targets; + + public MethodInvocationLocator(int line, CompilationUnit unit) { + super(false); + this.line = line; + this.unit = unit; + this.targets = new HashMap<>(); + } + + @Override + public boolean visit(FieldDeclaration node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(MethodDeclaration node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(TypeDeclaration node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(AnonymousClassDeclaration node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(EnumDeclaration node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(AnnotationTypeDeclaration node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(VariableDeclarationStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(ExpressionStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(AssertStatement node) { + return shouldVisitNode(node); + + } + + @Override + public boolean visit(BreakStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(ContinueStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(DoStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(EmptyStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(EnhancedForStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(ForStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(IfStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(LabeledStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(ReturnStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(SwitchStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(SynchronizedStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(ThrowStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(TryStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(TypeDeclarationStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(WhileStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(YieldStatement node) { + return shouldVisitNode(node); + } + + @Override + public boolean visit(ConstructorInvocation node) { + if (shouldVisitNode(node)) { + targets.put(node, node.resolveConstructorBinding()); + return true; + } + return false; + } + + @Override + public boolean visit(SuperConstructorInvocation node) { + if (shouldVisitNode(node)) { + targets.put(node, node.resolveConstructorBinding()); + return true; + } + return false; + } + + @Override + public boolean visit(MethodInvocation node) { + targets.put(node, node.resolveMethodBinding()); + return true; + } + + @Override + public boolean visit(ClassInstanceCreation node) { + targets.put(node, node.resolveConstructorBinding()); + return true; + } + + private boolean shouldVisitNode(ASTNode node) { + int start = unit.getLineNumber(node.getStartPosition()); + int end = unit.getLineNumber(node.getStartPosition() + node.getLength() - 1); + + if (line >= start && line <= end) { + return true; + } + + return false; + } + + public Map getTargets() { + return targets; + } +} diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainClassHandler.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainClassHandler.java index f70d0c044..fc8189445 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainClassHandler.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainClassHandler.java @@ -35,6 +35,7 @@ import org.eclipse.core.runtime.IPath; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.search.IJavaSearchConstants; import org.eclipse.jdt.core.search.IJavaSearchScope; @@ -43,6 +44,7 @@ import org.eclipse.jdt.core.search.SearchParticipant; import org.eclipse.jdt.core.search.SearchPattern; import org.eclipse.jdt.core.search.SearchRequestor; +import org.eclipse.jdt.internal.core.SourceMethod; import org.eclipse.jdt.ls.core.internal.ProjectUtils; import org.eclipse.jdt.ls.core.internal.ResourceUtils; import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager; @@ -99,10 +101,19 @@ private List resolveMainClassCore(List arguments) { private List resolveMainClassUnderPaths(List parentPaths) { // Limit to search main method from source code only. - IJavaSearchScope searchScope = SearchEngine.createJavaSearchScope(ProjectUtils.getJavaProjects(), - IJavaSearchScope.REFERENCED_PROJECTS | IJavaSearchScope.SOURCES); - SearchPattern pattern = SearchPattern.createPattern("main(String[]) void", IJavaSearchConstants.METHOD, - IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH); + IJavaProject[] projects; + if (parentPaths == null || parentPaths.isEmpty()) { + projects = ProjectUtils.getJavaProjects(); + } else { + projects = Stream.of(ProjectUtils.getAllProjects()) + .filter(p -> ProjectUtils.isJavaProject(p) && p.getLocation() != null && ResourceUtils.isContainedIn(p.getLocation(), parentPaths)) + .map(p -> JavaCore.create(p)) + .filter(p -> p.exists()) + .toArray(IJavaProject[]::new); + } + IJavaSearchScope searchScope = SearchEngine.createJavaSearchScope(projects, + IJavaSearchScope.SOURCES); + SearchPattern pattern = createMainMethodSearchPattern(); final List res = new ArrayList<>(); SearchRequestor requestor = new SearchRequestor() { @Override @@ -110,40 +121,36 @@ public void acceptSearchMatch(SearchMatch match) { Object element = match.getElement(); if (element instanceof IMethod) { IMethod method = (IMethod) element; - try { - if (method.isMainMethod()) { - IResource resource = method.getResource(); - if (resource != null) { - IProject project = resource.getProject(); - if (project != null) { - String mainClass = method.getDeclaringType().getFullyQualifiedName(); - IJavaProject javaProject = JdtUtils.getJavaProject(project); - if (javaProject != null) { - String moduleName = JdtUtils.getModuleName(javaProject); - if (moduleName != null) { - mainClass = moduleName + "/" + mainClass; - } + if (isMainMethod(method)) { + IResource resource = method.getResource(); + if (resource != null) { + IProject project = resource.getProject(); + if (project != null) { + String mainClass = method.getDeclaringType().getFullyQualifiedName(); + IJavaProject javaProject = JdtUtils.getJavaProject(project); + if (javaProject != null) { + String moduleName = JdtUtils.getModuleName(javaProject); + if (moduleName != null) { + mainClass = moduleName + "/" + mainClass; } - String projectName = ProjectsManager.DEFAULT_PROJECT_NAME.equals(project.getName()) ? null : project.getName(); - if (parentPaths.isEmpty() - || ResourceUtils.isContainedIn(project.getLocation(), parentPaths) - || isContainedInInvisibleProject(project, parentPaths)) { - String filePath = null; - - if (match.getResource() instanceof IFile) { - try { - filePath = match.getResource().getLocation().toOSString(); - } catch (Exception ex) { - // ignore - } + } + String projectName = ProjectsManager.DEFAULT_PROJECT_NAME.equals(project.getName()) ? null : project.getName(); + if (parentPaths.isEmpty() + || ResourceUtils.isContainedIn(project.getLocation(), parentPaths) + || isContainedInInvisibleProject(project, parentPaths)) { + String filePath = null; + + if (match.getResource() instanceof IFile) { + try { + filePath = match.getResource().getLocation().toOSString(); + } catch (Exception ex) { + // ignore } - res.add(new ResolutionItem(mainClass, projectName, filePath)); } + res.add(new ResolutionItem(mainClass, projectName, filePath)); } } } - } catch (JavaModelException e) { - // ignore } } } @@ -166,8 +173,7 @@ private List resolveMainClassUnderProject(final String projectNa IJavaProject javaProject = ProjectUtils.getJavaProject(projectName); IJavaSearchScope searchScope = SearchEngine.createJavaSearchScope(javaProject == null ? new IJavaProject[0] : new IJavaProject[] {javaProject}, IJavaSearchScope.REFERENCED_PROJECTS | IJavaSearchScope.SOURCES); - SearchPattern pattern = SearchPattern.createPattern("main(String[]) void", IJavaSearchConstants.METHOD, - IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH); + SearchPattern pattern = createMainMethodSearchPattern(); final List res = new ArrayList<>(); SearchRequestor requestor = new SearchRequestor() { @Override @@ -175,35 +181,31 @@ public void acceptSearchMatch(SearchMatch match) { Object element = match.getElement(); if (element instanceof IMethod) { IMethod method = (IMethod) element; - try { - if (method.isMainMethod()) { - IResource resource = method.getResource(); - if (resource != null) { - IProject project = resource.getProject(); - if (project != null) { - String mainClass = method.getDeclaringType().getFullyQualifiedName(); - IJavaProject javaProject = JdtUtils.getJavaProject(project); - if (javaProject != null) { - String moduleName = JdtUtils.getModuleName(javaProject); - if (moduleName != null) { - mainClass = moduleName + "/" + mainClass; - } + if (isMainMethod(method)) { + IResource resource = method.getResource(); + if (resource != null) { + IProject project = resource.getProject(); + if (project != null) { + String mainClass = method.getDeclaringType().getFullyQualifiedName(); + IJavaProject javaProject = JdtUtils.getJavaProject(project); + if (javaProject != null) { + String moduleName = JdtUtils.getModuleName(javaProject); + if (moduleName != null) { + mainClass = moduleName + "/" + mainClass; } + } - String filePath = null; - if (match.getResource() instanceof IFile) { - try { - filePath = match.getResource().getLocation().toOSString(); - } catch (Exception ex) { - // ignore - } + String filePath = null; + if (match.getResource() instanceof IFile) { + try { + filePath = match.getResource().getLocation().toOSString(); + } catch (Exception ex) { + // ignore } - res.add(new ResolutionItem(mainClass, projectName, filePath)); } + res.add(new ResolutionItem(mainClass, projectName, filePath)); } } - } catch (JavaModelException e) { - // ignore } } } @@ -221,6 +223,29 @@ public void acceptSearchMatch(SearchMatch match) { return resolutions; } + private SearchPattern createMainMethodSearchPattern() { + SearchPattern pattern1 = SearchPattern.createPattern("main(String[]) void", IJavaSearchConstants.METHOD, + IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH); + SearchPattern pattern2 = SearchPattern.createPattern("main() void", IJavaSearchConstants.METHOD, + IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH); + return SearchPattern.createOrPattern(pattern1, pattern2); + } + + private boolean isMainMethod(IMethod method) { + try { + if (method instanceof SourceMethod + && ((SourceMethod) method).isMainMethodCandidate()) { + return true; + } + + return method.isMainMethod(); + } catch (JavaModelException e) { + // do nothing + } + + return false; + } + private boolean isContainedInInvisibleProject(IProject project, Collection rootPaths) { if (project == null) { return false; diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainMethodHandler.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainMethodHandler.java index a3c61e814..be1092bb3 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainMethodHandler.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveMainMethodHandler.java @@ -15,6 +15,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; @@ -30,7 +31,11 @@ import org.eclipse.jdt.core.ISourceRange; import org.eclipse.jdt.core.ISourceReference; import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; +import org.eclipse.jdt.internal.core.SourceMethod; import org.eclipse.jdt.ls.core.internal.JDTUtils; import org.eclipse.jdt.ls.core.internal.handlers.DocumentLifeCycleHandler; import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager; @@ -104,16 +109,50 @@ private static List searchMainMethods(ICompilationUnit compilationUnit) * Returns the main method defined in the type. */ public static IMethod getMainMethod(IType type) throws JavaModelException { + boolean allowInstanceMethod = isInstanceMainMethodSupported(type); + List methods = new ArrayList<>(); for (IMethod method : type.getMethods()) { - // Have at most one main method in the member methods of the type. + if (method instanceof SourceMethod + && ((SourceMethod) method).isMainMethodCandidate()) { + methods.add(method); + } + if (method.isMainMethod()) { - return method; + methods.add(method); + } + + if (!allowInstanceMethod && !methods.isEmpty()) { + return methods.get(0); } } + if (!methods.isEmpty()) { + methods.sort((method1, method2) -> { + return getMainMethodPriority(method1) - getMainMethodPriority(method2); + }); + + return methods.get(0); + } + return null; } + private static boolean isInstanceMainMethodSupported(IType type) { + Map options = type.getJavaProject().getOptions(true); + return CompilerOptions.versionToJdkLevel(options.get(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM)) >= ClassFileConstants.JDK21; + } + + /** + * See Java 22 JEP 463 https://openjdk.org/jeps/463. + * It searches the main method in the launched class by following a specific order: + * - If the launched class contains a main method with a String[] parameter then choose that method. + * - Otherwise, if the class contains a main method with no parameters then choose that method. + */ + private static int getMainMethodPriority(IMethod method) { + String[] params = method.getParameterTypes(); + return params.length == 1 ? 1 : 2; + } + private static List getPotentialMainClassTypes(ICompilationUnit compilationUnit) throws JavaModelException { List result = new ArrayList<>(); IType[] topLevelTypes = compilationUnit.getTypes(); diff --git a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/eval/JdtEvaluationProvider.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/eval/JdtEvaluationProvider.java index e014552a8..a48cf7ead 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/eval/JdtEvaluationProvider.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/eval/JdtEvaluationProvider.java @@ -29,6 +29,7 @@ import org.apache.commons.lang3.reflect.FieldUtils; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IStatus; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; @@ -316,7 +317,7 @@ private JDIThread getMockJDIThread(ThreadReference thread) { @Override protected synchronized void invokeComplete(int restoreTimeout) { super.invokeComplete(restoreTimeout); - context.getStackFrameManager().reloadStackFrames(thread); + context.getStackFrameManager().reloadStackFrames(thread, false); } }); } @@ -328,9 +329,22 @@ private void internalEvaluate(ASTEvaluationEngine engine, ICompiledExpression co try { engine.evaluateExpression(compiledExpression, stackframe, evaluateResult -> { if (evaluateResult == null || evaluateResult.hasErrors()) { - Exception ex = evaluateResult.getException() != null ? evaluateResult.getException() - : new RuntimeException(StringUtils.join(evaluateResult.getErrorMessages())); - completableFuture.completeExceptionally(ex); + DebugException debugException = evaluateResult.getException(); + if (debugException == null) { + completableFuture.completeExceptionally( + new RuntimeException(String.join(" ", evaluateResult.getErrorMessages()))); + return; + } + IStatus status = debugException.getStatus(); + if (status.getCode() == DebugException.TARGET_REQUEST_FAILED) { + Throwable innerException = status.getException(); + if (innerException instanceof com.sun.jdi.InvocationException) { + ObjectReference objectReference = ((com.sun.jdi.InvocationException) innerException).exception(); + completableFuture.complete(objectReference); + return; + } + } + completableFuture.completeExceptionally(debugException); return; } try { diff --git a/com.microsoft.java.debug.repository/.project b/com.microsoft.java.debug.repository/.project index 887e4a7a8..255908647 100644 --- a/com.microsoft.java.debug.repository/.project +++ b/com.microsoft.java.debug.repository/.project @@ -16,12 +16,12 @@ - 1600224298119 + 1665543654735 30 org.eclipse.core.resources.regexFilterMatcher - node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ diff --git a/com.microsoft.java.debug.repository/category.xml b/com.microsoft.java.debug.repository/category.xml index ab60e9b37..f30a2f4c4 100644 --- a/com.microsoft.java.debug.repository/category.xml +++ b/com.microsoft.java.debug.repository/category.xml @@ -1,6 +1,6 @@ - + diff --git a/com.microsoft.java.debug.repository/pom.xml b/com.microsoft.java.debug.repository/pom.xml index 9e02c6804..573aff2ac 100644 --- a/com.microsoft.java.debug.repository/pom.xml +++ b/com.microsoft.java.debug.repository/pom.xml @@ -4,7 +4,7 @@ com.microsoft.java java-debug-parent - 0.36.0 + 0.53.2 com.microsoft.java.debug.repository eclipse-repository diff --git a/com.microsoft.java.debug.target/com.microsoft.java.debug.tp.target b/com.microsoft.java.debug.target/com.microsoft.java.debug.tp.target new file mode 100644 index 000000000..31462a770 --- /dev/null +++ b/com.microsoft.java.debug.target/com.microsoft.java.debug.tp.target @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/com.microsoft.java.debug.target/pom.xml b/com.microsoft.java.debug.target/pom.xml new file mode 100644 index 000000000..8e49b638b --- /dev/null +++ b/com.microsoft.java.debug.target/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + com.microsoft.java + java-debug-parent + 0.53.2 + + com.microsoft.java.debug.tp + ${base.name} :: Target Platform + eclipse-target-definition + diff --git a/java.debug.target b/java.debug.target deleted file mode 100644 index dcb0b8c5e..000000000 --- a/java.debug.target +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/javaConfig.json b/javaConfig.json index 91ccf4714..8ab611931 100644 --- a/javaConfig.json +++ b/javaConfig.json @@ -3,5 +3,5 @@ "com.microsoft.java.debug.core", "com.microsoft.java.debug.plugin" ], - "targetPlatform": "java.debug.target" + "targetPlatform": "com.microsoft.java.debug.target/com.microsoft.java.debug.tp.target" } diff --git a/mvnw b/mvnw index e96ccd5fb..e9cf8d330 100755 --- a/mvnw +++ b/mvnw @@ -19,209 +19,277 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Maven2 Start Up Batch script -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.3 # # Optional ENV vars # ----------------- -# M2_HOME - location of maven2's installed home dir -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac -fi +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false -case "`uname`" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - export JAVA_HOME="`/usr/libexec/java_home`" - else - export JAVA_HOME="/Library/Java/Home" + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 fi fi - ;; -esac + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=`java-config --jre-home` + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi -fi - -if [ -z "$M2_HOME" ] ; then - ## resolve links - $0 may be a link to maven's home - PRG="$0" +} - # need this for relative symlinks - while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG="`dirname "$PRG"`/$link" - fi +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } - saveddir=`pwd` +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac - M2_HOME=`dirname "$PRG"`/.. +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" - # make it fully qualified - M2_HOME=`cd "$M2_HOME" && pwd` +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} - cd "$saveddir" - # echo Using m2 at $M2_HOME +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --unix "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --unix "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" fi -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$M2_HOME" ] && - M2_HOME="`(cd "$M2_HOME"; pwd)`" - [ -n "$JAVA_HOME" ] && - JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" - # TODO classpath? +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -if [ -z "$JAVA_HOME" ]; then - javaExecutable="`which javac`" - if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=`which readlink` - if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then - if $darwin ; then - javaHome="`dirname \"$javaExecutable\"`" - javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" - else - javaExecutable="`readlink -f \"$javaExecutable\"`" - fi - javaHome="`dirname \"$javaExecutable\"`" - javaHome=`expr "$javaHome" : '\(.*\)/bin'` - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" fi -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi else - JAVACMD="`which java`" + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 fi fi -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi - -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi -CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" fi +fi - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=`cd "$wdir/.."; pwd` +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi fi - # end of workaround done - echo "${basedir}" -} - -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - echo "$(tr -s '\n' ' ' < "$1")" - fi -} - -BASE_DIR=`find_maven_basedir "$(pwd)"` -if [ -z "$BASE_DIR" ]; then - exit 1; + set -f fi -export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} -if [ "$MVNW_VERBOSE" = true ]; then - echo $MAVEN_PROJECTBASEDIR -fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$M2_HOME" ] && - M2_HOME=`cygpath --path --windows "$M2_HOME"` - [ -n "$JAVA_HOME" ] && - JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` - [ -n "$CLASSPATH" ] && - CLASSPATH=`cygpath --path --windows "$CLASSPATH"` - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" fi -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -exec "$JAVACMD" \ - $MAVEN_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 019bd74d7..3fd2be860 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,143 +1,189 @@ -@REM ---------------------------------------------------------------------------- -@REM Licensed to the Apache Software Foundation (ASF) under one -@REM or more contributor license agreements. See the NOTICE file -@REM distributed with this work for additional information -@REM regarding copyright ownership. The ASF licenses this file -@REM to you under the Apache License, Version 2.0 (the -@REM "License"); you may not use this file except in compliance -@REM with the License. You may obtain a copy of the License at -@REM -@REM http://www.apache.org/licenses/LICENSE-2.0 -@REM -@REM Unless required by applicable law or agreed to in writing, -@REM software distributed under the License is distributed on an -@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -@REM KIND, either express or implied. See the License for the -@REM specific language governing permissions and limitations -@REM under the License. -@REM ---------------------------------------------------------------------------- - -@REM ---------------------------------------------------------------------------- -@REM Maven2 Start Up Batch script -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir -@REM -@REM Optional ENV vars -@REM M2_HOME - location of maven2's installed home dir -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files -@REM ---------------------------------------------------------------------------- - -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" - -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%" == "on" pause - -if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% - -exit /B %ERROR_CODE% +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.3 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index b60bc4cf5..b8320ef8f 100644 --- a/pom.xml +++ b/pom.xml @@ -6,12 +6,12 @@ ${base.name} :: Parent The Java Debug Server is an implementation of Visual Studio Code (VSCode) Debug Protocol. It can be used in Visual Studio Code to debug Java programs. https://github.com/Microsoft/java-debug - 0.36.0 + 0.53.2 pom Java Debug Server for Visual Studio Code UTF-8 - 1.5.1 + 5.0.0 ${basedir} @@ -61,6 +61,7 @@ com.microsoft.java.debug.core com.microsoft.java.debug.plugin com.microsoft.java.debug.repository + com.microsoft.java.debug.target @@ -135,9 +136,27 @@ org.eclipse.tycho target-platform-configuration ${tycho-version} + + p2 + + + com.microsoft.java + com.microsoft.java.debug.tp + ${project.version} + + + + + + org.eclipse.tycho + tycho-maven-plugin + ${tycho-version} + true + + @@ -153,11 +172,6 @@ - - 202112 - p2 - https://download.eclipse.org/releases/2021-12/202112081000/ - oss.sonatype.org https://oss.sonatype.org/content/repositories/snapshots/ @@ -165,20 +179,5 @@ true - - JDT.LS - p2 - https://download.eclipse.org/jdtls/snapshots/repository/latest/ - - - JBOLL.TOOLS - p2 - https://download.jboss.org/jbosstools/updates/m2e-extensions/m2e-apt/1.5.3-2019-11-08_11-04-22-H22/ - - - orbit - p2 - https://download.eclipse.org/tools/orbit/R-builds/R20170516192513/repository/ - diff --git a/scripts/publishMaven.js b/scripts/publishMaven.js index 1f960766c..deb75fb8b 100644 --- a/scripts/publishMaven.js +++ b/scripts/publishMaven.js @@ -1,7 +1,8 @@ /** * Usage: - * node publishMaven.js -task [upload|promote] + * node publishMaven.js -task [gpg][upload|promote] * + * gpg: Sign artifacts with GPG. * upload: Upload artifacts to a nexus staging repo. * promote: Promote a repo to get it picked up by Maven Central. */ @@ -33,7 +34,9 @@ main(configs, artifactFolder); function main() { const argv = process.argv; const task = argv[argv.indexOf("-task") + 1]; - if (task === "upload") { + if (task === "gpg") { + gpgSign(configs, artifactFolder); + } else if (task === "upload") { uploadToStaging(configs, artifactFolder); } else if (task === "promote") { promoteToCentral(configs); @@ -43,6 +46,27 @@ function main() { } } +/** + * Task gpg: Sign artifacts with GPG. + * + * Required binaries: + * - gpg + * + * Required Environment Variables: + * - artifactFolder: folder containing *.jar/*.pom files. + * - GPGPASS: passphrase of GPG key. + */ +function gpgSign(configs, artifactFolder) { + const props = ["artifactFolder", "gpgpass" ]; + for (const prop of props) { + if (!configs[prop]) { + console.error(`${prop} is not set.`); + process.exit(1); + } + } + addChecksumsAndGpgSignature(configs, artifactFolder); +} + /** * Task upload: Upload artifacts to a nexus staging repo. * @@ -141,7 +165,7 @@ function addChecksumsAndGpgSignature(configs, artifactFolder) { fs.readdirSync(modulePath) .filter(name => name.endsWith(".md5") || name.endsWith(".sha1") || name.endsWith(".asc")) .forEach(name => fs.unlinkSync(path.join(modulePath, name))); - + const files = fs.readdirSync(modulePath); for (let file of files) { // calc md5. @@ -153,7 +177,7 @@ function addChecksumsAndGpgSignature(configs, artifactFolder) { const sha1 = childProcess.execSync(`sha1sum "${path.join(modulePath, file)}"`); const sha1Match = /([a-z0-9]{40})/.exec(sha1.toString()); fs.writeFileSync(path.join(modulePath, file + ".sha1"), sha1Match[0]); - + // gpg sign. childProcess.execSync(`gpg --batch --pinentry-mode loopback --passphrase "${configs.gpgpass}" -ab "${path.join(modulePath, file)}"`) }