/*******************************************************************************
* Copyright (c) 2015, 2017 Pivotal Software, 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 Software, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.launch;
import static org.eclipse.debug.core.DebugPlugin.ATTR_PROCESS_FACTORY_ID;
import static org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants.ATTR_VM_ARGUMENTS;
import static org.springframework.ide.eclipse.editor.support.util.StringUtil.hasText;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.osgi.framework.Bundle;
import org.springframework.ide.eclipse.boot.core.BootPropertyTester;
import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport;
import org.springframework.ide.eclipse.boot.launch.livebean.JmxBeanSupport.Feature;
import org.springframework.ide.eclipse.boot.launch.process.BootProcessFactory;
import org.springframework.ide.eclipse.boot.launch.profiles.ProfileHistory;
import org.springframework.ide.eclipse.boot.launch.util.PortFinder;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.ide.eclipse.editor.support.util.StringUtil;
import org.springsource.ide.eclipse.commons.core.util.OsUtils;
/**
* @author Kris De Volder
*/
public class BootLaunchConfigurationDelegate extends AbstractBootLaunchConfigurationDelegate {
private static DeletedLaunchConfTerminator deletedLaunchConfTerminator = null;
public synchronized static void ensureDeletedLaunchConfTerminator() {
if (deletedLaunchConfTerminator==null) {
deletedLaunchConfTerminator = new DeletedLaunchConfTerminator(DebugPlugin.getDefault().getLaunchManager(), (ILaunch l) -> {
try {
return l!=null && Boolean.valueOf(l.getAttribute(BOOT_LAUNCH_MARKER));
} catch (Exception e) {
Log.log(e);
return false;
}
});
}
}
// private static final boolean DEBUG = (""+Platform.getLocation()).contains("kdvolder");
// private static void debug(String string) {
// if (DEBUG) {
// System.out.println(string);
// }
// }
public static final String TYPE_ID = "org.springframework.ide.eclipse.boot.launch";
/**
* Launch attribute that helps recognize a launch as a boot launch even after the launch configuration has
* been deleted.
*/
public static final String BOOT_LAUNCH_MARKER = "isBootLaunch";
public static final String ENABLE_LIVE_BEAN_SUPPORT = "spring.boot.livebean.enable";
public static final boolean DEFAULT_ENABLE_LIVE_BEAN_SUPPORT = true;
public static final String ENABLE_JMX = "spring.boot.jmx.enable";
public static final boolean DEFAULT_ENABLE_JMX = true;
public static final String JMX_PORT = "spring.boot.livebean.port";
public static final int DEFAULT_JMX_PORT = 0; //means pick it dynamically
public static final String ANSI_CONSOLE_OUTPUT = "spring.boot.ansi.console";
private static final String PROFILE = "spring.boot.profile";
public static final String DEFAULT_PROFILE = "";
public static final String ENABLE_LIFE_CYCLE = "spring.boot.lifecycle.enable";
public static final boolean DEFAULT_ENABLE_LIFE_CYCLE = true;
public static final String HIDE_FROM_BOOT_DASH = "spring.boot.dash.hidden";
public static final boolean DEFAULT_HIDE_FROM_BOOT_DASH = false;
private static final String ENABLE_CHEAP_ENTROPY_VM_ARGS = "-Djava.security.egd=file:/dev/./urandom ";
private static final String TERMINATION_TIMEOUT = "spring.boot.lifecycle.termination.timeout";
public static final long DEFAULT_TERMINATION_TIMEOUT = 15000; // 15 seconds
private ProfileHistory profileHistory = new ProfileHistory();
/**
* Use threadlocal to gain access to current launch in some of the methods
* (i.e. getVMArguments in particular) of the {@link AbstractBootLaunchConfigurationDelegate}
* framework that, unfortunately don't pass it along as parameters. It's either this, or copy
* a whole bunch of inherited code just so we can modify it to add an extra argument.
*/
private static final ThreadLocal<ILaunch> CURRENT_LAUNCH = new ThreadLocal<>();
@Override
public void launch(ILaunchConfiguration conf, String mode,
ILaunch launch, IProgressMonitor monitor) throws CoreException {
ensureDeletedLaunchConfTerminator();
launch.setAttribute(BOOT_LAUNCH_MARKER, "true");
CURRENT_LAUNCH.set(launch);
try {
profileHistory.updateHistory(getProject(conf), getProfile(conf));
super.launch(conf, mode, launch, monitor);
} finally {
CURRENT_LAUNCH.remove();
}
}
@Override
public String getProgramArguments(ILaunchConfiguration conf) throws CoreException {
List<PropVal> props = getProperties(conf);
String profile = getProfile(conf);
boolean debugOutput = getEnableDebugOutput(conf);
boolean enableAnsiConsole = supportsAnsiConsoleOutput() && getEnableAnsiConsoleOutput(conf);
if ((props==null || props.isEmpty()) && !debugOutput && !hasText(profile) && !enableAnsiConsole) {
//shortcut for case where no boot-specific customizations are specified.
return super.getProgramArguments(conf);
}
ArrayList<String> args = new ArrayList<>();
if (debugOutput) {
args.add("--debug");
}
if (hasText(profile)) {
args.add(propertyAssignmentArgument("spring.profiles.active", profile));
}
if (enableAnsiConsole) {
args.add(propertyAssignmentArgument("spring.output.ansi.enabled", "always"));
}
addPropertiesArguments(args, props);
args.addAll(Arrays.asList(DebugPlugin.parseArguments(super.getProgramArguments(conf))));
return DebugPlugin.renderArguments(args.toArray(new String[args.size()]), null);
}
@Override
public String getVMArguments(ILaunchConfiguration conf)
throws CoreException {
try {
String vmArgs = super.getVMArguments(conf);
EnumSet<JmxBeanSupport.Feature> enabled = getEnabledJmxFeatures(conf);
if (!enabled.isEmpty()) {
int port = 0;
try {
port = Integer.parseInt(getJMXPort(conf));
} catch (Exception e) {
//ignore: bad data in launch config.
}
if (port==0) {
port = PortFinder.findFreePort(); //slightly better than calling JmxBeanSupport.randomPort()
}
String enableLiveBeanArgs = JmxBeanSupport.jmxBeanVmArgs(port, enabled);
vmArgs = enableLiveBeanArgs + vmArgs;
CURRENT_LAUNCH.get().setAttribute(JMX_PORT, ""+port);
}
return vmArgs;
} catch (Exception e) {
Log.log(e);
}
return super.getVMArguments(conf);
}
public static EnumSet<Feature> getEnabledJmxFeatures(ILaunchConfiguration conf) {
EnumSet<Feature> enabled = EnumSet.noneOf(Feature.class);
if (getEnableJmx(conf)) {
enabled.add(Feature.JMX);
}
if (getEnableLiveBeanSupport(conf)) {
enabled.add(Feature.LIVE_BEAN_GRAPH);
}
if (getEnableLifeCycle(conf)) {
enabled.add(Feature.LIFE_CYCLE);
}
return enabled;
}
public static boolean isHiddenFromBootDash(ILaunchConfiguration conf) {
try {
return conf.getAttribute(HIDE_FROM_BOOT_DASH, DEFAULT_HIDE_FROM_BOOT_DASH);
} catch (CoreException e) {
Log.log(e);
}
return DEFAULT_HIDE_FROM_BOOT_DASH;
}
public static void setHiddenFromBootDash(ILaunchConfigurationWorkingCopy conf, boolean hide) {
conf.setAttribute(HIDE_FROM_BOOT_DASH, hide);
}
/**
* Retrieve the 'Enable Life Cycle Tracking' option from the config. Note that
* this doesn't necesarily mean that this feature is effectively enabled as
* it is only supported on recent enough versions of Boot.
* <p>
* See also the 'supportsLifeCycleManagement' method.
*/
public static boolean getEnableLifeCycle(ILaunchConfiguration conf) {
try {
return conf.getAttribute(ENABLE_LIFE_CYCLE, DEFAULT_ENABLE_LIFE_CYCLE);
} catch (Exception e) {
Log.log(e);
}
return DEFAULT_ENABLE_LIFE_CYCLE;
}
public static void setEnableJMX(ILaunchConfigurationWorkingCopy wc, boolean enable) {
wc.setAttribute(ENABLE_JMX, enable);
}
public static void setEnableLifeCycle(ILaunchConfigurationWorkingCopy wc, boolean enable) {
wc.setAttribute(ENABLE_LIFE_CYCLE, enable);
}
public static boolean canUseLifeCycle(ILaunchConfiguration conf) {
return BootLaunchConfigurationDelegate.getEnableLifeCycle(conf)
&& BootLaunchConfigurationDelegate.supportsLifeCycleManagement(conf);
}
public static boolean canUseLifeCycle(ILaunch launch) {
ILaunchConfiguration conf = launch.getLaunchConfiguration();
return conf!=null && canUseLifeCycle(conf);
}
public static boolean supportsLifeCycleManagement(ILaunchConfiguration conf) {
IProject p = getProject(conf);
if (p!=null) {
return BootPropertyTester.supportsLifeCycleManagement(p);
}
return false;
}
/**
* Sets minimal default values to create a runnable launch configuration.
*/
public static void setDefaults(ILaunchConfigurationWorkingCopy wc,
IProject project,
String mainType
) {
setProcessFactory(wc, BootProcessFactory.class);
setProject(wc, project);
if (mainType!=null) {
setMainType(wc, mainType);
}
setEnableJMX(wc, DEFAULT_ENABLE_JMX);
setEnableLiveBeanSupport(wc, DEFAULT_ENABLE_LIVE_BEAN_SUPPORT);
setEnableLifeCycle(wc, DEFAULT_ENABLE_LIFE_CYCLE);
setTerminationTimeout(wc,""+DEFAULT_TERMINATION_TIMEOUT);
setJMXPort(wc, ""+DEFAULT_JMX_PORT);
if (!OsUtils.isWindows()) {
setVMArgs(wc, ENABLE_CHEAP_ENTROPY_VM_ARGS);
}
}
public static void setTerminationTimeout(ILaunchConfigurationWorkingCopy wc, String value) {
wc.setAttribute(TERMINATION_TIMEOUT, ""+value);
}
public static String getTerminationTimeout(ILaunchConfiguration conf) {
try {
return conf.getAttribute(TERMINATION_TIMEOUT, ""+DEFAULT_TERMINATION_TIMEOUT);
} catch (Exception e) {
Log.log(e);
return ""+DEFAULT_TERMINATION_TIMEOUT;
}
}
public static long getTerminationTimeoutAsLong(ILaunchConfiguration conf) {
String v = getTerminationTimeout(conf);
if (StringUtil.hasText(v)) {
try {
return Long.parseLong(v);
} catch (Exception e) {
Log.log(e);
}
}
return DEFAULT_TERMINATION_TIMEOUT;
}
private static void setVMArgs(ILaunchConfigurationWorkingCopy wc, String vmArgs) {
wc.setAttribute(ATTR_VM_ARGUMENTS, vmArgs);
}
/**
* Notes:
* <p>
* 1. we are assuming that the processFactoryId is the same as the classname of
* the class that implements it. This is not a given, but a convenient and logical convention.
* <p>
* 2. The class must be registered to this ID using plugin.xml (extension point
* org.eclipse.debug.core.processFactories)
*/
public static void setProcessFactory(ILaunchConfigurationWorkingCopy wc, Class<BootProcessFactory> klass) {
wc.setAttribute(ATTR_PROCESS_FACTORY_ID, klass.getName());
}
public static boolean getEnableJmx(ILaunchConfiguration conf) {
try {
return conf.getAttribute(ENABLE_JMX, DEFAULT_ENABLE_JMX);
} catch (Exception e) {
Log.log(e);
}
return DEFAULT_ENABLE_JMX;
}
public static boolean getEnableLiveBeanSupport(ILaunchConfiguration conf) {
try {
return conf.getAttribute(ENABLE_LIVE_BEAN_SUPPORT, DEFAULT_ENABLE_LIVE_BEAN_SUPPORT);
} catch (Exception e) {
Log.log(e);
}
return DEFAULT_ENABLE_LIVE_BEAN_SUPPORT;
}
public static String getJMXPort(ILaunchConfiguration conf) {
try {
return conf.getAttribute(JMX_PORT, "");
} catch (CoreException e) {
Log.log(e);
}
return "";
}
public static void setEnableLiveBeanSupport(ILaunchConfigurationWorkingCopy conf, boolean value) {
conf.setAttribute(ENABLE_LIVE_BEAN_SUPPORT, value);
}
public static void setJMXPort(ILaunchConfigurationWorkingCopy conf, String portAsStr) {
conf.setAttribute(JMX_PORT, portAsStr);
}
public static String getProfile(ILaunchConfiguration conf) {
try {
return conf.getAttribute(PROFILE, DEFAULT_PROFILE);
} catch (CoreException e) {
Log.log(e);
return DEFAULT_PROFILE;
}
}
public static void setProfile(ILaunchConfigurationWorkingCopy conf, String profile) {
conf.setAttribute(PROFILE, profile);
}
public static ILaunchConfiguration duplicate(ILaunchConfiguration conf) throws CoreException {
String newName = DebugPlugin.getDefault().getLaunchManager().generateLaunchConfigurationName(conf.getName());
ILaunchConfigurationWorkingCopy copy = conf.copy(newName);
int existingJmxPort = getJMXPortAsInt(conf);
if (existingJmxPort>0) {
//change port on duplicated config, but only if it was set to a specific port.
setJMXPort(copy, ""+JmxBeanSupport.randomPort());
}
return copy.doSave();
}
public static ILaunchConfigurationWorkingCopy createWorkingCopy(String nameHint) throws CoreException {
String name = getLaunchMan().generateLaunchConfigurationName(nameHint);
return getConfType().newInstance(null, name);
}
public static ILaunchConfigurationType getConfType() {
return getLaunchMan().getLaunchConfigurationType(TYPE_ID);
}
public static ILaunchConfiguration createConf(IType type) throws CoreException {
ILaunchConfigurationWorkingCopy wc = createWorkingCopy(type);
return wc.doSave();
}
public static ILaunchConfigurationWorkingCopy createWorkingCopy(IType type) throws CoreException {
ILaunchConfigurationWorkingCopy wc = null;
ILaunchConfigurationType configType = getConfType();
IProject project = type.getJavaProject().getProject();
String projectName = type.getJavaProject().getElementName();
String shortTypeName = type.getTypeQualifiedName('.');
String typeName = type.getFullyQualifiedName();
wc = configType.newInstance(null, getLaunchMan().generateLaunchConfigurationName(
projectName+" - "+shortTypeName));
BootLaunchConfigurationDelegate.setDefaults(wc, project, typeName);
wc.setMappedResources(new IResource[] {type.getUnderlyingResource()});
return wc;
}
public static ILaunchConfiguration createConf(IProject project) throws CoreException {
return createConf(JavaCore.create(project));
}
public static ILaunchConfiguration createConf(IJavaProject project) throws CoreException {
ILaunchConfigurationWorkingCopy wc = null;
ILaunchConfigurationType configType = getConfType();
String projectName = project.getElementName();
wc = configType.newInstance(null, getLaunchMan().generateLaunchConfigurationName(projectName));
BootLaunchConfigurationDelegate.setDefaults(wc, project.getProject(), null);
wc.setMappedResources(new IResource[] {project.getUnderlyingResource()});
return wc.doSave();
}
public static int getJMXPortAsInt(ILaunchConfiguration conf) {
String jmxPortStr = getJMXPort(conf);
if (jmxPortStr!=null) {
try {
return Integer.parseInt(jmxPortStr);
} catch (Exception e) {
//Ignore
}
}
return -1;
}
public static int getJMXPortAsInt(ILaunch launch) {
String jmxPortStr = launch.getAttribute(JMX_PORT);
if (jmxPortStr!=null) {
try {
return Integer.parseInt(jmxPortStr);
} catch (Exception e) {
//Ignore
}
}
return -1;
}
public static long getTerminationTimeoutAsLong(ILaunch launch) {
ILaunchConfiguration conf = launch.getLaunchConfiguration();
if (conf!=null) {
return BootLaunchConfigurationDelegate.getTerminationTimeoutAsLong(conf);
}
return BootLaunchConfigurationDelegate.DEFAULT_TERMINATION_TIMEOUT;
}
public static boolean supportsAnsiConsoleOutput() {
Bundle bundle = Platform.getBundle("net.mihai-nita.ansicon.plugin");
return bundle != null && bundle.getState() != Bundle.UNINSTALLED;
}
public static boolean getEnableAnsiConsoleOutput(ILaunchConfiguration conf) {
boolean defaultValue = supportsAnsiConsoleOutput();
try {
return conf.getAttribute(ANSI_CONSOLE_OUTPUT, defaultValue);
} catch (CoreException e) {
return defaultValue;
}
}
public static void setEnableAnsiConsoleOutput(ILaunchConfigurationWorkingCopy wc, boolean enable) {
wc.setAttribute(ANSI_CONSOLE_OUTPUT, enable);
}
}