From 15203b145435e322c02504e77fea5f89b5a01ad8 Mon Sep 17 00:00:00 2001 From: Lei Zhang Date: Tue, 30 Jun 2020 10:49:54 +0900 Subject: [PATCH 1/7] feat: support cwd anv env variables using sun jdi --- adapter/build.gradle | 3 + .../javacs/ktda/adapter/KotlinDebugAdapter.kt | 8 +- .../ktda/core/launch/LaunchConfiguration.kt | 4 +- .../org/javacs/ktda/jdi/launch/JDILauncher.kt | 22 +- .../ktda/jdi/launch/KDACommandLineLauncher.kt | 194 ++++++++++++++++++ .../javacs/ktda/jdi/launch/StringArgument.kt | 43 ++++ .../org/javacs/ktda/jdi/scope/JDIVariable.kt | 2 +- .../services/com.sun.jdi.connect.Connector | 1 + .../javacs/ktda/DebugAdapterTestFixture.kt | 8 +- .../org/javacs/ktda/SampleWorkspaceTest.kt | 17 +- .../SampleWorkspaceWithCustomConfigTest.kt | 93 +++++++++ .../jdi/launch/KDACommandLineLauncherTest.kt | 29 +++ .../src/main/kotlin/sample/workspace/App.kt | 5 +- 13 files changed, 403 insertions(+), 26 deletions(-) create mode 100644 adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt create mode 100644 adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/StringArgument.kt create mode 100644 adapter/src/main/resources/META-INF/services/com.sun.jdi.connect.Connector create mode 100644 adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt create mode 100644 adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt diff --git a/adapter/build.gradle b/adapter/build.gradle index 9a2d554..3465e3d 100644 --- a/adapter/build.gradle +++ b/adapter/build.gradle @@ -29,6 +29,9 @@ dependencies { implementation 'com.github.fwcd.kotlin-language-server:shared:229c762a4d75304d21eba6d8e1231ed949247629' // The Java Debug Interface classes (com.sun.jdi.*) implementation files("${System.properties['java.home']}/../lib/tools.jar") + // For CommandLineUtils.translateCommandLine + // https://mvnrepository.com/artifact/org.codehaus.plexus/plexus-utils + implementation 'org.codehaus.plexus:plexus-utils:3.3.0' testImplementation 'junit:junit:4.12' testImplementation 'org.hamcrest:hamcrest-all:1.3' } diff --git a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt index cd1fe33..3028ffd 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt @@ -100,13 +100,19 @@ class KotlinDebugAdapter( val vmArguments = (args["vmArguments"] as? String) ?: "" + var cwd = (args["cwd"] as? String).let { if(it.isNullOrBlank()) projectRoot else Paths.get(it) } + + var envs = args["envs"] as? List + setupCommonInitializationParams(args) val config = LaunchConfiguration( debugClassPathResolver(listOf(projectRoot)).classpathOrEmpty, mainClass, projectRoot, - vmArguments + vmArguments, + cwd, + envs ) debuggee = launcher.launch( config, diff --git a/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt b/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt index 2ffa567..bf0db37 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt @@ -6,5 +6,7 @@ class LaunchConfiguration( val classpath: Set, val mainClass: String, val projectRoot: Path, - val vmArguments: String = "" + val vmArguments: String = "", + val cwd: Path = projectRoot, + val envs: Collection? = null ) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt index 882032b..595e272 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt @@ -4,7 +4,6 @@ import org.javacs.kt.LOG import org.javacs.ktda.core.launch.DebugLauncher import org.javacs.ktda.core.launch.LaunchConfiguration import org.javacs.ktda.core.launch.AttachConfiguration -import org.javacs.ktda.core.Debuggee import org.javacs.ktda.core.DebugContext import org.javacs.ktda.util.KotlinDAException import org.javacs.ktda.jdi.JDIDebuggee @@ -16,14 +15,11 @@ import com.sun.jdi.connect.AttachingConnector import java.io.File import java.nio.file.Path import java.nio.file.Files -import java.net.URLEncoder -import java.net.URLDecoder -import java.nio.charset.StandardCharsets import java.util.stream.Collectors +import org.javacs.kt.LOG class JDILauncher( private val attachTimeout: Int = 50, - private val vmArguments: String? = null, private val modulePaths: String? = null ) : DebugLauncher { private val vmManager: VirtualMachineManager @@ -57,6 +53,8 @@ class JDILauncher( args["suspend"]!!.setValue("true") args["options"]!!.setValue(formatOptions(config)) args["main"]!!.setValue(formatMainClass(config)) + args["cwd"]!!.setValue(config.cwd.toAbsolutePath().toString()) + args["envs"]!!.setValue(KDACommandLineLauncher.urlEncode(config.envs) ?: "") } private fun createAttachArgs(config: AttachConfiguration, connector: Connector): Map = connector.defaultArguments() @@ -71,8 +69,8 @@ class JDILauncher( ?: throw KotlinDAException("Could not find an attaching connector (for a new debuggee VM)") private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors() - // Workaround for JDK 11+ where the first launcher (RawCommandLineLauncher) does not properly support args - .let { it.find { it.javaClass.name == "com.sun.tools.jdi.SunCommandLineLauncher" } ?: it.firstOrNull() } + // Using our own connector to support cwd and envs + .let { it.find { it.name().equals(KDACommandLineLauncher::class.java.name) } ?: it.firstOrNull() } ?: throw KotlinDAException("Could not find a launching connector (for a new debuggee VM)") private fun sourcesRootsOf(projectRoot: Path): Set = projectRoot.resolve("src") @@ -100,13 +98,5 @@ class JDILauncher( private fun formatClasspath(config: LaunchConfiguration): String = config.classpath .map { it.toAbsolutePath().toString() } .reduce { prev, next -> "$prev${File.pathSeparatorChar}$next" } - - private fun urlEncode(arg: Collection?) = arg - ?.map { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) } - ?.reduce { a, b -> "$a\n$b" } - - private fun urlDecode(arg: String?) = arg - ?.split("\n") - ?.map { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) } - ?.toList() + } diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt new file mode 100644 index 0000000..f4274c3 --- /dev/null +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt @@ -0,0 +1,194 @@ +package org.javacs.ktda.jdi.launch + +import com.sun.jdi.Bootstrap +import com.sun.jdi.VirtualMachine +import com.sun.jdi.connect.Connector +import com.sun.jdi.connect.IllegalConnectorArgumentsException +import com.sun.jdi.connect.Transport +import com.sun.jdi.connect.VMStartException +import com.sun.jdi.connect.spi.Connection +import com.sun.jdi.connect.spi.TransportService +import com.sun.tools.jdi.SocketTransportService +import com.sun.tools.jdi.SunCommandLineLauncher +import org.codehaus.plexus.util.cli.CommandLineUtils +import org.javacs.kt.LOG +import java.io.File +import java.io.IOException +import java.net.URLDecoder +import java.net.URLEncoder +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Paths +import java.util.concurrent.Callable +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +internal const val ARG_HOME = "home" +internal const val ARG_OPTIONS = "options" +internal const val ARG_MAIN = "main" +internal const val ARG_SUSPEND = "suspend" +internal const val ARG_QUOTE = "quote" +internal const val ARG_VM_EXEC = "vmexec" +internal const val ARG_CWD = "cwd" +internal const val ARG_ENVS = "envs" + +/** + * A custom LaunchingConnector that supports cwd and env variables + */ +open class KDACommandLineLauncher : SunCommandLineLauncher { + + protected val defaultArguments = mutableMapOf() + + /** + * We only support SocketTransportService + */ + protected val transportService = SocketTransportService() + protected val transport = Transport { "dt_socket" } + + companion object { + + fun urlEncode(arg: Collection?) = arg + ?.map { URLEncoder.encode(it, StandardCharsets.UTF_8.name()) } + ?.fold("") { a, b -> "$a\n$b" } + + fun urlDecode(arg: String?) = arg + ?.trim('\n') + ?.split("\n") + ?.map { URLDecoder.decode(it, StandardCharsets.UTF_8.name()) } + ?.toList() + } + + constructor() : super() { + + defaultArguments.putAll(super.defaultArguments()) + + defaultArguments[ARG_CWD] = StringArgument( + name = ARG_CWD, + description = "Current working directory") + + defaultArguments[ARG_ENVS] = StringArgument( + name = ARG_ENVS, + description = "Environment variables") + } + + override fun name(): String { + return this.javaClass.name + } + + override fun description(): String { + return "A custom launcher supporting cwd and env variables" + } + + override fun defaultArguments(): Map { + return this.defaultArguments + } + + override fun toString(): String { + return name() + } + + protected fun getOrDefault(arguments: Map, argName: String): String { + return arguments[argName]?.value() ?: defaultArguments[argName]?.value() ?: "" + } + + /** + * A customized method to launch the vm and connect to it, supporting cwd and env variables + */ + @Throws(IOException::class, IllegalConnectorArgumentsException::class, VMStartException::class) + override fun launch(arguments: Map): VirtualMachine { + val vm: VirtualMachine + + val home = getOrDefault(arguments, ARG_HOME) + val options = getOrDefault(arguments, ARG_OPTIONS) + val main = getOrDefault(arguments, ARG_MAIN) + val suspend = getOrDefault(arguments, ARG_SUSPEND).toBoolean() + val quote = getOrDefault(arguments, ARG_QUOTE) + var exe = getOrDefault(arguments, ARG_VM_EXEC) + val cwd = getOrDefault(arguments, ARG_CWD) + val envs = urlDecode(getOrDefault(arguments, ARG_ENVS))?.toTypedArray() + + check(quote.length == 1) {"Invalid length for $ARG_QUOTE: $quote"} + check(!options.contains("-Djava.compiler=") || + options.toLowerCase().contains("-djava.compiler=none")) { "Cannot debug with a JIT compiler. $ARG_OPTIONS: $options"} + + val listenKey = transportService.startListening() + val address = listenKey.address() + + try { + val command = StringBuilder() + + exe = if (home.isNotEmpty()) Paths.get(home, "bin", exe).toString() else exe + command.append(wrapWhitespace(exe)) + + command.append(" $options") + + //debug options + command.append(" -agentlib:jdwp=transport=${transport.name()},address=$address,server=n,suspend=${if (suspend) 'y' else 'n'}") + + command.append(" $main") + + LOG.debug("command before tokenize: $command") + + vm = launch(commandArray = CommandLineUtils.translateCommandline(command.toString()), listenKey = listenKey, + ts = transportService, cwd = cwd, envs = envs + ) + + } finally { + transportService.stopListening(listenKey) + } + return vm + } + + internal fun wrapWhitespace(str: String): String { + return if(str.contains(' ')) "\"$str\" " else str + } + + @Throws(IOException::class, VMStartException::class) + fun launch(commandArray: Array, + listenKey: TransportService.ListenKey, + ts: TransportService, cwd: String, envs: Array? = null): VirtualMachine { + + val (connection, process) = launchAndConnect(commandArray, listenKey, ts, cwd = cwd, envs = envs) + + return Bootstrap.virtualMachineManager().createVirtualMachine(connection, + process) + } + + + /** + * launch the command, connect to transportService, and returns the connection / process pair + */ + protected fun launchAndConnect(commandArray: Array, listenKey: TransportService.ListenKey, + ts: TransportService, cwd: String = "", envs: Array? = null): Pair{ + + val dir = if(cwd.isNotBlank() && Files.isDirectory(Paths.get(cwd))) File(cwd) else null + + var threadCount = 0 + + val executors = Executors.newFixedThreadPool(2) { Thread(it, "${this.javaClass.simpleName}-${threadCount++}") } + val process = Runtime.getRuntime().exec(commandArray, envs, dir) + + val connectionTask: Callable = Callable { ts.accept(listenKey, 0,0).also { LOG.debug("ts.accept invoked") } } + val exitCodeTask: Callable = Callable { process.waitFor().also { LOG.debug("process.waitFor invoked") } } + + try { + when (val result = executors.invokeAny(listOf(connectionTask, exitCodeTask))) { + // successfully connected to transport service + is Connection -> return Pair(result, process) + + // cmd exited before connection. some thing wrong + is Int -> throw VMStartException( + "VM initialization failed. exit code: ${process?.exitValue()}, cmd: $commandArray", process) + + // should never occur + else -> throw IllegalStateException("Unknown result: $result") + } + } finally { + // release the executors. no longer needed. + executors.shutdown() + executors.awaitTermination(1, TimeUnit.SECONDS) + } + + } + +} \ No newline at end of file diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/StringArgument.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/StringArgument.kt new file mode 100644 index 0000000..1868376 --- /dev/null +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/StringArgument.kt @@ -0,0 +1,43 @@ +package org.javacs.ktda.jdi.launch + +import com.sun.jdi.connect.Connector + +/** + * An implementation to Connector.Argument, used for arguments to launch a LaunchingConnector + */ +class StringArgument constructor(private val name: String, private val description: String = "", private val label: String = name, + private var value:String = "", private val mustSpecify: Boolean = false) : Connector.Argument { + + override fun name(): String { + return name + } + + override fun description(): String { + return description + } + + override fun label(): String { + return label + } + + override fun mustSpecify(): Boolean { + return mustSpecify + } + + override fun value(): String { + return value + } + + override fun setValue(value: String){ + this.value = value + } + + override fun isValid(value: String): Boolean{ + return true + } + override fun toString(): String { + return value + } + + +} \ No newline at end of file diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt index 1e512b0..8264597 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/scope/JDIVariable.kt @@ -31,7 +31,7 @@ class JDIVariable( } private fun arrayElementsOf(jdiValue: ArrayReference): List = jdiValue.values - .mapIndexed { i, it -> JDIVariable(i.toString(), it) } + .mapIndexed { i, it -> JDIVariable(i.toString(), it) } private fun fieldsOf(jdiValue: ObjectReference, jdiType: ReferenceType) = jdiType.allFields() .map { JDIVariable(it.name(), jdiValue.getValue(it), jdiType) } diff --git a/adapter/src/main/resources/META-INF/services/com.sun.jdi.connect.Connector b/adapter/src/main/resources/META-INF/services/com.sun.jdi.connect.Connector new file mode 100644 index 0000000..2a4c0ad --- /dev/null +++ b/adapter/src/main/resources/META-INF/services/com.sun.jdi.connect.Connector @@ -0,0 +1 @@ +org.javacs.ktda.jdi.launch.KDACommandLineLauncher \ No newline at end of file diff --git a/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt b/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt index de342c6..c2c6fa9 100644 --- a/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt +++ b/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt @@ -17,7 +17,9 @@ import org.hamcrest.Matchers.equalTo abstract class DebugAdapterTestFixture( relativeWorkspaceRoot: String, private val mainClass: String, - private val vmArguments: String = "" + private val vmArguments: String = "", + private val cwd: String = "", + private val envs: Collection = listOf() ) : IDebugProtocolClient { val absoluteWorkspaceRoot: Path = Paths.get(DebugAdapterTestFixture::class.java.getResource("/Anchor.txt").toURI()).parent.resolve(relativeWorkspaceRoot) lateinit var debugAdapter: KotlinDebugAdapter @@ -60,7 +62,9 @@ abstract class DebugAdapterTestFixture( debugAdapter.launch(mapOf( "projectRoot" to absoluteWorkspaceRoot.toString(), "mainClass" to mainClass, - "vmArguments" to vmArguments + "vmArguments" to vmArguments, + "cwd" to cwd, + "envs" to envs )).join() println("Launched") } diff --git a/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceTest.kt b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceTest.kt index 260ec5d..3376f88 100644 --- a/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceTest.kt +++ b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceTest.kt @@ -10,18 +10,18 @@ import org.eclipse.lsp4j.debug.StoppedEventArguments import org.eclipse.lsp4j.debug.VariablesArguments import org.junit.Assert.assertThat import org.junit.Test -import org.hamcrest.Matchers.containsInAnyOrder import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.hasItem import org.hamcrest.Matchers.nullValue import org.hamcrest.Matchers.not +import org.javacs.kt.LOG import java.util.concurrent.CountDownLatch /** * Tests a very basic debugging scenario * using a sample application. */ -class SampleWorkspaceTest : DebugAdapterTestFixture("sample-workspace", "sample.workspace.AppKt", "-Dtest=testVmArgs") { +class SampleWorkspaceTest : DebugAdapterTestFixture("sample-workspace", "sample.workspace.AppKt") { private val latch = CountDownLatch(1) private var asyncException: Throwable? = null @@ -39,7 +39,7 @@ class SampleWorkspaceTest : DebugAdapterTestFixture("sample-workspace", "sample. .toString() } breakpoints = arrayOf(SourceBreakpoint().apply { - line = 8 + line = 11 }) }).join() @@ -74,7 +74,16 @@ class SampleWorkspaceTest : DebugAdapterTestFixture("sample-workspace", "sample. variablesReference = receiver!!.variablesReference }).join().variables - assertThat(members.map { Pair(it.name, it.value) }, containsInAnyOrder(Pair("member", "\"testVmArgs\""))) + val memberMap = members.fold(mutableMapOf()) { + map, v -> + map[v.name] = v.value + map + } + + assertThat(memberMap["member"], equalTo(""""test"""")) + assertThat(memberMap["foo"], equalTo("null")) + assertThat(memberMap["cwd"]?.trim('"'), equalTo(absoluteWorkspaceRoot.toString())) + } catch (e: Throwable) { asyncException = e } finally { diff --git a/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt new file mode 100644 index 0000000..6664bd7 --- /dev/null +++ b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt @@ -0,0 +1,93 @@ +package org.javacs.ktda + +import org.eclipse.lsp4j.debug.ScopesArguments +import org.eclipse.lsp4j.debug.SetBreakpointsArguments +import org.eclipse.lsp4j.debug.Source +import org.eclipse.lsp4j.debug.SourceBreakpoint +import org.eclipse.lsp4j.debug.StackFrame +import org.eclipse.lsp4j.debug.StackTraceArguments +import org.eclipse.lsp4j.debug.StoppedEventArguments +import org.eclipse.lsp4j.debug.VariablesArguments +import org.hamcrest.Matchers.* +import org.junit.Assert.assertThat +import org.junit.Test +import org.javacs.kt.LOG +import java.util.concurrent.CountDownLatch + +/** + * Tests a very basic debugging scenario + * using a sample application. + */ +class SampleWorkspaceWithCustomConfigTest : DebugAdapterTestFixture( + "sample-workspace", "sample.workspace.AppKt", + vmArguments = "-Dfoo=bar", cwd = "/tmp", envs = listOf("MSG=hello")) { + private val latch = CountDownLatch(1) + private var asyncException: Throwable? = null + + @Test fun testBreakpointsAndVariables() { + debugAdapter.setBreakpoints(SetBreakpointsArguments().apply { + source = Source().apply { + name = "App.kt" + path = absoluteWorkspaceRoot + .resolve("src") + .resolve("main") + .resolve("kotlin") + .resolve("sample") + .resolve("workspace") + .resolve("App.kt") + .toString() + } + breakpoints = arrayOf(SourceBreakpoint().apply { + line = 11 + }) + }).join() + + launch() + latch.await() // Wait for the breakpoint event to finish + asyncException?.let { throw it } + } + + override fun stopped(args: StoppedEventArguments) { + try { + assertThat(args.reason, equalTo("breakpoint")) + + // Query information about the debuggee's current state + val stackTrace = debugAdapter.stackTrace(StackTraceArguments().apply { + threadId = args.threadId + }).join() + val locals = stackTrace.stackFrames.asSequence().flatMap { + debugAdapter.scopes(ScopesArguments().apply { + frameId = it.id + }).join().scopes.asSequence().flatMap { + debugAdapter.variables(VariablesArguments().apply { + variablesReference = it.variablesReference + }).join().variables.asSequence() + } + }.toList() + val receiver = locals.find { it.name == "this" } + + assertThat(locals.map { Pair(it.name, it.value) }, hasItem(Pair("local", "123"))) + assertThat(receiver, not(nullValue())) + + val members = debugAdapter.variables(VariablesArguments().apply { + variablesReference = receiver!!.variablesReference + }).join().variables + + val memberMap = members.fold(mutableMapOf()) { + map, v -> + map[v.name] = v.value + map + } + + assertThat(memberMap["member"], equalTo(""""test"""")) + assertThat(memberMap["foo"], equalTo(""""bar"""")) + assertThat(memberMap["cwd"]?.trim('"'), containsString("/tmp")) + assertThat(memberMap["msg"], equalTo(""""hello"""")) + + } catch (e: Throwable) { + asyncException = e + } finally { + latch.countDown() + } + } +} diff --git a/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt b/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt new file mode 100644 index 0000000..f56f977 --- /dev/null +++ b/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt @@ -0,0 +1,29 @@ +package org.javacs.ktda.jdi.launch + +import com.sun.jdi.connect.Connector +import org.hamcrest.Matchers.* +import org.javacs.ktda.DebugAdapterTestFixture +import org.junit.Assert.assertThat +import org.junit.Test +import java.nio.file.Path +import java.nio.file.Paths + +class KDACommandLineLauncherTest { + + @Test + fun `defaultArguments should include cwd, envs, suspend`() { + + val connector = KDACommandLineLauncher() + + val args = connector.defaultArguments() + + assertThat(args.size, greaterThanOrEqualTo(2)) + + assertThat(args["cwd"], notNullValue()) + assertThat(args["envs"], notNullValue()) + //suspend should default to true + assertThat(args["suspend"]?.value(), equalTo("true")) + + } + +} \ No newline at end of file diff --git a/adapter/src/test/resources/sample-workspace/src/main/kotlin/sample/workspace/App.kt b/adapter/src/test/resources/sample-workspace/src/main/kotlin/sample/workspace/App.kt index cee06ee..b5a73f5 100644 --- a/adapter/src/test/resources/sample-workspace/src/main/kotlin/sample/workspace/App.kt +++ b/adapter/src/test/resources/sample-workspace/src/main/kotlin/sample/workspace/App.kt @@ -1,7 +1,10 @@ package sample.workspace class App { - private val member: String = System.getProperty("test") + private val member: String = "test" + private val foo: String? = System.getProperty("foo") + private val cwd: String = System.getProperty("user.dir") + private val msg: String? = System.getenv("MSG") val greeting: String get() { val local: Int = 123 From 64821ae0c175a74fb7b4782a6ddd91957e795a8f Mon Sep 17 00:00:00 2001 From: Lei Zhang Date: Fri, 3 Jul 2020 13:59:10 +0900 Subject: [PATCH 2/7] add support to jdk11 --- adapter/build.gradle | 1 - .../org/javacs/ktda/jdi/launch/JDILauncher.kt | 4 +- .../ktda/jdi/launch/KDACommandLineLauncher.kt | 52 ++++++++++++------- .../jdi/launch/KDACommandLineLauncherTest.kt | 4 -- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/adapter/build.gradle b/adapter/build.gradle index 3465e3d..ed83fce 100644 --- a/adapter/build.gradle +++ b/adapter/build.gradle @@ -30,7 +30,6 @@ dependencies { // The Java Debug Interface classes (com.sun.jdi.*) implementation files("${System.properties['java.home']}/../lib/tools.jar") // For CommandLineUtils.translateCommandLine - // https://mvnrepository.com/artifact/org.codehaus.plexus/plexus-utils implementation 'org.codehaus.plexus:plexus-utils:3.3.0' testImplementation 'junit:junit:4.12' testImplementation 'org.hamcrest:hamcrest-all:1.3' diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt index 595e272..064e851 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt @@ -68,9 +68,9 @@ class JDILauncher( .let { it.find { it.name() == "com.sun.jdi.SocketAttach" } ?: it.firstOrNull() } ?: throw KotlinDAException("Could not find an attaching connector (for a new debuggee VM)") - private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors() + private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors().also { LOG.info("connectors: $it") } // Using our own connector to support cwd and envs - .let { it.find { it.name().equals(KDACommandLineLauncher::class.java.name) } ?: it.firstOrNull() } + .let { it.find { it.name() == KDACommandLineLauncher::class.java.name } ?: it.firstOrNull() } ?: throw KotlinDAException("Could not find a launching connector (for a new debuggee VM)") private fun sourcesRootsOf(projectRoot: Path): Set = projectRoot.resolve("src") diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt index f4274c3..c1db319 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt @@ -2,14 +2,9 @@ package org.javacs.ktda.jdi.launch import com.sun.jdi.Bootstrap import com.sun.jdi.VirtualMachine -import com.sun.jdi.connect.Connector -import com.sun.jdi.connect.IllegalConnectorArgumentsException -import com.sun.jdi.connect.Transport -import com.sun.jdi.connect.VMStartException +import com.sun.jdi.connect.* import com.sun.jdi.connect.spi.Connection import com.sun.jdi.connect.spi.TransportService -import com.sun.tools.jdi.SocketTransportService -import com.sun.tools.jdi.SunCommandLineLauncher import org.codehaus.plexus.util.cli.CommandLineUtils import org.javacs.kt.LOG import java.io.File @@ -19,6 +14,7 @@ import java.net.URLEncoder import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Paths +import java.util.* import java.util.concurrent.Callable import java.util.concurrent.Executors import java.util.concurrent.TimeUnit @@ -35,15 +31,15 @@ internal const val ARG_ENVS = "envs" /** * A custom LaunchingConnector that supports cwd and env variables */ -open class KDACommandLineLauncher : SunCommandLineLauncher { +open class KDACommandLineLauncher : LaunchingConnector { protected val defaultArguments = mutableMapOf() /** * We only support SocketTransportService */ - protected val transportService = SocketTransportService() - protected val transport = Transport { "dt_socket" } + protected var transportService : TransportService? = null + protected var transport = Transport { "dt_socket" } companion object { @@ -60,15 +56,29 @@ open class KDACommandLineLauncher : SunCommandLineLauncher { constructor() : super() { - defaultArguments.putAll(super.defaultArguments()) + defaultArguments[ARG_HOME] = StringArgument(ARG_HOME, description = "Java home", value = System.getProperty("java.home")) - defaultArguments[ARG_CWD] = StringArgument( - name = ARG_CWD, - description = "Current working directory") + defaultArguments[ARG_OPTIONS] = StringArgument(ARG_OPTIONS, description = "Jvm arguments") + + defaultArguments[ARG_MAIN] = StringArgument(ARG_MAIN, description = "Main class name and parameters", mustSpecify = true) + + defaultArguments[ARG_SUSPEND] = StringArgument(ARG_SUSPEND, description = "Whether launch the debugee in suspend mode", value = "true") + + defaultArguments[ARG_QUOTE] = StringArgument(ARG_QUOTE, description = "Quote char", value = "\"") + + defaultArguments[ARG_VM_EXEC] = StringArgument(ARG_VM_EXEC, description = "The java exec", value = "java") + + defaultArguments[ARG_CWD] = StringArgument(ARG_CWD, description = "Current working directory") + + defaultArguments[ARG_ENVS] = StringArgument(ARG_ENVS, description = "Environment variables") + + // Load TransportService 's implementation + transportService = Class.forName("com.sun.tools.jdi.SocketTransportService").getDeclaredConstructor().newInstance() as TransportService + + if(transportService == null){ + throw IllegalStateException("Failed to load com.sun.tools.jdi.SocketTransportService") + } - defaultArguments[ARG_ENVS] = StringArgument( - name = ARG_ENVS, - description = "Environment variables") } override fun name(): String { @@ -87,6 +97,10 @@ open class KDACommandLineLauncher : SunCommandLineLauncher { return name() } + override fun transport(): Transport { + return transport + } + protected fun getOrDefault(arguments: Map, argName: String): String { return arguments[argName]?.value() ?: defaultArguments[argName]?.value() ?: "" } @@ -111,7 +125,7 @@ open class KDACommandLineLauncher : SunCommandLineLauncher { check(!options.contains("-Djava.compiler=") || options.toLowerCase().contains("-djava.compiler=none")) { "Cannot debug with a JIT compiler. $ARG_OPTIONS: $options"} - val listenKey = transportService.startListening() + val listenKey = transportService?.startListening() ?: throw IllegalStateException("Failed to do transportService.startListening()") val address = listenKey.address() try { @@ -130,11 +144,11 @@ open class KDACommandLineLauncher : SunCommandLineLauncher { LOG.debug("command before tokenize: $command") vm = launch(commandArray = CommandLineUtils.translateCommandline(command.toString()), listenKey = listenKey, - ts = transportService, cwd = cwd, envs = envs + ts = transportService!!, cwd = cwd, envs = envs ) } finally { - transportService.stopListening(listenKey) + transportService?.stopListening(listenKey) } return vm } diff --git a/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt b/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt index f56f977..430615a 100644 --- a/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt +++ b/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt @@ -1,12 +1,8 @@ package org.javacs.ktda.jdi.launch -import com.sun.jdi.connect.Connector import org.hamcrest.Matchers.* -import org.javacs.ktda.DebugAdapterTestFixture import org.junit.Assert.assertThat import org.junit.Test -import java.nio.file.Path -import java.nio.file.Paths class KDACommandLineLauncherTest { From 37d80bd34190d877d14917c366eb2e0fa43d1fb0 Mon Sep 17 00:00:00 2001 From: Lei Zhang Date: Fri, 3 Jul 2020 17:27:44 +0900 Subject: [PATCH 3/7] change envs config from Array to Map for more friendly user experience --- .../kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt | 6 +++++- .../org/javacs/ktda/core/launch/LaunchConfiguration.kt | 2 +- .../main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt | 4 ++-- .../test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt | 2 +- .../org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt index 3028ffd..57c29a4 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt @@ -92,6 +92,8 @@ class KotlinDebugAdapter( override fun launch(args: Map) = launcherAsync.execute { performInitialization() + LOG.debug("launch args: $args") + val projectRoot = (args["projectRoot"] as? String)?.let { Paths.get(it) } ?: throw missingRequestArgument("launch", "projectRoot") @@ -102,7 +104,9 @@ class KotlinDebugAdapter( var cwd = (args["cwd"] as? String).let { if(it.isNullOrBlank()) projectRoot else Paths.get(it) } - var envs = args["envs"] as? List + // Cast from com.google.gson.internal.LinkedTreeMap + @Suppress("UNCHECKED_CAST") + var envs = args["envs"] as? Map ?: mapOf() setupCommonInitializationParams(args) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt b/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt index bf0db37..680ed80 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt @@ -8,5 +8,5 @@ class LaunchConfiguration( val projectRoot: Path, val vmArguments: String = "", val cwd: Path = projectRoot, - val envs: Collection? = null + val envs: Map = mapOf() ) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt index 064e851..88d43a1 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt @@ -54,7 +54,7 @@ class JDILauncher( args["options"]!!.setValue(formatOptions(config)) args["main"]!!.setValue(formatMainClass(config)) args["cwd"]!!.setValue(config.cwd.toAbsolutePath().toString()) - args["envs"]!!.setValue(KDACommandLineLauncher.urlEncode(config.envs) ?: "") + args["envs"]!!.setValue(KDACommandLineLauncher.urlEncode(config.envs.map { "${it.key}=${it.value}" }) ?: "") } private fun createAttachArgs(config: AttachConfiguration, connector: Connector): Map = connector.defaultArguments() @@ -68,7 +68,7 @@ class JDILauncher( .let { it.find { it.name() == "com.sun.jdi.SocketAttach" } ?: it.firstOrNull() } ?: throw KotlinDAException("Could not find an attaching connector (for a new debuggee VM)") - private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors().also { LOG.info("connectors: $it") } + private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors().also { LOG.debug("connectors: $it") } // Using our own connector to support cwd and envs .let { it.find { it.name() == KDACommandLineLauncher::class.java.name } ?: it.firstOrNull() } ?: throw KotlinDAException("Could not find a launching connector (for a new debuggee VM)") diff --git a/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt b/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt index c2c6fa9..e267250 100644 --- a/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt +++ b/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt @@ -19,7 +19,7 @@ abstract class DebugAdapterTestFixture( private val mainClass: String, private val vmArguments: String = "", private val cwd: String = "", - private val envs: Collection = listOf() + private val envs: Map = mapOf() ) : IDebugProtocolClient { val absoluteWorkspaceRoot: Path = Paths.get(DebugAdapterTestFixture::class.java.getResource("/Anchor.txt").toURI()).parent.resolve(relativeWorkspaceRoot) lateinit var debugAdapter: KotlinDebugAdapter diff --git a/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt index 6664bd7..655429d 100644 --- a/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt +++ b/adapter/src/test/kotlin/org/javacs/ktda/SampleWorkspaceWithCustomConfigTest.kt @@ -20,7 +20,7 @@ import java.util.concurrent.CountDownLatch */ class SampleWorkspaceWithCustomConfigTest : DebugAdapterTestFixture( "sample-workspace", "sample.workspace.AppKt", - vmArguments = "-Dfoo=bar", cwd = "/tmp", envs = listOf("MSG=hello")) { + vmArguments = "-Dfoo=bar", cwd = "/tmp", envs = mapOf("MSG" to "hello")) { private val latch = CountDownLatch(1) private var asyncException: Throwable? = null From 4ed5648ac21de27a0be2984170757b7fd131c175 Mon Sep 17 00:00:00 2001 From: Mikalai Kukhta Date: Wed, 7 Aug 2024 13:22:08 +0200 Subject: [PATCH 4/7] rename 'envs' to 'env' to align with other debug adapters --- .../javacs/ktda/adapter/KotlinDebugAdapter.kt | 4 ++-- .../ktda/core/launch/LaunchConfiguration.kt | 2 +- .../org/javacs/ktda/jdi/launch/JDILauncher.kt | 21 ++++++++++--------- .../ktda/jdi/launch/KDACommandLineLauncher.kt | 2 +- .../javacs/ktda/DebugAdapterTestFixture.kt | 2 +- .../jdi/launch/KDACommandLineLauncherTest.kt | 2 +- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt index b9abb38..3013204 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt @@ -104,7 +104,7 @@ class KotlinDebugAdapter( // Cast from com.google.gson.internal.LinkedTreeMap @Suppress("UNCHECKED_CAST") - var envs = args["envs"] as? Map ?: mapOf() + val env = args["env"] as? Map ?: mapOf() setupCommonInitializationParams(args) @@ -114,7 +114,7 @@ class KotlinDebugAdapter( projectRoot, vmArguments, cwd, - envs + env ) debuggee = launcher.launch( config, diff --git a/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt b/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt index 680ed80..e61e1e6 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/core/launch/LaunchConfiguration.kt @@ -8,5 +8,5 @@ class LaunchConfiguration( val projectRoot: Path, val vmArguments: String = "", val cwd: Path = projectRoot, - val envs: Map = mapOf() + val env: Map = mapOf() ) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt index e6e7878..5b9c5e8 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/JDILauncher.kt @@ -27,7 +27,7 @@ class JDILauncher( override fun launch(config: LaunchConfiguration, context: DebugContext): JDIDebuggee { val connector = createLaunchConnector() - LOG.info("Starting JVM debug session with main class {}", config.mainClass) + LOG.info("Starting JVM debug session with main class {} and connector {}", config.mainClass, connector) LOG.debug("Launching VM") val vm = connector.launch(createLaunchArgs(config, connector)) ?: throw KotlinDAException("Could not launch a new VM") @@ -50,11 +50,11 @@ class JDILauncher( private fun createLaunchArgs(config: LaunchConfiguration, connector: Connector): Map = connector.defaultArguments() .also { args -> - args["suspend"]!!.setValue("true") - args["options"]!!.setValue(formatOptions(config)) - args["main"]!!.setValue(formatMainClass(config)) - args["cwd"]!!.setValue(config.cwd.toAbsolutePath().toString()) - args["envs"]!!.setValue(KDACommandLineLauncher.urlEncode(config.envs.map { "${it.key}=${it.value}" }) ?: "") + args.get("suspend")?.setValue("true") + args.get("options")?.setValue(formatOptions(config)) + args.get("main")?.setValue(formatMainClass(config)) + args.get("cwd")?.setValue(config.cwd.toAbsolutePath().toString()) + args.get("env")?.setValue(KDACommandLineLauncher.urlEncode(config.env.map { "${it.key}=${it.value}" }) ?: "") } private fun createAttachArgs(config: AttachConfiguration, connector: Connector): Map = connector.defaultArguments() @@ -65,12 +65,13 @@ class JDILauncher( } private fun createAttachConnector(): AttachingConnector = vmManager.attachingConnectors() - .let { it.find { it.name() == "com.sun.jdi.SocketAttach" } ?: it.firstOrNull() } + .let { it.find { it.name() == "com.sun.jdi.SocketAttach" } } ?: throw KotlinDAException("Could not find an attaching connector (for a new debuggee VM)") - - private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors().also { LOG.debug("connectors: $it") } + + private fun createLaunchConnector(): LaunchingConnector = vmManager.launchingConnectors() + .also { LOG.debug("connectors: $it") } // Using our own connector to support cwd and envs - .let { it.find { it.name() == KDACommandLineLauncher::class.java.name } ?: it.firstOrNull() } + .let { it.find { it.name() == KDACommandLineLauncher::class.java.name } } ?: throw KotlinDAException("Could not find a launching connector (for a new debuggee VM)") private fun sourcesRootsOf(projectRoot: Path): Set = diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt index 210932d..0e63a7c 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt @@ -26,7 +26,7 @@ internal const val ARG_SUSPEND = "suspend" internal const val ARG_QUOTE = "quote" internal const val ARG_VM_EXEC = "vmexec" internal const val ARG_CWD = "cwd" -internal const val ARG_ENVS = "envs" +internal const val ARG_ENV = "env" /** * A custom LaunchingConnector that supports cwd and env variables diff --git a/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt b/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt index 2fcfca7..0172664 100644 --- a/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt +++ b/adapter/src/test/kotlin/org/javacs/ktda/DebugAdapterTestFixture.kt @@ -64,7 +64,7 @@ abstract class DebugAdapterTestFixture( "mainClass" to mainClass, "vmArguments" to vmArguments, "cwd" to cwd, - "envs" to envs + "env" to envs )).join() println("Launched") } diff --git a/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt b/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt index 430615a..17c608b 100644 --- a/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt +++ b/adapter/src/test/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncherTest.kt @@ -16,7 +16,7 @@ class KDACommandLineLauncherTest { assertThat(args.size, greaterThanOrEqualTo(2)) assertThat(args["cwd"], notNullValue()) - assertThat(args["envs"], notNullValue()) + assertThat(args["env"], notNullValue()) //suspend should default to true assertThat(args["suspend"]?.value(), equalTo("true")) From 468b192f007fe5f963e1ece579babcc6bf9d0282 Mon Sep 17 00:00:00 2001 From: Mikalai Kukhta Date: Wed, 7 Aug 2024 13:26:05 +0200 Subject: [PATCH 5/7] prettify KDACommandLineLauncher code a little. remove external dependency on org.codehaus.plexus.util.cli.CommandLineUtils --- .../ktda/jdi/launch/KDACommandLineLauncher.kt | 358 +++++++++--------- 1 file changed, 180 insertions(+), 178 deletions(-) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt index 0e63a7c..b209489 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt @@ -5,10 +5,9 @@ import com.sun.jdi.VirtualMachine import com.sun.jdi.connect.* import com.sun.jdi.connect.spi.Connection import com.sun.jdi.connect.spi.TransportService -import org.codehaus.plexus.util.cli.CommandLineUtils -import org.javacs.kt.LOG import java.io.File import java.io.IOException +import java.io.InputStream import java.net.URLDecoder import java.net.URLEncoder import java.nio.charset.StandardCharsets @@ -16,8 +15,10 @@ import java.nio.file.Files import java.nio.file.Paths import java.util.* import java.util.concurrent.Callable +import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import org.javacs.kt.LOG internal const val ARG_HOME = "home" internal const val ARG_OPTIONS = "options" @@ -28,18 +29,14 @@ internal const val ARG_VM_EXEC = "vmexec" internal const val ARG_CWD = "cwd" internal const val ARG_ENV = "env" -/** - * A custom LaunchingConnector that supports cwd and env variables - */ +/** A custom LaunchingConnector that supports cwd and env variables */ open class KDACommandLineLauncher : LaunchingConnector { protected val defaultArguments = mutableMapOf() - /** - * We only support SocketTransportService - */ - protected var transportService : TransportService? = null - protected var transport = Transport { "dt_socket" } + /** We only support SocketTransportService */ + protected val transportService: TransportService + protected val transport = Transport { "dt_socket" } companion object { @@ -55,226 +52,231 @@ open class KDACommandLineLauncher : LaunchingConnector { } constructor() : super() { - - defaultArguments[ARG_HOME] = StringArgument(ARG_HOME, description = "Java home", value = System.getProperty("java.home")) - - defaultArguments[ARG_OPTIONS] = StringArgument(ARG_OPTIONS, description = "Jvm arguments") - - defaultArguments[ARG_MAIN] = StringArgument(ARG_MAIN, description = "Main class name and parameters", mustSpecify = true) - - defaultArguments[ARG_SUSPEND] = StringArgument(ARG_SUSPEND, description = "Whether launch the debugee in suspend mode", value = "true") - - defaultArguments[ARG_QUOTE] = StringArgument(ARG_QUOTE, description = "Quote char", value = "\"") - - defaultArguments[ARG_VM_EXEC] = StringArgument(ARG_VM_EXEC, description = "The java exec", value = "java") - - defaultArguments[ARG_CWD] = StringArgument(ARG_CWD, description = "Current working directory") - - defaultArguments[ARG_ENVS] = StringArgument(ARG_ENVS, description = "Environment variables") + defaultArguments.apply { + set(ARG_HOME, StringArgument(ARG_HOME, "Java home", value = System.getProperty("java.home"))) + set(ARG_OPTIONS, StringArgument(ARG_OPTIONS, "Jvm arguments")) + set(ARG_MAIN, StringArgument(ARG_MAIN, "Main class name and parameters", mustSpecify = true)) + set(ARG_SUSPEND, StringArgument(ARG_SUSPEND, "Whether launch the debugee in suspend mode", "true")) + set(ARG_QUOTE, StringArgument(ARG_QUOTE, "Quote char", value = "\"")) + set(ARG_VM_EXEC, StringArgument(ARG_VM_EXEC, "The java exec", value = "java")) + set(ARG_CWD, StringArgument(ARG_CWD, "Current working directory")) + set(ARG_ENV, StringArgument(ARG_ENV, "Environment variables")) + } // Load TransportService 's implementation - transportService = Class.forName("com.sun.tools.jdi.SocketTransportService").getDeclaredConstructor().newInstance() as TransportService - - if(transportService == null){ + try { + transportService = + Class.forName("com.sun.tools.jdi.SocketTransportService") + .getDeclaredConstructor() + .newInstance() as + TransportService + } catch (e: Exception) { throw IllegalStateException("Failed to load com.sun.tools.jdi.SocketTransportService") } - } - override fun name(): String { - return this.javaClass.name - } + override fun name(): String = javaClass.name - override fun description(): String { - return "A custom launcher supporting cwd and env variables" - } + override fun description(): String = "A custom launcher supporting cwd and env variables" - override fun defaultArguments(): Map { - return this.defaultArguments - } + override fun defaultArguments(): Map = defaultArguments - override fun toString(): String { - return name() - } + override fun toString(): String = name() - override fun transport(): Transport { - return transport - } + override fun transport(): Transport = transport - protected fun getOrDefault(arguments: Map, argName: String): String { + protected fun getOrDefault( + arguments: Map, + argName: String + ): String { return arguments[argName]?.value() ?: defaultArguments[argName]?.value() ?: "" } - protected fun translateCommandline(toProcess: String): Array { - if (toProcess.length == 0 ) return emptyArray() - - // parse with a simple finite state machine - val normal = 0; - val inQuote = 1; - val inDoubleQuote = 2; - var state = normal; - - val tok = StringTokenizer( toProcess, "\"\' ", true ); - var v = mutableListOf(); - val current = StringBuilder(); - - while ( tok.hasMoreTokens() ) { - val nextTok = tok.nextToken(); - when ( state ) { - inQuote -> { - if ( "\'".equals( nextTok ) ) - { - state = normal; - } - else - { - current.append( nextTok ); - } + private fun tokenizeCommandLine(args: String): Array { + val result = mutableListOf() + + val DEFAULT = 0 + val ARG = 1 + val IN_DOUBLE_QUOTE = 2 + val IN_SINGLE_QUOTE = 3 + + var state = DEFAULT + val buf = StringBuilder() + val len = args.length + var i = 0 + + while (i < len) { + var ch = args[i] + if (ch.isWhitespace()) { + if (state == DEFAULT) { + // skip + i++ + continue + } else if (state == ARG) { + state = DEFAULT + result.add(buf.toString()) + buf.setLength(0) + i++ + continue } - inDoubleQuote -> { - if ( "\"".equals( nextTok ) ) - { - state = normal; - } - else - { - current.append( nextTok ); + } + when (state) { + DEFAULT, ARG -> { + when { + ch == '"' -> state = IN_DOUBLE_QUOTE + ch == '\'' -> state = IN_SINGLE_QUOTE + ch == '\\' && i + 1 < len -> { + state = ARG + ch = args[++i] + buf.append(ch) + } + else -> { + state = ARG + buf.append(ch) + } } } - else -> { - if ( "\'".equals( nextTok ) ) - { - state = inQuote; - } - else if ( "\"".equals( nextTok ) ) - { - state = inDoubleQuote; - } - else if ( " ".equals( nextTok ) ) - { - if ( current.length != 0 ) - { - v.add( current.toString() ); - current.setLength( 0 ); + IN_DOUBLE_QUOTE -> { + when { + ch == '"' -> state = ARG + ch == '\\' && i + 1 < len && (args[i + 1] == '\\' || args[i + 1] == '"') -> { + ch = args[++i] + buf.append(ch) } + else -> buf.append(ch) } - else - { - current.append( nextTok ); + } + IN_SINGLE_QUOTE -> { + if (ch == '\'') { + state = ARG + } else { + buf.append(ch) } - break; } + else -> throw IllegalStateException() } + i++ } - - if (current.length != 0) v.add( current.toString() ) - - if ( ( state == inQuote ) || ( state == inDoubleQuote ) ) - { - throw Exception( "unbalanced quotes in " + toProcess ); + if (buf.isNotEmpty() || state != DEFAULT) { + result.add(buf.toString()) } - - return v.toTypedArray() + + return result.toTypedArray() } - /** - * A customized method to launch the vm and connect to it, supporting cwd and env variables - */ - @Throws(IOException::class, IllegalConnectorArgumentsException::class, VMStartException::class) - override fun launch(arguments: Map): VirtualMachine { - val vm: VirtualMachine - + private fun buildCommandLine(arguments: Map, address: String): String { val home = getOrDefault(arguments, ARG_HOME) + val javaExe = getOrDefault(arguments, ARG_VM_EXEC) val options = getOrDefault(arguments, ARG_OPTIONS) - val main = getOrDefault(arguments, ARG_MAIN) val suspend = getOrDefault(arguments, ARG_SUSPEND).toBoolean() + val main = getOrDefault(arguments, ARG_MAIN) + + val exe = if (home.isNotEmpty()) Paths.get(home, "bin", javaExe).toString() else javaExe + + return StringBuilder().apply { + append("$exe") + val jdwpLine = arrayOf( + "transport=${transport.name()}", + "address=$address", + "server=n", + "suspend=${if (suspend) 'y' else 'n'}" + ) + append(" -agentlib:jdwp=${jdwpLine.joinToString(",")}") + append(" $options") + append(" $main") + }.toString() + } + + /** A customized method to launch the vm and connect to it, supporting cwd and env variables */ + @Throws(IOException::class, IllegalConnectorArgumentsException::class, VMStartException::class) + override fun launch(arguments: Map): VirtualMachine { val quote = getOrDefault(arguments, ARG_QUOTE) - var exe = getOrDefault(arguments, ARG_VM_EXEC) + val options = getOrDefault(arguments, ARG_OPTIONS) val cwd = getOrDefault(arguments, ARG_CWD) - val envs = urlDecode(getOrDefault(arguments, ARG_ENVS))?.toTypedArray() - - check(quote.length == 1) {"Invalid length for $ARG_QUOTE: $quote"} - check(!options.contains("-Djava.compiler=") || - options.toLowerCase().contains("-djava.compiler=none")) { "Cannot debug with a JIT compiler. $ARG_OPTIONS: $options"} - - val listenKey = transportService?.startListening() ?: throw IllegalStateException("Failed to do transportService.startListening()") - val address = listenKey.address() - - try { - val command = StringBuilder() + val env = urlDecode(getOrDefault(arguments, ARG_ENV))?.toTypedArray() - exe = if (home.isNotEmpty()) Paths.get(home, "bin", exe).toString() else exe - command.append(wrapWhitespace(exe)) + check(quote.length == 1) { "Invalid length for $ARG_QUOTE: $quote" } + check(!options.contains("-Djava.compiler=") || options.lowercase().contains("-djava.compiler=none")) { + "Cannot debug with a JIT compiler. $ARG_OPTIONS: $options" + } - command.append(" $options") + val listenKey = transportService.startListening() + ?: throw IllegalStateException("Failed to do transportService.startListening()") - //debug options - command.append(" -agentlib:jdwp=transport=${transport.name()},address=$address,server=n,suspend=${if (suspend) 'y' else 'n'}") + val command = buildCommandLine(arguments, listenKey.address()) + val tokenizedCommand = tokenizeCommandLine(command) - command.append(" $main") + val (connection, process) = launchAndConnect(tokenizedCommand, listenKey, transportService, cwd, env) + return Bootstrap.virtualMachineManager().createVirtualMachine(connection, process) + } - LOG.debug("command before tokenize: $command") + private fun streamToString(stream: InputStream): String { + val lineSep = System.getProperty("line.separator") + return StringBuilder().apply { + var line: String? + val reader = stream.bufferedReader() - vm = launch(commandArray = this.translateCommandline(command.toString()), listenKey = listenKey, - ts = transportService!!, cwd = cwd, envs = envs - ) - - } finally { - transportService?.stopListening(listenKey) - } - return vm + while (reader.readLine().also { line = it } != null) { + append(line) + append(lineSep) + } + }.toString() } - internal fun wrapWhitespace(str: String): String { - return if(str.contains(' ')) "\"$str\" " else str - } + /** + * launch the command, connect to transportService, and returns the connection / process pair + */ + protected fun launchAndConnect( + commandArray: Array, + listenKey: TransportService.ListenKey, + ts: TransportService, + cwd: String = "", + envs: Array? = null + ): Pair { - @Throws(IOException::class, VMStartException::class) - fun launch(commandArray: Array, - listenKey: TransportService.ListenKey, - ts: TransportService, cwd: String, envs: Array? = null): VirtualMachine { + val dir = if (cwd.isNotBlank() && Files.isDirectory(Paths.get(cwd))) File(cwd) else null - val (connection, process) = launchAndConnect(commandArray, listenKey, ts, cwd = cwd, envs = envs) + val process = Runtime.getRuntime().exec(commandArray, envs, dir) - return Bootstrap.virtualMachineManager().createVirtualMachine(connection, - process) - } + val result = CompletableFuture>() + process.onExit().thenAcceptAsync() { pr -> + if (result.isDone) return@thenAcceptAsync - /** - * launch the command, connect to transportService, and returns the connection / process pair - */ - protected fun launchAndConnect(commandArray: Array, listenKey: TransportService.ListenKey, - ts: TransportService, cwd: String = "", envs: Array? = null): Pair{ + val exitCode = pr.exitValue() + val stdOut = streamToString(process.inputStream) + val stdErr = streamToString(process.errorStream) - val dir = if(cwd.isNotBlank() && Files.isDirectory(Paths.get(cwd))) File(cwd) else null + LOG.error("Process exited with status: ${exitCode} \n $stdOut \n $stdErr") + result.completeExceptionally(VMStartException("Process exited with status: ${exitCode}", process)) - var threadCount = 0 + try { + ts.stopListening(listenKey) + } catch (e: Exception) {} + } - val executors = Executors.newFixedThreadPool(2) { Thread(it, "${this.javaClass.simpleName}-${threadCount++}") } - val process = Runtime.getRuntime().exec(commandArray, envs, dir) + CompletableFuture.runAsync() { + try { + val vm = ts.accept(listenKey, 5_000, 5_000) + result.complete(Pair(vm, process)) + } catch (e: IllegalConnectorArgumentsException) { + result.completeExceptionally(e) + } catch (e: IOException) { + if (result.isDone) return@runAsync - val connectionTask: Callable = Callable { ts.accept(listenKey, 0,0).also { LOG.debug("ts.accept invoked") } } - val exitCodeTask: Callable = Callable { process.waitFor().also { LOG.debug("process.waitFor invoked") } } + val stdOut = streamToString(process.inputStream) + val stdErr = streamToString(process.errorStream) - try { - when (val result = executors.invokeAny(listOf(connectionTask, exitCodeTask))) { - // successfully connected to transport service - is Connection -> return Pair(result, process) + LOG.error("Failed to connect to the launched process: \n $stdOut \n $stdErr") - // cmd exited before connection. some thing wrong - is Int -> throw VMStartException( - "VM initialization failed. exit code: ${process?.exitValue()}, cmd: $commandArray", process) + process.destroy() - // should never occur - else -> throw IllegalStateException("Unknown result: $result") + result.completeExceptionally(e) + } catch (e: RuntimeException) { + result.completeExceptionally(e) } - } finally { - // release the executors. no longer needed. - executors.shutdown() - executors.awaitTermination(1, TimeUnit.SECONDS) } + return result.get() } - -} \ No newline at end of file +} From cd54dc69a1195b7adbf8a7db7bca374492e3d85f Mon Sep 17 00:00:00 2001 From: Mikalai Kukhta Date: Wed, 7 Aug 2024 14:41:39 +0200 Subject: [PATCH 6/7] added support for envFile --- .../javacs/ktda/adapter/KotlinDebugAdapter.kt | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt index 3013204..892a92e 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/adapter/KotlinDebugAdapter.kt @@ -102,9 +102,24 @@ class KotlinDebugAdapter( var cwd = (args["cwd"] as? String).let { if(it.isNullOrBlank()) projectRoot else Paths.get(it) } - // Cast from com.google.gson.internal.LinkedTreeMap - @Suppress("UNCHECKED_CAST") - val env = args["env"] as? Map ?: mapOf() + val env = mutableMapOf().apply { + (args["envFile"] as? String)?.let { Paths.get(it) }?.let { envFile -> + envFile.toFile().readLines() + .map { it.trim() } + .filter { it.isNotBlank() && !it.startsWith("#") } + .forEach { + val (key, value) = it.split("=", limit = 2) + set(key, value) + } + } + + // apply 'env' from launch request overriding contents of 'envFile' + args.get("env")?.let { env -> + // Cast from com.google.gson.internal.LinkedTreeMap + @Suppress("UNCHECKED_CAST") + putAll(env as Map) + } + }.toMap() setupCommonInitializationParams(args) From ee1f547ad3900526512e0ed1a9c85c9582391f96 Mon Sep 17 00:00:00 2001 From: Mikalai Kukhta Date: Thu, 8 Aug 2024 09:57:20 +0200 Subject: [PATCH 7/7] set missing default value to suspend --- .../kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt index b209489..7b22ef2 100644 --- a/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt +++ b/adapter/src/main/kotlin/org/javacs/ktda/jdi/launch/KDACommandLineLauncher.kt @@ -56,7 +56,7 @@ open class KDACommandLineLauncher : LaunchingConnector { set(ARG_HOME, StringArgument(ARG_HOME, "Java home", value = System.getProperty("java.home"))) set(ARG_OPTIONS, StringArgument(ARG_OPTIONS, "Jvm arguments")) set(ARG_MAIN, StringArgument(ARG_MAIN, "Main class name and parameters", mustSpecify = true)) - set(ARG_SUSPEND, StringArgument(ARG_SUSPEND, "Whether launch the debugee in suspend mode", "true")) + set(ARG_SUSPEND, StringArgument(ARG_SUSPEND, "Whether launch the debugee in suspend mode", value = "true")) set(ARG_QUOTE, StringArgument(ARG_QUOTE, "Quote char", value = "\"")) set(ARG_VM_EXEC, StringArgument(ARG_VM_EXEC, "The java exec", value = "java")) set(ARG_CWD, StringArgument(ARG_CWD, "Current working directory"))