Skip to content

Commit f2bd492

Browse files
committed
feat: support cwd anv env variables
1 parent c987ae2 commit f2bd492

File tree

19 files changed

+923
-30
lines changed

19 files changed

+923
-30
lines changed

.vscode/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"java.configuration.updateBuildConfiguration": "interactive"
2+
"java.configuration.updateBuildConfiguration": "disabled"
33
}

adapter/build.gradle

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ startScripts {
1919
repositories {
2020
mavenCentral()
2121
maven { url 'https://jitpack.io' }
22+
maven { url 'https://repo.eclipse.org/content/groups/releases/' }
23+
2224
}
2325

2426
dependencies {
@@ -29,6 +31,17 @@ dependencies {
2931
implementation 'com.github.fwcd.kotlin-language-server:shared:229c762a4d75304d21eba6d8e1231ed949247629'
3032
// The Java Debug Interface classes (com.sun.jdi.*)
3133
implementation files("${System.properties['java.home']}/../lib/tools.jar")
34+
// The Implementation of jdi by eclipse
35+
// implementation group: 'org.eclipse.jdt', name: 'org.eclipse.jdt.debug', version: '3.15.100'
36+
// Failed to use the one above because jdi.jar and jdimodel.jar are sub jars included in org.eclipse.jdt.debug-3.15.100.jar,
37+
// and cannot be recognized correctly.
38+
// TODO: a fix for this
39+
implementation files('lib/org.eclipse.jdt.debug-3.15.100.jdi.jar')
40+
implementation files('lib/org.eclipse.jdt.debug-3.15.100.jdimodel.jar')
41+
// For org.eclipse.osgi.util.NLS used in eclipse.jdt.debug
42+
implementation group: 'org.eclipse.platform', name: 'org.eclipse.osgi', version: '3.15.200'
43+
// For com.ibm.icu.text.DateFormat used in eclipse.jdt.debug
44+
implementation group: 'at.bestsolution.eclipse', name: 'com.ibm.icu.base', version: '54.1.1'
3245
testImplementation 'junit:junit:4.12'
3346
testImplementation 'org.hamcrest:hamcrest-all:1.3'
3447
}
Binary file not shown.
Binary file not shown.
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package com.sun.tools.jdi
2+
3+
import com.sun.jdi.Bootstrap
4+
import com.sun.jdi.VirtualMachine
5+
import com.sun.jdi.connect.Connector
6+
import com.sun.jdi.connect.IllegalConnectorArgumentsException
7+
import com.sun.jdi.connect.VMStartException
8+
import com.sun.jdi.connect.spi.Connection
9+
import com.sun.jdi.connect.spi.TransportService
10+
import java.io.File
11+
import java.io.IOException
12+
import java.io.InterruptedIOException
13+
import java.net.URLDecoder
14+
import java.net.URLEncoder
15+
import java.nio.charset.StandardCharsets
16+
import java.util.*
17+
18+
internal const val ARG_HOME = "home"
19+
internal const val ARG_OPTIONS = "options"
20+
internal const val ARG_MAIN = "main"
21+
internal const val ARG_INIT_SUSPEND = "suspend"
22+
internal const val ARG_QUOTE = "quote"
23+
internal const val ARG_VM_EXEC = "vmexec"
24+
internal const val ARG_CWD = "cwd"
25+
internal const val ARG_ENVS = "envs"
26+
27+
28+
class KDACommandLineLauncher : SunCommandLineLauncher {
29+
30+
companion object {
31+
32+
fun urlEncode(arg: Collection<String>?) = arg
33+
?.map { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) }
34+
?.fold("") { a, b -> "$a\n$b" }
35+
36+
fun urlDecode(arg: String?) = arg
37+
?.trim('\n')
38+
?.split("\n")
39+
?.map { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) }
40+
?.toList()
41+
}
42+
43+
constructor() : super() {
44+
addStringArgument(
45+
ARG_CWD,
46+
ARG_CWD,
47+
"Current working directory",
48+
"",
49+
false)
50+
addStringArgument(
51+
ARG_ENVS,
52+
ARG_ENVS,
53+
"Environment variables",
54+
"",
55+
false)
56+
}
57+
58+
override fun name(): String? {
59+
return this.javaClass.name
60+
}
61+
62+
override fun description(): String? {
63+
return "A custom launcher supporting cwd and env variables"
64+
}
65+
66+
/**
67+
* Copied from SunCommandLineLauncher.java and added cwd / env processing logic
68+
*/
69+
@Throws(IOException::class, IllegalConnectorArgumentsException::class, VMStartException::class)
70+
override fun launch(arguments: Map<String, Connector.Argument>): VirtualMachine {
71+
val vm: VirtualMachine
72+
73+
val home = argument(ARG_HOME, arguments).value()
74+
val options = argument(ARG_OPTIONS, arguments).value()
75+
val mainClassAndArgs = argument(ARG_MAIN, arguments).value()
76+
val wait = (argument(ARG_INIT_SUSPEND,
77+
arguments) as BooleanArgumentImpl).booleanValue()
78+
val quote = argument(ARG_QUOTE, arguments).value()
79+
val exe = argument(ARG_VM_EXEC, arguments).value()
80+
val cwd = argument(ARG_CWD, arguments).value()
81+
val envs = argument(ARG_ENVS, arguments).value()?.let { urlDecode(it) }?.toTypedArray()
82+
var exePath: String?
83+
if (quote.length > 1) {
84+
throw IllegalConnectorArgumentsException("Invalid length",
85+
ARG_QUOTE)
86+
}
87+
if (options.indexOf("-Djava.compiler=") != -1 &&
88+
options.toLowerCase().indexOf("-djava.compiler=none") == -1) {
89+
throw IllegalConnectorArgumentsException("Cannot debug with a JIT compiler",
90+
ARG_OPTIONS)
91+
}
92+
93+
/*
94+
* Start listening.
95+
* If we're using the shared memory transport then we pick a
96+
* random address rather than using the (fixed) default.
97+
* Random() uses System.currentTimeMillis() as the seed
98+
* which can be a problem on windows (many calls to
99+
* currentTimeMillis can return the same value), so
100+
* we do a few retries if we get an IOException (we
101+
* assume the IOException is the filename is already in use.)
102+
*/
103+
var listenKey: TransportService.ListenKey
104+
if (usingSharedMemory) {
105+
val rr = Random()
106+
var failCount = 0
107+
while (true) {
108+
try {
109+
val address = "javadebug" + rr.nextInt(100000).toString()
110+
listenKey = transportService().startListening(address)
111+
break
112+
} catch (ioe: IOException) {
113+
if (++failCount > 5) {
114+
throw ioe
115+
}
116+
}
117+
}
118+
} else {
119+
listenKey = transportService().startListening()
120+
}
121+
val address = listenKey.address()
122+
try {
123+
exePath = if (home.length > 0) {
124+
home + File.separator + "bin" + File.separator + exe
125+
} else {
126+
exe
127+
}
128+
// Quote only if necessary in case the quote arg value is bogus
129+
if (hasWhitespace(exePath)) {
130+
exePath = quote + exePath + quote
131+
}
132+
var xrun = "transport=" + transport().name() +
133+
",address=" + address +
134+
",suspend=" + if (wait) 'y' else 'n'
135+
// Quote only if necessary in case the quote arg value is bogus
136+
if (hasWhitespace(xrun)) {
137+
xrun = quote + xrun + quote
138+
}
139+
val command = exePath + ' ' +
140+
options + ' ' +
141+
"-Xdebug " +
142+
"-Xrunjdwp:" + xrun + ' ' +
143+
mainClassAndArgs
144+
145+
vm = launch(commandArray = tokenizeCommand(command, quote[0]), listenKey = listenKey,
146+
ts = transportService(), cwd = cwd, envs = envs, grp = grp
147+
)
148+
} finally {
149+
transportService().stopListening(listenKey)
150+
}
151+
return vm
152+
}
153+
154+
@Throws(IOException::class, VMStartException::class)
155+
fun launch(commandArray: Array<String>,
156+
listenKey: TransportService.ListenKey,
157+
ts: TransportService, cwd: String?, envs: Array<String>? = null, grp: ThreadGroup): VirtualMachine {
158+
val helper = Helper(commandArray, listenKey, ts, cwd = cwd, envs = envs, grp = grp)
159+
helper.launchAndAccept()
160+
val manager = Bootstrap.virtualMachineManager()
161+
return manager.createVirtualMachine(helper.connection(),
162+
helper.process())
163+
}
164+
165+
/**
166+
*
167+
* Copied from com.sun.tools.jdi.AbstractLauncher.Helper. Add cwd support.
168+
*
169+
* This class simply provides a context for a single launch and
170+
* accept. It provides instance fields that can be used by
171+
* all threads involved. This stuff can't be in the Connector proper
172+
* because the connector is a singleton and is not specific to any
173+
* one launch.
174+
*/
175+
class Helper internal constructor(private val commandArray: Array<String>, private val listenKey: TransportService.ListenKey,
176+
private val ts: TransportService, private val cwd: String? = null, private val envs: Array<String>? = null, private val grp: ThreadGroup) {
177+
private var process: Process? = null
178+
private var connection: Connection? = null
179+
private var acceptException: IOException? = null
180+
private var exited = false
181+
182+
/**
183+
* for wait()/notify()
184+
*/
185+
private val lock: java.lang.Object = Object()
186+
187+
fun commandString(): String {
188+
var str = ""
189+
for (i in commandArray.indices) {
190+
if (i > 0) {
191+
str += " "
192+
}
193+
str += commandArray[i]
194+
}
195+
return str
196+
}
197+
198+
@Throws(IOException::class, VMStartException::class)
199+
fun launchAndAccept() {
200+
synchronized(lock) {
201+
process = Runtime.getRuntime().exec(commandArray, envs, cwd?.let { File(it) })
202+
val acceptingThread = acceptConnection()
203+
val monitoringThread = monitorTarget()
204+
try {
205+
while (connection == null &&
206+
acceptException == null &&
207+
!exited) {
208+
lock.wait()
209+
}
210+
if (exited) {
211+
throw VMStartException(
212+
"VM initialization failed for: " + commandString(), process)
213+
}
214+
if (acceptException != null) {
215+
// Rethrow the exception in this thread
216+
throw acceptException ?: IOException("acceptException")
217+
}
218+
} catch (e: InterruptedException) {
219+
throw InterruptedIOException("Interrupted during accept")
220+
} finally {
221+
acceptingThread.interrupt()
222+
monitoringThread.interrupt()
223+
}
224+
}
225+
}
226+
227+
fun process(): Process? {
228+
return process
229+
}
230+
231+
fun connection(): Connection? {
232+
return connection
233+
}
234+
235+
fun notifyOfExit() {
236+
synchronized(lock) {
237+
exited = true
238+
lock.notify()
239+
}
240+
}
241+
242+
fun notifyOfConnection(connection: Connection?) {
243+
synchronized(lock) {
244+
this.connection = connection
245+
lock.notify()
246+
}
247+
}
248+
249+
fun notifyOfAcceptException(acceptException: IOException?) {
250+
synchronized(lock) {
251+
this.acceptException = acceptException
252+
lock.notify()
253+
}
254+
}
255+
256+
fun monitorTarget(): Thread {
257+
val thread: Thread = object : Thread(grp,
258+
"launched target monitor") {
259+
override fun run() {
260+
try {
261+
process!!.waitFor()
262+
/*
263+
* Notify waiting thread of VM error termination
264+
*/notifyOfExit()
265+
} catch (e: InterruptedException) {
266+
// Connection has been established, stop monitoring
267+
}
268+
}
269+
}
270+
thread.isDaemon = true
271+
thread.start()
272+
return thread
273+
}
274+
275+
fun acceptConnection(): Thread {
276+
val thread: Thread = object : Thread(grp,
277+
"connection acceptor") {
278+
override fun run() {
279+
try {
280+
val connection = ts.accept(listenKey, 0, 0)
281+
/*
282+
* Notify waiting thread of connection
283+
*/notifyOfConnection(connection)
284+
} catch (e: InterruptedIOException) {
285+
// VM terminated, stop accepting
286+
} catch (e: IOException) {
287+
// Report any other exception to waiting thread
288+
notifyOfAcceptException(e)
289+
}
290+
}
291+
}
292+
thread.isDaemon = true
293+
thread.start()
294+
return thread
295+
}
296+
297+
}
298+
}

adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.javacs.ktda.adapter
22

3+
import com.sun.tools.jdi.KDACommandLineLauncher
34
import java.util.concurrent.CompletableFuture
45
import java.util.concurrent.CompletableFuture.completedFuture
56
import java.io.InputStream
@@ -100,13 +101,19 @@ class KotlinDebugAdapter(
100101

101102
val vmArguments = (args["vmArguments"] as? String) ?: ""
102103

104+
var cwd = (args["cwd"] as? String).let { if(it.isNullOrBlank()) projectRoot else Paths.get(it) }
105+
106+
var envs = args["envs"] as? List<String>
107+
103108
setupCommonInitializationParams(args)
104109

105110
val config = LaunchConfiguration(
106111
debugClassPathResolver(listOf(projectRoot)).classpathOrEmpty,
107112
mainClass,
108113
projectRoot,
109-
vmArguments
114+
vmArguments,
115+
cwd,
116+
envs
110117
)
111118
debuggee = launcher.launch(
112119
config,

adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@ class LaunchConfiguration(
66
val classpath: Set<Path>,
77
val mainClass: String,
88
val projectRoot: Path,
9-
val vmArguments: String = ""
9+
val vmArguments: String = "",
10+
val cwd: Path = projectRoot,
11+
val envs: Collection<String>? = null
1012
)

0 commit comments

Comments
 (0)