diff options
Diffstat (limited to 'gdb/python/lib')
53 files changed, 480 insertions, 291 deletions
diff --git a/gdb/python/lib/gdb/FrameDecorator.py b/gdb/python/lib/gdb/FrameDecorator.py index 5cdfbe1..fa6effa 100644 --- a/gdb/python/lib/gdb/FrameDecorator.py +++ b/gdb/python/lib/gdb/FrameDecorator.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2024 Free Software Foundation, Inc. +# Copyright (C) 2013-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -174,7 +174,7 @@ class FrameDecorator(_FrameDecoratorBase): sub-classed from FrameDecorator. If Decorator1 just overrides the 'function' method, then all of the other methods are carried out by the super-class FrameDecorator. But Decorator2 may have - overriden other methods, so FrameDecorator will look at the + overridden other methods, so FrameDecorator will look at the 'base' parameter and defer to that class's methods. And so on, down the chain.""" diff --git a/gdb/python/lib/gdb/FrameIterator.py b/gdb/python/lib/gdb/FrameIterator.py index 75176c3..54534fe 100644 --- a/gdb/python/lib/gdb/FrameIterator.py +++ b/gdb/python/lib/gdb/FrameIterator.py @@ -1,4 +1,4 @@ -# Copyright (C) 2013-2024 Free Software Foundation, Inc. +# Copyright (C) 2013-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py index 146a963..cedd897 100644 --- a/gdb/python/lib/gdb/__init__.py +++ b/gdb/python/lib/gdb/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2024 Free Software Foundation, Inc. +# Copyright (C) 2010-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,17 +19,22 @@ import sys import threading import traceback from contextlib import contextmanager +from importlib import reload -# Python 3 moved "reload" -if sys.version_info >= (3, 4): - from importlib import reload -else: - from imp import reload - -import _gdb - +# The star import imports _gdb names. When the names are used locally, they +# trigger F405 warnings unless added to the explicit import list. # Note that two indicators are needed here to silence flake8. from _gdb import * # noqa: F401,F403 +from _gdb import ( + STDERR, + STDOUT, + Command, + execute, + flush, + parameter, + selected_inferior, + write, +) # isort: split @@ -60,14 +65,14 @@ class _GdbFile(object): self.write(line) def flush(self): - _gdb.flush(stream=self.stream) + flush(stream=self.stream) def write(self, s): - _gdb.write(s, stream=self.stream) + write(s, stream=self.stream) -sys.stdout = _GdbFile(_gdb.STDOUT) -sys.stderr = _GdbFile(_gdb.STDERR) +sys.stdout = _GdbFile(STDOUT) +sys.stderr = _GdbFile(STDERR) # Default prompt hook does nothing. prompt_hook = None @@ -189,7 +194,7 @@ def GdbSetPythonDirectory(dir): def current_progspace(): "Return the current Progspace." - return _gdb.selected_inferior().progspace + return selected_inferior().progspace def objfiles(): @@ -226,14 +231,14 @@ def set_parameter(name, value): value = "on" else: value = "off" - _gdb.execute("set " + name + " " + str(value), to_string=True) + execute("set " + name + " " + str(value), to_string=True) @contextmanager def with_parameter(name, value): """Temporarily set the GDB parameter NAME to VALUE. Note that this is a context manager.""" - old_value = _gdb.parameter(name) + old_value = parameter(name) set_parameter(name, value) try: # Nothing that useful to return. @@ -392,3 +397,121 @@ def _handle_missing_objfile(pspace, buildid, filename): return _handle_missing_files( pspace, "objfile", lambda h: h(pspace, buildid, filename) ) + + +class ParameterPrefix: + # A wrapper around gdb.Command for creating set/show prefixes. + # + # When creating a gdb.Parameter sub-classes, it is sometimes necessary + # to first create a gdb.Command object in order to create the needed + # command prefix. However, for parameters, we actually need two + # prefixes, a 'set' prefix, and a 'show' prefix. With this helper + # class, a single instance of this class will create both prefixes at + # once. + # + # It is important that this class-level documentation not be a __doc__ + # string. Users are expected to sub-class this ParameterPrefix class + # and add their own documentation. If they don't, then GDB will + # generate a suitable doc string. But, if this (parent) class has a + # __doc__ string of its own, then sub-classes will inherit that __doc__ + # string, and GDB will not understand that it needs to generate one. + + class _PrefixCommand(Command): + """A gdb.Command used to implement both the set and show prefixes. + + This documentation string is not used as the prefix command + documentation as it is overridden in the __init__ method below.""" + + # This private method is connected to the 'invoke' attribute within + # this _PrefixCommand object if the containing ParameterPrefix + # object has an invoke_set or invoke_show method. + # + # This method records within self.__delegate which _PrefixCommand + # object is currently active, and then calls the correct invoke + # method on the delegat object (the ParameterPrefix sub-class + # object). + # + # Recording the currently active _PrefixCommand object is important; + # if from the invoke method the user calls dont_repeat, then this is + # forwarded to the currently active _PrefixCommand object. + def __invoke(self, args, from_tty): + + # A helper class for use as part of a Python 'with' block. + # Records which gdb.Command object is currently running its + # invoke method. + class MarkActiveCallback: + # The CMD is a _PrefixCommand object, and the DELEGATE is + # the ParameterPrefix class, or sub-class object. At this + # point we simple record both of these within the + # MarkActiveCallback object. + def __init__(self, cmd, delegate): + self.__cmd = cmd + self.__delegate = delegate + + # Record the currently active _PrefixCommand object within + # the outer ParameterPrefix sub-class object. + def __enter__(self): + self.__delegate.active_prefix = self.__cmd + + # Once the invoke method has completed, then clear the + # _PrefixCommand object that was stored into the outer + # ParameterPrefix sub-class object. + def __exit__(self, exception_type, exception_value, traceback): + self.__delegate.active_prefix = None + + # The self.__cb attribute is set when the _PrefixCommand object + # is created, and is either invoke_set or invoke_show within the + # ParameterPrefix sub-class object. + assert callable(self.__cb) + + # Record the currently active _PrefixCommand object within the + # ParameterPrefix sub-class object, then call the relevant + # invoke method within the ParameterPrefix sub-class object. + with MarkActiveCallback(self, self.__delegate): + self.__cb(args, from_tty) + + @staticmethod + def __find_callback(delegate, mode): + """The MODE is either 'set' or 'show'. Look for an invoke_MODE method + on DELEGATE, if a suitable method is found, then return it, otherwise, + return None. + """ + cb = getattr(delegate, "invoke_" + mode, None) + if callable(cb): + return cb + return None + + def __init__(self, mode, name, cmd_class, delegate, doc=None): + """Setup this gdb.Command. Mode is a string, either 'set' or 'show'. + NAME is the name for this prefix command, that is, the + words that appear after both 'set' and 'show' in the + command name. CMD_CLASS is the usual enum. And DELEGATE + is the gdb.ParameterPrefix object this prefix is part of. + """ + assert mode == "set" or mode == "show" + if doc is None: + self.__doc__ = delegate.__doc__ + else: + self.__doc__ = doc + self.__cb = self.__find_callback(delegate, mode) + self.__delegate = delegate + if self.__cb is not None: + self.invoke = self.__invoke + super().__init__(mode + " " + name, cmd_class, prefix=True) + + def __init__(self, name, cmd_class, doc=None): + """Create a _PrefixCommand for both the set and show prefix commands. + NAME is the command name without either the leading 'set ' or + 'show ' strings, and CMD_CLASS is the usual enum value. + """ + self.active_prefix = None + self._set_prefix_cmd = self._PrefixCommand("set", name, cmd_class, self, doc) + self._show_prefix_cmd = self._PrefixCommand("show", name, cmd_class, self, doc) + + # When called from within an invoke method the self.active_prefix + # attribute should be set to a gdb.Command sub-class (a _PrefixCommand + # object, see above). Forward the dont_repeat call to this object to + # register the actual command as none repeating. + def dont_repeat(self): + if self.active_prefix is not None: + self.active_prefix.dont_repeat() diff --git a/gdb/python/lib/gdb/command/__init__.py b/gdb/python/lib/gdb/command/__init__.py index f1b13bd..3688152 100644 --- a/gdb/python/lib/gdb/command/__init__.py +++ b/gdb/python/lib/gdb/command/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2010-2024 Free Software Foundation, Inc. +# Copyright (C) 2010-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/command/explore.py b/gdb/python/lib/gdb/command/explore.py index e359fa5..6107338 100644 --- a/gdb/python/lib/gdb/command/explore.py +++ b/gdb/python/lib/gdb/command/explore.py @@ -1,5 +1,5 @@ # GDB 'explore' command. -# Copyright (C) 2012-2024 Free Software Foundation, Inc. +# Copyright (C) 2012-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/command/frame_filters.py b/gdb/python/lib/gdb/command/frame_filters.py index 4e1b320..be7be9a 100644 --- a/gdb/python/lib/gdb/command/frame_filters.py +++ b/gdb/python/lib/gdb/command/frame_filters.py @@ -1,5 +1,5 @@ # Frame-filter commands. -# Copyright (C) 2013-2024 Free Software Foundation, Inc. +# Copyright (C) 2013-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/command/missing_files.py b/gdb/python/lib/gdb/command/missing_files.py index 463853b..09d9684 100644 --- a/gdb/python/lib/gdb/command/missing_files.py +++ b/gdb/python/lib/gdb/command/missing_files.py @@ -1,6 +1,6 @@ # Missing debug and objfile related commands. # -# Copyright 2023-2024 Free Software Foundation, Inc. +# Copyright 2023-2025 Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/command/pretty_printers.py b/gdb/python/lib/gdb/command/pretty_printers.py index cb9b9f3..f62d329 100644 --- a/gdb/python/lib/gdb/command/pretty_printers.py +++ b/gdb/python/lib/gdb/command/pretty_printers.py @@ -1,5 +1,5 @@ # Pretty-printer commands. -# Copyright (C) 2010-2024 Free Software Foundation, Inc. +# Copyright (C) 2010-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/command/prompt.py b/gdb/python/lib/gdb/command/prompt.py index 2cfb25d..6574c83 100644 --- a/gdb/python/lib/gdb/command/prompt.py +++ b/gdb/python/lib/gdb/command/prompt.py @@ -1,5 +1,5 @@ # Extended prompt. -# Copyright (C) 2011-2024 Free Software Foundation, Inc. +# Copyright (C) 2011-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/command/type_printers.py b/gdb/python/lib/gdb/command/type_printers.py index a2be226..9fc654c 100644 --- a/gdb/python/lib/gdb/command/type_printers.py +++ b/gdb/python/lib/gdb/command/type_printers.py @@ -1,5 +1,5 @@ # Type printer commands. -# Copyright (C) 2010-2024 Free Software Foundation, Inc. +# Copyright (C) 2010-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/command/unwinders.py b/gdb/python/lib/gdb/command/unwinders.py index b863b33..ffedab8 100644 --- a/gdb/python/lib/gdb/command/unwinders.py +++ b/gdb/python/lib/gdb/command/unwinders.py @@ -1,5 +1,5 @@ # Unwinder commands. -# Copyright 2015-2024 Free Software Foundation, Inc. +# Copyright 2015-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/command/xmethods.py b/gdb/python/lib/gdb/command/xmethods.py index f786227..719c146 100644 --- a/gdb/python/lib/gdb/command/xmethods.py +++ b/gdb/python/lib/gdb/command/xmethods.py @@ -1,5 +1,5 @@ # Xmethod commands. -# Copyright 2013-2024 Free Software Foundation, Inc. +# Copyright 2013-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -37,7 +37,7 @@ def parse_xm_command_args(arg): Returns: A 3-tuple: (<locus matching regular expression>, <matcher matching regular expression>, - <name matching regular experession>) + <name matching regular expression>) """ argv = gdb.string_to_argv(arg) argc = len(argv) diff --git a/gdb/python/lib/gdb/dap/__init__.py b/gdb/python/lib/gdb/dap/__init__.py index 145aeb6..1c3cf8e 100644 --- a/gdb/python/lib/gdb/dap/__init__.py +++ b/gdb/python/lib/gdb/dap/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,6 +26,7 @@ from . import startup # server object. "F401" is the flake8 "imported but unused" code. from . import breakpoint # noqa: F401 from . import bt # noqa: F401 +from . import completions # noqa: F401 from . import disassemble # noqa: F401 from . import evaluate # noqa: F401 from . import launch # noqa: F401 @@ -95,5 +96,4 @@ def pre_command_loop(): # These are handy for bug reports. startup.exec_and_log("show version") startup.exec_and_log("show configuration") - global server startup.start_dap(server.main_loop) diff --git a/gdb/python/lib/gdb/dap/breakpoint.py b/gdb/python/lib/gdb/dap/breakpoint.py index f0fe073..4d4ca18 100644 --- a/gdb/python/lib/gdb/dap/breakpoint.py +++ b/gdb/python/lib/gdb/dap/breakpoint.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -51,7 +51,6 @@ def suppress_new_breakpoint_event(): @in_gdb_thread def _bp_modified(event): - global _suppress_bp if not _suppress_bp: send_event( "breakpoint", @@ -64,7 +63,6 @@ def _bp_modified(event): @in_gdb_thread def _bp_created(event): - global _suppress_bp if not _suppress_bp: send_event( "breakpoint", @@ -77,7 +75,6 @@ def _bp_created(event): @in_gdb_thread def _bp_deleted(event): - global _suppress_bp if not _suppress_bp: send_event( "breakpoint", @@ -151,7 +148,6 @@ def _remove_entries(table, *names): # the breakpoint. @in_gdb_thread def _set_breakpoints_callback(kind, specs, creator): - global breakpoint_map # Try to reuse existing breakpoints if possible. if kind in breakpoint_map: saved_map = breakpoint_map[kind] @@ -218,11 +214,11 @@ class _PrintBreakpoint(gdb.Breakpoint): def __init__(self, logMessage, **args): super().__init__(**args) # Split the message up for easier processing. - self.message = re.split("{(.*?)}", logMessage) + self._message = re.split("{(.*?)}", logMessage) def stop(self): output = "" - for idx, item in enumerate(self.message): + for idx, item in enumerate(self._message): if idx % 2 == 0: # Even indices are plain text. output += item @@ -330,7 +326,7 @@ def _rewrite_fn_breakpoint( } -@request("setFunctionBreakpoints") +@request("setFunctionBreakpoints", expect_stopped=False) @capability("supportsFunctionBreakpoints") def set_fn_breakpoint(*, breakpoints: Sequence, **args): specs = [_rewrite_fn_breakpoint(**bp) for bp in breakpoints] @@ -363,7 +359,7 @@ def _rewrite_insn_breakpoint( } -@request("setInstructionBreakpoints") +@request("setInstructionBreakpoints", expect_stopped=False) @capability("supportsInstructionBreakpoints") def set_insn_breakpoints( *, breakpoints: Sequence, offset: Optional[int] = None, **args @@ -414,7 +410,7 @@ def _rewrite_exception_breakpoint( } -@request("setExceptionBreakpoints") +@request("setExceptionBreakpoints", expect_stopped=False) @capability("supportsExceptionFilterOptions") @capability( "exceptionBreakpointFilters", diff --git a/gdb/python/lib/gdb/dap/bt.py b/gdb/python/lib/gdb/dap/bt.py index 0fefa69..41c7d00 100644 --- a/gdb/python/lib/gdb/dap/bt.py +++ b/gdb/python/lib/gdb/dap/bt.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/dap/completions.py b/gdb/python/lib/gdb/dap/completions.py new file mode 100644 index 0000000..e5003ff --- /dev/null +++ b/gdb/python/lib/gdb/dap/completions.py @@ -0,0 +1,63 @@ +# Copyright 2025 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from typing import Optional + +from .frames import select_frame +from .server import capability, import_column, import_line, request +from .startup import exec_mi_and_log + + +@request("completions") +@capability("supportsCompletionsRequest") +@capability("completionTriggerCharacters", [" ", "."]) +def completions( + *, + frameId: Optional[int] = None, + text: str, + column: int, + line: Optional[int] = None, + **extra, +): + if frameId is not None: + select_frame(frameId) + + column = import_column(column) + if line is None: + line = 1 + else: + line = import_line(line) + if text: + text = text.splitlines()[line - 1] + text = text[: column - 1] + else: + text = "" + mi_result = exec_mi_and_log("-complete", text) + result = [] + completion = None + if "completion" in mi_result: + completion = mi_result["completion"] + result.append({"label": completion, "length": len(completion)}) + # If `-complete' finds one match then `completion' and `matches' + # will contain the same one match. + if ( + completion is not None + and len(mi_result["matches"]) == 1 + and completion == mi_result["matches"][0] + ): + return {"targets": result} + for match in mi_result["matches"]: + result.append({"label": match, "length": len(match)}) + return {"targets": result} diff --git a/gdb/python/lib/gdb/dap/disassemble.py b/gdb/python/lib/gdb/dap/disassemble.py index 5389803..42cad3e 100644 --- a/gdb/python/lib/gdb/dap/disassemble.py +++ b/gdb/python/lib/gdb/dap/disassemble.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -26,30 +26,30 @@ class _BlockTracker: # Map from PC to symbol names. A given PC is assumed to have # just one label -- DAP wouldn't let us return multiple labels # anyway. - self.labels = {} + self._labels = {} # Blocks that have already been handled. - self.blocks = set() + self._blocks = set() # Add a gdb.Block and its superblocks, ignoring the static and # global block. BLOCK can also be None, which is ignored. def add_block(self, block): while block is not None: - if block.is_static or block.is_global or block in self.blocks: + if block.is_static or block.is_global or block in self._blocks: return - self.blocks.add(block) + self._blocks.add(block) if block.function is not None: - self.labels[block.start] = block.function.name + self._labels[block.start] = block.function.name for sym in block: if sym.addr_class == gdb.SYMBOL_LOC_LABEL: - self.labels[int(sym.value())] = sym.name + self._labels[int(sym.value())] = sym.name block = block.superblock # Add PC to this tracker. Update RESULT as appropriate with # information about the source and any label. def add_pc(self, pc, result): self.add_block(gdb.block_for_pc(pc)) - if pc in self.labels: - result["symbol"] = self.labels[pc] + if pc in self._labels: + result["symbol"] = self._labels[pc] sal = gdb.find_pc_line(pc) if sal.symtab is not None: if sal.line != 0: diff --git a/gdb/python/lib/gdb/dap/evaluate.py b/gdb/python/lib/gdb/dap/evaluate.py index 34843f4..fcbcc99 100644 --- a/gdb/python/lib/gdb/dap/evaluate.py +++ b/gdb/python/lib/gdb/dap/evaluate.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -69,7 +69,7 @@ def _repl(command, frame_id): } -@request("evaluate") +@request("evaluate", defer_events=False) @capability("supportsEvaluateForHovers") @capability("supportsValueFormattingOptions") def eval_request( @@ -110,7 +110,7 @@ def variables( @capability("supportsSetExpression") -@request("setExpression") +@request("setExpression", defer_events=False) def set_expression( *, expression: str, value: str, frameId: Optional[int] = None, format=None, **args ): @@ -126,7 +126,7 @@ def set_expression( @capability("supportsSetVariable") -@request("setVariable") +@request("setVariable", defer_events=False) def set_variable( *, variablesReference: int, name: str, value: str, format=None, **args ): diff --git a/gdb/python/lib/gdb/dap/events.py b/gdb/python/lib/gdb/dap/events.py index 2e6fe98..e8f2655 100644 --- a/gdb/python/lib/gdb/dap/events.py +++ b/gdb/python/lib/gdb/dap/events.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -17,12 +17,12 @@ import gdb from .modules import is_module, make_module from .scopes import set_finish_value -from .server import send_event, send_event_maybe_later +from .server import send_event from .startup import exec_and_log, in_gdb_thread, log # True when the inferior is thought to be running, False otherwise. # This may be accessed from any thread, which can be racy. However, -# this unimportant because this global is only used for the +# this is unimportant because this global is only used for the # 'notStopped' response, which itself is inherently racy. inferior_running = False @@ -238,10 +238,9 @@ def _on_stop(event): ): obj["reason"] = "pause" else: - global stop_reason_map obj["reason"] = stop_reason_map[event.details["reason"]] _expected_pause = False - send_event_maybe_later("stopped", obj) + send_event("stopped", obj) # This keeps a bit of state between the start of an inferior call and diff --git a/gdb/python/lib/gdb/dap/frames.py b/gdb/python/lib/gdb/dap/frames.py index f4e6565..4dacb87 100644 --- a/gdb/python/lib/gdb/dap/frames.py +++ b/gdb/python/lib/gdb/dap/frames.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -53,12 +53,11 @@ gdb.events.cont.connect(_clear_frame_ids) @in_gdb_thread def frame_for_id(id): """Given a frame identifier ID, return the corresponding frame.""" - global thread_ids if id in thread_ids: thread_id = thread_ids[id] if thread_id != gdb.selected_thread().global_num: set_thread(thread_id) - global _all_frames + return _all_frames[id] @@ -75,16 +74,16 @@ def select_frame(id): # what is needed for the current callers. class _MemoizingIterator: def __init__(self, iterator): - self.iterator = iterator - self.seen = [] + self._iterator = iterator + self._seen = [] def __iter__(self): # First the memoized items. - for item in self.seen: + for item in self._seen: yield item # Now memoize new items. - for item in self.iterator: - self.seen.append(item) + for item in self._iterator: + self._seen.append(item) yield item @@ -103,10 +102,8 @@ def _frame_id_generator(): # Helper function to assign an ID to a frame. def get_id(frame): - global _all_frames num = len(_all_frames) _all_frames.append(frame) - global thread_ids thread_ids[num] = gdb.selected_thread().global_num return num @@ -128,7 +125,6 @@ def _frame_id_generator(): @in_gdb_thread def _get_frame_iterator(): thread_id = gdb.selected_thread().global_num - global _iter_map if thread_id not in _iter_map: _iter_map[thread_id] = _MemoizingIterator(_frame_id_generator()) return _iter_map[thread_id] diff --git a/gdb/python/lib/gdb/dap/globalvars.py b/gdb/python/lib/gdb/dap/globalvars.py index 104b242..9d64d28 100644 --- a/gdb/python/lib/gdb/dap/globalvars.py +++ b/gdb/python/lib/gdb/dap/globalvars.py @@ -1,4 +1,4 @@ -# Copyright 2024 Free Software Foundation, Inc. +# Copyright 2024-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -37,8 +37,8 @@ gdb.events.cont.connect(clear) class _Globals(BaseReference): def __init__(self, filename, var_list): super().__init__("Globals") - self.filename = filename - self.var_list = var_list + self._filename = filename + self._var_list = var_list def to_object(self): result = super().to_object() @@ -46,8 +46,8 @@ class _Globals(BaseReference): # How would we know? result["expensive"] = False result["namedVariables"] = self.child_count() - if self.filename is not None: - result["source"] = make_source(self.filename) + if self._filename is not None: + result["source"] = make_source(self._filename) return result def has_children(self): @@ -56,11 +56,11 @@ class _Globals(BaseReference): return True def child_count(self): - return len(self.var_list) + return len(self._var_list) @in_gdb_thread def fetch_one_child(self, idx): - sym = self.var_list[idx] + sym = self._var_list[idx] return (sym.name, sym.value()) @@ -78,7 +78,6 @@ def get_global_scope(frame): except RuntimeError: return None - global _id_to_scope block = block.static_block if block in _id_to_scope: return _id_to_scope[block] diff --git a/gdb/python/lib/gdb/dap/io.py b/gdb/python/lib/gdb/dap/io.py index 03031a7..45890da 100644 --- a/gdb/python/lib/gdb/dap/io.py +++ b/gdb/python/lib/gdb/dap/io.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/dap/launch.py b/gdb/python/lib/gdb/dap/launch.py index fc1890c..8ac4c77 100644 --- a/gdb/python/lib/gdb/dap/launch.py +++ b/gdb/python/lib/gdb/dap/launch.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -164,7 +164,6 @@ def attach( @request("configurationDone", on_dap_thread=True) def config_done(**args): # Handle the launch or attach. - global _launch_or_attach_promise if _launch_or_attach_promise is None: raise DAPException("launch or attach not specified") # Resolve the launch or attach, but only after the diff --git a/gdb/python/lib/gdb/dap/locations.py b/gdb/python/lib/gdb/dap/locations.py index 1ef5a34..fffc038 100644 --- a/gdb/python/lib/gdb/dap/locations.py +++ b/gdb/python/lib/gdb/dap/locations.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 Free Software Foundation, Inc. +# Copyright 2023-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/dap/memory.py b/gdb/python/lib/gdb/dap/memory.py index 4aa4996..d0f8825 100644 --- a/gdb/python/lib/gdb/dap/memory.py +++ b/gdb/python/lib/gdb/dap/memory.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 Free Software Foundation, Inc. +# Copyright 2023-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/dap/modules.py b/gdb/python/lib/gdb/dap/modules.py index 69e5a40..b06f771 100644 --- a/gdb/python/lib/gdb/dap/modules.py +++ b/gdb/python/lib/gdb/dap/modules.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 Free Software Foundation, Inc. +# Copyright 2023-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/dap/next.py b/gdb/python/lib/gdb/dap/next.py index 7e06b1b..898fff1 100644 --- a/gdb/python/lib/gdb/dap/next.py +++ b/gdb/python/lib/gdb/dap/next.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,7 +16,7 @@ import gdb from .events import exec_and_expect_stop -from .server import capability, request, send_gdb, send_gdb_with_response +from .server import capability, request from .startup import in_gdb_thread from .state import set_thread @@ -73,19 +73,14 @@ def step_in( exec_and_expect_stop(cmd) -@request("stepOut", defer_stop_events=True) +@request("stepOut") def step_out(*, threadId: int, singleThread: bool = False, **args): _handle_thread_step(threadId, singleThread, True) exec_and_expect_stop("finish &", propagate_exception=True) -# This is a server-side request because it is funny: it wants to -# 'continue' but also return a result, which precludes using -# response=False. Using 'continue &' would mostly work ok, but this -# yields races when a stop occurs before the response is sent back to -# the client. -@request("continue", on_dap_thread=True) +@request("continue") def continue_request(*, threadId: int, singleThread: bool = False, **args): - locked = send_gdb_with_response(lambda: _handle_thread_step(threadId, singleThread)) - send_gdb(lambda: exec_and_expect_stop("continue")) + locked = _handle_thread_step(threadId, singleThread) + exec_and_expect_stop("continue &") return {"allThreadsContinued": not locked} diff --git a/gdb/python/lib/gdb/dap/pause.py b/gdb/python/lib/gdb/dap/pause.py index d874a60..c254e45 100644 --- a/gdb/python/lib/gdb/dap/pause.py +++ b/gdb/python/lib/gdb/dap/pause.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/dap/scopes.py b/gdb/python/lib/gdb/dap/scopes.py index 221ae35..7ce3a7f 100644 --- a/gdb/python/lib/gdb/dap/scopes.py +++ b/gdb/python/lib/gdb/dap/scopes.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,7 +22,7 @@ from .sources import make_source from .startup import in_gdb_thread from .varref import BaseReference -# Map DAP frame IDs to scopes. This ensures that scopes are re-used. +# Map DAP frame IDs to scopes. This ensures that scopes are reused. frame_to_scope = {} @@ -78,19 +78,19 @@ def symbol_value(sym, frame): class _ScopeReference(BaseReference): def __init__(self, name, hint, frameId: int, var_list): super().__init__(name) - self.hint = hint - self.frameId = frameId + self._hint = hint + self._frameId = frameId # VAR_LIST might be any kind of iterator, but it's convenient # here if it is just a collection. - self.var_list = tuple(var_list) + self._var_list = tuple(var_list) def to_object(self): result = super().to_object() - result["presentationHint"] = self.hint + result["presentationHint"] = self._hint # How would we know? result["expensive"] = False result["namedVariables"] = self.child_count() - frame = frame_for_id(self.frameId) + frame = frame_for_id(self._frameId) if frame.line() is not None: result["line"] = export_line(frame.line()) filename = frame.filename() @@ -102,11 +102,11 @@ class _ScopeReference(BaseReference): return True def child_count(self): - return len(self.var_list) + return len(self._var_list) @in_gdb_thread def fetch_one_child(self, idx): - return symbol_value(self.var_list[idx], frame_for_id(self.frameId)) + return symbol_value(self._var_list[idx], frame_for_id(self._frameId)) # A _ScopeReference that wraps the 'finish' value. Note that this @@ -120,7 +120,6 @@ class _FinishScopeReference(_ScopeReference): def fetch_one_child(self, idx): assert idx == 0 - global _last_return_value return ("(return)", _last_return_value) @@ -136,17 +135,15 @@ class _RegisterReference(_ScopeReference): @in_gdb_thread def fetch_one_child(self, idx): return ( - self.var_list[idx].name, - frame_for_id(self.frameId) + self._var_list[idx].name, + frame_for_id(self._frameId) .inferior_frame() - .read_register(self.var_list[idx]), + .read_register(self._var_list[idx]), ) @request("scopes") def scopes(*, frameId: int, **extra): - global _last_return_value - global frame_to_scope if frameId in frame_to_scope: scopes = frame_to_scope[frameId] else: diff --git a/gdb/python/lib/gdb/dap/server.py b/gdb/python/lib/gdb/dap/server.py index 6f3af73..7dab582 100644 --- a/gdb/python/lib/gdb/dap/server.py +++ b/gdb/python/lib/gdb/dap/server.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -49,6 +49,7 @@ _server = None # This is set by the initialize request and is used when rewriting # line numbers. _lines_start_at_1 = False +_columns_start_at_1 = False class DeferredRequest: @@ -73,6 +74,13 @@ class DeferredRequest: self._result = result @in_dap_thread + def defer_events(self): + """Return True if events should be deferred during execution. + + This may be overridden by subclasses.""" + return True + + @in_dap_thread def invoke(self): """Implement the deferred request. @@ -94,7 +102,10 @@ class DeferredRequest: """ with _server.canceller.current_request(self._req): + if self.defer_events(): + _server.set_defer_events() _server.invoke_request(self._req, self._result, self.invoke) + _server.emit_pending_events() # A subclass of Exception that is used solely for reporting that a @@ -115,10 +126,10 @@ class CancellationHandler: # The request currently being handled, or None. self.in_flight_dap_thread = None self.in_flight_gdb_thread = None - self.reqs = [] + self._reqs = [] # A set holding the request IDs of all deferred requests that # are still unresolved. - self.deferred_ids = set() + self._deferred_ids = set() @contextmanager def current_request(self, req): @@ -138,7 +149,7 @@ class CancellationHandler: def defer_request(self, req): """Indicate that the request REQ has been deferred.""" with self.lock: - self.deferred_ids.add(req) + self._deferred_ids.add(req) def request_finished(self, req): """Indicate that the request REQ is finished. @@ -150,7 +161,7 @@ class CancellationHandler: with self.lock: # Use discard here, not remove, because this is called # regardless of whether REQ was deferred. - self.deferred_ids.discard(req) + self._deferred_ids.discard(req) def check_cancel(self, req): """Check whether request REQ is cancelled. @@ -163,15 +174,15 @@ class CancellationHandler: deferred = [] try: # If the request is cancelled, don't execute the region. - while len(self.reqs) > 0 and self.reqs[0] <= req: + while len(self._reqs) > 0 and self._reqs[0] <= req: # In most cases, if we see a cancellation request # on the heap that is before REQ, we can just # ignore it -- we missed our chance to cancel that # request. - next_id = heapq.heappop(self.reqs) + next_id = heapq.heappop(self._reqs) if next_id == req: raise KeyboardInterrupt() - elif next_id in self.deferred_ids: + elif next_id in self._deferred_ids: # We could be in a situation where we're # processing request 23, but request 18 is # still deferred. In this case, popping @@ -180,7 +191,7 @@ class CancellationHandler: deferred.append(next_id) finally: for x in deferred: - heapq.heappush(self.reqs, x) + heapq.heappush(self._reqs, x) def cancel(self, req): """Call to cancel a request. @@ -198,7 +209,7 @@ class CancellationHandler: # the weird property that a request can be cancelled # before it is even sent. It didn't seem worthwhile # to try to check for this. - heapq.heappush(self.reqs, req) + heapq.heappush(self._reqs, req) @contextmanager def interruptable_region(self, req): @@ -225,20 +236,20 @@ class Server: """The DAP server class.""" def __init__(self, in_stream, out_stream, child_stream): - self.in_stream = in_stream - self.out_stream = out_stream - self.child_stream = child_stream - self.delayed_fns_lock = threading.Lock() - self.defer_stop_events = False - self.delayed_fns = [] + self._in_stream = in_stream + self._out_stream = out_stream + self._child_stream = child_stream + self._delayed_fns_lock = threading.Lock() + self._defer_events = False + self._delayed_fns = [] # This queue accepts JSON objects that are then sent to the # DAP client. Writing is done in a separate thread to avoid # blocking the read loop. - self.write_queue = DAPQueue() + self._write_queue = DAPQueue() # Reading is also done in a separate thread, and a queue of # requests is kept. - self.read_queue = DAPQueue() - self.done = False + self._read_queue = DAPQueue() + self._done = False self.canceller = CancellationHandler() global _server _server = self @@ -306,7 +317,6 @@ class Server: args = {} def fn(): - global _commands return _commands[params["command"]](**args) self.invoke_request(req, result, fn) @@ -315,8 +325,8 @@ class Server: # is run in its own thread. def _read_inferior_output(self): while True: - line = self.child_stream.readline() - self.send_event( + line = self._child_stream.readline() + self.send_event_maybe_later( "output", { "category": "stdout", @@ -327,7 +337,7 @@ class Server: # Send OBJ to the client, logging first if needed. def _send_json(self, obj): log("WROTE: <<<" + json.dumps(obj) + ">>>") - self.write_queue.put(obj) + self._write_queue.put(obj) # This is run in a separate thread and simply reads requests from # the client and puts them into a queue. A separate thread is @@ -335,7 +345,7 @@ class Server: # will normally block, waiting for each request to complete. def _reader_thread(self): while True: - cmd = read_json(self.in_stream) + cmd = read_json(self._in_stream) if cmd is None: break log("READ: <<<" + json.dumps(cmd) + ">>>") @@ -351,9 +361,20 @@ class Server: and "requestId" in cmd["arguments"] ): self.canceller.cancel(cmd["arguments"]["requestId"]) - self.read_queue.put(cmd) + self._read_queue.put(cmd) # When we hit EOF, signal it with None. - self.read_queue.put(None) + self._read_queue.put(None) + + @in_dap_thread + def emit_pending_events(self): + """Emit any pending events.""" + fns = None + with self._delayed_fns_lock: + fns = self._delayed_fns + self._delayed_fns = [] + self._defer_events = False + for fn in fns: + fn() @in_dap_thread def main_loop(self): @@ -361,38 +382,32 @@ class Server: # Before looping, start the thread that writes JSON to the # client, and the thread that reads output from the inferior. start_thread("output reader", self._read_inferior_output) - json_writer = start_json_writer(self.out_stream, self.write_queue) + json_writer = start_json_writer(self._out_stream, self._write_queue) start_thread("JSON reader", self._reader_thread) - while not self.done: - cmd = self.read_queue.get() + while not self._done: + cmd = self._read_queue.get() # A None value here means the reader hit EOF. if cmd is None: break req = cmd["seq"] with self.canceller.current_request(req): self._handle_command(cmd) - fns = None - with self.delayed_fns_lock: - fns = self.delayed_fns - self.delayed_fns = [] - self.defer_stop_events = False - for fn in fns: - fn() + self.emit_pending_events() # Got the terminate request. This is handled by the # JSON-writing thread, so that we can ensure that all # responses are flushed to the client before exiting. - self.write_queue.put(None) + self._write_queue.put(None) json_writer.join() send_gdb("quit") @in_dap_thread - def send_event_later(self, event, body=None): - """Send a DAP event back to the client, but only after the - current request has completed.""" - with self.delayed_fns_lock: - self.delayed_fns.append(lambda: self.send_event(event, body)) + def set_defer_events(self): + """Defer any events until the current request has completed.""" + with self._delayed_fns_lock: + self._defer_events = True - @in_gdb_thread + # Note that this does not need to be run in any particular thread, + # because it uses locks for thread-safety. def send_event_maybe_later(self, event, body=None): """Send a DAP event back to the client, but if a request is in-flight within the dap thread and that request is configured to delay the event, @@ -400,22 +415,22 @@ class Server: the client.""" with self.canceller.lock: if self.canceller.in_flight_dap_thread: - with self.delayed_fns_lock: - if self.defer_stop_events: - self.delayed_fns.append(lambda: self.send_event(event, body)) + with self._delayed_fns_lock: + if self._defer_events: + self._delayed_fns.append(lambda: self._send_event(event, body)) return - self.send_event(event, body) + self._send_event(event, body) @in_dap_thread def call_function_later(self, fn): """Call FN later -- after the current request's response has been sent.""" - with self.delayed_fns_lock: - self.delayed_fns.append(fn) + with self._delayed_fns_lock: + self._delayed_fns.append(fn) # Note that this does not need to be run in any particular thread, # because it just creates an object and writes it to a thread-safe # queue. - def send_event(self, event, body=None): + def _send_event(self, event, body=None): """Send an event to the DAP client. EVENT is the name of the event, a string. BODY is the body of the event, an arbitrary object.""" @@ -432,29 +447,18 @@ class Server: # Just set a flag. This operation is complicated because we # want to write the result of the request before exiting. See # main_loop. - self.done = True + self._done = True def send_event(event, body=None): """Send an event to the DAP client. EVENT is the name of the event, a string. BODY is the body of the event, an arbitrary object.""" - global _server - _server.send_event(event, body) - - -def send_event_maybe_later(event, body=None): - """Send a DAP event back to the client, but if a request is in-flight - within the dap thread and that request is configured to delay the event, - wait until the response has been sent until the event is sent back to - the client.""" - global _server _server.send_event_maybe_later(event, body) def call_function_later(fn): """Call FN later -- after the current request's response has been sent.""" - global _server _server.call_function_later(fn) @@ -479,7 +483,7 @@ def request( response: bool = True, on_dap_thread: bool = False, expect_stopped: bool = True, - defer_stop_events: bool = False + defer_events: bool = True ): """A decorator for DAP requests. @@ -501,9 +505,9 @@ def request( inferior is running. When EXPECT_STOPPED is False, the request will proceed regardless of the inferior's state. - If DEFER_STOP_EVENTS is True, then make sure any stop events sent - during the request processing are not sent to the client until the - response has been sent. + If DEFER_EVENTS is True, then make sure any events sent during the + request processing are not sent to the client until the response + has been sent. """ # Validate the parameters. @@ -526,27 +530,33 @@ def request( # Verify that the function is run on the correct thread. if on_dap_thread: - cmd = in_dap_thread(func) + check_cmd = in_dap_thread(func) else: func = in_gdb_thread(func) if response: - if defer_stop_events: - global _server - if _server is not None: - with _server.delayed_events_lock: - _server.defer_stop_events = True def sync_call(**args): return send_gdb_with_response(lambda: func(**args)) - cmd = sync_call + check_cmd = sync_call else: def non_sync_call(**args): return send_gdb(lambda: func(**args)) - cmd = non_sync_call + check_cmd = non_sync_call + + if defer_events: + + def deferring(**args): + _server.set_defer_events() + return check_cmd(**args) + + cmd = deferring + + else: + cmd = check_cmd # If needed, check that the inferior is not running. This # wrapping is done last, so the check is done first, before @@ -554,7 +564,6 @@ def request( if expect_stopped: cmd = _check_not_running(cmd) - global _commands assert name not in _commands _commands[name] = cmd return cmd @@ -567,7 +576,6 @@ def capability(name, value=True): the DAP capability NAME.""" def wrap(func): - global _capabilities assert name not in _capabilities _capabilities[name] = value return func @@ -580,7 +588,6 @@ def client_bool_capability(name, default=False): If the capability was not specified, or did not have boolean type, DEFAULT is returned. DEFAULT defaults to False.""" - global _server if name in _server.config and isinstance(_server.config[name], bool): return _server.config[name] return default @@ -588,11 +595,12 @@ def client_bool_capability(name, default=False): @request("initialize", on_dap_thread=True) def initialize(**args): - global _server, _capabilities _server.config = args - _server.send_event_later("initialized") + _server.send_event_maybe_later("initialized") global _lines_start_at_1 _lines_start_at_1 = client_bool_capability("linesStartAt1", True) + global _columns_start_at_1 + _columns_start_at_1 = client_bool_capability("columnsStartAt1", True) return _capabilities.copy() @@ -629,19 +637,19 @@ class Invoker(object): """A simple class that can invoke a gdb command.""" def __init__(self, cmd): - self.cmd = cmd + self._cmd = cmd # This is invoked in the gdb thread to run the command. @in_gdb_thread def __call__(self): - exec_and_log(self.cmd) + exec_and_log(self._cmd) class Cancellable(object): def __init__(self, fn, result_q=None): - self.fn = fn - self.result_q = result_q + self._fn = fn + self._result_q = result_q with _server.canceller.lock: self.req = _server.canceller.in_flight_dap_thread @@ -650,13 +658,13 @@ class Cancellable(object): def __call__(self): try: with _server.canceller.interruptable_region(self.req): - val = self.fn() - if self.result_q is not None: - self.result_q.put(val) + val = self._fn() + if self._result_q is not None: + self._result_q.put(val) except (Exception, KeyboardInterrupt) as e: - if self.result_q is not None: + if self._result_q is not None: # Pass result or exception to caller. - self.result_q.put(e) + self._result_q.put(e) elif isinstance(e, KeyboardInterrupt): # Fn was cancelled. pass @@ -698,11 +706,10 @@ def send_gdb_with_response(fn): return val -def export_line(line): +def export_line(line: int) -> int: """Rewrite LINE according to client capability. This applies the linesStartAt1 capability as needed, when sending a line number from gdb to the client.""" - global _lines_start_at_1 if not _lines_start_at_1: # In gdb, lines start at 1, so we only need to change this if # the client starts at 0. @@ -710,13 +717,26 @@ def export_line(line): return line -def import_line(line): +def import_line(line: int) -> int: """Rewrite LINE according to client capability. This applies the linesStartAt1 capability as needed, when the client sends a line number to gdb.""" - global _lines_start_at_1 if not _lines_start_at_1: # In gdb, lines start at 1, so we only need to change this if # the client starts at 0. line = line + 1 return line + + +def export_column(column: int) -> int: + """Rewrite COLUMN according to client capability. + This applies the columnsStartAt1 capability as needed, + when sending a column number from gdb to the client.""" + return column if _columns_start_at_1 else column - 1 + + +def import_column(column: int) -> int: + """Rewrite COLUMN according to client capability. + This applies the columnsStartAt1 capability as needed, + when the client sends a column number to gdb.""" + return column if _columns_start_at_1 else column + 1 diff --git a/gdb/python/lib/gdb/dap/sources.py b/gdb/python/lib/gdb/dap/sources.py index a9f4ea6..efcd799 100644 --- a/gdb/python/lib/gdb/dap/sources.py +++ b/gdb/python/lib/gdb/dap/sources.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 Free Software Foundation, Inc. +# Copyright 2023-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -37,7 +37,6 @@ def make_source(fullname, filename=None): FILENAME is the base name; if None (the default), then it is computed from FULLNAME. """ - global _source_map if fullname in _source_map: result = _source_map[fullname] else: @@ -53,7 +52,6 @@ def make_source(fullname, filename=None): global _next_source result["sourceReference"] = _next_source - global _id_map _id_map[_next_source] = result _next_source += 1 @@ -66,12 +64,11 @@ def decode_source(source): """Decode a Source object. Finds and returns the filename of a given Source object.""" - if "path" in source: - return source["path"] - if "sourceReference" not in source: + if "sourceReference" not in source or source["sourceReference"] <= 0: + if "path" in source: + return source["path"] raise DAPException("either 'path' or 'sourceReference' must appear in Source") ref = source["sourceReference"] - global _id_map if ref not in _id_map: raise DAPException("no sourceReference " + str(ref)) return _id_map[ref]["path"] diff --git a/gdb/python/lib/gdb/dap/startup.py b/gdb/python/lib/gdb/dap/startup.py index a3f048b..ab3e8fd 100644 --- a/gdb/python/lib/gdb/dap/startup.py +++ b/gdb/python/lib/gdb/dap/startup.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/dap/state.py b/gdb/python/lib/gdb/dap/state.py index 57ae355..5fdfbb2 100644 --- a/gdb/python/lib/gdb/dap/state.py +++ b/gdb/python/lib/gdb/dap/state.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/dap/threads.py b/gdb/python/lib/gdb/dap/threads.py index e65495b..89046a8 100644 --- a/gdb/python/lib/gdb/dap/threads.py +++ b/gdb/python/lib/gdb/dap/threads.py @@ -1,4 +1,4 @@ -# Copyright 2022-2024 Free Software Foundation, Inc. +# Copyright 2022-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -16,27 +16,32 @@ import gdb from .server import request +from .startup import in_gdb_thread +@in_gdb_thread def _thread_name(thr): if thr.name is not None: return thr.name if thr.details is not None: return thr.details - return None + # Always return a name, as the protocol doesn't allow for nameless + # threads. Use the local thread number here... it doesn't matter + # without multi-inferior but in that case it might make more + # sense. + return f"Thread #{thr.num}" -@request("threads") +@request("threads", expect_stopped=False) def threads(**args): result = [] for thr in gdb.selected_inferior().threads(): - one_result = { - "id": thr.global_num, - } - name = _thread_name(thr) - if name is not None: - one_result["name"] = name - result.append(one_result) + result.append( + { + "id": thr.global_num, + "name": _thread_name(thr), + } + ) return { "threads": result, } diff --git a/gdb/python/lib/gdb/dap/typecheck.py b/gdb/python/lib/gdb/dap/typecheck.py index 55896cc..1496b67 100644 --- a/gdb/python/lib/gdb/dap/typecheck.py +++ b/gdb/python/lib/gdb/dap/typecheck.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 Free Software Foundation, Inc. +# Copyright 2023-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/dap/varref.py b/gdb/python/lib/gdb/dap/varref.py index 0dd9879..8a13c51 100644 --- a/gdb/python/lib/gdb/dap/varref.py +++ b/gdb/python/lib/gdb/dap/varref.py @@ -1,4 +1,4 @@ -# Copyright 2023-2024 Free Software Foundation, Inc. +# Copyright 2023-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -60,8 +60,6 @@ class BaseReference(ABC): This class is just a base class, some methods must be implemented in subclasses. - - The 'ref' field can be used as the variablesReference in the protocol. """ @in_gdb_thread @@ -71,10 +69,9 @@ class BaseReference(ABC): NAME is a string or None. None means this does not have a name, e.g., the result of expression evaluation.""" - global all_variables all_variables.append(self) - self.ref = len(all_variables) - self.name = name + self._ref = len(all_variables) + self._name = name self.reset_children() @in_gdb_thread @@ -83,9 +80,9 @@ class BaseReference(ABC): The resulting object is a starting point that can be filled in further. See the Scope or Variable types in the spec""" - result = {"variablesReference": self.ref if self.has_children() else 0} - if self.name is not None: - result["name"] = str(self.name) + result = {"variablesReference": self._ref if self.has_children() else 0} + if self._name is not None: + result["name"] = str(self._name) return result @abstractmethod @@ -97,13 +94,13 @@ class BaseReference(ABC): """Reset any cached information about the children of this object.""" # A list of all the children. Each child is a BaseReference # of some kind. - self.children = None + self._children = None # Map from the name of a child to a BaseReference. - self.by_name = {} + self._by_name = {} # Keep track of how many duplicates there are of a given name, # so that unique names can be generated. Map from base name # to a count. - self.name_counts = defaultdict(lambda: 1) + self._name_counts = defaultdict(lambda: 1) @abstractmethod def fetch_one_child(self, index): @@ -128,13 +125,13 @@ class BaseReference(ABC): # and # https://github.com/microsoft/debug-adapter-protocol/issues/149 def _compute_name(self, name): - if name in self.by_name: - self.name_counts[name] += 1 + if name in self._by_name: + self._name_counts[name] += 1 # In theory there's no safe way to compute a name, because # a pretty-printer might already be generating names of # that form. In practice I think we should not worry too # much. - name = name + " #" + str(self.name_counts[name]) + name = name + " #" + str(self._name_counts[name]) return name @in_gdb_thread @@ -146,16 +143,16 @@ class BaseReference(ABC): Returns an iterable of some kind.""" if count == 0: count = self.child_count() - if self.children is None: - self.children = [None] * self.child_count() + if self._children is None: + self._children = [None] * self.child_count() for idx in range(start, start + count): - if self.children[idx] is None: + if self._children[idx] is None: (name, value) = self.fetch_one_child(idx) name = self._compute_name(name) var = VariableReference(name, value) - self.children[idx] = var - self.by_name[name] = var - yield self.children[idx] + self._children[idx] = var + self._by_name[name] = var + yield self._children[idx] @in_gdb_thread def find_child_by_name(self, name): @@ -165,8 +162,8 @@ class BaseReference(ABC): # A lookup by name can only be done using names previously # provided to the client, so we can simply rely on the by-name # map here. - if name in self.by_name: - return self.by_name[name] + if name in self._by_name: + return self._by_name[name] raise DAPException("no variable named '" + name + "'") @@ -181,15 +178,15 @@ class VariableReference(BaseReference): RESULT_NAME can be used to change how the simple string result is emitted in the result dictionary.""" super().__init__(name) - self.result_name = result_name - self.value = value + self._result_name = result_name + self._value = value self._update_value() # Internal method to update local data when the value changes. def _update_value(self): self.reset_children() - self.printer = gdb.printing.make_visualizer(self.value) - self.child_cache = None + self._printer = gdb.printing.make_visualizer(self._value) + self._child_cache = None if self.has_children(): self.count = -1 else: @@ -197,32 +194,32 @@ class VariableReference(BaseReference): def assign(self, value): """Assign VALUE to this object and update.""" - self.value.assign(value) + self._value.assign(value) self._update_value() def has_children(self): - return hasattr(self.printer, "children") + return hasattr(self._printer, "children") def cache_children(self): - if self.child_cache is None: + if self._child_cache is None: # This discards all laziness. This could be improved # slightly by lazily evaluating children, but because this # code also generally needs to know the number of # children, it probably wouldn't help much. Note that # this is only needed with legacy (non-ValuePrinter) # printers. - self.child_cache = list(self.printer.children()) - return self.child_cache + self._child_cache = list(self._printer.children()) + return self._child_cache def child_count(self): if self.count is None: return None if self.count == -1: num_children = None - if isinstance(self.printer, gdb.ValuePrinter) and hasattr( - self.printer, "num_children" + if isinstance(self._printer, gdb.ValuePrinter) and hasattr( + self._printer, "num_children" ): - num_children = self.printer.num_children() + num_children = self._printer.num_children() if num_children is None: num_children = len(self.cache_children()) self.count = num_children @@ -230,12 +227,12 @@ class VariableReference(BaseReference): def to_object(self): result = super().to_object() - result[self.result_name] = str(self.printer.to_string()) + result[self._result_name] = str(self._printer.to_string()) num_children = self.child_count() if num_children is not None: if ( - hasattr(self.printer, "display_hint") - and self.printer.display_hint() == "array" + hasattr(self._printer, "display_hint") + and self._printer.display_hint() == "array" ): result["indexedVariables"] = num_children else: @@ -245,18 +242,18 @@ class VariableReference(BaseReference): # changed DAP to allow memory references for any of the # variable response requests, and to lift the restriction # to pointer-to-function from Variable. - if self.value.type.strip_typedefs().code == gdb.TYPE_CODE_PTR: - result["memoryReference"] = hex(int(self.value)) + if self._value.type.strip_typedefs().code == gdb.TYPE_CODE_PTR: + result["memoryReference"] = hex(int(self._value)) if client_bool_capability("supportsVariableType"): - result["type"] = str(self.value.type) + result["type"] = str(self._value.type) return result @in_gdb_thread def fetch_one_child(self, idx): - if isinstance(self.printer, gdb.ValuePrinter) and hasattr( - self.printer, "child" + if isinstance(self._printer, gdb.ValuePrinter) and hasattr( + self._printer, "child" ): - (name, val) = self.printer.child(idx) + (name, val) = self._printer.child(idx) else: (name, val) = self.cache_children()[idx] # A pretty-printer can return something other than a @@ -269,7 +266,7 @@ class VariableReference(BaseReference): @in_gdb_thread def find_variable(ref): """Given a variable reference, return the corresponding variable object.""" - global all_variables + # Variable references are offset by 1. ref = ref - 1 if ref < 0 or ref > len(all_variables): diff --git a/gdb/python/lib/gdb/disassembler.py b/gdb/python/lib/gdb/disassembler.py index 7d0e781..8f8e768 100644 --- a/gdb/python/lib/gdb/disassembler.py +++ b/gdb/python/lib/gdb/disassembler.py @@ -1,4 +1,4 @@ -# Copyright (C) 2021-2024 Free Software Foundation, Inc. +# Copyright (C) 2021-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -81,7 +81,7 @@ def register_disassembler(disassembler, architecture=None): # Call the private _set_enabled function within the # _gdb.disassembler module. This function sets a global flag - # within GDB's C++ code that enables or dissables the Python + # within GDB's C++ code that enables or disables the Python # disassembler functionality, this improves performance of the # disassembler by avoiding unneeded calls into Python when we know # that no disassemblers are registered. diff --git a/gdb/python/lib/gdb/frames.py b/gdb/python/lib/gdb/frames.py index a3be80c7..96174e9 100644 --- a/gdb/python/lib/gdb/frames.py +++ b/gdb/python/lib/gdb/frames.py @@ -1,5 +1,5 @@ # Frame-filter commands. -# Copyright (C) 2013-2024 Free Software Foundation, Inc. +# Copyright (C) 2013-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/function/__init__.py b/gdb/python/lib/gdb/function/__init__.py index 4b64bc3..62b6422 100644 --- a/gdb/python/lib/gdb/function/__init__.py +++ b/gdb/python/lib/gdb/function/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2012-2024 Free Software Foundation, Inc. +# Copyright (C) 2012-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/function/as_string.py b/gdb/python/lib/gdb/function/as_string.py index a255fff..ff1304f 100644 --- a/gdb/python/lib/gdb/function/as_string.py +++ b/gdb/python/lib/gdb/function/as_string.py @@ -1,4 +1,4 @@ -# Copyright (C) 2016-2024 Free Software Foundation, Inc. +# Copyright (C) 2016-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/function/caller_is.py b/gdb/python/lib/gdb/function/caller_is.py index bacd8c0..3687e4c 100644 --- a/gdb/python/lib/gdb/function/caller_is.py +++ b/gdb/python/lib/gdb/function/caller_is.py @@ -1,5 +1,5 @@ # Caller-is functions. -# Copyright (C) 2008-2024 Free Software Foundation, Inc. +# Copyright (C) 2008-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/function/strfns.py b/gdb/python/lib/gdb/function/strfns.py index 90c9cea..a620f4f 100644 --- a/gdb/python/lib/gdb/function/strfns.py +++ b/gdb/python/lib/gdb/function/strfns.py @@ -1,5 +1,5 @@ # Useful gdb string convenience functions. -# Copyright (C) 2012-2024 Free Software Foundation, Inc. +# Copyright (C) 2012-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/missing_debug.py b/gdb/python/lib/gdb/missing_debug.py index 2c2ceba..b03aaad 100644 --- a/gdb/python/lib/gdb/missing_debug.py +++ b/gdb/python/lib/gdb/missing_debug.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023-2024 Free Software Foundation, Inc. +# Copyright (C) 2023-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/missing_files.py b/gdb/python/lib/gdb/missing_files.py index 5f2df88c..9f24db7 100644 --- a/gdb/python/lib/gdb/missing_files.py +++ b/gdb/python/lib/gdb/missing_files.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023-2024 Free Software Foundation, Inc. +# Copyright (C) 2023-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/missing_objfile.py b/gdb/python/lib/gdb/missing_objfile.py index ace0e13..3d06bdd 100644 --- a/gdb/python/lib/gdb/missing_objfile.py +++ b/gdb/python/lib/gdb/missing_objfile.py @@ -1,4 +1,4 @@ -# Copyright (C) 2024 Free Software Foundation, Inc. +# Copyright (C) 2024-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/printer/__init__.py b/gdb/python/lib/gdb/printer/__init__.py index 6692044..854ff3a 100644 --- a/gdb/python/lib/gdb/printer/__init__.py +++ b/gdb/python/lib/gdb/printer/__init__.py @@ -1,4 +1,4 @@ -# Copyright (C) 2014-2024 Free Software Foundation, Inc. +# Copyright (C) 2014-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/printing.py b/gdb/python/lib/gdb/printing.py index 0635993..cba27d2 100644 --- a/gdb/python/lib/gdb/printing.py +++ b/gdb/python/lib/gdb/printing.py @@ -1,5 +1,5 @@ # Pretty-printer utilities. -# Copyright (C) 2010-2024 Free Software Foundation, Inc. +# Copyright (C) 2010-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/prompt.py b/gdb/python/lib/gdb/prompt.py index 497ab83..060474c 100644 --- a/gdb/python/lib/gdb/prompt.py +++ b/gdb/python/lib/gdb/prompt.py @@ -1,5 +1,5 @@ # Extended prompt utilities. -# Copyright (C) 2011-2024 Free Software Foundation, Inc. +# Copyright (C) 2011-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/ptwrite.py b/gdb/python/lib/gdb/ptwrite.py index 3be65fe..fcc72de 100644 --- a/gdb/python/lib/gdb/ptwrite.py +++ b/gdb/python/lib/gdb/ptwrite.py @@ -1,5 +1,5 @@ # Ptwrite utilities. -# Copyright (C) 2023 Free Software Foundation, Inc. +# Copyright (C) 2023-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/styling.py b/gdb/python/lib/gdb/styling.py index 1c5394e..60c470f 100644 --- a/gdb/python/lib/gdb/styling.py +++ b/gdb/python/lib/gdb/styling.py @@ -1,5 +1,5 @@ # Styling related hooks. -# Copyright (C) 2010-2024 Free Software Foundation, Inc. +# Copyright (C) 2010-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -22,6 +22,7 @@ try: from pygments import formatters, highlight, lexers from pygments.filters import TokenMergeFilter from pygments.token import Comment, Error, Text + from pygments.util import ClassNotFound _formatter = None @@ -31,10 +32,13 @@ try: _formatter = formatters.TerminalFormatter() return _formatter - def colorize(filename, contents): + def colorize(filename, contents, lang): # Don't want any errors. try: - lexer = lexers.get_lexer_for_filename(filename, stripnl=False) + try: + lexer = lexers.get_lexer_by_name(lang, stripnl=False) + except ClassNotFound: + lexer = lexers.get_lexer_for_filename(filename, stripnl=False) formatter = get_formatter() return highlight(contents, lexer, formatter).encode( gdb.host_charset(), "backslashreplace" @@ -76,7 +80,6 @@ try: # ignore. pass - global _asm_lexers if lexer_type not in _asm_lexers: _asm_lexers[lexer_type] = lexers.get_lexer_by_name(lexer_type) _asm_lexers[lexer_type].add_filter(HandleNasmComments()) @@ -94,7 +97,7 @@ try: except ImportError: - def colorize(filename, contents): + def colorize(filename, contents, lang): return None def colorize_disasm(content, gdbarch): diff --git a/gdb/python/lib/gdb/types.py b/gdb/python/lib/gdb/types.py index b4af59c..bac1fb9 100644 --- a/gdb/python/lib/gdb/types.py +++ b/gdb/python/lib/gdb/types.py @@ -1,5 +1,5 @@ # Type utilities. -# Copyright (C) 2010-2024 Free Software Foundation, Inc. +# Copyright (C) 2010-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/unwinder.py b/gdb/python/lib/gdb/unwinder.py index bb0db79..3e1f756 100644 --- a/gdb/python/lib/gdb/unwinder.py +++ b/gdb/python/lib/gdb/unwinder.py @@ -1,4 +1,4 @@ -# Copyright (C) 2015-2024 Free Software Foundation, Inc. +# Copyright (C) 2015-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/gdb/python/lib/gdb/xmethod.py b/gdb/python/lib/gdb/xmethod.py index e12d51c..310585a 100644 --- a/gdb/python/lib/gdb/xmethod.py +++ b/gdb/python/lib/gdb/xmethod.py @@ -1,5 +1,5 @@ # Python side of the support for xmethods. -# Copyright (C) 2013-2024 Free Software Foundation, Inc. +# Copyright (C) 2013-2025 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by |