blob: 4ce7bdf674344f670fab653406d6c283876cf8d0 [file] [log] [blame]
/*
* Author: atotic
* Created on Mar 23, 2004
* License: Common Public License v1.0
*/
package com.jetbrains.python.debugger.pydev;
import com.google.common.collect.Maps;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.BaseOutputReader;
import com.intellij.xdebugger.frame.XValueChildrenList;
import com.jetbrains.python.console.pydev.PydevCompletionVariant;
import com.jetbrains.python.debugger.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
public class RemoteDebugger implements ProcessDebugger {
private static final int RESPONSE_TIMEOUT = 60000;
private static final Logger LOG = Logger.getInstance("#com.jetbrains.python.pydev.remote.RemoteDebugger");
private static final String LOCAL_VERSION = "0.1";
public static final String TEMP_VAR_PREFIX = "__py_debug_temp_var_";
private static final SecureRandom ourRandom = new SecureRandom();
private final IPyDebugProcess myDebugProcess;
@NotNull
private final ServerSocket myServerSocket;
private final int myConnectionTimeout;
private final Object mySocketObject = new Object(); // for synchronization on socket
private Socket mySocket;
private volatile boolean myConnected = false;
private int mySequence = -1;
private final Map<String, PyThreadInfo> myThreads = new ConcurrentHashMap<String, PyThreadInfo>();
private final Map<Integer, ProtocolFrame> myResponseQueue = new HashMap<Integer, ProtocolFrame>();
private final TempVarsHolder myTempVars = new TempVarsHolder();
private Map<Pair<String, Integer>, String> myTempBreakpoints = Maps.newHashMap();
private final List<RemoteDebuggerCloseListener> myCloseListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private DebuggerReader myDebuggerReader;
public RemoteDebugger(final IPyDebugProcess debugProcess, @NotNull final ServerSocket serverSocket, final int timeout) {
myDebugProcess = debugProcess;
myServerSocket = serverSocket;
myConnectionTimeout = timeout;
}
public IPyDebugProcess getDebugProcess() {
return myDebugProcess;
}
@Override
public boolean isConnected() {
return myConnected;
}
@Override
public void waitForConnect() throws Exception {
try {
//noinspection SocketOpenedButNotSafelyClosed
myServerSocket.setSoTimeout(myConnectionTimeout);
synchronized (mySocketObject) {
mySocket = myServerSocket.accept();
myConnected = true;
}
}
finally {
//it is closed in close() method on process termination
}
if (myConnected) {
try {
myDebuggerReader = createReader(mySocket);
}
catch (Exception e) {
synchronized (mySocketObject) {
mySocket.close();
}
throw e;
}
}
}
@Override
public String handshake() throws PyDebuggerException {
final VersionCommand command = new VersionCommand(this, LOCAL_VERSION, SystemInfo.isUnix ? "UNIX" : "WIN");
command.execute();
return command.getRemoteVersion();
}
@Override
public PyDebugValue evaluate(final String threadId,
final String frameId,
final String expression, final boolean execute) throws PyDebuggerException {
return evaluate(threadId, frameId, expression, execute, true);
}
@Override
public PyDebugValue evaluate(final String threadId,
final String frameId,
final String expression,
final boolean execute,
boolean trimResult)
throws PyDebuggerException {
final EvaluateCommand command = new EvaluateCommand(this, threadId, frameId, expression, execute, trimResult);
command.execute();
return command.getValue();
}
@Override
public void consoleExec(String threadId, String frameId, String expression, PyDebugCallback<String> callback) {
final ConsoleExecCommand command = new ConsoleExecCommand(this, threadId, frameId, expression);
command.execute(callback);
}
@Override
public XValueChildrenList loadFrame(final String threadId, final String frameId) throws PyDebuggerException {
final GetFrameCommand command = new GetFrameCommand(this, threadId, frameId);
command.execute();
return command.getVariables();
}
// todo: don't generate temp variables for qualified expressions - just split 'em
@Override
public XValueChildrenList loadVariable(final String threadId, final String frameId, final PyDebugValue var) throws PyDebuggerException {
setTempVariable(threadId, frameId, var);
final GetVariableCommand command = new GetVariableCommand(this, threadId, frameId, var);
command.execute();
return command.getVariables();
}
@Override
public void loadReferrers(final String threadId,
final String frameId,
final PyReferringObjectsValue var,
final PyDebugCallback<XValueChildrenList> callback) {
RunCustomOperationCommand cmd = new GetReferrersCommand(this, threadId, frameId, var);
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);
}
});
}
@Override
public PyDebugValue changeVariable(final String threadId, final String frameId, final PyDebugValue var, final String value)
throws PyDebuggerException {
setTempVariable(threadId, frameId, var);
return doChangeVariable(threadId, frameId, var.getEvaluationExpression(), value);
}
private PyDebugValue doChangeVariable(final String threadId, final String frameId, final String varName, final String value)
throws PyDebuggerException {
final ChangeVariableCommand command = new ChangeVariableCommand(this, threadId, frameId, varName, value);
command.execute();
return command.getNewValue();
}
@Override
@Nullable
public String loadSource(String path) {
LoadSourceCommand command = new LoadSourceCommand(this, path);
try {
command.execute();
return command.getContent();
}
catch (PyDebuggerException e) {
return "#Couldn't load source of file " + path;
}
}
private void cleanUp() {
myThreads.clear();
myResponseQueue.clear();
mySequence = -1;
myTempVars.clear();
}
// todo: change variable in lists doesn't work - either fix in pydevd or format var name appropriately
private void setTempVariable(final String threadId, final String frameId, final PyDebugValue var) {
final PyDebugValue topVar = var.getTopParent();
if (!myDebugProcess.canSaveToTemp(topVar.getName())) {
return;
}
if (myTempVars.contains(threadId, frameId, topVar.getTempName())) {
return;
}
topVar.setTempName(generateTempName());
try {
doChangeVariable(threadId, frameId, topVar.getTempName(), topVar.getName());
myTempVars.put(threadId, frameId, topVar.getTempName());
}
catch (PyDebuggerException e) {
LOG.error(e);
topVar.setTempName(null);
}
}
private void clearTempVariables(final String threadId) {
final Map<String, Set<String>> threadVars = myTempVars.get(threadId);
if (threadVars == null || threadVars.size() == 0) return;
for (Map.Entry<String, Set<String>> entry : threadVars.entrySet()) {
final Set<String> frameVars = entry.getValue();
if (frameVars == null || frameVars.size() == 0) continue;
final String expression = "del " + StringUtil.join(frameVars, ",");
try {
evaluate(threadId, entry.getKey(), expression, true);
}
catch (PyDebuggerException e) {
LOG.error(e);
}
}
myTempVars.clear(threadId);
}
private static String generateTempName() {
return TEMP_VAR_PREFIX + ourRandom.nextInt(Integer.MAX_VALUE);
}
@Override
public Collection<PyThreadInfo> getThreads() {
return Collections.unmodifiableCollection(new ArrayList<PyThreadInfo>(myThreads.values()));
}
int getNextSequence() {
mySequence += 2;
return mySequence;
}
void placeResponse(final int sequence, final ProtocolFrame response) {
synchronized (myResponseQueue) {
if (response == null || myResponseQueue.containsKey(sequence)) {
myResponseQueue.put(sequence, response);
}
if (response != null) {
myResponseQueue.notifyAll();
}
}
}
@Nullable
ProtocolFrame waitForResponse(final int sequence) {
ProtocolFrame response;
long until = System.currentTimeMillis() + RESPONSE_TIMEOUT;
synchronized (myResponseQueue) {
do {
try {
myResponseQueue.wait(1000);
}
catch (InterruptedException ignore) {
}
response = myResponseQueue.get(sequence);
}
while (response == null && isConnected() && System.currentTimeMillis() < until);
myResponseQueue.remove(sequence);
}
return response;
}
@Override
public void execute(@NotNull final AbstractCommand command) {
if (command instanceof ResumeOrStepCommand) {
final String threadId = ((ResumeOrStepCommand)command).getThreadId();
clearTempVariables(threadId);
}
try {
command.execute();
}
catch (PyDebuggerException e) {
LOG.error(e);
}
}
boolean sendFrame(final ProtocolFrame frame) {
logFrame(frame, true);
try {
final byte[] packed = frame.pack();
synchronized (mySocketObject) {
final OutputStream os = mySocket.getOutputStream();
os.write(packed);
os.flush();
return true;
}
}
catch (SocketException se) {
disconnect();
fireCommunicationError();
}
catch (IOException e) {
LOG.error(e);
}
return false;
}
private static void logFrame(ProtocolFrame frame, boolean out) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("%1$tH:%1$tM:%1$tS.%1$tL %2$s %3$s\n", new Date(), (out ? "<<<" : ">>>"), frame));
}
}
@Override
public void suspendAllThreads() {
for (PyThreadInfo thread : getThreads()) {
suspendThread(thread.getId());
}
}
@Override
public void suspendThread(String threadId) {
final SuspendCommand command = new SuspendCommand(this, threadId);
execute(command);
}
@Override
public void close() {
if (!myServerSocket.isClosed()) {
try {
myServerSocket.close();
}
catch (IOException e) {
LOG.warn("Error closing socket", e);
}
}
if (myDebuggerReader != null) {
myDebuggerReader.stop();
}
fireCloseEvent();
}
@Override
public void disconnect() {
synchronized (mySocketObject) {
myConnected = false;
if (mySocket != null && !mySocket.isClosed()) {
try {
mySocket.close();
}
catch (IOException ignore) {
}
}
}
cleanUp();
}
@Override
public void run() throws PyDebuggerException {
new RunCommand(this).execute();
}
@Override
public void smartStepInto(String threadId, String functionName) {
final SmartStepIntoCommand command = new SmartStepIntoCommand(this, threadId, functionName);
execute(command);
}
@Override
public void resumeOrStep(String threadId, ResumeOrStepCommand.Mode mode) {
final ResumeOrStepCommand command = new ResumeOrStepCommand(this, threadId, mode);
execute(command);
}
@Override
public void setTempBreakpoint(String type, String file, int line) {
final SetBreakpointCommand command =
new SetBreakpointCommand(this, type, file, line);
execute(command); // set temp. breakpoint
myTempBreakpoints.put(Pair.create(file, line), type);
}
@Override
public void removeTempBreakpoint(String file, int line) {
String type = myTempBreakpoints.get(Pair.create(file, line));
if (type != null) {
final RemoveBreakpointCommand command = new RemoveBreakpointCommand(this, type, file, line);
execute(command); // remove temp. breakpoint
}
else {
LOG.error("Temp breakpoint not found for " + file + ":" + line);
}
}
@Override
public void setBreakpoint(String typeId, String file, int line, String condition, String logExpression) {
final SetBreakpointCommand command =
new SetBreakpointCommand(this, typeId, file, line,
condition,
logExpression);
execute(command);
}
@Override
public void removeBreakpoint(String typeId, String file, int line) {
final RemoveBreakpointCommand command =
new RemoveBreakpointCommand(this, typeId, file, line);
execute(command);
}
public DebuggerReader createReader(@NotNull Socket socket) throws IOException {
synchronized (mySocketObject) {
final InputStream myInputStream = socket.getInputStream();
//noinspection IOResourceOpenedButNotSafelyClosed
final Reader reader = new InputStreamReader(myInputStream, Charset.forName("UTF-8")); //TODO: correct econding?
return new DebuggerReader(reader);
}
}
private class DebuggerReader extends BaseOutputReader {
private Reader myReader;
private DebuggerReader(final Reader reader) throws IOException {
super(reader);
myReader = reader;
start();
}
protected void doRun() {
try {
while (true) {
boolean read = readAvailable();
if (isStopped) {
break;
}
TimeoutUtil.sleep(mySleepingPolicy.getTimeToSleep(read));
}
}
catch (Exception e) {
fireCommunicationError();
}
finally {
close();
fireExitEvent();
}
}
private void processResponse(final String line) {
try {
final ProtocolFrame frame = new ProtocolFrame(line);
logFrame(frame, false);
if (AbstractThreadCommand.isThreadCommand(frame.getCommand())) {
processThreadEvent(frame);
}
else if (AbstractCommand.isWriteToConsole(frame.getCommand())) {
writeToConsole(ProtocolParser.parseIo(frame.getPayload()));
}
else if (AbstractCommand.isExitEvent(frame.getCommand())) {
fireCommunicationError();
}
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());
}
placeResponse(frame.getSequence(), frame);
}
}
catch (Throwable t) {
// shouldn't interrupt reader thread
LOG.error(t);
}
}
private void recordCallSignature(PySignature signature) {
myDebugProcess.recordSignature(signature);
}
// todo: extract response processing
private void processThreadEvent(ProtocolFrame frame) throws PyDebuggerException {
switch (frame.getCommand()) {
case AbstractCommand.CREATE_THREAD: {
final PyThreadInfo thread = parseThreadEvent(frame);
if (!thread.isPydevThread()) { // ignore pydevd threads
myThreads.put(thread.getId(), thread);
}
break;
}
case AbstractCommand.SUSPEND_THREAD: {
final PyThreadInfo event = parseThreadEvent(frame);
PyThreadInfo thread = myThreads.get(event.getId());
if (thread == null) {
LOG.error("Trying to stop on non-existent thread: " + event.getId() + ", " + event.getStopReason() + ", " + event.getMessage());
myThreads.put(event.getId(), event);
thread = event;
}
thread.updateState(PyThreadInfo.State.SUSPENDED, event.getFrames());
thread.setStopReason(event.getStopReason());
thread.setMessage(event.getMessage());
myDebugProcess.threadSuspended(thread);
break;
}
case AbstractCommand.RESUME_THREAD: {
final String id = ProtocolParser.getThreadId(frame.getPayload());
final PyThreadInfo thread = myThreads.get(id);
if (thread != null) {
thread.updateState(PyThreadInfo.State.RUNNING, null);
myDebugProcess.threadResumed(thread);
}
break;
}
case AbstractCommand.KILL_THREAD: {
final String id = frame.getPayload();
final PyThreadInfo thread = myThreads.get(id);
if (thread != null) {
thread.updateState(PyThreadInfo.State.KILLED, null);
myThreads.remove(id);
}
break;
}
case AbstractCommand.SHOW_CONSOLE: {
final PyThreadInfo event = parseThreadEvent(frame);
PyThreadInfo thread = myThreads.get(event.getId());
if (thread == null) {
myThreads.put(event.getId(), event);
thread = event;
}
thread.updateState(PyThreadInfo.State.SUSPENDED, event.getFrames());
thread.setStopReason(event.getStopReason());
thread.setMessage(event.getMessage());
myDebugProcess.showConsole(thread);
break;
}
}
}
private PyThreadInfo parseThreadEvent(ProtocolFrame frame) throws PyDebuggerException {
return ProtocolParser.parseThread(frame.getPayload(), myDebugProcess.getPositionConverter());
}
private void closeReader(Reader reader) {
try {
reader.close();
}
catch (IOException ignore) {
}
}
@Override
protected Future<?> executeOnPooledThread(Runnable runnable) {
return ApplicationManager.getApplication().executeOnPooledThread(runnable);
}
public void close() {
closeReader(myReader);
}
@Override
public void stop() {
super.stop();
close();
}
@Override
protected void onTextAvailable(@NotNull String text) {
processResponse(text);
}
}
private void writeToConsole(PyIo io) {
ConsoleViewContentType contentType;
if (io.getCtx() == 2) {
contentType = ConsoleViewContentType.ERROR_OUTPUT;
}
else {
contentType = ConsoleViewContentType.NORMAL_OUTPUT;
}
myDebugProcess.printToConsole(io.getText(), contentType);
}
private static class TempVarsHolder {
private final Map<String, Map<String, Set<String>>> myData = new HashMap<String, Map<String, Set<String>>>();
public boolean contains(final String threadId, final String frameId, final String name) {
final Map<String, Set<String>> threadVars = myData.get(threadId);
if (threadVars == null) return false;
final Set<String> frameVars = threadVars.get(frameId);
if (frameVars == null) return false;
return frameVars.contains(name);
}
private void put(final String threadId, final String frameId, final String name) {
Map<String, Set<String>> threadVars = myData.get(threadId);
if (threadVars == null) myData.put(threadId, (threadVars = new HashMap<String, Set<String>>()));
Set<String> frameVars = threadVars.get(frameId);
if (frameVars == null) threadVars.put(frameId, (frameVars = new HashSet<String>()));
frameVars.add(name);
}
private Map<String, Set<String>> get(final String threadId) {
return myData.get(threadId);
}
private void clear() {
myData.clear();
}
private void clear(final String threadId) {
final Map<String, Set<String>> threadVars = myData.get(threadId);
if (threadVars != null) {
threadVars.clear();
}
}
}
public void addCloseListener(RemoteDebuggerCloseListener listener) {
myCloseListeners.add(listener);
}
public void removeCloseListener(RemoteDebuggerCloseListener listener) {
myCloseListeners.remove(listener);
}
@Override
public List<PydevCompletionVariant> getCompletions(String threadId, String frameId, String prefix) {
final GetCompletionsCommand command = new GetCompletionsCommand(this, threadId, frameId, prefix);
execute(command);
return command.getCompletions();
}
@Override
public void addExceptionBreakpoint(ExceptionBreakpointCommandFactory factory) {
execute(factory.createAddCommand(this));
}
@Override
public void removeExceptionBreakpoint(ExceptionBreakpointCommandFactory factory) {
execute(factory.createRemoveCommand(this));
}
private void fireCloseEvent() {
for (RemoteDebuggerCloseListener listener : myCloseListeners) {
listener.closed();
}
}
private void fireCommunicationError() {
for (RemoteDebuggerCloseListener listener : myCloseListeners) {
listener.communicationError();
}
}
private void fireExitEvent() {
for (RemoteDebuggerCloseListener listener : myCloseListeners) {
listener.detached();
}
}
}