/*******************************************************************************
* Copyright (c) 2016 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is 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:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/
package org.jboss.tools.openshift.internal.core.server.debug;
import static org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants.ID_REMOTE_JAVA_APPLICATION;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.debug.core.DebugException;
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.debug.core.ILaunchManager;
import org.eclipse.jdt.launching.IJavaLaunchConfigurationConstants;
import org.eclipse.osgi.util.NLS;
import org.eclipse.wst.server.core.IServer;
import org.jboss.tools.foundation.core.plugin.log.StatusFactory;
import org.jboss.tools.openshift.common.core.OpenShiftCoreException;
import org.jboss.tools.openshift.common.core.connection.ConnectionsRegistrySingleton;
import org.jboss.tools.openshift.internal.core.OpenShiftCoreActivator;
import org.jboss.tools.openshift.internal.core.models.PortSpecAdapter;
import org.jboss.tools.openshift.internal.core.util.ResourceUtils;
import com.openshift.restclient.IClient;
import com.openshift.restclient.ResourceKind;
import com.openshift.restclient.model.IContainer;
import com.openshift.restclient.model.IEnvironmentVariable;
import com.openshift.restclient.model.IPod;
import com.openshift.restclient.model.IPort;
import com.openshift.restclient.model.IReplicationController;
/**
* @author Fred Bricon
* @author Jeff Maury
* @author Andre Dietisheim
*/
public class OpenShiftDebugUtils {
private static final String DEBUG_KEY = "DEBUG";
private static final String DEBUG_PORT_KEY = "DEBUG_PORT";
private ILaunchManager launchManager;
public static OpenShiftDebugUtils get() {
return get(DebugPlugin.getDefault().getLaunchManager());
}
/** For testing purposes **/
public static OpenShiftDebugUtils get(ILaunchManager launchManager) {
return new OpenShiftDebugUtils(launchManager);
}
private OpenShiftDebugUtils(ILaunchManager launchManager) {
this.launchManager = launchManager;
}
public DebuggingContext enableDebugMode(IReplicationController replicationController, DebuggingContext debugContext, IProgressMonitor monitor) throws CoreException {
Assert.isNotNull(replicationController);
Assert.isNotNull(debugContext);
IDebugListener listener = debugContext.getDebugListener();
if (debugContext.isDebugEnabled() && listener != null) {
IPod pod = getFirstPod(replicationController);
debugContext.setPod(pod);
listener.onDebugChange(debugContext, monitor);
} else {
debugContext.setDebugEnabled(true);
updateDebugConfig(replicationController, debugContext, monitor);
}
return debugContext;
}
public DebuggingContext disableDebugMode(IReplicationController replicationController, DebuggingContext debugContext, IProgressMonitor monitor) throws CoreException {
Assert.isNotNull(replicationController);
Assert.isNotNull(debugContext);
IDebugListener listener = debugContext.getDebugListener();
if (listener != null) {
IPod pod = getFirstPod(replicationController);
debugContext.setPod(pod);
listener.onDebugChange(debugContext, monitor);
}
if (debugContext.isDebugEnabled()) {
debugContext.setDebugEnabled(false);
updateDebugConfig(replicationController, debugContext, monitor);
}
return debugContext;
}
public void updateDebugConfig(IReplicationController replicationController, DebuggingContext debugContext, IProgressMonitor monitor)
throws CoreException {
monitor.subTask(NLS.bind("Updating replication controller {0}", replicationController.getName()));
if (replicationController == null
|| replicationController.getEnvironmentVariables() == null) {
return;
}
setDebugPort(replicationController, debugContext);
setEnvVariables(replicationController, debugContext.isDebugEnabled(), debugContext.getDebugPort());
ReplicationControllerListenerJob rcListenerJob = new ReplicationControllerListenerJob(replicationController);
rcListenerJob.addJobChangeListener(new JobChangeAdapter() {
@Override
public void done(IJobChangeEvent event) {
ConnectionsRegistrySingleton.getInstance().removeListener(rcListenerJob.getConnectionsRegistryListener());
debugContext.setPod(rcListenerJob.getPod());
if (event.getResult().isOK()
&& debugContext.getDebugListener() != null) {
try {
debugContext.getDebugListener().onPodRestart(debugContext, monitor);
} catch (CoreException e) {
throw new OpenShiftCoreException(e);
}
}
};
});
ConnectionsRegistrySingleton.getInstance().addListener(rcListenerJob.getConnectionsRegistryListener());
rcListenerJob.schedule();
IClient client = getClient(replicationController);
client.update(replicationController);
deleteAllPods(replicationController, client);
waitFor(rcListenerJob, monitor);
IStatus result = rcListenerJob.getResult();
if (result == null) {//timed out!
throw new CoreException(rcListenerJob.getTimeOutStatus());
} else if (!result.isOK()) {
throw new CoreException(result);
}
}
private void waitFor(ReplicationControllerListenerJob rcListenerJob, IProgressMonitor monitor) {
try {
rcListenerJob.join(ReplicationControllerListenerJob.TIMEOUT, monitor);
} catch (OperationCanceledException | InterruptedException e) {
throw new OpenShiftCoreException(e);
}
}
private IClient getClient(IReplicationController replicationController) throws CoreException {
IClient client = ResourceUtils.getClient(replicationController);
if (client == null) {
throw new CoreException(
StatusFactory.errorStatus(OpenShiftCoreActivator.PLUGIN_ID,
NLS.bind("Could not get client for resource {0}", replicationController.getName())));
}
return client;
}
private void deleteAllPods(IReplicationController replicationController, IClient client) {
if (ResourceKind.REPLICATION_CONTROLLER.equals(replicationController.getKind())) {
for(IPod pod : ResourceUtils.getPodsFor(replicationController)) {
client.delete(pod);
}
}
}
private void setDebugPort(IReplicationController replicationController,
DebuggingContext debugContext) {
Collection<IContainer> originalContainers = replicationController.getContainers();
if (originalContainers != null
&& !originalContainers.isEmpty()
&& debugContext.isDebugEnabled()) {
Collection<IContainer> containers = new ArrayList<>(originalContainers);
IContainer container = containers.iterator().next();
Set<IPort> ports = new HashSet<>(container.getPorts());
IPort existing = ports.stream()
.filter(p -> p.getContainerPort() == debugContext.getDebugPort())
.findFirst()
.orElse(null);
boolean added = addDebugPort(debugContext, ports, existing);
if (added) {
container.setPorts(ports);
replicationController.setContainers(containers);
}
}
}
private void setEnvVariables(IReplicationController replicationController, boolean debugEnabled, int debugPort) {
//TODO the list of env var to set in debug mode should probably be defined in the server settings instead
if (debugEnabled) {
replicationController.setEnvironmentVariable(DEBUG_PORT_KEY, String.valueOf(debugPort));
replicationController.setEnvironmentVariable(DEBUG_KEY, String.valueOf(debugEnabled));
} else {
replicationController.removeEnvironmentVariable(DEBUG_PORT_KEY);
replicationController.setEnvironmentVariable(DEBUG_KEY, String.valueOf(debugEnabled));
}
}
private boolean addDebugPort(DebuggingContext debugContext, Set<IPort> ports, IPort existing) {
boolean added = false;
if (existing == null) {
PortSpecAdapter newPort = new PortSpecAdapter("debug", "TCP", debugContext.getDebugPort());
added = ports.add(newPort);
} else {
PortSpecAdapter newPort = new PortSpecAdapter(existing.getName(), existing.getProtocol(), debugContext.getDebugPort());
if (!existing.equals(newPort)) {
ports.remove(existing);
added = ports.add(newPort);
}
}
return added;
}
public DebuggingContext getDebuggingContext(IReplicationController replicationController) {
if (replicationController == null) {
return null;
}
DebuggingContext debugContext = initDebuggingContext(replicationController, new DebuggingContext());
return debugContext;
}
private DebuggingContext initDebuggingContext(IReplicationController replicationController, DebuggingContext debugContext) {
String debugPort = getEnv(replicationController, DEBUG_PORT_KEY);
debugContext.setDebugPort(NumberUtils.toInt(debugPort, DebuggingContext.NO_DEBUG_PORT));
boolean debugEnabled = StringUtils.isNotBlank(getEnv(replicationController, DEBUG_PORT_KEY));
debugContext.setDebugEnabled(debugEnabled);
return debugContext;
}
public String getEnv(IReplicationController replicationController, String key) {
if (replicationController == null || replicationController.getEnvironmentVariables() == null) {
return null;
}
Optional<IEnvironmentVariable> envVar = replicationController.getEnvironmentVariables().stream()
.filter(ev -> key.equals(ev.getName()))
.findFirst();
if (envVar.isPresent()) {
return envVar.get().getValue();
}
return null;
}
public IPod getFirstPod(IReplicationController replicationController) {
IPod pod = ResourceUtils.getPodsFor(replicationController).stream().findFirst()
.orElse(null);
return pod;
}
public ILaunchConfiguration getRemoteDebuggerLaunchConfiguration(IServer server) throws CoreException {
ILaunchConfigurationType launchConfigurationType = launchManager.getLaunchConfigurationType(ID_REMOTE_JAVA_APPLICATION);
ILaunchConfiguration[] launchConfigs = launchManager.getLaunchConfigurations(launchConfigurationType);
String name = getRemoteDebuggerLaunchConfigurationName(server);
Optional<ILaunchConfiguration> maybeLaunch = Stream.of(launchConfigs)
.filter(lc -> name.equals(lc.getName()))
.findFirst();
return maybeLaunch.orElse(null);
}
public ILaunchConfigurationWorkingCopy createRemoteDebuggerLaunchConfiguration(IServer server) throws CoreException {
String name = getRemoteDebuggerLaunchConfigurationName(server);
ILaunchConfigurationType launchConfigurationType = launchManager.getLaunchConfigurationType(ID_REMOTE_JAVA_APPLICATION);
ILaunchConfigurationWorkingCopy workingCopy = launchConfigurationType.newInstance(null, name);
return workingCopy;
}
public void setupRemoteDebuggerLaunchConfiguration(ILaunchConfigurationWorkingCopy workingCopy, IProject project, int debugPort) throws CoreException {
String portString = String.valueOf(debugPort);
workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_ALLOW_TERMINATE, false);
workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_VM_CONNECTOR, IJavaLaunchConfigurationConstants.ID_SOCKET_ATTACH_VM_CONNECTOR);
Map<String, String> connectMap = new HashMap<>(2);
connectMap.put("port", portString); //$NON-NLS-1$
connectMap.put("hostname", "localhost"); //$NON-NLS-1$ //$NON-NLS-2$
workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_CONNECT_MAP, connectMap);
if(project != null) {
workingCopy.setAttribute(IJavaLaunchConfigurationConstants.ATTR_PROJECT_NAME, project.getName());
}
}
public boolean isRunning(ILaunchConfiguration launchConfiguration, int localDebugPort) {
boolean isRunning = getLaunches()
.filter(l -> !l.isTerminated() && launchMatches(l, launchConfiguration, localDebugPort))
.findFirst().isPresent();
return isRunning;
}
private boolean launchMatches(ILaunch l, ILaunchConfiguration launchConfiguration, int localDebugPort) {
return Objects.equals(l.getLaunchConfiguration(), launchConfiguration);
}
public static String getRemoteDebuggerLaunchConfigurationName(IServer server) {
String name ="Remote debugger to "+server.getName();
return name;
}
public void terminateRemoteDebugger(IServer server) throws CoreException {
ILaunchConfiguration launchConfig = getRemoteDebuggerLaunchConfiguration(server);
if (launchConfig == null) {
return;
}
List<IStatus> errors = new ArrayList<>();
getLaunches().filter(l -> launchConfig.equals(l.getLaunchConfiguration()))
.filter(l -> l.canTerminate())
.forEach(l -> terminate(l, errors));
if (!errors.isEmpty()) {
MultiStatus status = new MultiStatus(OpenShiftCoreActivator.PLUGIN_ID, IStatus.ERROR, errors.toArray(new IStatus[errors.size()]), "Failed to terminate remote launch configuration", null);
throw new CoreException(status);
}
}
private void terminate(ILaunch launch, Collection<IStatus> errors) {
try {
launch.terminate();
} catch (DebugException e) {
errors.add(e.getStatus());
}
}
private Stream<ILaunch> getLaunches() {
return Stream.of(launchManager.getLaunches());
}
}