diff --git a/src/debugpy/server/adapters.py b/src/debugpy/server/adapters.py index 7a7cfd7f..1f538e42 100644 --- a/src/debugpy/server/adapters.py +++ b/src/debugpy/server/adapters.py @@ -12,7 +12,7 @@ from debugpy import adapter from debugpy.adapter import components from debugpy.common import json, log, messaging, sockets from debugpy.common.messaging import MessageDict, Request -from debugpy.common.util import IDMap, contains_line +from debugpy.common.util import IDMap from debugpy.server import eval, new_dap_id from debugpy.server.tracing import ( Breakpoint, @@ -290,7 +290,11 @@ class Adapter: if thread is None: raise request.isnt_valid(f'Unknown thread with "threadId":{thread_id}') - stop_frame = thread.stack_trace_len() if levels == () or levels == 0 else start_frame + levels + stop_frame = ( + thread.stack_trace_len() + if levels == () or levels == 0 + else start_frame + levels + ) log.info(f"stackTrace info {start_frame} {stop_frame}") frames = None try: @@ -351,7 +355,7 @@ class Adapter: return {} def gotoTargets_request(self, request: Request) -> dict: - source = request("source", json.object()) + source = request("source", json.object()) path = source("path", str) source = Source(path) line = request("line", int) @@ -359,7 +363,6 @@ class Adapter: target = {"id": target_id, "label": f"({path}:{line})", "line": line} return {"targets": [target]} - def goto_request(self, request: Request) -> dict: thread_id = request("threadId", int) @@ -370,20 +373,36 @@ class Adapter: try: source, line = self._goto_targets_map.obtain_value(target_id) except KeyError: - return request.isnt_valid('Invalid targetId for goto') + return request.isnt_valid("Invalid targetId for goto") # Make sure the thread is in the same source file current_path = inspect.getsourcefile(thread.current_frame.f_code) current_source = Source(current_path) if current_path is not None else None if current_source != source: - return request.cant_handle(f'{source} is not in the same code block as the current frame', silent=True) - - # Make sure line number is in the same code black - if not contains_line(thread.current_frame.f_code, line): - return request.cant_handle(f'Line {line} is not in the same code block as the current frame', silent=True) + return request.cant_handle( + f"{source} is not in the same code block as the current frame", + silent=True, + ) - self._tracer.goto(thread, source, line) - return {} + # Create a callback for when the goto actually finishes. We don't + # want to send our response until then. + def goto_finished(e: Exception | None): + log.info(f"Inside goto finished handler for {line}") + if e is not None: + request.cant_handle( + f"Line {line} is not in the same code block as the current frame", + silent=True + ) + else: + request.respond({}) + + + self._tracer.goto( + thread, source, line, goto_finished + ) + + # Response will happen when the line_change_callback happens + return messaging.NO_RESPONSE def exceptionInfo_request(self, request: Request): thread_id = request("threadId", int) diff --git a/src/debugpy/server/tracing/__init__.py b/src/debugpy/server/tracing/__init__.py index 13004552..8a2a307a 100644 --- a/src/debugpy/server/tracing/__init__.py +++ b/src/debugpy/server/tracing/__init__.py @@ -14,7 +14,7 @@ from debugpy.server.eval import Scope, VariableContainer from enum import Enum from pathlib import Path from sys import monitoring -from types import CodeType, FrameType +from types import CodeType, FrameType, FunctionType from typing import Any, ClassVar, Generator, Literal, Union, override # Shared for all global state pertaining to breakpoints and stepping. @@ -145,10 +145,10 @@ class Thread: can exclude a specific thread from tracing. """ - pending_ip: int | None + pending_callback: FunctionType | None """ As a result of a https://microsoft.github.io/debug-adapter-protocol/specification#Requests_Goto - this is the line number for the current thread to switch to while it is stopped. + this is a callback to run when the thread is notified after a goto """ _all: ClassVar[dict[int, "Thread"]] = {} @@ -163,7 +163,7 @@ class Thread: self.current_frame = None self.is_known_to_adapter = False self.is_traced = True - self.pending_ip = None + self.pending_callback = None # Thread IDs are serialized as JSON numbers in DAP, which are handled as 64-bit # floats by most DAP clients. However, OS thread IDs can be large 64-bit integers @@ -284,7 +284,6 @@ class Thread: log.info("{0}", f"{self}: End stack trace.") - class StackFrame: """ Represents a DAP StackFrame object. Instances must never be created directly; diff --git a/src/debugpy/server/tracing/tracer.py b/src/debugpy/server/tracing/tracer.py index a08efeef..1cfd6e74 100644 --- a/src/debugpy/server/tracing/tracer.py +++ b/src/debugpy/server/tracing/tracer.py @@ -25,7 +25,7 @@ from debugpy.server.tracing import ( is_internal_python_frame, ) from sys import monitoring -from types import CodeType, FrameType, TracebackType +from types import CodeType, FrameType, FunctionType, TracebackType from typing import Literal @@ -236,20 +236,38 @@ class Tracer: self._end_stop() monitoring.restart_events() - def goto(self, thread: Thread, source: Source, line: int): + def goto( + self, thread: Thread, source: Source, line: int, finish_callback: FunctionType + ): log.info(f"Goto {source}:{line} on {thread}") """ Change the instruction pointer of the current thread to point to the new line/source file. """ + + def goto_handler(): + log.info(f"Inside goto handler for {thread}:{line}") + # Filter out runtime warnings that come from doing a goto + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + try: + thread.current_frame.f_lineno = line + except ValueError as e: + finish_callback(e) + else: + finish_callback(None) + + # Send a stop once we finish changing the line number + self._begin_stop(thread, "goto") + + with _cvar: - thread.pending_ip = line + # We do this with a callback because only the trace function + # can change the lineno + thread.pending_callback = goto_handler # Notify just this thread _cvar.notify(thread.id) - # Act like a new stop happened - self._begin_stop(thread, "goto") - def _begin_stop( self, thread: Thread, @@ -308,15 +326,11 @@ class Tracer: while self._stopped_by is not None: _cvar.wait() - # This thread may have had its IP changed. We - # want to change the IP before we resume but we - # need to change the IP during a trace callback. - if thread.pending_ip is not None: - # Filter out runtime warnings that come from doing a goto - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - thread.current_frame.f_lineno = thread.pending_ip - thread.pending_ip = None + # This thread may need to run some code while it's still + # stopped. + if thread.pending_callback is not None: + thread.pending_callback() + thread.pending_callback = None thread.current_frame = None log.info(f"{thread} resumed.")