diff --git a/python/pydevSrc/com/jetbrains/python/debugger/pydev/RemoteDebugger.java b/python/pydevSrc/com/jetbrains/python/debugger/pydev/RemoteDebugger.java index be3d428d96fa..1a8d98f31b27 100644 --- a/python/pydevSrc/com/jetbrains/python/debugger/pydev/RemoteDebugger.java +++ b/python/pydevSrc/com/jetbrains/python/debugger/pydev/RemoteDebugger.java @@ -594,73 +594,76 @@ public class RemoteDebugger implements ProcessDebugger { // 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); - if (myDebugProcess.getSession().isSuspended() && myDebugProcess.isSuspendedOnAllThreadsPolicy()) { - // Sometimes the notification about new threads may come slow from the Python side. We should check if - // the current session is suspended in the "Suspend all threads" mode and suspend new thread, which hasn't been suspended - suspendThread(thread.getId()); - } - } - } - 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()); - boolean updateSourcePosition = true; - if (event.getStopReason() == AbstractCommand.SUSPEND_THREAD) { - // That means that the thread was stopped manually from the Java side either while suspending all threads - // or after the "Pause" command. In both cases we shouldn't change debugger focus if session is already suspended. - updateSourcePosition = !myDebugProcess.getSession().isSuspended(); - } - myDebugProcess.threadSuspended(thread, updateSourcePosition); - } - 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); - } - } - 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); - } - if (myDebugProcess.getSession().getCurrentPosition() == null) { - for (PyThreadInfo threadInfo : myThreads.values()) { - // notify UI of suspended threads left in debugger if one thread finished its work - if ((threadInfo != null) && (threadInfo.getState() == PyThreadInfo.State.SUSPENDED)) { - myDebugProcess.threadResumed(threadInfo); - myDebugProcess.threadSuspended(threadInfo, true); + // The method must be synchronized because in the case of multiprocess debugging, + // each process `RemoteDebugger` shares the same session. Altering the session's state + // in an unsynchronized manner can cause race conditions. + synchronized (myDebugProcess.getSession()) { + switch (frame.getCommand()) { + case AbstractCommand.CREATE_THREAD -> { + final PyThreadInfo thread = parseThreadEvent(frame); + if (!thread.isPydevThread()) { // ignore pydevd threads + myThreads.put(thread.getId(), thread); + if (myDebugProcess.getSession().isSuspended() && myDebugProcess.isSuspendedOnAllThreadsPolicy()) { + // Sometimes the notification about new threads may come slow from the Python side. We should check if + // the current session is suspended in the "Suspend all threads" mode and suspend new thread, which hasn't been suspended + suspendThread(thread.getId()); } } } - } - 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; + 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()); + boolean updateSourcePosition = true; + if (event.getStopReason() == AbstractCommand.SUSPEND_THREAD || event.getStopReason() == AbstractCommand.SET_BREAKPOINT) { + updateSourcePosition = !myDebugProcess.getSession().isSuspended(); + } + myDebugProcess.threadSuspended(thread, updateSourcePosition); + } + 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); + } + } + 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); + } + if (myDebugProcess.getSession().getCurrentPosition() == null) { + for (PyThreadInfo threadInfo : myThreads.values()) { + // notify UI of suspended threads left in debugger if one thread finished its work + if ((threadInfo != null) && (threadInfo.getState() == PyThreadInfo.State.SUSPENDED)) { + myDebugProcess.threadResumed(threadInfo); + myDebugProcess.threadSuspended(threadInfo, true); + } + } + } + } + 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); } - thread.updateState(PyThreadInfo.State.SUSPENDED, event.getFrames()); - thread.setStopReason(event.getStopReason()); - thread.setMessage(event.getMessage()); - myDebugProcess.showConsole(thread); } } } diff --git a/python/src/com/jetbrains/python/debugger/PyDebugProcess.java b/python/src/com/jetbrains/python/debugger/PyDebugProcess.java index fae51d3c9c1e..9add3e5a56c3 100644 --- a/python/src/com/jetbrains/python/debugger/PyDebugProcess.java +++ b/python/src/com/jetbrains/python/debugger/PyDebugProcess.java @@ -107,6 +107,14 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr new ConcurrentHashMap<>(); private final Set mySuspendedThreads = Collections.synchronizedSet(new HashSet<>()); + + private record BreakpointHitContext(@NotNull XBreakpoint breakpoint, + @Nullable String evaluatedLogExpression, + @NotNull XSuspendContext suspendContext) { + } + + private final List myBreakpointHits = new LinkedList<>(); + private final Map myStackFrameCache = Maps.newConcurrentMap(); private final Object myFrameCacheObject = new Object(); private final Map myNewVariableValue = Maps.newHashMap(); @@ -649,7 +657,17 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr @Override public void resume(@Nullable XSuspendContext context) { - passResumeToAllThreads(); + if (myBreakpointHits.isEmpty()) { + passResumeToAllThreads(); + } + else { + var breakpointHitContext = myBreakpointHits.remove(0); + var shouldStop = getSession().breakpointReached(breakpointHitContext.breakpoint, breakpointHitContext.evaluatedLogExpression, + breakpointHitContext.suspendContext); + if (!shouldStop) { + resume(breakpointHitContext.suspendContext); + } + } } @Override @@ -708,6 +726,7 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr private void passToCurrentThread(@Nullable XSuspendContext context, final ResumeOrStepCommand.Mode mode) { dropFrameCaches(); + myBreakpointHits.clear(); if (isConnected()) { String threadId = threadIdBeforeResumeOrStep(context); @@ -801,7 +820,8 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr } @Override - public String execTableCommand(String command, TableCommandType commandType, TableCommandParameters tableCommandParameters) throws PyDebuggerException { + public String execTableCommand(String command, TableCommandType commandType, TableCommandParameters tableCommandParameters) + throws PyDebuggerException { final PyStackFrame frame = currentFrame(); return myDebugger.execTableCommand(frame.getThreadId(), frame.getFrameId(), command, commandType, tableCommandParameters); } @@ -836,9 +856,9 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr @Override @NotNull - public XValueChildrenList loadSpecialVariables(ProcessDebugger.GROUP_TYPE groupType) throws PyDebuggerException { + public XValueChildrenList loadSpecialVariables(ProcessDebugger.GROUP_TYPE groupType) throws PyDebuggerException { final PyStackFrame frame = currentFrame(); - XValueChildrenList values = myDebugger.loadFrame(frame.getThreadId(), frame.getFrameId(), groupType); + XValueChildrenList values = myDebugger.loadFrame(frame.getThreadId(), frame.getFrameId(), groupType); if (values != null) { PyDebugValue.getAsyncValues(frame, this, values); } @@ -1176,13 +1196,20 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr if (updateSourcePosition) { if (breakpoint != null) { - boolean shouldSuspend = getSession().breakpointReached(breakpoint, threadInfo.getMessage(), suspendContext); - if (!shouldSuspend) resume(suspendContext); + if (!getSession().breakpointReached(breakpoint, threadInfo.getMessage(), suspendContext)) { + resume(suspendContext); + } } else { ((XDebugSessionImpl)getSession()).positionReached(suspendContext, isFailedTestStop(threadInfo)); } } + else { + if (breakpoint != null) { + // Hit a breakpoint while already suspended. We have to remember it and stop on this breakpoint later. + myBreakpointHits.add(new BreakpointHitContext(breakpoint, threadInfo.getMessage(), suspendContext)); + } + } } } }