/******************************************************************************* * Copyright (c) 2015 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Pivotal, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.eclipse.boot.launch.devtools; import java.io.IOException; import java.net.ServerSocket; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; import org.eclipse.debug.core.model.IDebugTarget; import org.eclipse.debug.core.model.IProcess; import org.eclipse.jdt.internal.launching.LaunchingMessages; import org.eclipse.jdt.internal.launching.LaunchingPlugin; import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants; import org.eclipse.jdt.launching.IVMConnector; import org.eclipse.jdt.launching.JavaRuntime; import org.eclipse.osgi.util.NLS; import org.springframework.ide.eclipse.boot.core.BootActivator; import org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate; import org.springframework.ide.eclipse.boot.launch.util.WaitFor; import org.springframework.ide.eclipse.boot.util.ProcessListenerAdapter; import org.springframework.ide.eclipse.boot.util.ProcessTracker; import org.springframework.ide.eclipse.editor.support.util.StringUtil; import org.springsource.ide.eclipse.commons.frameworks.core.ExceptionUtil; @SuppressWarnings("restriction") public class BootDevtoolsClientLaunchConfigurationDelegate extends AbstractBootLaunchConfigurationDelegate { public static final String TYPE_ID = "org.springframework.ide.eclipse.boot.devtools.client.launch"; private static final long DEBUG_CONNECT_TIMEOUT = 20000; public static final String REMOTE_SPRING_APPLICATION = "org.springframework.boot.devtools.RemoteSpringApplication"; public static final String REMOTE_URL = "spring.devtools.remote.url"; public static final String REMOTE_SECRET = "spring.devtools.remote.secret"; public static final String DEFAULT_REMOTE_SECRET = ""; public static final String DEBUG_PORT = "spring.devtools.remote.debug.local-port"; private final ThreadLocal<Integer> localDebugPort = new ThreadLocal<Integer>(); @Override public String getMainTypeName(ILaunchConfiguration configuration) throws CoreException { return REMOTE_SPRING_APPLICATION; } @Override public String getProgramArguments(ILaunchConfiguration conf) throws CoreException { List<PropVal> props = getProperties(conf); ArrayList<String> args = new ArrayList<String>(); addPropertiesArguments(args, props); String secret = getSecret(conf); if (StringUtil.hasText(secret)) { args.add(propertyAssignmentArgument(REMOTE_SECRET, secret)); } Integer debugPort = localDebugPort.get(); if (debugPort!=null) { args.add(propertyAssignmentArgument(DEBUG_PORT, ""+debugPort)); } args.add(getRemoteUrl(conf)); return DebugPlugin.renderArguments(args.toArray(new String[args.size()]), null); } private String getSecret(ILaunchConfiguration conf) { try { return conf.getAttribute(REMOTE_SECRET, DEFAULT_REMOTE_SECRET); } catch (CoreException e) { BootActivator.log(e); } return ""; } @Override public void launch(ILaunchConfiguration conf, String mode, ILaunch launch, IProgressMonitor mon) throws CoreException { boolean isDebug = ILaunchManager.DEBUG_MODE.equals(mode); int work = isDebug ? 2 : 1; mon.beginTask("Launching Devtools Client for"+getProjectName(conf), work); if (isDebug) { localDebugPort.set(findFreePort()); } try { //Launch client: Generally we don't wanna debug the client itself so always use 'RUN_MODE' super.launch(conf, ILaunchManager.RUN_MODE, launch, new SubProgressMonitor(mon, 1)); if (isDebug) { //TODO: set debug port in config (or chosen dynamically?) launchRemote(localDebugPort.get(), conf, launch, new SubProgressMonitor(mon, 1)); } } finally { localDebugPort.remove(); mon.done(); } } /** * Returns a free port number on localhost, or -1 if unable to find a free port. * * @return a free port number on localhost, or -1 if unable to find a free port */ public static int findFreePort() { try (ServerSocket socket = new ServerSocket(0)) { socket.setReuseAddress(true); return socket.getLocalPort(); } catch (IOException e) { } return -1; } public static String getRemoteUrl(ILaunchConfiguration conf) { try { return conf.getAttribute(REMOTE_URL, (String)null); } catch (CoreException e) { BootActivator.log(e); } return null; } public static void setRemoteUrl(ILaunchConfigurationWorkingCopy conf, String value) { conf.setAttribute(REMOTE_URL, value); } public static void setRemoteSecret(ILaunchConfigurationWorkingCopy conf, String value) { conf.setAttribute(REMOTE_SECRET, value); } public static String getRemoteSecret(ILaunchConfigurationWorkingCopy conf) { try { return conf.getAttribute(REMOTE_SECRET, (String)null); } catch (CoreException e) { BootActivator.log(e); } return null; } /** * Create debugging target similar to a remote debugging session would and add them to the launch. * This is to support debugging of the remote boot-app that is reachable over http tunnel * the client creates. From our side this just as if we are opening a remote debug * session to the client. */ private void launchRemote(int port, ILaunchConfiguration configuration, final ILaunch launch, IProgressMonitor _monitor) throws CoreException { if (port<0) { return; } final IProgressMonitor monitor = _monitor==null?new NullProgressMonitor():_monitor; monitor.beginTask(NLS.bind(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Attaching_to__0_____1, new String[]{configuration.getName()}), 3); // check for cancellation if (monitor.isCanceled()) { return; } try { monitor.subTask(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Verifying_launch_attributes____1); //String connectorId = "org.eclipse.jdt.launching.socketListenConnector";//getVMConnectorId(configuration); String connectorId = "org.eclipse.jdt.launching.socketAttachConnector"; final IVMConnector connector = JavaRuntime.getVMConnector(connectorId); if (connector == null) { abort(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Connector_not_specified_2, null, IJavaLaunchConfigurationConstants.ERR_CONNECTOR_NOT_AVAILABLE); } final Map<String, String> argMap = new HashMap<String, String>(); int connectTimeout = Platform.getPreferencesService().getInt( LaunchingPlugin.ID_PLUGIN, JavaRuntime.PREF_CONNECT_TIMEOUT, JavaRuntime.DEF_CONNECT_TIMEOUT, null); argMap.put("hostname", "localhost"); argMap.put("timeout", ""+connectTimeout); argMap.put("port", ""+port); // check for cancellation if (monitor.isCanceled()) { return; } monitor.worked(1); //Don't think we need to set source location since the main launch method already does this. // monitor.subTask(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Creating_source_locator____2); // // set the default source locator if required // setDefaultSourceLocator(launch, configuration); // monitor.worked(1); // connect to remote VM try { new WaitFor(DEBUG_CONNECT_TIMEOUT) { public void run() throws Exception { connector.connect(argMap, monitor, launch); } }; new ProcessTracker(new ProcessListenerAdapter() { public void debugTargetTerminated(ProcessTracker tracker, IDebugTarget target) { handleTermination(tracker, target.getLaunch()); } public void processTerminated(ProcessTracker tracker, IProcess process) { handleTermination(tracker, process.getLaunch()); } private void handleTermination(ProcessTracker tracker, ILaunch targetLaunch) { if (launch.equals(targetLaunch)) { tracker.dispose(); terminateAllTargets(launch); } } }); } catch (Exception e) { terminateAllTargets(launch); throw ExceptionUtil.coreException(e); } // check for cancellation if (monitor.isCanceled()) { terminateAllTargets(launch); return; } } finally { monitor.done(); } } public void terminateAllTargets(final ILaunch launch) { //Note: its better to discconect debugtargets before terminating processes // because that allows a cleaner disconnect from the debugged process. // (If the devtools client process is terminated its no longer possible to talk to the // debugged process). IDebugTarget[] debugTargets = launch.getDebugTargets(); for (int i = 0; i < debugTargets.length; i++) { IDebugTarget target = debugTargets[i]; if (target.canDisconnect()) { try { target.disconnect(); } catch (Exception e) { BootActivator.log(e); } } } IProcess[] processes = launch.getProcesses(); for (IProcess process : processes) { if (process.canTerminate()) { try { process.terminate(); } catch (Exception e) { BootActivator.log(e); } } } } }