1 /*
   2  * Copyright (c) 1998, 2016, 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 /*
  27  * This source code is provided to illustrate the usage of a given feature
  28  * or technique and has been deliberately simplified. Additional steps
  29  * required for a production-quality application, such as security checks,
  30  * input validation and proper error handling, might not be present in
  31  * this sample code.
  32  */
  33 
  34 
  35 package jdk.internal.jshell.jdi;
  36 
  37 import com.sun.jdi.*;
  38 import com.sun.jdi.connect.*;
  39 
  40 import java.util.*;
  41 import java.util.Map.Entry;
  42 import java.io.*;
  43 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN;
  44 
  45 /**
  46  * Connection to a Java Debug Interface VirtualMachine instance.
  47  * Adapted from jdb VMConnection. Message handling, exception handling, and I/O
  48  * redirection changed.  Interface to JShell added.
  49  */
  50 class JDIConnection {
  51 
  52     private static final String REMOTE_AGENT = "jdk.internal.jshell.remote.RemoteAgent";
  53 
  54     private VirtualMachine vm;
  55     private boolean active = true;
  56     private Process process = null;
  57     private int outputCompleteCount = 0;
  58 
  59     private final JDIExecutionControl ec;
  60     private final Connector connector;
  61     private final Map<String, com.sun.jdi.connect.Connector.Argument> connectorArgs;
  62     private final int traceFlags;
  63 
  64     private synchronized void notifyOutputComplete() {
  65         outputCompleteCount++;
  66         notifyAll();
  67     }
  68 
  69     private synchronized void waitOutputComplete() {
  70         // Wait for stderr and stdout
  71         if (process != null) {
  72             while (outputCompleteCount < 2) {
  73                 try {wait();} catch (InterruptedException e) {}
  74             }
  75         }
  76     }
  77 
  78     private Connector findConnector(String name) {
  79         for (Connector cntor :
  80                  Bootstrap.virtualMachineManager().allConnectors()) {
  81             if (cntor.name().equals(name)) {
  82                 return cntor;
  83             }
  84         }
  85         return null;
  86     }
  87 
  88     private Map <String, Connector.Argument> mergeConnectorArgs(Connector connector, Map<String, String> argumentName2Value) {
  89         Map<String, Connector.Argument> arguments = connector.defaultArguments();
  90 
  91         for (Entry<String, String> argumentEntry : argumentName2Value.entrySet()) {
  92             String name = argumentEntry.getKey();
  93             String value = argumentEntry.getValue();
  94             Connector.Argument argument = arguments.get(name);
  95 
  96             if (argument == null) {
  97                 throw new IllegalArgumentException("Argument is not defined for connector:" +
  98                                           name + " -- " + connector.name());
  99             }
 100 
 101             argument.setValue(value);
 102         }
 103 
 104         return arguments;
 105     }
 106 
 107     /**
 108      * The JShell specific Connector args for the LaunchingConnector.
 109      *
 110      * @param portthe socket port for (non-JDI) commands
 111      * @param remoteVMOptions any user requested VM options
 112      * @return the argument map
 113      */
 114     private static Map<String, String> launchArgs(int port, String remoteVMOptions) {
 115         Map<String, String> argumentName2Value = new HashMap<>();
 116         argumentName2Value.put("main", REMOTE_AGENT + " " + port);
 117         argumentName2Value.put("options", remoteVMOptions);
 118         return argumentName2Value;
 119     }
 120 
 121     /**
 122      * Start the remote agent and establish a JDI connection to it.
 123      *
 124      * @param ec the execution control instance
 125      * @param port the socket port for (non-JDI) commands
 126      * @param remoteVMOptions any user requested VM options
 127      * @param isLaunch does JDI do the launch? That is, LaunchingConnector,
 128      * otherwise we start explicitly and use ListeningConnector
 129      */
 130     JDIConnection(JDIExecutionControl ec, int port, List<String> remoteVMOptions, boolean isLaunch) {
 131         this(ec,
 132                 isLaunch
 133                         ? "com.sun.jdi.CommandLineLaunch"
 134                         : "com.sun.jdi.SocketListen",
 135                 isLaunch
 136                         ? launchArgs(port, String.join(" ", remoteVMOptions))
 137                         : new HashMap<>(),
 138                 0);
 139         if (isLaunch) {
 140             vm = launchTarget();
 141         } else {
 142             vm = listenTarget(port, remoteVMOptions);
 143         }
 144 
 145         if (isOpen() && vm().canBeModified()) {
 146             /*
 147              * Connection opened on startup.
 148              */
 149             new JDIEventHandler(vm(), (b) -> ec.handleVMExit())
 150                     .start();
 151         }
 152     }
 153 
 154     /**
 155      * Base constructor -- set-up a JDI connection.
 156      *
 157      * @param ec the execution control instance
 158      * @param connectorName the standardized name of the connector
 159      * @param argumentName2Value the argument map
 160      * @param traceFlags should we trace JDI behavior
 161      */
 162     JDIConnection(JDIExecutionControl ec, String connectorName, Map<String, String> argumentName2Value, int traceFlags) {
 163         this.ec = ec;
 164         this.connector = findConnector(connectorName);
 165         if (connector == null) {
 166             throw new IllegalArgumentException("No connector named: " + connectorName);
 167         }
 168         connectorArgs = mergeConnectorArgs(connector, argumentName2Value);
 169         this.traceFlags = traceFlags;
 170     }
 171 
 172     final synchronized VirtualMachine vm() {
 173         if (vm == null) {
 174             throw new JDINotConnectedException();
 175         } else {
 176             return vm;
 177         }
 178     }
 179 
 180     private synchronized boolean isOpen() {
 181         return (vm != null);
 182     }
 183 
 184     synchronized boolean isRunning() {
 185         return process != null && process.isAlive();
 186     }
 187 
 188     // Beginning shutdown, ignore any random dying squeals
 189     void beginShutdown() {
 190         active = false;
 191     }
 192 
 193     synchronized void disposeVM() {
 194         try {
 195             if (vm != null) {
 196                 vm.dispose(); // This could NPE, so it is caught below
 197                 vm = null;
 198             }
 199         } catch (VMDisconnectedException ex) {
 200             // Ignore if already closed
 201         } catch (Throwable e) {
 202             ec.debug(DBG_GEN, null, "disposeVM threw: " + e);
 203         } finally {
 204             if (process != null) {
 205                 process.destroy();
 206                 process = null;
 207             }
 208             waitOutputComplete();
 209         }
 210     }
 211 
 212     private void dumpStream(InputStream inStream, final PrintStream pStream) throws IOException {
 213         BufferedReader in =
 214             new BufferedReader(new InputStreamReader(inStream));
 215         int i;
 216         try {
 217             while ((i = in.read()) != -1) {
 218                 // directly copy input to output, but skip if asked to close
 219                 if (active) {
 220                     pStream.print((char) i);
 221                 }
 222             }
 223         } catch (IOException ex) {
 224             String s = ex.getMessage();
 225             if (active && !s.startsWith("Bad file number")) {
 226                 throw ex;
 227             }
 228             // else we are being shutdown (and don't want any spurious death
 229             // throws to ripple) or
 230             // we got a Bad file number IOException which just means
 231             // that the debuggee has gone away.  We'll just treat it the
 232             // same as if we got an EOF.
 233         }
 234     }
 235 
 236     /**
 237      *  Create a Thread that will retrieve and display any output.
 238      *  Needs to be high priority, else debugger may exit before
 239      *  it can be displayed.
 240      */
 241     private void displayRemoteOutput(final InputStream inStream, final PrintStream pStream) {
 242         Thread thr = new Thread("output reader") {
 243             @Override
 244             public void run() {
 245                 try {
 246                     dumpStream(inStream, pStream);
 247                 } catch (IOException ex) {
 248                     ec.debug(ex, "Failed reading output");
 249                     ec.handleVMExit();
 250                 } finally {
 251                     notifyOutputComplete();
 252                 }
 253             }
 254         };
 255         thr.setPriority(Thread.MAX_PRIORITY-1);
 256         thr.start();
 257     }
 258 
 259     /**
 260      *  Create a Thread that will ship all input to remote.
 261      *  Does it need be high priority?
 262      */
 263     private void readRemoteInput(final OutputStream outStream, final InputStream inputStream) {
 264         Thread thr = new Thread("input reader") {
 265             @Override
 266             public void run() {
 267                 try {
 268                     byte[] buf = new byte[256];
 269                     int cnt;
 270                     while ((cnt = inputStream.read(buf)) != -1) {
 271                         outStream.write(buf, 0, cnt);
 272                         outStream.flush();
 273                     }
 274                 } catch (IOException ex) {
 275                     ec.debug(ex, "Failed reading output");
 276                     ec.handleVMExit();
 277                 }
 278             }
 279         };
 280         thr.setPriority(Thread.MAX_PRIORITY-1);
 281         thr.start();
 282     }
 283 
 284     private void forwardIO() {
 285         displayRemoteOutput(process.getErrorStream(), ec.execEnv.userErr());
 286         displayRemoteOutput(process.getInputStream(), ec.execEnv.userOut());
 287         readRemoteInput(process.getOutputStream(), ec.execEnv.userIn());
 288     }
 289 
 290     /* launch child target vm */
 291     private VirtualMachine launchTarget() {
 292         LaunchingConnector launcher = (LaunchingConnector)connector;
 293         try {
 294             VirtualMachine new_vm = launcher.launch(connectorArgs);
 295             process = new_vm.process();
 296             forwardIO();
 297             return new_vm;
 298         } catch (Exception ex) {
 299             reportLaunchFail(ex, "launch");
 300         }
 301         return null;
 302     }
 303 
 304     /**
 305      * Directly launch the remote agent and connect JDI to it with a
 306      * ListeningConnector.
 307      */
 308     private VirtualMachine listenTarget(int port, List<String> remoteVMOptions) {
 309         ListeningConnector listener = (ListeningConnector) connector;
 310         try {
 311             // Start listening, get the JDI connection address
 312             String addr = listener.startListening(connectorArgs);
 313             ec.debug(DBG_GEN, "Listening at address: " + addr);
 314 
 315             // Launch the RemoteAgent requesting a connection on that address
 316             String javaHome = System.getProperty("java.home");
 317             List<String> args = new ArrayList<>();
 318             args.add(javaHome == null
 319                     ? "java"
 320                     : javaHome + File.separator + "bin" + File.separator + "java");
 321             args.add("-agentlib:jdwp=transport=" + connector.transport().name() +
 322                     ",address=" + addr);
 323             args.addAll(remoteVMOptions);
 324             args.add(REMOTE_AGENT);
 325             args.add("" + port);
 326             ProcessBuilder pb = new ProcessBuilder(args);
 327             process = pb.start();
 328 
 329             // Forward out, err, and in
 330             forwardIO();
 331 
 332             // Accept the connection from the remote agent
 333             vm = listener.accept(connectorArgs);
 334             listener.stopListening(connectorArgs);
 335             return vm;
 336         } catch (Exception ex) {
 337             reportLaunchFail(ex, "listen");
 338         }
 339         return null;
 340     }
 341 
 342     private void reportLaunchFail(Exception ex, String context) {
 343         throw new InternalError("Failed remote " + context + ": " + connector +
 344                 " -- " + connectorArgs, ex);
 345     }
 346 }