Skip to content

Commit 15203b1

Browse files
committed
feat: support cwd anv env variables using sun jdi
1 parent c987ae2 commit 15203b1

File tree

13 files changed

+403
-26
lines changed

13 files changed

+403
-26
lines changed

adapter/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ dependencies {
2929
implementation 'com.github.fwcd.kotlin-language-server:shared:229c762a4d75304d21eba6d8e1231ed949247629'
3030
// The Java Debug Interface classes (com.sun.jdi.*)
3131
implementation files("${System.properties['java.home']}/../lib/tools.jar")
32+
// For CommandLineUtils.translateCommandLine
33+
// https://mvnrepository.com/artifact/org.codehaus.plexus/plexus-utils
34+
implementation 'org.codehaus.plexus:plexus-utils:3.3.0'
3235
testImplementation 'junit:junit:4.12'
3336
testImplementation 'org.hamcrest:hamcrest-all:1.3'
3437
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,19 @@ class KotlinDebugAdapter(
100100

101101
val vmArguments = (args["vmArguments"] as? String) ?: ""
102102

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

105109
val config = LaunchConfiguration(
106110
debugClassPathResolver(listOf(projectRoot)).classpathOrEmpty,
107111
mainClass,
108112
projectRoot,
109-
vmArguments
113+
vmArguments,
114+
cwd,
115+
envs
110116
)
111117
debuggee = launcher.launch(
112118
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
)

adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import org.javacs.kt.LOG
44
import org.javacs.ktda.core.launch.DebugLauncher
55
import org.javacs.ktda.core.launch.LaunchConfiguration
66
import org.javacs.ktda.core.launch.AttachConfiguration
7-
import org.javacs.ktda.core.Debuggee
87
import org.javacs.ktda.core.DebugContext
98
import org.javacs.ktda.util.KotlinDAException
109
import org.javacs.ktda.jdi.JDIDebuggee
@@ -16,14 +15,11 @@ import com.sun.jdi.connect.AttachingConnector
1615
import java.io.File
1716
import java.nio.file.Path
1817
import java.nio.file.Files
19-
import java.net.URLEncoder
20-
import java.net.URLDecoder
21-
import java.nio.charset.StandardCharsets
2218
import java.util.stream.Collectors
19+
import org.javacs.kt.LOG
2320

2421
class JDILauncher(
2522
private val attachTimeout: Int = 50,
26-
private val vmArguments: String? = null,
2723
private val modulePaths: String? = null
2824
) : DebugLauncher {
2925
private val vmManager: VirtualMachineManager
@@ -57,6 +53,8 @@ class JDILauncher(
5753
args["suspend"]!!.setValue("true")
5854
args["options"]!!.setValue(formatOptions(config))
5955
args["main"]!!.setValue(formatMainClass(config))
56+
args["cwd"]!!.setValue(config.cwd.toAbsolutePath().toString())
57+
args["envs"]!!.setValue(KDACommandLineLauncher.urlEncode(config.envs) ?: "")
6058
}
6159

6260
private fun createAttachArgs(config: AttachConfiguration, connector: Connector): Map<String, Connector.Argument> = connector.defaultArguments()
@@ -71,8 +69,8 @@ class JDILauncher(
7169
?: throw KotlinDAException("Could not find an attaching connector (for a new debuggee VM)")
7270

7371
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() }
72+
// Using our own connector to support cwd and envs
73+
.let { it.find { it.name().equals(KDACommandLineLauncher::class.java.name) } ?: it.firstOrNull() }
7674
?: throw KotlinDAException("Could not find a launching connector (for a new debuggee VM)")
7775

7876
private fun sourcesRootsOf(projectRoot: Path): Set<Path> = projectRoot.resolve("src")
@@ -100,13 +98,5 @@ class JDILauncher(
10098
private fun formatClasspath(config: LaunchConfiguration): String = config.classpath
10199
.map { it.toAbsolutePath().toString() }
102100
.reduce { prev, next -> "$prev${File.pathSeparatorChar}$next" }
103-
104-
private fun urlEncode(arg: Collection<String>?) = arg
105-
?.map { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) }
106-
?.reduce { a, b -> "$a\n$b" }
107-
108-
private fun urlDecode(arg: String?) = arg
109-
?.split("\n")
110-
?.map { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) }
111-
?.toList()
101+
112102
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package org.javacs.ktda.jdi.launch
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.Transport
8+
import com.sun.jdi.connect.VMStartException
9+
import com.sun.jdi.connect.spi.Connection
10+
import com.sun.jdi.connect.spi.TransportService
11+
import com.sun.tools.jdi.SocketTransportService
12+
import com.sun.tools.jdi.SunCommandLineLauncher
13+
import org.codehaus.plexus.util.cli.CommandLineUtils
14+
import org.javacs.kt.LOG
15+
import java.io.File
16+
import java.io.IOException
17+
import java.net.URLDecoder
18+
import java.net.URLEncoder
19+
import java.nio.charset.StandardCharsets
20+
import java.nio.file.Files
21+
import java.nio.file.Paths
22+
import java.util.concurrent.Callable
23+
import java.util.concurrent.Executors
24+
import java.util.concurrent.TimeUnit
25+
26+
internal const val ARG_HOME = "home"
27+
internal const val ARG_OPTIONS = "options"
28+
internal const val ARG_MAIN = "main"
29+
internal const val ARG_SUSPEND = "suspend"
30+
internal const val ARG_QUOTE = "quote"
31+
internal const val ARG_VM_EXEC = "vmexec"
32+
internal const val ARG_CWD = "cwd"
33+
internal const val ARG_ENVS = "envs"
34+
35+
/**
36+
* A custom LaunchingConnector that supports cwd and env variables
37+
*/
38+
open class KDACommandLineLauncher : SunCommandLineLauncher {
39+
40+
protected val defaultArguments = mutableMapOf<String, Connector.Argument>()
41+
42+
/**
43+
* We only support SocketTransportService
44+
*/
45+
protected val transportService = SocketTransportService()
46+
protected val transport = Transport { "dt_socket" }
47+
48+
companion object {
49+
50+
fun urlEncode(arg: Collection<String>?) = arg
51+
?.map { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) }
52+
?.fold("") { a, b -> "$a\n$b" }
53+
54+
fun urlDecode(arg: String?) = arg
55+
?.trim('\n')
56+
?.split("\n")
57+
?.map { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) }
58+
?.toList()
59+
}
60+
61+
constructor() : super() {
62+
63+
defaultArguments.putAll(super.defaultArguments())
64+
65+
defaultArguments[ARG_CWD] = StringArgument(
66+
name = ARG_CWD,
67+
description = "Current working directory")
68+
69+
defaultArguments[ARG_ENVS] = StringArgument(
70+
name = ARG_ENVS,
71+
description = "Environment variables")
72+
}
73+
74+
override fun name(): String {
75+
return this.javaClass.name
76+
}
77+
78+
override fun description(): String {
79+
return "A custom launcher supporting cwd and env variables"
80+
}
81+
82+
override fun defaultArguments(): Map<String, Connector.Argument> {
83+
return this.defaultArguments
84+
}
85+
86+
override fun toString(): String {
87+
return name()
88+
}
89+
90+
protected fun getOrDefault(arguments: Map<String, Connector.Argument>, argName: String): String {
91+
return arguments[argName]?.value() ?: defaultArguments[argName]?.value() ?: ""
92+
}
93+
94+
/**
95+
* A customized method to launch the vm and connect to it, supporting cwd and env variables
96+
*/
97+
@Throws(IOException::class, IllegalConnectorArgumentsException::class, VMStartException::class)
98+
override fun launch(arguments: Map<String, Connector.Argument>): VirtualMachine {
99+
val vm: VirtualMachine
100+
101+
val home = getOrDefault(arguments, ARG_HOME)
102+
val options = getOrDefault(arguments, ARG_OPTIONS)
103+
val main = getOrDefault(arguments, ARG_MAIN)
104+
val suspend = getOrDefault(arguments, ARG_SUSPEND).toBoolean()
105+
val quote = getOrDefault(arguments, ARG_QUOTE)
106+
var exe = getOrDefault(arguments, ARG_VM_EXEC)
107+
val cwd = getOrDefault(arguments, ARG_CWD)
108+
val envs = urlDecode(getOrDefault(arguments, ARG_ENVS))?.toTypedArray()
109+
110+
check(quote.length == 1) {"Invalid length for $ARG_QUOTE: $quote"}
111+
check(!options.contains("-Djava.compiler=") ||
112+
options.toLowerCase().contains("-djava.compiler=none")) { "Cannot debug with a JIT compiler. $ARG_OPTIONS: $options"}
113+
114+
val listenKey = transportService.startListening()
115+
val address = listenKey.address()
116+
117+
try {
118+
val command = StringBuilder()
119+
120+
exe = if (home.isNotEmpty()) Paths.get(home, "bin", exe).toString() else exe
121+
command.append(wrapWhitespace(exe))
122+
123+
command.append(" $options")
124+
125+
//debug options
126+
command.append(" -agentlib:jdwp=transport=${transport.name()},address=$address,server=n,suspend=${if (suspend) 'y' else 'n'}")
127+
128+
command.append(" $main")
129+
130+
LOG.debug("command before tokenize: $command")
131+
132+
vm = launch(commandArray = CommandLineUtils.translateCommandline(command.toString()), listenKey = listenKey,
133+
ts = transportService, cwd = cwd, envs = envs
134+
)
135+
136+
} finally {
137+
transportService.stopListening(listenKey)
138+
}
139+
return vm
140+
}
141+
142+
internal fun wrapWhitespace(str: String): String {
143+
return if(str.contains(' ')) "\"$str\" " else str
144+
}
145+
146+
@Throws(IOException::class, VMStartException::class)
147+
fun launch(commandArray: Array<String>,
148+
listenKey: TransportService.ListenKey,
149+
ts: TransportService, cwd: String, envs: Array<String>? = null): VirtualMachine {
150+
151+
val (connection, process) = launchAndConnect(commandArray, listenKey, ts, cwd = cwd, envs = envs)
152+
153+
return Bootstrap.virtualMachineManager().createVirtualMachine(connection,
154+
process)
155+
}
156+
157+
158+
/**
159+
* launch the command, connect to transportService, and returns the connection / process pair
160+
*/
161+
protected fun launchAndConnect(commandArray: Array<String>, listenKey: TransportService.ListenKey,
162+
ts: TransportService, cwd: String = "", envs: Array<String>? = null): Pair<Connection, Process>{
163+
164+
val dir = if(cwd.isNotBlank() && Files.isDirectory(Paths.get(cwd))) File(cwd) else null
165+
166+
var threadCount = 0
167+
168+
val executors = Executors.newFixedThreadPool(2) { Thread(it, "${this.javaClass.simpleName}-${threadCount++}") }
169+
val process = Runtime.getRuntime().exec(commandArray, envs, dir)
170+
171+
val connectionTask: Callable<Any> = Callable { ts.accept(listenKey, 0,0).also { LOG.debug("ts.accept invoked") } }
172+
val exitCodeTask: Callable<Any> = Callable { process.waitFor().also { LOG.debug("process.waitFor invoked") } }
173+
174+
try {
175+
when (val result = executors.invokeAny(listOf(connectionTask, exitCodeTask))) {
176+
// successfully connected to transport service
177+
is Connection -> return Pair(result, process)
178+
179+
// cmd exited before connection. some thing wrong
180+
is Int -> throw VMStartException(
181+
"VM initialization failed. exit code: ${process?.exitValue()}, cmd: $commandArray", process)
182+
183+
// should never occur
184+
else -> throw IllegalStateException("Unknown result: $result")
185+
}
186+
} finally {
187+
// release the executors. no longer needed.
188+
executors.shutdown()
189+
executors.awaitTermination(1, TimeUnit.SECONDS)
190+
}
191+
192+
}
193+
194+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.javacs.ktda.jdi.launch
2+
3+
import com.sun.jdi.connect.Connector
4+
5+
/**
6+
* An implementation to Connector.Argument, used for arguments to launch a LaunchingConnector
7+
*/
8+
class StringArgument constructor(private val name: String, private val description: String = "", private val label: String = name,
9+
private var value:String = "", private val mustSpecify: Boolean = false) : Connector.Argument {
10+
11+
override fun name(): String {
12+
return name
13+
}
14+
15+
override fun description(): String {
16+
return description
17+
}
18+
19+
override fun label(): String {
20+
return label
21+
}
22+
23+
override fun mustSpecify(): Boolean {
24+
return mustSpecify
25+
}
26+
27+
override fun value(): String {
28+
return value
29+
}
30+
31+
override fun setValue(value: String){
32+
this.value = value
33+
}
34+
35+
override fun isValid(value: String): Boolean{
36+
return true
37+
}
38+
override fun toString(): String {
39+
return value
40+
}
41+
42+
43+
}

adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class JDIVariable(
3131
}
3232

3333
private fun arrayElementsOf(jdiValue: ArrayReference): List<VariableTreeNode> = jdiValue.values
34-
.mapIndexed { i, it -> JDIVariable(i.toString(), it) }
34+
.mapIndexed { i, it -> JDIVariable(i.toString(), it) }
3535

3636
private fun fieldsOf(jdiValue: ObjectReference, jdiType: ReferenceType) = jdiType.allFields()
3737
.map { JDIVariable(it.name(), jdiValue.getValue(it), jdiType) }
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.javacs.ktda.jdi.launch.KDACommandLineLauncher

adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import org.hamcrest.Matchers.equalTo
1717
abstract class DebugAdapterTestFixture(
1818
relativeWorkspaceRoot: String,
1919
private val mainClass: String,
20-
private val vmArguments: String = ""
20+
private val vmArguments: String = "",
21+
private val cwd: String = "",
22+
private val envs: Collection<String> = listOf()
2123
) : IDebugProtocolClient {
2224
val absoluteWorkspaceRoot: Path = Paths.get(DebugAdapterTestFixture::class.java.getResource("/Anchor.txt").toURI()).parent.resolve(relativeWorkspaceRoot)
2325
lateinit var debugAdapter: KotlinDebugAdapter
@@ -60,7 +62,9 @@ abstract class DebugAdapterTestFixture(
6062
debugAdapter.launch(mapOf(
6163
"projectRoot" to absoluteWorkspaceRoot.toString(),
6264
"mainClass" to mainClass,
63-
"vmArguments" to vmArguments
65+
"vmArguments" to vmArguments,
66+
"cwd" to cwd,
67+
"envs" to envs
6468
)).join()
6569
println("Launched")
6670
}

0 commit comments

Comments
 (0)