PY-65353 Make sure debugger stops on breakpoints in every subprocess

GitOrigin-RevId: 908ef552d42f569471410a8c24bcc05a9e571946
This commit is contained in:
Andrey Lisin
2024-01-09 15:00:55 +00:00
committed by intellij-monorepo-bot
parent 3c81687759
commit 6ee09adb6e
2 changed files with 99 additions and 69 deletions
@@ -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);
}
}
}
@@ -107,6 +107,14 @@ public class PyDebugProcess extends XDebugProcess implements IPyDebugProcess, Pr
new ConcurrentHashMap<>();
private final Set<PyThreadInfo> mySuspendedThreads = Collections.synchronizedSet(new HashSet<>());
private record BreakpointHitContext(@NotNull XBreakpoint<?> breakpoint,
@Nullable String evaluatedLogExpression,
@NotNull XSuspendContext suspendContext) {
}
private final List<BreakpointHitContext> myBreakpointHits = new LinkedList<>();
private final Map<String, XValueChildrenList> myStackFrameCache = Maps.newConcurrentMap();
private final Object myFrameCacheObject = new Object();
private final Map<String, PyDebugValue> 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));
}
}
}
}
}