/*
 * Copyright (c) 2016, 2017, 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 VirtualMachine and the Process the remote agent is running in.
Since:9
/** * 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.
Params:
  • port – the socket port for (non-JDI) commands
  • remoteVMOptions – any user requested VM command-line options
  • remoteAgent – full class name of remote agent to launch
  • isLaunch – does JDI do the launch? That is, LaunchingConnector, otherwise we start explicitly and use ListeningConnector
  • host – explicit hostname to use, if null use discovered hostname, applies to listening only (!isLaunch)
  • timeout – the start-up time-out in milliseconds. If zero or negative, will not wait thus will timeout immediately if not already started.
  • customConnectorArgs – custom arguments passed to the connector. These are JDI com.sun.jdi.connect.Connector arguments.
/** * 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. */
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); } } argumentName2Value.putAll(customConnectorArgs); this.connectorArgs = mergeConnectorArgs(connector, argumentName2Value); this.vm = isLaunch ? launchTarget() : listenTarget(port, remoteVMOptions); }
Returns the resulting VirtualMachine instance.
Returns:the virtual machine
/** * Returns the resulting {@code VirtualMachine} instance. * * @return the virtual machine */
public VirtualMachine vm() { return vm; }
Returns the launched process.
Returns:the remote agent process
/** * 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.
/** * 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; // Files to collection to output of a start-up failure File crashErrorFile = createTempFile("error"); File crashOutputFile = createTempFile("output"); try { // Start listening, get the JDI connection address String addr = listener.startListening(connectorArgs); debug("Listening at address: " + addr); // 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=" + addr); args.addAll(remoteVMOptions); args.add(remoteAgent); args.add("" + port); ProcessBuilder pb = new ProcessBuilder(args); pb.redirectError(crashErrorFile); pb.redirectOutput(crashOutputFile); process = pb.start(); // Accept the connection from the remote agent vm = timedVirtualMachineCreation(() -> listener.accept(connectorArgs), () -> process.waitFor()); try { listener.stopListening(connectorArgs); } catch (IOException | IllegalConnectorArgumentsException ex) { // ignore } crashErrorFile.delete(); crashOutputFile.delete(); return vm; } catch (Throwable ex) { if (process != null) { process.destroyForcibly(); } try { listener.stopListening(connectorArgs); } catch (IOException | IllegalConnectorArgumentsException iex) { // ignore } 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.
Params:
  • portthe – socket port for (non-JDI) commands
  • remoteVMOptions – any user requested VM options
Returns:the argument map
/** * 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 printf.
Params:
  • format – a format string as described in Format string syntax
  • args – arguments referenced by the format specifiers in the format string.
/** * 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.
Params:
  • ex – the exception
  • where – a description of the context of the exception
/** * 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 } }