1 /* 2 * Copyright (c) 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 package jdk.jshell.execution; 26 27 import java.io.File; 28 import java.util.ArrayList; 29 import java.util.HashMap; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Map.Entry; 33 import com.sun.jdi.Bootstrap; 34 import com.sun.jdi.VirtualMachine; 35 import com.sun.jdi.connect.Connector; 36 import com.sun.jdi.connect.LaunchingConnector; 37 import com.sun.jdi.connect.ListeningConnector; 38 39 /** 40 * Sets up a JDI connection, providing the resulting JDI {@link VirtualMachine} 41 * and the {@link Process} the remote agent is running in. 42 */ 43 public class JDIInitiator { 44 45 private VirtualMachine vm; 46 private Process process = null; 47 private final Connector connector; 48 private final String remoteAgent; 49 private final Map<String, com.sun.jdi.connect.Connector.Argument> connectorArgs; 50 51 /** 52 * Start the remote agent and establish a JDI connection to it. 53 * 54 * @param port the socket port for (non-JDI) commands 55 * @param remoteVMOptions any user requested VM options 56 * @param remoteAgent full class name of remote agent to launch 57 * @param isLaunch does JDI do the launch? That is, LaunchingConnector, 58 * otherwise we start explicitly and use ListeningConnector 59 * @param useLocalhost explicitly use "localhost" rather than discovered 60 * hostname, applies to listening only (!isLaunch) 61 */ 62 public JDIInitiator(int port, List<String> remoteVMOptions, String remoteAgent, 63 boolean isLaunch, boolean useLocalhost) { 64 this.remoteAgent = remoteAgent; 65 String connectorName 66 = isLaunch 67 ? "com.sun.jdi.CommandLineLaunch" 68 : "com.sun.jdi.SocketListen"; 69 this.connector = findConnector(connectorName); 70 if (connector == null) { 71 throw new IllegalArgumentException("No connector named: " + connectorName); 72 } 73 Map<String, String> argumentName2Value 74 = isLaunch 75 ? launchArgs(port, String.join(" ", remoteVMOptions)) 76 : new HashMap<>(); 77 if (useLocalhost && !isLaunch) { 78 argumentName2Value.put("localAddress", "localhost"); 79 } 80 this.connectorArgs = mergeConnectorArgs(connector, argumentName2Value); 81 this.vm = isLaunch 82 ? launchTarget() 83 : listenTarget(port, remoteVMOptions); 84 85 } 86 87 /** 88 * Returns the resulting {@code VirtualMachine} instance. 89 * 90 * @return the virtual machine 91 */ 92 public VirtualMachine vm() { 93 return vm; 94 } 95 96 /** 97 * Returns the launched process. 98 * 99 * @return the remote agent process 100 */ 101 public Process process() { 102 return process; 103 } 104 105 /* launch child target vm */ 106 private VirtualMachine launchTarget() { 107 LaunchingConnector launcher = (LaunchingConnector) connector; 108 try { 109 VirtualMachine new_vm = launcher.launch(connectorArgs); 110 process = new_vm.process(); 111 return new_vm; 112 } catch (Exception ex) { 113 reportLaunchFail(ex, "launch"); 114 } 115 return null; 116 } 117 118 /** 119 * Directly launch the remote agent and connect JDI to it with a 120 * ListeningConnector. 121 */ 122 private VirtualMachine listenTarget(int port, List<String> remoteVMOptions) { 123 ListeningConnector listener = (ListeningConnector) connector; 124 try { 125 // Start listening, get the JDI connection address 126 String addr = listener.startListening(connectorArgs); 127 debug("Listening at address: " + addr); 128 129 // Launch the RemoteAgent requesting a connection on that address 130 String javaHome = System.getProperty("java.home"); 131 List<String> args = new ArrayList<>(); 132 args.add(javaHome == null 133 ? "java" 134 : javaHome + File.separator + "bin" + File.separator + "java"); 135 args.add("-agentlib:jdwp=transport=" + connector.transport().name() + 136 ",address=" + addr); 137 args.addAll(remoteVMOptions); 138 args.add(remoteAgent); 139 args.add("" + port); 140 ProcessBuilder pb = new ProcessBuilder(args); 141 process = pb.start(); 142 143 // Forward out, err, and in 144 // Accept the connection from the remote agent 145 vm = listener.accept(connectorArgs); 146 listener.stopListening(connectorArgs); 147 return vm; 148 } catch (Exception ex) { 149 reportLaunchFail(ex, "listen"); 150 } 151 return null; 152 } 153 154 private Connector findConnector(String name) { 155 for (Connector cntor 156 : Bootstrap.virtualMachineManager().allConnectors()) { 157 if (cntor.name().equals(name)) { 158 return cntor; 159 } 160 } 161 return null; 162 } 163 164 private Map<String, Connector.Argument> mergeConnectorArgs(Connector connector, Map<String, String> argumentName2Value) { 165 Map<String, Connector.Argument> arguments = connector.defaultArguments(); 166 167 for (Entry<String, String> argumentEntry : argumentName2Value.entrySet()) { 168 String name = argumentEntry.getKey(); 169 String value = argumentEntry.getValue(); 170 Connector.Argument argument = arguments.get(name); 171 172 if (argument == null) { 173 throw new IllegalArgumentException("Argument is not defined for connector:" + 174 name + " -- " + connector.name()); 175 } 176 177 argument.setValue(value); 178 } 179 180 return arguments; 181 } 182 183 /** 184 * The JShell specific Connector args for the LaunchingConnector. 185 * 186 * @param portthe socket port for (non-JDI) commands 187 * @param remoteVMOptions any user requested VM options 188 * @return the argument map 189 */ 190 private Map<String, String> launchArgs(int port, String remoteVMOptions) { 191 Map<String, String> argumentName2Value = new HashMap<>(); 192 argumentName2Value.put("main", remoteAgent + " " + port); 193 argumentName2Value.put("options", remoteVMOptions); 194 return argumentName2Value; 195 } 196 197 private void reportLaunchFail(Exception ex, String context) { 198 throw new InternalError("Failed remote " + context + ": " + connector + 199 " -- " + connectorArgs, ex); 200 } 201 202 /** 203 * Log debugging information. Arguments as for {@code printf}. 204 * 205 * @param format a format string as described in Format string syntax 206 * @param args arguments referenced by the format specifiers in the format 207 * string. 208 */ 209 private void debug(String format, Object... args) { 210 // Reserved for future logging 211 } 212 213 /** 214 * Log a serious unexpected internal exception. 215 * 216 * @param ex the exception 217 * @param where a description of the context of the exception 218 */ 219 private void debug(Throwable ex, String where) { 220 // Reserved for future logging 221 } 222 223 }