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.IOException; 28 import java.io.InputStream; 29 import java.io.ObjectInput; 30 import java.io.ObjectOutput; 31 import java.io.OutputStream; 32 import java.net.ServerSocket; 33 import java.net.Socket; 34 import java.util.ArrayList; 35 import java.util.HashMap; 36 import java.util.List; 37 import java.util.Map; 38 import java.util.function.Consumer; 39 import com.sun.jdi.BooleanValue; 40 import com.sun.jdi.ClassNotLoadedException; 41 import com.sun.jdi.Field; 42 import com.sun.jdi.IncompatibleThreadStateException; 43 import com.sun.jdi.InvalidTypeException; 44 import com.sun.jdi.ObjectReference; 45 import com.sun.jdi.StackFrame; 46 import com.sun.jdi.ThreadReference; 47 import com.sun.jdi.VMDisconnectedException; 48 import com.sun.jdi.VirtualMachine; 49 import jdk.jshell.spi.ExecutionControl; 50 import jdk.jshell.spi.ExecutionEnv; 51 import static jdk.jshell.execution.Util.remoteInputOutput; 52 53 /** 54 * The implementation of {@link jdk.jshell.spi.ExecutionControl} that the 55 * JShell-core uses by default. 56 * Launches a remote process -- the "remote agent". 57 * Interfaces to the remote agent over a socket and via JDI. 58 * Designed to work with {@link RemoteExecutionControl}. 59 * 60 * @author Robert Field 61 * @author Jan Lahoda 62 */ 63 public class JDIDefaultExecutionControl extends JDIExecutionControl { 64 65 private static final String REMOTE_AGENT = RemoteExecutionControl.class.getName(); 66 67 private VirtualMachine vm; 68 private Process process; 69 70 private final Object STOP_LOCK = new Object(); 71 private boolean userCodeRunning = false; 72 73 /** 74 * Creates an ExecutionControl instance based on a JDI 75 * {@code LaunchingConnector}. 76 * 77 * @return the generator 78 */ 79 public static ExecutionControl.Generator launch() { 80 return env -> create(env, true); 81 } 82 83 /** 84 * Creates an ExecutionControl instance based on a JDI 85 * {@code ListeningConnector}. 86 * 87 * @return the generator 88 */ 89 public static ExecutionControl.Generator listen() { 90 return env -> create(env, false); 91 } 92 93 /** 94 * Creates an ExecutionControl instance based on a JDI 95 * {@code ListeningConnector} or {@code LaunchingConnector}. 96 * 97 * Initialize JDI and use it to launch the remote JVM. Set-up a socket for 98 * commands and results. This socket also transports the user 99 * input/output/error. 100 * 101 * @param env the context passed by 102 * {@link jdk.jshell.spi.ExecutionControl#start(jdk.jshell.spi.ExecutionEnv) } 103 * @return the channel 104 * @throws IOException if there are errors in set-up 105 */ 106 private static ExecutionControl create(ExecutionEnv env, boolean isLaunch) throws IOException { 107 try (final ServerSocket listener = new ServerSocket(0)) { 108 // timeout after 60 seconds 109 listener.setSoTimeout(60000); 110 int port = listener.getLocalPort(); 111 112 // Set-up the JDI connection 113 JDIInitiator jdii = new JDIInitiator(port, 114 env.extraRemoteVMOptions(), REMOTE_AGENT, isLaunch); 115 VirtualMachine vm = jdii.vm(); 116 Process process = jdii.process(); 117 118 List<Consumer<String>> deathListeners = new ArrayList<>(); 119 deathListeners.add(s -> env.closeDown()); 120 Util.detectJDIExitEvent(vm, s -> { 121 for (Consumer<String> h : deathListeners) { 122 h.accept(s); 123 } 124 }); 125 126 // Set-up the commands/reslts on the socket. Piggy-back snippet 127 // output. 128 Socket socket = listener.accept(); 129 // out before in -- match remote creation so we don't hang 130 OutputStream out = socket.getOutputStream(); 131 Map<String, OutputStream> outputs = new HashMap<>(); 132 outputs.put("out", env.userOut()); 133 outputs.put("err", env.userErr()); 134 Map<String, InputStream> input = new HashMap<>(); 135 input.put("in", env.userIn()); 136 return remoteInputOutput(socket.getInputStream(), out, outputs, input, (objIn, objOut) -> new JDIDefaultExecutionControl(objOut, objIn, vm, process, deathListeners)); 137 } 138 } 139 140 /** 141 * Create an instance. 142 * 143 * @param cmdout the output for commands 144 * @param cmdin the input for responses 145 */ 146 private JDIDefaultExecutionControl(ObjectOutput cmdout, ObjectInput cmdin, 147 VirtualMachine vm, Process process, List<Consumer<String>> deathListeners) { 148 super(cmdout, cmdin); 149 this.vm = vm; 150 this.process = process; 151 deathListeners.add(s -> disposeVM()); 152 } 153 154 @Override 155 public String invoke(String classname, String methodname) 156 throws RunException, 157 EngineTerminationException, InternalException { 158 String res; 159 synchronized (STOP_LOCK) { 160 userCodeRunning = true; 161 } 162 try { 163 res = super.invoke(classname, methodname); 164 } finally { 165 synchronized (STOP_LOCK) { 166 userCodeRunning = false; 167 } 168 } 169 return res; 170 } 171 172 /** 173 * Interrupts a running remote invoke by manipulating remote variables 174 * and sending a stop via JDI. 175 * 176 * @throws EngineTerminationException the execution engine has terminated 177 * @throws InternalException an internal problem occurred 178 */ 179 @Override 180 public void stop() throws EngineTerminationException, InternalException { 181 synchronized (STOP_LOCK) { 182 if (!userCodeRunning) { 183 return; 184 } 185 186 vm().suspend(); 187 try { 188 OUTER: 189 for (ThreadReference thread : vm().allThreads()) { 190 // could also tag the thread (e.g. using name), to find it easier 191 for (StackFrame frame : thread.frames()) { 192 if (REMOTE_AGENT.equals(frame.location().declaringType().name()) && 193 ( "invoke".equals(frame.location().method().name()) 194 || "varValue".equals(frame.location().method().name()))) { 195 ObjectReference thiz = frame.thisObject(); 196 Field inClientCode = thiz.referenceType().fieldByName("inClientCode"); 197 Field expectingStop = thiz.referenceType().fieldByName("expectingStop"); 198 Field stopException = thiz.referenceType().fieldByName("stopException"); 199 if (((BooleanValue) thiz.getValue(inClientCode)).value()) { 200 thiz.setValue(expectingStop, vm().mirrorOf(true)); 201 ObjectReference stopInstance = (ObjectReference) thiz.getValue(stopException); 202 203 vm().resume(); 204 debug("Attempting to stop the client code...\n"); 205 thread.stop(stopInstance); 206 thiz.setValue(expectingStop, vm().mirrorOf(false)); 207 } 208 209 break OUTER; 210 } 211 } 212 } 213 } catch (ClassNotLoadedException | IncompatibleThreadStateException | InvalidTypeException ex) { 214 throw new InternalException("Exception on remote stop: " + ex); 215 } finally { 216 vm().resume(); 217 } 218 } 219 } 220 221 @Override 222 public void close() { 223 super.close(); 224 disposeVM(); 225 } 226 227 private synchronized void disposeVM() { 228 try { 229 if (vm != null) { 230 vm.dispose(); // This could NPE, so it is caught below 231 vm = null; 232 } 233 } catch (VMDisconnectedException ex) { 234 // Ignore if already closed 235 } catch (Throwable ex) { 236 debug(ex, "disposeVM"); 237 } finally { 238 if (process != null) { 239 process.destroy(); 240 process = null; 241 } 242 } 243 } 244 245 @Override 246 protected synchronized VirtualMachine vm() throws EngineTerminationException { 247 if (vm == null) { 248 throw new EngineTerminationException("VM closed"); 249 } else { 250 return vm; 251 } 252 } 253 254 /** 255 * Log debugging information. Arguments as for {@code printf}. 256 * 257 * @param format a format string as described in Format string syntax 258 * @param args arguments referenced by the format specifiers in the format 259 * string. 260 */ 261 private static void debug(String format, Object... args) { 262 // Reserved for future logging 263 } 264 265 /** 266 * Log a serious unexpected internal exception. 267 * 268 * @param ex the exception 269 * @param where a description of the context of the exception 270 */ 271 private static void debug(Throwable ex, String where) { 272 // Reserved for future logging 273 } 274 275 }