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+ }
0 commit comments