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 }