() {
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 | MIT License
14 |
15 | Copyright (c) [year] [fullname]
16 |
17 | Permission is hereby granted, free of charge, to any person obtaining a copy
18 | of this software and associated documentation files (the "Software"), to deal
19 | in the Software without restriction, including without limitation the rights
20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
21 | copies of the Software, and to permit persons to whom the Software is
22 | furnished to do so, subject to the following conditions:
23 |
24 | The above copyright notice and this permission notice shall be included in all
25 | copies or substantial portions of the Software.
26 |
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33 | SOFTWARE.
34 |
35 |
36 | Fwcd/kotlin-language-server
37 |
38 |
39 | MIT License
40 |
41 | Copyright (c) [year] [fullname]
42 |
43 | Permission is hereby granted, free of charge, to any person obtaining a copy
44 | of this software and associated documentation files (the "Software"), to deal
45 | in the Software without restriction, including without limitation the rights
46 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
47 | copies of the Software, and to permit persons to whom the Software is
48 | furnished to do so, subject to the following conditions:
49 |
50 | The above copyright notice and this permission notice shall be included in all
51 | copies or substantial portions of the Software.
52 |
53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
54 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
55 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
56 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
57 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
58 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
59 | SOFTWARE.
60 |
61 |
62 | Error-prone annotations
63 |
64 |
65 | FindBugs-jsr305
66 |
67 |
68 | Gson
69 |
70 |
71 | Guava InternalFutureFailureAccess and InternalFutures
72 |
73 |
74 | Guava ListenableFuture only
75 |
76 |
77 | Guava: Google Core Libraries for Java
78 |
79 |
80 | IntelliJ IDEA Annotations
81 |
82 |
83 | J2ObjC Annotations
84 |
85 |
86 | Org.jetbrains.kotlin:kotlin-reflect
87 |
88 |
89 | Org.jetbrains.kotlin:kotlin-stdlib
90 |
91 |
92 | Org.jetbrains.kotlin:kotlin-stdlib-common
93 |
94 |
95 | Apache License
96 | Version 2.0, January 2004
97 | http://www.apache.org/licenses/
98 |
99 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
100 |
101 | 1. Definitions.
102 |
103 | "License" shall mean the terms and conditions for use, reproduction,
104 | and distribution as defined by Sections 1 through 9 of this document.
105 |
106 | "Licensor" shall mean the copyright owner or entity authorized by
107 | the copyright owner that is granting the License.
108 |
109 | "Legal Entity" shall mean the union of the acting entity and all
110 | other entities that control, are controlled by, or are under common
111 | control with that entity. For the purposes of this definition,
112 | "control" means (i) the power, direct or indirect, to cause the
113 | direction or management of such entity, whether by contract or
114 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
115 | outstanding shares, or (iii) beneficial ownership of such entity.
116 |
117 | "You" (or "Your") shall mean an individual or Legal Entity
118 | exercising permissions granted by this License.
119 |
120 | "Source" form shall mean the preferred form for making modifications,
121 | including but not limited to software source code, documentation
122 | source, and configuration files.
123 |
124 | "Object" form shall mean any form resulting from mechanical
125 | transformation or translation of a Source form, including but
126 | not limited to compiled object code, generated documentation,
127 | and conversions to other media types.
128 |
129 | "Work" shall mean the work of authorship, whether in Source or
130 | Object form, made available under the License, as indicated by a
131 | copyright notice that is included in or attached to the work
132 | (an example is provided in the Appendix below).
133 |
134 | "Derivative Works" shall mean any work, whether in Source or Object
135 | form, that is based on (or derived from) the Work and for which the
136 | editorial revisions, annotations, elaborations, or other modifications
137 | represent, as a whole, an original work of authorship. For the purposes
138 | of this License, Derivative Works shall not include works that remain
139 | separable from, or merely link (or bind by name) to the interfaces of,
140 | the Work and Derivative Works thereof.
141 |
142 | "Contribution" shall mean any work of authorship, including
143 | the original version of the Work and any modifications or additions
144 | to that Work or Derivative Works thereof, that is intentionally
145 | submitted to Licensor for inclusion in the Work by the copyright owner
146 | or by an individual or Legal Entity authorized to submit on behalf of
147 | the copyright owner. For the purposes of this definition, "submitted"
148 | means any form of electronic, verbal, or written communication sent
149 | to the Licensor or its representatives, including but not limited to
150 | communication on electronic mailing lists, source code control systems,
151 | and issue tracking systems that are managed by, or on behalf of, the
152 | Licensor for the purpose of discussing and improving the Work, but
153 | excluding communication that is conspicuously marked or otherwise
154 | designated in writing by the copyright owner as "Not a Contribution."
155 |
156 | "Contributor" shall mean Licensor and any individual or Legal Entity
157 | on behalf of whom a Contribution has been received by Licensor and
158 | subsequently incorporated within the Work.
159 |
160 | 2. Grant of Copyright License. Subject to the terms and conditions of
161 | this License, each Contributor hereby grants to You a perpetual,
162 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
163 | copyright license to reproduce, prepare Derivative Works of,
164 | publicly display, publicly perform, sublicense, and distribute the
165 | Work and such Derivative Works in Source or Object form.
166 |
167 | 3. Grant of Patent License. Subject to the terms and conditions of
168 | this License, each Contributor hereby grants to You a perpetual,
169 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
170 | (except as stated in this section) patent license to make, have made,
171 | use, offer to sell, sell, import, and otherwise transfer the Work,
172 | where such license applies only to those patent claims licensable
173 | by such Contributor that are necessarily infringed by their
174 | Contribution(s) alone or by combination of their Contribution(s)
175 | with the Work to which such Contribution(s) was submitted. If You
176 | institute patent litigation against any entity (including a
177 | cross-claim or counterclaim in a lawsuit) alleging that the Work
178 | or a Contribution incorporated within the Work constitutes direct
179 | or contributory patent infringement, then any patent licenses
180 | granted to You under this License for that Work shall terminate
181 | as of the date such litigation is filed.
182 |
183 | 4. Redistribution. You may reproduce and distribute copies of the
184 | Work or Derivative Works thereof in any medium, with or without
185 | modifications, and in Source or Object form, provided that You
186 | meet the following conditions:
187 |
188 | (a) You must give any other recipients of the Work or
189 | Derivative Works a copy of this License; and
190 |
191 | (b) You must cause any modified files to carry prominent notices
192 | stating that You changed the files; and
193 |
194 | (c) You must retain, in the Source form of any Derivative Works
195 | that You distribute, all copyright, patent, trademark, and
196 | attribution notices from the Source form of the Work,
197 | excluding those notices that do not pertain to any part of
198 | the Derivative Works; and
199 |
200 | (d) If the Work includes a "NOTICE" text file as part of its
201 | distribution, then any Derivative Works that You distribute must
202 | include a readable copy of the attribution notices contained
203 | within such NOTICE file, excluding those notices that do not
204 | pertain to any part of the Derivative Works, in at least one
205 | of the following places: within a NOTICE text file distributed
206 | as part of the Derivative Works; within the Source form or
207 | documentation, if provided along with the Derivative Works; or,
208 | within a display generated by the Derivative Works, if and
209 | wherever such third-party notices normally appear. The contents
210 | of the NOTICE file are for informational purposes only and
211 | do not modify the License. You may add Your own attribution
212 | notices within Derivative Works that You distribute, alongside
213 | or as an addendum to the NOTICE text from the Work, provided
214 | that such additional attribution notices cannot be construed
215 | as modifying the License.
216 |
217 | You may add Your own copyright statement to Your modifications and
218 | may provide additional or different license terms and conditions
219 | for use, reproduction, or distribution of Your modifications, or
220 | for any such Derivative Works as a whole, provided Your use,
221 | reproduction, and distribution of the Work otherwise complies with
222 | the conditions stated in this License.
223 |
224 | 5. Submission of Contributions. Unless You explicitly state otherwise,
225 | any Contribution intentionally submitted for inclusion in the Work
226 | by You to the Licensor shall be under the terms and conditions of
227 | this License, without any additional terms or conditions.
228 | Notwithstanding the above, nothing herein shall supersede or modify
229 | the terms of any separate license agreement you may have executed
230 | with Licensor regarding such Contributions.
231 |
232 | 6. Trademarks. This License does not grant permission to use the trade
233 | names, trademarks, service marks, or product names of the Licensor,
234 | except as required for reasonable and customary use in describing the
235 | origin of the Work and reproducing the content of the NOTICE file.
236 |
237 | 7. Disclaimer of Warranty. Unless required by applicable law or
238 | agreed to in writing, Licensor provides the Work (and each
239 | Contributor provides its Contributions) on an "AS IS" BASIS,
240 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
241 | implied, including, without limitation, any warranties or conditions
242 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
243 | PARTICULAR PURPOSE. You are solely responsible for determining the
244 | appropriateness of using or redistributing the Work and assume any
245 | risks associated with Your exercise of permissions under this License.
246 |
247 | 8. Limitation of Liability. In no event and under no legal theory,
248 | whether in tort (including negligence), contract, or otherwise,
249 | unless required by applicable law (such as deliberate and grossly
250 | negligent acts) or agreed to in writing, shall any Contributor be
251 | liable to You for damages, including any direct, indirect, special,
252 | incidental, or consequential damages of any character arising as a
253 | result of this License or out of the use or inability to use the
254 | Work (including but not limited to damages for loss of goodwill,
255 | work stoppage, computer failure or malfunction, or any and all
256 | other commercial damages or losses), even if such Contributor
257 | has been advised of the possibility of such damages.
258 |
259 | 9. Accepting Warranty or Additional Liability. While redistributing
260 | the Work or Derivative Works thereof, You may choose to offer,
261 | and charge a fee for, acceptance of support, warranty, indemnity,
262 | or other liability obligations and/or rights consistent with this
263 | License. However, in accepting such obligations, You may act only
264 | on Your own behalf and on Your sole responsibility, not on behalf
265 | of any other Contributor, and only if You agree to indemnify,
266 | defend, and hold each Contributor harmless for any liability
267 | incurred by, or claims asserted against, such Contributor by reason
268 | of your accepting any such warranty or additional liability.
269 |
270 | END OF TERMS AND CONDITIONS
271 |
272 | APPENDIX: How to apply the Apache License to your work.
273 |
274 | To apply the Apache License to your work, attach the following
275 | boilerplate notice, with the fields enclosed by brackets "[]"
276 | replaced with your own identifying information. (Don't include
277 | the brackets!) The text should be enclosed in the appropriate
278 | comment syntax for the file format. We also recommend that a
279 | file or class name and description of purpose be included on the
280 | same "printed page" as the copyright notice for easier
281 | identification within third-party archives.
282 |
283 | Copyright [yyyy] [name of copyright owner]
284 |
285 | Licensed under the Apache License, Version 2.0 (the "License");
286 | you may not use this file except in compliance with the License.
287 | You may obtain a copy of the License at
288 |
289 | http://www.apache.org/licenses/LICENSE-2.0
290 |
291 | Unless required by applicable law or agreed to in writing, software
292 | distributed under the License is distributed on an "AS IS" BASIS,
293 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
294 | See the License for the specific language governing permissions and
295 | limitations under the License.
296 |
297 |
298 | Animal Sniffer Annotations
299 |
300 | MIT license
301 | http://www.opensource.org/licenses/mit-license.php
302 |
303 | Eclipse Xbase Runtime Library
304 |
305 |
306 | Eclipse Xtend Active Annotations Library
307 |
308 |
309 | Eclipse Xtend Runtime Library
310 |
311 | Eclipse Public License, Version 2.0
312 | http://www.eclipse.org/legal/epl-2.0
313 |
314 | LSP4J Debug
315 |
316 |
317 | LSP4J Generator
318 |
319 |
320 | LSP4J JSON-RPC
321 |
322 |
323 | LSP4J JSON-RPC Debug
324 |
325 | Eclipse Public License, Version 2.0
326 | https://www.eclipse.org/legal/epl-2.0
327 |
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 |
--------------------------------------------------------------------------------