/**
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
package org.python.pydev.debug.model;
import java.util.List;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IMarkerDelta;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.debug.core.DebugEvent;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IBreakpointManager;
import org.eclipse.debug.core.ILaunch;
import org.eclipse.debug.core.ILaunchListener;
import org.eclipse.debug.core.model.IBreakpoint;
import org.eclipse.debug.core.model.IDebugTarget;
import org.eclipse.debug.core.model.IMemoryBlock;
import org.eclipse.debug.core.model.IStackFrame;
import org.eclipse.debug.core.model.IThread;
import org.eclipse.debug.internal.ui.views.console.ProcessConsole;
import org.eclipse.debug.ui.DebugUITools;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.internal.console.IOConsolePartition;
import org.eclipse.ui.views.properties.IPropertySource;
import org.eclipse.ui.views.tasklist.ITaskListResourceAdapter;
import org.python.pydev.core.ExtensionHelper;
import org.python.pydev.core.docutils.StringUtils;
import org.python.pydev.core.log.Log;
import org.python.pydev.debug.core.IConsoleInputListener;
import org.python.pydev.debug.core.PydevDebugPlugin;
import org.python.pydev.debug.core.PydevDebugPreferencesInitializer;
import org.python.pydev.debug.model.remote.AbstractDebuggerCommand;
import org.python.pydev.debug.model.remote.AbstractRemoteDebugger;
import org.python.pydev.debug.model.remote.RemoveBreakpointCommand;
import org.python.pydev.debug.model.remote.RunCommand;
import org.python.pydev.debug.model.remote.SendPyExceptionCommand;
import org.python.pydev.debug.model.remote.SetBreakpointCommand;
import org.python.pydev.debug.model.remote.SetPropertyTraceCommand;
import org.python.pydev.debug.model.remote.ThreadListCommand;
import org.python.pydev.debug.model.remote.VersionCommand;
import org.python.pydev.debug.ui.launching.PythonRunnerConfig;
import com.aptana.shared_core.string.FastStringBuffer;
import com.aptana.shared_core.structure.Tuple;
/**
* This is the target for the debug (
*
* @author Fabio
*/
@SuppressWarnings("restriction")
public abstract class AbstractDebugTarget extends AbstractDebugTargetWithTransmission implements IDebugTarget,
ILaunchListener, IExceptionsBreakpointListener, IPropertyTraceListener {
private static final boolean DEBUG = false;
/**
* Path pointing to the file that started the debug (e.g.: file with __name__ == '__main__')
*/
protected IPath[] file;
/**
* The threads found in the debugger.
*/
protected PyThread[] threads;
/**
* Indicates whether we've already disconnected from the debugger.
*/
protected boolean disconnected = false;
/**
* This is the instance used to pass messages to the debugger.
*/
protected AbstractRemoteDebugger debugger;
/**
* Launch that triggered the debug session.
*/
protected ILaunch launch;
/**
* Class used to check for modifications in the values already found.
*/
private ValueModificationChecker modificationChecker;
private PyRunToLineTarget runToLineTarget;
public AbstractDebugTarget() {
modificationChecker = new ValueModificationChecker();
}
public ValueModificationChecker getModificationChecker() {
return modificationChecker;
}
public abstract boolean canTerminate();
public abstract boolean isTerminated();
public void terminate() {
if (socket != null) {
try {
socket.shutdownInput(); // trying to make my pydevd notice that the socket is gone
} catch (Exception e) {
// ok, ignore
}
try {
socket.shutdownOutput();
} catch (Exception e) {
// ok, ignore
}
try {
socket.close();
} catch (Exception e) {
// ok, ignore
}
}
socket = null;
disconnected = true;
if (writer != null) {
writer.done();
writer = null;
}
if (reader != null) {
reader.done();
reader = null;
}
if (DEBUG) {
System.out.println("TERMINATE");
}
threads = new PyThread[0];
fireEvent(new DebugEvent(this, DebugEvent.TERMINATE));
}
public AbstractRemoteDebugger getDebugger() {
return debugger;
}
public void launchAdded(ILaunch launch) {
// noop
}
public void launchChanged(ILaunch launch) {
// noop
}
// From IDebugElement
public String getModelIdentifier() {
return PyDebugModelPresentation.PY_DEBUG_MODEL_ID;
}
// From IDebugElement
public IDebugTarget getDebugTarget() {
return this;
}
public String getName() throws DebugException {
if (file != null) {
return PythonRunnerConfig.getRunningName(file);
} else {
return "unknown"; //TODO: SHOW PROPER PROCESS ID!
}
}
public boolean canResume() {
for (int i = 0; i < threads.length; i++) {
if (threads[i].canResume()) {
return true;
}
}
return false;
}
public boolean canSuspend() {
for (int i = 0; i < threads.length; i++) {
if (threads[i].canSuspend()) {
return true;
}
}
return false;
}
public boolean isSuspended() {
return false;
}
public void resume() throws DebugException {
for (int i = 0; i < threads.length; i++)
threads[i].resume();
}
public void suspend() throws DebugException {
for (int i = 0; i < threads.length; i++) {
threads[i].suspend();
}
}
public IThread[] getThreads() throws DebugException {
if (debugger == null) {
return null;
}
if (threads == null) {
ThreadListCommand cmd = new ThreadListCommand(this);
this.postCommand(cmd);
try {
cmd.waitUntilDone(1000);
threads = cmd.getThreads();
} catch (InterruptedException e) {
threads = new PyThread[0];
}
}
return threads;
}
public boolean hasThreads() throws DebugException {
return true;
}
//Breakpoints ------------------------------------------------------------------------------------------------------
/* (non-Javadoc)
* @see org.python.pydev.debug.model.IExceptionsBreakpointListener#onSetConfiguredExceptions()
*/
public void onSetConfiguredExceptions() {
// Sending python exceptions to the debugger
SendPyExceptionCommand sendCmd = new SendPyExceptionCommand(this);
this.postCommand(sendCmd);
}
/*
* (non-Javadoc)
* @see org.python.pydev.debug.model.IPropertyTraceListener#onSetPropertyTraceConfiguration()
*/
public void onSetPropertyTraceConfiguration() {
// Sending whether to trace python property
SetPropertyTraceCommand sendCmd = new SetPropertyTraceCommand(this);
this.postCommand(sendCmd);
}
/**
* @return true if the given breakpoint is supported by this target
*/
public boolean supportsBreakpoint(IBreakpoint breakpoint) {
return breakpoint instanceof PyBreakpoint;
}
/**
* @return true if all the breakpoints should be skipped. Patch from bug:
* http://sourceforge.net/tracker/index.php?func=detail&aid=1960983&group_id=85796&atid=577329
*/
private boolean shouldSkipBreakpoints() {
DebugPlugin manager = DebugPlugin.getDefault();
return manager != null && !manager.getBreakpointManager().isEnabled();
}
/**
* Adds a breakpoint if it's enabled.
*/
public void breakpointAdded(IBreakpoint breakpoint) {
try {
if (breakpoint instanceof PyBreakpoint) {
PyBreakpoint b = (PyBreakpoint) breakpoint;
if (b.isEnabled() && !shouldSkipBreakpoints()) {
String condition = null;
if (b.isConditionEnabled()) {
condition = b.getCondition();
if (condition != null) {
condition = StringUtils.replaceAll(condition, "\n", "@_@NEW_LINE_CHAR@_@");
condition = StringUtils.replaceAll(condition, "\t", "@_@TAB_CHAR@_@");
}
}
SetBreakpointCommand cmd = new SetBreakpointCommand(this, b.getFile(), b.getLine(), condition,
b.getFunctionName());
this.postCommand(cmd);
}
}
} catch (CoreException e) {
Log.log(e);
}
}
/**
* Removes an existing breakpoint from the debug target.
*/
public void breakpointRemoved(IBreakpoint breakpoint, IMarkerDelta delta) {
if (breakpoint instanceof PyBreakpoint) {
PyBreakpoint b = (PyBreakpoint) breakpoint;
RemoveBreakpointCommand cmd = new RemoveBreakpointCommand(this, b.getFile(), b.getLine());
this.postCommand(cmd);
}
}
/**
* Called when a breakpoint is changed.
* E.g.:
* - When line numbers change in the file
* - When the manager decides to enable/disable all existing markers
* - When the breakpoint properties (hit condition) are edited
*/
public void breakpointChanged(IBreakpoint breakpoint, IMarkerDelta delta) {
if (breakpoint instanceof PyBreakpoint) {
breakpointRemoved(breakpoint, null);
breakpointAdded(breakpoint);
}
}
//End Breakpoints --------------------------------------------------------------------------------------------------
// Storage retrieval is not supported
public boolean supportsStorageRetrieval() {
return false;
}
public IMemoryBlock getMemoryBlock(long startAddress, long length) throws DebugException {
return null;
}
/**
* When a command that originates from daemon is received,
* this routine processes it.
* The responses to commands originating from here
* are processed by commands themselves
*/
public void processCommand(String sCmdCode, String sSeqCode, String payload) {
if (DEBUG) {
System.out.println("process command:" + sCmdCode +
"\tseq:" + sSeqCode +
"\tpayload:" + payload +
"\n\n");
}
try {
int cmdCode = Integer.parseInt(sCmdCode);
if (cmdCode == AbstractDebuggerCommand.CMD_THREAD_CREATED) {
processThreadCreated(payload);
} else if (cmdCode == AbstractDebuggerCommand.CMD_THREAD_KILL) {
processThreadKilled(payload);
} else if (cmdCode == AbstractDebuggerCommand.CMD_THREAD_SUSPEND) {
processThreadSuspended(payload);
} else if (cmdCode == AbstractDebuggerCommand.CMD_THREAD_RUN) {
processThreadRun(payload);
} else {
PydevDebugPlugin.log(IStatus.WARNING, "Unexpected debugger command:" + sCmdCode +
"\nseq:" + sSeqCode
+
"\npayload:" + payload, null);
}
} catch (Exception e) {
PydevDebugPlugin.log(IStatus.ERROR, "Error processing: " + sCmdCode +
"\npayload: " + payload, e);
}
}
public void fireEvent(DebugEvent event) {
DebugPlugin manager = DebugPlugin.getDefault();
if (manager != null) {
manager.fireDebugEventSet(new DebugEvent[] { event });
}
}
/**
* @return an existing thread with a given id (null if none)
*/
protected PyThread findThreadByID(String thread_id) {
for (IThread thread : threads) {
if (thread_id.equals(((PyThread) thread).getId())) {
return (PyThread) thread;
}
}
return null;
}
/**
* Add it to the list of threads
*/
private void processThreadCreated(String payload) {
PyThread[] newThreads;
try {
newThreads = XMLUtils.ThreadsFromXML(this, payload);
} catch (CoreException e) {
PydevDebugPlugin.errorDialog("Error in processThreadCreated", e);
return;
}
// Hide Pydevd threads if requested
if (PydevDebugPlugin.getDefault().getPreferenceStore()
.getBoolean(PydevDebugPreferencesInitializer.HIDE_PYDEVD_THREADS)) {
int removeThisMany = 0;
for (int i = 0; i < newThreads.length; i++) {
if (((PyThread) newThreads[i]).isPydevThread()) {
removeThisMany++;
}
}
if (removeThisMany > 0) {
int newSize = newThreads.length - removeThisMany;
if (newSize == 0) { // no threads to add
return;
} else {
PyThread[] newnewThreads = new PyThread[newSize];
int i = 0;
for (PyThread newThread : newThreads) {
if (!((PyThread) newThread).isPydevThread()) {
newnewThreads[i] = newThread;
i += 1;
}
}
newThreads = newnewThreads;
}
}
}
// add threads to the thread list, and fire event
if (threads == null) {
threads = newThreads;
} else {
PyThread[] combined = new PyThread[threads.length + newThreads.length];
int i = 0;
for (i = 0; i < threads.length; i++) {
combined[i] = threads[i];
}
for (int j = 0; j < newThreads.length; i++, j++) {
combined[i] = newThreads[j];
}
threads = combined;
}
// Now notify debugger that new threads were added
for (int i = 0; i < newThreads.length; i++) {
fireEvent(new DebugEvent(newThreads[i], DebugEvent.CREATE));
}
}
// Remote this from our thread list
private void processThreadKilled(String thread_id) {
PyThread threadToDelete = findThreadByID(thread_id);
if (threadToDelete != null) {
int j = 0;
PyThread[] newThreads = new PyThread[threads.length - 1];
for (int i = 0; i < threads.length; i++) {
if (threads[i] != threadToDelete) {
newThreads[j++] = threads[i];
}
}
threads = newThreads;
fireEvent(new DebugEvent(threadToDelete, DebugEvent.TERMINATE));
}
}
private void processThreadSuspended(String payload) {
Object[] threadNstack;
try {
threadNstack = XMLUtils.XMLToStack(this, payload);
} catch (CoreException e) {
PydevDebugPlugin.errorDialog("Error reading ThreadSuspended", e);
return;
}
PyThread t = (PyThread) threadNstack[0];
int reason = DebugEvent.UNSPECIFIED;
String stopReason = (String) threadNstack[1];
if (stopReason != null) {
int stopReason_i = Integer.parseInt(stopReason);
if (stopReason_i == AbstractDebuggerCommand.CMD_STEP_OVER
|| stopReason_i == AbstractDebuggerCommand.CMD_STEP_INTO
|| stopReason_i == AbstractDebuggerCommand.CMD_STEP_RETURN
|| stopReason_i == AbstractDebuggerCommand.CMD_RUN_TO_LINE
|| stopReason_i == AbstractDebuggerCommand.CMD_SET_NEXT_STATEMENT) {
reason = DebugEvent.STEP_END;
} else if (stopReason_i == AbstractDebuggerCommand.CMD_THREAD_SUSPEND) {
reason = DebugEvent.CLIENT_REQUEST;
} else if (stopReason_i == AbstractDebuggerCommand.CMD_SET_BREAK) {
reason = DebugEvent.BREAKPOINT;
} else {
PydevDebugPlugin.log(IStatus.ERROR, "Unexpected reason for suspension", null);
reason = DebugEvent.UNSPECIFIED;
}
}
if (t != null) {
modificationChecker.onlyLeaveThreads((PyThread[]) this.threads);
IStackFrame stackFrame[] = (IStackFrame[]) threadNstack[2];
t.setSuspended(true, stackFrame);
fireEvent(new DebugEvent(t, DebugEvent.SUSPEND, reason));
}
}
/**
* @param payload a string in the format: thread_id\tresume_reason
* E.g.: pid3720_zad_seq1\t108
*
* @return a tuple with the thread id and the reason it stopped.
* @throws CoreException
*/
public static Tuple<String, String> getThreadIdAndReason(String payload) throws CoreException {
List<String> split = StringUtils.split(payload.trim(), '\t');
if (split.size() != 2) {
String msg = "Unexpected threadRun payload " + payload +
"(unable to match)";
throw new CoreException(PydevDebugPlugin.makeStatus(IStatus.ERROR, msg, new RuntimeException(msg)));
}
return new Tuple<String, String>(split.get(0), split.get(1));
}
/**
* ThreadRun event processing
*/
private void processThreadRun(String payload) {
try {
Tuple<String, String> threadIdAndReason = getThreadIdAndReason(payload);
int resumeReason = DebugEvent.UNSPECIFIED;
try {
int raw_reason = Integer.parseInt(threadIdAndReason.o2);
if (raw_reason == AbstractDebuggerCommand.CMD_STEP_OVER)
resumeReason = DebugEvent.STEP_OVER;
else if (raw_reason == AbstractDebuggerCommand.CMD_STEP_RETURN)
resumeReason = DebugEvent.STEP_RETURN;
else if (raw_reason == AbstractDebuggerCommand.CMD_STEP_INTO)
resumeReason = DebugEvent.STEP_INTO;
else if (raw_reason == AbstractDebuggerCommand.CMD_RUN_TO_LINE)
resumeReason = DebugEvent.UNSPECIFIED;
else if (raw_reason == AbstractDebuggerCommand.CMD_SET_NEXT_STATEMENT)
resumeReason = DebugEvent.UNSPECIFIED;
else if (raw_reason == AbstractDebuggerCommand.CMD_THREAD_RUN)
resumeReason = DebugEvent.CLIENT_REQUEST;
else {
PydevDebugPlugin.log(IStatus.ERROR, "Unexpected resume reason code", null);
resumeReason = DebugEvent.UNSPECIFIED;
}
} catch (NumberFormatException e) {
// expected, when pydevd reports "None"
resumeReason = DebugEvent.UNSPECIFIED;
}
String threadID = threadIdAndReason.o1;
PyThread t = (PyThread) findThreadByID(threadID);
if (t != null) {
t.setSuspended(false, null);
fireEvent(new DebugEvent(t, DebugEvent.RESUME, resumeReason));
} else {
FastStringBuffer buf = new FastStringBuffer();
for (PyThread thread : threads) {
if (buf.length() > 0) {
buf.append(", ");
}
buf.append("id: " + thread.getId());
}
String msg = "Unable to find thread: " + threadID +
" available: " + buf;
PydevDebugPlugin.log(IStatus.ERROR, msg, new RuntimeException(msg));
}
} catch (CoreException e1) {
Log.log(e1);
}
}
/**
* Called after debugger has been connected.
*
* Here we send all the initialization commands
* and exceptions on which pydev debugger needs to break
*/
public void initialize() {
// we post version command just for fun
// it establishes the connection
this.postCommand(new VersionCommand(this));
// now, register all the breakpoints in all projects
addBreakpointsFor(ResourcesPlugin.getWorkspace().getRoot());
// Sending python exceptions and property trace state before sending run command
this.onSetConfiguredExceptions();
this.onSetPropertyTraceConfiguration();
// Send the run command, and we are off
RunCommand run = new RunCommand(this);
this.postCommand(run);
}
/**
* Adds the breakpoints associated with a container.
* @param container the container we're interested in (usually workspace root)
*/
private void addBreakpointsFor(IContainer container) {
try {
IMarker[] markers = container.findMarkers(PyBreakpoint.PY_BREAK_MARKER, true, IResource.DEPTH_INFINITE);
IMarker[] condMarkers = container.findMarkers(PyBreakpoint.PY_CONDITIONAL_BREAK_MARKER, true,
IResource.DEPTH_INFINITE);
IBreakpointManager breakpointManager = DebugPlugin.getDefault().getBreakpointManager();
for (IMarker marker : markers) {
PyBreakpoint brk = (PyBreakpoint) breakpointManager.getBreakpoint(marker);
breakpointAdded(brk);
}
for (IMarker marker : condMarkers) {
PyBreakpoint brk = (PyBreakpoint) breakpointManager.getBreakpoint(marker);
breakpointAdded(brk);
}
} catch (Throwable t) {
PydevDebugPlugin.errorDialog("Error setting breakpoints", t);
}
}
/**
* This function adds the input listener extension point, so that plugins that only care about
* the input in the console can know about it.
*/
@SuppressWarnings({ "unchecked" })
public void addConsoleInputListener() {
IConsole console = DebugUITools.getConsole(this.getProcess());
if (console instanceof ProcessConsole) {
final ProcessConsole c = (ProcessConsole) console;
final List<IConsoleInputListener> participants = ExtensionHelper
.getParticipants(ExtensionHelper.PYDEV_DEBUG_CONSOLE_INPUT_LISTENER);
final AbstractDebugTarget target = this;
//let's listen the doc for the changes
c.getDocument().addDocumentListener(new IDocumentListener() {
public void documentAboutToBeChanged(DocumentEvent event) {
//only report when we have a new line
if (event.fText.indexOf('\r') != -1 || event.fText.indexOf('\n') != -1) {
try {
ITypedRegion partition = event.fDocument.getPartition(event.fOffset);
if (partition instanceof IOConsolePartition) {
IOConsolePartition p = (IOConsolePartition) partition;
//we only communicate about inputs (because we only care about what the user writes)
if (p.getType().equals(IOConsolePartition.INPUT_PARTITION_TYPE)) {
if (event.fText.length() <= 2) {
//the user typed something
final String inputFound = p.getString();
for (IConsoleInputListener listener : participants) {
listener.newLineReceived(inputFound, target);
}
}
}
}
} catch (Exception e) {
Log.log(e);
}
}
}
public void documentChanged(DocumentEvent event) {
//only report when we have a new line
if (event.fText.indexOf('\r') != -1 || event.fText.indexOf('\n') != -1) {
try {
ITypedRegion partition = event.fDocument.getPartition(event.fOffset);
if (partition instanceof IOConsolePartition) {
IOConsolePartition p = (IOConsolePartition) partition;
//we only communicate about inputs (because we only care about what the user writes)
if (p.getType().equals(IOConsolePartition.INPUT_PARTITION_TYPE)) {
if (event.fText.length() > 2) {
//the user pasted something
for (IConsoleInputListener listener : participants) {
listener.pasteReceived(event.fText, target);
}
}
}
}
} catch (Exception e) {
Log.log(e);
}
}
}
});
}
}
public boolean canDisconnect() {
return !disconnected;
}
public void disconnect() throws DebugException {
this.terminate();
modificationChecker = null;
}
public boolean isDisconnected() {
return disconnected;
}
public Object getAdapter(Class adapter) {
AdapterDebug.print(this, adapter);
// Not really sure what to do here, but I am trying
if (adapter.equals(ILaunch.class)) {
return launch;
} else if (adapter.equals(IResource.class)) {
// used by Variable ContextManager, and Project:Properties menu item
if (file != null) {
IFile[] files = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocation(file[0]);
if (files != null && files.length > 0) {
return files[0];
} else {
return null;
}
}
} else if (adapter.equals(org.eclipse.debug.ui.actions.IRunToLineTarget.class)) {
return this.getRunToLineTarget();
} else if (adapter.equals(IPropertySource.class)) {
return launch.getAdapter(adapter);
} else if (adapter.equals(ITaskListResourceAdapter.class)
|| adapter.equals(org.eclipse.debug.ui.actions.IToggleBreakpointsTarget.class)) {
return super.getAdapter(adapter);
}
AdapterDebug.printDontKnow(this, adapter);
return super.getAdapter(adapter);
}
public PyRunToLineTarget getRunToLineTarget() {
if (this.runToLineTarget == null) {
this.runToLineTarget = new PyRunToLineTarget();
}
return this.runToLineTarget;
}
//From IDebugElement
public ILaunch getLaunch() {
return launch;
}
}