[J-Debugger] PY-82513 Subsequent debug sessions don't start correctly #(PY-82513, PY-64509, PY-79518) Ready for Merge

This is a flaky bug: in the case of the second and following debug sessions, the Jupyter Debugger stops at the random place at the debug beginning.
In Python Debugger, we add to threads an additional attribute called additional_thread_info.
The debugger's additional_thread_info stores the current execution state (run/suspended) and step commands.
**When these values weren't cleared after a debug session ended,**
threads with "suspended" state would carry over to the next session, causing it to stop immediately upon start.

Fixed by properly resetting threads' debug state between debug sessions.

GitOrigin-RevId: ba23933f616c79f9a9e0307f0830f53eebc84ccd
This commit is contained in:
Natalia.Murycheva
2025-07-23 17:45:55 +00:00
committed by intellij-monorepo-bot
parent 23e425ee60
commit ba90318d2d
2 changed files with 72 additions and 26 deletions
@@ -26,8 +26,15 @@ class JupyterLineBreakpoint(LineBreakpoint):
def add_line_breakpoint(plugin, pydb, type, file, line, condition, expression, func_name, hit_condition=None, is_logpoint=False):
if type == 'jupyter-line':
breakpoint = JupyterLineBreakpoint(file, line, condition, func_name, expression, hit_condition=hit_condition,
is_logpoint=is_logpoint)
breakpoint = JupyterLineBreakpoint(
file,
line,
condition,
func_name,
expression,
hit_condition=hit_condition,
is_logpoint=is_logpoint
)
if not hasattr(pydb, 'jupyter_breakpoints'):
_init_plugin_breaks(pydb)
return breakpoint, pydb.jupyter_breakpoints
@@ -61,7 +68,8 @@ def remove_exception_breakpoint(plugin, pydb, type, exception):
def get_breakpoints(plugin, pydb, type):
if type == 'jupyter-line':
return pydb.jupyter_breakpoints
if hasattr(pydb, 'jupyter_breakpoints'):
return pydb.jupyter_breakpoints
return None
@@ -70,8 +78,9 @@ def change_variable(plugin, frame, attr, expression):
def has_exception_breaks(plugin):
if len(plugin.main_debugger.jupyter_exception_break) > 0:
return True
if hasattr(plugin.main_debugger, 'jupyter_exception_break'):
if len(plugin.main_debugger.jupyter_exception_break) > 0:
return True
return False
@@ -81,9 +90,10 @@ def has_exception_breaks(plugin):
def has_line_breaks(plugin):
for file, breakpoints in dict_iter_items(plugin.main_debugger.jupyter_breakpoints):
if len(breakpoints) > 0:
return True
if hasattr(plugin.main_debugger, 'jupyter_breakpoints'):
for file, breakpoints in dict_iter_items(plugin.main_debugger.jupyter_breakpoints):
if len(breakpoints) > 0:
return True
return False
@@ -91,6 +101,8 @@ def can_not_skip(plugin, pydb, frame, info):
step_cmd = info.pydev_step_cmd
if step_cmd == 108 and _is_equals(frame, _get_stop_frame(info)):
return True
if not hasattr(pydb, 'cell_info') or not hasattr(pydb, 'jupyter_breakpoints'):
return False
if pydb.jupyter_breakpoints:
filename = frame.f_code.co_filename
cell_info = pydb.cell_info
@@ -125,6 +137,8 @@ def _is_equals(frame, other_frame):
# We can't compare frames directly, because Jupyter compiles ast nodes
# in cell separately. At the same time, the frame filename is unique and stays
# the same within a cell.
if frame is None or other_frame is None:
return False
return frame.f_code.co_filename == other_frame.f_code.co_filename \
and ((frame.f_code.co_name.startswith('<cell line:')
and other_frame.f_code.co_name.startswith('<cell line:'))
@@ -177,17 +191,18 @@ def get_breakpoint(plugin, pydb, frame, event, args):
filename = frame.f_code.co_filename
frame_line = frame.f_lineno
if event == "line":
cell_info = pydb.cell_info
if is_cell_filename(filename):
if filename not in cell_info.cell_filename_to_cell_id_map:
cell_info.cache_cell_mapping(filename)
if hasattr(pydb, 'cell_info'):
cell_info = pydb.cell_info
if is_cell_filename(filename):
if filename not in cell_info.cell_filename_to_cell_id_map:
cell_info.cache_cell_mapping(filename)
cell_id = cell_info.cell_filename_to_cell_id_map[filename]
if cell_id in pydb.jupyter_breakpoints:
line_to_bp = pydb.jupyter_breakpoints[cell_id]
if frame_line in line_to_bp:
bp = line_to_bp[frame_line]
return True, bp, frame, "jupyter-line"
cell_id = cell_info.cell_filename_to_cell_id_map[filename]
if cell_id in pydb.jupyter_breakpoints:
line_to_bp = pydb.jupyter_breakpoints[cell_id]
if frame_line in line_to_bp:
bp = line_to_bp[frame_line]
return True, bp, frame, "jupyter-line"
return False
@@ -215,13 +230,14 @@ def _is_inside_jupyter_cell(frame, pydb):
while frame is not None:
filename = frame.f_code.co_filename
file_basename = os.path.basename(filename)
if is_cell_filename(filename):
if filename not in pydb.cell_info.cell_filename_to_cell_id_map:
send_cell_modified_warning_once(pydb, file_basename)
# IDE side doesn't know about the cell, so we should ignore it
return False
return True
frame = frame.f_back
if hasattr(pydb, 'cell_info'):
if is_cell_filename(filename):
if filename not in pydb.cell_info.cell_filename_to_cell_id_map:
send_cell_modified_warning_once(pydb, file_basename)
# IDE side doesn't know about the cell, so we should ignore it
return False
return True
frame = frame.f_back
return False
@@ -255,7 +271,7 @@ def exception_break(plugin, pydb, frame, args, arg):
def _convert_filename(frame, pydb):
filename = frame.f_code.co_filename
if is_cell_filename(filename):
if hasattr(pydb, 'cell_info') and is_cell_filename(filename):
return pydb.cell_info.cell_filename_to_cell_id_map[filename]
else:
return filename
@@ -79,6 +79,7 @@ def attach_to_debugger(debugger_port):
debugger = pydevd.PyDB()
debugger.frame_eval_func = None
ipython_shell.debugger = debugger
reset_threads_debug_state()
try:
debugger.connect(pydev_localhost.get_localhost(), debugger_port)
debugger.prepare_to_run(enable_tracing_from_start=False)
@@ -136,3 +137,32 @@ def is_cell_filename(filename):
pass
return False
def reset_threads_debug_state():
"""
Resets the debugging state of all non-daemon threads to their initial values.
This is called when initializing or reinitializing the debugger
to ensure all threads start with a clean debugging state.
"""
try:
import threading
from _pydevd_bundle.pydevd_trace_dispatch_regular import set_additional_thread_info
# get_non_pydevd_threads
all_threads = threading.enumerate()
non_pydevd_threads = [t for t in all_threads if t and not getattr(t, 'is_pydev_daemon_thread', False)]
for t in non_pydevd_threads:
if t is None:
continue
try:
additional_info = set_additional_thread_info(t)
additional_info.pydev_step_cmd = -1
additional_info.pydev_step_stop = None
additional_info.pydev_state = 1 # STATE_RUN
except:
sys.stderr.write('Jupyter Debugger Plugin: Unable to reset debug state for thread. Debug functionality may be incorrect\n')
except:
sys.stderr.write('Jupyter Debugger Plugin: Failed to initialize thread debug states. Debugger may not function correctly\n')