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 }