diff options
Diffstat (limited to 'gdb/python/lib')
54 files changed, 1341 insertions, 635 deletions
diff --git a/gdb/python/lib/gdb/FrameDecorator.py b/gdb/python/lib/gdb/FrameDecorator.py index 82412de..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.""" @@ -285,6 +285,9 @@ class FrameVars(object): # returns False for arguments as well. Anyway, # don't include non-variables here. continue + elif sym.is_artificial: + # Skip artificial symbols. + continue lvars.append(SymValueWrapper(frame, sym)) if block.function is not None: 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 6c3e241..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 @@ -87,8 +92,9 @@ xmethods = [] frame_filters = {} # Initial frame unwinders. frame_unwinders = [] -# Initial missing debug handlers. -missing_debug_handlers = [] +# The missing file handlers. Each item is a tuple with the form +# (TYPE, HANDLER) where TYPE is a string either 'debug' or 'objfile'. +missing_file_handlers = [] def _execute_unwinders(pending_frame): @@ -188,7 +194,7 @@ def GdbSetPythonDirectory(dir): def current_progspace(): "Return the current Progspace." - return _gdb.selected_inferior().progspace + return selected_inferior().progspace def objfiles(): @@ -225,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. @@ -271,6 +277,61 @@ class Thread(threading.Thread): super().start() +def _filter_missing_file_handlers(handlers, handler_type): + """Each list of missing file handlers is a list of tuples, the first + item in the tuple is a string either 'debug' or 'objfile' to + indicate what type of handler it is. The second item in the tuple + is the actual handler object. + + This function takes HANDLER_TYPE which is a string, either 'debug' + or 'objfile' and HANDLERS, a list of tuples. The function returns + an iterable over all of the handler objects (extracted from the + tuples) which match HANDLER_TYPE. + """ + + return map(lambda t: t[1], filter(lambda t: t[0] == handler_type, handlers)) + + +def _handle_missing_files(pspace, handler_type, cb): + """Helper for _handle_missing_debuginfo and _handle_missing_objfile. + + Arguments: + pspace: The gdb.Progspace in which we're operating. Used to + lookup program space specific handlers. + handler_type: A string, either 'debug' or 'objfile', this is the + type of handler we're looking for. + cb: A callback which takes a handler and returns the result of + calling the handler. + + Returns: + None: No suitable file could be found. + False: A handler has decided that the requested file cannot be + found, and no further searching should be done. + True: The file has been found and installed in a location + where GDB would normally look for it. GDB should + repeat its lookup process, the file should now be in + place. + A string: This is the filename of where the missing file can + be found. + """ + + for handler in _filter_missing_file_handlers( + pspace.missing_file_handlers, handler_type + ): + if handler.enabled: + result = cb(handler) + if result is not None: + return result + + for handler in _filter_missing_file_handlers(missing_file_handlers, handler_type): + if handler.enabled: + result = cb(handler) + if result is not None: + return result + + return None + + def _handle_missing_debuginfo(objfile): """Internal function called from GDB to execute missing debug handlers. @@ -293,18 +354,164 @@ def _handle_missing_debuginfo(objfile): A string: This is the filename of a file containing the required debug information. """ + pspace = objfile.progspace - for handler in pspace.missing_debug_handlers: - if handler.enabled: - result = handler(objfile) - if result is not None: - return result + return _handle_missing_files(pspace, "debug", lambda h: h(objfile)) - for handler in missing_debug_handlers: - if handler.enabled: - result = handler(objfile) - if result is not None: - return result - return None +def _handle_missing_objfile(pspace, buildid, filename): + """Internal function called from GDB to execute missing objfile + handlers. + + Run each of the currently registered, and enabled missing objfile + handler objects for the gdb.Progspace passed in as an argument, + and then from the global list. Stop after the first handler that + returns a result other than None. + + Arguments: + pspace: A gdb.Progspace for which the missing objfile handlers + should be run. This is the program space in which an + objfile was found to be missing. + buildid: A string containing the build-id we're looking for. + filename: The filename of the file GDB tried to find but + couldn't. This is not where the file should be + placed if found, in fact, this file might already + exist on disk but have the wrong build-id. This is + mostly provided in order to be used in messages to + the user. + + Returns: + None: No objfile could be found for this build-id. + False: A handler has done all it can with for this build-id, + but no objfile could be found. + True: An objfile might have been installed by a handler, GDB + should check again. The only place GDB checks is within + the .build-id sub-directory within the + debug-file-directory. If the required file was not + installed there then GDB will not find it. + A string: This is the filename of a file containing the + missing objfile. + """ + + 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_debug.py b/gdb/python/lib/gdb/command/missing_files.py index 313b88c..09d9684 100644 --- a/gdb/python/lib/gdb/command/missing_debug.py +++ b/gdb/python/lib/gdb/command/missing_files.py @@ -1,6 +1,6 @@ -# Missing debug related commands. +# 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 @@ -21,7 +21,7 @@ import gdb def validate_regexp(exp, idstring): - """Compile exp into a compiler regular expression object. + """Compile exp into a compiled regular expression object. Arguments: exp: The string to compile into a re.Pattern object. @@ -33,14 +33,15 @@ def validate_regexp(exp, idstring): Raises: SyntaxError: If exp is an invalid regexp. """ + try: return re.compile(exp) except SyntaxError: raise SyntaxError("Invalid %s regexp: %s." % (idstring, exp)) -def parse_missing_debug_command_args(arg): - """Internal utility to parse missing debug handler command argv. +def parse_missing_file_command_args(arg): + """Internal utility to parse missing file handler command argv. Arguments: arg: The arguments to the command. The format is: @@ -52,6 +53,7 @@ def parse_missing_debug_command_args(arg): Raises: SyntaxError: an error processing ARG """ + argv = gdb.string_to_argv(arg) argc = len(argv) if argc > 2: @@ -68,10 +70,10 @@ def parse_missing_debug_command_args(arg): ) -class InfoMissingDebugHanders(gdb.Command): - """GDB command to list missing debug handlers. +class InfoMissingFileHandlers(gdb.Command): + """GDB command to list missing HTYPE handlers. - Usage: info missing-debug-handlers [LOCUS-REGEXP [NAME-REGEXP]] + Usage: info missing-HTYPE-handlers [LOCUS-REGEXP [NAME-REGEXP]] LOCUS-REGEXP is a regular expression matching the location of the handler. If it is omitted, all registered handlers from all @@ -79,38 +81,47 @@ class InfoMissingDebugHanders(gdb.Command): the handlers from the current progspace, or a regular expression matching filenames of progspaces. - NAME-REGEXP is a regular expression to filter missing debug + NAME-REGEXP is a regular expression to filter missing HTYPE handler names. If this omitted for a specified locus, then all registered handlers in the locus are listed. """ - def __init__(self): - super().__init__("info missing-debug-handlers", gdb.COMMAND_FILES) + def __init__(self, handler_type): + # Update the doc string before calling the parent constructor, + # replacing the string 'HTYPE' with the value of HANDLER_TYPE. + # The parent constructor will grab a copy of this string to + # use as the commands help text. + self.__doc__ = self.__doc__.replace("HTYPE", handler_type) + super().__init__( + "info missing-" + handler_type + "-handlers", gdb.COMMAND_FILES + ) + self.handler_type = handler_type def list_handlers(self, title, handlers, name_re): - """Lists the missing debug handlers whose name matches regexp. + """Lists the missing file handlers whose name matches regexp. Arguments: title: The line to print before the list. - handlers: The list of the missing debug handlers. + handlers: The list of the missing file handlers. name_re: handler name filter. """ + if not handlers: return print(title) - for handler in handlers: + for handler in gdb._filter_missing_file_handlers(handlers, self.handler_type): if name_re.match(handler.name): print( " %s%s" % (handler.name, "" if handler.enabled else " [disabled]") ) def invoke(self, arg, from_tty): - locus_re, name_re = parse_missing_debug_command_args(arg) + locus_re, name_re = parse_missing_file_command_args(arg) if locus_re.match("progspace") and locus_re.pattern != "": cp = gdb.current_progspace() self.list_handlers( - "Progspace %s:" % cp.filename, cp.missing_debug_handlers, name_re + "Progspace %s:" % cp.filename, cp.missing_file_handlers, name_re ) for progspace in gdb.progspaces(): @@ -125,58 +136,71 @@ class InfoMissingDebugHanders(gdb.Command): msg = "Progspace %s:" % filename self.list_handlers( msg, - progspace.missing_debug_handlers, + progspace.missing_file_handlers, name_re, ) # Print global handlers last, as these are invoked last. if locus_re.match("global"): - self.list_handlers("Global:", gdb.missing_debug_handlers, name_re) + self.list_handlers("Global:", gdb.missing_file_handlers, name_re) -def do_enable_handler1(handlers, name_re, flag): - """Enable/disable missing debug handlers whose names match given regex. +def do_enable_handler1(handlers, name_re, flag, handler_type): + """Enable/disable missing file handlers whose names match given regex. Arguments: - handlers: The list of missing debug handlers. + handlers: The list of missing file handlers. name_re: Handler name filter. flag: A boolean indicating if we should enable or disable. + handler_type: A string, either 'debug' or 'objfile', use to control + which handlers are modified. Returns: The number of handlers affected. """ + total = 0 - for handler in handlers: + for handler in gdb._filter_missing_file_handlers(handlers, handler_type): if name_re.match(handler.name) and handler.enabled != flag: handler.enabled = flag total += 1 return total -def do_enable_handler(arg, flag): - """Enable or disable missing debug handlers.""" - (locus_re, name_re) = parse_missing_debug_command_args(arg) +def do_enable_handler(arg, flag, handler_type): + """Enable or disable missing file handlers.""" + + (locus_re, name_re) = parse_missing_file_command_args(arg) total = 0 if locus_re.match("global"): - total += do_enable_handler1(gdb.missing_debug_handlers, name_re, flag) + total += do_enable_handler1( + gdb.missing_file_handlers, name_re, flag, handler_type + ) if locus_re.match("progspace") and locus_re.pattern != "": total += do_enable_handler1( - gdb.current_progspace().missing_debug_handlers, name_re, flag + gdb.current_progspace().missing_file_handlers, name_re, flag, handler_type ) for progspace in gdb.progspaces(): filename = progspace.filename or "" if locus_re.match(filename): - total += do_enable_handler1(progspace.missing_debug_handlers, name_re, flag) + total += do_enable_handler1( + progspace.missing_file_handlers, name_re, flag, handler_type + ) print( - "%d missing debug handler%s %s" - % (total, "" if total == 1 else "s", "enabled" if flag else "disabled") + "%d missing %s handler%s %s" + % ( + total, + handler_type, + "" if total == 1 else "s", + "enabled" if flag else "disabled", + ) ) -class EnableMissingDebugHandler(gdb.Command): - """GDB command to enable missing debug handlers. +class EnableMissingFileHandler(gdb.Command): + """GDB command to enable missing HTYPE handlers. - Usage: enable missing-debug-handler [LOCUS-REGEXP [NAME-REGEXP]] + Usage: enable missing-HTYPE-handler [LOCUS-REGEXP [NAME-REGEXP]] LOCUS-REGEXP is a regular expression specifying the handlers to enable. It can be 'global', 'progspace' for the current @@ -187,18 +211,26 @@ class EnableMissingDebugHandler(gdb.Command): in the locus are affected. """ - def __init__(self): - super().__init__("enable missing-debug-handler", gdb.COMMAND_FILES) + def __init__(self, handler_type): + # Update the doc string before calling the parent constructor, + # replacing the string 'HTYPE' with the value of HANDLER_TYPE. + # The parent constructor will grab a copy of this string to + # use as the commands help text. + self.__doc__ = self.__doc__.replace("HTYPE", handler_type) + super().__init__( + "enable missing-" + handler_type + "-handler", gdb.COMMAND_FILES + ) + self.handler_type = handler_type def invoke(self, arg, from_tty): """GDB calls this to perform the command.""" - do_enable_handler(arg, True) + do_enable_handler(arg, True, self.handler_type) -class DisableMissingDebugHandler(gdb.Command): - """GDB command to disable missing debug handlers. +class DisableMissingFileHandler(gdb.Command): + """GDB command to disable missing HTYPE handlers. - Usage: disable missing-debug-handler [LOCUS-REGEXP [NAME-REGEXP]] + Usage: disable missing-HTYPE-handler [LOCUS-REGEXP [NAME-REGEXP]] LOCUS-REGEXP is a regular expression specifying the handlers to enable. It can be 'global', 'progspace' for the current @@ -209,19 +241,28 @@ class DisableMissingDebugHandler(gdb.Command): in the locus are affected. """ - def __init__(self): - super().__init__("disable missing-debug-handler", gdb.COMMAND_FILES) + def __init__(self, handler_type): + # Update the doc string before calling the parent constructor, + # replacing the string 'HTYPE' with the value of HANDLER_TYPE. + # The parent constructor will grab a copy of this string to + # use as the commands help text. + self.__doc__ = self.__doc__.replace("HTYPE", handler_type) + super().__init__( + "disable missing-" + handler_type + "-handler", gdb.COMMAND_FILES + ) + self.handler_type = handler_type def invoke(self, arg, from_tty): """GDB calls this to perform the command.""" - do_enable_handler(arg, False) + do_enable_handler(arg, False, self.handler_type) -def register_missing_debug_handler_commands(): - """Installs the missing debug handler commands.""" - InfoMissingDebugHanders() - EnableMissingDebugHandler() - DisableMissingDebugHandler() +def register_missing_file_handler_commands(): + """Installs the missing file handler commands.""" + for handler_type in ["debug", "objfile"]: + InfoMissingFileHandlers(handler_type) + EnableMissingFileHandler(handler_type) + DisableMissingFileHandler(handler_type) -register_missing_debug_handler_commands() +register_missing_file_handler_commands() 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 db8ac10..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 @@ -21,7 +21,7 @@ from typing import Optional, Sequence import gdb -from .server import capability, request, send_event +from .server import capability, export_line, import_line, request, send_event from .sources import make_source from .startup import ( DAPException, @@ -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", @@ -128,7 +125,7 @@ def _breakpoint_descriptor(bp): result.update( { "source": make_source(filename), - "line": line, + "line": export_line(line), } ) @@ -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] @@ -208,9 +204,9 @@ def _set_breakpoints_callback(kind, specs, creator): } ) - # Delete any breakpoints that were not reused. - for entry in saved_map.values(): - entry.delete() + # Delete any breakpoints that were not reused. + for entry in saved_map.values(): + entry.delete() return result @@ -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 @@ -281,14 +277,14 @@ def _rewrite_src_breakpoint( ): return { "source": source["path"], - "line": line, + "line": import_line(line), "condition": condition, "hitCondition": hitCondition, "logMessage": logMessage, } -@request("setBreakpoints") +@request("setBreakpoints", expect_stopped=False) @capability("supportsHitConditionalBreakpoints") @capability("supportsConditionalBreakpoints") @capability("supportsLogPoints") @@ -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 668bcc7..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 @@ -21,7 +21,7 @@ import gdb from .frames import dap_frame_generator from .modules import module_id from .scopes import symbol_value -from .server import capability, request +from .server import capability, export_line, request from .sources import make_source from .startup import in_gdb_thread from .state import set_thread @@ -86,8 +86,11 @@ def _backtrace(thread_id, levels, startFrame, stack_format): } line = current_frame.line() if line is not None: - newframe["line"] = line + newframe["line"] = export_line(line) if stack_format["line"]: + # Unclear whether export_line should be called + # here, but since it's just for users we pick the + # gdb representation. name += ", line " + str(line) objfile = gdb.current_progspace().objfile_for_address(pc) if objfile is not None: 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 a2e27e5..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 @@ -15,7 +15,7 @@ import gdb -from .server import capability, request +from .server import capability, export_line, request from .sources import make_source @@ -26,34 +26,34 @@ 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: - result["line"] = sal.line + result["line"] = export_line(sal.line) if sal.symtab.filename is not None: # The spec says this can be omitted in some # situations, but it's a little simpler to just always 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 07a4e3e..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 @@ -14,11 +14,13 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import itertools +from typing import Dict import gdb from gdb.frames import frame_iterator from .startup import in_gdb_thread +from .state import set_thread # A list of all the frames we've reported. A frame's index in the # list is its ID. We don't use a hash here because frames are not @@ -29,6 +31,9 @@ _all_frames = [] # Map from a global thread ID to a memoizing frame iterator. _iter_map = {} +# Map from a global frame ID to a thread ID. +thread_ids: Dict[int, int] = {} + # Clear all the frame IDs. @in_gdb_thread @@ -37,6 +42,8 @@ def _clear_frame_ids(evt): _all_frames = [] global _iter_map _iter_map = {} + global thread_ids + thread_ids = {} # Clear the frame ID map whenever the inferior runs. @@ -46,7 +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 _all_frames + if id in thread_ids: + thread_id = thread_ids[id] + if thread_id != gdb.selected_thread().global_num: + set_thread(thread_id) + return _all_frames[id] @@ -63,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 @@ -91,9 +102,9 @@ 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) + thread_ids[num] = gdb.selected_thread().global_num return num def yield_frames(iterator, for_elided): @@ -114,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 38bdc5c..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] @@ -86,7 +85,7 @@ def get_global_scope(frame): syms = [] block_iter = block while block_iter is not None: - syms += [sym for sym in block_iter if sym.is_variable] + syms += [sym for sym in block_iter if sym.is_variable and not sym.is_artificial] block_iter = block_iter.superblock if len(syms) == 0: 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 65444bf..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 @@ -21,8 +21,42 @@ from typing import Mapping, Optional, Sequence import gdb from .events import exec_and_expect_stop, expect_process, expect_stop -from .server import capability, request -from .startup import DAPException, exec_and_log, in_gdb_thread +from .server import ( + DeferredRequest, + call_function_later, + capability, + request, + send_gdb, + send_gdb_with_response, +) +from .startup import DAPException, exec_and_log, in_dap_thread, in_gdb_thread + +# A launch or attach promise that that will be fulfilled after a +# configurationDone request has been processed. +_launch_or_attach_promise = None + + +# A DeferredRequest that handles either a "launch" or "attach" +# request. +class _LaunchOrAttachDeferredRequest(DeferredRequest): + def __init__(self, callback): + self._callback = callback + global _launch_or_attach_promise + if _launch_or_attach_promise is not None: + raise DAPException("launch or attach already specified") + _launch_or_attach_promise = self + + # Invoke the callback and return the result. + @in_dap_thread + def invoke(self): + return self._callback() + + # Override this so we can clear the global when rescheduling. + @in_dap_thread + def reschedule(self): + global _launch_or_attach_promise + _launch_or_attach_promise = None + super().reschedule() # A wrapper for the 'file' command that correctly quotes its argument. @@ -37,7 +71,7 @@ def file_command(program): # Any parameters here are necessarily extensions -- DAP requires this # from implementations. Any additions or changes here should be # documented in the gdb manual. -@request("launch", response=False) +@request("launch", on_dap_thread=True) def launch( *, program: Optional[str] = None, @@ -48,25 +82,51 @@ def launch( stopOnEntry: bool = False, **extra, ): - if cwd is not None: - exec_and_log("cd " + cwd) - if program is not None: - file_command(program) - inf = gdb.selected_inferior() - if stopAtBeginningOfMainSubprogram: - main = inf.main_name - if main is not None: - exec_and_log("tbreak " + main) - inf.arguments = args - if env is not None: - inf.clear_env() - for name, value in env.items(): - inf.set_env(name, value) - expect_process("process") - exec_and_expect_stop("starti" if stopOnEntry else "run") - - -@request("attach") + # Launch setup is handled here. This is done synchronously so + # that errors can be reported in a natural way. + @in_gdb_thread + def _setup_launch(): + if cwd is not None: + exec_and_log("cd " + cwd) + if program is not None: + file_command(program) + inf = gdb.selected_inferior() + inf.arguments = args + if env is not None: + inf.clear_env() + for name, value in env.items(): + inf.set_env(name, value) + + # Actual launching done here. See below for more info. + @in_gdb_thread + def _do_launch(): + expect_process("process") + if stopAtBeginningOfMainSubprogram: + cmd = "start" + elif stopOnEntry: + cmd = "starti" + else: + cmd = "run" + exec_and_expect_stop(cmd) + + @in_dap_thread + def _launch_impl(): + send_gdb_with_response(_setup_launch) + # We do not wait for the result here. It might be a little + # nicer if we did -- perhaps the various thread events would + # occur in a more logical sequence -- but if the inferior does + # not stop, then the launch response will not be seen either, + # which seems worse. + send_gdb(_do_launch) + # Launch response does not have a body. + return None + + # The launch itself is deferred until the configurationDone + # request. + return _LaunchOrAttachDeferredRequest(_launch_impl) + + +@request("attach", on_dap_thread=True) def attach( *, program: Optional[str] = None, @@ -74,21 +134,38 @@ def attach( target: Optional[str] = None, **args, ): - if program is not None: - file_command(program) - if pid is not None: - cmd = "attach " + str(pid) - elif target is not None: - cmd = "target remote " + target - else: - raise DAPException("attach requires either 'pid' or 'target'") - expect_process("attach") - expect_stop("attach") - exec_and_log(cmd) + # The actual attach is handled by this function. + @in_gdb_thread + def _do_attach(): + if program is not None: + file_command(program) + if pid is not None: + cmd = "attach " + str(pid) + elif target is not None: + cmd = "target remote " + target + else: + raise DAPException("attach requires either 'pid' or 'target'") + expect_process("attach") + expect_stop("attach") + exec_and_log(cmd) + # Attach response does not have a body. + return None + + @in_dap_thread + def _attach_impl(): + return send_gdb_with_response(_do_attach) + + # The attach itself is deferred until the configurationDone + # request. + return _LaunchOrAttachDeferredRequest(_attach_impl) @capability("supportsConfigurationDoneRequest") -@request("configurationDone") +@request("configurationDone", on_dap_thread=True) def config_done(**args): - # Nothing to do. - return None + # Handle the launch or attach. + if _launch_or_attach_promise is None: + raise DAPException("launch or attach not specified") + # Resolve the launch or attach, but only after the + # configurationDone response has been sent. + call_function_later(_launch_or_attach_promise.reschedule) diff --git a/gdb/python/lib/gdb/dap/locations.py b/gdb/python/lib/gdb/dap/locations.py index 967322f..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 @@ -16,7 +16,7 @@ # This is deprecated in 3.9, but required in older versions. from typing import Optional -from .server import capability, request +from .server import capability, export_line, import_line, request from .sources import decode_source from .startup import exec_mi_and_log @@ -28,15 +28,18 @@ from .startup import exec_mi_and_log # This points out that fixing this would be an incompatibility but # goes on to propose "if arguments property is missing, debug adapters # should return an error". -@request("breakpointLocations") +@request("breakpointLocations", expect_stopped=False) @capability("supportsBreakpointLocationsRequest") def breakpoint_locations(*, source, line: int, endLine: Optional[int] = None, **extra): + line = import_line(line) if endLine is None: endLine = line + else: + endLine = import_line(endLine) filename = decode_source(source) lines = set() for entry in exec_mi_and_log("-symbol-list-lines", filename)["lines"]: this_line = entry["line"] if this_line >= line and this_line <= endLine: - lines.add(this_line) + lines.add(export_line(this_line)) return {"breakpoints": [{"line": x} for x in sorted(lines)]} 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 fb90f64..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 @@ -17,12 +17,12 @@ import gdb from .frames import frame_for_id from .globalvars import get_global_scope -from .server import request +from .server import export_line, request 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 = {} @@ -76,26 +76,24 @@ def symbol_value(sym, frame): class _ScopeReference(BaseReference): - def __init__(self, name, hint, frame, var_list): + def __init__(self, name, hint, frameId: int, var_list): super().__init__(name) - self.hint = hint - self.frame = frame - self.inf_frame = frame.inferior_frame() - self.func = frame.function() - self.line = frame.line() + 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() - if self.line is not None: - result["line"] = self.line - filename = self.frame.filename() + frame = frame_for_id(self._frameId) + if frame.line() is not None: + result["line"] = export_line(frame.line()) + filename = frame.filename() if filename is not None: result["source"] = make_source(filename) return result @@ -104,46 +102,48 @@ 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], self.frame) + return symbol_value(self._var_list[idx], frame_for_id(self._frameId)) # A _ScopeReference that wraps the 'finish' value. Note that this # object is only created if such a value actually exists. class _FinishScopeReference(_ScopeReference): - def __init__(self, frame): - super().__init__("Return", "returnValue", frame, ()) + def __init__(self, frameId): + super().__init__("Return", "returnValue", frameId, ()) def child_count(self): return 1 def fetch_one_child(self, idx): assert idx == 0 - global _last_return_value return ("(return)", _last_return_value) class _RegisterReference(_ScopeReference): - def __init__(self, name, frame): + def __init__(self, name, frameId): super().__init__( - name, "registers", frame, frame.inferior_frame().architecture().registers() + name, + "registers", + frameId, + frame_for_id(frameId).inferior_frame().architecture().registers(), ) @in_gdb_thread def fetch_one_child(self, idx): return ( - self.var_list[idx].name, - self.inf_frame.read_register(self.var_list[idx]), + self._var_list[idx].name, + frame_for_id(self._frameId) + .inferior_frame() + .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: @@ -153,16 +153,16 @@ def scopes(*, frameId: int, **extra): # iterator case. args = tuple(frame.frame_args() or ()) if args: - scopes.append(_ScopeReference("Arguments", "arguments", frame, args)) + scopes.append(_ScopeReference("Arguments", "arguments", frameId, args)) has_return_value = frameId == 0 and _last_return_value is not None # Make sure to handle the None case as well as the empty # iterator case. locs = tuple(frame.frame_locals() or ()) if locs: - scopes.append(_ScopeReference("Locals", "locals", frame, locs)) - scopes.append(_RegisterReference("Registers", frame)) + scopes.append(_ScopeReference("Locals", "locals", frameId, locs)) + scopes.append(_RegisterReference("Registers", frameId)) if has_return_value: - scopes.append(_FinishScopeReference(frame)) + scopes.append(_FinishScopeReference(frameId)) frame_to_scope[frameId] = scopes global_scope = get_global_scope(frame) if global_scope is not None: diff --git a/gdb/python/lib/gdb/dap/server.py b/gdb/python/lib/gdb/dap/server.py index 8c6d908..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 @@ -46,6 +46,67 @@ _commands = {} # The global server. _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: + """If a DAP request function returns a deferred request, no + response is sent immediately. + + Instead, request processing continues, with this particular + request remaining un-replied-to. + + Later, when the result is available, the deferred request can be + scheduled. This causes 'invoke' to be called and then the + response to be sent to the client. + + """ + + # This is for internal use by the server. It should not be + # overridden by any subclass. This adds the request ID and the + # result template object to this object. These are then used + # during rescheduling. + def set_request(self, req, result): + self._req = req + 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. + + This will be called from 'reschedule' (and should not be + called elsewhere). It should return the 'body' that will be + sent in the response. None means no 'body' field will be set. + + Subclasses must override this. + + """ + pass + + @in_dap_thread + def reschedule(self): + """Call this to reschedule this deferred request. + + This will call 'invoke' after the appropriate bookkeeping and + will arrange for its result to be reported to the client. + + """ + 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 # request needs the inferior to be stopped, but it is not stopped. @@ -59,21 +120,78 @@ class NotStoppedException(Exception): class CancellationHandler: def __init__(self): # Methods on this class acquire this lock before proceeding. - self.lock = threading.Lock() + # A recursive lock is used to simplify the 'check_cancel' + # callers. + self.lock = threading.RLock() # 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() + + @contextmanager + def current_request(self, req): + """Return a new context manager that registers that request + REQ has started.""" + try: + with self.lock: + self.in_flight_dap_thread = req + # Note we do not call check_cancel here. This is a bit of + # a hack, but it's because the direct callers of this + # aren't prepared for a KeyboardInterrupt. + yield + finally: + with self.lock: + self.in_flight_dap_thread = None + + def defer_request(self, req): + """Indicate that the request REQ has been deferred.""" + with self.lock: + self._deferred_ids.add(req) + + def request_finished(self, req): + """Indicate that the request REQ is finished. + + It doesn't matter whether REQ succeeded or failed, only that + processing for it is done. - def starting(self, req): - """Call at the start of the given request.""" + """ with self.lock: - self.in_flight_dap_thread = req + # Use discard here, not remove, because this is called + # regardless of whether REQ was deferred. + self._deferred_ids.discard(req) - def done(self, req): - """Indicate that the request is done.""" + def check_cancel(self, req): + """Check whether request REQ is cancelled. + If so, raise KeyboardInterrupt.""" with self.lock: - self.in_flight_dap_thread = None + # We want to drop any cancellations that come before REQ, + # but keep ones for any deferred requests that are still + # unresolved. This holds any such requests that were + # popped during the loop. + deferred = [] + try: + # If the request is cancelled, don't execute the region. + 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) + if next_id == req: + raise KeyboardInterrupt() + 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 + # request 18 here will lose the cancellation. + # So, we preserve it. + deferred.append(next_id) + finally: + for x in deferred: + heapq.heappush(self._reqs, x) def cancel(self, req): """Call to cancel a request. @@ -86,12 +204,12 @@ class CancellationHandler: gdb.interrupt() else: # We don't actually ignore the request here, but in - # the 'starting' method. This way we don't have to + # the 'check_cancel' method. This way we don't have to # track as much state. Also, this implementation has # 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): @@ -103,10 +221,7 @@ class CancellationHandler: return try: with self.lock: - # If the request is cancelled, don't execute the region. - while len(self.reqs) > 0 and self.reqs[0] <= req: - if heapq.heappop(self.reqs) == req: - raise KeyboardInterrupt() + self.check_cancel(req) # Request is being handled by the gdb thread. self.in_flight_gdb_thread = req # Execute region. This may be interrupted by gdb.interrupt. @@ -121,45 +236,45 @@ 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_events_lock = threading.Lock() - self.defer_stop_events = False - self.delayed_events = [] + 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 - # Treat PARAMS as a JSON-RPC request and perform its action. - # PARAMS is just a dictionary from the JSON. + # A helper for request processing. REQ is the request ID. RESULT + # is a result "template" -- a dictionary with a few items already + # filled in. This helper calls FN and then fills in the remaining + # parts of RESULT, as needed. If FN returns an ordinary result, + # or if it fails, then the final RESULT is sent as a response to + # the client. However, if FN returns a DeferredRequest, then that + # request is updated (see DeferredRequest.set_request) and no + # response is sent. @in_dap_thread - def _handle_command(self, params): - req = params["seq"] - result = { - "request_seq": req, - "type": "response", - "command": params["command"], - } + def invoke_request(self, req, result, fn): try: - self.canceller.starting(req) - if "arguments" in params: - args = params["arguments"] - else: - args = {} - global _commands - body = _commands[params["command"]](**args) - if body is not None: - result["body"] = body + self.canceller.check_cancel(req) + fn_result = fn() result["success"] = True + if isinstance(fn_result, DeferredRequest): + fn_result.set_request(req, result) + self.canceller.defer_request(req) + # Do not send a response. + return + elif fn_result is not None: + result["body"] = fn_result except NotStoppedException: # This is an expected exception, and the result is clearly # visible in the log, so do not log it. @@ -179,19 +294,39 @@ class Server: log_stack() result["success"] = False result["message"] = str(e) - return result + self.canceller.request_finished(req) + # We have a response for the request, so send it back to the + # client. + self._send_json(result) + + # Treat PARAMS as a JSON-RPC request and perform its action. + # PARAMS is just a dictionary from the JSON. @in_dap_thread - def _handle_command_finish(self, params): + def _handle_command(self, params): req = params["seq"] - self.canceller.done(req) + result = { + "request_seq": req, + "type": "response", + "command": params["command"], + } + + if "arguments" in params: + args = params["arguments"] + else: + args = {} + + def fn(): + return _commands[params["command"]](**args) + + self.invoke_request(req, result, fn) # Read inferior output and sends OutputEvents to the client. It # 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", @@ -202,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 @@ -210,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) + ">>>") @@ -226,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): @@ -236,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 - result = self._handle_command(cmd) - self._send_json(result) - self._handle_command_finish(cmd) - events = None - with self.delayed_events_lock: - events = self.delayed_events - self.delayed_events = [] - self.defer_stop_events = False - for event, body in events: - self.send_event(event, body) + req = cmd["seq"] + with self.canceller.current_request(req): + self._handle_command(cmd) + 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_events_lock: - self.delayed_events.append((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, @@ -275,16 +415,22 @@ class Server: the client.""" with self.canceller.lock: if self.canceller.in_flight_dap_thread: - with self.delayed_events_lock: - if self.defer_stop_events: - self.delayed_events.append((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) # 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.""" @@ -301,24 +447,19 @@ 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) + _server.send_event_maybe_later(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.""" + _server.call_function_later(fn) # A helper decorator that checks whether the inferior is running. @@ -342,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. @@ -364,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. @@ -389,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 @@ -417,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 @@ -430,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 @@ -438,22 +583,24 @@ def capability(name, value=True): return wrap -def client_bool_capability(name): +def client_bool_capability(name, default=False): """Return the value of a boolean client capability. If the capability was not specified, or did not have boolean type, - False is returned.""" - global _server + DEFAULT is returned. DEFAULT defaults to False.""" if name in _server.config and isinstance(_server.config[name], bool): return _server.config[name] - return False + return default @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() @@ -490,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 @@ -511,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 @@ -557,3 +704,39 @@ def send_gdb_with_response(fn): if isinstance(val, (Exception, KeyboardInterrupt)): raise val return val + + +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.""" + 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 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.""" + 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 57e84a1..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 @@ -18,6 +18,7 @@ from collections import defaultdict from contextlib import contextmanager import gdb +import gdb.printing from .server import client_bool_capability from .startup import DAPException, in_gdb_thread @@ -59,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 @@ -70,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 @@ -82,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 @@ -96,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): @@ -127,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 @@ -145,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): @@ -164,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 + "'") @@ -180,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: @@ -196,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 @@ -229,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: @@ -244,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 @@ -268,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 72d311b..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. @@ -147,7 +147,7 @@ class maint_info_py_disassemblers_cmd(gdb.Command): # Figure out the name of the current architecture. There # should always be a current inferior, but if, somehow, there # isn't, then leave curr_arch as the empty string, which will - # not then match agaisnt any architecture in the dictionary. + # not then match against any architecture in the dictionary. curr_arch = "" if gdb.selected_inferior() is not None: curr_arch = gdb.selected_inferior().architecture().name() 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 7ccc4fe..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 @@ -17,72 +17,11 @@ MissingDebugHandler base class, and register_handler function. """ -import sys - import gdb +from gdb.missing_files import MissingFileHandler -if sys.version_info >= (3, 7): - # Functions str.isascii() and str.isalnum are available starting Python - # 3.7. - def isascii(ch): - return ch.isascii() - - def isalnum(ch): - return ch.isalnum() - -else: - # Older version of Python doesn't have str.isascii() and - # str.isalnum() so provide our own. - # - # We could import isalnum() and isascii() from the curses library, - # but that adds an extra dependency. Given these functions are - # both small and trivial lets implement them here. - # - # These definitions are based on those in the curses library, but - # simplified as we know C will always be a single character 'str'. - - def isdigit(c): - return 48 <= ord(c) <= 57 - - def islower(c): - return 97 <= ord(c) <= 122 - - def isupper(c): - return 65 <= ord(c) <= 90 - - def isalpha(c): - return isupper(c) or islower(c) - - def isalnum(c): - return isalpha(c) or isdigit(c) - - def isascii(c): - return 0 <= ord(c) <= 127 - -def _validate_name(name): - """Validate a missing debug handler name string. - - If name is valid as a missing debug handler name, then this - function does nothing. If name is not valid then an exception is - raised. - - Arguments: - name: A string, the name of a missing debug handler. - - Returns: - Nothing. - - Raises: - ValueError: If name is invalid as a missing debug handler - name. - """ - for ch in name: - if not isascii(ch) or not (isalnum(ch) or ch in "_-"): - raise ValueError("invalid character '%s' in handler name: %s" % (ch, name)) - - -class MissingDebugHandler(object): +class MissingDebugHandler(MissingFileHandler): """Base class for missing debug handlers written in Python. A missing debug handler has a single method __call__ along with @@ -93,41 +32,8 @@ class MissingDebugHandler(object): enabled: When true this handler is enabled. """ - def __init__(self, name): - """Constructor. - - Args: - name: An identifying name for this handler. - - Raises: - TypeError: name is not a string. - ValueError: name contains invalid characters. - """ - - if not isinstance(name, str): - raise TypeError("incorrect type for name: %s" % type(name)) - - _validate_name(name) - - self._name = name - self._enabled = True - - @property - def name(self): - return self._name - - @property - def enabled(self): - return self._enabled - - @enabled.setter - def enabled(self, value): - if not isinstance(value, bool): - raise TypeError("incorrect type for enabled attribute: %s" % type(value)) - self._enabled = value - def __call__(self, objfile): - """GDB handle missing debug information for an objfile. + """Handle missing debug information for an objfile. Arguments: objfile: A gdb.Objfile for which GDB could not find any @@ -148,62 +54,5 @@ class MissingDebugHandler(object): def register_handler(locus, handler, replace=False): - """Register handler in given locus. - - The handler is prepended to the locus's missing debug handlers - list. The name of handler should be unique (or replace must be - True). - - Arguments: - locus: Either a progspace, or None (in which case the unwinder - is registered globally). - handler: An object of a gdb.MissingDebugHandler subclass. - - replace: If True, replaces existing handler with the same name - within locus. Otherwise, raises RuntimeException if - unwinder with the same name already exists. - - Returns: - Nothing. - - Raises: - RuntimeError: The name of handler is not unique. - TypeError: Bad locus type. - AttributeError: Required attributes of handler are missing. - """ - - if locus is None: - if gdb.parameter("verbose"): - gdb.write("Registering global %s handler ...\n" % handler.name) - locus = gdb - elif isinstance(locus, gdb.Progspace): - if gdb.parameter("verbose"): - gdb.write( - "Registering %s handler for %s ...\n" % (handler.name, locus.filename) - ) - else: - raise TypeError("locus should be gdb.Progspace or None") - - # Some sanity checks on HANDLER. Calling getattr will raise an - # exception if the attribute doesn't exist, which is what we want. - # These checks are not exhaustive; we don't check the attributes - # have the correct types, or the method has the correct signature, - # but this should catch some basic mistakes. - getattr(handler, "name") - getattr(handler, "enabled") - call_method = getattr(handler, "__call__") - if not callable(call_method): - raise AttributeError( - "'%s' object's '__call__' attribute is not callable" - % type(handler).__name__ - ) - - i = 0 - for needle in locus.missing_debug_handlers: - if needle.name == handler.name: - if replace: - del locus.missing_debug_handlers[i] - else: - raise RuntimeError("Handler %s already exists." % handler.name) - i += 1 - locus.missing_debug_handlers.insert(0, handler) + """See gdb.missing_files.register_handler.""" + gdb.missing_files.register_handler("debug", locus, handler, replace) diff --git a/gdb/python/lib/gdb/missing_files.py b/gdb/python/lib/gdb/missing_files.py new file mode 100644 index 0000000..9f24db7 --- /dev/null +++ b/gdb/python/lib/gdb/missing_files.py @@ -0,0 +1,204 @@ +# 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 +# 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/>. + +""" +MissingFileHandler base class, and support functions used by the +missing_debug.py and missing_objfile.py modules. +""" + +import sys + +import gdb + +if sys.version_info >= (3, 7): + # Functions str.isascii() and str.isalnum are available starting Python + # 3.7. + def isascii(ch): + return ch.isascii() + + def isalnum(ch): + return ch.isalnum() + +else: + # Older version of Python doesn't have str.isascii() and + # str.isalnum() so provide our own. + # + # We could import isalnum() and isascii() from the curses library, + # but that adds an extra dependency. Given these functions are + # both small and trivial lets implement them here. + # + # These definitions are based on those in the curses library, but + # simplified as we know C will always be a single character 'str'. + + def isdigit(c): + return 48 <= ord(c) <= 57 + + def islower(c): + return 97 <= ord(c) <= 122 + + def isupper(c): + return 65 <= ord(c) <= 90 + + def isalpha(c): + return isupper(c) or islower(c) + + def isalnum(c): + return isalpha(c) or isdigit(c) + + def isascii(c): + return 0 <= ord(c) <= 127 + + +def _validate_name(name): + """Validate a missing file handler name string. + + If name is valid as a missing file handler name, then this + function does nothing. If name is not valid then an exception is + raised. + + Arguments: + name: A string, the name of a missing file handler. + + Returns: + Nothing. + + Raises: + ValueError: If name is invalid as a missing file handler + name. + """ + + for ch in name: + if not isascii(ch) or not (isalnum(ch) or ch in "_-"): + raise ValueError("invalid character '%s' in handler name: %s" % (ch, name)) + + +class MissingFileHandler(object): + """Base class for missing file handlers written in Python. + + A missing file handler has a single method __call__ along with the + read/write attribute enabled, and a read-only attribute name. The + attributes are provided by this class while the __call__ method is + provided by a sub-class. Each sub-classes __call__ method will + have a different signature. + + Attributes: + name: Read-only attribute, the name of this handler. + enabled: When true this handler is enabled. + """ + + def __init__(self, name): + """Constructor. + + Args: + name: An identifying name for this handler. + + Raises: + TypeError: name is not a string. + ValueError: name contains invalid characters. + """ + + if not isinstance(name, str): + raise TypeError("incorrect type for name: %s" % type(name)) + + _validate_name(name) + + self._name = name + self._enabled = True + + @property + def name(self): + return self._name + + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, value): + if not isinstance(value, bool): + raise TypeError("incorrect type for enabled attribute: %s" % type(value)) + self._enabled = value + + +def register_handler(handler_type, locus, handler, replace=False): + """Register handler in given locus. + + The handler is prepended to the locus's missing file handlers + list. The name of handler should be unique (or replace must be + True), and the name must pass the _validate_name check. + + Arguments: + handler_type: A string, either 'debug' or 'objfile' indicating the + type of handler to be registered. + locus: Either a progspace, or None (in which case the unwinder + is registered globally). + handler: An object used as a missing file handler. Usually a + sub-class of MissingFileHandler. + replace: If True, replaces existing handler with the same name + within locus. Otherwise, raises RuntimeException if + unwinder with the same name already exists. + + Returns: + Nothing. + + Raises: + RuntimeError: The name of handler is not unique. + TypeError: Bad locus type. + AttributeError: Required attributes of handler are missing. + ValueError: If the name of the handler is invalid, or if + handler_type is neither 'debug' or 'objfile'. + """ + + if handler_type != "debug" and handler_type != "objfile": + raise ValueError("handler_type must be 'debug' or 'objfile'") + + if locus is None: + if gdb.parameter("verbose"): + gdb.write("Registering global %s handler ...\n" % handler.name) + locus = gdb + elif isinstance(locus, gdb.Progspace): + if gdb.parameter("verbose"): + gdb.write( + "Registering %s handler for %s ...\n" % (handler.name, locus.filename) + ) + else: + raise TypeError("locus should be gdb.Progspace or None") + + # Some sanity checks on HANDLER. Calling getattr will raise an + # exception if the attribute doesn't exist, which is what we want. + # These checks are not exhaustive; we don't check the attributes + # have the correct types, or the method has the correct signature, + # but this should catch some basic mistakes. + name = getattr(handler, "name") + _validate_name(name) + + getattr(handler, "enabled") + + call_method = getattr(handler, "__call__") + if not callable(call_method): + raise AttributeError( + "'%s' object's '__call__' attribute is not callable" + % type(handler).__name__ + ) + + i = 0 + for needle in locus.missing_file_handlers: + if needle[0] == handler_type and needle[1].name == handler.name: + if replace: + del locus.missing_file_handlers[i] + else: + raise RuntimeError("Handler %s already exists." % handler.name) + i += 1 + locus.missing_file_handlers.insert(0, (handler_type, handler)) diff --git a/gdb/python/lib/gdb/missing_objfile.py b/gdb/python/lib/gdb/missing_objfile.py new file mode 100644 index 0000000..3d06bdd --- /dev/null +++ b/gdb/python/lib/gdb/missing_objfile.py @@ -0,0 +1,67 @@ +# 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 +# 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/>. + +""" +MissingObjfileHandler base class, and register_handler function. +""" + +import gdb +from gdb.missing_files import MissingFileHandler + + +class MissingObjfileHandler(MissingFileHandler): + """Base class for missing objfile handlers written in Python. + + A missing objfile handler has a single method __call__ along with + the read/write attribute enabled, and a read-only attribute name. + + Attributes: + name: Read-only attribute, the name of this handler. + enabled: When true this handler is enabled. + """ + + def __call__(self, buildid, filename): + """Handle a missing objfile when GDB can knows the build-id. + + Arguments: + + buildid: A string containing the build-id for the objfile + GDB is searching for. + filename: A string containing the name of the file GDB is + searching for. This is provided only for the purpose + of creating diagnostic messages. If the file is found + it does not have to be placed here, and this file + might already exist but GDB has determined it is not + suitable for use, e.g. if the build-id doesn't match. + + Returns: + + True: GDB should try again to locate the missing objfile, + the handler may have installed the missing file. + False: GDB should move on without the objfile. The + handler has determined that this objfile is not + available. + A string: GDB should load the file at the given path; it + contains the requested objfile. + None: This handler can't help with this objfile. GDB + should try any other registered handlers. + + """ + raise NotImplementedError("MissingObjfileHandler.__call__()") + + +def register_handler(locus, handler, replace=False): + """See gdb.missing_files.register_handler.""" + gdb.missing_files.register_handler("objfile", locus, handler, replace) 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/printer/bound_registers.py b/gdb/python/lib/gdb/printer/bound_registers.py deleted file mode 100644 index d00b455..0000000 --- a/gdb/python/lib/gdb/printer/bound_registers.py +++ /dev/null @@ -1,39 +0,0 @@ -# Pretty-printers for bounds registers. -# Copyright (C) 2013-2024 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/>. - -import gdb -import gdb.printing - - -class MpxBound128Printer(gdb.ValuePrinter): - """Adds size field to a mpx __gdb_builtin_type_bound128 type.""" - - def __init__(self, val): - self.__val = val - - def to_string(self): - upper = self.__val["ubound"] - lower = self.__val["lbound"] - size = upper - lower - if size > -1: - size = size + 1 - result = "{lbound = %s, ubound = %s} : size %s" % (lower, upper, size) - return result - - -gdb.printing.add_builtin_pretty_printer( - "mpx_bound128", "^builtin_type_bound128", MpxBound128Printer -) diff --git a/gdb/python/lib/gdb/printing.py b/gdb/python/lib/gdb/printing.py index 55ba435..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 @@ -281,6 +281,44 @@ class NoOpScalarPrinter(gdb.ValuePrinter): return self.__value.format_string(raw=True) +class NoOpStringPrinter(gdb.ValuePrinter): + """A no-op pretty printer that wraps a string value.""" + + def __init__(self, ty, value): + self.__ty = ty + self.__value = value + + def to_string(self): + # We need some special cases here. + # + # * If the gdb.Value was created from a Python string, it will + # be a non-lazy array -- but will have address 0 and so the + # contents will be lost on conversion to lazy string. + # (Weirdly, the .address attribute will not be 0 though.) + # Since conversion to lazy string is to avoid fetching too + # much data, and since the array is already non-lazy, just + # return it. + # + # * To avoid weird printing for a C "string" that is just a + # NULL pointer, special case this as well. + # + # * Lazy strings only understand arrays and pointers; other + # string-like objects (like a Rust &str) should simply be + # returned. + code = self.__ty.code + if code == gdb.TYPE_CODE_ARRAY and not self.__value.is_lazy: + return self.__value + elif code == gdb.TYPE_CODE_PTR and self.__value == 0: + return self.__value + elif code != gdb.TYPE_CODE_PTR and code != gdb.TYPE_CODE_ARRAY: + return self.__value + else: + return self.__value.lazy_string() + + def display_hint(self): + return "string" + + class NoOpPointerReferencePrinter(gdb.ValuePrinter): """A no-op pretty printer that wraps a pointer or reference.""" @@ -368,7 +406,7 @@ def make_visualizer(value): else: ty = value.type.strip_typedefs() if ty.is_string_like: - result = NoOpScalarPrinter(value) + result = NoOpStringPrinter(ty, value) elif ty.code == gdb.TYPE_CODE_ARRAY: result = NoOpArrayPrinter(ty, value) elif ty.is_array_like: diff --git a/gdb/python/lib/gdb/prompt.py b/gdb/python/lib/gdb/prompt.py index 4ad38e4..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 @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -""" Extended prompt library functions.""" +"""Extended prompt library functions.""" import os 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 c98402d..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 @@ -266,9 +266,14 @@ def register_xmethod_matcher(locus, matcher, replace=False): del locus.xmethods[index] else: raise RuntimeError( - "Xmethod matcher already registered with " - "%s: %s" % (locus_name, matcher.name) + "Xmethod matcher already registered with {}: {}".format( + locus_name, matcher.name + ) ) if gdb.parameter("verbose"): - gdb.write("Registering xmethod matcher '%s' with %s' ...\n") + gdb.write( + "Registering xmethod matcher '{}' with '{}' ...\n".format( + locus_name, matcher.name + ) + ) locus.xmethods.insert(0, matcher) |