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 new file mode 100644 index 000000000..553d95a9a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,94 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + linux: + name: Linux + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - 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 + + 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 + + - 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: $HOME/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Verify + run: ./mvnw.cmd clean verify + + - name: Checkstyle + run: ./mvnw.cmd checkstyle:check + + darwin: + name: macOS + runs-on: macos-latest + timeout-minutes: 30 + steps: + - 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/.travis.yml b/.travis.yml deleted file mode 100644 index f331372be..000000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: java - -os: - - linux - - osx - -script: - - ./mvnw clean verify - - ./mvnw checkstyle:check - -cache: - directories: - - $HOME/.m2 diff --git a/.vscode/settings.json b/.vscode/settings.json index fbaf055a0..2c67a2d4b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "java.configuration.updateBuildConfiguration": "automatic", "files.exclude": { "**/.git": true, "**/*.class": true, 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/ThirdPartyNotices.txt b/ThirdPartyNotices.txt new file mode 100644 index 000000000..b9fc26420 --- /dev/null +++ b/ThirdPartyNotices.txt @@ -0,0 +1,437 @@ +java-debug + +THIRD-PARTY SOFTWARE NOTICES AND INFORMATION +Do Not Translate or Localize + +This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise. + +1. ReactiveX/RxJava (https://github.com/ReactiveX/RxJava) +2. reactive-streams/reactive-streams-jvm (https://github.com/reactive-streams/reactive-streams-jvm) +3. apache/commons-io (https://github.com/apache/commons-io) + + +%% ReactiveX/RxJava NOTICES AND INFORMATION BEGIN HERE +========================================= + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +========================================= +END OF ReactiveX/RxJava NOTICES AND INFORMATION + +%% reactive-streams/reactive-streams-jvm NOTICES AND INFORMATION BEGIN HERE +========================================= +MIT No Attribution + +Copyright 2014 Reactive Streams + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +========================================= +END OF reactive-streams/reactive-streams-jvm NOTICES AND INFORMATION + +%% apache/commons-io NOTICES AND INFORMATION BEGIN HERE +========================================= + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +========================================= +END OF apache/commons-io NOTICES AND INFORMATION \ No newline at end of file diff --git a/com.microsoft.java.debug.core/.classpath b/com.microsoft.java.debug.core/.classpath index f0257c5a5..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 24ec1b741..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.29.0 + 0.53.2 com.microsoft.java.debug.core jar @@ -28,8 +28,8 @@ maven-compiler-plugin 3.7.0 - 1.8 - 1.8 + 11 + 11 @@ -42,27 +42,27 @@ org.apache.commons commons-lang3 - 3.6 + 3.18.0 com.google.code.gson gson - 2.7 + 2.8.9 io.reactivex.rxjava2 rxjava - 2.1.1 + 2.2.21 org.reactivestreams reactive-streams - 1.0.0 + 1.0.4 commons-io commons-io - 2.5 + 2.14.0 @@ -78,21 +78,4 @@ test - - - default-tools.jar - - (,9) - - - - com.sun - tools - 1.8 - system - ${java.home}/../lib/tools.jar - - - - 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..82859b268 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,13 +37,14 @@ 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 boolean async = false; + Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber) { this(vm, eventHub, className, lineNumber, 0, null); } @@ -48,21 +54,42 @@ public class Breakpoint implements IBreakpoint { } Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, String condition) { + this(vm, eventHub, className, lineNumber, hitCount, condition, null); + } + + Breakpoint(VirtualMachine vm, IEventHub eventHub, String className, int lineNumber, int hitCount, String condition, String logMessage) { this.vm = vm; this.eventHub = eventHub; - this.className = className; - this.lineNumber = lineNumber; + 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) { + this.vm = vm; + this.eventHub = eventHub; + this.sourceLocation = sourceLocation; + this.hitCount = hitCount; + this.condition = condition; this.logMessage = logMessage; } // IDebugResource - private List requests = new ArrayList<>(); + private List requests = Collections.synchronizedList(new ArrayList<>()); private List subscriptions = new ArrayList<>(); @Override @@ -91,14 +118,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 +143,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 +173,7 @@ public void setHitCount(int hitCount) { .filter(request -> request instanceof BreakpointRequest) .subscribe(request -> { request.addCountFilter(hitCount); + request.disable(); request.enable(); }); } @@ -148,18 +193,28 @@ public String getLogMessage() { return this.logMessage; } + @Override + public boolean async() { + return this.async; + } + + @Override + public void setAsync(boolean async) { + this.async = async; + } + @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 +226,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 +237,226 @@ 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; + } - if (!newRequests.isEmpty() && !future.isDone()) { - this.putProperty("verified", true); - future.complete(this); + requests.addAll(newRequests); + 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 (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..38a234fa9 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,18 +11,31 @@ 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<>(); public DebugSession(VirtualMachine virtualMachine) { vm = virtualMachine; @@ -30,18 +43,49 @@ public DebugSession(VirtualMachine virtualMachine) { @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 +101,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,11 +119,18 @@ 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); + } + @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); @@ -93,9 +148,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 +183,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(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 +259,28 @@ public IEventHub getEventHub() { public VirtualMachine getVM() { return vm; } + + @Override + public IMethodBreakpoint createFunctionBreakpoint(String className, String functionName, String condition, + int hitCount) { + return new MethodBreakpoint(vm, this.getEventHub(), className, functionName, condition, hitCount); + } + + 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(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..0a3e05ec8 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,12 @@ 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 static DebugSettings getCurrent() { return current; @@ -87,6 +89,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..4a2a49e9b 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 @@ -444,7 +444,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 +477,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 +495,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..723e2cadf 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; @@ -48,6 +48,12 @@ public class EvaluatableBreakpoint extends Breakpoint implements IEvaluatableBre this.eventHub = eventHub; } + EvaluatableBreakpoint(VirtualMachine vm, IEventHub eventHub, JavaBreakpointLocation sourceLocation, int hitCount, + String condition, String logMessage) { + super(vm, eventHub, sourceLocation, hitCount, condition, logMessage); + this.eventHub = eventHub; + } + @Override public boolean containsEvaluatableExpression() { return containsConditionalExpression() || containsLogpointExpression(); 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..40995e9dd 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,11 @@ public interface IBreakpoint extends IDebugResource { String getLogMessage(); void setLogMessage(String logMessage); + + 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/IDebugSession.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/IDebugSession.java index 24138fef1..6cc3f3a46 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(); 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/LaunchException.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/LaunchException.java new file mode 100644 index 000000000..6c1e1a513 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/LaunchException.java @@ -0,0 +1,51 @@ +/******************************************************************************* +* Copyright (c) 2018-2021 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 com.sun.jdi.connect.VMStartException; + +/** + * Extends {@link VMStartException} to provide more detail about the failed process + * from before it is destroyed. + */ +public class LaunchException extends VMStartException { + + boolean exited; + int exitStatus; + String stdout; + String stderr; + + public LaunchException(String message, Process process, boolean exited, int exitStatus, String stdout, String stderr) { + super(message, process); + this.exited = exited; + this.exitStatus = exitStatus; + this.stdout = stdout; + this.stderr = stderr; + } + + public boolean isExited() { + return exited; + } + + public int getExitStatus() { + return exitStatus; + } + + public String getStdout() { + return stdout; + } + + public String getStderr() { + return stderr; + } + +} 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..7a6e74c6b --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/MethodBreakpoint.java @@ -0,0 +1,294 @@ +/******************************************************************************* +* 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 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) { + 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; + } + + @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(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/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 798200e97..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; @@ -30,15 +31,21 @@ import com.microsoft.java.debug.core.adapter.handler.ExceptionInfoRequestHandler; import com.microsoft.java.debug.core.adapter.handler.HotCodeReplaceHandler; import com.microsoft.java.debug.core.adapter.handler.InitializeRequestHandler; +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; @@ -95,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()); @@ -121,21 +128,28 @@ private void initialize() { registerHandlerForDebug(new ExceptionInfoRequestHandler()); registerHandlerForDebug(new DataBreakpointInfoRequestHandler()); registerHandlerForDebug(new SetDataBreakpointsRequestHandler()); + 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); } @@ -151,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 395d39ec6..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,6 +56,13 @@ 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<>(); @@ -62,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; @@ -98,6 +109,10 @@ public void setDebuggerLinesStartAt1(boolean debuggerLinesStartAt1) { this.debuggerLinesStartAt1 = debuggerLinesStartAt1; } + public boolean isDebuggerColumnsStartAt1() { + return debuggerColumnStartAt1; + } + @Override public boolean isDebuggerPathsAreUri() { return debuggerPathsAreUri; @@ -197,7 +212,7 @@ public void setVariableFormatter(IVariableFormatter variableFormatter) { } @Override - public Map getSourceLookupCache() { + public Map getSourceLookupCache() { return sourceMappingCache; } @@ -326,4 +341,81 @@ public IBreakpointManager getBreakpointManager() { public IStepResultManager getStepResultManager() { return stepResultManager; } + + @Override + public long getProcessId() { + return this.processId; + } + + @Override + public long getShellProcessId() { + return this.shellProcessId; + } + + @Override + public void setProcessId(long processId) { + this.processId = processId; + } + + @Override + 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 4f843fd09..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); @@ -128,4 +130,32 @@ public interface IDebugAdapterContext { IBreakpointManager getBreakpointManager(); IStepResultManager getStepResultManager(); + + void setShellProcessId(long shellProcessId); + + long getShellProcessId(); + + 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/ProcessConsole.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java index 6b384c628..3d823df91 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/ProcessConsole.java @@ -129,7 +129,7 @@ public void stop() { } private void monitor(InputStream input, PublishSubject subject) { - BufferedReader reader = new BufferedReader(new InputStreamReader(input, encoding)); + BufferedReader reader = new BufferedReader(encoding == null ? new InputStreamReader(input) : new InputStreamReader(input, encoding)); final int BUFFERSIZE = 4096; char[] buffer = new char[BUFFERSIZE]; while (true) { 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/CompletionsHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/CompletionsHandler.java index 9f660644a..eba7d5153 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/CompletionsHandler.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/CompletionsHandler.java @@ -11,8 +11,8 @@ package com.microsoft.java.debug.core.adapter.handler; -import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -46,7 +46,7 @@ public CompletableFuture handle(Command command, Arguments arguments, // completions should be illegal when frameId is zero, it is sent when the program is running, while during running we cannot resolve // the completion candidates if (completionsArgs.frameId == 0) { - response.body = new ArrayList<>(); + response.body = new Responses.CompletionsResponseBody(Collections.emptyList()); 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..1c543bce4 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 @@ -76,6 +76,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,19 +105,20 @@ 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.getThreadCache().addEventThread(thread, "exception"); context.getProtocolServer().sendEvent(new Events.StoppedEvent("exception", thread.uniqueID())); debugEvent.shouldResume = false; } else { 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 2a498608e..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-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 @@ -14,7 +14,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; @@ -65,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.", @@ -94,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) { @@ -103,10 +109,8 @@ public CompletableFuture handle(Command command, Arguments arguments, indexedVariables = ((IntegerValue) sizeValue).value(); } } - } catch (CancellationException | IllegalArgumentException | InterruptedException - | ExecutionException | UnsupportedOperationException e) { - logger.log(Level.INFO, - String.format("Failed to get the logical size for the type %s.", value.type().name()), e); + } catch (Exception e) { + logger.log(Level.INFO, "Failed to get the logical size of the variable", e); } } int referenceId = 0; @@ -114,20 +118,49 @@ public CompletableFuture handle(Command command, Arguments arguments, referenceId = context.getRecyclableIdPool().addObject(threadId, varProxy); } - String valueString = variableFormatter.valueToString(value, options); + boolean hasErrors = false; + String valueString = null; + try { + valueString = variableFormatter.valueToString(value, options); + } catch (OutOfMemoryError e) { + hasErrors = true; + logger.log(Level.SEVERE, "Failed to convert the value of a large object to a string", e); + valueString = ""; + } catch (Exception e) { + hasErrors = true; + logger.log(Level.SEVERE, "Failed to resolve the variable value", e); + valueString = ""; + } + String detailsString = null; - if (sizeValue != null) { + if (hasErrors) { + // 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) { - detailsString = VariableDetailUtils.formatDetailsValue(value, stackFrameReference.getThread(), variableFormatter, options, engine); + } else if (supportsToStringView(context, evalArguments.context)) { + try { + detailsString = VariableDetailUtils.formatDetailsValue(value, stackFrameReference.getThread(), variableFormatter, options, engine); + } catch (OutOfMemoryError e) { + logger.log(Level.SEVERE, "Failed to compute the toString() value of a large object", e); + detailsString = ""; + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to compute the toString() value", e); + detailsString = ""; + } } if ("clipboard".equals(evalArguments.context) && detailsString != null) { response.body = new Responses.EvaluateResponseBody(detailsString, -1, "String", 0); } else { + String typeString = ""; + try { + typeString = variableFormatter.typeToString(value == null ? null : value.type(), options); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to resolve the variable type", e); + typeString = ""; + } response.body = new Responses.EvaluateResponseBody((detailsString == null) ? valueString : valueString + " " + detailsString, - referenceId, variableFormatter.typeToString(value == null ? null : value.type(), options), - Math.max(indexedVariables, 0)); + referenceId, typeString, Math.max(indexedVariables, 0)); } return response; } @@ -151,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 new file mode 100644 index 000000000..21f77ee5a --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/InlineValuesRequestHandler.java @@ -0,0 +1,257 @@ +/******************************************************************************* +* Copyright (c) 2021 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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.microsoft.java.debug.core.Configuration; +import com.microsoft.java.debug.core.DebugSettings; +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.adapter.IStackFrameManager; +import com.microsoft.java.debug.core.adapter.variables.IVariableFormatter; +import com.microsoft.java.debug.core.adapter.variables.JavaLogicalStructure; +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.Variable; +import com.microsoft.java.debug.core.adapter.variables.VariableDetailUtils; +import com.microsoft.java.debug.core.protocol.Responses; +import com.microsoft.java.debug.core.protocol.Types; +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.InlineVariable; +import com.microsoft.java.debug.core.protocol.Requests.InlineValuesArguments; +import com.sun.jdi.ArrayReference; +import com.sun.jdi.Field; +import com.sun.jdi.IntegerValue; +import com.sun.jdi.Method; +import com.sun.jdi.ObjectReference; +import com.sun.jdi.ReferenceType; +import com.sun.jdi.StackFrame; +import com.sun.jdi.Value; + +import org.apache.commons.lang3.math.NumberUtils; + +public class InlineValuesRequestHandler implements IDebugRequestHandler { + protected static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); + + @Override + public List getTargetCommands() { + return Arrays.asList(Command.INLINEVALUES); + } + + /** + * This request only resolves the values for those non-local variables, such as + * field variables and captured variables from outer scope. Because the values + * of local variables in current stackframe are usually expanded by Variables View + * by default, inline values can reuse these values directly. However, for field + * variables and variables captured from external scopes, they are hidden as properties + * of 'this' variable and require additional evaluation to get their values. + */ + @Override + public CompletableFuture handle(Command command, Arguments arguments, Response response, + IDebugAdapterContext context) { + InlineValuesArguments inlineValuesArgs = (InlineValuesArguments) arguments; + 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) { + logger.log(Level.SEVERE, String.format("InlineValues failed: invalid stackframe id %d.", inlineValuesArgs.frameId)); + response.body = new Responses.InlineValuesResponse(null); + 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) { + logger.log(Level.SEVERE, String.format("InlineValues failed: stale stackframe id %d.", inlineValuesArgs.frameId)); + response.body = new Responses.InlineValuesResponse(null); + return CompletableFuture.completedFuture(response); + } + + Variable[] values = new Variable[variableCount]; + try { + if (isLambdaFrame(frame)) { + // Lambda expression stores the captured variables from 'outer' scope in a synthetic stackframe below the lambda frame. + StackFrame syntheticLambdaFrame = stackFrameReference.getThread().frame(stackFrameReference.getDepth() + 1); + resolveValuesFromThisVariable(syntheticLambdaFrame.thisObject(), inlineVariables, values, true); + } + + resolveValuesFromThisVariable(frame.thisObject(), inlineVariables, values, false); + } catch (Exception ex) { + // do nothig + } + + Types.Variable[] result = new Types.Variable[variableCount]; + IVariableFormatter variableFormatter = context.getVariableFormatter(); + Map formatterOptions = variableFormatter.getDefaultOptions(); + Map calculatedValues = new HashMap<>(); + IEvaluationProvider evaluationEngine = context.getProvider(IEvaluationProvider.class); + for (int i = 0; i < variableCount; i++) { + if (values[i] == null) { + continue; + } + + if (calculatedValues.containsKey(inlineVariables[i])) { + result[i] = calculatedValues.get(inlineVariables[i]); + continue; + } + + Value value = values[i].value; + String name = values[i].name; + int indexedVariables = -1; + Value sizeValue = null; + if (value instanceof ArrayReference) { + indexedVariables = ((ArrayReference) value).length(); + } else if (value instanceof ObjectReference && DebugSettings.getCurrent().showLogicalStructure && evaluationEngine != null) { + try { + JavaLogicalStructure structure = JavaLogicalStructureManager.getLogicalStructure((ObjectReference) value); + if (structure != null && structure.getSizeExpression() != null) { + sizeValue = structure.getSize((ObjectReference) value, frame.thread(), evaluationEngine); + if (sizeValue != null && sizeValue instanceof IntegerValue) { + indexedVariables = ((IntegerValue) sizeValue).value(); + } + } + } catch (CancellationException | IllegalArgumentException | InterruptedException | ExecutionException | UnsupportedOperationException e) { + logger.log(Level.INFO, + String.format("Failed to get the logical size for the type %s.", value.type().name()), e); + } + } + + Types.Variable formattedVariable = new Types.Variable(name, variableFormatter.valueToString(value, formatterOptions)); + formattedVariable.indexedVariables = Math.max(indexedVariables, 0); + String detailsValue = null; + if (sizeValue != null) { + detailsValue = "size=" + variableFormatter.valueToString(sizeValue, formatterOptions); + } else if (DebugSettings.getCurrent().showToString) { + detailsValue = VariableDetailUtils.formatDetailsValue(value, frame.thread(), variableFormatter, formatterOptions, evaluationEngine); + } + + if (detailsValue != null) { + formattedVariable.value = formattedVariable.value + " " + detailsValue; + } + + result[i] = formattedVariable; + calculatedValues.put(inlineVariables[i], formattedVariable); + } + + response.body = new Responses.InlineValuesResponse(result); + return CompletableFuture.completedFuture(response); + } + + private static boolean isCapturedLocalVariable(String fieldName, String variableName) { + String capturedVariableName = "val$" + variableName; + return Objects.equals(fieldName, capturedVariableName) + || (fieldName.startsWith(capturedVariableName + "$") && NumberUtils.isDigits(fieldName.substring(capturedVariableName.length() + 1))); + } + + private static boolean isCapturedThisVariable(String fieldName) { + if (fieldName.startsWith("this$")) { + String suffix = fieldName.substring(5).replaceAll("\\$+$", ""); + return NumberUtils.isDigits(suffix); + } + + return false; + } + + private static boolean isLambdaFrame(StackFrame frame) { + Method method = frame.location().method(); + return method.isSynthetic() && method.name().startsWith("lambda$"); + } + + private void resolveValuesFromThisVariable(ObjectReference thisObj, InlineVariable[] unresolvedVariables, Variable[] result, + boolean isSyntheticLambdaFrame) { + if (thisObj == null) { + return; + } + + int unresolved = 0; + for (Variable item : result) { + if (item == null) { + unresolved++; + } + } + + try { + ReferenceType type = thisObj.referenceType(); + String typeName = type.name(); + ObjectReference enclosingInstance = null; + for (Field field : type.allFields()) { + String fieldName = field.name(); + boolean isSyntheticField = field.isSynthetic(); + Value fieldValue = null; + for (int i = 0; i < unresolvedVariables.length; i++) { + if (result[i] != null) { + continue; + } + + InlineVariable inlineVariable = unresolvedVariables[i]; + boolean isInlineFieldVariable = (inlineVariable.declaringClass != null); + boolean isMatch = false; + if (isSyntheticLambdaFrame) { + isMatch = !isInlineFieldVariable && Objects.equals(fieldName, inlineVariable.expression); + } else { + boolean isMatchedField = isInlineFieldVariable + && Objects.equals(fieldName, inlineVariable.expression) + && Objects.equals(typeName, inlineVariable.declaringClass); + boolean isMatchedCapturedVariable = !isInlineFieldVariable + && isSyntheticField + && isCapturedLocalVariable(fieldName, inlineVariable.expression); + isMatch = isMatchedField || isMatchedCapturedVariable; + + if (!isMatch && isSyntheticField && enclosingInstance == null && isCapturedThisVariable(fieldName)) { + Value value = thisObj.getValue(field); + if (value instanceof ObjectReference) { + enclosingInstance = (ObjectReference) value; + break; + } + } + } + + if (isMatch) { + fieldValue = fieldValue == null ? thisObj.getValue(field) : fieldValue; + result[i] = new Variable(inlineVariable.expression, fieldValue); + unresolved--; + } + } + + if (unresolved <= 0) { + break; + } + } + + if (unresolved > 0 && enclosingInstance != null) { + resolveValuesFromThisVariable(enclosingInstance, unresolvedVariables, result, isSyntheticLambdaFrame); + } + } catch (Exception ex) { + // do nothing + } + } +} 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 7d76e1ffd..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,7 +16,7 @@ import java.net.MalformedURLException; import java.net.URISyntaxException; import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; +import java.nio.charset.CharsetEncoder; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -42,6 +42,8 @@ import com.microsoft.java.debug.core.DebugSettings; 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); @@ -90,23 +104,21 @@ protected CompletableFuture handleLaunchCommand(Arguments arguments, R "Failed to launch debuggee VM. Missing mainClass or modulePaths/classPaths options in launch configuration.", ErrorCode.ARGUMENT_MISSING); } - if (StringUtils.isBlank(launchArguments.encoding)) { - context.setDebuggeeEncoding(StandardCharsets.UTF_8); - } else { + if (StringUtils.isNotBlank(launchArguments.encoding)) { if (!Charset.isSupported(launchArguments.encoding)) { throw AdapterUtils.createCompletionException( "Failed to launch debuggee VM. 'encoding' options in the launch configuration is not recognized.", ErrorCode.INVALID_ENCODING); } context.setDebuggeeEncoding(Charset.forName(launchArguments.encoding)); + if (StringUtils.isBlank(launchArguments.vmArgs)) { + launchArguments.vmArgs = String.format("-Dfile.encoding=%s", context.getDebuggeeEncoding().name()); + } else { + // if vmArgs already has the file.encoding settings, duplicate options for jvm will not cause an error, the right most value wins + launchArguments.vmArgs = String.format("%s -Dfile.encoding=%s", launchArguments.vmArgs, context.getDebuggeeEncoding().name()); + } } - if (StringUtils.isBlank(launchArguments.vmArgs)) { - launchArguments.vmArgs = String.format("-Dfile.encoding=%s", context.getDebuggeeEncoding().name()); - } else { - // if vmArgs already has the file.encoding settings, duplicate options for jvm will not cause an error, the right most value wins - launchArguments.vmArgs = String.format("%s -Dfile.encoding=%s", launchArguments.vmArgs, context.getDebuggeeEncoding().name()); - } context.setLaunchMode(launchArguments.noDebug ? LaunchMode.NO_DEBUG : LaunchMode.DEBUG); activeLaunchHandler.preLaunch(launchArguments, context); @@ -129,17 +141,67 @@ 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); } } return launch(launchArguments, response, context).thenCompose(res -> { + long processId = context.getProcessId(); + long shellProcessId = context.getShellProcessId(); + if (context.getDebuggeeProcess() != null) { + processId = context.getDebuggeeProcess().pid(); + } + + // If processId or shellProcessId exist, send a notification to client. + if (processId > 0 || shellProcessId > 0) { + context.getProtocolServer().sendEvent(new Events.ProcessIdNotification(processId, shellProcessId)); + } + LaunchUtils.releaseTempLaunchFile(context.getClasspathJar()); LaunchUtils.releaseTempLaunchFile(context.getArgsfile()); if (res.success) { @@ -248,6 +310,22 @@ protected CompletableFuture launch(LaunchArguments launchArguments, Re .subscribe((event) -> context.getProtocolServer().sendEvent(event)); debuggeeConsole.start(); resultFuture.complete(response); + } catch (LaunchException e) { + if (StringUtils.isNotBlank(e.getStdout())) { + OutputEvent event = convertToOutputEvent(e.getStdout(), Category.stdout, context); + context.getProtocolServer().sendEvent(event); + } + if (StringUtils.isNotBlank(e.getStderr())) { + OutputEvent event = convertToOutputEvent(e.getStderr(), Category.stderr, context); + context.getProtocolServer().sendEvent(event); + } + + resultFuture.completeExceptionally( + new DebugException( + String.format("Failed to launch debuggee VM. Reason: %s", e.getMessage()), + ErrorCode.LAUNCH_FAILURE.getId() + ) + ); } catch (IOException | IllegalConnectorArgumentsException | VMStartException e) { resultFuture.completeExceptionally( new DebugException( 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 be7f95126..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,12 +11,14 @@ 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.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.security.MessageDigest; @@ -24,19 +26,56 @@ 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 sun.security.action.GetPropertyAction; +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. @@ -56,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); @@ -73,21 +112,25 @@ 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 = "-classpath \"" + String.join(File.pathSeparator, classPaths) + "\""; + argfile += " -cp \"" + String.join(File.pathSeparator, classPaths) + "\""; } if (ArrayUtils.isNotEmpty(modulePaths)) { - argfile = " --module-path \"" + String.join(File.pathSeparator, modulePaths) + "\""; + argfile += " --module-path \"" + String.join(File.pathSeparator, modulePaths) + "\""; } 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; @@ -105,20 +148,188 @@ 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 { if (tmpdir == null) { + Path tmpfile = Files.createTempFile("", UUID.randomUUID().toString()); + tmpdir = tmpfile.getParent(); try { - tmpdir = Paths.get(java.security.AccessController.doPrivileged(new GetPropertyAction("java.io.tmpdir"))); - } catch (NullPointerException | InvalidPathException e) { - Path tmpfile = Files.createTempFile("", ".tmp"); - tmpdir = tmpfile.getParent(); - try { - Files.deleteIfExists(tmpfile); - } catch (Exception ex) { - // do nothing - } + Files.deleteIfExists(tmpfile); + } catch (Exception ex) { + // do nothing } } @@ -153,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 138455025..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 @@ -24,6 +24,7 @@ import org.apache.commons.lang3.SystemUtils; import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugException; import com.microsoft.java.debug.core.DebugSession; @@ -45,6 +46,7 @@ import com.microsoft.java.debug.core.protocol.Requests.Command; import com.microsoft.java.debug.core.protocol.Requests.LaunchArguments; import com.microsoft.java.debug.core.protocol.Requests.RunInTerminalRequestArguments; +import com.microsoft.java.debug.core.protocol.Responses.RunInTerminalResponseBody; import com.sun.jdi.VirtualMachine; import com.sun.jdi.connect.Connector; import com.sun.jdi.connect.IllegalConnectorArgumentsException; @@ -56,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(); @@ -75,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) { @@ -82,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)); @@ -102,10 +105,24 @@ public CompletableFuture launchInTerminal(LaunchArguments launchArgume if (runResponse != null) { if (runResponse.success) { try { + try { + RunInTerminalResponseBody terminalResponse = JsonUtils.fromJson( + JsonUtils.toJson(runResponse.body), RunInTerminalResponseBody.class); + context.setProcessId(terminalResponse.processId); + context.setShellProcessId(terminalResponse.shellProcessId); + } catch (JsonSyntaxException e) { + logger.severe("Failed to resolve runInTerminal response: " + e.toString()); + } VirtualMachine vm = listenConnector.accept(args); 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 c993dc5f1..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 @@ -21,6 +21,7 @@ import java.util.logging.Logger; import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import com.microsoft.java.debug.core.Configuration; import com.microsoft.java.debug.core.DebugException; import com.microsoft.java.debug.core.adapter.ErrorCode; @@ -33,12 +34,12 @@ import com.microsoft.java.debug.core.protocol.Requests.Command; import com.microsoft.java.debug.core.protocol.Requests.LaunchArguments; import com.microsoft.java.debug.core.protocol.Requests.RunInTerminalRequestArguments; +import com.microsoft.java.debug.core.protocol.Responses.RunInTerminalResponseBody; import com.sun.jdi.connect.IllegalConnectorArgumentsException; import com.sun.jdi.connect.VMStartException; 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; @@ -90,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)); @@ -112,9 +115,32 @@ public CompletableFuture launchInTerminal(LaunchArguments launchArgume context.getProtocolServer().sendRequest(request, RUNINTERMINAL_TIMEOUT).whenComplete((runResponse, ex) -> { if (runResponse != null) { if (runResponse.success) { - // Without knowing the pid, debugger has lost control of the process. - // So simply send `terminated` event to end the session. - context.getProtocolServer().sendEvent(new Events.TerminatedEvent()); + 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()); + } + + 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/ProcessIdHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ProcessIdHandler.java new file mode 100644 index 000000000..d3eb5ad13 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/ProcessIdHandler.java @@ -0,0 +1,43 @@ +/******************************************************************************* +* 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 com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +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.Command; +import com.microsoft.java.debug.core.protocol.Responses.ProcessIdResponseBody; + +public class ProcessIdHandler implements IDebugRequestHandler { + @Override + public List getTargetCommands() { + return Arrays.asList(Command.PROCESSID); + } + + @Override + public CompletableFuture handle(Command command, Arguments arguments, Response response, + IDebugAdapterContext context) { + long processId = context.getProcessId(); + long shellProcessId = context.getShellProcessId(); + if (context.getDebuggeeProcess() != null) { + processId = context.getDebuggeeProcess().pid(); + } + + response.body = new ProcessIdResponseBody(processId, shellProcessId); + return CompletableFuture.completedFuture(response); + } +} 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/RefreshVariablesHandler.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RefreshVariablesHandler.java new file mode 100644 index 000000000..01214b615 --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/RefreshVariablesHandler.java @@ -0,0 +1,50 @@ +/******************************************************************************* +* Copyright (c) 2021 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 com.microsoft.java.debug.core.DebugSettings; +import com.microsoft.java.debug.core.adapter.IDebugAdapterContext; +import com.microsoft.java.debug.core.adapter.IDebugRequestHandler; +import com.microsoft.java.debug.core.protocol.Events.InvalidatedAreas; +import com.microsoft.java.debug.core.protocol.Events.InvalidatedEvent; +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.RefreshVariablesArguments; + +public class RefreshVariablesHandler implements IDebugRequestHandler { + + @Override + public List getTargetCommands() { + return Arrays.asList(Command.REFRESHVARIABLES); + } + + @Override + public CompletableFuture handle(Command command, Arguments arguments, Response response, + IDebugAdapterContext context) { + RefreshVariablesArguments refreshArgs = (RefreshVariablesArguments) arguments; + if (refreshArgs != null) { + DebugSettings.getCurrent().showHex = refreshArgs.showHex; + DebugSettings.getCurrent().showQualifiedNames = refreshArgs.showQualifiedNames; + DebugSettings.getCurrent().showStaticVariables = refreshArgs.showStaticVariables; + DebugSettings.getCurrent().showLogicalStructure = refreshArgs.showLogicalStructure; + DebugSettings.getCurrent().showToString = refreshArgs.showToString; + } + + context.getProtocolServer().sendEvent(new InvalidatedEvent(InvalidatedAreas.VARIABLES)); + return CompletableFuture.completedFuture(response); + } +} 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..164909656 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,6 +30,7 @@ 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.StepRequest; @@ -59,7 +60,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 +81,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 +107,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) { @@ -108,6 +122,7 @@ private void stepInto(IDebugAdapterContext context, ThreadReference thread) { // 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())); + 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 fe246a61e..09dafd1b0 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; @@ -45,12 +49,14 @@ import com.sun.jdi.Field; import com.sun.jdi.ObjectReference; 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 { @@ -91,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( @@ -128,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) { @@ -160,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 -> { @@ -187,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(() -> { @@ -198,12 +211,16 @@ 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); + context.getProtocolServer().sendEvent(new Events.StoppedEvent( + breakpointName, bpThread.uniqueID())); } }); }); } else { - context.getProtocolServer().sendEvent(new Events.StoppedEvent("breakpoint", bpThread.uniqueID())); + context.getThreadCache().addEventThread(bpThread, breakpointName); + context.getProtocolServer().sendEvent(new Events.StoppedEvent( + breakpointName, bpThread.uniqueID())); } debugEvent.shouldResume = false; } @@ -211,6 +228,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. */ @@ -222,6 +250,12 @@ public static boolean handleEvaluationResult(IDebugAdapterContext context, Threa context.getProtocolServer().sendEvent(new Events.UserNotificationEvent( Events.UserNotificationEvent.NotificationType.ERROR, String.format("[Logpoint] Log message '%s' error: %s", breakpoint.getLogMessage(), ex.getMessage()))); + } else if (value != null) { + if (value instanceof StringReference) { + String message = ((StringReference) value).value(); + context.getProtocolServer().sendEvent(Events.OutputEvent.createConsoleOutput( + message + System.lineSeparator())); + } } return true; } else { @@ -264,28 +298,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..373b1c31b 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 @@ -151,11 +151,13 @@ private void registerWatchpointHandler(IDebugAdapterContext context) { if (resume) { debugEvent.eventSet.resume(); } else { + context.getThreadCache().addEventThread(bpThread, "data breakpoint"); context.getProtocolServer().sendEvent(new Events.StoppedEvent("data breakpoint", bpThread.uniqueID())); } }); }); } else { + context.getThreadCache().addEventThread(bpThread, "data breakpoint"); context.getProtocolServer().sendEvent(new Events.StoppedEvent("data breakpoint", bpThread.uniqueID())); } 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..96a0e395b --- /dev/null +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/handler/SetFunctionBreakpointsRequestHandler.java @@ -0,0 +1,192 @@ +/******************************************************************************* +* 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; + +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"); + context.getProtocolServer().sendEvent(new Events.StoppedEvent( + "function breakpoint", bpThread.uniqueID())); + } + }); + }); + + } else { + context.getThreadCache().addEventThread(bpThread, "function breakpoint"); + context.getProtocolServer() + .sendEvent(new Events.StoppedEvent("function breakpoint", bpThread.uniqueID())); + } + + 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 94e0d3078..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(); } - } catch (Exception e) { - // ignore + 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 + } + } + 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); } } @@ -145,12 +224,20 @@ public CompletableFuture handle(Command command, Arguments arguments, private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, IDebugAdapterContext context, ThreadState threadState) { Event event = debugEvent.event; + EventRequestManager eventRequestManager = debugSession.getVM().eventRequestManager(); // 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.deleteStepRequests(debugSession.getVM().eventRequestManager()); + threadState.deleteStepRequest(eventRequestManager); + threadState.deleteMethodExitRequest(eventRequestManager); context.getStepResultManager().removeMethodResult(threadId); if (threadState.eventSubscription != null) { threadState.eventSubscription.dispose(); @@ -158,20 +245,57 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, } } else if (event instanceof StepEvent) { ThreadReference thread = ((StepEvent) event).thread(); - threadState.deleteStepRequests(debugSession.getVM().eventRequestManager()); - if (isStepFiltersConfigured(context.getStepFilters())) { + long threadId = thread.uniqueID(); + threadState.deleteStepRequest(eventRequestManager); + 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; @@ -181,9 +305,11 @@ private void handleDebugEvent(DebugEvent debugEvent, IDebugSession debugSession, // ignore. } } + threadState.deleteMethodExitRequest(eventRequestManager); 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) { @@ -202,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; @@ -232,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; @@ -245,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)) { @@ -253,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. * @@ -277,13 +475,18 @@ 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 deleteStepRequests(EventRequestManager manager) { - DebugUtility.deleteEventRequestSafely(manager, this.pendingStepRequest); + public void deleteMethodExitRequest(EventRequestManager manager) { DebugUtility.deleteEventRequestSafely(manager, this.pendingMethodExitRequest); this.pendingMethodExitRequest = null; + } + + public void deleteStepRequest(EventRequestManager manager) { + DebugUtility.deleteEventRequestSafely(manager, this.pendingStepRequest); this.pendingStepRequest = null; } } 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..6573f11da 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,75 @@ 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); + } /** * 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 +176,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 +237,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 +246,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 70d5dae31..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-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,11 +21,12 @@ import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; +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; @@ -41,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; @@ -49,6 +51,7 @@ 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.VariablesArguments; +import com.microsoft.java.debug.core.protocol.Types.VariablePresentationHint; import com.microsoft.java.debug.core.protocol.Responses; import com.microsoft.java.debug.core.protocol.Types; import com.sun.jdi.AbsentInformationException; @@ -57,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,6 +116,15 @@ public CompletableFuture handle(Command command, Arguments arguments, } VariableProxy containerNode = (VariableProxy) container; + + if (supportsToStringView(context) && containerNode.isLazyVariable()) { + Types.Variable typedVariable = this.resolveLazyVariable(context, containerNode, variableFormatter, options, evaluationEngine); + if (typedVariable != null) { + list.add(typedVariable); + response.body = new Responses.VariablesResponseBody(list); + return CompletableFuture.completedFuture(response); + } + } List childrenList = new ArrayList<>(); IStackFrameManager stackFrameManager = context.getStackFrameManager(); String containerEvaluateName = containerNode.getEvaluateName(); @@ -115,25 +144,35 @@ 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) { - JavaLogicalStructure logicalStructure = JavaLogicalStructureManager.getLogicalStructure(containerObj); + if (supportsLogicStructureView(context) && evaluationEngine != null) { + JavaLogicalStructure logicalStructure = null; + try { + logicalStructure = JavaLogicalStructureManager.getLogicalStructure(containerObj); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to get the logical structure for the variable, fall back to the Object view.", e); + } if (isUnboundedTypeContainer && logicalStructure != null && containerEvaluateName != null) { containerEvaluateName = "((" + logicalStructure.getFullyQualifiedName() + ")" + containerEvaluateName + ")"; isUnboundedTypeContainer = false; @@ -162,11 +201,8 @@ public CompletableFuture handle(Command command, Arguments arguments, childrenList.add(variable); } } - } catch (IllegalArgumentException | CancellationException | InterruptedException | ExecutionException e) { - logger.log(Level.WARNING, - String.format("Failed to get the logical structure for the type %s, fall back to the Object view.", - containerObj.type().name()), - e); + } catch (Exception e) { + logger.log(Level.WARNING, "Failed to get the logical structure for the variable, fall back to the Object view.", e); } logicalStructure = null; @@ -177,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) { @@ -190,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 @@ -222,6 +270,7 @@ public CompletableFuture handle(Command command, Arguments arguments, } }); } + for (Variable javaVariable : childrenList) { Value value = javaVariable.value; String name = javaVariable.name; @@ -232,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) { @@ -241,9 +290,8 @@ public CompletableFuture handle(Command command, Arguments arguments, indexedVariables = ((IntegerValue) sizeValue).value(); } } - } catch (CancellationException | IllegalArgumentException | InterruptedException | ExecutionException | UnsupportedOperationException e) { - logger.log(Level.INFO, - String.format("Failed to get the logical size for the type %s.", value.type().name()), e); + } catch (Exception e) { + logger.log(Level.INFO, "Failed to get the logical size of the variable", e); } } @@ -267,23 +315,65 @@ public CompletableFuture handle(Command command, Arguments arguments, } } - int referenceId = 0; + VariableProxy varProxy = null; if (indexedVariables > 0 || (indexedVariables < 0 && value instanceof ObjectReference)) { - VariableProxy varProxy = new VariableProxy(containerNode.getThread(), containerNode.getScope(), value, containerNode, evaluateName); - referenceId = context.getRecyclableIdPool().addObject(containerNode.getThreadId(), varProxy); + varProxy = new VariableProxy(containerNode.getThread(), containerNode.getScope(), value, containerNode, evaluateName); varProxy.setIndexedVariable(indexedVariables >= 0); varProxy.setUnboundedType(javaVariable.isUnboundedType()); } - Types.Variable typedVariables = new Types.Variable(name, variableFormatter.valueToString(value, options), - variableFormatter.typeToString(value == null ? null : value.type(), options), - referenceId, evaluateName); - typedVariables.indexedVariables = Math.max(indexedVariables, 0); + boolean hasErrors = false; + String valueString = null; + try { + valueString = variableFormatter.valueToString(value, options); + } catch (OutOfMemoryError e) { + hasErrors = true; + logger.log(Level.SEVERE, "Failed to convert the value of a large object to a string", e); + valueString = ""; + } catch (Exception e) { + hasErrors = true; + logger.log(Level.SEVERE, "Failed to resolve the variable value", e); + valueString = ""; + } + + String typeString = ""; + try { + typeString = variableFormatter.typeToString(value == null ? null : value.type(), options); + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to resolve the variable type", e); + typeString = ""; + } + String detailsValue = null; - if (sizeValue != null) { + if (hasErrors) { + // 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) { - detailsValue = VariableDetailUtils.formatDetailsValue(value, containerNode.getThread(), variableFormatter, options, evaluationEngine); + } else if (supportsToStringView(context)) { + if (VariableDetailUtils.isLazyLoadingSupported(value) && varProxy != null) { + varProxy.setLazyVariable(true); + } else { + try { + detailsValue = VariableDetailUtils.formatDetailsValue(value, containerNode.getThread(), variableFormatter, options, evaluationEngine); + } catch (OutOfMemoryError e) { + logger.log(Level.SEVERE, "Failed to compute the toString() value of a large object", e); + detailsValue = ""; + } catch (Exception e) { + logger.log(Level.SEVERE, "Failed to compute the toString() value", e); + detailsValue = ""; + } + } + } + + int referenceId = 0; + if (varProxy != null) { + referenceId = context.getRecyclableIdPool().addObject(containerNode.getThreadId(), varProxy); + } + + Types.Variable typedVariables = new Types.Variable(name, valueString, typeString, referenceId, evaluateName); + typedVariables.indexedVariables = Math.max(indexedVariables, 0); + if (varProxy != null && varProxy.isLazyVariable()) { + typedVariables.presentationHint = new VariablePresentationHint(true); } if (detailsValue != null) { @@ -301,6 +391,39 @@ 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(), + containerNode.getProxiedVariable(), null /** container */, containerNode.getEvaluateName()); + valueReferenceProxy.setIndexedVariable(containerNode.isIndexedVariable()); + valueReferenceProxy.setUnboundedType(containerNode.isUnboundedType()); + int referenceId = context.getRecyclableIdPool().addObject(containerNode.getThreadId(), valueReferenceProxy); + // this proxiedVariable is intermediate object, see https://github.com/microsoft/vscode/issues/135147#issuecomment-1076240074 + Object proxiedVariable = containerNode.getProxiedVariable(); + if (proxiedVariable instanceof ObjectReference) { + ObjectReference variable = (ObjectReference) proxiedVariable; + String valueString = variableFormatter.valueToString(variable, options); + String detailString = VariableDetailUtils.formatDetailsValue(variable, containerNode.getThread(), variableFormatter, options, + evaluationEngine); + return new Types.Variable("", valueString + " " + detailString, "", referenceId, containerNode.getEvaluateName()); + } + return null; + } + private Set getDuplicateNames(Collection list) { Set result = new HashSet<>(); Set set = new HashSet<>(); @@ -314,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/VariableDetailUtils.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableDetailUtils.java index e82ab79f9..c6d0d38e0 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableDetailUtils.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableDetailUtils.java @@ -154,4 +154,18 @@ private static boolean isClassType(Value value, String typeName) { return Objects.equals(((ObjectReference) value).type().name(), typeName); } + + public static boolean isLazyLoadingSupported(Value value) { + if (isClassType(value, STRING_TYPE)) { + return false; + } + if (!(value instanceof ObjectReference)) { + return false; + } + String inheritedType = findInheritedType(value, COLLECTION_TYPES); + if (inheritedType == null && !containsToStringMethod((ObjectReference) value)) { + return false; + } + return true; + } } diff --git a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableProxy.java b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableProxy.java index 9f208464c..5ebf93735 100644 --- a/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableProxy.java +++ b/com.microsoft.java.debug.core/src/main/java/com/microsoft/java/debug/core/adapter/variables/VariableProxy.java @@ -24,6 +24,7 @@ public class VariableProxy { private final String evaluateName; private boolean isIndexedVariable; private boolean isUnboundedType = false; + private boolean isLazyVariable = false; /** * Create a variable reference. @@ -75,7 +76,8 @@ public boolean equals(Object obj) { } VariableProxy other = (VariableProxy) obj; return Objects.equals(scopeName, other.scopeName) && Objects.equals(getThreadId(), other.getThreadId()) - && Objects.equals(variable, other.variable) && Objects.equals(evaluateName, other.evaluateName); + && Objects.equals(variable, other.variable) && Objects.equals(evaluateName, other.evaluateName) + && Objects.equals(isLazyVariable, other.isLazyVariable); } public long getThreadId() { @@ -109,4 +111,13 @@ public boolean isUnboundedType() { public void setUnboundedType(boolean isUnboundedType) { this.isUnboundedType = isUnboundedType; } + + public boolean isLazyVariable() { + return isLazyVariable; + } + + public void setLazyVariable(boolean isLazyVariable) { + this.isLazyVariable = isLazyVariable; + } + } 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 ea18f183c..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 @@ -11,6 +11,7 @@ package com.microsoft.java.debug.core.protocol; +import com.google.gson.annotations.SerializedName; import com.microsoft.java.debug.core.protocol.Types.Source; /** @@ -37,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. @@ -244,4 +250,88 @@ public UserNotificationEvent(NotificationType notifyType, String message) { this.message = 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, + @SerializedName("stacks") + STACKS, + @SerializedName("threads") + THREADS, + @SerializedName("variables") + VARIABLES; + } + + public static class InvalidatedEvent extends DebugEvent { + public InvalidatedAreas[] areas; + public long threadId; + public int frameId; + + public InvalidatedEvent() { + super("invalidated"); + } + + public InvalidatedEvent(InvalidatedAreas area) { + super("invalidated"); + this.areas = new InvalidatedAreas[]{area}; + } + + public InvalidatedEvent(InvalidatedAreas area, long threadId) { + super("invalidated"); + this.areas = new InvalidatedAreas[]{area}; + this.threadId = threadId; + } + + public InvalidatedEvent(InvalidatedAreas area, int frameId) { + super("invalidated"); + this.areas = new InvalidatedAreas[]{area}; + this.frameId = frameId; + } + } + + public static class ProcessIdNotification extends DebugEvent { + /** + * The process ID. + */ + public long processId = -1; + /** + * The process ID of the terminal shell if the process is running in a terminal shell. + */ + public long shellProcessId = -1; + + public ProcessIdNotification(long processId) { + super("processid"); + this.processId = processId; + } + + public ProcessIdNotification(long processId, long shellProcessId) { + super("processid"); + this.processId = processId; + this.shellProcessId = shellProcessId; + } + } } 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 f62a241a1..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 @@ -13,9 +13,11 @@ import java.util.Arrays; import java.util.Map; +import java.util.Objects; 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. @@ -65,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. @@ -269,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; } @@ -296,6 +311,14 @@ public static class SetVariableArguments extends Arguments { public ValueFormat format; } + public static class RefreshVariablesArguments extends Arguments { + public boolean showStaticVariables = false; + public boolean showQualifiedNames = false; + public boolean showHex = false; + public boolean showLogicalStructure = true; + public boolean showToString = true; + } + public static class SourceArguments extends Arguments { public int sourceReference; } @@ -340,6 +363,78 @@ public static class SetDataBreakpointsArguments extends Arguments { public DataBreakpoint[] breakpoints; } + public static class InlineValuesArguments extends Arguments { + public int frameId; + public InlineVariable[] variables; + } + + public static class InlineVariable { + public String expression; + public String declaringClass; + + @Override + public int hashCode() { + return Objects.hash(declaringClass, expression); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof InlineVariable)) { + return false; + } + InlineVariable other = (InlineVariable) obj; + return Objects.equals(declaringClass, other.declaringClass) && Objects.equals(expression, other.expression); + } + } + + /** + * 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), @@ -350,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), @@ -372,6 +469,11 @@ public static enum Command { CONTINUEOTHERS("continueOthers", ThreadOperationArguments.class), PAUSEALL("pauseAll", ThreadOperationArguments.class), PAUSEOTHERS("pauseOthers", ThreadOperationArguments.class), + 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 1b4bc7b4a..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,12 @@ 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; /** * The response content types defined by VSCode Debug Protocol. @@ -38,12 +41,35 @@ public InitializeResponseBody(Types.Capabilities capabilities) { } } - public static class RunInTerminalResponseBody extends ResponseBody { - public int processId; + public static class ProcessIdResponseBody extends ResponseBody { + /** + * The process ID. + */ + public long processId = -1; + /** + * The process ID of the terminal shell if the process is running in a terminal shell. + */ + public long shellProcessId = -1; - public RunInTerminalResponseBody(int processId) { + public ProcessIdResponseBody(long processId) { this.processId = processId; } + + public ProcessIdResponseBody(long processId, long shellProcessId) { + this.processId = processId; + this.shellProcessId = shellProcessId; + } + } + + public static class RunInTerminalResponseBody extends ProcessIdResponseBody { + + public RunInTerminalResponseBody(long processId) { + super(processId); + } + + public RunInTerminalResponseBody(long processId, long shellProcessId) { + super(processId, shellProcessId); + } } public static class ErrorResponseBody extends ResponseBody { @@ -258,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; @@ -316,4 +357,20 @@ public RedefineClassesResponse(String[] changedClasses, String errorMessage) { this.errorMessage = errorMessage; } } + + public static class InlineValuesResponse extends ResponseBody { + public Types.Variable[] variables; + + 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 3ce41869c..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; } } @@ -90,6 +96,7 @@ public static class Variable { public int namedVariables; public int indexedVariables; public String evaluateName; + public VariablePresentationHint presentationHint; /** * Constructor. @@ -101,6 +108,14 @@ public Variable(String name, String val, String type, int rf, String evaluateNam this.variablesReference = rf; this.evaluateName = evaluateName; } + + /** + * Constructor. + */ + public Variable(String name, String value) { + this.name = name; + this.value = value; + } } public static class Thread { @@ -185,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; } /** @@ -202,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 { @@ -274,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; @@ -335,6 +406,14 @@ public static class ExceptionDetails { public ExceptionDetails[] innerException; } + public static class VariablePresentationHint { + public boolean lazy; + + public VariablePresentationHint(boolean lazy) { + this.lazy = lazy; + } + } + public static class Capabilities { public boolean supportsConfigurationDoneRequest; public boolean supportsHitConditionalBreakpoints; @@ -351,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 3291a02bb..b2be945c8 100644 --- a/com.microsoft.java.debug.plugin/.classpath +++ b/com.microsoft.java.debug.plugin/.classpath @@ -1,11 +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 905946192..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.29.0 -Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-Version: 0.53.2 +Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy Bundle-Activator: com.microsoft.java.debug.plugin.internal.JavaDebuggerServerPlugin Bundle-Vendor: Microsoft @@ -14,14 +14,15 @@ Require-Bundle: org.eclipse.core.runtime, org.eclipse.debug.core, org.eclipse.jdt.debug, org.eclipse.jdt.core, + org.eclipse.jdt.core.manipulation, org.eclipse.jdt.ls.core, org.eclipse.jdt.launching, com.google.gson, org.apache.commons.lang3, org.eclipse.lsp4j, com.google.guava -Bundle-ClassPath: lib/commons-io-2.5.jar, +Bundle-ClassPath: lib/commons-io-2.19.0.jar, ., - lib/rxjava-2.1.1.jar, - lib/reactive-streams-1.0.0.jar, - lib/com.microsoft.java.debug.core-0.29.0.jar + lib/rxjava-2.2.21.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/plugin.xml b/com.microsoft.java.debug.plugin/plugin.xml index 361cdcbda..69d76830d 100644 --- a/com.microsoft.java.debug.plugin/plugin.xml +++ b/com.microsoft.java.debug.plugin/plugin.xml @@ -20,6 +20,7 @@ + diff --git a/com.microsoft.java.debug.plugin/pom.xml b/com.microsoft.java.debug.plugin/pom.xml index 28901c18d..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.29.0 + 0.53.2 com.microsoft.java.debug.plugin eclipse-plugin @@ -17,6 +17,17 @@ tycho-maven-plugin ${tycho-version} true + + + org.eclipse.tycho + tycho-compiler-plugin + ${tycho-version} + + + --limit-modules + java.base,java.logging,java.xml,jdk.jdi,java.compiler + + org.apache.maven.plugins @@ -30,22 +41,22 @@ io.reactivex.rxjava2 rxjava - 2.1.1 + 2.2.21 org.reactivestreams reactive-streams - 1.0.0 + 1.0.4 commons-io commons-io - 2.5 + 2.19.0 com.microsoft.java com.microsoft.java.debug.core - 0.29.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 d025c9c1d..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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017 Microsoft Corporation and others. + * Copyright (c) 2017-2021 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,16 +13,22 @@ import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import org.apache.commons.io.IOUtils; import org.eclipse.jdi.internal.VirtualMachineImpl; import org.eclipse.jdi.internal.VirtualMachineManagerImpl; import org.eclipse.jdi.internal.connect.SocketLaunchingConnectorImpl; import org.eclipse.jdi.internal.connect.SocketListeningConnectorImpl; import com.microsoft.java.debug.core.DebugUtility; +import com.microsoft.java.debug.core.LaunchException; import com.sun.jdi.VirtualMachine; import com.sun.jdi.connect.Connector; import com.sun.jdi.connect.IllegalConnectorArgumentsException; @@ -44,11 +50,11 @@ public AdvancedLaunchingConnector(VirtualMachineManagerImpl virtualMachineManage public Map defaultArguments() { Map defaultArgs = super.defaultArguments(); - Argument cwdArg = new AdvancedStringArgumentImpl(DebugUtility.CWD, "Current working directory", DebugUtility.CWD, false); + Argument cwdArg = new JDIStringArgumentImpl(DebugUtility.CWD, "Current working directory", DebugUtility.CWD, false); cwdArg.setValue(null); defaultArgs.put(DebugUtility.CWD, cwdArg); - Argument envArg = new AdvancedStringArgumentImpl(DebugUtility.ENV, "Environment variables", DebugUtility.ENV, false); + Argument envArg = new JDIStringArgumentImpl(DebugUtility.ENV, "Environment variables", DebugUtility.ENV, false); envArg.setValue(null); defaultArgs.put(DebugUtility.ENV, envArg); @@ -83,18 +89,100 @@ public VirtualMachine launch(Map connectionArgs) String address = listenConnector.startListening(args); String[] cmds = constructLaunchCommand(connectionArgs, address); - Process process = Runtime.getRuntime().exec(cmds, envVars, workingDir); - VirtualMachineImpl vm; + /* Launch the Java process */ + final Process process = Runtime.getRuntime().exec(cmds, envVars, workingDir); + + /* A Future that will be completed if we successfully connect to the launched process, or + will fail with an Exception if we do not. + */ + final CompletableFuture result = new CompletableFuture<>(); + + /* Listen for the debug connection from the Java process */ + CompletableFuture.runAsync(() -> { + try { + VirtualMachineImpl vm = (VirtualMachineImpl) listenConnector.accept(args); + vm.setLaunchedProcess(process); + result.complete(vm); + } catch (IllegalConnectorArgumentsException e) { + result.completeExceptionally(e); + } catch (IOException e) { + if (result.isDone()) { + /* The result Future has already been completed by the Process onExit hook */ + return; + } + + final String stdout = streamToString(process.getInputStream()); + final String stderr = streamToString(process.getErrorStream()); + + process.destroy(); + + result.completeExceptionally(new LaunchException( + String.format("VM did not connect within given time: %d ms", ACCEPT_TIMEOUT), + process, + false, + -1, + stdout, + stderr + )); + } catch (RuntimeException e) { + result.completeExceptionally(e); + } + }); + + /* Wait for the Java process to exit; if it exits before the debug connection is made, report it as an error. */ + process.onExit().thenAcceptAsync(theProcess -> { + if (result.isDone()) { + /* The result Future has already been completed by successfully connecting to the debug connection */ + return; + } + + final int exitStatus = theProcess.exitValue(); + final String stdout = streamToString(process.getInputStream()); + final String stderr = streamToString(process.getErrorStream()); + + result.completeExceptionally(new LaunchException( + String.format("VM exited with status %d", exitStatus), + theProcess, + true, + exitStatus, + stdout, + stderr + )); + + /* Stop the debug connection attempt */ + try { + listenConnector.stopListening(args); + } catch (IOException e) { + /* Ignore */ + } + }); + try { - vm = (VirtualMachineImpl) listenConnector.accept(args); - } catch (IOException | IllegalConnectorArgumentsException e) { - process.destroy(); - throw new VMStartException(String.format("VM did not connect within given time: %d ms", ACCEPT_TIMEOUT), process); + return result.get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else if (e.getCause() instanceof IllegalConnectorArgumentsException) { + throw (IllegalConnectorArgumentsException) e.getCause(); + } else if (e.getCause() instanceof VMStartException) { + throw (VMStartException) e.getCause(); + } else if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new IllegalStateException("Unexpected exception thrown when launching VM", e.getCause()); + } + } catch (InterruptedException e) { + throw new VMStartException("VM start interrupted", process); } + } - vm.setLaunchedProcess(process); - return vm; + private String streamToString(final InputStream inputStream) { + try { + return IOUtils.toString(inputStream, StandardCharsets.UTF_8); + } catch (IOException ioe) { + return null; + } } private static String[] constructLaunchCommand(Map launchingOptions, String address) { @@ -107,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); @@ -117,11 +204,79 @@ private static String[] constructLaunchCommand(Map l return DebugUtility.parseArguments(execString.toString()).toArray(new String[0]); } - class AdvancedStringArgumentImpl extends StringArgumentImpl implements StringArgument { - private static final long serialVersionUID = 1L; + abstract class JDIArgumentImpl implements Argument { + private static final long serialVersionUID = 8850533280769854833L; + private String name; + private String description; + private String label; + private boolean mustSpecify; + + protected JDIArgumentImpl(String name, String description, String label, boolean mustSpecify) { + this.name = name; + this.description = description; + this.label = label; + this.mustSpecify = mustSpecify; + } + + @Override + public String name() { + return name; + } + + @Override + public String description() { + return description; + } - protected AdvancedStringArgumentImpl(String name, String description, String label, boolean mustSpecify) { + @Override + public String label() { + return label; + } + + @Override + public boolean mustSpecify() { + return mustSpecify; + } + + @Override + public abstract String value(); + + @Override + public abstract void setValue(String value); + + @Override + public abstract boolean isValid(String value); + + @Override + public abstract String toString(); + } + + class JDIStringArgumentImpl extends JDIArgumentImpl implements StringArgument { + private static final long serialVersionUID = 6009335074727417445L; + private String value; + + protected JDIStringArgumentImpl(String name, String description, String label, boolean mustSpecify) { super(name, description, label, mustSpecify); } + + @Override + public String value() { + return value; + } + + @Override + public void setValue(String value) { + this.value = value; + } + + @Override + public boolean isValid(String value) { + return true; + } + + @Override + public String toString() { + return value; + } } } 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 new file mode 100644 index 000000000..15aeeb2b4 --- /dev/null +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/InlineValueHandler.java @@ -0,0 +1,644 @@ +/******************************************************************************* +* Copyright (c) 2021 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.plugin.internal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.Flags; +import org.eclipse.jdt.core.IBuffer; +import org.eclipse.jdt.core.IClassFile; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.ISourceRange; +import org.eclipse.jdt.core.ISourceReference; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeRoot; +import org.eclipse.jdt.core.JavaModelException; +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.Block; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.DoStatement; +import org.eclipse.jdt.core.dom.ForStatement; +import org.eclipse.jdt.core.dom.IBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.SwitchStatement; +import org.eclipse.jdt.core.dom.TypeDeclarationStatement; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.dom.WhileStatement; +import org.eclipse.jdt.core.dom.IVariableBinding; +import org.eclipse.jdt.core.dom.IfStatement; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.LambdaExpression; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.ModuleDeclaration; +import org.eclipse.jdt.core.dom.PackageDeclaration; +import org.eclipse.jdt.core.dom.QualifiedName; +import org.eclipse.jdt.core.dom.QualifiedType; +import org.eclipse.jdt.core.manipulation.CoreASTProvider; +import org.eclipse.jdt.ls.core.internal.JDTUtils; +import org.eclipse.jdt.ls.core.internal.handlers.JsonRpcHelpers; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +public class InlineValueHandler { + + /** + * Find the valid inline variables belonging to the visible view port. + */ + public static InlineVariable[] resolveInlineVariables(InlineParams params, IProgressMonitor monitor) { + ITypeRoot root = JDTUtils.resolveTypeRoot(params.uri); + try { + if (root == null || root.getBuffer() == null) { + return new InlineVariable[0]; + } + + Position stoppedLocation = params.stoppedLocation.getStart(); + int stoppedOffset = JsonRpcHelpers.toOffset(root.getBuffer(), stoppedLocation.getLine(), stoppedLocation.getCharacter()); + IMethod enclosingMethod = findEnclosingMethod(root, stoppedOffset); + if (enclosingMethod == null) { + return new InlineVariable[0]; + } + + Position startLocation = getPosition(root.getBuffer(), enclosingMethod.getSourceRange().getOffset()); + Range stoppedRange = new Range(startLocation, stoppedLocation); + if (params.viewPort != null + && (params.viewPort.getEnd().getLine() < startLocation.getLine() || params.viewPort.getStart().getLine() > stoppedLocation.getLine())) { + return new InlineVariable[0]; + } + + CompilationUnit astRoot = CoreASTProvider.getInstance().getAST(root, CoreASTProvider.WAIT_YES, monitor); + VariableVisitor visitor = new VariableVisitor(astRoot, stoppedRange, params.viewPort, Flags.isStatic(enclosingMethod.getFlags())); + astRoot.accept(visitor); + InlineVariable[] result = visitor.getInlineVariables(); + return result; + } catch (JavaModelException e) { + return new InlineVariable[0]; + } + } + + private static IMethod findEnclosingMethod(ITypeRoot root, int stoppedOffset) throws JavaModelException { + IType enclosingType = null; + if (root instanceof ICompilationUnit) { + IType[] types = ((ICompilationUnit) root).getAllTypes(); + for (IType type : types) { + if (isEnclosed(type, stoppedOffset)) { + enclosingType = type; + } + } + } else if (root instanceof IClassFile) { + enclosingType = ((IClassFile) root).getType(); + } + + if (enclosingType == null) { + return null; + } + + IMethod enclosingMethod = null; + for (IMethod method : enclosingType.getMethods()) { + if (isEnclosed(method, stoppedOffset)) { + enclosingMethod = method; + break; + } + } + + if (enclosingMethod == null) { + return null; + } + + // Deal with the scenario that the stopped location is inside the local types defined in method. + return findMethodInLocalTypes(enclosingMethod, stoppedOffset); + } + + private static boolean isEnclosed(ISourceReference sourceReference, int offset) throws JavaModelException { + ISourceRange sourceRange = sourceReference.getSourceRange(); + return sourceRange != null && offset >= sourceRange.getOffset() + && offset < sourceRange.getOffset() + sourceRange.getLength(); + } + + private static IMethod findMethodInLocalTypes(IMethod enclosingMethod, int stoppedOffset) throws JavaModelException { + if (enclosingMethod == null) { + return null; + } + + for (IJavaElement element : enclosingMethod.getChildren()) { + if (element instanceof IType) { + if (isEnclosed((IType) element, stoppedOffset)) { + for (IMethod method : ((IType) element).getMethods()) { + if (isEnclosed(method, stoppedOffset)) { + IMethod nearerMethod = findMethodInLocalTypes(method, stoppedOffset); + return nearerMethod == null ? enclosingMethod : nearerMethod; + } + } + + break; + } + } + } + + return enclosingMethod; + } + + /** + * Returns the zero based line and column number. + */ + private static Position getPosition(IBuffer buffer, int offset) { + int[] result = JsonRpcHelpers.toLine(buffer, offset); + if (result == null || result.length < 1) { + return new Position(-1, -1); + } + + return new Position(result[0], result[1]); + } + + static class VariableVisitor extends ASTVisitor { + private CompilationUnit unit = null; + private Range stoppedSourceRange; + private Range viewPort; + private boolean isStoppingAtStaticMethod; + private int baseLine; + private Set[] tokens; + private List localVarDecls = new ArrayList<>(); + private List localVarDeclPositions = new ArrayList<>(); + private boolean isStoppingAtLambda = false; + private Set varDeclsAtLastLine = new HashSet<>(); + private Range visibleInlineRange = null; + + public VariableVisitor(CompilationUnit unit, Range stoppedSourceRange, Range viewPort, boolean stopAtStaticMethod) { + this.unit = unit; + this.stoppedSourceRange = stoppedSourceRange; + this.viewPort = viewPort; + this.isStoppingAtStaticMethod = stopAtStaticMethod; + this.baseLine = stoppedSourceRange.getStart().getLine(); + this.tokens = new Set[stoppedSourceRange.getEnd().getLine() - stoppedSourceRange.getStart().getLine() + 1]; + updateVisibleRange(); + } + + private void updateVisibleRange() { + if (viewPort == null) { + visibleInlineRange = stoppedSourceRange; + } else if (compare(viewPort.getStart(), stoppedSourceRange.getEnd()) > 0 + || compare(viewPort.getEnd(), stoppedSourceRange.getStart()) < 0) { + visibleInlineRange = null; + } else { + Position start = compare(viewPort.getStart(), stoppedSourceRange.getStart()) >= 0 ? viewPort.getStart() : stoppedSourceRange.getStart(); + Position end = compare(viewPort.getEnd(), stoppedSourceRange.getEnd()) <= 0 ? viewPort.getEnd() : stoppedSourceRange.getEnd(); + visibleInlineRange = new Range(start, end); + } + } + + /** + * Handle the variables in the visible source ranges. + */ + @Override + public boolean visit(SimpleName node) { + if (visibleInlineRange == null) { + return false; + } + + Position startPosition = getStartPosition(node); + boolean isAtLastLine = isAtStopLocation(startPosition); + if (isEnclosed(visibleInlineRange, startPosition) || isAtLastLine) { + IBinding binding = node.resolveBinding(); + if (!(binding instanceof IVariableBinding)) { + return false; + } else if (isAtLastLine && this.varDeclsAtLastLine.contains(binding.getKey())) { + return false; + } + + String declaringClass = null; + if (((IVariableBinding) binding).isField()) { + ITypeBinding typeBinding = ((IVariableBinding) binding).getDeclaringClass(); + if (typeBinding == null) { + return false; + } + + declaringClass = typeBinding.getBinaryName(); + } + + Token token = new Token(node.getIdentifier(), startPosition, declaringClass); + int index = startPosition.getLine() - baseLine; + if (tokens[index] == null) { + tokens[index] = new LinkedHashSet<>(); + } + + if (!tokens[index].contains(token)) { + tokens[index].add(token); + } + } + + return false; + } + + /** + * Handle local variable declarations happening in current method. + */ + @Override + public boolean visit(VariableDeclarationFragment node) { + SimpleName name = node.getName(); + Position startPosition = getStartPosition(name); + if (isEnclosed(stoppedSourceRange, startPosition)) { + this.localVarDecls.add(name.getIdentifier()); + this.localVarDeclPositions.add(startPosition); + } + + if (isAtStopLocation(startPosition)) { + IVariableBinding binding = node.resolveBinding(); + if (binding != null) { + this.varDeclsAtLastLine.add(binding.getKey()); + } + } + + return true; + } + + /** + * Handle formal parameter declarations happening in current method. + */ + @Override + public boolean visit(SingleVariableDeclaration node) { + SimpleName name = node.getName(); + Position startPosition = getStartPosition(name); + if (isEnclosed(stoppedSourceRange, startPosition)) { + this.localVarDecls.add(name.getIdentifier()); + this.localVarDeclPositions.add(startPosition); + } + + return false; + } + + /** + * Handle the lambda expression containing the stopped location. + * If the execution instruction stops on a lambda expression, then + * crop the visible source ranges to the lambda expression body. + */ + @Override + public boolean visit(LambdaExpression node) { + Position startPosition = getStartPosition(node); + Position endPosition = getEndPosition(node); + if (compare(startPosition, stoppedSourceRange.getStart()) >= 0 + && isEnclosed(new Range(startPosition, endPosition), stoppedSourceRange.getEnd())) { + stoppedSourceRange.setStart(startPosition); + updateVisibleRange(); + isStoppingAtLambda = true; + localVarDecls.clear(); + localVarDeclPositions.clear(); + return true; + } + + return super.visit(node); + } + + /** + * Handle the method containing the stopped location. + */ + @Override + public boolean visit(MethodDeclaration node) { + Position startPosition = getStartPosition(node); + Position endPosition = getEndPosition(node); + if (compare(startPosition, stoppedSourceRange.getStart()) <= 0 && compare(endPosition, stoppedSourceRange.getEnd()) >= 0) { + return true; + } + + return false; + } + + @Override + public boolean visit(Block node) { + if (isUnreachableNode(node)) { + return false; + } + + return true; + } + + @Override + public boolean visit(DoStatement node) { + if (isUnreachableNode(node) && !isAtStopLocation(node)) { + return false; + } + + return super.visit(node); + } + + @Override + public boolean visit(ForStatement node) { + if (isUnreachableNode(node) && !isAtStopLocation(node)) { + return false; + } + + return super.visit(node); + } + + @Override + public boolean visit(IfStatement node) { + if (isUnreachableNode(node) && !isAtStopLocation(node)) { + return false; + } + + return super.visit(node); + } + + @Override + public boolean visit(SwitchStatement node) { + if (isUnreachableNode(node) && !isAtStopLocation(node)) { + return false; + } + + return super.visit(node); + } + + @Override + public boolean visit(WhileStatement node) { + if (isUnreachableNode(node) && !isAtStopLocation(node)) { + return false; + } + + return super.visit(node); + } + + @Override + public boolean visit(AnnotationTypeDeclaration node) { + if (isUnreachableNode(node)) { + return false; + } + + return super.visit(node); + } + + @Override + public boolean visit(AnonymousClassDeclaration node) { + if (isUnreachableNode(node)) { + return false; + } + + return super.visit(node); + } + + @Override + public boolean visit(TypeDeclarationStatement node) { + if (isUnreachableNode(node)) { + return false; + } + + return super.visit(node); + } + + @Override + public boolean visit(ImportDeclaration node) { + return false; + } + + @Override + public boolean visit(ModuleDeclaration node) { + return false; + } + + @Override + public boolean visit(PackageDeclaration node) { + return false; + } + + @Override + public boolean visit(QualifiedName node) { + return Objects.equals("length", node.getName().getIdentifier()); + } + + @Override + public boolean visit(QualifiedType node) { + return false; + } + + /** + * Return the valid inline variables in the visible source ranges. + * + *

There are four typical kinds of variable: + * - Local variables declared in method body. + * - Formal parameters declared in method declaration. + * - Field variables. + * - Captured variables from outer scope. This includes local type is accessing + * variables of enclosing method, and lambda expression body is accessing to + * variables of enclosing method.

+ * + *

For the first two kinds such as local variables and formal parameters, + * we're going to return them with VariableLookup kind since their values are + * expanded by Variables View by default.

+ * + *

For the last two kinds, we're going to return them with Evaluation kind + * since it requires additional evaluation to get its values.

+ */ + public InlineVariable[] getInlineVariables() { + if (visibleInlineRange == null) { + return new InlineVariable[0]; + } + + // Adding the local variable declarations to the token list. + for (int i = 0; i < localVarDecls.size(); i++) { + String name = localVarDecls.get(i); + Position position = localVarDeclPositions.get(i); + if (isEnclosed(visibleInlineRange, position)) { + int index = position.getLine() - baseLine; + if (tokens[index] == null) { + tokens[index] = new LinkedHashSet<>(); + } + + Token token = new Token(name, position, null); + if (!tokens[index].contains(token)) { + tokens[index].add(token); + } + } + } + + // For lambda expression in non static method, the captured variable 'arg$1' + // points to 'this' object of the enclosing method, and the index of other + // captured variables starts with 2. + int capturedArgIndexInLambda = isStoppingAtStaticMethod ? 1 : 2; + Map capturedVarsInLambda = new HashMap<>(); + List result = new ArrayList<>(); + for (int i = 0; i < tokens.length; i++) { + int line = baseLine + i; + if (tokens[i] == null || line < visibleInlineRange.getStart().getLine()) { + continue; + } + + for (Token token : tokens[i]) { + if (!isEnclosed(visibleInlineRange, token.position) && !isAtLastVisibleLine(token.position)) { + continue; + } + + // Local Variables + if (token.declaringClass == null && localVarDecls.contains(token.name)) { + int declIndex = localVarDecls.lastIndexOf(token.name); + Position declPosition = localVarDeclPositions.get(declIndex); + if (compare(token.position, declPosition) >= 0) { + result.add(new InlineVariable(new Range(token.position, token.position), token.name, InlineKind.VariableLookup)); + continue; + } + } + + InlineVariable value = new InlineVariable( + new Range(token.position, token.position), token.name, InlineKind.Evaluation, token.declaringClass); + // Captured variables by lambda expression + if (isStoppingAtLambda && token.declaringClass == null) { + /** + * When the lambda body accesses variables from its "outer" scope such as + * its enclosing method, these variables will be captured as properties of + * 'this' object of a synthetic lambda instance by Java runtime. However, + * when the compiler parses the lambda expression, it erases the specific + * variable name but keeps the captured variable names with format like + * 'arg$'. In order to evaluate the correct value from Java runtime, + * we have to encode the variable name using the same rule 'arg$' as + * the compiler. + */ + if (capturedVarsInLambda.containsKey(token.name)) { + value.expression = capturedVarsInLambda.get(token.name); + } else { + value.expression = "arg$" + capturedArgIndexInLambda++; + capturedVarsInLambda.put(token.name, value.expression); + } + } + + result.add(value); + } + } + + return result.toArray(new InlineVariable[0]); + } + + private Position getStartPosition(ASTNode node) { + // Line number returned by AST unit is one based, converts it to zero based. + int lineNumber = unit.getLineNumber(node.getStartPosition()) - 1; + int columnNumber = unit.getColumnNumber(node.getStartPosition()); + return new Position(lineNumber, columnNumber); + } + + private Position getEndPosition(ASTNode node) { + // Line number returned by AST unit is one based, converts it to zero based. + int lineNumber = unit.getLineNumber(node.getStartPosition() + node.getLength() - 1) - 1; + int columnNumber = unit.getColumnNumber(node.getStartPosition() + node.getLength() - 1); + return new Position(lineNumber, columnNumber); + } + + private boolean isUnreachableNode(ASTNode node) { + Position startPosition = getStartPosition(node); + Position endPosition = getEndPosition(node); + return compare(startPosition, stoppedSourceRange.getEnd()) > 0 + || compare(endPosition, stoppedSourceRange.getEnd()) < 0; + } + + private boolean isEnclosed(Range range, Position position) { + return compare(range.getStart(), position) <= 0 && compare(range.getEnd(), position) >= 0; + } + + private int compare(Position p1, Position p2) { + if (p1.getLine() < p2.getLine()) { + return -1; + } else if (p1.getLine() == p2.getLine()) { + return p1.getCharacter() - p2.getCharacter(); + } + + return 1; + } + + private boolean isAtStopLocation(Position position) { + return position.getLine() == stoppedSourceRange.getEnd().getLine(); + } + + private boolean isAtStopLocation(ASTNode node) { + Position startPosition = getStartPosition(node); + return isAtStopLocation(startPosition); + } + + private boolean isAtLastVisibleLine(Position position) { + return visibleInlineRange != null && visibleInlineRange.getEnd().getLine() == position.getLine(); + } + } + + static class InlineVariable { + Range range; + String name; + InlineKind kind; + String expression; + String declaringClass; + + public InlineVariable(Range range, String name, InlineKind kind) { + this.range = range; + this.name = name; + this.kind = kind; + } + + public InlineVariable(Range range, String name, InlineKind kind, String declaringClass) { + this.range = range; + this.name = name; + this.kind = kind; + this.declaringClass = declaringClass; + } + } + + static enum InlineKind { + VariableLookup, + Evaluation + } + + static class InlineParams { + String uri; + Range viewPort; + Range stoppedLocation; + } + + static class Token { + String name; + Position position; + String declaringClass = null; + + public Token(String name, Position position) { + this.name = name; + this.position = position; + } + + public Token(String name, Position position, String declaringClass) { + this.name = name; + this.position = position; + this.declaringClass = declaringClass; + } + + @Override + public int hashCode() { + return Objects.hash(declaringClass, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Token)) { + return false; + } + Token other = (Token) obj; + return Objects.equals(declaringClass, other.declaringClass) && Objects.equals(name, other.name); + } + } +} 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 032d0420c..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 @@ -28,6 +28,7 @@ import com.microsoft.java.debug.core.UsageDataStore; import com.microsoft.java.debug.core.protocol.JsonUtils; import com.microsoft.java.debug.core.protocol.Requests.LaunchArguments; +import com.microsoft.java.debug.plugin.internal.InlineValueHandler.InlineParams; public class JavaDebugDelegateCommandHandler implements IDelegateCommandHandler { public static final String FETCH_USER_DATA = "vscode.java.fetchUsageData"; @@ -47,6 +48,7 @@ public class JavaDebugDelegateCommandHandler implements IDelegateCommandHandler public static final String FETCH_PLATFORM_SETTINGS = "vscode.java.fetchPlatformSettings"; public static final String RESOLVE_CLASSFILTERS = "vscode.java.resolveClassFilters"; public static final String RESOLVE_SOURCEURI = "vscode.java.resolveSourceUri"; + public static final String RESOLVE_INLINEVARIABLES = "vscode.java.resolveInlineVariables"; @Override public Object executeCommand(String commandId, List arguments, IProgressMonitor progress) throws Exception { @@ -62,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: @@ -90,6 +96,8 @@ public Object executeCommand(String commandId, List arguments, IProgress return JavaClassFilter.resolveClassFilters(arguments); case RESOLVE_SOURCEURI: return ResolveSourceMappingHandler.resolveSourceUri(arguments); + case RESOLVE_INLINEVARIABLES: + return InlineValueHandler.resolveInlineVariables(JsonUtils.fromJson((String) arguments.get(0), InlineParams.class), progress); default: break; } 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 65ba6074a..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-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 @@ -17,25 +17,31 @@ import java.io.InputStreamReader; import java.net.URI; import java.net.URISyntaxException; -import java.nio.charset.Charset; 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; @@ -45,22 +51,61 @@ 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(); + private String latestJavaVersion = null; + private int latestASTLevel; + + public JdtSourceLookUpProvider() { + // Get the latest supported Java version by JDT tooling. + this.latestJavaVersion = JavaCore.latestSupportedJavaVersion(); + // Get the mapped AST level for the latest Java version. + Map javaOptions = JavaCore.getOptions(); + javaOptions.put(JavaCore.COMPILER_SOURCE, latestJavaVersion); + this.latestASTLevel = new AST(javaOptions).apiLevel(); + } @Override public void initialize(IDebugAdapterContext context, Map props) { @@ -68,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(); @@ -101,71 +147,246 @@ public String[] getFullyQualifiedName(String uri, int[] lines, int[] columns) th return new String[0]; } - // Currently the highest version the debugger supports is JavaSE-13 Edition (JLS13). - final ASTParser parser = ASTParser.newParser(AST.JLS13); + 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))) { - Charset cs = (Charset) this.options.get(Constants.DEBUGGEE_ENCODING); - if (cs == null) { - cs = Charset.defaultCharset(); - } - String source = readFile(filePath, cs); + 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, JavaCore.VERSION_13); - javaOptions.put(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, JavaCore.VERSION_13); - javaOptions.put(JavaCore.COMPILER_COMPLIANCE, JavaCore.VERSION_13); - 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 @@ -195,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); } } @@ -204,6 +426,7 @@ public String getJavaRuntimeVersion(String projectName) { /** * Get the project associated source containers. + * * @return the initialized source container list */ public synchronized ISourceContainer[] getSourceContainers() { @@ -232,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 = ""; @@ -247,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; } @@ -282,10 +506,9 @@ private static IClassFile resolveClassFile(String uriString) { return null; } - private static String readFile(String filePath, Charset cs) { + private static String readFile(String filePath) { StringBuilder builder = new StringBuilder(); - try (BufferedReader bufferReader = - new BufferedReader(new InputStreamReader(new FileInputStream(filePath), cs))) { + try (BufferedReader bufferReader = new BufferedReader(new InputStreamReader(new FileInputStream(filePath)))) { final int BUFFER_SIZE = 4096; char[] buffer = new char[BUFFER_SIZE]; while (true) { @@ -301,7 +524,8 @@ private static String readFile(String filePath, Charset cs) { 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()); @@ -318,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/ResolveClasspathsHandler.java b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveClasspathsHandler.java index be37285e3..5e7017128 100644 --- a/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveClasspathsHandler.java +++ b/com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/ResolveClasspathsHandler.java @@ -70,6 +70,9 @@ public class ResolveClasspathsHandler { */ public String[][] resolveClasspaths(List arguments) throws Exception { try { + if (arguments.size() == 3) { + return computeClassPath((String) arguments.get(0), (String) arguments.get(1), (String) arguments.get(2)); + } return computeClassPath((String) arguments.get(0), (String) arguments.get(1)); } catch (CoreException e) { logger.log(Level.SEVERE, "Failed to resolve classpath: " + e.getMessage(), e); @@ -159,6 +162,23 @@ public void acceptSearchMatch(SearchMatch match) { * CoreException */ private static String[][] computeClassPath(String mainClass, String projectName) throws CoreException { + return computeClassPath(mainClass, projectName, null); + } + + /** + * Accord to the project name and the main class, compute runtime classpath. + * + * @param mainClass + * fully qualified class name + * @param projectName + * project name + * @param scope + * scope of the classpath + * @return class path + * @throws CoreException + * CoreException + */ + private static String[][] computeClassPath(String mainClass, String projectName, String scope) throws CoreException { IJavaProject project = null; // if type exists in multiple projects, debug configuration need provide // project name. @@ -179,6 +199,12 @@ private static String[][] computeClassPath(String mainClass, String projectName) project = projects.get(0); } + if ("test".equals(scope)) { + return computeClassPath(project, mainClass, false /*excludeTestCode*/, Collections.EMPTY_LIST); + } else if ("runtime".equals(scope)) { + return computeClassPath(project, mainClass, true /*excludeTestCode*/, Collections.EMPTY_LIST); + } + IJavaElement testElement = findMainClassInTestFolders(project, mainClass); List mappedResources = (testElement != null && testElement.getResource() != null) ? Arrays.asList(testElement.getResource()) : Collections.EMPTY_LIST; @@ -275,7 +301,7 @@ public void acceptSearchMatch(SearchMatch match) { private static class JavaApplicationLaunchConfiguration extends LaunchConfiguration { public static final String JAVA_APPLICATION_LAUNCH = "\n" - + "\n" + + "\n" + "\n" + "\n" + "\n" @@ -302,7 +328,18 @@ protected JavaApplicationLaunchConfiguration(IProject project, String mainType, } else if (ProjectUtils.isGradleProject(project)) { classpathProvider = "org.eclipse.buildship.core.classpathprovider"; } - this.launchInfo = new JavaLaunchConfigurationInfo(JAVA_APPLICATION_LAUNCH); + + // Since MavenRuntimeClasspathProvider will only including test entries when: + // 1. Launch configuration is JUnit/TestNG type + // 2. Mapped resource is in test path. + // That's why we use JUnit launch configuration here to make sure the result is right when excludeTestCode is false. + String launchXml = null; + if (!excludeTestCode && mappedResources.isEmpty()) { + launchXml = String.format(JAVA_APPLICATION_LAUNCH, "org.eclipse.jdt.junit.launchconfig"); + } else { + launchXml = String.format(JAVA_APPLICATION_LAUNCH, "org.eclipse.jdt.launching.localJavaApplication"); + } + this.launchInfo = new JavaLaunchConfigurationInfo(launchXml); } @Override 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 9d3b1014e..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 @@ -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.plugin.internal; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -21,6 +22,9 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.lang.model.SourceVersion; import org.apache.commons.lang3.StringUtils; import org.eclipse.core.resources.IFile; @@ -31,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; @@ -39,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; @@ -47,8 +53,10 @@ public class ResolveMainClassHandler { private static final Logger logger = Logger.getLogger(Configuration.LOGGER_NAME); - // Java command line supports two kinds of main class format: and [/] - private static final String CLASSNAME_REGX = "([$\\w]+\\.)*[$\\w]+(/([$\\w]+\\.)*[$\\w]+)?"; + private static final int CONFIGERROR_INVALID_CLASS_NAME = 1; + private static final int CONFIGERROR_MAIN_CLASS_NOT_EXIST = 2; + private static final int CONFIGERROR_MAIN_CLASS_NOT_UNIQUE = 3; + private static final int CONFIGERROR_INVALID_JAVA_PROJECT = 4; /** * resolve main class and project name. @@ -75,17 +83,37 @@ public Object validateLaunchConfig(List arguments) throws Exception { } private List resolveMainClassCore(List arguments) { - IPath rootPath = null; if (arguments != null && arguments.size() > 0 && arguments.get(0) != null) { - rootPath = ResourceUtils.filePathFromURI((String) arguments.get(0)); + String argument = (String) arguments.get(0); + IProject[] projects = ProjectUtils.getAllProjects(); + if (Stream.of(projects).anyMatch(project -> Objects.equals(project.getName(), argument))) { + return resolveMainClassUnderProject(argument); + } + + IPath rootPath = ResourceUtils.filePathFromURI(argument); + if (rootPath != null) { + return resolveMainClassUnderPaths(Arrays.asList(rootPath)); + } } - final ArrayList targetProjectPath = new ArrayList<>(); - if (rootPath != null) { - targetProjectPath.add(rootPath); + + return resolveMainClassUnderPaths(Collections.emptyList()); + } + + private List resolveMainClassUnderPaths(List parentPaths) { + // Limit to search main method from source code only. + 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.createWorkspaceScope(); - SearchPattern pattern = SearchPattern.createPattern("main(String[]) void", IJavaSearchConstants.METHOD, - IJavaSearchConstants.DECLARATIONS, SearchPattern.R_CASE_SENSITIVE | SearchPattern.R_EXACT_MATCH); + IJavaSearchScope searchScope = SearchEngine.createJavaSearchScope(projects, + IJavaSearchScope.SOURCES); + SearchPattern pattern = createMainMethodSearchPattern(); final List res = new ArrayList<>(); SearchRequestor requestor = new SearchRequestor() { @Override @@ -93,41 +121,91 @@ 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 (projectName == null - || targetProjectPath.isEmpty() - || ResourceUtils.isContainedIn(project.getLocation(), targetProjectPath) - || isContainedInInvisibleProject(project, targetProjectPath)) { - 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)); + } + } + } + } + } + } + }; + SearchEngine searchEngine = new SearchEngine(); + try { + searchEngine.search(pattern, new SearchParticipant[] {SearchEngine.getDefaultSearchParticipant()}, + searchScope, requestor, null /* progress monitor */); + } catch (Exception e) { + logger.log(Level.SEVERE, String.format("Searching the main class failure: %s", e.toString()), e); + } + + List resolutions = res.stream().distinct().collect(Collectors.toList()); + Collections.sort(resolutions); + return resolutions; + } + + private List resolveMainClassUnderProject(final String projectName) { + // Limit to search main method from source code only. + IJavaProject javaProject = ProjectUtils.getJavaProject(projectName); + IJavaSearchScope searchScope = SearchEngine.createJavaSearchScope(javaProject == null ? new IJavaProject[0] : new IJavaProject[] {javaProject}, + IJavaSearchScope.REFERENCED_PROJECTS | IJavaSearchScope.SOURCES); + SearchPattern pattern = createMainMethodSearchPattern(); + final List res = new ArrayList<>(); + SearchRequestor requestor = new SearchRequestor() { + @Override + public void acceptSearchMatch(SearchMatch match) { + Object element = match.getElement(); + if (element instanceof IMethod) { + IMethod method = (IMethod) element; + 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 + } + } + res.add(new ResolutionItem(mainClass, projectName, filePath)); } } - } catch (JavaModelException e) { - // ignore } } } @@ -145,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; @@ -185,23 +286,41 @@ private ValidationResponse validateLaunchConfigCore(List arguments) thro private ValidationResult validateMainClass(final String mainClass, final String projectName, boolean containsExternalClasspaths) throws CoreException { if (StringUtils.isEmpty(mainClass)) { return new ValidationResult(true); - } else if (!mainClass.matches(CLASSNAME_REGX)) { - return new ValidationResult(false, String.format("ConfigError: '%s' is not a valid class name.", mainClass)); + } else if (!isValidMainClassName(mainClass)) { + return new ValidationResult(false, String.format("ConfigError: '%s' is not a valid class name.", mainClass), + CONFIGERROR_INVALID_CLASS_NAME); } if (!containsExternalClasspaths && StringUtils.isEmpty(projectName)) { List javaProjects = searchClassInProjectClasspaths(mainClass); if (javaProjects.size() == 0) { - return new ValidationResult(false, String.format("ConfigError: Main class '%s' doesn't exist in the workspace.", mainClass)); + return new ValidationResult(false, String.format("ConfigError: Main class '%s' doesn't exist in the workspace.", mainClass), + CONFIGERROR_MAIN_CLASS_NOT_EXIST); } if (javaProjects.size() > 1) { - return new ValidationResult(false, String.format("ConfigError: Main class '%s' isn't unique in the workspace.", mainClass)); + return new ValidationResult(false, String.format("ConfigError: Main class '%s' isn't unique in the workspace.", mainClass), + CONFIGERROR_MAIN_CLASS_NOT_UNIQUE); } } return new ValidationResult(true); } + // Java command line supports two kinds of main class format: and [/] + private boolean isValidMainClassName(String mainClass) { + if (StringUtils.isEmpty(mainClass)) { + return true; + } + + int index = mainClass.indexOf('/'); + if (index == -1) { + return SourceVersion.isName(mainClass); + } + + return SourceVersion.isName(mainClass.substring(0, index)) + && SourceVersion.isName(mainClass.substring(index + 1)); + } + private List searchClassInProjectClasspaths(String fullyQualifiedClassName) throws CoreException { return ResolveClasspathsHandler.getJavaProjectFromType(fullyQualifiedClassName); } @@ -212,7 +331,8 @@ private ValidationResult validateProjectName(final String mainClass, final Strin } if (JdtUtils.getJavaProject(projectName) == null) { - return new ValidationResult(false, String.format("ConfigError: The project '%s' is not a valid java project.", projectName)); + return new ValidationResult(false, String.format("ConfigError: The project '%s' is not a valid java project.", projectName), + CONFIGERROR_INVALID_JAVA_PROJECT); } return new ValidationResult(true); @@ -302,14 +422,16 @@ class ValidationResponse { class ValidationResult { boolean isValid; String message; + int kind; ValidationResult(boolean isValid) { this.isValid = isValid; } - ValidationResult(boolean isValid, String message) { + ValidationResult(boolean isValid, String message, int kind) { this.isValid = isValid; this.message = message; + this.kind = kind; } } } 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 85ff58978..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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018-2020 Microsoft Corporation and others. + * Copyright (c) 2018-2021 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,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; @@ -26,10 +27,15 @@ import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IOpenable; 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; @@ -103,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(); @@ -167,7 +207,7 @@ private static MainMethod extractMainMethodInfo(ICompilationUnit typeRoot, IMeth private static Range getRange(ICompilationUnit typeRoot, IJavaElement element) throws JavaModelException { ISourceRange r = ((ISourceReference) element).getNameRange(); - return JDTUtils.toRange(typeRoot, r.getOffset(), r.getLength()); + return JDTUtils.toRange((IOpenable) typeRoot, r.getOffset(), r.getLength()); } static class MainMethod { 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 21e2dff6e..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; @@ -226,9 +227,9 @@ private String logMessageToExpression(String logMessage) { } if (arguments.size() > 0) { - return "System.out.println(String.format(\"" + format + "\"," + String.join(",", arguments) + "))"; + return "String.format(\"" + format + "\"," + String.join(",", arguments) + ")"; } else { - return "System.out.println(\"" + format + "\")"; + return "\"" + format + "\""; } } @@ -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 e873f4578..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 ce56bf3a5..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.29.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 835c63801..000000000 --- a/java.debug.target +++ /dev/null @@ -1,26 +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 994191f7b..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.29.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 @@ - - 201912 - p2 - https://download.eclipse.org/releases/2019-12/ - 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.0-2018-05-16_00-46-30-H11 - - - 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)}"`) }