mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Working goto handler
This commit is contained in:
parent
2f4ed23203
commit
fcc292e736
5 changed files with 157 additions and 36 deletions
|
|
@ -67,6 +67,7 @@ CAPABILITIES_V2 = {
|
|||
"supportsSetExpression": True,
|
||||
"supportsTerminateRequest": True,
|
||||
"supportsClipboardContext": True,
|
||||
"supportsGotoTargetsRequest": True,
|
||||
}
|
||||
|
||||
access_token = None
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue