Implement support for the Debug Adapter Protocol#944
Implement support for the Debug Adapter Protocol#944jvican merged 86 commits intoscalacenter:masterfrom mzarnowski:dap-integration
Conversation
tgodzik
left a comment
There was a problem hiding this comment.
Added some preliminary comments - let me know when you want the full review
This prevents the files being served in non-deterministic order
This commit is the result of a pair-programming session between me and Marek where we went through some of the previous abstractions and simplified them. Now, there are only two core abstractions `DebugServer` and a `DebugSession`. Everything else reuses the machinery created by `run`. Co-authored-by: Marek Żarnowski <mzarnowski@virtuslab.com>
`ConnectionHandle` is not a handle when it holds the state of the connection. Therefore, this commit removes any state from it so that the client is responsible for opening and closing the socket. This simplifies reasoning about what does `ConnectionHandle` do. Furthermore, as connection handle is too generic of a name, we settle on the `ServerHandle` name which makes it more clear that this handle is mapped to a server instead of a client connection.
The listening logic before was a while loop that lacked any communication with the current spawned session. Since most of the times there should only be one open connection and we cannot process more while that one is running, there is no use in having an independent task blocking on `serverSocket.accept()`. Instead, what we do is that we only have one task listening and then serving a client. When the DAP client signals that it's interested in restarting the session, then we recurse (safely, because of Task's trampolining) and start accepting connections again.
jvican
left a comment
There was a problem hiding this comment.
Excellent work @marek1840 🔥 💯 I'm really excited with the current state of the implementation, it's obvious you have put a lot of effort into implementing this and improving it from version to version. I don't have any more feedback to add aside from some comments on testing that can be addressed in a follow-up PR.
I'm going to be merging this as soon as the bsp artifacts hit Maven Central and CI passes. 🎉 Thanks a lot for this! Let's schedule a discussion tomorrow to discuss over some of the suggestions I leave in my comments.
|
|
||
| val lines = output.lines.toSeq | ||
| assert(lines.size == 2) | ||
| assert(lines(0).contains("Running Tests")) |
There was a problem hiding this comment.
Could we use assertNoDiff here instead of manually asserting?
Also, I don't understand this test. Here's a few questions:
- Why is our ouput only showing test output for one test suite?
- Why are we passing the test filters?
- Are those filters acting as some kind of blacklist? If so, why? The semantics of test filters is that they specify test suites that must be included in the test execution, effectively acting as a whitelist, this seems contradictory from what I read in the test suite.
- Why does removing those filters cause the test to deadlock?
There was a problem hiding this comment.
- because of a bug and my ill understanding of what happened ;)
- Now, we use them to verify the output
- those are exactly the same kind of filters as expected by the
buildTarget/testendpoints - I am not sure, I could not reproduce. Can you verify it still happens with the newest version?
| } | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Can we add a test when we restart the execution of a test suite while the program is still running tests suites? Maybe also think of other test cases we might want to be testing? Like what happens when things go wrong?
There was a problem hiding this comment.
DebugServerSpec.restarting closes current client and debuggee should verify that both the connection and the debuggee (a task obtained from TestTask.runTestSuites) are cancelled.
It's true, I have not verified if this task cancels correctly - I will look into it soon.
There was a problem hiding this comment.
in short, I was thinking of DebugServerSpec as a test suite which abstracts over the debuggee and verifies the server itself - something closer to unit tests than integration tests. DebugProtocolSpec is used to verify that the actual protocol works correctly - those are the "real" integration tests.
| } | ||
| } | ||
|
|
||
| test("picks up source changes across sessions") { |
There was a problem hiding this comment.
While thinking about more scenarios for our test cases, I've realized that we're only picking up source changes across sessions. So, a restart action from vscode will not pick up any changes to the sources, it will instead just force a new start of the VM with the same class files. Let's have a call tomorrow about how we can work around this.
There was a problem hiding this comment.
here is a small PoC for recompiling sources before starting the debuggee: https://github.com/marek1840/bloop/pull/4
| val jdiStatus = if (JavaEnv.resolveDebugInterface.isSuccess) "available" else "unavailable" | ||
| logger.info(s"Java Debug interface is $jdiStatus") | ||
| logger.info(s"Running on Java v$javaVersion ($javaHome)") | ||
| logger.info(s"Detected Java ${JavaEnv.detectRuntime} runtime $jdiStatus") |
There was a problem hiding this comment.
Since the message in Feedback explicitly mentions Java Debug Interface, I think we should mention it here as well so we can avoid the confusion.
Also "Detected Java JRE runtime ..." sounds a bit off when dropped the abbreviation:
Detected Java Java Runtime Environment runtime ...
There was a problem hiding this comment.
maybe we could display the JDK/JRE info with the java version and leave this line for JDI exclusively?
| val jdiStatus = | ||
| if (JavaEnv.loadJavaDebugInterface.isSuccess) "supports debugging" | ||
| else "doesn't support debugging!" | ||
| val runtimeInfo = s"Detected Java ${JavaEnv.detectRuntime} runtime $jdiStatus" |
There was a problem hiding this comment.
not used
| val runtimeInfo = s"Detected Java ${JavaEnv.detectRuntime} runtime $jdiStatus" |
It seems this event might not be emitted when we disconnect from the JVM
Previously the response might not have been sent, because of a race condition between: 1. asynchronously sending a response to the client 2. receiving a terminated event from the debuggee, finishing communication done promise and, as a result, triggering the shutdown sequence which closes the socket
|
@marek1840 I think we're ready to merge this! Thanks for all of your hard work. 👍 I have added a documentation page in our website talking about the DAP <> BSP implementation in Bloop. I have added a limitations section that explains what users might expect from this integration and current gotchas. Given that there are some important things missing in our current debug implementation, I'll omit this feature in 1.3.3 release notes. Between 1.3.3 and 1.4.0, we can work on:
I'll take care of adding breakpoint support, I want to get more familiar with the debugging JDI APIs and make sure I understand them well enough to maintain them during the next years. @marek1840 Could you look at the second item? We should have a chat to discuss what the best way to implement that is because there are several solutions. After that, I propose you work on the debugging UX side of things in Metals as well as the unified debug/test/run interface. That's where most of the impact is to be made. Let me know what your thoughts are when you come back from vacation! 😄 |
It looks the previous logic would fail in in certain Windows systems where the Windows Search is disabled, with a similar error to this one: ``` java.lang.IllegalStateException: Missing java executable at C:\Program Files\Java\jdk1.8.0_202\jre\bin\java! bloop.exec.JvmForker.javaExecutable(JvmProcessForker.scala:133) bloop.exec.JvmForker.runMain(JvmProcessForker.scala:114) bloop.exec.JvmProcessForker.runMain(JvmProcessForker.scala:56) bloop.exec.JvmProcessForker.runMain$(JvmProcessForker.scala:44) bloop.exec.JvmForker.runMain(JvmProcessForker.scala:90) ``` This bug intends to avoid throwing that exception by resolving `java.exe` instead of just `java`.
|
One limitation I forgot to add to our docs is that restarting a session via DAP doesn't trigger compilation of the involved projects. This is an intentional design decision. BSP clients can decide how to handle this particular scenario. For clients such as Metals that are proxying DAP communication from the BSP server to the end client (VS Code), this limitation can be avoided by intercepting the DAP restart request from VS Code, closing the current DAP session, triggering a compile and then starting a new DAP session. This is the ideal way to solve this problem because Metals will be aware there's a compilation going on and will correctly report diagnostics to the user as well as results and timings. If restarting a DAP session implied recompiling a project, its implementation in bloop and Metals would be more complicated. It's better to make BSP compile requests explicit in the protocol and not rely on action side effects of the DAP request. |
|
@jvican Good to know, thanks for sharing! |
mzarnowski
left a comment
There was a problem hiding this comment.
I have noticed some discrepancies, mostly in the docs.
| } finally { | ||
| // "exited" event may not be sent if the debugger disconnects from the jvm beforehand | ||
| remainingTerminalEvents.remove("exited") | ||
| // Exited event should not be expected if debugger has already disconnected from the JVM |
There was a problem hiding this comment.
this comment tells "what" and not "why", and "what" can be read in the expectedTerminalEvents.remove("exited") statement. In my experience it is better to explain - at least partially - why we are doing something instead of repeating what we are doing, but using different words.
| listener: InetSocketAddress => Unit, | ||
| underlying: Logger | ||
| underlying: Logger, | ||
| initialized0: Option[Atomic[Boolean]] = None |
There was a problem hiding this comment.
if we make it Atomic[Boolean] = Atomic(false) then the field initialized is unnecessary and this parameter can lose the 0 suffix
| sidebar_label: Debugging Reference | ||
| --- | ||
|
|
||
| Bloop allows [BSP][bsp] build clients to start a [DAP][dap] (Debugging |
There was a problem hiding this comment.
bsp build clients == build server protocol build client. we can drop the second build
| sidebar_label: Debugging Reference | ||
| --- | ||
|
|
||
| Bloop allows [BSP][bsp] build clients to start a [DAP][dap] (Debugging |
There was a problem hiding this comment.
DAP stands for Debug Adapter Protocol, not Debugging Adapter Protocol
| Bloop allows [BSP][bsp] build clients to start a [DAP][dap] (Debugging | ||
| Adapter Protocol) session to run a project or its tests. The core of the | ||
| debugging implementation in Bloop relies on Microsoft's [java-debug][] | ||
| library and it's exposed through [DAP][dap] support in [BSP][bsp]. |
| def targets: List[BuildTargetIdentifier] | ||
|
|
||
| /** The kind of data to expect in the `data` field. Mandatory. */ | ||
| def dataKind: Option[String] |
There was a problem hiding this comment.
If it is Mandatory, then it should be of type String, not Option[String]
| def dataKind: Option[String] | ||
|
|
||
| /** A language-agnostic JSON object interpreted by the server. Mandatory. */ | ||
| def data: Option[Json] |
There was a problem hiding this comment.
If it is Mandatory, then it should be of type String, not Option[String]
| The data field contains language-agnostic information about what action | ||
| should the server debug. Bloop supports only two data kinds: | ||
|
|
||
| 1. `scala-main-class` when `data` contains [`ScalaRunParams`]( https://github.com/scalacenter/bsp/blob/master/docs/bsp.md#scala-run-params). |
There was a problem hiding this comment.
the link leads nowhere.
Also, see the implementation:
scala-main-class corresponds to ScalaMainClass, not ScalaRunParams.
scala-test-suites corresponds to a list of strings (test suites), not ScalaTestParams
No description provided.