1 /* 2 * Copyright (c) 2014, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package jdk.internal.jshell.jdi; 27 28 import static jdk.internal.jshell.remote.RemoteCodes.*; 29 import java.io.DataInputStream; 30 import java.io.InputStream; 31 import java.io.IOException; 32 import java.io.ObjectInputStream; 33 import java.io.ObjectOutputStream; 34 import java.io.PrintStream; 35 import java.net.ServerSocket; 36 import java.net.Socket; 37 import java.io.EOFException; 38 import java.util.Arrays; 39 import java.util.Collection; 40 import java.util.List; 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; 51 import static java.util.stream.Collectors.toList; 52 import jdk.jshell.JShellException; 53 import jdk.jshell.spi.ExecutionControl; 54 import jdk.jshell.spi.ExecutionEnv; 55 import jdk.internal.jshell.jdi.ClassTracker.ClassInfo; 56 import static java.util.stream.Collectors.toMap; 57 import jdk.internal.jshell.debug.InternalDebugControl; 58 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN; 59 60 /** 61 * Controls the remote execution environment. 62 * Interfaces to the JShell-core by implementing ExecutionControl SPI. 63 * Interfaces to RemoteAgent over a socket and via JDI. 64 * Launches a remote process. 65 */ 66 public class JDIExecutionControl implements ExecutionControl { 67 68 ExecutionEnv execEnv; 69 private final boolean isLaunch; 70 private JDIConnection connection; 71 private ClassTracker classTracker; 72 private Socket socket; 73 private ObjectInputStream remoteIn; 74 private ObjectOutputStream remoteOut; 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 } 91 92 /** 93 * Initializes the launching JDI execution engine. Initialize JDI and use it 94 * to launch the remote JVM. Set-up control and result communications socket 95 * to the remote execution environment. This socket also transports the 96 * input/output channels. 97 * 98 * @param execEnv the execution environment provided by the JShell-core 99 * @throws IOException 100 */ 101 @Override 102 public void start(ExecutionEnv execEnv) throws IOException { 103 this.execEnv = execEnv; 104 StringBuilder sb = new StringBuilder(); 105 try (ServerSocket listener = new ServerSocket(0)) { 106 // timeout after 60 seconds 107 listener.setSoTimeout(60000); 108 int port = listener.getLocalPort(); 109 connection = new JDIConnection(this, port, execEnv.extraRemoteVMOptions(), isLaunch); 110 this.socket = listener.accept(); 111 // out before in -- match remote creation so we don't hang 112 this.remoteOut = new ObjectOutputStream(socket.getOutputStream()); 113 PipeInputStream commandIn = new PipeInputStream(); 114 new DemultiplexInput(socket.getInputStream(), commandIn, execEnv.userOut(), execEnv.userErr()).start(); 115 this.remoteIn = new ObjectInputStream(commandIn); 116 } 117 } 118 119 /** 120 * Closes the execution engine. Send an exit command to the remote agent. 121 * Shuts down the JDI connection. Should this close the socket? 122 */ 123 @Override 124 public void close() { 125 try { 126 if (connection != null) { 127 connection.beginShutdown(); 128 } 129 if (remoteOut != null) { 130 remoteOut.writeInt(CMD_EXIT); 131 remoteOut.flush(); 132 } 133 if (connection != null) { 134 connection.disposeVM(); 135 } 136 } catch (IOException ex) { 137 debug(DBG_GEN, "Exception on JDI exit: %s\n", ex); 138 } 139 } 140 141 /** 142 * Loads the list of classes specified. Sends a load command to the remote 143 * agent with pairs of classname/bytes. 144 * 145 * @param classes the names of the wrapper classes to loaded 146 * @return true if all classes loaded successfully 147 */ 148 @Override 149 public boolean load(Collection<String> classes) { 150 try { 151 // Create corresponding ClassInfo instances to track the classes. 152 // Each ClassInfo has the current class bytes associated with it. 153 List<ClassInfo> infos = withBytes(classes); 154 // Send a load command to the remote agent. 155 remoteOut.writeInt(CMD_LOAD); 156 remoteOut.writeInt(classes.size()); 157 for (ClassInfo ci : infos) { 158 remoteOut.writeUTF(ci.getClassName()); 159 remoteOut.writeObject(ci.getBytes()); 160 } 161 remoteOut.flush(); 162 // Retrieve and report results from the remote agent. 163 boolean result = readAndReportResult(); 164 // For each class that now has a JDI ReferenceType, mark the bytes 165 // as loaded. 166 infos.stream() 167 .filter(ci -> ci.getReferenceTypeOrNull() != null) 168 .forEach(ci -> ci.markLoaded()); 169 return result; 170 } catch (IOException ex) { 171 debug(DBG_GEN, "IOException on remote load operation: %s\n", ex); 172 return false; 173 } 174 } 175 176 /** 177 * Invoke the doit method on the specified class. 178 * 179 * @param classname name of the wrapper class whose doit should be invoked 180 * @return return the result value of the doit 181 * @throws JShellException if a user exception was thrown (EvalException) or 182 * an unresolved reference was encountered (UnresolvedReferenceException) 183 */ 184 @Override 185 public String invoke(String classname, String methodname) throws JShellException { 186 try { 187 synchronized (STOP_LOCK) { 188 userCodeRunning = true; 189 } 190 // Send the invoke command to the remote agent. 191 remoteOut.writeInt(CMD_INVOKE); 192 remoteOut.writeUTF(classname); 193 remoteOut.writeUTF(methodname); 194 remoteOut.flush(); 195 // Retrieve and report results from the remote agent. 196 if (readAndReportExecutionResult()) { 197 String result = remoteIn.readUTF(); 198 return result; 199 } 200 } catch (IOException | RuntimeException ex) { 201 if (!connection.isRunning()) { 202 // The JDI connection is no longer live, shutdown. 203 handleVMExit(); 204 } else { 205 debug(DBG_GEN, "Exception on remote invoke: %s\n", ex); 206 return "Execution failure: " + ex.getMessage(); 207 } 208 } finally { 209 synchronized (STOP_LOCK) { 210 userCodeRunning = false; 211 } 212 } 213 return ""; 214 } 215 216 /** 217 * Retrieves the value of a JShell variable. 218 * 219 * @param classname name of the wrapper class holding the variable 220 * @param varname name of the variable 221 * @return the value as a String 222 */ 223 @Override 224 public String varValue(String classname, String varname) { 225 try { 226 // Send the variable-value command to the remote agent. 227 remoteOut.writeInt(CMD_VARVALUE); 228 remoteOut.writeUTF(classname); 229 remoteOut.writeUTF(varname); 230 remoteOut.flush(); 231 // Retrieve and report results from the remote agent. 232 if (readAndReportResult()) { 233 String result = remoteIn.readUTF(); 234 return result; 235 } 236 } catch (EOFException ex) { 237 handleVMExit(); 238 } catch (IOException ex) { 239 debug(DBG_GEN, "Exception on remote var value: %s\n", ex); 240 return "Execution failure: " + ex.getMessage(); 241 } 242 return ""; 243 } 244 245 /** 246 * Adds a path to the remote classpath. 247 * 248 * @param cp the additional path element 249 * @return true if succesful 250 */ 251 @Override 252 public boolean addToClasspath(String cp) { 253 try { 254 // Send the classpath addition command to the remote agent. 255 remoteOut.writeInt(CMD_CLASSPATH); 256 remoteOut.writeUTF(cp); 257 remoteOut.flush(); 258 // Retrieve and report results from the remote agent. 259 return readAndReportResult(); 260 } catch (IOException ex) { 261 throw new InternalError("Classpath addition failed: " + cp, ex); 262 } 263 } 264 265 /** 266 * Redefine the specified classes. Where 'redefine' is, as in JDI and JVMTI, 267 * an in-place replacement of the classes (preserving class identity) -- 268 * that is, existing references to the class do not need to be recompiled. 269 * This implementation uses JDI redefineClasses. It will be unsuccessful if 270 * the signature of the class has changed (see the JDI spec). The 271 * JShell-core is designed to adapt to unsuccessful redefine. 272 * 273 * @param classes the names of the classes to redefine 274 * @return true if all the classes were redefined 275 */ 276 @Override 277 public boolean redefine(Collection<String> classes) { 278 try { 279 // Create corresponding ClassInfo instances to track the classes. 280 // Each ClassInfo has the current class bytes associated with it. 281 List<ClassInfo> infos = withBytes(classes); 282 // Convert to the JDI ReferenceType to class bytes map form needed 283 // by JDI. 284 Map<ReferenceType, byte[]> rmp = infos.stream() 285 .collect(toMap( 286 ci -> ci.getReferenceTypeOrNull(), 287 ci -> ci.getBytes())); 288 // Attempt redefine. Throws exceptions on failure. 289 connection.vm().redefineClasses(rmp); 290 // Successful: mark the bytes as loaded. 291 infos.stream() 292 .forEach(ci -> ci.markLoaded()); 293 return true; 294 } catch (UnsupportedOperationException ex) { 295 // A form of class transformation not supported by JDI 296 return false; 297 } catch (Exception ex) { 298 debug(DBG_GEN, "Exception on JDI redefine: %s\n", ex); 299 return false; 300 } 301 } 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 321 /** 322 * Converts a collection of class names into ClassInfo instances associated 323 * with the most recently compiled class bytes. 324 * 325 * @param classes names of the classes 326 * @return a list of corresponding ClassInfo instances 327 */ 328 private List<ClassInfo> withBytes(Collection<String> classes) { 329 return classes.stream() 330 .map(cn -> classTracker().classInfo(cn, execEnv.getClassBytes(cn))) 331 .collect(toList()); 332 } 333 334 /** 335 * Reports the status of the named class. UNKNOWN if not loaded. CURRENT if 336 * the most recent successfully loaded/redefined bytes match the current 337 * compiled bytes. 338 * 339 * @param classname the name of the class to test 340 * @return the status 341 */ 342 @Override 343 public ClassStatus getClassStatus(String classname) { 344 ClassInfo ci = classTracker().get(classname); 345 if (ci.getReferenceTypeOrNull() == null) { 346 // If the class does not have a JDI ReferenceType it has not been loaded 347 return ClassStatus.UNKNOWN; 348 } 349 // Compare successfully loaded with last compiled bytes. 350 return (Arrays.equals(execEnv.getClassBytes(classname), ci.getLoadedBytes())) 351 ? ClassStatus.CURRENT 352 : ClassStatus.NOT_CURRENT; 353 } 354 355 /** 356 * Reports results from a remote agent command that does not expect 357 * exceptions. 358 * 359 * @return true if successful 360 * @throws IOException if the connection has dropped 361 */ 362 private boolean readAndReportResult() throws IOException { 363 int ok = remoteIn.readInt(); 364 switch (ok) { 365 case RESULT_SUCCESS: 366 return true; 367 case RESULT_FAIL: { 368 String ex = remoteIn.readUTF(); 369 debug(DBG_GEN, "Exception on remote operation: %s\n", ex); 370 return false; 371 } 372 default: { 373 debug(DBG_GEN, "Bad remote result code: %s\n", ok); 374 return false; 375 } 376 } 377 } 378 379 /** 380 * Reports results from a remote agent command that expects runtime 381 * exceptions. 382 * 383 * @return true if successful 384 * @throws IOException if the connection has dropped 385 * @throws EvalException if a user exception was encountered on invoke 386 * @throws UnresolvedReferenceException if an unresolved reference was 387 * encountered 388 */ 389 private boolean readAndReportExecutionResult() throws IOException, JShellException { 390 int ok = remoteIn.readInt(); 391 switch (ok) { 392 case RESULT_SUCCESS: 393 return true; 394 case RESULT_FAIL: { 395 // An internal error has occurred. 396 String ex = remoteIn.readUTF(); 397 return false; 398 } 399 case RESULT_EXCEPTION: { 400 // A user exception was encountered. 401 String exceptionClassName = remoteIn.readUTF(); 402 String message = remoteIn.readUTF(); 403 StackTraceElement[] elems = readStackTrace(); 404 throw execEnv.createEvalException(message, exceptionClassName, elems); 405 } 406 case RESULT_CORRALLED: { 407 // An unresolved reference was encountered. 408 int id = remoteIn.readInt(); 409 StackTraceElement[] elems = readStackTrace(); 410 throw execEnv.createUnresolvedReferenceException(id, elems); 411 } 412 case RESULT_KILLED: { 413 // Execution was aborted by the stop() 414 debug(DBG_GEN, "Killed."); 415 return false; 416 } 417 default: { 418 debug(DBG_GEN, "Bad remote result code: %s\n", ok); 419 return false; 420 } 421 } 422 } 423 424 private StackTraceElement[] readStackTrace() throws IOException { 425 int elemCount = remoteIn.readInt(); 426 StackTraceElement[] elems = new StackTraceElement[elemCount]; 427 for (int i = 0; i < elemCount; ++i) { 428 String className = remoteIn.readUTF(); 429 String methodName = remoteIn.readUTF(); 430 String fileName = remoteIn.readUTF(); 431 int line = remoteIn.readInt(); 432 elems[i] = new StackTraceElement(className, methodName, fileName, line); 433 } 434 return elems; 435 } 436 437 private final Object STOP_LOCK = new Object(); 438 private boolean userCodeRunning = false; 439 440 /** 441 * Interrupt a running invoke. 442 */ 443 @Override 444 public void stop() { 445 synchronized (STOP_LOCK) { 446 if (!userCodeRunning) { 447 return; 448 } 449 450 VirtualMachine vm = connection.vm(); 451 vm.suspend(); 452 try { 453 OUTER: 454 for (ThreadReference thread : vm.allThreads()) { 455 // could also tag the thread (e.g. using name), to find it easier 456 for (StackFrame frame : thread.frames()) { 457 String remoteAgentName = "jdk.internal.jshell.remote.RemoteAgent"; 458 if (remoteAgentName.equals(frame.location().declaringType().name()) 459 && "commandLoop".equals(frame.location().method().name())) { 460 ObjectReference thiz = frame.thisObject(); 461 if (((BooleanValue) thiz.getValue(thiz.referenceType().fieldByName("inClientCode"))).value()) { 462 thiz.setValue(thiz.referenceType().fieldByName("expectingStop"), vm.mirrorOf(true)); 463 ObjectReference stopInstance = (ObjectReference) thiz.getValue(thiz.referenceType().fieldByName("stopException")); 464 465 vm.resume(); 466 debug(DBG_GEN, "Attempting to stop the client code...\n"); 467 thread.stop(stopInstance); 468 thiz.setValue(thiz.referenceType().fieldByName("expectingStop"), vm.mirrorOf(false)); 469 } 470 471 break OUTER; 472 } 473 } 474 } 475 } catch (ClassNotLoadedException | IncompatibleThreadStateException | InvalidTypeException ex) { 476 debug(DBG_GEN, "Exception on remote stop: %s\n", ex); 477 } finally { 478 vm.resume(); 479 } 480 } 481 } 482 483 void debug(int flags, String format, Object... args) { 484 InternalDebugControl.debug(execEnv.state(), execEnv.userErr(), flags, format, args); 485 } 486 487 void debug(Exception ex, String where) { 488 InternalDebugControl.debug(execEnv.state(), execEnv.userErr(), ex, where); 489 } 490 491 private final class DemultiplexInput extends Thread { 492 493 private final DataInputStream delegate; 494 private final PipeInputStream command; 495 private final PrintStream out; 496 private final PrintStream err; 497 498 public DemultiplexInput(InputStream input, 499 PipeInputStream command, 500 PrintStream out, 501 PrintStream err) { 502 super("output reader"); 503 this.delegate = new DataInputStream(input); 504 this.command = command; 505 this.out = out; 506 this.err = err; 507 } 508 509 public void run() { 510 try { 511 while (true) { 512 int nameLen = delegate.read(); 513 if (nameLen == (-1)) 514 break; 515 byte[] name = new byte[nameLen]; 516 DemultiplexInput.this.delegate.readFully(name); 517 int dataLen = delegate.read(); 518 byte[] data = new byte[dataLen]; 519 DemultiplexInput.this.delegate.readFully(data); 520 switch (new String(name, "UTF-8")) { 521 case "err": 522 err.write(data); 523 break; 524 case "out": 525 out.write(data); 526 break; 527 case "command": 528 for (byte b : data) { 529 command.write(Byte.toUnsignedInt(b)); 530 } 531 break; 532 } 533 } 534 } catch (IOException ex) { 535 debug(ex, "Failed reading output"); 536 } finally { 537 command.close(); 538 } 539 } 540 541 } 542 543 public static final class PipeInputStream extends InputStream { 544 public static final int INITIAL_SIZE = 128; 545 546 private int[] buffer = new int[INITIAL_SIZE]; 547 private int start; 548 private int end; 549 private boolean closed; 550 551 @Override 552 public synchronized int read() { 553 while (start == end) { 554 if (closed) { 555 return -1; 556 } 557 try { 558 wait(); 559 } catch (InterruptedException ex) { 560 //ignore 561 } 562 } 563 try { 564 return buffer[start]; 565 } finally { 566 start = (start + 1) % buffer.length; 567 } 568 } 569 570 public synchronized void write(int b) { 571 if (closed) 572 throw new IllegalStateException("Already closed."); 573 int newEnd = (end + 1) % buffer.length; 574 if (newEnd == start) { 575 //overflow: 576 int[] newBuffer = new int[buffer.length * 2]; 577 int rightPart = (end > start ? end : buffer.length) - start; 578 int leftPart = end > start ? 0 : start - 1; 579 System.arraycopy(buffer, start, newBuffer, 0, rightPart); 580 System.arraycopy(buffer, 0, newBuffer, rightPart, leftPart); 581 buffer = newBuffer; 582 start = 0; 583 end = rightPart + leftPart; 584 newEnd = end + 1; 585 } 586 buffer[end] = b; 587 end = newEnd; 588 notifyAll(); 589 } 590 591 @Override 592 public synchronized void close() { 593 closed = true; 594 notifyAll(); 595 } 596 597 } 598 }