| /* | |
| * Copyright (c) 2016, 2023, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. Oracle designates this | |
| * particular file as subject to the "Classpath" exception as provided | |
| * by Oracle in the LICENSE file that accompanied this code. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| package jdk.jshell.execution; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.nio.charset.StandardCharsets; | |
| import java.nio.file.Files; | |
| import java.util.ArrayList; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Map.Entry; | |
| import com.sun.jdi.Bootstrap; | |
| import com.sun.jdi.VirtualMachine; | |
| import com.sun.jdi.connect.Connector; | |
| import com.sun.jdi.connect.LaunchingConnector; | |
| import com.sun.jdi.connect.ListeningConnector; | |
| import java.util.concurrent.Callable; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.Future; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.concurrent.TimeoutException; | |
| import com.sun.jdi.connect.IllegalConnectorArgumentsException; | |
| /** | |
| * Sets up a JDI connection, providing the resulting JDI {@link VirtualMachine} | |
| * and the {@link Process} the remote agent is running in. | |
| * | |
| * @since 9 | |
| */ | |
| public class JdiInitiator { | |
| // factor for the timeout on all of connect | |
| private static final double CONNECT_TIMEOUT_FACTOR = 1.5; | |
| // Over-all connect time-out | |
| private final int connectTimeout; | |
| private VirtualMachine vm; | |
| private Process process = null; | |
| private final Connector connector; | |
| private final String remoteAgent; | |
| private final Map<String, com.sun.jdi.connect.Connector.Argument> connectorArgs; | |
| /** | |
| * Start the remote agent and establish a JDI connection to it. | |
| * | |
| * @param port the socket port for (non-JDI) commands | |
| * @param remoteVMOptions any user requested VM command-line options | |
| * @param remoteAgent full class name of remote agent to launch | |
| * @param isLaunch does JDI do the launch? That is, LaunchingConnector, | |
| * otherwise we start explicitly and use ListeningConnector | |
| * @param host explicit hostname to use, if null use discovered | |
| * hostname, applies to listening only (!isLaunch) | |
| * @param timeout the start-up time-out in milliseconds. If zero or negative, | |
| * will not wait thus will timeout immediately if not already started. | |
| * @param customConnectorArgs custom arguments passed to the connector. | |
| * These are JDI com.sun.jdi.connect.Connector arguments. The {@code vmexec} | |
| * argument is not supported. | |
| */ | |
| @SuppressWarnings("this-escape") | |
| public JdiInitiator(int port, List<String> remoteVMOptions, String remoteAgent, | |
| boolean isLaunch, String host, int timeout, | |
| Map<String, String> customConnectorArgs) { | |
| this.remoteAgent = remoteAgent; | |
| this.connectTimeout = (int) (timeout * CONNECT_TIMEOUT_FACTOR); | |
| String connectorName | |
| = isLaunch | |
| ? "com.sun.jdi.CommandLineLaunch" | |
| : "com.sun.jdi.SocketListen"; | |
| this.connector = findConnector(connectorName); | |
| if (connector == null) { | |
| throw new IllegalArgumentException("No connector named: " + connectorName); | |
| } | |
| Map<String, String> argumentName2Value | |
| = isLaunch | |
| ? launchArgs(port, String.join(" ", remoteVMOptions)) | |
| : new HashMap<>(); | |
| if (!isLaunch) { | |
| argumentName2Value.put("timeout", ""+timeout); | |
| if (host != null && !isLaunch) { | |
| argumentName2Value.put("localAddress", host); | |
| } | |
| } | |
| customConnectorArgs.entrySet() | |
| .stream() | |
| .filter(e -> !"vmexec".equals(e.getKey())) | |
| .forEach(e -> argumentName2Value.put(e.getKey(), e.getValue())); | |
| this.connectorArgs = mergeConnectorArgs(connector, argumentName2Value); | |
| this.vm = isLaunch | |
| ? launchTarget() | |
| : listenTarget(port, remoteVMOptions); | |
| } | |
| /** | |
| * Returns the resulting {@code VirtualMachine} instance. | |
| * | |
| * @return the virtual machine | |
| */ | |
| public VirtualMachine vm() { | |
| return vm; | |
| } | |
| /** | |
| * Returns the launched process. | |
| * | |
| * @return the remote agent process | |
| */ | |
| public Process process() { | |
| return process; | |
| } | |
| /* launch child target vm */ | |
| private VirtualMachine launchTarget() { | |
| LaunchingConnector launcher = (LaunchingConnector) connector; | |
| try { | |
| VirtualMachine new_vm = timedVirtualMachineCreation(() -> launcher.launch(connectorArgs), null); | |
| process = new_vm.process(); | |
| return new_vm; | |
| } catch (Throwable ex) { | |
| throw reportLaunchFail(ex, "launch"); | |
| } | |
| } | |
| /** | |
| * Directly launch the remote agent and connect JDI to it with a | |
| * ListeningConnector. | |
| */ | |
| private VirtualMachine listenTarget(int port, List<String> remoteVMOptions) { | |
| ListeningConnector listener = (ListeningConnector) connector; | |
| try { | |
| String addr; | |
| try { | |
| // Start listening, get the JDI connection address | |
| addr = listener.startListening(connectorArgs); | |
| debug("Listening at address: " + addr); | |
| } catch (Throwable t) { | |
| throw reportLaunchFail(t, "listen"); | |
| } | |
| runListenProcess(addr, port, remoteVMOptions, process -> { | |
| // Accept the connection from the remote agent | |
| vm = timedVirtualMachineCreation(() -> listener.accept(connectorArgs), | |
| () -> process.waitFor()); | |
| try { | |
| listener.stopListening(connectorArgs); | |
| } catch (IOException | IllegalConnectorArgumentsException ex) { | |
| // ignore | |
| } | |
| }); | |
| return vm; | |
| } catch (Throwable ex) { | |
| try { | |
| listener.stopListening(connectorArgs); | |
| } catch (IOException | IllegalConnectorArgumentsException iex) { | |
| // ignore | |
| } | |
| throw ex; | |
| } | |
| } | |
| /** | |
| * Create a process that will attach to the given address. | |
| * @param jdiAddress address on which a JDI server is waiting for a connection | |
| * @param jshellControlPort the port which the remote agent should connect to | |
| * @param remoteVMOptions VM options for the remote agent VM | |
| * @param setupVM a callback that should be called then the remote agent process | |
| * is created. The callback will setup the JDI's {@code VirtualMachine}. | |
| * @since 22 | |
| */ | |
| protected void runListenProcess(String jdiAddress, | |
| int jshellControlPort, | |
| List<String> remoteVMOptions, | |
| ProcessStarted setupVM) { | |
| // Files to collection to output of a start-up failure | |
| File crashErrorFile = createTempFile("error"); | |
| File crashOutputFile = createTempFile("output"); | |
| try { | |
| // Launch the RemoteAgent requesting a connection on that address | |
| String javaHome = System.getProperty("java.home"); | |
| List<String> args = new ArrayList<>(); | |
| args.add(javaHome == null | |
| ? "java" | |
| : javaHome + File.separator + "bin" + File.separator + "java"); | |
| args.add("-agentlib:jdwp=transport=" + connector.transport().name() + | |
| ",address=" + jdiAddress); | |
| args.addAll(remoteVMOptions); | |
| args.add(remoteAgent); | |
| args.add("" + jshellControlPort); | |
| ProcessBuilder pb = new ProcessBuilder(args); | |
| pb.redirectError(crashErrorFile); | |
| pb.redirectOutput(crashOutputFile); | |
| process = pb.start(); | |
| setupVM.processStarted(process); | |
| crashErrorFile.delete(); | |
| crashOutputFile.delete(); | |
| } catch (Throwable ex) { | |
| if (process != null) { | |
| process.destroyForcibly(); | |
| } | |
| String text = readFile(crashErrorFile) + readFile(crashOutputFile); | |
| crashErrorFile.delete(); | |
| crashOutputFile.delete(); | |
| if (text.isEmpty()) { | |
| throw reportLaunchFail(ex, "listen"); | |
| } else { | |
| throw new IllegalArgumentException(text); | |
| } | |
| } | |
| } | |
| private File createTempFile(String label) { | |
| try { | |
| File f = File.createTempFile("remote", label); | |
| f.deleteOnExit(); | |
| return f; | |
| } catch (IOException ex) { | |
| throw new InternalError("Failed create temp ", ex); | |
| } | |
| } | |
| private String readFile(File f) { | |
| try { | |
| return new String(Files.readAllBytes(f.toPath()), | |
| StandardCharsets.UTF_8); | |
| } catch (IOException ex) { | |
| return "error reading " + f + " : " + ex.toString(); | |
| } | |
| } | |
| VirtualMachine timedVirtualMachineCreation(Callable<VirtualMachine> creator, | |
| Callable<Integer> processComplete) throws Exception { | |
| VirtualMachine result; | |
| ExecutorService executor = Executors.newCachedThreadPool(runnable -> { | |
| Thread thread = Executors.defaultThreadFactory().newThread(runnable); | |
| thread.setDaemon(true); | |
| return thread; | |
| }); | |
| try { | |
| Future<VirtualMachine> future = executor.submit(creator); | |
| if (processComplete != null) { | |
| executor.submit(() -> { | |
| Integer i = processComplete.call(); | |
| future.cancel(true); | |
| return i; | |
| }); | |
| } | |
| try { | |
| result = future.get(connectTimeout, TimeUnit.MILLISECONDS); | |
| } catch (TimeoutException ex) { | |
| future.cancel(true); | |
| throw ex; | |
| } | |
| } finally { | |
| executor.shutdownNow(); | |
| } | |
| return result; | |
| } | |
| private Connector findConnector(String name) { | |
| for (Connector cntor | |
| : Bootstrap.virtualMachineManager().allConnectors()) { | |
| if (cntor.name().equals(name)) { | |
| return cntor; | |
| } | |
| } | |
| return null; | |
| } | |
| private Map<String, Connector.Argument> mergeConnectorArgs(Connector connector, Map<String, String> argumentName2Value) { | |
| Map<String, Connector.Argument> arguments = connector.defaultArguments(); | |
| for (Entry<String, String> argumentEntry : argumentName2Value.entrySet()) { | |
| String name = argumentEntry.getKey(); | |
| String value = argumentEntry.getValue(); | |
| Connector.Argument argument = arguments.get(name); | |
| if (argument == null) { | |
| throw new IllegalArgumentException("Argument is not defined for connector:" + | |
| name + " -- " + connector.name()); | |
| } | |
| argument.setValue(value); | |
| } | |
| return arguments; | |
| } | |
| /** | |
| * The JShell specific Connector args for the LaunchingConnector. | |
| * | |
| * @param portthe socket port for (non-JDI) commands | |
| * @param remoteVMOptions any user requested VM options | |
| * @return the argument map | |
| */ | |
| private Map<String, String> launchArgs(int port, String remoteVMOptions) { | |
| Map<String, String> argumentName2Value = new HashMap<>(); | |
| argumentName2Value.put("main", remoteAgent + " " + port); | |
| argumentName2Value.put("options", remoteVMOptions); | |
| return argumentName2Value; | |
| } | |
| private InternalError reportLaunchFail(Throwable ex, String context) { | |
| return new InternalError("Failed remote " + context + ": " | |
| + ex.toString() | |
| + " @ " + connector + | |
| " -- " + connectorArgs, ex); | |
| } | |
| /** | |
| * Log debugging information. Arguments as for {@code printf}. | |
| * | |
| * @param format a format string as described in Format string syntax | |
| * @param args arguments referenced by the format specifiers in the format | |
| * string. | |
| */ | |
| private void debug(String format, Object... args) { | |
| // Reserved for future logging | |
| } | |
| /** | |
| * Log a serious unexpected internal exception. | |
| * | |
| * @param ex the exception | |
| * @param where a description of the context of the exception | |
| */ | |
| private void debug(Throwable ex, String where) { | |
| // Reserved for future logging | |
| } | |
| /** | |
| * Callback that should invoked when the remote process is invoked. | |
| * | |
| * @since 22 | |
| */ | |
| protected interface ProcessStarted { | |
| /** | |
| * Notify the process has been started. | |
| * | |
| * @param p the {@code Process} | |
| * @throws Throwable thrown when anything goes wrong. | |
| */ | |
| public void processStarted(Process p) throws Throwable; | |
| } | |
| } |