diff --git a/python/helpers/pydev/_pydevd_bundle/pydevd_pep_669_tracing.py b/python/helpers/pydev/_pydevd_bundle/pydevd_pep_669_tracing.py index 5513f6cd73c4..122410d61e1d 100644 --- a/python/helpers/pydev/_pydevd_bundle/pydevd_pep_669_tracing.py +++ b/python/helpers/pydev/_pydevd_bundle/pydevd_pep_669_tracing.py @@ -35,6 +35,10 @@ try: except AttributeError: pass +_EVENT_ACTIONS = { + "ADD": lambda x, y: x | y, + "REMOVE": lambda x, y: x & ~y, +} def _make_frame_cache_key(code): return code.co_firstlineno, code.co_name, code.co_filename @@ -121,6 +125,10 @@ def _should_enable_line_events_for_code(frame, code, filename, info): # will match either global or some function if breakpoint.func_name in ('None', curr_func_name): has_breakpoint_in_frame = True + # New breakpoint was processed -> stop tracing monitoring.events.INSTRUCTION + if getattr(breakpoint, '_not_processed', None): + breakpoint._not_processed = False + _modify_global_events(_EVENT_ACTIONS["REMOVE"], monitoring.events.INSTRUCTION) break # Check is f_back has a breakpoint => need register return event @@ -200,6 +208,7 @@ def enable_pep669_monitoring(): (monitoring.events.LINE, py_line_callback), (monitoring.events.PY_RETURN, py_return_callback), (monitoring.events.RAISE, py_raise_callback), + (monitoring.events.INSTRUCTION, instruction_callback), ): monitoring.register_callback(DEBUGGER_ID, event_type, callback) @@ -208,6 +217,20 @@ def enable_pep669_monitoring(): debugger.is_pep669_monitoring_enabled = True +def process_new_breakpoint(breakpoint): + breakpoint._not_processed = True + _modify_global_events(_EVENT_ACTIONS["ADD"], monitoring.events.INSTRUCTION) + + +def _modify_global_events(action, event): + DEBUGGER_ID = monitoring.DEBUGGER_ID + if not monitoring.get_tool(DEBUGGER_ID): + return + + current_events = monitoring.get_events(DEBUGGER_ID) + monitoring.set_events(DEBUGGER_ID, action(current_events, event)) + + def _enable_return_tracing(code): local_events = monitoring.get_local_events(monitoring.DEBUGGER_ID, code) monitoring.set_local_events(monitoring.DEBUGGER_ID, code, @@ -220,6 +243,57 @@ def _enable_line_tracing(code): local_events | monitoring.events.LINE) +def instruction_callback(code, instruction_offset): + try: + py_db = GlobalDebuggerHolder.global_dbg + except AttributeError: + return + if py_db is None: + return monitoring.DISABLE + + frame = _getframe(1) + # print('ENTER: INSTRUCTION ', code.co_filename, frame.f_lineno, code.co_name) + + try: + if py_db._finish_debugging_session: + return monitoring.DISABLE + + thread = get_current_thread() + + if not is_thread_alive(thread): + return + + frame_cache_key = _make_frame_cache_key(code) + + info = _get_additional_info(thread) + pydev_step_cmd = info.pydev_step_cmd + is_stepping = pydev_step_cmd != -1 + + if not is_stepping and frame_cache_key in global_cache_skips: + return + + abs_path_real_path_and_base = _get_abs_path_real_path_and_base_from_frame(frame) + filename = abs_path_real_path_and_base[1] + + breakpoints_for_file = (py_db.breakpoints.get(filename) + or py_db.has_plugin_line_breaks) + if not breakpoints_for_file and not is_stepping: + return + + if _should_enable_line_events_for_code(frame, code, filename, info): + _enable_line_tracing(code) + _enable_return_tracing(code) + except SystemExit: + return monitoring.DISABLE + except Exception: + try: + if traceback is not None: + traceback.print_exc() + except: + pass + return monitoring.DISABLE + + def py_start_callback(code, instruction_offset): try: py_db = GlobalDebuggerHolder.global_dbg diff --git a/python/helpers/pydev/_pydevd_bundle/pydevd_process_net_command.py b/python/helpers/pydev/_pydevd_bundle/pydevd_process_net_command.py index cc329a67026d..1f7b898e67c2 100644 --- a/python/helpers/pydev/_pydevd_bundle/pydevd_process_net_command.py +++ b/python/helpers/pydev/_pydevd_bundle/pydevd_process_net_command.py @@ -8,6 +8,7 @@ import pydevd_tracing from _pydev_bundle import pydev_log from _pydev_imps._pydev_saved_modules import threading from _pydevd_bundle import pydevd_traceproperty, pydevd_dont_trace, pydevd_utils +from _pydevd_bundle.pydevd_pep_669_tracing import process_new_breakpoint from _pydevd_bundle.pydevd_additional_thread_info import set_additional_thread_info from _pydevd_bundle.pydevd_breakpoints import LineBreakpoint, get_exception_class from _pydevd_bundle.pydevd_comm import (CMD_RUN, CMD_VERSION, CMD_LIST_THREADS, @@ -452,6 +453,10 @@ def process_net_command(py_db, cmd_id, seq, text): id_to_pybreakpoint[breakpoint_id] = breakpoint py_db.consolidate_breakpoints(file, id_to_pybreakpoint, breakpoints) + + if py_db.is_pep669_monitoring_enabled: + process_new_breakpoint(breakpoint) + if py_db.plugin is not None: py_db.has_plugin_line_breaks = py_db.plugin.has_line_breaks() if py_db.has_plugin_line_breaks: diff --git a/python/testData/debug/test_add_breakpoint_after_run.py b/python/testData/debug/test_add_breakpoint_after_run.py new file mode 100644 index 000000000000..d4644da43343 --- /dev/null +++ b/python/testData/debug/test_add_breakpoint_after_run.py @@ -0,0 +1,27 @@ +import time + + +def calculate_sum(a, b): + # Use a breakpoint here to inspect the program flow + result = a + b + return result + + +def greet(name): + # Use a breakpoint here to debug the call + print(f"Hello, {name}!") + + +if __name__ == "__main__": + counter = 0 + + while True: + print("\nIteration:", counter) # You can set a breakpoint here while the loop runs + num1 = counter + num2 = counter + 1 + sum_result = calculate_sum(num1, num2) # Step into this function during debugging + print(f"The sum of {num1} and {num2} is: {sum_result}") + greet("Debugger") # Also step into this function if needed + counter += 1 + # Add a sleep to slow down the loop and make debugging easier + time.sleep(2) \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/env/debug/PythonDebuggerAggregatorTest.kt b/python/testSrc/com/jetbrains/env/debug/PythonDebuggerAggregatorTest.kt index 789bc523353d..9d4a105c664c 100644 --- a/python/testSrc/com/jetbrains/env/debug/PythonDebuggerAggregatorTest.kt +++ b/python/testSrc/com/jetbrains/env/debug/PythonDebuggerAggregatorTest.kt @@ -1,16 +1,25 @@ // Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.jetbrains.env.debug +import com.intellij.openapi.projectRoots.Sdk import com.intellij.xdebugger.XDebuggerManager import com.jetbrains.env.PyEnvTestCase -import org.junit.Test +import com.jetbrains.python.run.AbstractPythonRunConfiguration +import com.jetbrains.python.run.PythonRunConfiguration import org.junit.Assert.assertEquals +import org.junit.Test class PythonDebuggerAggregatorTest: PyEnvTestCase() { @Test fun testSeveralReturnSignal() { runPythonTest(object: PyDebuggerTask("/debug", "test_several_return_signal.py") { + override fun createRunConfiguration(sdkHome: String, existingSdk: Sdk?): AbstractPythonRunConfiguration<*> { + val runConfiguration = super.createRunConfiguration(sdkHome, existingSdk) as PythonRunConfiguration + runConfiguration.envs["PYDEVD_USE_CYTHON"] = "NO" + return runConfiguration + } + override fun before() { toggleBreakpoints(6) } @@ -48,4 +57,29 @@ class PythonDebuggerAggregatorTest: PyEnvTestCase() { } }) } + + @Test + fun testAddBreakpointAfterRun() + { + runPythonTest(object: PyDebuggerTask("/debug", "test_add_breakpoint_after_run.py") { + override fun createRunConfiguration(sdkHome: String, existingSdk: Sdk?): AbstractPythonRunConfiguration<*> { + val runConfiguration = super.createRunConfiguration(sdkHome, existingSdk) as PythonRunConfiguration + runConfiguration.envs["PYDEVD_USE_CYTHON"] = "NO" + return runConfiguration + } + + override fun before() { + setWaitForTermination(false) + toggleBreakpoints(20) + } + + override fun testing() { + waitForPause() + removeBreakpoint(scriptName, 20) + resume() + toggleBreakpoints(20) + waitForPause() + } + }) + } } \ No newline at end of file