blob: ea492e43cf2d14ccf9c3feae39de4271790d3fb8 [file] [log] [blame]
/*
* Copyright 2000-2013 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.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.process.ProcessListener;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.execution.ui.ExecutionConsole;
import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.remote.RemoteProcessHandlerBase;
import com.intellij.util.ui.UIUtil;
import com.intellij.xdebugger.*;
import com.intellij.xdebugger.breakpoints.XBreakpoint;
import com.intellij.xdebugger.breakpoints.XBreakpointHandler;
import com.intellij.xdebugger.breakpoints.XBreakpointType;
import com.intellij.xdebugger.breakpoints.XLineBreakpoint;
import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider;
import com.intellij.xdebugger.frame.XValueChildrenList;
import com.intellij.xdebugger.stepping.XSmartStepIntoHandler;
import com.jetbrains.python.console.PythonDebugLanguageConsoleView;
import com.jetbrains.python.console.pydev.PydevCompletionVariant;
import com.jetbrains.python.debugger.pydev.*;
import com.jetbrains.python.run.PythonProcessHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static javax.swing.SwingUtilities.invokeLater;
/**
* @author yole
*/
// todo: bundle messages
// todo: pydevd supports module reloading - look for a way to use the feature
public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, ProcessListener {
private static final int CONNECTION_TIMEOUT = 60000;
private final ProcessDebugger myDebugger;
private final XBreakpointHandler[] myBreakpointHandlers;
private final PyDebuggerEditorsProvider myEditorsProvider;
private final ProcessHandler myProcessHandler;
private final ExecutionConsole myExecutionConsole;
private final Map<PySourcePosition, XLineBreakpoint> myRegisteredBreakpoints = new ConcurrentHashMap<PySourcePosition, XLineBreakpoint>();
private final Map<String, XBreakpoint<? extends ExceptionBreakpointProperties>> myRegisteredExceptionBreakpoints =
new ConcurrentHashMap<String, XBreakpoint<? extends ExceptionBreakpointProperties>>();
private final List<PyThreadInfo> mySuspendedThreads = Collections.synchronizedList(Lists.<PyThreadInfo>newArrayList());
private final Map<String, XValueChildrenList> myStackFrameCache = Maps.newHashMap();
private final Map<String, PyDebugValue> myNewVariableValue = Maps.newHashMap();
private boolean myDownloadSources = false;
private boolean myClosing = false;
private PyPositionConverter myPositionConverter;
private final XSmartStepIntoHandler<?> mySmartStepIntoHandler;
private boolean myWaitingForConnection = false;
private PyStackFrame myStackFrameBeforeResume;
private PyStackFrame myConsoleContextFrame = null;
private PyReferrersLoader myReferrersProvider;
public PyDebugProcess(final @NotNull XDebugSession session,
@NotNull final ServerSocket serverSocket,
@NotNull final ExecutionConsole executionConsole,
@Nullable final ProcessHandler processHandler, boolean multiProcess) {
super(session);
session.setPauseActionSupported(true);
if (multiProcess) {
myDebugger = createMultiprocessDebugger(serverSocket);
}
else {
myDebugger = new RemoteDebugger(this, serverSocket, getConnectTimeout());
}
List<XBreakpointHandler> breakpointHandlers = new ArrayList<XBreakpointHandler>();
breakpointHandlers.add(new PyLineBreakpointHandler(this));
breakpointHandlers.add(new PyExceptionBreakpointHandler(this));
for (PyBreakpointHandlerFactory factory : Extensions.getExtensions(PyBreakpointHandlerFactory.EP_NAME)) {
breakpointHandlers.add(factory.createBreakpointHandler(this));
}
myBreakpointHandlers = breakpointHandlers.toArray(new XBreakpointHandler[breakpointHandlers.size()]);
myEditorsProvider = new PyDebuggerEditorsProvider();
mySmartStepIntoHandler = new PySmartStepIntoHandler(this);
myProcessHandler = processHandler;
myExecutionConsole = executionConsole;
if (myProcessHandler != null) {
myProcessHandler.addProcessListener(this);
}
if (processHandler instanceof PositionConverterProvider) {
myPositionConverter = ((PositionConverterProvider)processHandler).createPositionConverter(this);
}
else {
myPositionConverter = new PyLocalPositionConverter();
}
myDebugger.addCloseListener(new RemoteDebuggerCloseListener() {
@Override
public void closed() {
handleStop();
}
@Override
public void communicationError() {
detachDebuggedProcess();
}
@Override
public void detached() {
detachDebuggedProcess();
}
});
session.addSessionListener(new XDebugSessionAdapter() {
@Override
public void beforeSessionResume() {
if (session.getCurrentStackFrame() instanceof PyStackFrame) {
myStackFrameBeforeResume = (PyStackFrame)session.getCurrentStackFrame();
}
else {
myStackFrameBeforeResume = null;
}
}
});
}
private MultiProcessDebugger createMultiprocessDebugger(ServerSocket serverSocket) {
MultiProcessDebugger debugger = new MultiProcessDebugger(this, serverSocket, 10000);
debugger.addOtherDebuggerCloseListener(new MultiProcessDebugger.DebuggerProcessListener() {
@Override
public void threadsClosed(Set<String> threadIds) {
for (PyThreadInfo t : mySuspendedThreads) {
if (threadIds.contains(t.getId())) {
if (getSession().isSuspended()) {
getSession().resume();
break;
}
}
}
}
});
return debugger;
}
protected void detachDebuggedProcess() {
handleStop(); //in case of normal debug we stop the session
}
protected void handleStop() {
getSession().stop();
}
public void setPositionConverter(PyPositionConverter positionConverter) {
myPositionConverter = positionConverter;
}
@Override
public PyPositionConverter getPositionConverter() {
return myPositionConverter;
}
@NotNull
@Override
public XBreakpointHandler<?>[] getBreakpointHandlers() {
return myBreakpointHandlers;
}
@Override
@NotNull
public XDebuggerEditorsProvider getEditorsProvider() {
return myEditorsProvider;
}
@Override
@Nullable
protected ProcessHandler doGetProcessHandler() {
return myProcessHandler;
}
@Override
@NotNull
public ExecutionConsole createConsole() {
return myExecutionConsole;
}
@Override
public XSmartStepIntoHandler<?> getSmartStepIntoHandler() {
return mySmartStepIntoHandler;
}
@Override
public void sessionInitialized() {
waitForConnection(getConnectionMessage(), getConnectionTitle());
}
protected void waitForConnection(final String connectionMessage, String connectionTitle) {
ProgressManager.getInstance().run(new Task.Backgroundable(getSession().getProject(), connectionTitle, false) {
@Override
public void run(@NotNull final ProgressIndicator indicator) {
indicator.setText(connectionMessage);
try {
beforeConnect();
myWaitingForConnection = true;
myDebugger.waitForConnect();
myWaitingForConnection = false;
afterConnect();
handshake();
init();
myDebugger.run();
}
catch (final Exception e) {
myWaitingForConnection = false;
if (myProcessHandler != null) {
myProcessHandler.destroyProcess();
}
if (!myClosing) {
invokeLater(new Runnable() {
@Override
public void run() {
Messages.showErrorDialog("Unable to establish connection with debugger:\n" + e.getMessage(), getConnectionTitle());
}
});
}
}
}
});
}
@Override
public void init() {
getSession().rebuildViews();
registerBreakpoints();
}
@Override
public int handleDebugPort(int localPort) throws IOException {
if (myProcessHandler instanceof RemoteProcessHandlerBase) {
return getRemoteTunneledPort(localPort, (RemoteProcessHandlerBase)myProcessHandler);
}
else {
return localPort;
}
}
protected static int getRemoteTunneledPort(int localPort, @NotNull RemoteProcessHandlerBase handler) throws IOException {
try {
Pair<String, Integer> remoteSocket = handler.obtainRemoteSocket();
handler.addRemoteForwarding(remoteSocket.getSecond(), localPort);
return remoteSocket.getSecond();
}
catch (Exception e) {
throw new IOException(e);
}
}
@Override
public void recordSignature(PySignature signature) {
PySignatureCacheManager.getInstance(getSession().getProject()).recordSignature(myPositionConverter.convertSignature(signature));
}
@Override
public void showConsole(PyThreadInfo thread) {
myConsoleContextFrame = new PyExecutionStack(this, thread).getTopFrame();
if (myExecutionConsole instanceof PythonDebugLanguageConsoleView) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
((PythonDebugLanguageConsoleView)myExecutionConsole).enableConsole(false);
}
});
}
}
protected void afterConnect() {
}
protected void beforeConnect() {
}
protected String getConnectionMessage() {
return "Connecting to debugger...";
}
protected String getConnectionTitle() {
return "Connecting To Debugger";
}
private void handshake() throws PyDebuggerException {
String remoteVersion = myDebugger.handshake();
String currentBuild = ApplicationInfo.getInstance().getBuild().asStringWithoutProductCode();
if ("@@BUILD_NUMBER@@".equals(remoteVersion)) {
remoteVersion = currentBuild;
}
else if (remoteVersion.startsWith("PY-")) {
remoteVersion = remoteVersion.substring(3);
}
else {
remoteVersion = null;
}
printToConsole("Connected to pydev debugger (build " + remoteVersion + ")\n", ConsoleViewContentType.SYSTEM_OUTPUT);
if (remoteVersion != null) {
if (!remoteVersion.equals(currentBuild)) {
printToConsole("Warning: wrong debugger version. Use pycharm-debugger.egg from PyCharm installation folder.\n",
ConsoleViewContentType.ERROR_OUTPUT);
}
}
}
@Override
public void printToConsole(String text, ConsoleViewContentType contentType) {
((ConsoleView)myExecutionConsole).print(text, contentType);
}
private void registerBreakpoints() {
registerLineBreakpoints();
registerExceptionBreakpoints();
}
private void registerExceptionBreakpoints() {
for (XBreakpoint<? extends ExceptionBreakpointProperties> bp : myRegisteredExceptionBreakpoints.values()) {
addExceptionBreakpoint(bp);
}
}
public void registerLineBreakpoints() {
for (Map.Entry<PySourcePosition, XLineBreakpoint> entry : myRegisteredBreakpoints.entrySet()) {
addBreakpoint(entry.getKey(), entry.getValue());
}
}
@Override
public void startStepOver() {
passToCurrentThread(ResumeOrStepCommand.Mode.STEP_OVER);
}
@Override
public void startStepInto() {
passToCurrentThread(ResumeOrStepCommand.Mode.STEP_INTO);
}
@Override
public void startStepOut() {
passToCurrentThread(ResumeOrStepCommand.Mode.STEP_OUT);
}
public void startSmartStepInto(String functionName) {
dropFrameCaches();
if (isConnected()) {
for (PyThreadInfo suspendedThread : mySuspendedThreads) {
myDebugger.smartStepInto(suspendedThread.getId(), functionName);
}
}
}
@Override
public void stop() {
myDebugger.close();
}
@Override
public void resume() {
passToAllThreads(ResumeOrStepCommand.Mode.RESUME);
}
@Override
public void startPausing() {
if (isConnected()) {
myDebugger.suspendAllThreads();
}
}
private void passToAllThreads(final ResumeOrStepCommand.Mode mode) {
dropFrameCaches();
if (isConnected()) {
for (PyThreadInfo suspendedThread : Lists.newArrayList(mySuspendedThreads)) {
myDebugger.resumeOrStep(suspendedThread.getId(), mode);
}
}
}
private void passToCurrentThread(final ResumeOrStepCommand.Mode mode) {
dropFrameCaches();
if (isConnected()) {
String threadId = threadIdBeforeResumeOrStep();
for (PyThreadInfo suspendedThread : mySuspendedThreads) {
if (threadId == null || threadId.equals(suspendedThread.getId())) {
myDebugger.resumeOrStep(suspendedThread.getId(), mode);
break;
}
}
}
}
@Nullable
private String threadIdBeforeResumeOrStep() {
String threadId = null;
if (myStackFrameBeforeResume != null) {
threadId = myStackFrameBeforeResume.getThreadId();
}
return threadId;
}
protected boolean isConnected() {
return myDebugger.isConnected();
}
protected void disconnect() {
myDebugger.disconnect();
cleanUp();
}
public boolean isDownloadSources() {
return myDownloadSources;
}
public void setDownloadSources(boolean downloadSources) {
myDownloadSources = downloadSources;
}
protected void cleanUp() {
mySuspendedThreads.clear();
myDownloadSources = false;
}
@Override
public void runToPosition(@NotNull final XSourcePosition position) {
dropFrameCaches();
if (isConnected() && !mySuspendedThreads.isEmpty()) {
final PySourcePosition pyPosition = myPositionConverter.convertToPython(position);
String type = PyLineBreakpointType.ID;
final Document document = FileDocumentManager.getInstance().getDocument(position.getFile());
if (document != null) {
for (XBreakpointType breakpointType : Extensions.getExtensions(XBreakpointType.EXTENSION_POINT_NAME)) {
if (breakpointType instanceof PyBreakpointType &&
((PyBreakpointType)breakpointType).canPutInDocument(getSession().getProject(), document)) {
type = breakpointType.getId();
break;
}
}
}
myDebugger.setTempBreakpoint(type, pyPosition.getFile(), pyPosition.getLine());
passToCurrentThread(ResumeOrStepCommand.Mode.RESUME);
}
}
@Override
public PyDebugValue evaluate(final String expression, final boolean execute, boolean doTrunc) throws PyDebuggerException {
dropFrameCaches();
final PyStackFrame frame = currentFrame();
return evaluate(expression, execute, frame, doTrunc);
}
private PyDebugValue evaluate(String expression, boolean execute, PyStackFrame frame, boolean trimResult) throws PyDebuggerException {
return myDebugger.evaluate(frame.getThreadId(), frame.getFrameId(), expression, execute, trimResult);
}
public void consoleExec(String command, PyDebugCallback<String> callback) {
dropFrameCaches();
try {
final PyStackFrame frame = currentFrame();
myDebugger.consoleExec(frame.getThreadId(), frame.getFrameId(), command, callback);
}
catch (PyDebuggerException e) {
callback.error(e);
}
}
@Override
@Nullable
public XValueChildrenList loadFrame() throws PyDebuggerException {
final PyStackFrame frame = currentFrame();
//do not reload frame every time it is needed, because due to bug in pdb, reloading frame clears all variable changes
if (!myStackFrameCache.containsKey(frame.getThreadFrameId())) {
XValueChildrenList values = myDebugger.loadFrame(frame.getThreadId(), frame.getFrameId());
myStackFrameCache.put(frame.getThreadFrameId(), values);
}
return applyNewValue(myStackFrameCache.get(frame.getThreadFrameId()), frame.getThreadFrameId());
}
private XValueChildrenList applyNewValue(XValueChildrenList pyDebugValues, String threadFrameId) {
if (myNewVariableValue.containsKey(threadFrameId)) {
PyDebugValue newValue = myNewVariableValue.get(threadFrameId);
XValueChildrenList res = new XValueChildrenList();
for (int i = 0; i < pyDebugValues.size(); i++) {
final String name = pyDebugValues.getName(i);
if (name.equals(newValue.getName())) {
res.add(name, newValue);
}
else {
res.add(name, pyDebugValues.getValue(i));
}
}
return res;
}
else {
return pyDebugValues;
}
}
@Override
public XValueChildrenList loadVariable(final PyDebugValue var) throws PyDebuggerException {
final PyStackFrame frame = currentFrame();
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();
PyDebugValue newValue = myDebugger.changeVariable(frame.getThreadId(), frame.getFrameId(), var, value);
myNewVariableValue.put(frame.getThreadFrameId(), newValue);
}
@Nullable
@Override
public PyReferrersLoader getReferrersLoader() {
if (myReferrersProvider == null) {
myReferrersProvider = new PyReferrersLoader(this);
}
return myReferrersProvider;
}
@Nullable
public String loadSource(String path) {
return myDebugger.loadSource(path);
}
@Override
public boolean canSaveToTemp(String name) {
final Project project = getSession().getProject();
return PyDebugSupportUtils.canSaveToTemp(project, name);
}
private PyStackFrame currentFrame() throws PyDebuggerException {
if (!isConnected()) {
throw new PyDebuggerException("Disconnected");
}
final PyStackFrame frame = (PyStackFrame)getSession().getCurrentStackFrame();
if (frame == null && myConsoleContextFrame != null) {
return myConsoleContextFrame;
}
if (frame == null) {
throw new PyDebuggerException("Process is running");
}
return frame;
}
public void addBreakpoint(final PySourcePosition position, final XLineBreakpoint breakpoint) {
myRegisteredBreakpoints.put(position, breakpoint);
if (isConnected()) {
myDebugger.setBreakpoint(breakpoint.getType().getId(), position.getFile(), position.getLine(),
breakpoint.getCondition(),
breakpoint.getLogExpression());
}
}
public void removeBreakpoint(final PySourcePosition position) {
XLineBreakpoint breakpoint = myRegisteredBreakpoints.get(position);
if (breakpoint != null) {
myRegisteredBreakpoints.remove(position);
if (isConnected()) {
myDebugger.removeBreakpoint(breakpoint.getType().getId(), position.getFile(), position.getLine());
}
}
}
public void addExceptionBreakpoint(XBreakpoint<? extends ExceptionBreakpointProperties> breakpoint) {
myRegisteredExceptionBreakpoints.put(breakpoint.getProperties().getException(), breakpoint);
if (isConnected()) {
myDebugger.addExceptionBreakpoint(breakpoint.getProperties());
}
}
public void removeExceptionBreakpoint(XBreakpoint<? extends ExceptionBreakpointProperties> breakpoint) {
myRegisteredExceptionBreakpoints.remove(breakpoint.getProperties().getException());
if (isConnected()) {
myDebugger.removeExceptionBreakpoint(breakpoint.getProperties());
}
}
public Collection<PyThreadInfo> getThreads() {
return myDebugger.getThreads();
}
@Override
public void threadSuspended(final PyThreadInfo threadInfo) {
if (!mySuspendedThreads.contains(threadInfo)) {
mySuspendedThreads.add(threadInfo);
final List<PyStackFrameInfo> frames = threadInfo.getFrames();
if (frames != null) {
final PySuspendContext suspendContext = new PySuspendContext(this, threadInfo);
XBreakpoint<?> breakpoint = null;
if (threadInfo.isStopOnBreakpoint()) {
final PySourcePosition position = frames.get(0).getPosition();
breakpoint = myRegisteredBreakpoints.get(position);
if (breakpoint == null) {
myDebugger.removeTempBreakpoint(position.getFile(), position.getLine());
}
}
else if (threadInfo.isExceptionBreak()) {
String exceptionName = threadInfo.getMessage();
threadInfo.setMessage(null);
if (exceptionName != null) {
breakpoint = myRegisteredExceptionBreakpoints.get(exceptionName);
}
}
if (breakpoint != null) {
if (!getSession().breakpointReached(breakpoint, threadInfo.getMessage(), suspendContext)) {
resume();
}
}
else {
getSession().positionReached(suspendContext);
}
}
}
}
@Override
public void threadResumed(final PyThreadInfo threadInfo) {
mySuspendedThreads.remove(threadInfo);
}
private void dropFrameCaches() {
myStackFrameCache.clear();
myNewVariableValue.clear();
}
@NotNull
public List<PydevCompletionVariant> getCompletions(String prefix) throws Exception {
if (isConnected()) {
dropFrameCaches();
final PyStackFrame frame = currentFrame();
return myDebugger.getCompletions(frame.getThreadId(), frame.getFrameId(), prefix);
}
return Lists.newArrayList();
}
@Override
public void startNotified(ProcessEvent event) {
}
@Override
public void processTerminated(ProcessEvent event) {
myDebugger.close();
}
@Override
public void processWillTerminate(ProcessEvent event, boolean willBeDestroyed) {
myClosing = true;
setKillingStrategy();
}
private void setKillingStrategy() {
if (myProcessHandler instanceof PythonProcessHandler) {
((PythonProcessHandler)myProcessHandler).setShouldKillProcessSoftly(false);
//while process is suspended it can't terminate softly,
//multiple processes in debug mode also can not terminate properly
//so its better to kill all the tree in a hard way
}
}
@Override
public void onTextAvailable(ProcessEvent event, Key outputType) {
}
public PyStackFrame createStackFrame(PyStackFrameInfo frameInfo) {
return new PyStackFrame(getSession().getProject(), this, frameInfo,
getPositionConverter().convertFromPython(frameInfo.getPosition()));
}
@Override
public String getCurrentStateMessage() {
if (getSession().isStopped()) {
return XDebuggerBundle.message("debugger.state.message.disconnected");
}
else if (isConnected()) {
return XDebuggerBundle.message("debugger.state.message.connected");
}
else {
return "Waiting for connection...";
}
}
public void addProcessListener(ProcessListener listener) {
ProcessHandler handler = doGetProcessHandler();
if (handler != null) {
handler.addProcessListener(listener);
}
}
public boolean isWaitingForConnection() {
return myWaitingForConnection;
}
public void setWaitingForConnection(boolean waitingForConnection) {
myWaitingForConnection = waitingForConnection;
}
public int getConnectTimeout() {
return CONNECTION_TIMEOUT;
}
}