/**
* Copyright (C) 2014 Eric Van Dewoestine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.eclim.plugin.jdt.command.debug.context;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclim.logging.Logger;
import org.eclim.plugin.core.util.VimClient;
import org.eclim.plugin.jdt.command.debug.event.DebugEventSetListener;
import org.eclim.plugin.jdt.command.debug.ui.ThreadView;
import org.eclim.plugin.jdt.command.debug.ui.VariableView;
import org.eclim.plugin.jdt.command.debug.ui.ViewUtils;
import org.eclim.plugin.jdt.util.JavaUtils;
import org.eclim.util.CollectionUtils;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.debug.core.DebugException;
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.ILaunchManager;
import org.eclipse.debug.core.Launch;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.ISourceLocator;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.internal.launching.SocketAttachConnector;
import org.eclipse.jdt.launching.sourcelookup.JavaSourceLocator;
/**
* Maintains the state of currently active debug session and exposes methods to
* interact with it.
*/
public class DebuggerContext
{
private static final Logger logger = Logger.getLogger(DebuggerContext.class);
private static final String KEY_HOSTNAME = "hostname";
private static final String KEY_PORT = "port";
private IProject project;
private String host;
private int port;
/**
* ID of this context obtained by concatenating user input.
* This should be unique across all active contexts.
*/
private String id;
private volatile DebuggerState state = DebuggerState.CONNECTING;
private final ThreadContext threadCtx;
private final ThreadView threadView;
private final VariableView varView;
/**
* Debug target for currently active session.
*/
private IDebugTarget debugTarget;
private VimClient vimClient;
private IDebugEventSetListener listener = new DebugEventSetListener();
/**
* Starts the debug session by creating the debug target with given parameters.
*/
public DebuggerContext(
IProject project,
String host,
int port,
String vimInstanceId)
throws Exception
{
this.project = project;
this.host = host;
this.port = port;
this.id = project.getName() + " - " + host + ":" + port;
this.vimClient = new VimClient(vimInstanceId);
this.threadCtx = new ThreadContext();
this.threadView = new ThreadView(threadCtx);
this.varView = new VariableView(threadCtx);
}
public void start()
throws Exception
{
logger.info(
"Starting debug session at " + host + ":" + port +
" for vim instance: " + vimClient.getId());
// Setup listener before connecting to VM so that events are not missed
DebugPlugin.getDefault().addDebugEventListener(listener);
ILaunchConfiguration config = null;
ArrayList<IProject> projects = new ArrayList<IProject>();
projects.add(project);
CollectionUtils.addAll(projects, project.getReferencedProjects());
CollectionUtils.addAll(projects, project.getReferencingProjects());
ArrayList<IJavaProject> javaProjects = new ArrayList<IJavaProject>();
for (IProject p : projects) {
if (p.exists() && p.hasNature(JavaCore.NATURE_ID)){
javaProjects.add(JavaUtils.getJavaProject(p));
}
}
ISourceLocator srcLocator = new JavaSourceLocator(
javaProjects.toArray(new IJavaProject[javaProjects.size()]), true);
ILaunch launch = new Launch(config, ILaunchManager.DEBUG_MODE, srcLocator);
IProgressMonitor monitor = null;
Map<String, String> args = new HashMap<String, String>();
args.put(KEY_HOSTNAME, host);
args.put(KEY_PORT, String.valueOf(port));
SocketAttachConnector connector = new SocketAttachConnector();
try {
connector.connect(args, monitor, launch);
} catch (CoreException e) {
throw new RuntimeException(
"Debug VM not available at " + host + ":" + port + ". " +
"Check hostname and port number.");
}
this.debugTarget = launch.getDebugTarget();
}
/**
* Suspends the debug session.
*/
public void suspend()
throws DebugException
{
logger.info("Suspending debug session");
if (debugTarget != null) {
debugTarget.suspend();
}
}
/**
* Disconnects the debug session.
*/
public void stop()
throws DebugException
{
logger.info("Stopping debug session");
if (debugTarget != null) {
debugTarget.disconnect();
}
clear();
}
/**
* Terminates the debug session.
*/
public void terminate()
throws DebugException
{
logger.info("Terminating debug session");
if (debugTarget != null) {
debugTarget.terminate();
}
clear();
}
/**
* Resumes execution from the current breakpoint.
*/
public void resume()
throws DebugException
{
logger.debug("Resuming breakpoint");
if (debugTarget != null) {
debugTarget.resume();
}
}
private void clear()
{
DebugPlugin.getDefault().removeDebugEventListener(listener);
}
public Map<String, Object> getStatus()
throws DebugException
{
Map<String, Object> statusMap = new HashMap<String, Object>();
statusMap.put("state", ViewUtils.EXPANDED_NODE_SYMBOL + getId() +
" (" + state.getName() + ")");
statusMap.put("threads", threadView.get());
statusMap.put("variables", varView.get());
return statusMap;
}
public DebuggerState getState()
{
return this.state;
}
public void setState(DebuggerState state)
{
this.state = state;
}
public String getId()
{
return this.id;
}
public ThreadContext getThreadContext()
{
return threadCtx;
}
public ThreadView getThreadView()
{
return threadView;
}
public VariableView getVariableView()
{
return varView;
}
public IDebugTarget getDebugTarget()
{
return debugTarget;
}
public void jumpToFilePosition(String fileName, int lineNum)
throws Exception
{
vimClient.remoteFunctionCall(
"eclim#java#debug#GoToFile",
fileName, String.valueOf(lineNum));
}
public void refreshDebugStatus()
throws Exception
{
vimClient.remoteSend(":JavaDebugStatus");
}
public void signalSessionTermination()
throws Exception
{
vimClient.remoteFunctionCall("eclim#java#debug#SessionTerminated");
}
public void updateThreadView(long threadId, String kind, List<String> results)
throws Exception
{
vimClient.remoteFunctionCall(
"eclim#java#debug#ThreadViewUpdate",
String.valueOf(threadId), kind, concatenateList(results));
}
public void updateVariableView(List<String> results)
throws Exception
{
vimClient.remoteFunctionCall(
"eclim#java#debug#VariableViewUpdate",
concatenateList(results));
// Hack to force VIM to execute the previous remote send command.
// In some cases, VIM seems to buffer the command and not execute
// until the next remote command is sent.
vimClient.remoteSend(":echo ' '");
}
/**
* Returns a string by concatenating all the given entries using <eol> as
* delimiter.
*/
private String concatenateList(List<String> entries)
{
if (entries == null || entries.isEmpty()) {
// Return an empty string since the remote VIM command expects an arg
return "\\ ";
}
StringBuilder sb = new StringBuilder();
for (String entry : entries){
sb.append(entry.replaceAll(" ", "\\\\ "));
sb.append("<eol>");
}
return sb.toString();
}
}