├── .github └── workflows │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── BUILDING.md ├── CHANGELOG.md ├── CONFIGURATION.md ├── EDITORS.md ├── Icon128.png ├── LICENSE ├── README.md ├── adapter ├── build.gradle.kts └── src │ ├── main │ ├── dist │ │ └── licenseReport.html │ ├── kotlin │ │ └── org │ │ │ └── javacs │ │ │ └── ktda │ │ │ ├── KDAMain.kt │ │ │ ├── adapter │ │ │ ├── DAPConverter.kt │ │ │ ├── KotlinDebugAdapter.kt │ │ │ └── LineNumberConverter.kt │ │ │ ├── classpath │ │ │ ├── DebugClassPathResolver.kt │ │ │ ├── PathUtils.kt │ │ │ └── ProjectClassesResolver.kt │ │ │ ├── core │ │ │ ├── DebugContext.kt │ │ │ ├── Debuggee.kt │ │ │ ├── DebuggeeThread.kt │ │ │ ├── Position.kt │ │ │ ├── Source.kt │ │ │ ├── breakpoint │ │ │ │ ├── Breakpoint.kt │ │ │ │ ├── BreakpointManager.kt │ │ │ │ ├── ExceptionBreakpoint.kt │ │ │ │ └── SourceBreakpoint.kt │ │ │ ├── completion │ │ │ │ ├── CompletionItem.kt │ │ │ │ └── CompletionItemType.kt │ │ │ ├── event │ │ │ │ ├── BreakpointStopEvent.kt │ │ │ │ ├── DebuggeeEventBus.kt │ │ │ │ ├── ExceptionStopEvent.kt │ │ │ │ ├── ExitEvent.kt │ │ │ │ ├── StepStopEvent.kt │ │ │ │ ├── ThreadEvent.kt │ │ │ │ └── ThreadEventReason.kt │ │ │ ├── exception │ │ │ │ └── DebuggeeException.kt │ │ │ ├── launch │ │ │ │ ├── AttachConfiguration.kt │ │ │ │ ├── DebugLauncher.kt │ │ │ │ └── LaunchConfiguration.kt │ │ │ ├── scope │ │ │ │ ├── BasicVariableTreeNode.kt │ │ │ │ └── VariableTreeNode.kt │ │ │ └── stack │ │ │ │ ├── StackFrame.kt │ │ │ │ └── StackTrace.kt │ │ │ ├── jdi │ │ │ ├── JDIDebuggee.kt │ │ │ ├── JDISessionContext.kt │ │ │ ├── JDIThread.kt │ │ │ ├── event │ │ │ │ ├── VMEvent.kt │ │ │ │ └── VMEventBus.kt │ │ │ ├── exception │ │ │ │ └── JDIException.kt │ │ │ ├── launch │ │ │ │ └── JDILauncher.kt │ │ │ ├── scope │ │ │ │ ├── JDILocalScope.kt │ │ │ │ └── JDIVariable.kt │ │ │ └── stack │ │ │ │ ├── JDIStackFrame.kt │ │ │ │ └── JDIStackTrace.kt │ │ │ └── util │ │ │ ├── Box.kt │ │ │ ├── Identifiable.kt │ │ │ ├── JsonLogger.kt │ │ │ ├── KotlinDAException.kt │ │ │ ├── ListenerList.kt │ │ │ ├── LoggingInputStream.kt │ │ │ ├── LoggingOutputStream.kt │ │ │ ├── ObjectPool.kt │ │ │ ├── Observable.kt │ │ │ ├── ObservableList.kt │ │ │ ├── ObservableMap.kt │ │ │ ├── ObservableSet.kt │ │ │ ├── Subscription.kt │ │ │ ├── SubscriptionBag.kt │ │ │ └── Utils.kt │ └── resources │ │ └── classpathFinder.gradle │ └── test │ ├── kotlin │ └── org │ │ └── javacs │ │ └── ktda │ │ ├── DebugAdapterTestFixture.kt │ │ ├── SampleWorkspaceTest.kt │ │ ├── classpath │ │ └── PathUtilsTest.kt │ │ └── util │ │ └── ObjectPoolTest.kt │ └── resources │ ├── Anchor.txt │ └── sample-workspace │ ├── .gitattributes │ ├── build.gradle │ ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src │ └── main │ └── kotlin │ └── sample │ └── workspace │ └── App.kt ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | java: ['11', '17'] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Setup JDK 19 | uses: actions/setup-java@v2 20 | with: 21 | distribution: 'temurin' 22 | java-version: ${{ matrix.java }} 23 | - name: Build 24 | run: ./gradlew :adapter:build -PjavaVersion=${{ matrix.java }} 25 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Setup JDK 13 | uses: actions/setup-java@v2 14 | with: 15 | distribution: 'temurin' 16 | java-version: 11 17 | - name: Build distribution 18 | run: ./gradlew :adapter:distZip 19 | - name: Create release 20 | uses: actions/create-release@v1 21 | id: create_release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | tag_name: ${{ github.ref }} 26 | release_name: Version ${{ github.ref }} 27 | draft: false 28 | prerelease: false 29 | - name: Upload asset 30 | uses: actions/upload-release-asset@v1 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | upload_url: ${{ steps.create_release.outputs.upload_url }} 35 | asset_path: ./adapter/build/distributions/adapter.zip 36 | asset_name: adapter.zip 37 | asset_content_type: application/zip 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .settings 3 | .project 4 | .classpath 5 | .idea 6 | build 7 | bin 8 | target 9 | .DS_Store 10 | node_modules 11 | *.vsix 12 | *.log 13 | out 14 | .vscode/* 15 | !.vscode/launch.json 16 | !.vscode/tasks.json 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "java", 6 | "name": "Java Debug (Attach)", 7 | "request": "attach", 8 | "hostName": "localhost", 9 | "port": 5005 10 | }, 11 | { 12 | "type": "kotlin", 13 | "request": "launch", 14 | "name": "Debug KotlinDebugAdapter", 15 | "projectRoot": "${workspaceFolder}/adapter", 16 | "mainClass": "org.javacs.ktda.KDAMainKt" 17 | }, 18 | { 19 | "type": "kotlin", 20 | "request": "launch", 21 | "name": "Debug Sample Workspace", 22 | "projectRoot": "${workspaceFolder}/adapter/src/test/resources/sample-workspace", 23 | "mainClass": "sample.workspace.AppKt" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Compile Debug Adapter", 6 | "type": "shell", 7 | "command": "gradle :adapter:installDist", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Building 2 | Describes how to build and run the debug adapter and the editor extensions. 3 | 4 | ## Setup 5 | * Java 8+ should be installed and located under `JAVA_HOME` or `PATH` 6 | * Note that you might need to use `gradlew` instead of `./gradlew` for the commands on Windows 7 | 8 | ## Debug Adapter 9 | 10 | ### Building 11 | If you just want to build the debug adapter and use its binaries in your client of choice, run: 12 | 13 | >`./gradlew :adapter:installDist` 14 | 15 | The debug adapter executable is now located under `adapter/build/install/adapter/bin/kotlin-debug-adapter`. (Depending on your debug client, you might want to add it to your `PATH`) 16 | 17 | Note that there are external dependent libraries, so if you want to put the server somewhere else, you have to move the entire `install`-directory. 18 | 19 | ## VSCode extension 20 | 21 | ### Development/Running 22 | First run `npm run watch` from the `editors/vscode` directory in a background shell. The extension will then incrementally build in the background. 23 | 24 | Every time you want to run the extension with the language server: 25 | * Prepare the extension using `./gradlew :editors:vscode:prepare` (this automatically build and copies the language server's binaries into the extension folder) 26 | * Open the debug tab in VSCode 27 | * Run the `Extension` launch configuration 28 | 29 | ### Debugging 30 | >TODO 31 | 32 | ### Packaging 33 | Run `./gradlew :editors:vscode:packageExtension` from the repository's top-level-directory. The extension will then be located under the name `kotlindebug-[version].vsix` in `editors/vscode`. 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the debug adapter will be documented in this file. 3 | 4 | ## [0.4.4] 5 | - Kotlin 1.9.10 6 | - Fix class path resolution by depending on the language server's shared module via a source dependency (see #78) 7 | 8 | ## [0.4.3] 9 | - Kotlin 1.5.20 10 | - Small fixes 11 | 12 | ## [0.4.2] 13 | - Improve handling of cyclic references 14 | - Ignore breakpoints from other languages 15 | - Fix suspension policy on user breakpoints 16 | 17 | ## [0.4.1] 18 | - Emit threads in attached VM correctly 19 | - Fix breakpoint placements in local classes 20 | 21 | ## [0.4.0] 22 | - Emit thread start/exit events 23 | - Support vm arguments 24 | 25 | ## [0.3.1] 26 | - Fix `attach` 27 | 28 | ## [0.3.0] 29 | - Support evaluation of expressions 30 | - Provide completions 31 | - Display exception information 32 | 33 | ## [0.2.7] 34 | - Add JDK 11+ support 35 | 36 | ## [0.2.6] 37 | - Support for stepping into Java classes 38 | - Automatic termination of the debugger once the VM exits 39 | 40 | ## [0.2.2] 41 | - Move VSCode extension into external repository 42 | 43 | ## [0.2.1] 44 | - Bugfixes to the variable list 45 | 46 | ## [0.2.0] 47 | - `this` is now part of the local scope 48 | - Inspection of variable fields and array elements 49 | 50 | ## [0.1.2] 51 | - Local variable inspection 52 | - Travis CI 53 | - Bugfixes (executing on linux) 54 | 55 | ## [0.1.1] 56 | - Badges 57 | 58 | ## [0.1.0] 59 | - Initial release 60 | - Breakpoints 61 | - Threads 62 | - Call stacks 63 | - Debuggee Output in the Debug Console 64 | 65 | ## [0.0.1] 66 | - Initial version 67 | -------------------------------------------------------------------------------- /CONFIGURATION.md: -------------------------------------------------------------------------------- 1 | # Configuration examples 2 | On this page we will collect some configuration examples. This is meant to help you configure debug templates for your editor. The examples are provided in JSON format, but should be easily translatable to VSCode launch.json files, Emacs dap-mode templates, etc. 3 | 4 | 5 | > [!NOTE] 6 | > Some editors like Emacs using lsp-mode will require you to set a `noDebug` argument to `false`/`nil` to start a debug process, if not you will simply run the code.** 7 | 8 | 9 | > [!NOTE] 10 | > A general tip for working with the debug adapter is building your codebase before launching the debugger. A lot of people experiencing "class not found"-style issues, and they are in most cases caused by not building before debugging. A simple Maven or Gradle compile should suffice.** 11 | 12 | 13 | ## launch 14 | ### Regular main method 15 | Replace the `mainClassName` with your fully qualified class name where your main method resides (e.g, `com.example.MyApplicationKt`), and `projectRootPath` with the root of your project. If your main method resides in a file, the class name will be the package and name of the file (+ Kt at the end). 16 | 17 | 18 | > [!NOTE] 19 | > If you use [the VSCode Kotlin extension](https://github.com/fwcd/vscode-kotlin) or [Emacs lsp-mode](https://emacs-lsp.github.io/lsp-mode/), you will have the option to use code lenses in your editor to run or debug main methods. These will fill out the details below automatically for you. 20 | 21 | 22 | ```json 23 | { 24 | "type": "kotlin", 25 | "request": "launch", 26 | "mainClass": mainClassName, 27 | "projectRoot": projectRootPath 28 | } 29 | ``` 30 | 31 | 32 | ### Test 33 | Debugging or running tests using the debug adapter might seem a bit daunting at first. What should the `mainClass` be? Fortunately it is quite simple if you use JUnit, as it provides a [Console Launcher](https://junit.org/junit5/docs/current/user-guide/#running-tests-console-launcher) that we can utilize. 34 | 35 | ```json 36 | { 37 | "type": "kotlin", 38 | "request": "launch", 39 | "mainClass": "org.junit.platform.console.ConsoleLauncher --scan-class-path", 40 | "projectRoot": projectRootPath 41 | } 42 | ``` 43 | 44 | 45 | For this to work, you will need the console launcher in your classpath during tests. Some JUnit related dependencies may already provide it, but you can also add it explicitly if you get ClassNotFound style errors (Maven example): 46 | ```xml 47 | 48 | org.junit.platform 49 | junit-platform-console-standalone 50 | 1.9.2 51 | test 52 | 53 | ``` 54 | 55 | 56 | There is [a neat plugin for Neovim utilizing this way of running tests already](https://github.com/Mgenuit/nvim-dap-kotlin). 57 | 58 | 59 | 60 | ## attach (connecting to an existing debug process) 61 | `attach` configurations are meant for attaching to already running processes. These could be processes you start in the terminal, from your editor or something else. 62 | 63 | 64 | The setup will be the same whether you connect to a regular main method debugging session, or a test session. 65 | 66 | ```json 67 | { 68 | "type": "kotlin", 69 | "request": "attach", 70 | "projectRoot": projectRootPath, 71 | "hostName": "localhost", 72 | "port": 5005, 73 | "timeout": 2000 74 | } 75 | ``` 76 | (replace `projectRootPath` with the path to your project root) 77 | 78 | If you connect to a process on an external machine, then replace `"localhost"` with your hostname. You can also tweak the port (5005 is the default for both Maven and Gradle). 79 | 80 | 81 | ## General: Logging to file 82 | If you want to add Kotlin Debug Adapter logging (client and debug adapter communications) to file, you can also the following parameters to any of the examples above: 83 | ```json 84 | { 85 | "enableJsonLogging": true, 86 | "jsonLogFile": pathToLogFile 87 | } 88 | ``` 89 | (where `pathToLogFile` is replaced with the actual path to your log file) 90 | -------------------------------------------------------------------------------- /EDITORS.md: -------------------------------------------------------------------------------- 1 | # Editor Integration 2 | 3 | ## Visual Studio Code 4 | See [vscode-kotlin](https://github.com/fwcd/vscode-kotlin) or install the extension from the [marketplace](https://marketplace.visualstudio.com/items?itemName=fwcd.kotlin). 5 | 6 | ## Other Editors 7 | Install a [Debug Adapter Protocol client](https://microsoft.github.io/debug-adapter-protocol/implementors/tools/) for your tool. Then invoke the debug adapter executable in a client-specific way. The server uses `stdio` to send and receive `JSON` messages. 8 | 9 | ### Usage 10 | * Start the debugger with a `launch` request after the initialization procedure: 11 | 12 | ![Sketch](https://microsoft.github.io/debug-adapter-protocol/img/init-launch.png) 13 | 14 | * Please note: 15 | * The `projectRoot` and `mainClass` arguments must be specified 16 | * The `projectRoot` argument should contain the absolute path to a Maven or a Gradle project folder with 17 | * a buildfile (`pom.xml` or `build.gradle`) 18 | * compiled output classes (located in `build/classes/kotlin/main` or `target/classes/kotlin/main`) 19 | -------------------------------------------------------------------------------- /Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/kotlin-debug-adapter/7f05669b642d21afa46ac7b75307fa5d523a7263/Icon128.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 fwcd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Debug Adapter 2 | 3 | [![Release](https://img.shields.io/github/release/fwcd/kotlin-debug-adapter)](https://github.com/fwcd/kotlin-debug-adapter/releases) 4 | [![Build](https://github.com/fwcd/kotlin-debug-adapter/actions/workflows/build.yml/badge.svg)](https://github.com/fwcd/kotlin-debug-adapter/actions/workflows/build.yml) 5 | [![Downloads](https://img.shields.io/github/downloads/fwcd/kotlin-debug-adapter/total)](https://github.com/fwcd/kotlin-debug-adapter/releases) 6 | [![Chat](https://img.shields.io/badge/chat-on%20discord-7289da)](https://discord.gg/cNtppzN) 7 | 8 | A [debug adapter](https://microsoft.github.io/debug-adapter-protocol/) that provides IDE-independent debugging support for Kotlin/JVM. 9 | 10 | ![Icon](Icon128.png) 11 | 12 | Any editor conforming to DAP is supported, including [VSCode](https://github.com/fwcd/vscode-kotlin). 13 | 14 | ## Getting Started 15 | * See [BUILDING.md](BUILDING.md) for build instructions 16 | * See [Editor Integration](EDITORS.md) for editor-specific instructions 17 | * See [CONFIGURATION.md](CONFIGURATION.md) for examples on debug configurations (mostly editor agnostic). 18 | * See [Kotlin Quick Start](https://github.com/fwcd/kotlin-quick-start) for a sample project 19 | * See [Kotlin Language Server](https://github.com/fwcd/kotlin-language-server) for smart code completion, diagnostics and more 20 | 21 | ## Architecture 22 | `DAP client` <= JSON => `KotlinDebugAdapter` <=> `Core abstractions` <=> `Java Debug Interface` 23 | -------------------------------------------------------------------------------- /adapter/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | id("maven-publish") 4 | id("application") 5 | id("com.jaredsburrows.license") 6 | } 7 | 8 | val debugPort = 8000 9 | val debugArgs = "-agentlib:jdwp=transport=dt_socket,server=y,address=8000,suspend=n,quiet=y" 10 | 11 | val adapterMainClassName = "org.javacs.ktda.KDAMainKt" 12 | 13 | application { 14 | mainClass.set(adapterMainClassName) 15 | description = "Debug Adapter for Kotlin" 16 | applicationDistribution.into("bin") { 17 | fileMode = 755 18 | } 19 | } 20 | 21 | dependencies { 22 | // The JSON-RPC and Debug Adapter Protocol implementations 23 | implementation("org.eclipse.lsp4j:org.eclipse.lsp4j.debug:0.15.0") 24 | implementation("org.jetbrains.kotlin:kotlin-stdlib") 25 | implementation("org.jetbrains.kotlin:kotlin-reflect") 26 | implementation("kotlin-language-server:shared") 27 | 28 | // modules temporarily needed because of shared module import above 29 | implementation("org.jetbrains.exposed:exposed-core:0.37.3") 30 | implementation("org.jetbrains.exposed:exposed-dao:0.37.3") 31 | 32 | 33 | testImplementation("junit:junit:4.12") 34 | testImplementation("org.hamcrest:hamcrest-all:1.3") 35 | } 36 | 37 | tasks.startScripts { 38 | applicationName = "kotlin-debug-adapter" 39 | } 40 | 41 | tasks.register("fixFilePermissions") { 42 | // When running on macOS or Linux the start script 43 | // needs executable permissions to run. 44 | 45 | onlyIf { !System.getProperty("os.name").lowercase().contains("windows") } 46 | commandLine("chmod", "+x", "${tasks.installDist.get().destinationDir}/bin/kotlin-debug-adapter") 47 | } 48 | 49 | tasks.register("debugRun") { 50 | mainClass.set(adapterMainClassName) 51 | classpath(sourceSets.main.get().runtimeClasspath) 52 | standardInput = System.`in` 53 | 54 | jvmArgs(debugArgs) 55 | doLast { 56 | println("Using debug port $debugPort") 57 | } 58 | } 59 | 60 | tasks.register("debugStartScripts") { 61 | applicationName = "kotlin-debug-adapter" 62 | mainClass.set(adapterMainClassName) 63 | outputDir = tasks.installDist.get().destinationDir.toPath().resolve("bin").toFile() 64 | classpath = tasks.startScripts.get().classpath 65 | defaultJvmOpts = listOf(debugArgs) 66 | } 67 | 68 | tasks.register("installDebugDist") { 69 | dependsOn("installDist") 70 | finalizedBy("debugStartScripts") 71 | } 72 | 73 | tasks.withType() { 74 | testLogging { 75 | events("failed") 76 | exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.SHORT 77 | } 78 | } 79 | 80 | tasks.installDist { 81 | finalizedBy("fixFilePermissions") 82 | } 83 | 84 | tasks.build { 85 | finalizedBy("installDist") 86 | } 87 | -------------------------------------------------------------------------------- /adapter/src/main/dist/licenseReport.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Open source licenses 5 | 6 | 7 |

Notice for packages:

8 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/KDAMain.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda 2 | 3 | import java.util.concurrent.Executors 4 | import org.javacs.kt.LOG 5 | import org.javacs.kt.util.ExitingInputStream 6 | import org.javacs.ktda.adapter.KotlinDebugAdapter 7 | import org.javacs.ktda.core.launch.DebugLauncher 8 | import org.javacs.ktda.jdi.launch.JDILauncher 9 | import org.javacs.ktda.util.LoggingInputStream 10 | import org.javacs.ktda.util.LoggingOutputStream 11 | import org.eclipse.lsp4j.debug.launch.DSPLauncher 12 | import org.eclipse.lsp4j.debug.services.IDebugProtocolClient 13 | import org.eclipse.lsp4j.debug.TerminatedEventArguments 14 | 15 | /** Enable logging of raw input JSON messages (if it is enabled in the user's debug configuration). */ 16 | private const val JSON_IN_LOGGING = true 17 | private const val JSON_IN_LOGGING_BUFFER_LINES = true 18 | 19 | /** Enable logging of raw output JSON messages (if it is enabled in the user's debug configuration). */ 20 | private const val JSON_OUT_LOGGING = true 21 | private const val JSON_OUT_LOGGING_BUFFER_LINES = true 22 | 23 | fun main(args: Array) { 24 | LOG.connectJULFrontend() 25 | 26 | val launcher: DebugLauncher = JDILauncher() 27 | 28 | // Setup IO streams for JSON communication 29 | 30 | val input = LoggingInputStream(ExitingInputStream(System.`in`), JSON_IN_LOGGING, JSON_IN_LOGGING_BUFFER_LINES) 31 | val output = LoggingOutputStream(System.out, JSON_OUT_LOGGING, JSON_OUT_LOGGING_BUFFER_LINES) 32 | 33 | // Create debug adapter and launcher 34 | 35 | val debugAdapter = KotlinDebugAdapter(launcher) 36 | val threads = Executors.newSingleThreadExecutor { Thread(it, "server") } 37 | val serverLauncher = DSPLauncher.createServerLauncher(debugAdapter, input, output, threads) { it } 38 | 39 | debugAdapter.connect(serverLauncher.remoteProxy) 40 | serverLauncher.startListening() 41 | } 42 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/adapter/DAPConverter.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.adapter 2 | 3 | import java.nio.file.Paths 4 | import org.javacs.ktda.core.Position 5 | import org.javacs.ktda.core.DebuggeeThread 6 | import org.javacs.ktda.core.scope.VariableTreeNode 7 | import org.javacs.ktda.util.ObjectPool 8 | import org.javacs.ktda.util.KotlinDAException 9 | 10 | private typealias DAPSource = org.eclipse.lsp4j.debug.Source 11 | private typealias DAPSourceBreakpoint = org.eclipse.lsp4j.debug.SourceBreakpoint 12 | private typealias DAPBreakpoint = org.eclipse.lsp4j.debug.Breakpoint 13 | private typealias DAPStackFrame = org.eclipse.lsp4j.debug.StackFrame 14 | private typealias DAPScope = org.eclipse.lsp4j.debug.Scope 15 | private typealias DAPVariable = org.eclipse.lsp4j.debug.Variable 16 | private typealias DAPThread = org.eclipse.lsp4j.debug.Thread 17 | private typealias DAPExceptionBreakpointsFilter = org.eclipse.lsp4j.debug.ExceptionBreakpointsFilter 18 | private typealias DAPCompletionItem = org.eclipse.lsp4j.debug.CompletionItem 19 | private typealias DAPCompletionItemType = org.eclipse.lsp4j.debug.CompletionItemType 20 | private typealias DAPExceptionDetails = org.eclipse.lsp4j.debug.ExceptionDetails 21 | private typealias DAPThreadEventReason = org.eclipse.lsp4j.debug.ThreadEventArgumentsReason 22 | private typealias InternalSource = org.javacs.ktda.core.Source 23 | private typealias InternalSourceBreakpoint = org.javacs.ktda.core.breakpoint.SourceBreakpoint 24 | private typealias InternalExceptionBreakpoint = org.javacs.ktda.core.breakpoint.ExceptionBreakpoint 25 | private typealias InternalBreakpoint = org.javacs.ktda.core.breakpoint.Breakpoint 26 | private typealias InternalStackFrame = org.javacs.ktda.core.stack.StackFrame 27 | private typealias InternalCompletionItem = org.javacs.ktda.core.completion.CompletionItem 28 | private typealias InternalCompletionItemType = org.javacs.ktda.core.completion.CompletionItemType 29 | private typealias InternalException = org.javacs.ktda.core.exception.DebuggeeException 30 | private typealias InternalThreadEventReason = org.javacs.ktda.core.event.ThreadEventReason 31 | 32 | /** 33 | * Handles conversions between debug adapter types 34 | * and internal types. This includes caching values 35 | * using ObjectPools and ids. 36 | */ 37 | class DAPConverter( 38 | var lineConverter: LineNumberConverter = LineNumberConverter(), 39 | var columnConverter: LineNumberConverter = LineNumberConverter() 40 | ) { 41 | val stackFramePool = ObjectPool() // Contains stack frames owned by thread ids 42 | val variablesPool = ObjectPool() // Contains unowned variable trees (the ids are used as 'variables references') 43 | 44 | fun toInternalSource(dapSource: DAPSource) = InternalSource( 45 | name = dapSource.name, 46 | filePath = Paths.get(dapSource.path) 47 | ) 48 | 49 | fun toDAPSource(internalSource: InternalSource) = DAPSource().apply { 50 | name = internalSource.name 51 | path = internalSource.filePath.toAbsolutePath().toString() 52 | } 53 | 54 | fun toInternalSourceBreakpoint(dapSource: DAPSource, dapSrcBreakpoint: DAPSourceBreakpoint) = InternalSourceBreakpoint( 55 | position = Position( 56 | source = toInternalSource(dapSource), 57 | lineNumber = lineConverter.toInternalLine(dapSrcBreakpoint.line) 58 | ) 59 | ) 60 | 61 | fun toInternalExceptionBreakpoint(filterID: String) = InternalExceptionBreakpoint 62 | .values() 63 | .find { it.id == filterID } 64 | ?: throw KotlinDAException("${filterID} is not a valid exception breakpoint") 65 | 66 | fun toDAPExceptionBreakpointsFilter(internalBreakpoint: InternalExceptionBreakpoint) = DAPExceptionBreakpointsFilter().apply { 67 | filter = internalBreakpoint.id 68 | label = internalBreakpoint.label 69 | default_ = false 70 | } 71 | 72 | fun toDAPBreakpoint(internalBreakpoint: InternalBreakpoint) = DAPBreakpoint().apply { 73 | source = toDAPSource(internalBreakpoint.position.source) 74 | line = lineConverter.toExternalLine(internalBreakpoint.position.lineNumber) 75 | isVerified = true 76 | } 77 | 78 | fun toDAPBreakpoint(internalBreakpoint: InternalExceptionBreakpoint) = DAPBreakpoint().apply { 79 | id = internalBreakpoint.id.toInt() 80 | message = internalBreakpoint.label 81 | isVerified = true 82 | } 83 | 84 | fun toInternalStackFrame(frameId: Long) = stackFramePool.getByID(frameId) 85 | 86 | fun toDAPStackFrame(internalFrame: InternalStackFrame, threadId: Long) = DAPStackFrame().apply { 87 | id = stackFramePool.store(threadId, internalFrame).toInt() 88 | name = internalFrame.name 89 | line = internalFrame.position?.lineNumber?.let(lineConverter::toExternalLine) ?: 0 90 | column = (internalFrame.position?.columnNumber ?: 1).let(columnConverter::toExternalLine) 91 | source = internalFrame.position?.source?.let(::toDAPSource) 92 | } 93 | 94 | fun toDAPScope(variableTree: VariableTreeNode) = DAPScope().apply { 95 | name = variableTree.name 96 | variablesReference = variablesPool.store(Unit, variableTree).toInt() 97 | isExpensive = false 98 | } 99 | 100 | fun toVariableTree(variablesReference: Long) = variablesPool.getByID(variablesReference) 101 | 102 | fun toDAPVariable(variableTree: VariableTreeNode) = DAPVariable().apply { 103 | name = variableTree.name 104 | value = variableTree.value 105 | type = variableTree.type 106 | variablesReference = (variableTree.childs?.takeIf { it.isNotEmpty() }?.let { variablesPool.store(Unit, variableTree) } ?: 0).toInt() 107 | } 108 | 109 | fun toDAPThread(internalThread: DebuggeeThread) = DAPThread().apply { 110 | name = internalThread.name 111 | id = internalThread.id.toInt() 112 | } 113 | 114 | fun toDAPCompletionItem(internalItem: InternalCompletionItem) = DAPCompletionItem().apply { 115 | label = internalItem.label 116 | type = toDAPCompletionItemType(internalItem.type) 117 | } 118 | 119 | fun toDAPCompletionItemType(internalType: InternalCompletionItemType) = when (internalType) { 120 | InternalCompletionItemType.METHOD -> DAPCompletionItemType.METHOD 121 | InternalCompletionItemType.FUNCTION -> DAPCompletionItemType.FUNCTION 122 | InternalCompletionItemType.CONSTRUCTOR -> DAPCompletionItemType.CONSTRUCTOR 123 | InternalCompletionItemType.FIELD -> DAPCompletionItemType.FIELD 124 | InternalCompletionItemType.VARIABLE -> DAPCompletionItemType.VARIABLE 125 | InternalCompletionItemType.CLASS -> DAPCompletionItemType.CLASS 126 | InternalCompletionItemType.INTERFACE -> DAPCompletionItemType.INTERFACE 127 | InternalCompletionItemType.MODULE -> DAPCompletionItemType.MODULE 128 | InternalCompletionItemType.PROPERTY -> DAPCompletionItemType.PROPERTY 129 | InternalCompletionItemType.UNIT -> DAPCompletionItemType.UNIT 130 | InternalCompletionItemType.VALUE -> DAPCompletionItemType.VALUE 131 | InternalCompletionItemType.ENUM -> DAPCompletionItemType.ENUM 132 | InternalCompletionItemType.KEYWORD -> DAPCompletionItemType.KEYWORD 133 | InternalCompletionItemType.SNIPPET -> DAPCompletionItemType.SNIPPET 134 | InternalCompletionItemType.TEXT -> DAPCompletionItemType.TEXT 135 | InternalCompletionItemType.COLOR -> DAPCompletionItemType.COLOR 136 | InternalCompletionItemType.FILE -> DAPCompletionItemType.FILE 137 | InternalCompletionItemType.REFERENCE -> DAPCompletionItemType.REFERENCE 138 | InternalCompletionItemType.CUSTOMCOLOR -> DAPCompletionItemType.CUSTOMCOLOR 139 | } 140 | 141 | fun toDAPExceptionDetails(internalException: InternalException): DAPExceptionDetails = DAPExceptionDetails().apply { 142 | message = internalException.message 143 | typeName = internalException.typeName 144 | fullTypeName = internalException.fullTypeName 145 | stackTrace = internalException.stackTrace 146 | innerException = internalException.innerException?.let(::toDAPExceptionDetails)?.let { arrayOf(it) } 147 | } 148 | 149 | fun toDAPThreadEventReason(reason: InternalThreadEventReason): String = when (reason) { 150 | InternalThreadEventReason.STARTED -> DAPThreadEventReason.STARTED 151 | InternalThreadEventReason.STOPPED -> DAPThreadEventReason.EXITED 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.adapter 2 | 3 | import java.util.concurrent.CompletableFuture 4 | import java.util.concurrent.CompletableFuture.completedFuture 5 | import java.io.InputStream 6 | import java.io.File 7 | import java.nio.file.Path 8 | import java.nio.file.Paths 9 | import java.util.concurrent.ThreadLocalRandom 10 | import org.eclipse.lsp4j.debug.* 11 | import org.eclipse.lsp4j.jsonrpc.services.JsonRequest 12 | import org.eclipse.lsp4j.debug.services.IDebugProtocolServer 13 | import org.eclipse.lsp4j.debug.services.IDebugProtocolClient 14 | import org.javacs.kt.LOG 15 | import org.javacs.kt.LogLevel 16 | import org.javacs.kt.LogMessage 17 | import org.javacs.kt.util.AsyncExecutor 18 | import org.javacs.ktda.util.JSON_LOG 19 | import org.javacs.ktda.util.KotlinDAException 20 | import org.javacs.ktda.util.ObjectPool 21 | import org.javacs.ktda.util.waitFor 22 | import org.javacs.ktda.core.Debuggee 23 | import org.javacs.ktda.core.DebugContext 24 | import org.javacs.ktda.core.exception.DebuggeeException 25 | import org.javacs.ktda.core.event.DebuggeeEventBus 26 | import org.javacs.ktda.core.event.BreakpointStopEvent 27 | import org.javacs.ktda.core.event.StepStopEvent 28 | import org.javacs.ktda.core.stack.StackFrame 29 | import org.javacs.ktda.core.launch.DebugLauncher 30 | import org.javacs.ktda.core.launch.LaunchConfiguration 31 | import org.javacs.ktda.core.launch.AttachConfiguration 32 | import org.javacs.ktda.core.breakpoint.ExceptionBreakpoint 33 | import org.javacs.ktda.classpath.debugClassPathResolver 34 | import org.javacs.ktda.classpath.findValidKtFilePath 35 | 36 | /** The debug server interface conforming to the Debug Adapter Protocol */ 37 | class KotlinDebugAdapter( 38 | private val launcher: DebugLauncher 39 | ) : IDebugProtocolServer { 40 | private val async = AsyncExecutor() 41 | private val launcherAsync = AsyncExecutor() 42 | private val stdoutAsync = AsyncExecutor() 43 | private val stderrAsync = AsyncExecutor() 44 | 45 | private var debuggee: Debuggee? = null 46 | private var client: IDebugProtocolClient? = null 47 | private var converter = DAPConverter() 48 | private val context = DebugContext() 49 | 50 | private val exceptionsPool = ObjectPool() // Contains exceptions thrown by the debuggee owned by thread ids 51 | 52 | // TODO: This is a workaround for https://github.com/eclipse/lsp4j/issues/229 53 | // For more information, see launch() method 54 | private var configurationDoneResponse: CompletableFuture? = null 55 | 56 | override fun initialize(args: InitializeRequestArguments): CompletableFuture = async.compute { 57 | converter.lineConverter = LineNumberConverter( 58 | externalLineOffset = if (args.linesStartAt1) 0 else -1 59 | ) 60 | converter.columnConverter = LineNumberConverter( 61 | externalLineOffset = if (args.columnsStartAt1) 0 else -1 62 | ) 63 | 64 | val capabilities = Capabilities() 65 | capabilities.supportsConfigurationDoneRequest = true 66 | capabilities.supportsCompletionsRequest = true 67 | capabilities.supportsExceptionInfoRequest = true 68 | capabilities.exceptionBreakpointFilters = ExceptionBreakpoint.values() 69 | .map(converter::toDAPExceptionBreakpointsFilter) 70 | .toTypedArray() 71 | 72 | LOG.trace("Returning capabilities...") 73 | capabilities 74 | } 75 | 76 | fun connect(client: IDebugProtocolClient) { 77 | connectLoggingBackend(client) 78 | this.client = client 79 | client.initialized() 80 | LOG.info("Connected to client") 81 | } 82 | 83 | override fun configurationDone(args: ConfigurationDoneArguments?): CompletableFuture { 84 | LOG.trace("Got configurationDone request") 85 | val response = CompletableFuture() 86 | configurationDoneResponse = response 87 | return response 88 | } 89 | 90 | override fun launch(args: Map) = launcherAsync.execute { 91 | performInitialization() 92 | 93 | val projectRoot = (args["projectRoot"] as? String)?.let { Paths.get(it) } 94 | ?: throw missingRequestArgument("launch", "projectRoot") 95 | 96 | val mainClass = (args["mainClass"] as? String) 97 | ?: throw missingRequestArgument("launch", "mainClass") 98 | 99 | val vmArguments = (args["vmArguments"] as? String) ?: "" 100 | 101 | setupCommonInitializationParams(args) 102 | 103 | val config = LaunchConfiguration( 104 | debugClassPathResolver(listOf(projectRoot)).classpathOrEmpty.map { it.compiledJar }.toSet(), 105 | mainClass, 106 | projectRoot, 107 | vmArguments 108 | ) 109 | debuggee = launcher.launch( 110 | config, 111 | context 112 | ).also(::setupDebuggeeListeners) 113 | LOG.trace("Instantiated debuggee") 114 | } 115 | 116 | private fun missingRequestArgument(requestName: String, argumentName: String) = 117 | KotlinDAException("Sent $requestName to debug adapter without the required argument'$argumentName'") 118 | 119 | private fun performInitialization() { 120 | client!!.initialized() 121 | 122 | // Wait for configurationDone response to fully return 123 | // as sketched in https://github.com/Microsoft/vscode/issues/4902#issuecomment-368583522 124 | // TODO: Find a cleaner solution once https://github.com/eclipse/lsp4j/issues/229 is resolved 125 | // (LSP4J does currently not provide a mechanism to hook into the request/response machinery) 126 | 127 | LOG.trace("Waiting for configurationDoneResponse") 128 | waitFor("configuration done response") { (configurationDoneResponse?.numberOfDependents ?: 0) != 0 } 129 | LOG.trace("Done waiting for configurationDoneResponse") 130 | } 131 | 132 | private fun setupDebuggeeListeners(debuggee: Debuggee) { 133 | val eventBus = debuggee.eventBus 134 | eventBus.exitListeners.add { 135 | // TODO: Use actual exitCode instead 136 | sendExitEvent(0) 137 | } 138 | eventBus.breakpointListeners.add { 139 | sendStopEvent(it.threadID, StoppedEventArgumentsReason.BREAKPOINT) 140 | } 141 | eventBus.stepListeners.add { 142 | sendStopEvent(it.threadID, StoppedEventArgumentsReason.STEP) 143 | } 144 | eventBus.exceptionListeners.add { 145 | exceptionsPool.store(it.threadID, it.exception) 146 | sendStopEvent(it.threadID, StoppedEventArgumentsReason.EXCEPTION) 147 | } 148 | eventBus.threadListeners.add { 149 | sendThreadEvent(it.threadID, converter.toDAPThreadEventReason(it.reason)) 150 | } 151 | stdoutAsync.execute { 152 | debuggee.stdout?.let { pipeStreamToOutput(it, OutputEventArgumentsCategory.STDOUT) } 153 | } 154 | stderrAsync.execute { 155 | debuggee.stderr?.let { pipeStreamToOutput(it, OutputEventArgumentsCategory.STDERR) } 156 | } 157 | LOG.trace("Configured debuggee listeners") 158 | } 159 | 160 | private fun pipeStreamToOutput(stream: InputStream, outputCategory: String) { 161 | stream.bufferedReader().use { 162 | var line = it.readLine() 163 | while (line != null) { 164 | client?.output(OutputEventArguments().apply { 165 | category = outputCategory 166 | output = line + System.lineSeparator() 167 | }) 168 | line = it.readLine() 169 | } 170 | } 171 | } 172 | 173 | private fun sendThreadEvent(threadId: Long, reason: String) { 174 | client!!.thread(ThreadEventArguments().also { 175 | it.reason = reason 176 | it.threadId = threadId.toInt() 177 | }) 178 | } 179 | 180 | private fun sendStopEvent(threadId: Long, reason: String) { 181 | client!!.stopped(StoppedEventArguments().also { 182 | it.reason = reason 183 | it.threadId = threadId.toInt() 184 | }) 185 | } 186 | 187 | private fun sendExitEvent(exitCode: Long) { 188 | client!!.exited(ExitedEventArguments().also { 189 | it.exitCode = exitCode.toInt() 190 | }) 191 | client!!.terminated(TerminatedEventArguments()) 192 | LOG.info("Sent exit event") 193 | } 194 | 195 | override fun attach(args: Map) = launcherAsync.execute { 196 | performInitialization() 197 | 198 | val projectRoot = (args["projectRoot"] as? String)?.let { Paths.get(it) } 199 | ?: throw missingRequestArgument("attach", "projectRoot") 200 | 201 | val hostName = (args["hostName"] as? String) 202 | ?: throw missingRequestArgument("attach", "hostName") 203 | 204 | val port = (args["port"] as? Double)?.toInt() 205 | ?: throw missingRequestArgument("attach", "port") 206 | 207 | val timeout = (args["timeout"] as? Double)?.toInt() 208 | ?: throw missingRequestArgument("attach", "timeout") 209 | 210 | setupCommonInitializationParams(args) 211 | 212 | debuggee = launcher.attach( 213 | AttachConfiguration(projectRoot, hostName, port, timeout), 214 | context 215 | ).also(::setupDebuggeeListeners) 216 | 217 | // Since we are attaching to a running VM, we have to send custom 218 | // 'start' events for all executing threads 219 | for (thread in debuggee!!.threads) { 220 | sendThreadEvent(thread.id, ThreadEventArgumentsReason.STARTED) 221 | } 222 | } 223 | 224 | private fun setupCommonInitializationParams(args: Map) { 225 | val logLevel = (args["logLevel"] as? String)?.let(LogLevel::valueOf) 226 | ?: LogLevel.INFO 227 | 228 | LOG.level = logLevel 229 | 230 | connectJsonLoggingBackend(args) 231 | } 232 | 233 | private fun connectJsonLoggingBackend(args: Map) { 234 | val enableJsonLogging = (args["enableJsonLogging"] as? Boolean) ?: false 235 | 236 | if (enableJsonLogging) { 237 | val jsonLogFile = (args["jsonLogFile"] as? String)?.let(::File) 238 | ?: throw missingRequestArgument("launch/attach", "jsonLogFile") 239 | val newline = System.lineSeparator() 240 | 241 | if (!jsonLogFile.exists()) { 242 | jsonLogFile.createNewFile() 243 | } 244 | 245 | JSON_LOG.connectOutputBackend { msg -> jsonLogFile.appendText("[${msg.level}] ${msg.message}$newline") } 246 | JSON_LOG.connectErrorBackend { msg -> jsonLogFile.appendText("Error: [${msg.level}] ${msg.message}$newline") } 247 | } 248 | } 249 | 250 | override fun restart(args: RestartArguments): CompletableFuture = notImplementedDAPMethod() 251 | 252 | override fun disconnect(args: DisconnectArguments) = async.execute { 253 | debuggee?.exit() 254 | } 255 | 256 | override fun setBreakpoints(args: SetBreakpointsArguments) = async.compute { 257 | LOG.debug("{} breakpoints found", args.breakpoints.size) 258 | 259 | // TODO: Support logpoints and conditional breakpoints 260 | 261 | val placedBreakpoints = context 262 | .breakpointManager 263 | .setAllIn( 264 | converter.toInternalSource(args.source), 265 | args.breakpoints.map { converter.toInternalSourceBreakpoint(args.source, it) } 266 | ) 267 | .map(converter::toDAPBreakpoint) 268 | .toTypedArray() 269 | 270 | SetBreakpointsResponse().apply { 271 | breakpoints = placedBreakpoints 272 | } 273 | } 274 | 275 | override fun setFunctionBreakpoints(args: SetFunctionBreakpointsArguments): CompletableFuture = notImplementedDAPMethod() 276 | 277 | override fun setExceptionBreakpoints(args: SetExceptionBreakpointsArguments) = async.compute { 278 | val internalBreakpoints = args.filters 279 | .map(converter::toInternalExceptionBreakpoint) 280 | .toSet() 281 | internalBreakpoints.let(context.breakpointManager.exceptionBreakpoints::setAll) 282 | 283 | SetExceptionBreakpointsResponse().apply { 284 | breakpoints = internalBreakpoints.map(converter::toDAPBreakpoint).toTypedArray() 285 | } 286 | } 287 | 288 | override fun continue_(args: ContinueArguments) = async.compute { 289 | var success = debuggee!!.threadByID(args.threadId.toLong())?.resume() ?: false 290 | var allThreads = false 291 | 292 | if (!success) { 293 | debuggee!!.resume() 294 | success = true 295 | allThreads = true 296 | } 297 | 298 | if (success) { 299 | exceptionsPool.clear() 300 | converter.variablesPool.clear() 301 | converter.stackFramePool.removeAllOwnedBy(args.threadId.toLong()) 302 | } 303 | 304 | ContinueResponse().apply { 305 | allThreadsContinued = allThreads 306 | } 307 | } 308 | 309 | override fun next(args: NextArguments) = async.execute { 310 | debuggee!!.threadByID(args.threadId.toLong())?.stepOver() 311 | } 312 | 313 | override fun stepIn(args: StepInArguments) = async.execute { 314 | debuggee!!.threadByID(args.threadId.toLong())?.stepInto() 315 | } 316 | 317 | override fun stepOut(args: StepOutArguments) = async.execute { 318 | debuggee!!.threadByID(args.threadId.toLong())?.stepOut() 319 | } 320 | 321 | override fun stepBack(args: StepBackArguments): CompletableFuture = notImplementedDAPMethod() 322 | 323 | override fun reverseContinue(args: ReverseContinueArguments): CompletableFuture = notImplementedDAPMethod() 324 | 325 | override fun restartFrame(args: RestartFrameArguments): CompletableFuture = notImplementedDAPMethod() 326 | 327 | override fun goto_(args: GotoArguments): CompletableFuture = notImplementedDAPMethod() 328 | 329 | override fun pause(args: PauseArguments) = async.execute { 330 | val threadId = args.threadId 331 | val success = debuggee!!.threadByID(threadId.toLong())?.pause() 332 | if (success ?: false) { 333 | // If successful 334 | sendStopEvent(threadId.toLong(), 335 | StoppedEventArgumentsReason.PAUSE 336 | ) 337 | } 338 | } 339 | 340 | /* 341 | * Stack traces, scopes and variables are computed synchronously 342 | * to avoid race conditions when fetching elements from the pools 343 | */ 344 | 345 | override fun stackTrace(args: StackTraceArguments): CompletableFuture { 346 | val threadId = args.threadId 347 | return completedFuture(StackTraceResponse().apply { 348 | stackFrames = debuggee!! 349 | .threadByID(threadId.toLong()) 350 | ?.stackTrace() 351 | ?.frames 352 | ?.map { converter.toDAPStackFrame(it, threadId.toLong()) } 353 | ?.toTypedArray() 354 | .orEmpty() 355 | }) 356 | } 357 | 358 | override fun scopes(args: ScopesArguments) = completedFuture( 359 | ScopesResponse().apply { 360 | scopes = (converter.toInternalStackFrame(args.frameId.toLong()) 361 | ?: throw KotlinDAException("Could not find stackTrace with ID ${args.frameId}")) 362 | .scopes 363 | .map(converter::toDAPScope) 364 | .toTypedArray() 365 | } 366 | ) 367 | 368 | override fun variables(args: VariablesArguments) = completedFuture( 369 | VariablesResponse().apply { 370 | variables = (args.variablesReference 371 | .toLong() 372 | .let(converter::toVariableTree) 373 | ?: throw KotlinDAException("Could not find variablesReference with ID ${args.variablesReference}")) 374 | .childs 375 | ?.map(converter::toDAPVariable) 376 | ?.toTypedArray() 377 | .orEmpty() 378 | } 379 | ) 380 | 381 | override fun setVariable(args: SetVariableArguments): CompletableFuture = notImplementedDAPMethod() 382 | 383 | override fun source(args: SourceArguments): CompletableFuture = notImplementedDAPMethod() 384 | 385 | override fun threads() = async.compute { onceDebuggeeIsPresent { debuggee -> 386 | debuggee.updateThreads() 387 | ThreadsResponse().apply { 388 | threads = debuggee.threads 389 | .asSequence() 390 | .map(converter::toDAPThread) 391 | .toList() 392 | .toTypedArray() 393 | } 394 | } } 395 | 396 | override fun modules(args: ModulesArguments): CompletableFuture = notImplementedDAPMethod() 397 | 398 | override fun loadedSources(args: LoadedSourcesArguments): CompletableFuture = notImplementedDAPMethod() 399 | 400 | override fun evaluate(args: EvaluateArguments): CompletableFuture = async.compute { 401 | val variable = (args.frameId 402 | .toLong() 403 | .let(converter::toInternalStackFrame) 404 | ?: throw KotlinDAException("Could not find stack frame with ID ${args.frameId}")) 405 | .evaluate(args.expression) 406 | ?.let(converter::toDAPVariable) 407 | 408 | EvaluateResponse().apply { 409 | result = variable?.value ?: "unknown" 410 | variablesReference = variable?.variablesReference ?: 0 411 | } 412 | } 413 | 414 | override fun stepInTargets(args: StepInTargetsArguments): CompletableFuture = notImplementedDAPMethod() 415 | 416 | override fun gotoTargets(args: GotoTargetsArguments): CompletableFuture = notImplementedDAPMethod() 417 | 418 | override fun completions(args: CompletionsArguments): CompletableFuture = async.compute { 419 | CompletionsResponse().apply { 420 | targets = (args.frameId 421 | .toLong() 422 | .let(converter::toInternalStackFrame) 423 | ?: throw KotlinDAException("Could not find stack frame with ID ${args.frameId}")) 424 | .completions(args.text) 425 | .map(converter::toDAPCompletionItem) 426 | .toTypedArray() 427 | } 428 | } 429 | 430 | override fun exceptionInfo(args: ExceptionInfoArguments): CompletableFuture = async.compute { 431 | val id = exceptionsPool.getIDsOwnedBy(args.threadId.toLong()).firstOrNull() 432 | val exception = id?.let { exceptionsPool.getByID(it) } 433 | ExceptionInfoResponse().apply { 434 | exceptionId = id?.toString() ?: "" 435 | description = exception?.description ?: "Unknown exception" 436 | breakMode = ExceptionBreakMode.ALWAYS 437 | details = exception?.let(converter::toDAPExceptionDetails) 438 | } 439 | } 440 | 441 | private fun connectLoggingBackend(client: IDebugProtocolClient) { 442 | val backend: (LogMessage) -> Unit = { 443 | client.output(OutputEventArguments().apply { 444 | category = OutputEventArgumentsCategory.CONSOLE 445 | output = "[${it.level}] ${it.message}\n" 446 | }) 447 | } 448 | LOG.connectOutputBackend(backend) 449 | LOG.connectErrorBackend(backend) 450 | } 451 | 452 | private inline fun onceDebuggeeIsPresent(body: (Debuggee) -> T): T { 453 | waitFor("debuggee") { debuggee != null } 454 | return body(debuggee!!) 455 | } 456 | 457 | private fun notImplementedDAPMethod(): CompletableFuture { 458 | TODO("not implemented yet") 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/adapter/LineNumberConverter.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.adapter 2 | 3 | /** 4 | * Converts between external and internal line numbering. 5 | * For example: 6 | * 7 | * An "external" (debug adapter) line number could be zero-indexed and 8 | * an "internal" (core) line number would be one-indexed. 9 | * 10 | * In this case, externalLineOffset would be -1. 11 | */ 12 | class LineNumberConverter( 13 | private val externalLineOffset: Int = 0 // Internal line + externalLineOffset = External line 14 | ) { 15 | fun toInternalLine(lineNumber: Int) = lineNumber - externalLineOffset 16 | 17 | fun toExternalLine(lineNumber: Int) = lineNumber + externalLineOffset 18 | } 19 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/classpath/DebugClassPathResolver.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.classpath 2 | 3 | import org.javacs.kt.classpath.ClassPathResolver 4 | import org.javacs.kt.classpath.defaultClassPathResolver 5 | import org.javacs.kt.classpath.plus 6 | import org.javacs.kt.classpath.joined 7 | import java.nio.file.Path 8 | 9 | fun debugClassPathResolver(workspaceRoots: Collection): ClassPathResolver = 10 | defaultClassPathResolver(workspaceRoots) + workspaceRoots.map { ProjectClassesResolver(it) }.joined 11 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/classpath/PathUtils.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.classpath 2 | 3 | import org.javacs.kt.LOG 4 | import org.javacs.ktda.util.firstNonNull 5 | import java.nio.file.Files 6 | import java.nio.file.Paths 7 | import java.nio.file.Path 8 | 9 | private val fileSeparator by lazy { "[/\\\\]".toRegex() } 10 | private val sourceFileExtensions = setOf(".kt", ".kts", ".java") 11 | 12 | /** 13 | * Converts a file path to multiple possible JVM class names. 14 | * 15 | * For example: 16 | * 17 | * ".../src/main/kotlin/com/abc/MyClass.kt" will be converted to 18 | * [com.abc.MyClass, com.abc.MyClassKt] 19 | */ 20 | fun toJVMClassNames(filePath: String): List? { 21 | // TODO: Implement this using the Kotlin compiler API instead 22 | // See https://github.com/JetBrains/kotlin-netbeans/blob/c3360e8c89c1d4dac1e6f18267052ff740705079/src/main/java/org/jetbrains/kotlin/debugger/KotlinDebugUtils.java#L166-L194 23 | 24 | val rawClassName = filePath.split(fileSeparator) // TODO: Use Project.sourcesRoot instead 25 | .takeLastWhile { it != "kotlin" && it != "java" } // Assuming .../src/main/kotlin/... directory structure 26 | .joinToString(separator = ".") 27 | val className = sourceFileExtensions 28 | .asSequence() 29 | .find { filePath.endsWith(it) } 30 | ?.let { rawClassName.dropLast(it.length) } 31 | ?: return null 32 | val ktClassName = className 33 | .capitalizeCharAt(className.lastIndexOf(".") + 1) + "Kt" // Class name to PascalCase 34 | 35 | return listOf(className, ktClassName) 36 | } 37 | 38 | // TODO: Better path resolution, especially when dealing with 39 | // *.class files inside JARs 40 | fun findValidKtFilePath(filePathToClass: Path, sourceName: String?) = 41 | filePathToClass.resolveSibling(sourceName).ifExists() 42 | ?: filePathToClass.withExtension(".kt").ifExists() 43 | 44 | private fun Path.ifExists() = if (Files.exists(this)) this else null 45 | 46 | private fun Path.withExtension(extension: String) = resolveSibling(fileName.toString() + extension) 47 | 48 | private fun String.capitalizeCharAt(index: Int) = 49 | take(index) + this[index].uppercaseChar() + substring(index + 1) 50 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/classpath/ProjectClassesResolver.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.classpath 2 | 3 | import java.nio.file.Path 4 | import java.nio.file.Files 5 | import org.javacs.kt.classpath.ClassPathEntry 6 | import org.javacs.kt.classpath.ClassPathResolver 7 | 8 | /** Resolver for the project's own (compiled) class files. */ 9 | internal class ProjectClassesResolver(private val projectRoot: Path) : ClassPathResolver { 10 | override val resolverType: String = "Project classes" 11 | override val classpath: Set get() = sequenceOf( 12 | // Gradle 13 | sequenceOf("kotlin", "java").flatMap { language -> 14 | sequenceOf("main", "test").flatMap { sourceSet -> 15 | sequenceOf( 16 | resolveIfExists(projectRoot, "build", "classes", language, sourceSet), 17 | // kotlin multiplatform project jvm build path 18 | resolveIfExists(projectRoot, "build", "classes", language, "jvm", sourceSet) 19 | ) 20 | } 21 | }, 22 | // Maven 23 | sequenceOf(resolveIfExists(projectRoot, "target", "classes")), 24 | sequenceOf(resolveIfExists(projectRoot, "target", "test-classes")), 25 | // Spring Boot application.properties and templates. 26 | sequenceOf(resolveIfExists(projectRoot, "build", "resources", "main")) 27 | ).flatten().filterNotNull().map(::ClassPathEntry).toSet() 28 | } 29 | 30 | /** Joins the segments to a path and returns it if it exists or null otherwise. */ 31 | private fun resolveIfExists(root: Path, vararg segments: String): Path? { 32 | var result = root 33 | for (segment in segments) { 34 | result = result.resolve(segment) 35 | } 36 | return result.takeIf { Files.exists(it) } 37 | } 38 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/DebugContext.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core 2 | 3 | import org.javacs.ktda.core.breakpoint.BreakpointManager 4 | 5 | class DebugContext( 6 | val breakpointManager: BreakpointManager = BreakpointManager() 7 | ) 8 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/Debuggee.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core 2 | 3 | import org.javacs.ktda.core.event.DebuggeeEventBus 4 | import java.io.InputStream 5 | import java.io.OutputStream 6 | 7 | /** A debuggee that is launched upon construction */ 8 | interface Debuggee { 9 | val threads: List 10 | val eventBus: DebuggeeEventBus 11 | val stdin: OutputStream? 12 | get() = null 13 | val stdout: InputStream? 14 | get() = null 15 | val stderr: InputStream? 16 | get() = null 17 | 18 | fun exit() 19 | 20 | fun resume() 21 | 22 | fun updateThreads() 23 | 24 | fun threadByID(id: Long): DebuggeeThread? = threads 25 | .asSequence() 26 | .filter { it.id == id } 27 | .firstOrNull() 28 | } 29 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/DebuggeeThread.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core 2 | 3 | import org.javacs.ktda.core.stack.StackTrace 4 | 5 | interface DebuggeeThread { 6 | val name: String 7 | val id: Long 8 | 9 | /** Tries to pause the thread, returning whether the operation was successful or not */ 10 | fun pause(): Boolean 11 | 12 | /** Tries to resume the thread, returning whether the operation was successful or not */ 13 | fun resume(): Boolean 14 | 15 | fun stepOver() 16 | 17 | fun stepInto() 18 | 19 | fun stepOut() 20 | 21 | fun stackTrace(): StackTrace 22 | } 23 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/Position.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core 2 | 3 | /** A source code position. Line and column numbers are 1-indexed */ 4 | class Position( 5 | val source: Source, 6 | val lineNumber: Int, 7 | val columnNumber: Int? = null 8 | ) 9 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/Source.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core 2 | 3 | import java.nio.file.Path 4 | 5 | /** A source unit descriptor (usually a file) */ 6 | data class Source( 7 | val name: String, 8 | val filePath: Path 9 | ) 10 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/breakpoint/Breakpoint.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.breakpoint 2 | 3 | import org.javacs.ktda.core.Position 4 | 5 | /** An actual breakpoint */ 6 | class Breakpoint( 7 | val position: Position 8 | ) 9 | 10 | // TODO: Conditional breakpoints and logpoints 11 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/breakpoint/BreakpointManager.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.breakpoint 2 | 3 | import org.javacs.ktda.core.Source 4 | import org.javacs.ktda.util.ObservableSet 5 | import org.javacs.ktda.util.ObservableMap 6 | 7 | class BreakpointManager { 8 | val breakpoints = ObservableMap>() 9 | val exceptionBreakpoints = ObservableSet() 10 | 11 | /** Attempts to place breakpoints in a source and returns the successfully placed ones */ 12 | fun setAllIn(source: Source, sourceBreakpoints: List): List { 13 | val actualBreakpoints = sourceBreakpoints.mapNotNull { it.toActualBreakpoint() } 14 | breakpoints[source] = actualBreakpoints 15 | return actualBreakpoints 16 | } 17 | 18 | // TODO: Validation logic 19 | private fun SourceBreakpoint.toActualBreakpoint(): Breakpoint? = Breakpoint(position) 20 | } 21 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/breakpoint/ExceptionBreakpoint.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.breakpoint 2 | 3 | enum class ExceptionBreakpoint( 4 | val id: String, 5 | val label: String 6 | ) { 7 | CAUGHT("C", "Caught Exceptions"), 8 | UNCAUGHT("U", "Uncaught Exceptions") 9 | } 10 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/breakpoint/SourceBreakpoint.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.breakpoint 2 | 3 | import org.javacs.ktda.core.Position 4 | 5 | /** An unverified breakpoint */ 6 | class SourceBreakpoint( 7 | val position: Position 8 | ) 9 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/completion/CompletionItem.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.completion 2 | 3 | data class CompletionItem( 4 | val label: String, 5 | val type: CompletionItemType 6 | ) 7 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/completion/CompletionItemType.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.completion 2 | 3 | enum class CompletionItemType { 4 | METHOD, 5 | FUNCTION, 6 | CONSTRUCTOR, 7 | FIELD, 8 | VARIABLE, 9 | CLASS, 10 | INTERFACE, 11 | MODULE, 12 | PROPERTY, 13 | UNIT, 14 | VALUE, 15 | ENUM, 16 | KEYWORD, 17 | SNIPPET, 18 | TEXT, 19 | COLOR, 20 | FILE, 21 | REFERENCE, 22 | CUSTOMCOLOR 23 | } 24 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/event/BreakpointStopEvent.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.event 2 | 3 | class BreakpointStopEvent( 4 | val threadID: Long 5 | ) 6 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/event/DebuggeeEventBus.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.event 2 | 3 | import org.javacs.ktda.util.ListenerList 4 | 5 | interface DebuggeeEventBus { 6 | val exitListeners: ListenerList 7 | val breakpointListeners: ListenerList 8 | val stepListeners: ListenerList 9 | val exceptionListeners: ListenerList 10 | val threadListeners: ListenerList 11 | } 12 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/event/ExceptionStopEvent.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.event 2 | 3 | import org.javacs.ktda.core.exception.DebuggeeException 4 | 5 | class ExceptionStopEvent( 6 | val threadID: Long, 7 | val exception: DebuggeeException 8 | ) 9 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/event/ExitEvent.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.event 2 | 3 | object ExitEvent 4 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/event/StepStopEvent.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.event 2 | 3 | class StepStopEvent( 4 | val threadID: Long 5 | ) 6 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/event/ThreadEvent.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.event 2 | 3 | class ThreadEvent( 4 | val threadID: Long, 5 | val reason: ThreadEventReason 6 | ) 7 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/event/ThreadEventReason.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.event 2 | 3 | enum class ThreadEventReason { 4 | STARTED, 5 | STOPPED 6 | } 7 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/exception/DebuggeeException.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.exception 2 | 3 | interface DebuggeeException { 4 | val description: String 5 | val message: String? 6 | get() = null 7 | val typeName: String? 8 | get() = null 9 | val fullTypeName: String? 10 | get() = null 11 | val stackTrace: String? 12 | get() = null 13 | val innerException: DebuggeeException? 14 | get() = null 15 | } 16 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/launch/AttachConfiguration.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.launch 2 | 3 | import java.nio.file.Path 4 | 5 | class AttachConfiguration( 6 | val projectRoot: Path, 7 | val hostName: String, 8 | val port: Int, 9 | val timeout: Int 10 | ) 11 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/launch/DebugLauncher.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.launch 2 | 3 | import org.javacs.ktda.core.Debuggee 4 | import org.javacs.ktda.core.DebugContext 5 | 6 | interface DebugLauncher { 7 | fun launch(config: LaunchConfiguration, context: DebugContext): Debuggee 8 | 9 | fun attach(config: AttachConfiguration, context: DebugContext): Debuggee 10 | } 11 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.launch 2 | 3 | import java.nio.file.Path 4 | 5 | class LaunchConfiguration( 6 | val classpath: Set, 7 | val mainClass: String, 8 | val projectRoot: Path, 9 | val vmArguments: String = "" 10 | ) 11 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/scope/BasicVariableTreeNode.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.scope 2 | 3 | data class BasicVariableTreeNode( 4 | override val name: String, 5 | override val value: String? = null, 6 | override val type: String? = null, 7 | override val childs: List? = null 8 | ) : VariableTreeNode 9 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/scope/VariableTreeNode.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.scope 2 | 3 | import org.javacs.ktda.util.Identifiable 4 | 5 | /** 6 | * A descriptor for a collection of child variables. 7 | * (usually a scope or a variable's fields) 8 | */ 9 | interface VariableTreeNode : Identifiable { 10 | val name: String 11 | val value: String? 12 | get() = null 13 | val type: String? 14 | get() = null 15 | val childs: List? 16 | get() = null 17 | 18 | // TODO: Setters for values? 19 | } 20 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/stack/StackFrame.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.stack 2 | 3 | import org.javacs.ktda.core.Position 4 | import org.javacs.ktda.core.completion.CompletionItem 5 | import org.javacs.ktda.core.scope.VariableTreeNode 6 | 7 | interface StackFrame { 8 | val name: String 9 | val position: Position? 10 | val scopes: List 11 | 12 | fun evaluate(expression: String): VariableTreeNode? 13 | 14 | fun completions(expression: String): List 15 | } 16 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/core/stack/StackTrace.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.core.stack 2 | 3 | interface StackTrace { 4 | val frames: List 5 | } 6 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/JDIDebuggee.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi 2 | 3 | import org.javacs.ktda.core.Debuggee 4 | import org.javacs.ktda.core.DebuggeeThread 5 | import org.javacs.ktda.core.DebugContext 6 | import org.javacs.ktda.core.Position 7 | import org.javacs.ktda.core.Source 8 | import org.javacs.ktda.core.launch.LaunchConfiguration 9 | import org.javacs.ktda.core.event.DebuggeeEventBus 10 | import org.javacs.ktda.core.breakpoint.Breakpoint 11 | import org.javacs.ktda.core.breakpoint.ExceptionBreakpoint 12 | import org.javacs.kt.LOG 13 | import org.javacs.ktda.util.ObservableList 14 | import org.javacs.ktda.util.SubscriptionBag 15 | import org.javacs.ktda.classpath.findValidKtFilePath 16 | import org.javacs.ktda.classpath.toJVMClassNames 17 | import org.javacs.ktda.jdi.event.VMEventBus 18 | import com.sun.jdi.Location 19 | import com.sun.jdi.ReferenceType 20 | import com.sun.jdi.VirtualMachine 21 | import com.sun.jdi.VMDisconnectedException 22 | import com.sun.jdi.event.ClassPrepareEvent 23 | import com.sun.jdi.request.EventRequest 24 | import com.sun.jdi.AbsentInformationException 25 | import java.io.File 26 | import java.io.InputStream 27 | import java.io.OutputStream 28 | import java.nio.file.Path 29 | import java.nio.file.Paths 30 | import java.nio.charset.StandardCharsets 31 | 32 | class JDIDebuggee( 33 | private val vm: VirtualMachine, 34 | private val sourcesRoots: Set, 35 | private val context: DebugContext 36 | ) : Debuggee, JDISessionContext { 37 | override var threads = emptyList() 38 | override val eventBus: VMEventBus 39 | override val pendingStepRequestThreadIds = mutableSetOf() 40 | override val stdin: OutputStream? 41 | override val stdout: InputStream? 42 | override val stderr: InputStream? 43 | 44 | private var breakpointSubscriptions = SubscriptionBag() 45 | 46 | init { 47 | eventBus = VMEventBus(vm) 48 | 49 | val process = vm.process() 50 | stdin = process?.outputStream 51 | stdout = process?.inputStream 52 | stderr = process?.errorStream 53 | 54 | LOG.trace("Updating threads") 55 | updateThreads() 56 | 57 | LOG.trace("Updating breakpoints") 58 | hookBreakpoints() 59 | } 60 | 61 | override fun updateThreads() { 62 | threads = vm.allThreads().map { JDIThread(it, this) } 63 | } 64 | 65 | private fun hookBreakpoints() { 66 | context.breakpointManager.also { manager -> 67 | manager.breakpoints.listenAndFire { setAllBreakpoints(it.values.flatten()) } 68 | manager.exceptionBreakpoints.listenAndFire(::setExceptionBreakpoints) 69 | } 70 | } 71 | 72 | private fun setAllBreakpoints(breakpoints: List) { 73 | breakpointSubscriptions.unsubscribe() 74 | vm.eventRequestManager().deleteAllBreakpoints() 75 | breakpoints.forEach { bp -> 76 | bp.position.let { setBreakpoint( 77 | it.source.filePath.toAbsolutePath().toString(), 78 | it.lineNumber.toLong() 79 | ) } 80 | } 81 | } 82 | 83 | private fun setExceptionBreakpoints(breakpoints: Set) = vm 84 | .eventRequestManager() 85 | .also { it.deleteEventRequests(it.exceptionRequests()) } 86 | .takeIf { breakpoints.isNotEmpty() } 87 | // Workaround: JDI will otherwise not enable the request correctly 88 | ?.also { vm.allThreads() } 89 | ?.createExceptionRequest( 90 | null, 91 | breakpoints.contains(ExceptionBreakpoint.CAUGHT), 92 | breakpoints.contains(ExceptionBreakpoint.UNCAUGHT) 93 | ) 94 | ?.apply { setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD) } 95 | ?.enable() 96 | ?: Unit 97 | 98 | /** Tries to set a breakpoint */ 99 | private fun setBreakpoint(filePath: String, lineNumber: Long) { 100 | val eventRequestManager = vm.eventRequestManager() 101 | 102 | toJVMClassNames(filePath) 103 | ?.forEach { className -> 104 | // Try setting breakpoint using a ClassPrepareRequest 105 | 106 | for (name in listOf(className, "$className$*")) { // For local types 107 | val request = eventRequestManager 108 | .createClassPrepareRequest() 109 | .apply { addClassFilter(className) } 110 | 111 | breakpointSubscriptions.add(eventBus.subscribe(ClassPrepareEvent::class) { 112 | if (it.jdiEvent.request() == request) { 113 | val referenceType = it.jdiEvent.referenceType() 114 | LOG.trace("Setting breakpoint at prepared type {}", referenceType.name()) 115 | setBreakpointAtType(referenceType, lineNumber) 116 | } 117 | }) 118 | 119 | request.enable() 120 | } 121 | 122 | // Try setting breakpoint using loaded VM classes 123 | 124 | val classPattern = "^${Regex.escape(className)}(?:\\$.*)?".toRegex() 125 | vm.allClasses() 126 | .filter { classPattern.matches(it.name()) } 127 | .forEach { 128 | LOG.trace("Setting breakpoint at known type {}", it.name()) 129 | setBreakpointAtType(it, lineNumber) 130 | } 131 | } ?: LOG.warn("Not adding breakpoint in unrecognized source file {}", Paths.get(filePath).fileName) 132 | } 133 | 134 | /** Tries to set a breakpoint - Will return whether this was successful */ 135 | private fun setBreakpointAtType(refType: ReferenceType, lineNumber: Long): Boolean { 136 | try { 137 | val location = refType 138 | .locationsOfLine(lineNumber.toInt()) 139 | ?.firstOrNull() ?: return false 140 | val request = vm.eventRequestManager() 141 | .createBreakpointRequest(location) 142 | ?.apply { 143 | setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD) 144 | enable() 145 | } 146 | return request != null 147 | } catch (e: AbsentInformationException) { 148 | return false 149 | } 150 | } 151 | 152 | override fun resume() { 153 | vm.resume() 154 | } 155 | 156 | override fun exit() { 157 | LOG.info("Exiting JDI session") 158 | try { 159 | if (vm.process()?.isAlive() ?: false) { 160 | vm.exit(0) 161 | } 162 | } catch (e: VMDisconnectedException) { 163 | // Ignore since we wanted to stop the VM anyway 164 | } 165 | } 166 | 167 | override fun positionOf(location: Location): Position? = sourceOf(location) 168 | ?.let { Position(it, location.lineNumber()) } 169 | 170 | private fun sourceOf(location: Location): Source? = 171 | try { 172 | val sourcePath = location.sourcePath() 173 | val sourceName = location.sourceName() 174 | 175 | sourcesRoots 176 | .asSequence() 177 | .map { it.resolve(sourcePath) } 178 | .orEmpty() 179 | .mapNotNull { findValidKtFilePath(it, sourceName) } 180 | .firstOrNull() 181 | ?.let { Source( 182 | name = sourceName ?: it.fileName.toString(), 183 | filePath = it 184 | ) } 185 | } catch(exception: AbsentInformationException) { 186 | null 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/JDISessionContext.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi 2 | 3 | import org.javacs.ktda.core.Position 4 | import org.javacs.ktda.jdi.event.VMEventBus 5 | import com.sun.jdi.Location 6 | 7 | interface JDISessionContext { 8 | val eventBus: VMEventBus 9 | val pendingStepRequestThreadIds: MutableSet 10 | 11 | fun positionOf(location: Location): Position? 12 | } 13 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/JDIThread.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi 2 | 3 | import org.javacs.ktda.core.DebuggeeThread 4 | import org.javacs.ktda.core.stack.StackTrace 5 | import org.javacs.ktda.util.Subscription 6 | import org.javacs.ktda.jdi.stack.JDIStackTrace 7 | import org.javacs.ktda.jdi.JDISessionContext 8 | import com.sun.jdi.ThreadReference 9 | import com.sun.jdi.event.Event 10 | import com.sun.jdi.request.EventRequest 11 | import com.sun.jdi.request.StepRequest 12 | import kotlin.reflect.KClass 13 | 14 | class JDIThread( 15 | private val threadRef: ThreadReference, 16 | private val context: JDISessionContext 17 | ) : DebuggeeThread { 18 | override val name: String = threadRef.name() ?: "Unnamed Thread" 19 | override val id: Long = threadRef.uniqueID() 20 | 21 | override fun pause() = 22 | if (!threadRef.isSuspended()) { 23 | threadRef.suspend() 24 | true 25 | } else false 26 | 27 | override fun resume(): Boolean { 28 | val suspends = threadRef.suspendCount() 29 | (0 until suspends).forEach { 30 | threadRef.resume() 31 | } 32 | return suspends > 0 33 | } 34 | 35 | override fun stackTrace() = JDIStackTrace(threadRef.frames(), context) 36 | 37 | override fun stepOver() = stepLine(StepRequest.STEP_OVER) 38 | 39 | override fun stepInto() = stepLine(StepRequest.STEP_INTO) 40 | 41 | override fun stepOut() = stepLine(StepRequest.STEP_OUT) 42 | 43 | private fun stepLine(depth: Int) { 44 | stepRequest(StepRequest.STEP_LINE, depth) 45 | ?.let { performStep(it) } 46 | } 47 | 48 | private fun performStep(request: StepRequest) { 49 | request.enable() 50 | resume() 51 | } 52 | 53 | private fun stepRequest(size: Int, depth: Int) = 54 | if (context.pendingStepRequestThreadIds.contains(id)) null else { 55 | val eventRequestManager = threadRef.virtualMachine().eventRequestManager() 56 | eventRequestManager 57 | .createStepRequest(threadRef, size, depth) 58 | ?.also { request -> 59 | request.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD) 60 | request.addCountFilter(1) 61 | 62 | // Abort pending StepRequest when a breakpoint is hit 63 | context.pendingStepRequestThreadIds.add(id) 64 | 65 | fun abortUponEvent(eventClass: KClass) { 66 | var sub: Subscription? = null 67 | 68 | sub = context.eventBus.subscribe(eventClass) { 69 | val pending = context.pendingStepRequestThreadIds.contains(id) 70 | if (pending) { 71 | eventRequestManager.deleteEventRequest(request) 72 | context.pendingStepRequestThreadIds.remove(id) 73 | } 74 | sub?.unsubscribe() 75 | } 76 | } 77 | 78 | abortUponEvent(com.sun.jdi.event.StepEvent::class) 79 | abortUponEvent(com.sun.jdi.event.BreakpointEvent::class) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/event/VMEvent.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi.event 2 | 3 | import com.sun.jdi.event.Event 4 | import com.sun.jdi.event.EventSet 5 | 6 | class VMEvent( 7 | val jdiEvent: E, 8 | val jdiEventSet: EventSet 9 | ) { 10 | var resumeThreads = true 11 | } 12 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/event/VMEventBus.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi.event 2 | 3 | import org.javacs.kt.LOG 4 | import org.javacs.ktda.util.Box 5 | import org.javacs.ktda.util.ListenerList 6 | import org.javacs.ktda.util.Subscription 7 | import org.javacs.ktda.core.event.DebuggeeEventBus 8 | import org.javacs.ktda.core.exception.DebuggeeException 9 | import org.javacs.ktda.core.event.ExitEvent 10 | import org.javacs.ktda.core.event.BreakpointStopEvent 11 | import org.javacs.ktda.core.event.ExceptionStopEvent 12 | import org.javacs.ktda.core.event.StepStopEvent 13 | import org.javacs.ktda.core.event.ThreadEvent 14 | import org.javacs.ktda.core.event.ThreadEventReason 15 | import org.javacs.ktda.jdi.exception.JDIException 16 | import com.sun.jdi.VirtualMachine 17 | import com.sun.jdi.VMDisconnectedException 18 | import com.sun.jdi.event.VMDeathEvent 19 | import com.sun.jdi.event.Event as JDIEvent 20 | import com.sun.jdi.event.LocatableEvent as JDILocatableEvent 21 | import com.sun.jdi.event.EventSet as JDIEventSet 22 | import com.sun.jdi.event.BreakpointEvent as JDIBreakpointEvent 23 | import com.sun.jdi.event.StepEvent as JDIStepEvent 24 | import com.sun.jdi.event.ExceptionEvent as JDIExceptionEvent 25 | import com.sun.jdi.event.ThreadStartEvent as JDIThreadStartEvent 26 | import com.sun.jdi.event.ThreadDeathEvent as JDIThreadDeathEvent 27 | import java.util.concurrent.ConcurrentHashMap 28 | import kotlin.reflect.KClass 29 | 30 | /** 31 | * Asynchronously polls and publishes any events from 32 | * a debuggee virtual machine. 33 | */ 34 | class VMEventBus(private val vm: VirtualMachine): DebuggeeEventBus { 35 | private var exited = false 36 | private val eventListeners = ConcurrentHashMap, ListenerList>>() 37 | override val exitListeners = ListenerList() 38 | override val breakpointListeners = ListenerList() 39 | override val stepListeners = ListenerList() 40 | override val exceptionListeners = ListenerList() 41 | override val threadListeners = ListenerList() 42 | 43 | init { 44 | hookListeners() 45 | startAsyncPoller() 46 | } 47 | 48 | private fun startAsyncPoller() { 49 | Thread({ 50 | val eventQueue = vm.eventQueue() 51 | try { 52 | while (!exited) { 53 | val eventSet = eventQueue.remove() 54 | var resumeThreads = true 55 | 56 | for (event in eventSet) { 57 | LOG.debug("VM Event: {}", event) 58 | if (event is VMDeathEvent) { 59 | exited = true 60 | resumeThreads = false 61 | } else { 62 | val resume = dispatchEvent(event, eventSet) 63 | resumeThreads = resumeThreads && resume 64 | } 65 | } 66 | 67 | if (resumeThreads) { 68 | eventSet.resume() 69 | } 70 | } 71 | } catch (e: InterruptedException) { 72 | LOG.debug("VMEventBus event poller terminated by interrupt") 73 | } catch (e: VMDisconnectedException) { 74 | LOG.info("VMEventBus event poller terminated by disconnect: {}", e.message) 75 | } 76 | exitListeners.fire(ExitEvent) 77 | }, "eventBus").start() 78 | } 79 | 80 | private fun hookListeners() { 81 | val eventRequestManager = vm.eventRequestManager() 82 | eventRequestManager.createThreadStartRequest().enable() 83 | eventRequestManager.createThreadDeathRequest().enable() 84 | eventRequestManager.createVMDeathRequest().enable() 85 | 86 | subscribe(JDIBreakpointEvent::class) { 87 | breakpointListeners.fire(BreakpointStopEvent( 88 | threadID = toThreadID(it.jdiEvent) 89 | )) 90 | it.resumeThreads = false 91 | } 92 | subscribe(JDIStepEvent::class) { 93 | stepListeners.fire(StepStopEvent( 94 | threadID = toThreadID(it.jdiEvent) 95 | )) 96 | it.resumeThreads = false 97 | } 98 | subscribe(JDIExceptionEvent::class) { 99 | exceptionListeners.fire(ExceptionStopEvent( 100 | threadID = toThreadID(it.jdiEvent), 101 | exception = JDIException(it.jdiEvent.exception(), it.jdiEvent.thread()) 102 | )) 103 | it.resumeThreads = false 104 | } 105 | subscribe(JDIThreadStartEvent::class) { 106 | threadListeners.fire(ThreadEvent( 107 | threadID = it.jdiEvent.thread().uniqueID(), 108 | reason = ThreadEventReason.STARTED 109 | )) 110 | } 111 | subscribe(JDIThreadDeathEvent::class) { 112 | threadListeners.fire(ThreadEvent( 113 | threadID = it.jdiEvent.thread().uniqueID(), 114 | reason = ThreadEventReason.STOPPED 115 | )) 116 | } 117 | } 118 | 119 | private fun toThreadID(event: JDILocatableEvent) = event.thread().uniqueID() 120 | 121 | /** Subscribes to a JDI event type and lets the caller decide when to stop subscribing. */ 122 | @Suppress("UNCHECKED_CAST") 123 | fun subscribe(eventClass: KClass, listener: (VMEvent) -> Unit): Subscription { 124 | eventListeners.putIfAbsent(eventClass, ListenerList()) 125 | // This cast is safe, because dispatchEvent uses 126 | // reflection to assure that only a correct 'Event' type is passed 127 | // and due to type erasure on JVM 128 | eventListeners[eventClass]!!.add(listener as (VMEvent) -> Unit) 129 | return object: Subscription { 130 | override fun unsubscribe() { 131 | eventListeners[eventClass]?.remove(listener as (VMEvent) -> Unit) 132 | } 133 | } 134 | } 135 | 136 | private fun dispatchEvent(event: JDIEvent, eventSet: JDIEventSet): Boolean { 137 | val VMEvent = VMEvent(event, eventSet) 138 | val eventClass = event::class.java 139 | eventListeners 140 | .filterKeys { it.java.isAssignableFrom(eventClass) } 141 | .values 142 | .forEach { it.fire(VMEvent) } 143 | return VMEvent.resumeThreads 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/exception/JDIException.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi.exception 2 | 3 | import com.sun.jdi.ObjectReference 4 | import com.sun.jdi.ThreadReference 5 | import org.javacs.ktda.core.exception.DebuggeeException 6 | 7 | class JDIException( 8 | private val exception: ObjectReference, 9 | private val thread: ThreadReference 10 | ) : DebuggeeException { 11 | private val type by lazy { exception.referenceType() } 12 | 13 | override val fullTypeName: String by lazy { type.name() } 14 | override val typeName: String? by lazy { fullTypeName.split(".").last() } 15 | override val description: String by lazy { 16 | type.methodsByName("toString") 17 | .firstOrNull() 18 | ?.let { exception.invokeMethod(thread, it, emptyList(), 0) } 19 | ?.toString() 20 | ?: fullTypeName 21 | } 22 | override val message: String? by lazy { 23 | type.methodsByName("getMessage") 24 | .firstOrNull() 25 | ?.let { exception.invokeMethod(thread, it, emptyList(), 0) } 26 | ?.toString() 27 | } 28 | override val innerException: JDIException? by lazy { 29 | type.methodsByName("getCause") 30 | .firstOrNull() 31 | ?.let { exception.invokeMethod(thread, it, emptyList(), 0)?.let { it as? ObjectReference } } 32 | ?.let { JDIException(it, thread) } 33 | } 34 | 35 | // TODO: Stack frames 36 | } 37 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi.launch 2 | 3 | import org.javacs.kt.LOG 4 | import org.javacs.ktda.core.launch.DebugLauncher 5 | import org.javacs.ktda.core.launch.LaunchConfiguration 6 | import org.javacs.ktda.core.launch.AttachConfiguration 7 | import org.javacs.ktda.core.Debuggee 8 | import org.javacs.ktda.core.DebugContext 9 | import org.javacs.ktda.util.KotlinDAException 10 | import org.javacs.ktda.jdi.JDIDebuggee 11 | import com.sun.jdi.Bootstrap 12 | import com.sun.jdi.VirtualMachineManager 13 | import com.sun.jdi.connect.Connector 14 | import com.sun.jdi.connect.LaunchingConnector 15 | import com.sun.jdi.connect.AttachingConnector 16 | import java.io.File 17 | import java.nio.file.Path 18 | import java.nio.file.Files 19 | import java.net.URLEncoder 20 | import java.net.URLDecoder 21 | import java.nio.charset.StandardCharsets 22 | import java.util.stream.Collectors 23 | 24 | class JDILauncher( 25 | private val attachTimeout: Int = 50, 26 | private val vmArguments: String? = null, 27 | private val modulePaths: String? = null 28 | ) : DebugLauncher { 29 | private val vmManager: VirtualMachineManager 30 | get() = Bootstrap.virtualMachineManager() 31 | 32 | override fun launch(config: LaunchConfiguration, context: DebugContext): JDIDebuggee { 33 | val connector = createLaunchConnector() 34 | LOG.info("Starting JVM debug session with main class {}", config.mainClass) 35 | 36 | LOG.debug("Launching VM") 37 | val vm = connector.launch(createLaunchArgs(config, connector)) ?: throw KotlinDAException("Could not launch a new VM") 38 | 39 | LOG.debug("Finding sourcesRoots") 40 | val sourcesRoots = sourcesRootsOf(config.projectRoot) 41 | 42 | return JDIDebuggee(vm, sourcesRoots, context) 43 | } 44 | 45 | override fun attach(config: AttachConfiguration, context: DebugContext): JDIDebuggee { 46 | val connector = createAttachConnector() 47 | LOG.info("Attaching JVM debug session on {}:{}", config.hostName, config.port) 48 | return JDIDebuggee( 49 | connector.attach(createAttachArgs(config, connector)) ?: throw KotlinDAException("Could not attach the VM"), 50 | sourcesRootsOf(config.projectRoot), 51 | context 52 | ) 53 | } 54 | 55 | private fun createLaunchArgs(config: LaunchConfiguration, connector: Connector): Map = connector.defaultArguments() 56 | .also { args -> 57 | args["suspend"]!!.setValue("true") 58 | args["options"]!!.setValue(formatOptions(config)) 59 | args["main"]!!.setValue(formatMainClass(config)) 60 | } 61 | 62 | private fun createAttachArgs(config: AttachConfiguration, connector: Connector): Map = connector.defaultArguments() 63 | .also { args -> 64 | args["hostname"]!!.setValue(config.hostName) 65 | args["port"]!!.setValue(config.port.toString()) 66 | args["timeout"]!!.setValue(config.timeout.toString()) 67 | } 68 | 69 | private fun createAttachConnector(): AttachingConnector = vmManager.attachingConnectors() 70 | .let { it.find { it.name() == "com.sun.jdi.SocketAttach" } ?: it.firstOrNull() } 71 | ?: throw KotlinDAException("Could not find an attaching connector (for a new debuggee VM)") 72 | 73 | private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors() 74 | // Workaround for JDK 11+ where the first launcher (RawCommandLineLauncher) does not properly support args 75 | .let { it.find { it.javaClass.name == "com.sun.tools.jdi.SunCommandLineLauncher" } ?: it.firstOrNull() } 76 | ?: throw KotlinDAException("Could not find a launching connector (for a new debuggee VM)") 77 | 78 | private fun sourcesRootsOf(projectRoot: Path): Set = 79 | Files.walk(projectRoot, 2) // root project and submodule 80 | .filter { Files.isDirectory(it) } 81 | .map { it.resolve("src") } 82 | .filter { Files.isDirectory(it) } 83 | .flatMap(Files::list) // main, test 84 | .filter { Files.isDirectory(it) } 85 | .flatMap(Files::list) // kotlin, java 86 | .filter { Files.isDirectory(it) } 87 | .collect(Collectors.toSet()) 88 | 89 | private fun formatOptions(config: LaunchConfiguration): String { 90 | var options = config.vmArguments 91 | modulePaths?.let { options += " --module-path \"$modulePaths\"" } 92 | options += " -classpath \"${formatClasspath(config)}\"" 93 | return options 94 | } 95 | 96 | private fun formatMainClass(config: LaunchConfiguration): String { 97 | val mainClasses = config.mainClass.split("/") 98 | return if ((modulePaths != null) || (mainClasses.size == 2)) { 99 | // Required for Java 9 compatibility 100 | "-m ${config.mainClass}" 101 | } else config.mainClass 102 | } 103 | 104 | private fun formatClasspath(config: LaunchConfiguration): String = config.classpath 105 | .map { it.toAbsolutePath().toString() } 106 | .reduce { prev, next -> "$prev${File.pathSeparatorChar}$next" } 107 | 108 | private fun urlEncode(arg: Collection?) = arg 109 | ?.map { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) } 110 | ?.reduce { a, b -> "$a\n$b" } 111 | 112 | private fun urlDecode(arg: String?) = arg 113 | ?.split("\n") 114 | ?.map { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) } 115 | ?.toList() 116 | } 117 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDILocalScope.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi.scope 2 | 3 | import org.javacs.ktda.core.scope.VariableTreeNode 4 | import com.sun.jdi.AbsentInformationException 5 | import com.sun.jdi.StackFrame 6 | 7 | class JDILocalScope( 8 | frame: StackFrame 9 | ) : VariableTreeNode { 10 | override val name: String = "Locals" 11 | override val childs: List = variablesIn(frame) 12 | 13 | private fun variablesIn(frame: StackFrame) = try { 14 | listOfNotNull(thisIn(frame)) + localsIn(frame) 15 | } catch (e: AbsentInformationException) { emptyList() } 16 | 17 | private fun localsIn(frame: StackFrame) = frame.visibleVariables() 18 | .map { JDIVariable(it.name(), frame.getValue(it)) } 19 | 20 | private fun thisIn(frame: StackFrame) = try { 21 | JDIVariable("this", frame.thisObject()) 22 | } catch (e: IllegalStateException) { null } 23 | } 24 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi.scope 2 | 3 | import org.javacs.ktda.core.scope.VariableTreeNode 4 | import com.sun.jdi.ReferenceType 5 | import com.sun.jdi.LocalVariable 6 | import com.sun.jdi.ArrayType 7 | import com.sun.jdi.ArrayReference 8 | import com.sun.jdi.ObjectReference 9 | import com.sun.jdi.Value 10 | import com.sun.jdi.Type 11 | 12 | class JDIVariable( 13 | override val name: String, 14 | private val jdiValue: Value?, 15 | jdiType: Type? = null 16 | ) : VariableTreeNode { 17 | override val value: String = jdiValue?.toString() ?: "null" // TODO: Better string representation 18 | override val type: String = (jdiType?.name() ?: jdiValue?.type()?.name()) ?: "Unknown type" 19 | override val childs: List? by lazy { jdiValue?.let(::childrenOf) } 20 | override val id: Long? = (jdiValue as? ObjectReference)?.uniqueID() ?: (jdiValue as? ArrayReference)?.uniqueID() 21 | 22 | private fun childrenOf(jdiValue: Value): List { 23 | val jdiType = jdiValue.type() 24 | // LOG.info("$name has type {}", jdiType::class.simpleName) // DEBUG 25 | return when (jdiType) { 26 | is ReferenceType -> when (jdiType) { 27 | is ArrayType -> arrayElementsOf(jdiValue as ArrayReference) 28 | else -> fieldsOf(jdiValue as ObjectReference, jdiType) 29 | } 30 | else -> emptyList() 31 | } 32 | } 33 | 34 | private fun arrayElementsOf(jdiValue: ArrayReference): List = jdiValue.values 35 | .mapIndexed { i, it -> JDIVariable(i.toString(), it) } 36 | 37 | private fun fieldsOf(jdiValue: ObjectReference, jdiType: ReferenceType) = jdiType.allFields() 38 | .map { JDIVariable(it.name(), jdiValue.getValue(it), jdiType) } 39 | } 40 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/stack/JDIStackFrame.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi.stack 2 | 3 | import org.javacs.kt.LOG 4 | import org.javacs.ktda.core.Position 5 | import org.javacs.ktda.core.completion.CompletionItem 6 | import org.javacs.ktda.core.completion.CompletionItemType 7 | import org.javacs.ktda.core.scope.VariableTreeNode 8 | import org.javacs.ktda.core.stack.StackFrame 9 | import org.javacs.ktda.jdi.JDISessionContext 10 | import org.javacs.ktda.jdi.scope.JDILocalScope 11 | import com.sun.jdi.InvalidStackFrameException 12 | 13 | class JDIStackFrame( 14 | frame: com.sun.jdi.StackFrame, 15 | context: JDISessionContext 16 | ) : StackFrame { 17 | private val location = frame.location() 18 | override val name: String = location.method()?.name() ?: "Unknown" 19 | override val position: Position? = context.positionOf(location) 20 | override val scopes: List by lazy { 21 | try { 22 | listOf(JDILocalScope(frame)) 23 | } catch (e: InvalidStackFrameException) { 24 | LOG.warn("Could not fetch scopes, invalid stack frame: {}", e.message) 25 | emptyList() 26 | } 27 | } 28 | 29 | private val variables by lazy { scopes.flatMap { it.childs ?: emptyList() } } 30 | 31 | // TODO: Scope "Fields" 32 | // TODO: Argument values? 33 | 34 | private fun evaluateQualified(qualName: List, scopeVariables: List = variables): VariableTreeNode? = 35 | qualName.firstOrNull().let { qual -> 36 | val rest = qualName.drop(1) 37 | scopeVariables 38 | .filter { it.name == qual } 39 | .mapNotNull { if (rest.isEmpty()) it else evaluateQualified(rest, it.childs ?: emptyList()) } 40 | .firstOrNull() 41 | } 42 | 43 | private fun completeQualified(qualName: List, scopeVariables: List = variables): List = 44 | qualName.firstOrNull()?.let { qual -> 45 | val rest = qualName.drop(1) 46 | scopeVariables 47 | .filter { it.name == qual } 48 | .flatMap { completeQualified(rest, it.childs ?: emptyList()) } 49 | .takeIf { it.isNotEmpty() } 50 | ?: scopeVariables 51 | .takeIf { rest.isEmpty() } 52 | ?.filter { it.name.startsWith(qual) } 53 | ?.map { CompletionItem(it.name, CompletionItemType.VARIABLE) } 54 | }.orEmpty() 55 | 56 | private fun parseQualified(expression: String): List = expression.split(".") 57 | 58 | override fun evaluate(expression: String): VariableTreeNode? { 59 | // TODO: Implement proper expression parsing 60 | // 61 | // Note that expression parsing is not part of the JDI 62 | // (see https://www.oracle.com/technetwork/java/javase/tech/faqs-jsp-142584.html#QV1) 63 | // There is a JDI-compatible expression parser in the JDPA examples. 64 | // Unfortunately, however, it is not exported by the jdk/jdi module 65 | // and as such cannot be imported: 66 | // 67 | // com.sun.tools.example.debug.expr.ExpressionParser 68 | // 69 | // Creating JDI values from primitives and strings is possible though, 70 | // using VirtualMachine.mirrorOf. 71 | 72 | val qualified = parseQualified(expression) 73 | return evaluateQualified(qualified) 74 | ?: evaluateQualified(listOf("this") + qualified) 75 | } 76 | 77 | override fun completions(expression: String): List { 78 | val qualified = parseQualified(expression) 79 | return completeQualified(qualified) + completeQualified(listOf("this") + qualified) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/jdi/stack/JDIStackTrace.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.jdi.stack 2 | 3 | import org.javacs.ktda.core.stack.StackTrace 4 | import org.javacs.ktda.core.stack.StackFrame 5 | import org.javacs.ktda.jdi.JDISessionContext 6 | 7 | class JDIStackTrace( 8 | jdiFrames: List, 9 | context: JDISessionContext 10 | ) : StackTrace { 11 | override val frames: List = jdiFrames.map { JDIStackFrame(it, context) } 12 | } 13 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/Box.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | /** A simple boxing wrapper. Useful for captured local variables that have to be mutated. */ 4 | class Box(var value: T) 5 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/Identifiable.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | public interface Identifiable { 4 | val id: Long? 5 | get() = null 6 | } 7 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/JsonLogger.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | import org.javacs.kt.Logger 4 | 5 | val JSON_LOG = Logger() 6 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/KotlinDAException.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | /** 4 | * An exception related to the debug adapter 5 | */ 6 | class KotlinDAException : RuntimeException { 7 | constructor(msg: String) : super(msg) 8 | 9 | constructor(msg: String, cause: Throwable) : super(msg, cause) 10 | } 11 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/ListenerList.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | import java.util.ArrayDeque 4 | 5 | typealias Listener = (T) -> Unit 6 | 7 | class ListenerList { 8 | private val listeners = mutableListOf>() 9 | private val queuedModifications = ArrayDeque<(MutableList>) -> Unit>() 10 | @Volatile private var iterators = 0 11 | 12 | fun fire(event: T) { 13 | iterators += 1 14 | listeners.forEach { it(event) } 15 | iterators -= 1 16 | 17 | if (iterators <= 0) { 18 | applyModifications() 19 | } 20 | } 21 | 22 | fun add(listener: Listener) = withListeners { it.add(listener) } 23 | 24 | fun remove(listener: Listener) = withListeners { it.remove(listener) } 25 | 26 | fun propagateTo(next: ListenerList) = add(next::fire) 27 | 28 | private fun applyModifications() { 29 | while (!queuedModifications.isEmpty()) { 30 | queuedModifications.poll()(listeners) 31 | } 32 | } 33 | 34 | private fun withListeners(body: (MutableList>) -> Unit) { 35 | if (iterators > 0) { 36 | // Do not modify listener list concurrently 37 | queuedModifications.offer(body) 38 | } else body(listeners) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/LoggingInputStream.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | import java.io.InputStream 4 | import org.javacs.kt.util.DelegatePrintStream 5 | 6 | private val MESSAGE_FLUSH_MIN_LENGTH = 20 7 | 8 | class LoggingInputStream( 9 | private val upstream: InputStream, 10 | private val logEnabled: Boolean, 11 | private val bufferLines: Boolean 12 | ) : InputStream() { 13 | private val newline = System.lineSeparator() 14 | private val buffer = StringBuilder() 15 | private val printStream = DelegatePrintStream { 16 | if (bufferLines) { 17 | buffer.append(it) 18 | if (it.contains(newline) || it.length > MESSAGE_FLUSH_MIN_LENGTH) { 19 | JSON_LOG.info("IN >> {}", buffer) 20 | buffer.setLength(0) 21 | } 22 | } else JSON_LOG.info("IN >> {}", it) 23 | } 24 | 25 | override fun read(): Int { 26 | val result = upstream.read() 27 | if (logEnabled) { 28 | printStream.write(result) 29 | } 30 | return result 31 | } 32 | 33 | override fun read(b: ByteArray): Int { 34 | val result = upstream.read(b) 35 | if (logEnabled) { 36 | printStream.write(b) 37 | } 38 | return result 39 | } 40 | 41 | override fun read(b: ByteArray, off: Int, len: Int): Int { 42 | val result = upstream.read(b, off, len) 43 | if (logEnabled) { 44 | printStream.write(b, off, len) 45 | } 46 | return result 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/LoggingOutputStream.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | import java.io.OutputStream 4 | import org.javacs.kt.util.DelegatePrintStream 5 | 6 | private val MESSAGE_FLUSH_MIN_LENGTH = 20 7 | 8 | class LoggingOutputStream( 9 | private val downstream: OutputStream, 10 | private val logEnabled: Boolean, 11 | private val bufferLines: Boolean 12 | ) : OutputStream() { 13 | private val newline = System.lineSeparator() 14 | private val buffer = StringBuilder() 15 | private val printStream = DelegatePrintStream { 16 | if (bufferLines) { 17 | buffer.append(it) 18 | if (it.contains(newline)) { 19 | JSON_LOG.info("OUT << {}", buffer) 20 | buffer.setLength(0) 21 | } 22 | } else JSON_LOG.info("OUT << {}", it) 23 | } 24 | 25 | override fun write(b: Int) { 26 | if (logEnabled) { 27 | printStream.write(b) 28 | } 29 | downstream.write(b) 30 | } 31 | 32 | override fun write(b: ByteArray) { 33 | if (logEnabled) { 34 | printStream.write(b) 35 | } 36 | downstream.write(b) 37 | } 38 | 39 | override fun write(b: ByteArray, off: Int, len: Int) { 40 | if (logEnabled) { 41 | printStream.write(b, off, len) 42 | } 43 | downstream.write(b, off, len) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/ObjectPool.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | import org.javacs.ktda.util.Identifiable 4 | 5 | private data class ObjectKey( 6 | val id: Long, 7 | val owner: O 8 | ) 9 | 10 | private data class ObjectMapping ( 11 | val key: ObjectKey, 12 | val value: V 13 | ) 14 | 15 | /** 16 | * Maps objects of owners to multiple owned values. 17 | * To store and retrieve objects, unique ids are used. 18 | */ 19 | class ObjectPool { 20 | private val mappingsByID = mutableMapOf>() 21 | private val mappingsByOwner = mutableMapOf>>() 22 | 23 | private var currentID = 1L 24 | 25 | val empty: Boolean 26 | get() = mappingsByID.isEmpty() 27 | val size: Int 28 | get() = mappingsByID.size 29 | 30 | fun clear() { 31 | mappingsByID.clear() 32 | mappingsByOwner.clear() 33 | } 34 | 35 | /** Stores an object and returns its (unique) id */ 36 | fun store(owner: O, value: V): Long { 37 | val id = (value as? Identifiable)?.id ?: nextID() 38 | val key = ObjectKey(id, owner) 39 | val mapping = ObjectMapping(key, value) 40 | 41 | mappingsByID[id] = mapping 42 | mappingsByOwner.putIfAbsent(owner, mutableSetOf()) 43 | mappingsByOwner[owner]!!.add(mapping) 44 | 45 | return id 46 | } 47 | 48 | fun removeAllOwnedBy(owner: O) { 49 | mappingsByOwner[owner]?.let { 50 | it.forEach { mapping -> 51 | mappingsByID.remove(mapping.key.id) 52 | } 53 | } 54 | mappingsByOwner.remove(owner) 55 | } 56 | 57 | fun removeByID(id: Long) { 58 | mappingsByID[id]?.let { 59 | mappingsByOwner[it.key.owner]?.remove(it) 60 | } 61 | mappingsByID.remove(id) 62 | } 63 | 64 | fun getByID(id: Long) = mappingsByID[id]?.value 65 | 66 | fun getIDsOwnedBy(owner: O): Set = mappingsByOwner[owner] 67 | ?.map { it.key.id } 68 | ?.toSet() 69 | .orEmpty() 70 | 71 | fun getOwnedBy(owner: O): Set = mappingsByOwner[owner] 72 | ?.map { it.value } 73 | ?.toSet() 74 | .orEmpty() 75 | 76 | fun containsID(id: Long) = mappingsByID.contains(id) 77 | 78 | private fun nextID(): Long { 79 | do { 80 | currentID++ 81 | } while (containsID(currentID)); 82 | 83 | return currentID 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/Observable.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | class Observable(private var value: T) { 4 | private val listeners = ListenerList() 5 | 6 | fun set(value: T) { 7 | this.value = value 8 | fire() 9 | } 10 | 11 | fun listen(listener: (T) -> Unit) = listeners.add(listener) 12 | 13 | fun unlisten(listener: (T) -> Unit) = listeners.remove(listener) 14 | 15 | fun get() = value 16 | 17 | fun fire() = listeners.fire(value) 18 | } 19 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/ObservableList.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | class ObservableList( 4 | private var entries: MutableList = mutableListOf() 5 | ) { 6 | private val listeners = ListenerList>() 7 | 8 | val size: Int 9 | get() = entries.size 10 | val empty: Boolean 11 | get() = entries.isEmpty() 12 | 13 | fun add(element: T) { 14 | entries.add(element) 15 | fire() 16 | } 17 | 18 | fun remove(element: T) { 19 | entries.remove(element) 20 | fire() 21 | } 22 | 23 | fun get(): List = entries 24 | 25 | operator fun get(index: Int) = entries[index] 26 | 27 | operator fun set(index: Int, value: T) { 28 | entries[index] = value 29 | fire() 30 | } 31 | 32 | fun setAll(values: List) { 33 | entries = values.toMutableList() 34 | fire() 35 | } 36 | 37 | fun asSequence(): Sequence = entries.asSequence() 38 | 39 | fun listen(listener: (List) -> Unit) = listeners.add(listener) 40 | 41 | fun listenAndFire(listener: (List) -> Unit) = listeners.add(listener).also { listener(entries) } 42 | 43 | fun unlisten(listener: (List) -> Unit) = listeners.remove(listener) 44 | 45 | private fun fire() = listeners.fire(entries) 46 | } 47 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/ObservableMap.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | class ObservableMap( 4 | private var entries: MutableMap = mutableMapOf() 5 | ) { 6 | private val listeners = ListenerList>() 7 | 8 | val size: Int 9 | get() = entries.size 10 | val empty: Boolean 11 | get() = entries.isEmpty() 12 | 13 | fun remove(key: K) = entries.remove(key).also { fire() } 14 | 15 | operator fun set(key: K, value: V) { 16 | entries[key] = value 17 | fire() 18 | } 19 | 20 | operator fun get(key: K) = entries[key] 21 | 22 | fun get(): Map = entries 23 | 24 | fun listen(listener: (Map) -> Unit) = listeners.add(listener) 25 | 26 | fun listenAndFire(listener: (Map) -> Unit) = listeners.add(listener).also { listener(entries) } 27 | 28 | fun unlisten(listener: (Map) -> Unit) = listeners.remove(listener) 29 | 30 | private fun fire() = listeners.fire(entries) 31 | } 32 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/ObservableSet.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | class ObservableSet( 4 | private var entries: MutableSet = mutableSetOf() 5 | ) { 6 | private val listeners = ListenerList>() 7 | 8 | val size: Int 9 | get() = entries.size 10 | val empty: Boolean 11 | get() = entries.isEmpty() 12 | 13 | fun add(element: T) { 14 | entries.add(element) 15 | fire() 16 | } 17 | 18 | fun remove(element: T) { 19 | entries.remove(element) 20 | fire() 21 | } 22 | 23 | fun get(): Set = entries 24 | 25 | fun setAll(values: Set) { 26 | entries = values.toMutableSet() 27 | fire() 28 | } 29 | 30 | fun asSequence(): Sequence = entries.asSequence() 31 | 32 | fun listen(listener: (Set) -> Unit) = listeners.add(listener) 33 | 34 | fun listenAndFire(listener: (Set) -> Unit) = listeners.add(listener).also { listener(entries) } 35 | 36 | fun unlisten(listener: (Set) -> Unit) = listeners.remove(listener) 37 | 38 | private fun fire() = listeners.fire(entries) 39 | } 40 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/Subscription.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | interface Subscription { 4 | fun unsubscribe() 5 | } 6 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/SubscriptionBag.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | class SubscriptionBag: Subscription { 4 | private val subscriptions = mutableListOf() 5 | 6 | fun add(subscription: Subscription) { 7 | subscriptions.add(subscription) 8 | } 9 | 10 | override fun unsubscribe() { 11 | var iterator = subscriptions.iterator() 12 | while (iterator.hasNext()) { 13 | iterator.next().unsubscribe() 14 | iterator.remove() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /adapter/src/main/kotlin/org/javacs/ktda/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | import org.javacs.kt.LOG 4 | import java.nio.file.Path 5 | import java.nio.file.Paths 6 | 7 | fun execAndReadStdout(shellCommand: String, directory: Path): String { 8 | val process = Runtime.getRuntime().exec(shellCommand, null, directory.toFile()) 9 | val stdout = process.inputStream 10 | var result = "" 11 | stdout.bufferedReader().use { 12 | result = it.readText() 13 | } 14 | return result 15 | } 16 | 17 | fun winCompatiblePathOf(path: String): Path { 18 | if (path.get(2) == ':' && path.get(0) == '/') { 19 | // Strip leading '/' when dealing with paths on Windows 20 | return Paths.get(path.substring(1)) 21 | } else { 22 | return Paths.get(path) 23 | } 24 | } 25 | 26 | fun Path.replaceExtensionWith(newExtension: String): Path { 27 | val oldName = fileName.toString() 28 | val newName = oldName.substring(0, oldName.lastIndexOf(".")) + newExtension 29 | return resolveSibling(newName) 30 | } 31 | 32 | inline fun > C.ifEmpty(then: () -> C) = if (isEmpty()) then() else this 33 | 34 | fun firstNonNull(vararg optionals: () -> T?): T? { 35 | for (optional in optionals) { 36 | val result = optional() 37 | if (result != null) { 38 | return result 39 | } 40 | } 41 | return null 42 | } 43 | 44 | fun nonNull(item: T?, errorMsgIfNull: String): T = 45 | if (item == null) { 46 | throw NullPointerException(errorMsgIfNull) 47 | } else item 48 | 49 | /** 50 | * Blocks the current thread until the condition becomes true. 51 | * Checks are performed in 80 ms intervals. 52 | */ 53 | inline fun waitFor(what: String, condition: () -> Boolean) { 54 | val delayUntilNotificationMs = 10_000 55 | val startTime = System.currentTimeMillis() 56 | var lastTime = startTime 57 | 58 | while (!condition()) { 59 | Thread.sleep(80) 60 | 61 | val now = System.currentTimeMillis() 62 | if ((now - lastTime) > delayUntilNotificationMs) { 63 | LOG.info("Waiting for {} for {} seconds...", what, (now - startTime) / 1000) 64 | lastTime = now 65 | } 66 | } 67 | } 68 | 69 | fun tryResolving(what: String, resolver: () -> T?): T? { 70 | try { 71 | val resolved = resolver() 72 | if (resolved != null) { 73 | LOG.debug("Successfully resolved {}", what) 74 | return resolved 75 | } else { 76 | LOG.debug("Could not resolve {} as it is null", what) 77 | } 78 | } catch (e: Exception) { 79 | LOG.debug("Could not resolve {}: {}", what, e.message) 80 | } 81 | return null 82 | } 83 | -------------------------------------------------------------------------------- /adapter/src/main/resources/classpathFinder.gradle: -------------------------------------------------------------------------------- 1 | allprojects { project -> 2 | task kotlinLSPDeps { 3 | task -> doLast { 4 | System.out.println "" 5 | System.out.println "gradle-version $gradleVersion" 6 | System.out.println "kotlin-lsp-project ${project.name}" 7 | 8 | if (project.hasProperty('android')) { 9 | project.android.getBootClasspath().each { 10 | System.out.println "kotlin-lsp-gradle $it" 11 | } 12 | if (project.android.hasProperty('applicationVariants')) { 13 | project.android.applicationVariants.all { variant -> 14 | 15 | def variantBase = variant.baseName.replaceAll("-", File.separator) 16 | 17 | def buildClasses = project.getBuildDir().absolutePath + 18 | File.separator + "intermediates" + 19 | File.separator + "classes" + 20 | File.separator + variantBase 21 | 22 | System.out.println "kotlin-lsp-gradle $buildClasses" 23 | 24 | def userClasses = project.getBuildDir().absolutePath + 25 | File.separator + "intermediates" + 26 | File.separator + "javac" + 27 | File.separator + variant.baseName.replaceAll("-", File.separator) + 28 | File.separator + "compile" + variantBase.capitalize() + "JavaWithJavac" + File.separator + "classes" 29 | 30 | System.out.println "kotlin-lsp-gradle $userClasses" 31 | 32 | variant.getCompileClasspath().each { 33 | System.out.println "kotlin-lsp-gradle $it" 34 | } 35 | } 36 | } 37 | } else { 38 | // Print the list of all dependencies jar files. 39 | project.configurations.findAll { 40 | it.metaClass.respondsTo(it, "isCanBeResolved") ? it.isCanBeResolved() : false 41 | }.each { 42 | it.resolve().each { 43 | def inspected = it.inspect() 44 | 45 | if (inspected.endsWith("jar")) { 46 | if (!inspected.contains("zip!")) { 47 | System.out.println "kotlin-lsp-gradle $it" 48 | } 49 | } else if (inspected.endsWith("aar")) { 50 | // If the dependency is an AAR file we try to determine the location 51 | // of the classes.jar file in the exploded aar folder. 52 | def splitted = inspected.split("/") 53 | def namespace = splitted[-5] 54 | def name = splitted[-4] 55 | def version = splitted[-3] 56 | def explodedPath = "$project.buildDir/intermediates/exploded-aar/$namespace/$name/$version/jars/classes.jar" 57 | System.out.println "kotlin-lsp-gradle $explodedPath" 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda 2 | 3 | import java.nio.file.Path 4 | import java.nio.file.Paths 5 | import org.eclipse.lsp4j.debug.ConfigurationDoneArguments 6 | import org.eclipse.lsp4j.debug.InitializeRequestArguments 7 | import org.eclipse.lsp4j.debug.DisconnectArguments 8 | import org.eclipse.lsp4j.debug.OutputEventArguments 9 | import org.eclipse.lsp4j.debug.services.IDebugProtocolClient 10 | import org.javacs.ktda.adapter.KotlinDebugAdapter 11 | import org.javacs.ktda.jdi.launch.JDILauncher 12 | import org.junit.After 13 | import org.junit.Assert.assertThat 14 | import org.junit.Before 15 | import org.hamcrest.Matchers.equalTo 16 | 17 | abstract class DebugAdapterTestFixture( 18 | relativeWorkspaceRoot: String, 19 | private val mainClass: String, 20 | private val vmArguments: String = "" 21 | ) : IDebugProtocolClient { 22 | val absoluteWorkspaceRoot: Path = Paths.get(DebugAdapterTestFixture::class.java.getResource("/Anchor.txt").toURI()).parent.resolve(relativeWorkspaceRoot) 23 | lateinit var debugAdapter: KotlinDebugAdapter 24 | 25 | @Before fun startDebugAdapter() { 26 | // Build the project first 27 | val process = ProcessBuilder("./gradlew", "--no-daemon", "assemble") 28 | .directory(absoluteWorkspaceRoot.toFile()) 29 | .inheritIO() 30 | .start() 31 | process.waitFor() 32 | assertThat(process.exitValue(), equalTo(0)) 33 | 34 | debugAdapter = JDILauncher() 35 | .let(::KotlinDebugAdapter) 36 | .also { 37 | it.connect(this) 38 | val configDone = it.configurationDone(ConfigurationDoneArguments()) 39 | it.initialize(InitializeRequestArguments().apply { 40 | adapterID = "test-debug-adapter" 41 | linesStartAt1 = true 42 | columnsStartAt1 = true 43 | }).join() 44 | // Slightly hacky workaround to ensure someone is 45 | // waiting on the ConfigurationDoneResponse. See 46 | // KotlinDebugAdapter.kt:performInitialization for 47 | // details. 48 | Thread { 49 | configDone.join() 50 | }.start() 51 | // Wait until the thread has blocked on the future 52 | while (configDone.numberOfDependents == 0) { 53 | Thread.sleep(100) 54 | } 55 | } 56 | } 57 | 58 | fun launch() { 59 | println("Launching...") 60 | debugAdapter.launch(mapOf( 61 | "projectRoot" to absoluteWorkspaceRoot.toString(), 62 | "mainClass" to mainClass, 63 | "vmArguments" to vmArguments 64 | )).join() 65 | println("Launched") 66 | } 67 | 68 | @After fun closeDebugAdapter() { 69 | debugAdapter.disconnect(DisconnectArguments()).join() 70 | } 71 | 72 | override fun output(args: OutputEventArguments) { 73 | println(args.output) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceTest.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda 2 | 3 | import org.eclipse.lsp4j.debug.ScopesArguments 4 | import org.eclipse.lsp4j.debug.SetBreakpointsArguments 5 | import org.eclipse.lsp4j.debug.Source 6 | import org.eclipse.lsp4j.debug.SourceBreakpoint 7 | import org.eclipse.lsp4j.debug.StackFrame 8 | import org.eclipse.lsp4j.debug.StackTraceArguments 9 | import org.eclipse.lsp4j.debug.StoppedEventArguments 10 | import org.eclipse.lsp4j.debug.VariablesArguments 11 | import org.junit.Assert.assertThat 12 | import org.junit.Test 13 | import org.hamcrest.Matchers.containsInAnyOrder 14 | import org.hamcrest.Matchers.equalTo 15 | import org.hamcrest.Matchers.hasItem 16 | import org.hamcrest.Matchers.nullValue 17 | import org.hamcrest.Matchers.not 18 | import java.util.concurrent.CountDownLatch 19 | 20 | /** 21 | * Tests a very basic debugging scenario 22 | * using a sample application. 23 | */ 24 | class SampleWorkspaceTest : DebugAdapterTestFixture("sample-workspace", "sample.workspace.AppKt", "-Dtest=testVmArgs") { 25 | private val latch = CountDownLatch(1) 26 | private var asyncException: Throwable? = null 27 | 28 | @Test fun testBreakpointsAndVariables() { 29 | debugAdapter.setBreakpoints(SetBreakpointsArguments().apply { 30 | source = Source().apply { 31 | name = "App.kt" 32 | path = absoluteWorkspaceRoot 33 | .resolve("src") 34 | .resolve("main") 35 | .resolve("kotlin") 36 | .resolve("sample") 37 | .resolve("workspace") 38 | .resolve("App.kt") 39 | .toString() 40 | } 41 | breakpoints = arrayOf(SourceBreakpoint().apply { 42 | line = 8 43 | }) 44 | }).join() 45 | 46 | launch() 47 | latch.await() // Wait for the breakpoint event to finish 48 | asyncException?.let { throw it } 49 | } 50 | 51 | override fun stopped(args: StoppedEventArguments) { 52 | try { 53 | assertThat(args.reason, equalTo("breakpoint")) 54 | 55 | // Query information about the debuggee's current state 56 | val stackTrace = debugAdapter.stackTrace(StackTraceArguments().apply { 57 | threadId = args.threadId 58 | }).join() 59 | val locals = stackTrace.stackFrames.asSequence().flatMap { 60 | debugAdapter.scopes(ScopesArguments().apply { 61 | frameId = it.id 62 | }).join().scopes.asSequence().flatMap { 63 | debugAdapter.variables(VariablesArguments().apply { 64 | variablesReference = it.variablesReference 65 | }).join().variables.asSequence() 66 | } 67 | }.toList() 68 | val receiver = locals.find { it.name == "this" } 69 | 70 | assertThat(locals.map { Pair(it.name, it.value) }, hasItem(Pair("local", "123"))) 71 | assertThat(receiver, not(nullValue())) 72 | 73 | val members = debugAdapter.variables(VariablesArguments().apply { 74 | variablesReference = receiver!!.variablesReference 75 | }).join().variables 76 | 77 | assertThat(members.map { Pair(it.name, it.value) }, containsInAnyOrder(Pair("member", "\"testVmArgs\""))) 78 | } catch (e: Throwable) { 79 | asyncException = e 80 | } finally { 81 | latch.countDown() 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /adapter/src/test/kotlin/org/javacs/ktda/classpath/PathUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.classpath 2 | 3 | import org.junit.Test 4 | import org.junit.Assert.assertEquals 5 | 6 | class PathUtilsTest { 7 | @Test 8 | fun testFilePathToJVMClassNames() { 9 | assertEquals( 10 | listOf("com.abc.MyClass", "com.abc.MyClassKt"), 11 | toJVMClassNames("/project/src/main/kotlin/com/abc/MyClass.kt") 12 | ) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /adapter/src/test/kotlin/org/javacs/ktda/util/ObjectPoolTest.kt: -------------------------------------------------------------------------------- 1 | package org.javacs.ktda.util 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | class ObjectPoolTest { 7 | @Test 8 | fun testObjectPool() { 9 | val pool = ObjectPool() 10 | assertEquals(0, pool.size) 11 | 12 | val parkID = pool.store("city", "park") 13 | val riverID = pool.store("city", "river") 14 | val streetsID = pool.store("city", "streets") 15 | 16 | val doorID = pool.store("house", "door") 17 | val kitchenID = pool.store("house", "kitchen") 18 | val livingRoomID = pool.store("house", "livingRoom") 19 | 20 | assertEquals(6, pool.size) 21 | assertEquals(setOf("park", "river", "streets"), pool.getOwnedBy("city")) 22 | assertEquals(setOf("door", "kitchen", "livingRoom"), pool.getOwnedBy("house")) 23 | assertEquals("park", pool.getByID(parkID)) 24 | assertEquals("river", pool.getByID(riverID)) 25 | assertEquals("streets", pool.getByID(streetsID)) 26 | assertEquals("door", pool.getByID(doorID)) 27 | assertEquals("kitchen", pool.getByID(kitchenID)) 28 | assertEquals("livingRoom", pool.getByID(livingRoomID)) 29 | 30 | pool.removeAllOwnedBy("city") 31 | assertEquals(3, pool.size) 32 | 33 | pool.removeByID(doorID) 34 | assertEquals(2, pool.size) 35 | assertEquals(setOf("kitchen", "livingRoom"), pool.getOwnedBy("house")) 36 | 37 | pool.removeAllOwnedBy("house") 38 | assertEquals(0, pool.size) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /adapter/src/test/resources/Anchor.txt: -------------------------------------------------------------------------------- 1 | Anchor for finding the test/resources directory. 2 | -------------------------------------------------------------------------------- /adapter/src/test/resources/sample-workspace/.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /adapter/src/test/resources/sample-workspace/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.6.10' 3 | id 'application' 4 | } 5 | 6 | repositories { 7 | mavenCentral() 8 | } 9 | 10 | dependencies { 11 | // Align versions of all Kotlin components 12 | implementation platform('org.jetbrains.kotlin:kotlin-bom') 13 | // Use the Kotlin JDK 8 standard library. 14 | implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' 15 | } 16 | 17 | application { 18 | // Define the main class for the application. 19 | mainClass = 'sample.workspace.AppKt' 20 | } 21 | -------------------------------------------------------------------------------- /adapter/src/test/resources/sample-workspace/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/kotlin-debug-adapter/7f05669b642d21afa46ac7b75307fa5d523a7263/adapter/src/test/resources/sample-workspace/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /adapter/src/test/resources/sample-workspace/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /adapter/src/test/resources/sample-workspace/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /adapter/src/test/resources/sample-workspace/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /adapter/src/test/resources/sample-workspace/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'sample-workspace' 2 | -------------------------------------------------------------------------------- /adapter/src/test/resources/sample-workspace/src/main/kotlin/sample/workspace/App.kt: -------------------------------------------------------------------------------- 1 | package sample.workspace 2 | 3 | class App { 4 | private val member: String = System.getProperty("test") 5 | val greeting: String 6 | get() { 7 | val local: Int = 123 8 | return "Hello world." 9 | } 10 | 11 | override fun toString(): String = "App" 12 | } 13 | 14 | fun main(args: Array) { 15 | println(App().greeting) 16 | } 17 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | `maven-publish` 4 | } 5 | 6 | allprojects { 7 | repositories { 8 | mavenCentral() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | projectVersion=0.4.5 2 | kotlinVersion=1.9.10 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fwcd/kotlin-debug-adapter/7f05669b642d21afa46ac7b75307fa5d523a7263/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command; 206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 207 | # shell script including quotes and variable substitutions, so put them in 208 | # double quotes to make sure that they get re-expanded; and 209 | # * put everything else in single quotes, so that it's not re-expanded. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-debug-adapter" 2 | 3 | include("adapter") 4 | 5 | pluginManagement { 6 | repositories { 7 | gradlePluginPortal() 8 | } 9 | 10 | plugins { 11 | val kotlinVersion: String by settings 12 | kotlin("jvm") version "$kotlinVersion" apply false 13 | id("com.jaredsburrows.license") version "0.8.42" apply false 14 | } 15 | } 16 | 17 | sourceControl { 18 | gitRepository(java.net.URI.create("https://github.com/fwcd/kotlin-language-server.git")) { 19 | producesModule("kotlin-language-server:shared") 20 | } 21 | } 22 | --------------------------------------------------------------------------------