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