/*******************************************************************************
* 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.dash.cloudfoundry.debug.ssh;
import java.util.HashMap;
import java.util.Map;
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.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.SubProgressMonitor;
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.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.dash.BootDashActivator;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.CloudAppDashElement;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.CloudFoundryBootDashModel;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.CloudFoundryRunTarget;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.SshClientSupport;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.SshHost;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.debug.DebugSupport;
import org.springframework.ide.eclipse.boot.dash.model.BootDashModel;
import org.springframework.ide.eclipse.boot.dash.model.BootDashViewModel;
import org.springframework.ide.eclipse.boot.dash.model.RunTarget;
import org.springframework.ide.eclipse.boot.launch.AbstractBootLaunchConfigurationDelegate;
import org.springframework.ide.eclipse.boot.launch.BootLaunchConfigurationDelegate;
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.util.Assert;
import org.springsource.ide.eclipse.commons.frameworks.core.ExceptionUtil;
import org.springsource.ide.eclipse.commons.frameworks.core.util.IOUtil;
@SuppressWarnings("restriction")
public class SshDebugLaunchConfigurationDelegate extends AbstractBootLaunchConfigurationDelegate {
public static final String TYPE_ID = "org.springframework.ide.eclipse.boot.dash.ssh.tunnel.launch";
private static final long DEBUG_CONNECT_TIMEOUT = 20000;
public static final String RUN_TARGET = "ssh.debug.runtarget.id";
public static final String APP_NAME = "ssh.debug.app.name";
public static final String INSTANCE_IDX = "ssh.debug.app.instance";
private static BootDashViewModel getContext() {
//TODO: it may be necessary to allow injecting this, for example via setting a threadlocal,
// to make code more amenable to unit testing.
//This method is here because LaunchConf delegates are created by eclipse debug framework and
// so we can't easily inject a context object into it.
//The only method that should be calling this is 'launch'. Everything else should be doing
// the proper thing and pass in the model as parameter somehow.
return BootDashActivator.getDefault().getModel();
}
@Override
public void launch(ILaunchConfiguration conf, String mode, ILaunch launch, IProgressMonitor mon)
throws CoreException {
conf = configureSourcePathProvider(conf);
Assert.isTrue(ILaunchManager.DEBUG_MODE.equals(mode));
BootDashViewModel context = getContext();
mon.beginTask("Establish SSH Debug Connection to "+getAppName(conf)+" on "+getRunTarget(conf, context), 4);
try {
CloudFoundryRunTarget target = getRunTarget(conf, context);
SshDebugSupport debugSupport = getDebugSupport(conf, context);
CloudAppDashElement app = getApp(conf, context);
if (app!=null && target!=null && debugSupport.isSupported(app)) {
//1: determine SSH tunnel parameters
app.log("Fetching SSH tunnel parameters...");
SshClientSupport sshInfo = target.getSshClientSupport();
SshHost sshHost = sshInfo.getSshHost();
String sshUser = sshInfo.getSshUser(app.getAppGuid(), getInstanceIndex(conf));
String sshCode = sshInfo.getSshCode();
int remotePort = debugSupport.getRemotePort();
app.log("SSH tunnel parameters:");
app.log(" host: "+sshHost);
app.log(" user: "+sshUser);
app.log(" code: "+sshCode);
app.log(" remote port: "+remotePort);
mon.worked(1);
//2: create tunnel
app.log("Creating tunnel...");
SshTunnel tunnel = new SshTunnel(sshHost, sshUser, sshCode, remotePort, app);
//3: connect debugger stuff
app.log("Launching remote debug connector...");
launchRemote(tunnel, conf, launch, new SubProgressMonitor(mon, 1));
app.log("Launching remote debug connector... DONE");
}
} catch (Exception e) {
throw ExceptionUtil.coreException(e);
} finally {
mon.done();
}
}
public static int getInstanceIndex(ILaunchConfiguration conf) {
try {
return conf.getAttribute(INSTANCE_IDX, 0);
} catch (Exception e) {
BootDashActivator.log(e);
}
return 0;
}
private SshDebugSupport getDebugSupport(ILaunchConfiguration conf, BootDashViewModel context) {
CloudAppDashElement app = getApp(conf, context);
if (app!=null) {
DebugSupport ds = app.getDebugSupport();
if (ds instanceof DebugSupport) {
return (SshDebugSupport) ds;
}
}
return null;
}
public static String getAppName(ILaunchConfiguration conf) {
return getString(conf, APP_NAME);
}
public static CloudAppDashElement getApp(ILaunchConfiguration conf, BootDashViewModel context) {
String appName = getAppName(conf);
if (appName!=null) {
BootDashModel section = context.getSectionByTargetId(getRunTargetId(conf));
if (section instanceof CloudFoundryBootDashModel) {
CloudFoundryBootDashModel cfmodel = (CloudFoundryBootDashModel) section;
return cfmodel.getApplication(appName);
}
}
return null;
}
public static void setAppName(ILaunchConfigurationWorkingCopy conf, String name) {
if (name!=null) {
conf.setAttribute(APP_NAME, name);
} else {
conf.removeAttribute(APP_NAME);
}
}
public static void setRunTarget(ILaunchConfigurationWorkingCopy conf, CloudFoundryRunTarget target) {
if (target!=null) {
conf.setAttribute(RUN_TARGET, target.getId());
} else {
conf.removeAttribute(RUN_TARGET);
}
}
private static String getRunTargetId(ILaunchConfiguration conf) {
String at = RUN_TARGET;
return getString(conf, at);
}
protected static String getString(ILaunchConfiguration conf, String attName) {
try {
return conf.getAttribute(attName, (String)null);
} catch (CoreException e) {
BootDashActivator.log(e);
}
return null;
}
public static CloudFoundryRunTarget getRunTarget(ILaunchConfiguration conf, BootDashViewModel context) {
try {
String id = conf.getAttribute(RUN_TARGET, (String)null);
if (id!=null) {
RunTarget target = context.getRunTargetById(id);
if (target instanceof CloudFoundryRunTarget) {
return (CloudFoundryRunTarget) target;
}
}
} catch (Exception e) {
BootDashActivator.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(final SshTunnel tunnel, ILaunchConfiguration configuration, final ILaunch launch, IProgressMonitor _monitor) throws CoreException {
int port = tunnel.getLocalPort();
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<>();
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);
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();
IOUtil.close(tunnel);
}
}
});
} 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);
}
}
}
}
public static ILaunchConfiguration getOrCreateLaunchConfig(CloudAppDashElement app) throws CoreException {
IProject project = app.getProject();
String appName = app.getName();
CloudFoundryRunTarget target = app.getTarget();
Assert.isTrue(project!=null);
ILaunchConfiguration existing = findConfig(project, target, appName);
ILaunchConfigurationWorkingCopy wc;
if (existing!=null) {
return existing;
} else {
wc = createConfiguration(project, target, appName);
return wc.doSave();
}
}
private static ILaunchConfigurationWorkingCopy createConfiguration(IProject project, CloudFoundryRunTarget target, String appName) throws CoreException {
ILaunchConfigurationType configType = getLaunchType();
ILaunchConfigurationWorkingCopy wc = configType.newInstance(null, getLaunchMan().generateLaunchConfigurationName("ssh-tunnel["+appName+"]"));
BootLaunchConfigurationDelegate.setProject(wc, project);
setRunTarget(wc, target);
setAppName(wc, appName);
wc.setMappedResources(new IResource[] {project});
return wc;
}
public static ILaunchConfiguration findConfig(CloudAppDashElement app) {
IProject project = app.getProject();
String appName = app.getName();
CloudFoundryRunTarget target = app.getTarget();
return findConfig(project, target, appName);
}
private static ILaunchConfiguration findConfig(IProject project, CloudFoundryRunTarget target, String appName) {
try {
if (project!=null) {
for (ILaunchConfiguration c : getLaunchMan().getLaunchConfigurations(getLaunchType())) {
if (
project.equals(BootLaunchConfigurationDelegate.getProject(c)) &&
target.getId().equals(getRunTargetId(c)) &&
appName.equals(getAppName(c))
) {
return c;
}
}
}
} catch (CoreException e) {
BootActivator.log(e);
}
return null;
}
public static ILaunchConfigurationType getLaunchType() {
return getLaunchMan().getLaunchConfigurationType(TYPE_ID);
}
public static void doLaunch(CloudAppDashElement app, IProgressMonitor monitor) throws CoreException {
ILaunchConfiguration conf = SshDebugLaunchConfigurationDelegate.getOrCreateLaunchConfig(app);
conf.launch(ILaunchManager.DEBUG_MODE, monitor);
}
}