Working goto handler

This commit is contained in:
Rich Chiodo false 2024-04-30 15:55:54 -07:00
parent 2f4ed23203
commit fcc292e736
5 changed files with 157 additions and 36 deletions

View file

@ -67,6 +67,7 @@ CAPABILITIES_V2 = {
"supportsSetExpression": True,
"supportsTerminateRequest": True,
"supportsClipboardContext": True,
"supportsGotoTargetsRequest": True,
}
access_token = None

View file

@ -2,9 +2,12 @@
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
from functools import partial
import inspect
import itertools
import os
import sys
from types import CodeType
def evaluate(code, path=__file__, mode="eval"):
@ -163,3 +166,29 @@ def hide_thread_from_debugger(thread):
thread.is_debugpy_thread = True
thread.pydev_do_not_trace = True
thread.is_pydev_daemon_thread = True
class IDMap(object):
def __init__(self):
self._value_to_key = {}
self._key_to_value = {}
self._next_id = partial(next, itertools.count(0))
def obtain_value(self, key):
return self._key_to_value[key]
def obtain_key(self, value):
try:
key = self._value_to_key[value]
except KeyError:
key = self._next_id()
self._key_to_value[key] = value
self._value_to_key[value] = key
return key
def contains_line(code: CodeType, line: int) -> bool:
for co_line in code.co_lines():
if len(co_line) == 3:
if co_line[2] == line:
return True
return False

View file

@ -2,6 +2,7 @@
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
import inspect
import os
import sys
import threading
@ -11,6 +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.server import eval, new_dap_id
from debugpy.server.tracing import (
Breakpoint,
@ -60,6 +62,7 @@ class Adapter:
_capabilities: Capabilities = None
_expectations: Expectations = None
_start_request: messaging.Request = None
_goto_targets_map: IDMap = IDMap()
def __init__(self, stream: messaging.JsonIOStream):
self._is_initialized = False
@ -346,6 +349,40 @@ class Adapter:
self._tracer.step_over(thread)
return {}
def gotoTargets_request(self, request: Request) -> dict:
path = request("source", dict)["path"]
line = request("line", int)
target_id = self._goto_targets_map.obtain_key((path, line))
target = {"id": target_id, "label": "%s:%s" % (path, line), "line": line}
return {"targets": [target]}
def goto_request(self, request: Request) -> dict:
thread_id = request("threadId", int)
thread = Thread.get(thread_id)
if thread is None:
return request.isnt_valid(f'Unknown thread with "threadId":{thread_id}')
target_id = request("targetId", int)
if target_id is None:
return request.isnt_valid('Unknown targetId for goto')
try:
path, line = self._goto_targets_map.obtain_value(target_id)
except KeyError:
return request.isnt_valid('Invalid targetId for goto')
# Make sure the thread is in the same source file
current_source = inspect.getsourcefile(thread.current_frame.f_code)
if current_source.casefold() != path.casefold():
return request.cant_handle(f'{path} 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)
self._tracer.goto(thread, path, line)
return {}
def exceptionInfo_request(self, request: Request):
thread_id = request("threadId", int)
thread = Thread.get(thread_id)

View file

@ -7,7 +7,6 @@ import threading
import traceback
from collections import defaultdict
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from debugpy import server
from debugpy.common import log
from debugpy.server import new_dap_id
@ -16,7 +15,7 @@ from enum import Enum
from pathlib import Path
from sys import monitoring
from types import CodeType, FrameType
from typing import ClassVar, Literal, Union
from typing import ClassVar, Literal, Union, override
# Shared for all global state pertaining to breakpoints and stepping.
_cvar = threading.Condition()
@ -146,6 +145,12 @@ class Thread:
can exclude a specific thread from tracing.
"""
pending_ip: int | 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.
"""
_all: ClassVar[dict[int, "Thread"]] = {}
def __init__(self, python_thread: threading.Thread):
@ -158,6 +163,7 @@ class Thread:
self.current_frame = None
self.is_known_to_adapter = False
self.is_traced = True
self.pending_ip = 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
@ -375,42 +381,61 @@ class StackFrame:
frame.python_frame = None
@dataclass
class Step:
step: Literal["in", "out", "over"]
origin: FrameType = None
origin_line: int = None
def __init__(
self,
step: Literal["in", "out", "over", "goto"],
origin: FrameType = None,
origin_line: int = None,
):
self.step = step
self.origin = origin
self.origin_line = origin_line
def __repr__(self):
return f"Step({self.step})"
def is_complete(self, python_frame: FrameType) -> bool:
# TODO: avoid using traceback.walk_stack every time by counting stack
# depth via PY_CALL / PY_RETURN events.
is_complete = False
if self.step == "in":
is_complete = (
python_frame is not self.origin
or python_frame.f_lineno != self.origin_line
)
elif self.step == "over":
is_complete = True
for python_frame, _ in traceback.walk_stack(python_frame):
if (
python_frame is self.origin
and python_frame.f_lineno == self.origin_line
):
is_complete = False
break
return is_complete
elif self.step == "out":
is_complete = True
for python_frame, _ in traceback.walk_stack(python_frame):
if python_frame is self.origin:
is_complete = False
break
else:
raise ValueError(f"Unknown step type: {self.step}")
raise ValueError(f"Unknown step type: {self.step}")
class StepIn(Step):
def __init__(self):
super().__init__("in")
@override
def is_complete(self, python_frame: FrameType) -> bool:
return (
python_frame is not self.origin or python_frame.f_lineno != self.origin_line
)
class StepOver(Step):
def __init__(self):
super().__init__("over")
def is_complete(self, python_frame: FrameType) -> bool:
is_complete = True
for python_frame, _ in traceback.walk_stack(python_frame):
if (
python_frame is self.origin
and python_frame.f_lineno == self.origin_line
):
is_complete = False
break
return is_complete
class StepOut(Step):
def __init__(self):
super().__init__("out")
def is_complete(self, python_frame: FrameType) -> bool:
is_complete = True
for python_frame, _ in traceback.walk_stack(python_frame):
if python_frame is self.origin:
is_complete = False
break
return is_complete

View file

@ -8,6 +8,7 @@ import sys
import threading
import traceback
from collections.abc import Iterable
import warnings
from debugpy import server
from debugpy.server.tracing import (
Breakpoint,
@ -16,6 +17,9 @@ from debugpy.server.tracing import (
Source,
StackFrame,
Step,
StepIn,
StepOut,
StepOver,
Thread,
_cvar,
is_internal_python_frame,
@ -208,7 +212,7 @@ class Tracer:
"""
log.info(f"Step in on {thread}.")
with _cvar:
self._steps[thread] = Step("in")
self._steps[thread] = StepIn()
self._end_stop()
monitoring.restart_events()
@ -218,7 +222,7 @@ class Tracer:
"""
log.info(f"Step out on {thread}.")
with _cvar:
self._steps[thread] = Step("out")
self._steps[thread] = StepOut()
self._end_stop()
monitoring.restart_events()
@ -228,10 +232,24 @@ class Tracer:
Step over the next statement executed by the specified thread.
"""
with _cvar:
self._steps[thread] = Step("over")
self._steps[thread] = StepOver()
self._end_stop()
monitoring.restart_events()
def goto(self, thread: Thread, path: str, line: int):
log.info(f"Goto {path}:{line} on {thread}")
"""
Change the instruction pointer of the current thread to point to
the new line/source file.
"""
with _cvar:
thread.pending_ip = line
# 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,
@ -280,7 +298,7 @@ class Tracer:
Suspends execution of this thread until the current stop ends.
"""
thread = self._this_thread()
thread: Thread | None = self._this_thread()
with _cvar:
if self._stopped_by is None:
return
@ -289,6 +307,17 @@ class Tracer:
thread.current_frame = python_frame
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
thread.current_frame = None
log.info(f"{thread} resumed.")
StackFrame.invalidate(thread)