/*******************************************************************************
* Copyright (c) 2012, 2013 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.grails.ide.eclipse.core.launch;
import static org.grails.ide.eclipse.core.launch.LaunchListenerManager.getLaunchListener;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IDebugEventSetListener;
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.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.launching.AbstractJavaLaunchConfigurationDelegate;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.jdt.launching.IVMConnector;
import org.eclipse.jdt.launching.IVMInstall;
import org.eclipse.jdt.launching.IVMRunner;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.launching.VMRunnerConfiguration;
import org.eclipse.osgi.util.NLS;
import org.grails.ide.eclipse.core.GrailsCoreActivator;
import org.grails.ide.eclipse.core.model.GrailsBuildSettingsHelper;
import org.grails.ide.eclipse.core.model.GrailsVersion;
import org.grails.ide.eclipse.core.model.IGrailsInstall;
import org.grails.ide.eclipse.core.util.PortFinder;
import org.grails.ide.eclipse.core.workspace.GrailsWorkspace;
import org.grails.ide.eclipse.runtime.shared.DependencyData;
import org.springsource.ide.eclipse.commons.core.HttpUtil;
/**
* @author Christian Dupuis
* @since 2.2.0
*/
public class GrailsLaunchConfigurationDelegate extends AbstractJavaLaunchConfigurationDelegate {
private static final String SCRIPT_ATTR = GrailsCoreActivator.PLUGIN_ID + ".SCRIPT";
private static final String ORG_SCRIPT_ATTR = GrailsCoreActivator.PLUGIN_ID + ".ORG_SCRIPT";
private PortFinder portFinder = new PortFinder();
static {
LaunchListenerManager
.promiseSupportForType(getLaunchConfigurationTypeId());
}
@Override
public boolean preLaunchCheck(ILaunchConfiguration conf,
String mode, IProgressMonitor monitor) throws CoreException {
IGrailsInstall install = GrailsLaunchArgumentUtils.getGrailsInstall(conf);
IStatus status = install.verify();
if (!status.isOK()) {
throw new CoreException(status);
}
return super.preLaunchCheck(conf, mode, monitor);
}
@Override
public IVMInstall verifyVMInstall(ILaunchConfiguration conf) throws CoreException {
IVMInstall javaInstall = super.verifyVMInstall(conf);
IGrailsInstall grailsInstall = GrailsLaunchArgumentUtils.getGrailsInstall(conf);
grailsInstall.verifyJavaInstall(javaInstall);
return javaInstall;
}
/**
* Create debugging target similar to a remote debugging session would and add them to the launch.
* This is to support debugging of 'forked mode' run-app and test-app processes. These are
* processes spun-off by Grails in new JVM.
* @param port the remote launch will be listening on for forked process to connect to.
*/
private void launchRemote(int port, ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor) throws CoreException {
if (port<0) {
return;
}
if (monitor == null) {
monitor = new NullProgressMonitor();
}
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);
IVMConnector connector = JavaRuntime.getVMConnector(connectorId);
if (connector == null) {
abort(LaunchingMessages.JavaRemoteApplicationLaunchConfigurationDelegate_Connector_not_specified_2, null, IJavaLaunchConfigurationConstants.ERR_CONNECTOR_NOT_AVAILABLE);
}
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("timeout", "120000"); // Give grails run-app command enough time to build the app and kick off a forked process.
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
connector.connect(argMap, monitor, launch);
// check for cancellation
if (monitor.isCanceled()) {
IDebugTarget[] debugTargets = launch.getDebugTargets();
for (int i = 0; i < debugTargets.length; i++) {
IDebugTarget target = debugTargets[i];
if (target.canDisconnect()) {
target.disconnect();
}
}
return;
}
}
finally {
monitor.done();
}
}
@SuppressWarnings("unchecked")
public void launch(ILaunchConfiguration configuration, String mode, ILaunch launch, IProgressMonitor monitor)
throws CoreException {
try {
GrailsVersion version = GrailsLaunchArgumentUtils.getGrailsVersion(configuration);
IProgressMonitor subMonitor = new SubProgressMonitor(monitor, 5);
checkCancelled(subMonitor);
subMonitor.beginTask("Starting Grails", 5);
subMonitor.worked(1);
checkCancelled(subMonitor);
subMonitor.subTask("Configuring launch parameters...");
// FIXKDV FIXADE Copies of this code exist in
// GrailsLaunchArgumentUtils.prepareClasspath()
// and GrailsLaunchConfigurationDelegate.launch()
// consider refactoring to combine
IVMRunner runner;
IVMInstall vm = verifyVMInstall(configuration);
if (GrailsVersion.V_2_3_.compareTo(version) <= 0) {
//We'll be debugging the forked process, not the run-app command.
runner = vm.getVMRunner(ILaunchManager.RUN_MODE);
} else {
runner = vm.getVMRunner(mode);
}
if (runner == null) {
runner = vm.getVMRunner(ILaunchManager.RUN_MODE);
}
String projectName = configuration.getAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, "");
IProject project = null;
if (!"".equals(projectName)) {
project = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);
}
String grailsHome = GrailsLaunchArgumentUtils.getGrailsHome(configuration);
String baseDir = configuration.getAttribute(GrailsLaunchArgumentUtils.PROJECT_DIR_LAUNCH_ATTR, "");
if (baseDir.equals("")) {
baseDir = ResourcesPlugin.getWorkspace().getRoot().getLocation().toString();
}
String script = getScript(configuration);
File workingDir = verifyWorkingDirectory(configuration);
String workingDirName = null;
if (workingDir != null) {
workingDirName = workingDir.getAbsolutePath();
}
else {
workingDirName = baseDir;
}
List<String> programArguments = new ArrayList<String>();
programArguments.add("--conf");
programArguments.add(grailsHome + "conf" + File.separatorChar + "groovy-starter.conf");
programArguments.add("--main");
programArguments.add("org.codehaus.groovy.grails.cli.GrailsScriptRunner");
StringBuilder grailsCommand = new StringBuilder();
String grailsWorkDir = configuration.getAttribute(
GrailsLaunchArgumentUtils.GRAILS_WORK_DIR_LAUNCH_ATTR, "");
if (!grailsWorkDir.equals("")) {
grailsCommand.append("-Dgrails.work.dir=" + grailsWorkDir+" ");
}
grailsCommand.append(script+" ");
programArguments.add(grailsCommand.toString().trim());
List<String> vmArgs = new ArrayList<String>();
//vmArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8123");
// add manual configured vm options to the argument list
String existingVmArgs = getVMArguments(configuration);
boolean launchConfHasVMArgs = false;
if (existingVmArgs != null && existingVmArgs.length() > 0) {
launchConfHasVMArgs = true;
StringTokenizer additionalArguments = new StringTokenizer(existingVmArgs, " ");
while (additionalArguments.hasMoreTokens()) {
vmArgs.add(additionalArguments.nextToken());
}
}
Map<String, String> systemProps = GrailsCoreActivator.getDefault().getLaunchSystemProperties();
GrailsLaunchArgumentUtils.setMaybe(systemProps, "base.dir", baseDir);
GrailsLaunchArgumentUtils.setMaybe(systemProps, "grails.home", grailsHome);
for (Map.Entry<String, String> prop : systemProps.entrySet()) {
vmArgs.add("-D"+prop.getKey()+"="+prop.getValue());
}
int forkedProcessDebugPort = addForkedModeDebuggingArgs(configuration, mode, vmArgs);
ArrayList<Integer> killPorts = addKillPortArg(project, vmArgs);
if (!launchConfHasVMArgs) {
//If the user added their own vmargs to the launch config then the 'default' from global prefs should
// not be used.
GrailsLaunchArgumentUtils.addUserDefinedJVMArgs(vmArgs);
}
// Grails uses some default memory settings that we want to use as well if no others have been configured
vmArgs = GrailsLaunchArgumentUtils.addMemorySettings(vmArgs);
vmArgs = GrailsLaunchArgumentUtils.addSpringLoadedArgs(configuration, vmArgs);
String[] envp = getEnvironment(configuration);
Map<String, String> extra = new HashMap<String, String>();
extra.put("JAVA_HOME", vm.getInstallLocation().getAbsolutePath());
extra.put("GROOVY_PAGE_ADD_LINE_NUMBERS", "true"); // Enables line number info for GSP debugging
envp = GrailsLaunchArgumentUtils.addToEnvMaybe(envp, extra);
Map<String, Object> vmAttributesMap = getVMSpecificAttributesMap(configuration);
String[] classpath = getClasspath(configuration);
String mainTypeName = verifyMainTypeName(configuration);
VMRunnerConfiguration runConfiguration = new VMRunnerConfiguration(mainTypeName, classpath);
runConfiguration.setProgramArguments(programArguments.toArray(new String[programArguments.size()]));
runConfiguration.setVMArguments(vmArgs.toArray(new String[vmArgs.size()]));
runConfiguration.setWorkingDirectory(workingDirName);
runConfiguration.setEnvironment(envp);
runConfiguration.setVMSpecificAttributesMap(vmAttributesMap);
String[] bootpath = getBootpath(configuration);
if (bootpath != null && bootpath.length > 0) {
runConfiguration.setBootClassPath(bootpath);
}
subMonitor.worked(1);
checkCancelled(subMonitor);
subMonitor.subTask("Setting up source locator...");
setDefaultSourceLocator(launch, configuration);
subMonitor.worked(1);
checkCancelled(subMonitor);
subMonitor.worked(1);
checkCancelled(subMonitor);
subMonitor.subTask("Launching Grails...");
if (ILaunchManager.DEBUG_MODE.equals(mode)) {
launchRemote(forkedProcessDebugPort, configuration, mode, launch, subMonitor);
}
GrailsCoreActivator.getDefault().notifyCommandStart(project);
runner.run(runConfiguration, launch, monitor);
AbstractLaunchProcessListener listener = getLaunchListener(configuration);
if (listener != null) {
listener.init(launch.getProcesses()[0]);
}
DebugPlugin.getDefault().addDebugEventListener(new GrailsProcessListener(launch.getProcesses(), project, killPorts));
subMonitor.worked(1);
}
catch (Exception e) {
GrailsCoreActivator.log(e);
}
}
/**
* Add a system property arg to set killport for Grails 2.3 and higher.
* @param project
* @return An array of kill ports to try in order to ask Grails forked process to terminate.
*/
private ArrayList<Integer> addKillPortArg(IProject project, List<String> vmArgs) {
ArrayList<Integer> ports = null;
if (project!=null) {
if (GrailsVersion.V_2_3_.compareTo(GrailsVersion.getEclipseGrailsVersion(project))<=0) {
ports = new ArrayList<Integer>(2); //Will have 1 or two elements not more.
int serverPort = GrailsWorkspace.get().create(project).getServerPort();
if (serverPort!=DependencyData.UNKNOWN_PORT) {
ports.add(serverPort+1);
}
//The next bit really only expected to work in in Grails 2.3
try {
int allocatedKillPort = portFinder.findUniqueFreePort();
vmArgs.add("-Dgrails.forked.kill.port="+allocatedKillPort);
ports.add(allocatedKillPort);
} catch (IOException e) {
//non fatal... log and proceed
GrailsCoreActivator.log(e);
}
}
}
return ports;
}
/**
* Helper function to add system properties that tell Grails to debug run-app or test-app in
* as a forked process in debugging mode.
* <p>
* These properties are added only if applicable. I.e. recent enough graisl version and
* this is a debug-mode launch.
* <p>
* In the process of adding the arguments, we will pick a free port.
*
* @return Chosen debug port or -1 if not launching in forked debug mode.
*/
private int addForkedModeDebuggingArgs(ILaunchConfiguration conf,
String mode, List<String> args) throws IOException {
// TODO Auto-generated method stub
if (!ILaunchManager.DEBUG_MODE.equals(mode)) {
return -1;
}
GrailsVersion version = GrailsLaunchArgumentUtils.getGrailsVersion(conf);
if (GrailsVersion.V_2_3_.compareTo(version)>0) {
return -1; //Disable this feature for pre Grails 2.3.
}
int debugPort = portFinder.findUniqueFreePort();
args.add("-Dgrails.project.fork.run.debug=true");
args.add("-Dgrails.project.fork.test.debug=true");
String debugArgs = "-Xrunjdwp:transport=dt_socket,server=n,suspend=y,address=" + debugPort;
args.add("-Dgrails.project.fork.run.debugArgs="+debugArgs);
args.add("-Dgrails.project.fork.test.debugArgs="+debugArgs);
return debugPort;
}
protected void checkCancelled(IProgressMonitor monitor) throws CoreException {
if (monitor.isCanceled()) {
throw new CoreException(Status.CANCEL_STATUS);
}
}
public static String getScript(ILaunchConfiguration configuration) throws CoreException {
return configuration.getAttribute(
SCRIPT_ATTR, "run-app");
}
/**
* Gets the 'original' value of the script attribute. Note that this can't be set directly but it is set
* as a side effect of setting the script attribute.
*/
public static String getOrgScript(ILaunchConfiguration configuration) throws CoreException {
String value = configuration.getAttribute(ORG_SCRIPT_ATTR, (String)null);
if (value==null) {
//For legacy configurations that don't have ORG_SCRIPT_ATTR just use the regular script attribute
value = getScript(configuration);
}
return value;
}
public static void setScript(ILaunchConfigurationWorkingCopy wc,
String script) {
wc.setAttribute(SCRIPT_ATTR, script);
try {
String orgScript = wc.getAttribute(ORG_SCRIPT_ATTR, (String)null);
if (orgScript==null) {
//org sript should be set same value as script, but only the first time that script value is set.
wc.setAttribute(ORG_SCRIPT_ATTR, script);
}
} catch (CoreException e) {
GrailsCoreActivator.log(e);
}
}
private class GrailsProcessListener implements IDebugEventSetListener {
private final IProject project;
private List<Integer> killPorts;
private IProcess[] processes; //Can be 1 or 2 processes. If debugging forked mode grails one is the parent process the other the child.
public GrailsProcessListener(IProcess[] processes, IProject project, ArrayList<Integer> killPorts) {
this.project = project;
this.processes = processes;
this.killPorts = killPorts;
}
public void handleDebugEvents(DebugEvent[] events) {
if (events != null && project != null) {
int size = events.length;
for (int i = 0; i < size; i++) {
for (IProcess process : processes) {
if (process != null && process.equals(events[i].getSource())
&& events[i].getKind() == DebugEvent.TERMINATE) {
DebugPlugin.getDefault().removeDebugEventListener(this);
terminateForked();
Job job = new Job("refresh project") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
}
catch (CoreException e) {
}
GrailsCoreActivator.getDefault().notifyCommandFinish(project);
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.setRule(ResourcesPlugin.getWorkspace().getRuleFactory().buildRule());
job.setPriority(Job.INTERACTIVE);
job.schedule();
}
}
}
}
}
private void terminateForked() {
if (killPorts!=null) {
for (int killPort : killPorts) {
try {
URI killUrl = new URI("http://localhost:"+killPort);
HttpUtil.ping(killUrl);
} catch (Throwable e) {
}
}
}
if (processes!=null && processes.length>1) {
//Make sure all processes are terminated
for (IProcess process : processes) {
try {
if (process.canTerminate() && !process.isTerminated()) {
process.terminate();
}
} catch (Throwable e) {
GrailsCoreActivator.log(e);
}
}
processes = null;
}
}
}
public static ILaunchConfiguration getLaunchConfiguration(IProject project, String script, boolean persist) throws CoreException {
ILaunchConfigurationType configType = getLaunchConfigurationType();
IGrailsInstall install = GrailsCoreActivator.getDefault().getInstallManager().getGrailsInstall(project);
if (install == null) {
return null;
}
String nameAndScript = (script != null ? "(" + script + ")" : "");
if (project!=null) {
nameAndScript = project.getName()+" "+nameAndScript;
}
nameAndScript = sanitize(nameAndScript);
ILaunchConfigurationWorkingCopy wc = configType.newInstance(null, nameAndScript);
GrailsLaunchArgumentUtils.prepareLaunchConfiguration(project, script, install, GrailsBuildSettingsHelper.getBaseDir(project), wc);
if (persist) {
return wc.doSave();
} else {
return wc;
}
}
public static ILaunchConfigurationType getLaunchConfigurationType() {
return DebugPlugin.getDefault().getLaunchManager().getLaunchConfigurationType(
getLaunchConfigurationTypeId());
}
public static String sanitize(String name) {
final char[] DISALLOWED_CONFIG_NAME_CHARS = new char[] { '@', '&','\\', '/', ':', '*', '?', '"', '<', '>', '|', '\0' };
// The disallowed list of chars is copied from
// org.eclipse.debug.internal.core.LaunchManager
// Have to copy it here because it isn't public
for (char c : DISALLOWED_CONFIG_NAME_CHARS) {
name = name.replace(c, ' ');
}
return name;
}
private static String getLaunchConfigurationTypeId() {
return "org.grails.ide.eclipse.core.launchconfig";
}
}