32 import java.io.ObjectInputStream; |
32 import java.io.ObjectInputStream; |
33 import java.io.ObjectOutputStream; |
33 import java.io.ObjectOutputStream; |
34 import java.io.PrintStream; |
34 import java.io.PrintStream; |
35 import java.net.ServerSocket; |
35 import java.net.ServerSocket; |
36 import java.net.Socket; |
36 import java.net.Socket; |
37 import com.sun.jdi.*; |
|
38 import java.io.EOFException; |
37 import java.io.EOFException; |
39 import java.util.Arrays; |
38 import java.util.Arrays; |
40 import java.util.Collection; |
39 import java.util.Collection; |
41 import java.util.HashMap; |
|
42 import java.util.List; |
40 import java.util.List; |
43 import java.util.Map; |
41 import java.util.Map; |
|
42 import com.sun.jdi.BooleanValue; |
|
43 import com.sun.jdi.ClassNotLoadedException; |
|
44 import com.sun.jdi.IncompatibleThreadStateException; |
|
45 import com.sun.jdi.InvalidTypeException; |
|
46 import com.sun.jdi.ObjectReference; |
|
47 import com.sun.jdi.ReferenceType; |
|
48 import com.sun.jdi.StackFrame; |
|
49 import com.sun.jdi.ThreadReference; |
|
50 import com.sun.jdi.VirtualMachine; |
44 import static java.util.stream.Collectors.toList; |
51 import static java.util.stream.Collectors.toList; |
45 import jdk.jshell.JShellException; |
52 import jdk.jshell.JShellException; |
46 import jdk.jshell.spi.ExecutionControl; |
53 import jdk.jshell.spi.ExecutionControl; |
47 import jdk.jshell.spi.ExecutionEnv; |
54 import jdk.jshell.spi.ExecutionEnv; |
48 import jdk.internal.jshell.jdi.ClassTracker.ClassInfo; |
55 import jdk.internal.jshell.jdi.ClassTracker.ClassInfo; |
57 * Launches a remote process. |
64 * Launches a remote process. |
58 */ |
65 */ |
59 public class JDIExecutionControl implements ExecutionControl { |
66 public class JDIExecutionControl implements ExecutionControl { |
60 |
67 |
61 ExecutionEnv execEnv; |
68 ExecutionEnv execEnv; |
62 JDIEnv jdiEnv; |
69 private final boolean isLaunch; |
63 private ClassTracker tracker; |
70 private JDIConnection connection; |
64 private JDIEventHandler handler; |
71 private ClassTracker classTracker; |
65 private Socket socket; |
72 private Socket socket; |
66 private ObjectInputStream remoteIn; |
73 private ObjectInputStream remoteIn; |
67 private ObjectOutputStream remoteOut; |
74 private ObjectOutputStream remoteOut; |
68 private String remoteVMOptions; |
75 |
|
76 /** |
|
77 * Creates an ExecutionControl instance based on JDI. |
|
78 * |
|
79 * @param isLaunch true for LaunchingConnector; false for ListeningConnector |
|
80 */ |
|
81 public JDIExecutionControl(boolean isLaunch) { |
|
82 this.isLaunch = isLaunch; |
|
83 } |
|
84 |
|
85 /** |
|
86 * Creates an ExecutionControl instance based on a JDI LaunchingConnector. |
|
87 */ |
|
88 public JDIExecutionControl() { |
|
89 this.isLaunch = true; |
|
90 } |
69 |
91 |
70 /** |
92 /** |
71 * Initializes the launching JDI execution engine. Initialize JDI and use it |
93 * Initializes the launching JDI execution engine. Initialize JDI and use it |
72 * to launch the remote JVM. Set-up control and result communications socket |
94 * to launch the remote JVM. Set-up control and result communications socket |
73 * to the remote execution environment. This socket also transports the |
95 * to the remote execution environment. This socket also transports the |
77 * @throws IOException |
99 * @throws IOException |
78 */ |
100 */ |
79 @Override |
101 @Override |
80 public void start(ExecutionEnv execEnv) throws IOException { |
102 public void start(ExecutionEnv execEnv) throws IOException { |
81 this.execEnv = execEnv; |
103 this.execEnv = execEnv; |
82 this.jdiEnv = new JDIEnv(this); |
|
83 this.tracker = new ClassTracker(jdiEnv); |
|
84 StringBuilder sb = new StringBuilder(); |
104 StringBuilder sb = new StringBuilder(); |
85 execEnv.extraRemoteVMOptions().stream() |
|
86 .forEach(s -> { |
|
87 sb.append(" "); |
|
88 sb.append(s); |
|
89 }); |
|
90 this.remoteVMOptions = sb.toString(); |
|
91 try (ServerSocket listener = new ServerSocket(0)) { |
105 try (ServerSocket listener = new ServerSocket(0)) { |
92 // timeout after 60 seconds |
106 // timeout after 60 seconds |
93 listener.setSoTimeout(60000); |
107 listener.setSoTimeout(60000); |
94 int port = listener.getLocalPort(); |
108 int port = listener.getLocalPort(); |
95 jdiGo(port); |
109 connection = new JDIConnection(this, port, execEnv.extraRemoteVMOptions(), isLaunch); |
96 this.socket = listener.accept(); |
110 this.socket = listener.accept(); |
97 // out before in -- match remote creation so we don't hang |
111 // out before in -- match remote creation so we don't hang |
98 this.remoteOut = new ObjectOutputStream(socket.getOutputStream()); |
112 this.remoteOut = new ObjectOutputStream(socket.getOutputStream()); |
99 PipeInputStream commandIn = new PipeInputStream(); |
113 PipeInputStream commandIn = new PipeInputStream(); |
100 new DemultiplexInput(socket.getInputStream(), commandIn, execEnv.userOut(), execEnv.userErr()).start(); |
114 new DemultiplexInput(socket.getInputStream(), commandIn, execEnv.userOut(), execEnv.userErr()).start(); |
107 * Shuts down the JDI connection. Should this close the socket? |
121 * Shuts down the JDI connection. Should this close the socket? |
108 */ |
122 */ |
109 @Override |
123 @Override |
110 public void close() { |
124 public void close() { |
111 try { |
125 try { |
112 JDIConnection c = jdiEnv.connection(); |
126 if (connection != null) { |
113 if (c != null) { |
127 connection.beginShutdown(); |
114 c.beginShutdown(); |
|
115 } |
128 } |
116 if (remoteOut != null) { |
129 if (remoteOut != null) { |
117 remoteOut.writeInt(CMD_EXIT); |
130 remoteOut.writeInt(CMD_EXIT); |
118 remoteOut.flush(); |
131 remoteOut.flush(); |
119 } |
132 } |
120 if (c != null) { |
133 if (connection != null) { |
121 c.disposeVM(); |
134 connection.disposeVM(); |
122 } |
135 } |
123 } catch (IOException ex) { |
136 } catch (IOException ex) { |
124 debug(DBG_GEN, "Exception on JDI exit: %s\n", ex); |
137 debug(DBG_GEN, "Exception on JDI exit: %s\n", ex); |
125 } |
138 } |
126 } |
139 } |
183 if (readAndReportExecutionResult()) { |
196 if (readAndReportExecutionResult()) { |
184 String result = remoteIn.readUTF(); |
197 String result = remoteIn.readUTF(); |
185 return result; |
198 return result; |
186 } |
199 } |
187 } catch (IOException | RuntimeException ex) { |
200 } catch (IOException | RuntimeException ex) { |
188 if (!jdiEnv.connection().isRunning()) { |
201 if (!connection.isRunning()) { |
189 // The JDI connection is no longer live, shutdown. |
202 // The JDI connection is no longer live, shutdown. |
190 jdiEnv.shutdown(); |
203 handleVMExit(); |
191 } else { |
204 } else { |
192 debug(DBG_GEN, "Exception on remote invoke: %s\n", ex); |
205 debug(DBG_GEN, "Exception on remote invoke: %s\n", ex); |
193 return "Execution failure: " + ex.getMessage(); |
206 return "Execution failure: " + ex.getMessage(); |
194 } |
207 } |
195 } finally { |
208 } finally { |
219 if (readAndReportResult()) { |
232 if (readAndReportResult()) { |
220 String result = remoteIn.readUTF(); |
233 String result = remoteIn.readUTF(); |
221 return result; |
234 return result; |
222 } |
235 } |
223 } catch (EOFException ex) { |
236 } catch (EOFException ex) { |
224 jdiEnv.shutdown(); |
237 handleVMExit(); |
225 } catch (IOException ex) { |
238 } catch (IOException ex) { |
226 debug(DBG_GEN, "Exception on remote var value: %s\n", ex); |
239 debug(DBG_GEN, "Exception on remote var value: %s\n", ex); |
227 return "Execution failure: " + ex.getMessage(); |
240 return "Execution failure: " + ex.getMessage(); |
228 } |
241 } |
229 return ""; |
242 return ""; |
271 Map<ReferenceType, byte[]> rmp = infos.stream() |
284 Map<ReferenceType, byte[]> rmp = infos.stream() |
272 .collect(toMap( |
285 .collect(toMap( |
273 ci -> ci.getReferenceTypeOrNull(), |
286 ci -> ci.getReferenceTypeOrNull(), |
274 ci -> ci.getBytes())); |
287 ci -> ci.getBytes())); |
275 // Attempt redefine. Throws exceptions on failure. |
288 // Attempt redefine. Throws exceptions on failure. |
276 jdiEnv.vm().redefineClasses(rmp); |
289 connection.vm().redefineClasses(rmp); |
277 // Successful: mark the bytes as loaded. |
290 // Successful: mark the bytes as loaded. |
278 infos.stream() |
291 infos.stream() |
279 .forEach(ci -> ci.markLoaded()); |
292 .forEach(ci -> ci.markLoaded()); |
280 return true; |
293 return true; |
281 } catch (UnsupportedOperationException ex) { |
294 } catch (UnsupportedOperationException ex) { |
285 debug(DBG_GEN, "Exception on JDI redefine: %s\n", ex); |
298 debug(DBG_GEN, "Exception on JDI redefine: %s\n", ex); |
286 return false; |
299 return false; |
287 } |
300 } |
288 } |
301 } |
289 |
302 |
|
303 // the VM has gone down in flames or because user evaled System.exit() or the like |
|
304 void handleVMExit() { |
|
305 if (connection != null) { |
|
306 // If there is anything left dispose of it |
|
307 connection.disposeVM(); |
|
308 } |
|
309 // Tell JShell-core that the VM has died |
|
310 execEnv.closeDown(); |
|
311 } |
|
312 |
|
313 // Lazy init class tracker |
|
314 private ClassTracker classTracker() { |
|
315 if (classTracker == null) { |
|
316 classTracker = new ClassTracker(connection.vm()); |
|
317 } |
|
318 return classTracker; |
|
319 } |
|
320 |
290 /** |
321 /** |
291 * Converts a collection of class names into ClassInfo instances associated |
322 * Converts a collection of class names into ClassInfo instances associated |
292 * with the most recently compiled class bytes. |
323 * with the most recently compiled class bytes. |
293 * |
324 * |
294 * @param classes names of the classes |
325 * @param classes names of the classes |
295 * @return a list of corresponding ClassInfo instances |
326 * @return a list of corresponding ClassInfo instances |
296 */ |
327 */ |
297 private List<ClassInfo> withBytes(Collection<String> classes) { |
328 private List<ClassInfo> withBytes(Collection<String> classes) { |
298 return classes.stream() |
329 return classes.stream() |
299 .map(cn -> tracker.classInfo(cn, execEnv.getClassBytes(cn))) |
330 .map(cn -> classTracker().classInfo(cn, execEnv.getClassBytes(cn))) |
300 .collect(toList()); |
331 .collect(toList()); |
301 } |
332 } |
302 |
333 |
303 /** |
334 /** |
304 * Reports the status of the named class. UNKNOWN if not loaded. CURRENT if |
335 * Reports the status of the named class. UNKNOWN if not loaded. CURRENT if |
308 * @param classname the name of the class to test |
339 * @param classname the name of the class to test |
309 * @return the status |
340 * @return the status |
310 */ |
341 */ |
311 @Override |
342 @Override |
312 public ClassStatus getClassStatus(String classname) { |
343 public ClassStatus getClassStatus(String classname) { |
313 ClassInfo ci = tracker.get(classname); |
344 ClassInfo ci = classTracker().get(classname); |
314 if (ci.getReferenceTypeOrNull() == null) { |
345 if (ci.getReferenceTypeOrNull() == null) { |
315 // If the class does not have a JDI ReferenceType it has not been loaded |
346 // If the class does not have a JDI ReferenceType it has not been loaded |
316 return ClassStatus.UNKNOWN; |
347 return ClassStatus.UNKNOWN; |
317 } |
348 } |
318 // Compare successfully loaded with last compiled bytes. |
349 // Compare successfully loaded with last compiled bytes. |
401 elems[i] = new StackTraceElement(className, methodName, fileName, line); |
432 elems[i] = new StackTraceElement(className, methodName, fileName, line); |
402 } |
433 } |
403 return elems; |
434 return elems; |
404 } |
435 } |
405 |
436 |
406 /** |
|
407 * Launch the remote agent as a JDI connection. |
|
408 * |
|
409 * @param port the socket port for (non-JDI) commands |
|
410 */ |
|
411 private void jdiGo(int port) { |
|
412 //MessageOutput.textResources = ResourceBundle.getBundle("impl.TTYResources", |
|
413 // Locale.getDefault()); |
|
414 |
|
415 // Set-up for a fresh launch of a remote agent with any user-specified VM options. |
|
416 String connectorName = "com.sun.jdi.CommandLineLaunch"; |
|
417 Map<String, String> argumentName2Value = new HashMap<>(); |
|
418 argumentName2Value.put("main", "jdk.internal.jshell.remote.RemoteAgent " + port); |
|
419 argumentName2Value.put("options", remoteVMOptions); |
|
420 |
|
421 boolean launchImmediately = true; |
|
422 int traceFlags = 0;// VirtualMachine.TRACE_SENDS | VirtualMachine.TRACE_EVENTS; |
|
423 |
|
424 // Launch. |
|
425 jdiEnv.init(connectorName, argumentName2Value, launchImmediately, traceFlags); |
|
426 |
|
427 if (jdiEnv.connection().isOpen() && jdiEnv.vm().canBeModified()) { |
|
428 /* |
|
429 * Connection opened on startup. Start event handler |
|
430 * immediately, telling it (through arg 2) to stop on the |
|
431 * VM start event. |
|
432 */ |
|
433 handler = new JDIEventHandler(jdiEnv); |
|
434 } |
|
435 } |
|
436 |
|
437 private final Object STOP_LOCK = new Object(); |
437 private final Object STOP_LOCK = new Object(); |
438 private boolean userCodeRunning = false; |
438 private boolean userCodeRunning = false; |
439 |
439 |
440 /** |
440 /** |
441 * Interrupt a running invoke. |
441 * Interrupt a running invoke. |
445 synchronized (STOP_LOCK) { |
445 synchronized (STOP_LOCK) { |
446 if (!userCodeRunning) { |
446 if (!userCodeRunning) { |
447 return; |
447 return; |
448 } |
448 } |
449 |
449 |
450 VirtualMachine vm = handler.env.vm(); |
450 VirtualMachine vm = connection.vm(); |
451 vm.suspend(); |
451 vm.suspend(); |
452 try { |
452 try { |
453 OUTER: |
453 OUTER: |
454 for (ThreadReference thread : vm.allThreads()) { |
454 for (ThreadReference thread : vm.allThreads()) { |
455 // could also tag the thread (e.g. using name), to find it easier |
455 // could also tag the thread (e.g. using name), to find it easier |