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.ObjectInput; 29 import java.io.ObjectOutput; 30 import java.io.ObjectOutputStream; 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.remoteInput; 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 JDIDefaultExecutionControl 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 // Forward input to the remote agent 119 Util.forwardInputToRemote(env.userIn(), process.getOutputStream(), 120 ex -> debug(ex, "input forwarding failure")); 121 122 List<Consumer<String>> deathListeners = new ArrayList<>(); 123 deathListeners.add(s -> env.closeDown()); 124 Util.detectJDIExitEvent(vm, s -> { 125 for (Consumer<String> h : deathListeners) { 126 h.accept(s); 127 } 128 }); 129 130 // Set-up the commands/reslts on the socket. Piggy-back snippet 131 // output. 132 Socket socket = listener.accept(); 133 // out before in -- match remote creation so we don't hang 134 ObjectOutput cmdout = new ObjectOutputStream(socket.getOutputStream()); 135 Map<String, OutputStream> io = new HashMap<>(); 136 io.put("out", env.userOut()); 137 io.put("err", env.userErr()); 138 ObjectInput cmdin = remoteInput(socket.getInputStream(), io); 139 return new JDIDefaultExecutionControl(cmdout, cmdin, vm, process, deathListeners); 140 } 141 } 142 143 /** 144 * Create an instance. 145 * 146 * @param cmdout the output for commands 147 * @param cmdin the input for responses 148 */ 149 private JDIDefaultExecutionControl(ObjectOutput cmdout, ObjectInput cmdin, 150 VirtualMachine vm, Process process, List<Consumer<String>> deathListeners) { 151 super(cmdout, cmdin); 152 this.vm = vm; 153 this.process = process; 154 deathListeners.add(s -> disposeVM()); 155 } 156 157 @Override 158 public String invoke(String classname, String methodname) 159 throws RunException, 160 EngineTerminationException, InternalException { 161 String res; 162 synchronized (STOP_LOCK) { 163 userCodeRunning = true; 164 } 165 try { 166 res = super.invoke(classname, methodname); 167 } finally { 168 synchronized (STOP_LOCK) { 169 userCodeRunning = false; 170 } 171 } 172 return res; 173 } 174 175 /** 176 * Interrupts a running remote invoke by manipulating remote variables 177 * and sending a stop via JDI. 178 * 179 * @throws EngineTerminationException the execution engine has terminated 180 * @throws InternalException an internal problem occurred 181 */ 182 @Override 183 public void stop() throws EngineTerminationException, InternalException { 184 synchronized (STOP_LOCK) { 185 if (!userCodeRunning) { 186 return; 187 } 188 189 vm().suspend(); 190 try { 191 OUTER: 192 for (ThreadReference thread : vm().allThreads()) { 193 // could also tag the thread (e.g. using name), to find it easier 194 for (StackFrame frame : thread.frames()) { 195 if (REMOTE_AGENT.equals(frame.location().declaringType().name()) && 196 ( "invoke".equals(frame.location().method().name()) 197 || "varValue".equals(frame.location().method().name()))) { 198 ObjectReference thiz = frame.thisObject(); 199 Field inClientCode = thiz.referenceType().fieldByName("inClientCode"); 200 Field expectingStop = thiz.referenceType().fieldByName("expectingStop"); 201 Field stopException = thiz.referenceType().fieldByName("stopException"); 202 if (((BooleanValue) thiz.getValue(inClientCode)).value()) { 203 thiz.setValue(expectingStop, vm().mirrorOf(true)); 204 ObjectReference stopInstance = (ObjectReference) thiz.getValue(stopException); 205 206 vm().resume(); 207 debug("Attempting to stop the client code...\n"); 208 thread.stop(stopInstance); 209 thiz.setValue(expectingStop, vm().mirrorOf(false)); 210 } 211 212 break OUTER; 213 } 214 } 215 } 216 } catch (ClassNotLoadedException | IncompatibleThreadStateException | InvalidTypeException ex) { 217 throw new InternalException("Exception on remote stop: " + ex); 218 } finally { 219 vm().resume(); 220 } 221 } 222 } 223 224 @Override 225 public void close() { 226 super.close(); 227 disposeVM(); 228 } 229 230 private synchronized void disposeVM() { 231 try { 232 if (vm != null) { 233 vm.dispose(); // This could NPE, so it is caught below 234 vm = null; 235 } 236 } catch (VMDisconnectedException ex) { 237 // Ignore if already closed 238 } catch (Throwable ex) { 239 debug(ex, "disposeVM"); 240 } finally { 241 if (process != null) { 242 process.destroy(); 243 process = null; 244 } 245 } 246 } 247 248 @Override 249 protected synchronized VirtualMachine vm() throws EngineTerminationException { 250 if (vm == null) { 251 throw new EngineTerminationException("VM closed"); 252 } else { 253 return vm; 254 } 255 } 256 257 /** 258 * Log debugging information. Arguments as for {@code printf}. 259 * 260 * @param format a format string as described in Format string syntax 261 * @param args arguments referenced by the format specifiers in the format 262 * string. 263 */ 264 private static void debug(String format, Object... args) { 265 // Reserved for future logging 266 } 267 268 /** 269 * Log a serious unexpected internal exception. 270 * 271 * @param ex the exception 272 * @param where a description of the context of the exception 273 */ 274 private static void debug(Throwable ex, String where) { 275 // Reserved for future logging 276 } 277 278 }