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