Show referring objects in Python debugger.

This commit is contained in:
Dmitry Trofimov
2014-09-02 13:39:33 +02:00
parent 5ac0709ec3
commit 7c71b78d2f
16 changed files with 413 additions and 45 deletions
@@ -5,6 +5,7 @@ import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.xdebugger.frame.XNamedValue;
import com.intellij.xdebugger.frame.*;
import com.jetbrains.python.debugger.pydev.PyVariableLocator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -21,15 +22,14 @@ public class PyDebugValue extends XNamedValue {
private final String myValue;
private final boolean myContainer;
private final PyDebugValue myParent;
private String myId = null;
private final PyFrameAccessor myFrameAccessor;
protected final PyFrameAccessor myFrameAccessor;
private PyVariableLocator myVariableLocator;
private final boolean myErrorOnEval;
public PyDebugValue(@NotNull final String name, final String type, final String value, final boolean container, boolean errorOnEval) {
this(name, type, value, container, errorOnEval, null, null);
}
public PyDebugValue(@NotNull final String name, final String type, final String value, final boolean container,
boolean errorOnEval, final PyFrameAccessor frameAccessor) {
this(name, type, value, container, errorOnEval, null, frameAccessor);
@@ -179,4 +179,35 @@ public class PyDebugValue extends XNamedValue {
public PyDebugValue setName(String newName) {
return new PyDebugValue(newName, myType, myValue, myContainer, myErrorOnEval, myParent, myFrameAccessor);
}
@Nullable
@Override
public XReferrersProvider getReferrersProvider() {
return new XReferrersProvider() {
@Override
public XValue getReferringObjectsValue() {
return new PyReferringObjectsValue(PyDebugValue.this);
}
};
}
public PyFrameAccessor getFrameAccessor() {
return myFrameAccessor;
}
public PyVariableLocator getVariableLocator() {
return myVariableLocator;
}
public void setVariableLocator(PyVariableLocator variableLocator) {
myVariableLocator = variableLocator;
}
public String getId() {
return myId;
}
public void setId(String id) {
myId = id;
}
}
@@ -1,6 +1,8 @@
package com.jetbrains.python.debugger;
import com.intellij.util.Consumer;
import com.intellij.xdebugger.frame.XValueChildrenList;
import com.jetbrains.python.debugger.pydev.PyDebugCallback;
import org.jetbrains.annotations.Nullable;
/**
@@ -17,4 +19,6 @@ public interface PyFrameAccessor {
XValueChildrenList loadVariable(PyDebugValue var) throws PyDebuggerException;
void changeVariable(PyDebugValue variable, String expression) throws PyDebuggerException;
void loadReferrers(PyReferringObjectsValue value, PyDebugCallback<XValueChildrenList> callback);
}
@@ -0,0 +1,83 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jetbrains.python.debugger;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.xdebugger.frame.XCompositeNode;
import com.intellij.xdebugger.frame.XValueChildrenList;
import com.jetbrains.python.debugger.pydev.PyDebugCallback;
import org.jetbrains.annotations.NotNull;
public class PyReferringObjectsValue extends PyDebugValue {
private static final Logger LOG = Logger.getInstance(PyReferringObjectsValue.class);
//private final boolean myIsField;
private String myId;
private String myFoundAs;
public PyReferringObjectsValue(@NotNull String name,
String type,
String value,
boolean container, boolean errorOnEval, PyFrameAccessor frameAccessor) {
super(name, type, value, container, errorOnEval, frameAccessor);
}
public PyReferringObjectsValue(PyDebugValue debugValue) {
this(debugValue.getName(), debugValue.getType(), debugValue.getValue(), debugValue.isContainer(), debugValue.isErrorOnEval(), debugValue.getFrameAccessor());
//myIsField = isField;
}
@Override
public boolean canNavigateToSource() {
return true;
}
@Override
public void computeChildren(@NotNull final XCompositeNode node) {
if (node.isObsolete()) return;
myFrameAccessor.loadReferrers(this, new PyDebugCallback<XValueChildrenList>() {
@Override
public void ok(XValueChildrenList value) {
if (!node.isObsolete()) {
node.addChildren(value, true);
}
}
@Override
public void error(PyDebuggerException e) {
if (!node.isObsolete()) {
node.setErrorMessage("Unable to display children:" + e.getMessage());
}
LOG.warn(e);
}
});
}
public boolean isField() {
return false; //TODO
}
public String getId() {
return myId;
}
public void setReferrerFoundInfo(String id, String foundAs) {
myId = id;
myFoundAs = foundAs;
}
}
@@ -33,6 +33,7 @@ public abstract class AbstractCommand<T> {
public static final int SMART_STEP_INTO = 128;
public static final int EXIT = 129;
public static final int CALL_SIGNATURE_TRACE = 130;
public static final int CMD_RUN_CUSTOM_OPERATION = 135;
public static final int SHOW_CONSOLE = 142;
public static final int ERROR = 901;
@@ -107,7 +108,7 @@ public abstract class AbstractCommand<T> {
}
}
public void execute(final ProcessDebugger.DebugCallback<T> callback) {
public void execute(final PyDebugCallback<T> callback) {
final int sequence = myDebugger.getNextSequence();
final ResponseProcessor<T> processor = getResponseProcessor();
@@ -5,6 +5,7 @@ import com.jetbrains.python.debugger.PyDebugValue;
public class GetVariableCommand extends GetFrameCommand {
public static final String BY_ID = "BY_ID";
private final String myVariableName;
private final PyDebugValue myParent;
@@ -15,23 +16,40 @@ public class GetVariableCommand extends GetFrameCommand {
}
public static String composeName(final PyDebugValue var) {
final StringBuilder sb = new StringBuilder(var.getTempName());
final StringBuilder sb = new StringBuilder();
PyDebugValue p = var;
while ((p = p.getParent()) != null) {
sb.insert(0, '\t').insert(0, p.getTempName());
while (p != null) {
if (sb.length() > 0 ) {
sb.insert(0, '\t');
}
if (p.getId() != null) {
sb.insert(0, BY_ID).insert(0, '\t').insert(0, p.getId());
break;
} else {
sb.insert(0, p.getTempName());
}
p = p.getParent();
}
return sb.toString();
}
@Override
protected void buildPayload(Payload payload) {
super.buildPayload(payload);
payload.add(myVariableName);
if (myParent.getVariableLocator() != null) {
payload.add(myParent.getVariableLocator().getThreadId()).add(myParent.getVariableLocator().getPyDBLocation());
}
else if (myVariableName.contains(BY_ID)) {
payload.add(getThreadId()).add(myVariableName);
}
else {
super.buildPayload(payload);
payload.add(myVariableName);
}
}
@Override
protected PyDebugValue extend(final PyDebugValue value) {
return new PyDebugValue(value.getName(), value.getType(), value.getValue(), value.isContainer(), value.isErrorOnEval(), myParent, myDebugProcess);
return new PyDebugValue(value.getName(), value.getType(), value.getValue(), value.isContainer(), value.isErrorOnEval(), myParent,
myDebugProcess);
}
}
@@ -8,10 +8,7 @@ import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.xdebugger.frame.XValueChildrenList;
import com.jetbrains.python.console.pydev.PydevCompletionVariant;
import com.jetbrains.python.debugger.IPyDebugProcess;
import com.jetbrains.python.debugger.PyDebugValue;
import com.jetbrains.python.debugger.PyDebuggerException;
import com.jetbrains.python.debugger.PyThreadInfo;
import com.jetbrains.python.debugger.*;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
@@ -168,7 +165,7 @@ public class MultiProcessDebugger implements ProcessDebugger {
}
@Override
public void consoleExec(String threadId, String frameId, String expression, DebugCallback<String> callback) {
public void consoleExec(String threadId, String frameId, String expression, PyDebugCallback<String> callback) {
debugger(threadId).consoleExec(threadId, frameId, expression, callback);
}
@@ -182,6 +179,11 @@ public class MultiProcessDebugger implements ProcessDebugger {
return debugger(threadId).loadVariable(threadId, frameId, var);
}
@Override
public void loadReferrers(String threadId, String frameId, PyReferringObjectsValue var, PyDebugCallback<XValueChildrenList> callback) {
debugger(threadId).loadReferrers(threadId, frameId, var, callback);
}
@NotNull
private ProcessDebugger debugger(@NotNull String threadId) {
ProcessDebugger debugger = myThreadRegistry.getDebugger(threadId);
@@ -4,6 +4,7 @@ import com.intellij.xdebugger.frame.XValueChildrenList;
import com.jetbrains.python.console.pydev.PydevCompletionVariant;
import com.jetbrains.python.debugger.PyDebugValue;
import com.jetbrains.python.debugger.PyDebuggerException;
import com.jetbrains.python.debugger.PyReferringObjectsValue;
import com.jetbrains.python.debugger.PyThreadInfo;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -26,15 +27,17 @@ public interface ProcessDebugger {
String expression,
boolean execute,
boolean trimResult)
throws PyDebuggerException;
throws PyDebuggerException;
void consoleExec(String threadId, String frameId, String expression, DebugCallback<String> callback);
void consoleExec(String threadId, String frameId, String expression, PyDebugCallback<String> callback);
XValueChildrenList loadFrame(String threadId, String frameId) throws PyDebuggerException;
// todo: don't generate temp variables for qualified expressions - just split 'em
XValueChildrenList loadVariable(String threadId, String frameId, PyDebugValue var) throws PyDebuggerException;
void loadReferrers(String threadId, String frameId, PyReferringObjectsValue var, PyDebugCallback<XValueChildrenList> callback);
PyDebugValue changeVariable(String threadId, String frameId, PyDebugValue var, String value)
throws PyDebuggerException;
@@ -50,7 +53,7 @@ public interface ProcessDebugger {
void suspendThread(String threadId);
/**
* Disconnects current debug process. Closes all resources.
* Disconnects current debug process. Closes all resources.
*/
void close();
@@ -84,12 +87,4 @@ public interface ProcessDebugger {
void addExceptionBreakpoint(ExceptionBreakpointCommandFactory factory);
void removeExceptionBreakpoint(ExceptionBreakpointCommandFactory factory);
/**
* @author traff
*/
interface DebugCallback<T> {
void ok(T value);
void error(PyDebuggerException exception);
}
}
@@ -121,6 +121,32 @@ public class ProtocolParser {
return parseValue(reader, frameAccessor);
}
@NotNull
public static List<PyDebugValue> parseReferrers(final String text, final PyFrameAccessor frameAccessor) throws PyDebuggerException {
final List<PyDebugValue> values = new LinkedList<PyDebugValue>();
final XppReader reader = openReader(text, false);
while (reader.hasMoreChildren()) {
reader.moveDown();
if (reader.getNodeName().equals("var")) {
PyDebugValue value = parseValue(reader, frameAccessor);
value.setId(readString(reader, "id", null));
values.add(value);
}
else if (reader.getNodeName().equals("for")) {
//TODO
}
else {
throw new PyDebuggerException("Expected <var> or <for>, found " + reader.getNodeName());
}
reader.moveUp();
}
return values;
}
@NotNull
public static List<PyDebugValue> parseValues(final String text, final PyFrameAccessor frameAccessor) throws PyDebuggerException {
final List<PyDebugValue> values = new LinkedList<PyDebugValue>();
@@ -0,0 +1,12 @@
package com.jetbrains.python.debugger.pydev;
import com.jetbrains.python.debugger.PyDebuggerException;
/**
* @author traff
*/
public interface PyDebugCallback<T> {
void ok(T value);
void error(PyDebuggerException exception);
}
@@ -0,0 +1,17 @@
package com.jetbrains.python.debugger.pydev;
/**
* IVariableLocator knows how to produce location information
* for CMD_GET_VARIABLE
*
* The location is specified as:
*
* thread_id, stack_frame, LOCAL|GLOBAL, attribute*
*/
public interface PyVariableLocator {
public String getThreadId();
public String getPyDBLocation();
}
@@ -133,7 +133,7 @@ public class RemoteDebugger implements ProcessDebugger {
}
@Override
public void consoleExec(String threadId, String frameId, String expression, DebugCallback<String> callback) {
public void consoleExec(String threadId, String frameId, String expression, PyDebugCallback<String> callback) {
final ConsoleExecCommand command = new ConsoleExecCommand(this, threadId, frameId, expression);
command.execute(callback);
}
@@ -154,6 +154,65 @@ public class RemoteDebugger implements ProcessDebugger {
return command.getVariables();
}
@Override
public void loadReferrers(final String threadId,
final String frameId,
final PyReferringObjectsValue var,
final PyDebugCallback<XValueChildrenList> callback) {
RunCustomOperationCommand cmd = new RunCustomOperationCommand<List<PyDebugValue>>(this, createVariableLocator(threadId, frameId, var),
"from pydevd_referrers import get_referrer_info",
"get_referrer_info") {
@Override
protected ResponseProcessor<List<PyDebugValue>> createResponseProcessor() {
return new ResponseProcessor<List<PyDebugValue>>() {
@Override
protected List<PyDebugValue> parseResponse(ProtocolFrame response) throws PyDebuggerException {
return ProtocolParser.parseReferrers(decode(response.getPayload()), RemoteDebugger.this.getDebugProcess());
}
};
}
};
cmd.execute(new PyDebugCallback<List<PyDebugValue>>() {
@Override
public void ok(List<PyDebugValue> value) {
XValueChildrenList list = new XValueChildrenList();
for (PyDebugValue v: value) {
list.add(v);
}
callback.ok(list);
}
@Override
public void error(PyDebuggerException exception) {
callback.error(exception);
}
});
}
private PyVariableLocator createVariableLocator(final String threadId, final String frameId, final PyReferringObjectsValue var) {
return new PyVariableLocator() {
@Override
public String getThreadId() {
return threadId;
}
@Override
public String getPyDBLocation() {
if (var.getId() == null) {
return threadId + "\t" + frameId + "\tFRAME\t" + var.getName();
}
//Ok, this only happens when we're dealing with references with no proper scope given and we need to get
//things by id (which is usually not ideal). In this case we keep the proper thread id and set the frame id
//as the id of the object to be searched later on based on the list of all alive objects.
return getThreadId() + "\t" + var.getId() + "\tBY_ID";
}
};
}
@Override
public PyDebugValue changeVariable(final String threadId, final String frameId, final PyDebugValue var, final String value)
throws PyDebuggerException {
@@ -475,10 +534,10 @@ public class RemoteDebugger implements ProcessDebugger {
else if (AbstractCommand.isCallSignatureTrace(frame.getCommand())) {
recordCallSignature(ProtocolParser.parseCallSignature(frame.getPayload()));
}
else if (AbstractCommand.isErrorEvent(frame.getCommand())) {
LOG.error("Error response from debugger: " + frame.getPayload());
}
else {
if (AbstractCommand.isErrorEvent(frame.getCommand())) {
LOG.error("Error response from debugger: " + frame.getPayload());
}
placeResponse(frame.getSequence(), frame);
}
}
@@ -0,0 +1,108 @@
package com.jetbrains.python.debugger.pydev;
import com.intellij.openapi.diagnostic.Logger;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* Run a custom bit of Python in the context of the specified debug target.
* <p>
* This command takes a variable or expression (expressed as an {@link PyVariableLocator#getPyDBLocation()} style
* location) and passes it to the function provided in the constructor. The constructor also takes either a code
* snippet that should define the function, or a file to execfile that should define the function.
* <p>
* Once created, the command should be posted to the target with {@link AbstractDebugTarget#postCommand(AbstractDebuggerCommand)}.
* Optionally, the function run on the target can return a string for further processing. In this case the command's
* {@link #setCompletionListener(ICommandResponseListener)} should be set and on completion, {@link #getResponsePayload()}
* can be used to obtain the returned value.
* <p>
* For an example, see {@link PrettyPrintCommandHandler}
*/
public class RunCustomOperationCommand<T> extends AbstractCommand<T> {
private static final Logger LOG = Logger.getInstance(RunCustomOperationCommand.class);
private String encodedCodeOrFile;
private String operationFnName;
private PyVariableLocator locator;
private String style;
private String responsePayload;
private RunCustomOperationCommand(RemoteDebugger target, PyVariableLocator locator,
String style, String codeOrFile, String operationFnName) {
super(target, CMD_RUN_CUSTOM_OPERATION);
this.locator = locator;
this.style = style;
this.encodedCodeOrFile = encode(codeOrFile);
this.operationFnName = operationFnName;
}
/**
* Create a new command to run with the function defined in a string.
*
* @param target Debug Target to run on
* @param locator Location of variable or expression.
* @param operationSource Definition of the function to be run (this code is "exec"ed by the target)
* @param operationFnName Function to call, must be defined by operationSource
*/
public RunCustomOperationCommand(RemoteDebugger target, PyVariableLocator locator,
String operationSource, String operationFnName) {
this(target, locator, "EXEC", operationSource, operationFnName);
}
/**
* Create a new command to run with the function defined in a file.
*
* @param target Debug Target to run on
* @param locator Location of variable or expression.
* @param operationPyFile Definition of the function to be run (this file is "execfile"d by the target)
* @param operationFnName Function to call, must be defined by operationSource
*/
public RunCustomOperationCommand(RemoteDebugger target, PyVariableLocator locator,
File operationPyFile, String operationFnName) {
this(target, locator, "EXECFILE", operationPyFile.toString(), operationFnName);
}
@Override
protected void buildPayload(Payload payload) {
payload.add(locator.getPyDBLocation() + "||" + style).add(encodedCodeOrFile).add(operationFnName);
}
@Override
public boolean isResponseExpected() {
return true;
}
/**
* Return the response received from the custom command
* @return the response or <code>null</code> if an error or no response has been received.
*/
public String getResponsePayload() {
return responsePayload;
}
private static String encode(String in) {
try {
return URLEncoder.encode(in, "UTF-8");
} catch (UnsupportedEncodingException e) {
LOG.error("Unreachable? UTF-8 is always supported.", e);
return "";
}
}
protected static String decode(String in) {
try {
return URLDecoder.decode(in, "UTF-8");
} catch (UnsupportedEncodingException e) {
LOG.error("Unreachable? UTF-8 is always supported.", e);
return "";
}
}
}
@@ -31,12 +31,10 @@ import com.intellij.util.Function;
import com.intellij.xdebugger.frame.XValueChildrenList;
import com.jetbrains.python.console.parsing.PythonConsoleData;
import com.jetbrains.python.console.pydev.*;
import com.jetbrains.python.debugger.PyDebugValue;
import com.jetbrains.python.debugger.PyDebuggerException;
import com.jetbrains.python.debugger.PyFrameAccessor;
import com.jetbrains.python.debugger.PydevXmlUtils;
import com.jetbrains.python.debugger.*;
import com.jetbrains.python.debugger.pydev.GetVariableCommand;
import com.jetbrains.python.debugger.pydev.ProtocolParser;
import com.jetbrains.python.debugger.pydev.PyDebugCallback;
import org.apache.xmlrpc.WebServer;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.XmlRpcHandler;
@@ -528,6 +526,10 @@ public class PydevConsoleCommunication extends AbstractConsoleCommunication impl
}
}
@Override
public void loadReferrers(PyReferringObjectsValue value, PyDebugCallback<XValueChildrenList> callback) {
}
/**
* Request that pydevconsole connect (with pydevd) to the specified port
@@ -23,7 +23,7 @@ import com.jetbrains.python.console.pydev.InterpreterResponse;
import com.jetbrains.python.console.pydev.PydevCompletionVariant;
import com.jetbrains.python.debugger.PyDebugProcess;
import com.jetbrains.python.debugger.PyDebuggerException;
import com.jetbrains.python.debugger.pydev.ProcessDebugger;
import com.jetbrains.python.debugger.pydev.PyDebugCallback;
import org.jetbrains.annotations.NotNull;
import java.util.List;
@@ -63,8 +63,8 @@ public class PythonDebugConsoleCommunication extends AbstractConsoleCommunicatio
return false;
}
protected void exec(final ConsoleCodeFragment command, final ProcessDebugger.DebugCallback<Pair<String, Boolean>> callback) {
myDebugProcess.consoleExec(command.getText(), new ProcessDebugger.DebugCallback<String>() {
protected void exec(final ConsoleCodeFragment command, final PyDebugCallback<Pair<String, Boolean>> callback) {
myDebugProcess.consoleExec(command.getText(), new PyDebugCallback<String>() {
@Override
public void ok(String value) {
callback.ok(parseExecResponseString(value));
@@ -79,7 +79,7 @@ public class PythonDebugConsoleCommunication extends AbstractConsoleCommunicatio
public void execInterpreter(ConsoleCodeFragment code, final Function<InterpreterResponse, Object> callback) {
myExpression.append(code.getText());
exec(new ConsoleCodeFragment(myExpression.toString(), false), new ProcessDebugger.DebugCallback<Pair<String, Boolean>>() {
exec(new ConsoleCodeFragment(myExpression.toString(), false), new PyDebugCallback<Pair<String, Boolean>>() {
@Override
public void ok(Pair<String, Boolean> executed) {
boolean more = executed.second;
@@ -17,7 +17,6 @@ package com.jetbrains.python.debugger;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.execution.console.DuplexConsoleView;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessListener;
@@ -489,7 +488,7 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr
return myDebugger.evaluate(frame.getThreadId(), frame.getFrameId(), expression, execute, trimResult);
}
public void consoleExec(String command, ProcessDebugger.DebugCallback<String> callback) {
public void consoleExec(String command, PyDebugCallback<String> callback) {
dropFrameCaches();
try {
final PyStackFrame frame = currentFrame();
@@ -538,6 +537,17 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr
return myDebugger.loadVariable(frame.getThreadId(), frame.getFrameId(), var);
}
@Override
public void loadReferrers(PyReferringObjectsValue var, PyDebugCallback<XValueChildrenList> callback) {
try {
final PyStackFrame frame = currentFrame();
myDebugger.loadReferrers(frame.getThreadId(), frame.getFrameId(), var, callback);
}
catch (PyDebuggerException e) {
callback.error(e);
}
}
@Override
public void changeVariable(final PyDebugValue var, final String value) throws PyDebuggerException {
final PyStackFrame frame = currentFrame();
@@ -13,7 +13,7 @@ import com.jetbrains.python.console.pydev.PydevCompletionVariant;
import com.jetbrains.python.debugger.PyDebuggerException;
import com.jetbrains.python.debugger.PyExceptionBreakpointProperties;
import com.jetbrains.python.debugger.PyExceptionBreakpointType;
import com.jetbrains.python.debugger.pydev.ProcessDebugger;
import com.jetbrains.python.debugger.pydev.PyDebugCallback;
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor;
import java.util.List;
@@ -128,7 +128,7 @@ public class PythonDebuggerTest extends PyEnvTestCase {
}
private void consoleExec(String command) {
myDebugProcess.consoleExec(command, new ProcessDebugger.DebugCallback<String>() {
myDebugProcess.consoleExec(command, new PyDebugCallback<String>() {
@Override
public void ok(String value) {