From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from simark.ca by simark.ca with LMTP id OoMlA6OXiWRovQMAWB0awg (envelope-from ) for ; Wed, 14 Jun 2023 06:34:11 -0400 Authentication-Results: simark.ca; dkim=pass (1024-bit key; secure) header.d=sourceware.org header.i=@sourceware.org header.a=rsa-sha256 header.s=default header.b=S2FULCwc; dkim-atps=neutral Received: by simark.ca (Postfix, from userid 112) id F29191E0BB; Wed, 14 Jun 2023 06:34:10 -0400 (EDT) Received: from sourceware.org (server2.sourceware.org [8.43.85.97]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by simark.ca (Postfix) with ESMTPS id DC94F1E0AC for ; Wed, 14 Jun 2023 06:34:08 -0400 (EDT) Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id B41F03858422 for ; Wed, 14 Jun 2023 10:34:07 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org B41F03858422 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=sourceware.org; s=default; t=1686738847; bh=pIF+aIL0lLxDQWr8hZC+tKN1uJuvk8g0U5flTzV6Cys=; h=To:Cc:Subject:Date:List-Id:List-Unsubscribe:List-Archive: List-Post:List-Help:List-Subscribe:From:Reply-To:From; b=S2FULCwcZqEFsa1pb9yxDBZYA9x28h1QeClT4mTSAl31QO0CZzuGqdHwPpOWaecL8 egzoIgrRInCSmzvLbLa7NFed1XVGK7HT9SGjokFntdqo2A64cKOFS7d6bB0TO65K6Z WLeK0fHG07yxcoItb5lHFZXECVTddMM1zcXQoWr0= Received: from mail-lf1-x12b.google.com (mail-lf1-x12b.google.com [IPv6:2a00:1450:4864:20::12b]) by sourceware.org (Postfix) with ESMTPS id CE1DA3858D1E for ; Wed, 14 Jun 2023 10:33:44 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org CE1DA3858D1E Received: by mail-lf1-x12b.google.com with SMTP id 2adb3069b0e04-4f74cda5f1dso3865590e87.3 for ; Wed, 14 Jun 2023 03:33:44 -0700 (PDT) X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20221208; t=1686738822; x=1689330822; h=content-transfer-encoding:mime-version:message-id:date:subject:cc :to:from:x-gm-message-state:from:to:cc:subject:date:message-id :reply-to; bh=pIF+aIL0lLxDQWr8hZC+tKN1uJuvk8g0U5flTzV6Cys=; b=HvOf8HbWPMitvlqJJyG0OJX7kNefZeR9Nkw6zF/kWpphjyPnBSuP4DRfr8q4y0UbY/ WNoREboSSDhPR9qmhYvH0uFpqeJRSmldY3WwBOtq4t9gSaOCSI4LhOngrtpngZWQqBy5 ATJEoeogQ7+5v/SSTb0HbXcpjtGlsOp31wEpYoNECQqd6l5V2di74/QYoCDyn2NBuo8x jDMc/kIWPlupGvVCkgfjNNucvOP8MGQiZvmkTeBrhhGHusKEFZJGj7BkQvycritkkiwz UZBia3W0vTfl3OUBalabHT5S21SHce0/efwRdM9GK59iZBZS2vaenVaoDrZtO5Iajvxn l2UQ== X-Gm-Message-State: AC+VfDx2kwUkHPCjwJrU2yqQc/3ND9HkQOsxxVvOZfD6kbh20N+yKsiF w9FUURPYFCa5j8iLUZN1YNfpPl0wuoaO4Q== X-Google-Smtp-Source: ACHHUZ4m7zcAlh5ZEqNadxw/bo3LBg+vTgeIXZ+/zlVOEAanR14tSloBSB8hByFKGsktyCX+gQTY4A== X-Received: by 2002:a19:430e:0:b0:4f6:5068:3c0f with SMTP id q14-20020a19430e000000b004f650683c0fmr6986702lfa.58.1686738821970; Wed, 14 Jun 2023 03:33:41 -0700 (PDT) Received: from fedora.. (78-73-77-63-no2450.tbcn.telia.com. [78.73.77.63]) by smtp.gmail.com with ESMTPSA id q25-20020ac25a19000000b004eed68a68efsm139002lfn.280.2023.06.14.03.33.40 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 14 Jun 2023 03:33:41 -0700 (PDT) To: gdb-patches@sourceware.org Cc: tom@tromey.com, Simon Farre Subject: [PATCH v1] gdb/dap - dataBreakpointInfo & setDataBreakpoints Date: Wed, 14 Jun 2023 12:33:27 +0200 Message-Id: <20230614103327.30289-1-simon.farre.cx@gmail.com> X-Mailer: git-send-email 2.40.1 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Spam-Status: No, score=-11.7 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, DKIM_VALID_AU, DKIM_VALID_EF, FREEMAIL_FROM, GIT_PATCH_0, RCVD_IN_DNSWL_NONE, SPF_HELO_NONE, SPF_PASS, TXREP, T_SCC_BODY_TEXT_LINE autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server2.sourceware.org X-BeenThere: gdb-patches@sourceware.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Gdb-patches mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: Simon Farre via Gdb-patches Reply-To: Simon Farre Errors-To: gdb-patches-bounces+public-inbox=simark.ca@sourceware.org Sender: "Gdb-patches" This is v1 of the implementation of these two requests. I've tested the implementation in my debug adapter for VSCode (Midas) and it works fully. However, I believe these two requests are flawed from a spec-perspective and has filed an issue for the DAP spec here: https://github.com/microsoft/debug-adapter-protocol/issues/404 As such, it implements *most* of the functionality of these two requests and as the github issues suggests, I believe implementing all of it is not possible with the current spec design (also, VSCode does not use the frameId field, in dataBreakpointInfo request). --- gdb/python/lib/gdb/dap/breakpoint.py | 160 ++++++++++++++++++++++++++- gdb/python/lib/gdb/dap/frames.py | 2 +- gdb/python/lib/gdb/dap/varref.py | 12 +- 3 files changed, 166 insertions(+), 8 deletions(-) diff --git a/gdb/python/lib/gdb/dap/breakpoint.py b/gdb/python/lib/gdb/dap/breakpoint.py index 20e65aa0e61..2d5bf7d0ee4 100644 --- a/gdb/python/lib/gdb/dap/breakpoint.py +++ b/gdb/python/lib/gdb/dap/breakpoint.py @@ -17,11 +17,42 @@ import gdb import os # These are deprecated in 3.9, but required in older versions. -from typing import Optional, Sequence +from typing import Optional, Sequence, Mapping from .server import request, capability -from .startup import send_gdb_with_response, in_gdb_thread +from .startup import send_gdb_with_response, in_gdb_thread, log +from .varref import find_variable +from .frames import frame_for_id +class DataBreakpoint: + def wp_type(type: str): + if type == "read": + return gdb.WP_READ + if type == "write": + return gdb.WP_WRITE + if type == "readWrite": + return gdb.WP_ACCESS + raise gdb.GdbError("Erroneous watchpoint type") + + def __init__(self, dataId, type, condition=None, hitCondition=None): + self.dataId = dataId + self.type = type + self.condition = condition + self.hitCondition = hitCondition + # gdb.BP_HARDWARE_WATCHPOINT seem to consistently fail + self.gdb_wp = gdb.Breakpoint(spec=dataId, type=gdb.BP_WATCHPOINT, wp_class=DataBreakpoint.wp_type(type)) + if self.condition is not None: + self.gdb_wp.condition = condition + if self.hitCondition is not None: + self.gdb_wp.ignore_count = hitCondition + + def to_object(self): + return { + "id": self.gdb_wp.number, + "verified": True, + } + def is_same(self, dataId, type, condition=None, hitCondition=None): + return self.dataId == dataId and self.type == type and self.condition == condition and self.hitCondition == hitCondition # Map from the breakpoint "kind" (like "function") to a second map, of # breakpoints of that type. The second map uses the breakpoint spec @@ -30,14 +61,18 @@ from .startup import send_gdb_with_response, in_gdb_thread # allowing for reuse when a breakpoint can be kept. breakpoint_map = {} +watchpoint_map: Mapping[str, DataBreakpoint] = {} @in_gdb_thread def breakpoint_descriptor(bp): "Return the Breakpoint object descriptor given a gdb Breakpoint." - if bp.locations: + if bp.location is not None: # Just choose the first location, because DAP doesn't allow # multiple locations. See # https://github.com/microsoft/debug-adapter-protocol/issues/13 + # FIXME this does not matter; GDB can translate it's understanding + # of breakpoint locations into individual breakpoints and + # even though GDB can't delete locs, it can disable them. loc = bp.locations[0] (basename, line) = loc.source result = { @@ -59,7 +94,7 @@ def breakpoint_descriptor(bp): else: return { "id": bp.number, - "verified": False, + "verified": True, } @@ -248,3 +283,120 @@ def set_exception_breakpoints( return { "breakpoints": result, } + +@in_gdb_thread +def _databreakpoint_info( + name: str, + variablesReference: Optional[int] = None, + frameId: Optional[int] = None +): + # frameId does not have an effect when var ref is not none, + # however, frameId really never has an effect. An issue has been filed: + # https://github.com/microsoft/debug-adapter-protocol/issues/404 + path = [] + path.append(name) + if variablesReference is not None: + # append "path" to name until find_variable throws + try: + variable = find_variable(variablesReference) + while variable is not None and variable.is_variable(): + path.append(variable.name) + parent_ref = variable.parent_var_ref + variable = find_variable(parent_ref) + except: + pass + + # We've resolved the "path" for the variable using varrefs. Construct it + # for example: baz being a member variable of bar, which is a member of + # foo: foo.bar.baz + dataId = ".".join(reversed(path)) + description = dataId + + try: + # since frameId can't be used, always parse global (see issue) + v = gdb.parse_and_eval(dataId) + except: + dataId = None + description = "Symbol was not found" + + return { + "dataId": dataId, + "description": description, + "accessTypes": ["read", "write", "readWrite"], + } + +@capability("supportsDataBreakpoints") +@request("dataBreakpointInfo") +def data_breakpoint_info( + *, + name: str, + variablesReference: Optional[int] = None, + frameId: Optional[int] = None, **args +): + result = send_gdb_with_response( + lambda: _databreakpoint_info(name, variablesReference, frameId)) + return result + + +@in_gdb_thread +def _set_watchpoint(breakpoints: Sequence[dict]): + """Set (or keep) watchpoints passed in breakpoints and remove set + watchpoints not found in breakpoints""" + global watchpoint_map + if len(breakpoints) == 0: + for wp in watchpoint_map: + wp.gdb_wp.delete() + watchpoint_map.clear() + return [] + + new_wps_to_add = [] + # Because we can't remove during iteration. + remove_wps = [] + for req_wp in breakpoints: + wp = watchpoint_map.get(req_wp["dataId"]) + if wp is None: + new_wps_to_add.append(req_wp) + # If wp with dataId exist; but something about it has changed + # (another condition for instance), delete it, so it can be set + # new with the new spec. + elif not wp.is_same(req_wp["dataId"], req_wp["accessType"], + req_wp["condition"], req_wp["hitCondition"]): + remove_wps.append(wp.dataId) + new_wps_to_add.append(req_wp) + + # Delete wps that are not found in `breakpoints` request + for wp in watchpoint_map.values(): + if wp.dataId not in set([x["dataId"] for x in breakpoints]): + # tells gdb to remove wp + remove_wps.append(wp.dataId) + + for remove_wp in remove_wps: + watchpoint_map[remove_wp].gdb_wp.delete() + del watchpoint_map[remove_wp] + + # Set the new watchpoints + for req_wp in new_wps_to_add: + wp = DataBreakpoint(dataId=req_wp["dataId"], type=req_wp["accessType"], + condition=req_wp["condition"], + hitCondition=req_wp["hitCondition"]) + watchpoint_map[wp.dataId] = wp + + return [x.to_object() for x in watchpoint_map.values()] + +def _sanitize_wp_input(breakpoints: Sequence) -> Sequence: + """Force the request arg DataBreakpoints to contain these attributes""" + res = [] + for wp in breakpoints: + if not hasattr(wp, "condition"): + wp["condition"] = None + if not hasattr(wp, "hitCondition"): + wp["hitCondition"] = None + res.append(wp) + return res + +@request("setDataBreakpoints") +@capability("supportsDataBreakpoints") +def watchpoints(*, breakpoints: Sequence, **args): + bps = _sanitize_wp_input(breakpoints=breakpoints) + result = send_gdb_with_response(lambda: _set_watchpoint(bps)) + return { "breakpoints": result } diff --git a/gdb/python/lib/gdb/dap/frames.py b/gdb/python/lib/gdb/dap/frames.py index 08209d0b361..6102352e2e5 100644 --- a/gdb/python/lib/gdb/dap/frames.py +++ b/gdb/python/lib/gdb/dap/frames.py @@ -48,7 +48,7 @@ def frame_id(frame): @in_gdb_thread -def frame_for_id(id): +def frame_for_id(id) -> gdb.Frame: """Given a frame identifier ID, return the corresponding frame.""" global _all_frames return _all_frames[id] diff --git a/gdb/python/lib/gdb/dap/varref.py b/gdb/python/lib/gdb/dap/varref.py index 23f18d647c3..611d30b6a1d 100644 --- a/gdb/python/lib/gdb/dap/varref.py +++ b/gdb/python/lib/gdb/dap/varref.py @@ -22,7 +22,6 @@ from abc import abstractmethod # A list of all the variable references created during this pause. all_variables = [] - # When the inferior is re-started, we erase all variable references. # See the section "Lifetime of Objects References" in the spec. @in_gdb_thread @@ -101,15 +100,19 @@ class BaseReference: for idx in range(start, start + count): if self.children[idx] is None: (name, value) = self.fetch_one_child(idx) - self.children[idx] = VariableReference(name, value) + parent_ref = self.ref if isinstance(self, VariableReference) else -1 + self.children[idx] = VariableReference(name=name, value=value, parent_var_ref=parent_ref) result.append(self.children[idx]) return result + @abstractmethod + def is_variable(self): + return False class VariableReference(BaseReference): """Concrete subclass of BaseReference that handles gdb.Value.""" - def __init__(self, name, value, result_name="value"): + def __init__(self, name, value, parent_var_ref=-1, result_name="value"): """Initializer. NAME is the name of this reference, see superclass. @@ -120,6 +123,7 @@ class VariableReference(BaseReference): self.value = value self.printer = gdb.printing.make_visualizer(value) self.result_name = result_name + self.parent_var_ref = parent_var_ref # We cache all the children we create. self.child_cache = None if not hasattr(self.printer, "children"): @@ -174,6 +178,8 @@ class VariableReference(BaseReference): def fetch_one_child(self, idx): return self.cache_children()[idx] + def is_variable(self): + return True @in_gdb_thread def find_variable(ref): -- 2.40.1