diff options
Diffstat (limited to 'gdb/python/lib')
23 files changed, 554 insertions, 199 deletions
diff --git a/gdb/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py index 4ea5d06..d635b94 100644 --- a/gdb/python/lib/gdb/__init__.py +++ b/gdb/python/lib/gdb/__init__.py @@ -19,17 +19,31 @@ 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 ( + COMMAND_NONE, + INTENSITY_BOLD, + INTENSITY_DIM, + INTENSITY_NORMAL, + PARAM_COLOR, + PARAM_ENUM, + STDERR, + STDOUT, + Color, + Command, + Parameter, + Style, + execute, + flush, + parameter, + selected_inferior, + write, +) # isort: split @@ -60,14 +74,14 @@ class _GdbFile(object): self.write(line) def flush(self): - _gdb.flush(stream=self.stream) + flush(stream=self.stream) def write(self, s): - _gdb.write(s, stream=self.stream) + write(s, stream=self.stream) -sys.stdout = _GdbFile(_gdb.STDOUT) -sys.stderr = _GdbFile(_gdb.STDERR) +sys.stdout = _GdbFile(STDOUT) +sys.stderr = _GdbFile(STDERR) # Default prompt hook does nothing. prompt_hook = None @@ -189,7 +203,7 @@ def GdbSetPythonDirectory(dir): def current_progspace(): "Return the current Progspace." - return _gdb.selected_inferior().progspace + return selected_inferior().progspace def objfiles(): @@ -226,14 +240,14 @@ def set_parameter(name, value): value = "on" else: value = "off" - _gdb.execute("set " + name + " " + str(value), to_string=True) + execute("set " + name + " " + str(value), to_string=True) @contextmanager def with_parameter(name, value): """Temporarily set the GDB parameter NAME to VALUE. Note that this is a context manager.""" - old_value = _gdb.parameter(name) + old_value = parameter(name) set_parameter(name, value) try: # Nothing that useful to return. @@ -392,3 +406,330 @@ def _handle_missing_objfile(pspace, buildid, filename): return _handle_missing_files( pspace, "objfile", lambda h: h(pspace, buildid, filename) ) + + +class ParameterPrefix: + # A wrapper around gdb.Command for creating set/show prefixes. + # + # When creating a gdb.Parameter sub-classes, it is sometimes necessary + # to first create a gdb.Command object in order to create the needed + # command prefix. However, for parameters, we actually need two + # prefixes, a 'set' prefix, and a 'show' prefix. With this helper + # class, a single instance of this class will create both prefixes at + # once. + # + # It is important that this class-level documentation not be a __doc__ + # string. Users are expected to sub-class this ParameterPrefix class + # and add their own documentation. If they don't, then GDB will + # generate a suitable doc string. But, if this (parent) class has a + # __doc__ string of its own, then sub-classes will inherit that __doc__ + # string, and GDB will not understand that it needs to generate one. + + class _PrefixCommand(Command): + """A gdb.Command used to implement both the set and show prefixes. + + This documentation string is not used as the prefix command + documentation as it is overridden in the __init__ method below.""" + + # This private method is connected to the 'invoke' attribute within + # this _PrefixCommand object if the containing ParameterPrefix + # object has an invoke_set or invoke_show method. + # + # This method records within self.__delegate which _PrefixCommand + # object is currently active, and then calls the correct invoke + # method on the delegat object (the ParameterPrefix sub-class + # object). + # + # Recording the currently active _PrefixCommand object is important; + # if from the invoke method the user calls dont_repeat, then this is + # forwarded to the currently active _PrefixCommand object. + def __invoke(self, args, from_tty): + + # A helper class for use as part of a Python 'with' block. + # Records which gdb.Command object is currently running its + # invoke method. + class MarkActiveCallback: + # The CMD is a _PrefixCommand object, and the DELEGATE is + # the ParameterPrefix class, or sub-class object. At this + # point we simple record both of these within the + # MarkActiveCallback object. + def __init__(self, cmd, delegate): + self.__cmd = cmd + self.__delegate = delegate + + # Record the currently active _PrefixCommand object within + # the outer ParameterPrefix sub-class object. + def __enter__(self): + self.__delegate.active_prefix = self.__cmd + + # Once the invoke method has completed, then clear the + # _PrefixCommand object that was stored into the outer + # ParameterPrefix sub-class object. + def __exit__(self, exception_type, exception_value, traceback): + self.__delegate.active_prefix = None + + # The self.__cb attribute is set when the _PrefixCommand object + # is created, and is either invoke_set or invoke_show within the + # ParameterPrefix sub-class object. + assert callable(self.__cb) + + # Record the currently active _PrefixCommand object within the + # ParameterPrefix sub-class object, then call the relevant + # invoke method within the ParameterPrefix sub-class object. + with MarkActiveCallback(self, self.__delegate): + self.__cb(args, from_tty) + + @staticmethod + def __find_callback(delegate, mode): + """The MODE is either 'set' or 'show'. Look for an invoke_MODE method + on DELEGATE, if a suitable method is found, then return it, otherwise, + return None. + """ + cb = getattr(delegate, "invoke_" + mode, None) + if callable(cb): + return cb + return None + + def __init__(self, mode, name, cmd_class, delegate, doc=None): + """Setup this gdb.Command. Mode is a string, either 'set' or 'show'. + NAME is the name for this prefix command, that is, the + words that appear after both 'set' and 'show' in the + command name. CMD_CLASS is the usual enum. And DELEGATE + is the gdb.ParameterPrefix object this prefix is part of. + """ + assert mode == "set" or mode == "show" + if doc is None: + self.__doc__ = delegate.__doc__ + else: + self.__doc__ = doc + self.__cb = self.__find_callback(delegate, mode) + self.__delegate = delegate + if self.__cb is not None: + self.invoke = self.__invoke + super().__init__(mode + " " + name, cmd_class, prefix=True) + + def __init__(self, name, cmd_class, doc=None): + """Create a _PrefixCommand for both the set and show prefix commands. + NAME is the command name without either the leading 'set ' or + 'show ' strings, and CMD_CLASS is the usual enum value. + """ + self.active_prefix = None + self._set_prefix_cmd = self._PrefixCommand("set", name, cmd_class, self, doc) + self._show_prefix_cmd = self._PrefixCommand("show", name, cmd_class, self, doc) + + # When called from within an invoke method the self.active_prefix + # attribute should be set to a gdb.Command sub-class (a _PrefixCommand + # object, see above). Forward the dont_repeat call to this object to + # register the actual command as none repeating. + def dont_repeat(self): + if self.active_prefix is not None: + self.active_prefix.dont_repeat() + + +class StyleParameterSet: + """Create new style parameters. + + A style parameter is a set of parameters that start with 'set style ...' + and 'show style ...'. For example 'filename' is a style parameter, and + 'disassembler symbol' is another style parameter. + + The name of the style parameter is really a prefix command. Under this + we must have two commands 'foreground' and 'background', which are color + parameters. A third, optional command 'intensity', is an enum with + values 'normal', 'bold', and 'dim'. + + A StyleParameterSet is initialised with a name, e.g. 'filename' or + 'disassembler symbol'. The StyleParameterSet creates the prefix + commands in the 'set style' and 'show style' name space, and then adds + the 'foreground', 'background', and optionally, the 'intensity' + commands. + + If you want a whole new style group, similar to 'disassembler', + then you need to add this yourself first, then StyleParameterSet + can be used to create styles within the new prefix group. + + The 'value' attribute on this object can be used to get and set a + gdb.Style object which controls all aspects of this style. + + For readability, the alias 'style' is the same as 'value'. + """ + + def __init__(self, name, add_intensity=True, doc=None): + # The STYLE_NAME is something like 'filename' is 'set style + # filename ...', and PARAM_NAME is one of 'foreground', + # 'background', or 'intensity'. The DESC_TEXT is the long + # form used in help text, like 'foreground color' or 'display + # intensity'. The DEFAULT_VALUE is used to set the SELF.value + # attribute. And PARAM_TYPE is a gdb.PARAM_* constant. The + # ARGS is used for gdb.PARAM_ENUM, which ARGS should be the + # enum value list. + class style_parameter(Parameter): + def __init__( + self, + style_name, + parent_obj, + param_name, + desc_text, + default_value, + param_type, + *args + ): + # Setup documentation must be done before calling + # parent's __init__ method, as the __init__ reads (and + # copies) these values. + self.show_doc = "Show the " + desc_text + " for this property." + self.set_doc = "Set the " + desc_text + " for this property." + self.__doc__ = "" + + # Call the parent's __init__ method to actually create + # the parameter. + super().__init__( + "style " + style_name + " " + param_name, + COMMAND_NONE, + param_type, + *args + ) + + # Store information we need in other methods. + self._style_name = style_name + self._desc_text = desc_text + self._parent = parent_obj + + # Finally, setup the default value. + self.value = default_value + + # Return the 'show style <style-name> <attribute>' string, + # which has styling applied. + def get_show_string(self, value): + s = self._parent.style + return ( + "The " + + s.apply('"' + self._style_name + '" style') + + " " + + self._desc_text + + " is: " + + value + ) + + class style_foreground_parameter(style_parameter): + def __init__(self, name, parent): + super().__init__( + name, + parent, + "foreground", + "foreground color", + Color(), + PARAM_COLOR, + ) + + class style_background_parameter(style_parameter): + def __init__(self, name, parent): + super().__init__( + name, + parent, + "background", + "background color", + Color(), + PARAM_COLOR, + ) + + class style_intensity_parameter(style_parameter): + def __init__(self, name, parent): + super().__init__( + name, + parent, + "intensity", + "display intensity", + "normal", + PARAM_ENUM, + ["normal", "bold", "dim"], + ) + + if doc is None: + doc = ( + "The " + + name + + " display styling.\nConfigure " + + name + + " colors and display intensity." + ) + + ParameterPrefix("style " + name, COMMAND_NONE, doc) + self._foreground = style_foreground_parameter(name, self) + self._background = style_background_parameter(name, self) + if add_intensity: + self._intensity = style_intensity_parameter(name, self) + self._name = name + self._style = None + + @property + def value(self): + """Return the gdb.Style object for this parameter set.""" + if self._style is None: + self._style = Style(self._name) + return self._style + + @property + def style(self): + """Return the gdb.Style object for this parameter set. + + This is an alias for self.value.""" + return self.value + + @value.setter + def value(self, new_value): + """Set this parameter set to NEW_VALUE, a gdb.Style object. + + The attributes of NEW_VALUE are used to update the current settings + of this parameter set. If this parameter set was created without + an intensity setting, then the intensity of NEW_VALUE is ignored.""" + if not isinstance(new_value, Style): + raise TypeError("value must be gdb.Style, not %s" % type(new_value)) + self._foreground.value = new_value.foreground + self._background.value = new_value.background + if hasattr(self, "_intensity"): + intensity_value = new_value.intensity + if intensity_value == INTENSITY_BOLD: + intensity_string = "bold" + elif intensity_value == INTENSITY_DIM: + intensity_string = "dim" + elif intensity_value == INTENSITY_NORMAL: + intensity_string = "normal" + else: + raise ValueError( + "unknown intensity value %d from Style" % intensity_value + ) + + self._intensity.value = intensity_string + + @style.setter + def style(self, new_value): + """Set this parameter set to NEW_VALUE, a gdb.Style object. + + This is an alias for self.value.""" + self.value = new_value + + def apply(self, *args, **kwargs): + """Apply this style to the arguments. + + Forwards all arguments to self.style.apply(). The arguments should be a string, + to which this style is applied. This function returns the same string with + escape sequences added to apply this style. + + If styling is globally disabled ('set style enabled off') then no escape sequences + will be added, the input string is returned.""" + return self.style.apply(*args, **kwargs) + + def __repr__(self): + """A string representation of SELF.""" + + def full_typename(obj): + module = type(obj).__module__ + qualname = type(obj).__qualname__ + + if module is None or module == "builtins": + return qualname + else: + return module + "." + qualname + + return "<" + full_typename(self) + " name='" + self._name + "'>" diff --git a/gdb/python/lib/gdb/command/frame_filters.py b/gdb/python/lib/gdb/command/frame_filters.py index be7be9a..3b7cd03 100644 --- a/gdb/python/lib/gdb/command/frame_filters.py +++ b/gdb/python/lib/gdb/command/frame_filters.py @@ -16,8 +16,6 @@ """GDB commands for working with frame-filters.""" -import sys - import gdb import gdb.frames @@ -68,7 +66,11 @@ class InfoFrameFilter(gdb.Command): return 0 print(title) - print(" Priority Enabled Name") + style = gdb.Style("title") + print( + " %s %s %s" + % (style.apply("Priority"), style.apply("Enabled"), style.apply("Name")) + ) for frame_filter in sorted_frame_filters: name = frame_filter[0] try: @@ -77,9 +79,8 @@ class InfoFrameFilter(gdb.Command): self.enabled_string(gdb.frames.get_enabled(frame_filter[1])) ) print(" %s %s %s" % (priority, enabled, name)) - except Exception: - e = sys.exc_info()[1] - print(" Error printing filter '" + name + "': " + str(e)) + except Exception as e: + gdb.warning("Error printing filter '" + name + "': " + str(e)) if blank_line: print("") return 1 @@ -87,14 +88,20 @@ class InfoFrameFilter(gdb.Command): def invoke(self, arg, from_tty): any_printed = self.print_list("global frame-filters:", gdb.frame_filters, True) + file_style = gdb.Style("filename") cp = gdb.current_progspace() + cp_filename = cp.filename + if cp_filename is None: + cp_filename = "<no-file>" + else: + cp_filename = file_style.apply(cp_filename) any_printed += self.print_list( - "progspace %s frame-filters:" % cp.filename, cp.frame_filters, True + "progspace %s frame-filters:" % cp_filename, cp.frame_filters, True ) for objfile in gdb.objfiles(): any_printed += self.print_list( - "objfile %s frame-filters:" % objfile.filename, + "objfile %s frame-filters:" % file_style.apply(objfile.filename), objfile.frame_filters, False, ) diff --git a/gdb/python/lib/gdb/command/missing_files.py b/gdb/python/lib/gdb/command/missing_files.py index 09d9684..b2477ce 100644 --- a/gdb/python/lib/gdb/command/missing_files.py +++ b/gdb/python/lib/gdb/command/missing_files.py @@ -118,10 +118,16 @@ class InfoMissingFileHandlers(gdb.Command): def invoke(self, arg, from_tty): locus_re, name_re = parse_missing_file_command_args(arg) + file_style = gdb.Style("filename") if locus_re.match("progspace") and locus_re.pattern != "": cp = gdb.current_progspace() + cp_filename = cp.filename + if cp.filename is None: + cp_filename = "<no-file>" + else: + cp_filename = file_style.apply(cp_filename) self.list_handlers( - "Progspace %s:" % cp.filename, cp.missing_file_handlers, name_re + "Progspace %s:" % cp_filename, cp.missing_file_handlers, name_re ) for progspace in gdb.progspaces(): @@ -133,7 +139,7 @@ class InfoMissingFileHandlers(gdb.Command): else: msg = "Progspace <no-file>:" else: - msg = "Progspace %s:" % filename + msg = "Progspace %s:" % file_style.apply(filename) self.list_handlers( msg, progspace.missing_file_handlers, diff --git a/gdb/python/lib/gdb/command/pretty_printers.py b/gdb/python/lib/gdb/command/pretty_printers.py index f62d329..cdf9054 100644 --- a/gdb/python/lib/gdb/command/pretty_printers.py +++ b/gdb/python/lib/gdb/command/pretty_printers.py @@ -161,9 +161,15 @@ class InfoPrettyPrinter(gdb.Command): name_re, subname_re, ) + file_style = gdb.Style("filename") cp = gdb.current_progspace() + cp_filename = cp.filename + if cp_filename is None: + cp_filename = "<no-file>" + else: + cp_filename = file_style.apply(cp_filename) self.invoke1( - "progspace %s pretty-printers:" % cp.filename, + "progspace %s pretty-printers:" % cp_filename, cp.pretty_printers, "progspace", object_re, @@ -172,7 +178,7 @@ class InfoPrettyPrinter(gdb.Command): ) for objfile in gdb.objfiles(): self.invoke1( - "objfile %s pretty-printers:" % objfile.filename, + "objfile %s pretty-printers:" % file_style.apply(objfile.filename), objfile.pretty_printers, objfile.filename, object_re, diff --git a/gdb/python/lib/gdb/command/xmethods.py b/gdb/python/lib/gdb/command/xmethods.py index 719c146..2f87074 100644 --- a/gdb/python/lib/gdb/command/xmethods.py +++ b/gdb/python/lib/gdb/command/xmethods.py @@ -101,6 +101,7 @@ def get_method_matchers_in_loci(loci, locus_re, matcher_re): A dict of matching xmethod matchers. The keys of the dict are the filenames of the loci the xmethod matchers belong to. """ + file_style = gdb.Style("filename") xm_dict = {} for locus in loci: if isinstance(locus, gdb.Progspace): @@ -111,7 +112,12 @@ def get_method_matchers_in_loci(loci, locus_re, matcher_re): if not locus_re.match(locus.filename): continue locus_type = "objfile" - locus_str = "%s %s" % (locus_type, locus.filename) + filename = locus.filename + if filename is None: + filename = "<no-file>" + else: + filename = file_style.apply(filename) + locus_str = "%s %s" % (locus_type, filename) xm_dict[locus_str] = [m for m in locus.xmethods if matcher_re.match(m.name)] return xm_dict diff --git a/gdb/python/lib/gdb/dap/__init__.py b/gdb/python/lib/gdb/dap/__init__.py index f908c91..1c3cf8e 100644 --- a/gdb/python/lib/gdb/dap/__init__.py +++ b/gdb/python/lib/gdb/dap/__init__.py @@ -96,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 bfb7bd3..3d1cfef 100644 --- a/gdb/python/lib/gdb/dap/breakpoint.py +++ b/gdb/python/lib/gdb/dap/breakpoint.py @@ -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", @@ -150,8 +147,7 @@ def _remove_entries(table, *names): # specifications and a callback function to do the work of creating # the breakpoint. @in_gdb_thread -def _set_breakpoints_callback(kind, specs, creator): - global breakpoint_map +def _set_breakpoints(kind, specs, creator): # Try to reuse existing breakpoints if possible. if kind in breakpoint_map: saved_map = breakpoint_map[kind] @@ -258,13 +254,6 @@ def _set_one_breakpoint(*, logMessage=None, **args): return gdb.Breakpoint(**args) -# Helper function to set ordinary breakpoints according to a list of -# specifications. -@in_gdb_thread -def _set_breakpoints(kind, specs): - return _set_breakpoints_callback(kind, specs, _set_one_breakpoint) - - # A helper function that rewrites a SourceBreakpoint into the internal # form passed to the creator. This function also allows for # type-checking of each SourceBreakpoint. @@ -306,7 +295,7 @@ def set_breakpoint(*, source, breakpoints: Sequence = (), **args): # Be sure to include the path in the key, so that we only # clear out breakpoints coming from this same source. key = "source:" + source["path"] - result = _set_breakpoints(key, specs) + result = _set_breakpoints(key, specs, _set_one_breakpoint) return { "breakpoints": result, } @@ -330,12 +319,12 @@ 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] return { - "breakpoints": _set_breakpoints("function", specs), + "breakpoints": _set_breakpoints("function", specs, _set_one_breakpoint), } @@ -363,24 +352,26 @@ def _rewrite_insn_breakpoint( } -@request("setInstructionBreakpoints") +@request("setInstructionBreakpoints", expect_stopped=False) @capability("supportsInstructionBreakpoints") def set_insn_breakpoints( *, breakpoints: Sequence, offset: Optional[int] = None, **args ): specs = [_rewrite_insn_breakpoint(**bp) for bp in breakpoints] return { - "breakpoints": _set_breakpoints("instruction", specs), + "breakpoints": _set_breakpoints("instruction", specs, _set_one_breakpoint), } @in_gdb_thread def _catch_exception(filterId, **args): if filterId in ("assert", "exception", "throw", "rethrow", "catch"): - cmd = "-catch-" + filterId + cmd = ["-catch-" + filterId] else: raise DAPException("Invalid exception filterID: " + str(filterId)) - result = exec_mi_and_log(cmd) + if "exception" in args and args["exception"] is not None: + cmd += ["-e", args["exception"]] + result = exec_mi_and_log(*cmd) # While the Ada catchpoints emit a "bkptno" field here, the C++ # ones do not. So, instead we look at the "number" field. num = result["bkpt"]["number"] @@ -392,11 +383,6 @@ def _catch_exception(filterId, **args): raise Exception("Could not find catchpoint after creating") -@in_gdb_thread -def _set_exception_catchpoints(filter_options): - return _set_breakpoints_callback("exception", filter_options, _catch_exception) - - # A helper function that rewrites an ExceptionFilterOptions into the # internal form passed to the creator. This function also allows for # type-checking of each ExceptionFilterOptions. @@ -408,13 +394,20 @@ def _rewrite_exception_breakpoint( # Note that exception breakpoints do not support a hit count. **args, ): + if filterId == "exception": + # Treat Ada exceptions specially -- in particular the + # condition is just an exception name, not an expression. + return { + "filterId": filterId, + "exception": condition, + } return { "filterId": filterId, "condition": condition, } -@request("setExceptionBreakpoints") +@request("setExceptionBreakpoints", expect_stopped=False) @capability("supportsExceptionFilterOptions") @capability( "exceptionBreakpointFilters", @@ -453,6 +446,4 @@ def set_exception_breakpoints( options = [{"filterId": filter} for filter in filters] options.extend(filterOptions) options = [_rewrite_exception_breakpoint(**bp) for bp in options] - return { - "breakpoints": _set_exception_catchpoints(options), - } + return {"breakpoints": _set_breakpoints("exception", options, _catch_exception)} diff --git a/gdb/python/lib/gdb/dap/completions.py b/gdb/python/lib/gdb/dap/completions.py index 85acc43..e5003ff 100644 --- a/gdb/python/lib/gdb/dap/completions.py +++ b/gdb/python/lib/gdb/dap/completions.py @@ -39,8 +39,11 @@ def completions( line = 1 else: line = import_line(line) - text = text.splitlines()[line - 1] - text = text[: column - 1] + if text: + text = text.splitlines()[line - 1] + text = text[: column - 1] + else: + text = "" mi_result = exec_mi_and_log("-complete", text) result = [] completion = None diff --git a/gdb/python/lib/gdb/dap/evaluate.py b/gdb/python/lib/gdb/dap/evaluate.py index 55538e6..fcbcc99 100644 --- a/gdb/python/lib/gdb/dap/evaluate.py +++ b/gdb/python/lib/gdb/dap/evaluate.py @@ -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 54caea0..778acc5 100644 --- a/gdb/python/lib/gdb/dap/events.py +++ b/gdb/python/lib/gdb/dap/events.py @@ -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 @@ -161,7 +161,7 @@ _expected_pause = False @in_gdb_thread -def exec_and_expect_stop(cmd, expected_pause=False, propagate_exception=False): +def exec_and_expect_stop(cmd, expected_pause=False): """A wrapper for exec_and_log that sets the continue-suppression flag. When EXPECTED_PAUSE is True, a stop that looks like a pause (e.g., @@ -174,7 +174,7 @@ def exec_and_expect_stop(cmd, expected_pause=False, propagate_exception=False): # continuing. _suppress_cont = not expected_pause # FIXME if the call fails should we clear _suppress_cont? - exec_and_log(cmd, propagate_exception) + exec_and_log(cmd) # Map from gdb stop reasons to DAP stop reasons. Some of these can't @@ -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 6433dbe..4dacb87 100644 --- a/gdb/python/lib/gdb/dap/frames.py +++ b/gdb/python/lib/gdb/dap/frames.py @@ -53,12 +53,11 @@ gdb.events.cont.connect(_clear_frame_ids) @in_gdb_thread def frame_for_id(id): """Given a frame identifier ID, return the corresponding frame.""" - global thread_ids if id in thread_ids: thread_id = thread_ids[id] if thread_id != gdb.selected_thread().global_num: set_thread(thread_id) - global _all_frames + return _all_frames[id] @@ -103,10 +102,8 @@ def _frame_id_generator(): # Helper function to assign an ID to a frame. def get_id(frame): - global _all_frames num = len(_all_frames) _all_frames.append(frame) - global thread_ids thread_ids[num] = gdb.selected_thread().global_num return num @@ -128,7 +125,6 @@ def _frame_id_generator(): @in_gdb_thread def _get_frame_iterator(): thread_id = gdb.selected_thread().global_num - global _iter_map if thread_id not in _iter_map: _iter_map[thread_id] = _MemoizingIterator(_frame_id_generator()) return _iter_map[thread_id] diff --git a/gdb/python/lib/gdb/dap/globalvars.py b/gdb/python/lib/gdb/dap/globalvars.py index 2e4b2a6..9d64d28 100644 --- a/gdb/python/lib/gdb/dap/globalvars.py +++ b/gdb/python/lib/gdb/dap/globalvars.py @@ -78,7 +78,6 @@ def get_global_scope(frame): except RuntimeError: return None - global _id_to_scope block = block.static_block if block in _id_to_scope: return _id_to_scope[block] diff --git a/gdb/python/lib/gdb/dap/launch.py b/gdb/python/lib/gdb/dap/launch.py index e093e60..8ac4c77 100644 --- a/gdb/python/lib/gdb/dap/launch.py +++ b/gdb/python/lib/gdb/dap/launch.py @@ -164,7 +164,6 @@ def attach( @request("configurationDone", on_dap_thread=True) def config_done(**args): # Handle the launch or attach. - global _launch_or_attach_promise if _launch_or_attach_promise is None: raise DAPException("launch or attach not specified") # Resolve the launch or attach, but only after the diff --git a/gdb/python/lib/gdb/dap/next.py b/gdb/python/lib/gdb/dap/next.py index ddf4e05..993e9d2 100644 --- a/gdb/python/lib/gdb/dap/next.py +++ b/gdb/python/lib/gdb/dap/next.py @@ -16,8 +16,8 @@ import gdb from .events import exec_and_expect_stop -from .server import capability, request, send_gdb, send_gdb_with_response -from .startup import in_gdb_thread +from .server import capability, request +from .startup import DAPException, exec_and_log, in_gdb_thread from .state import set_thread @@ -36,11 +36,9 @@ def _handle_thread_step(thread_id, single_thread, select=False): result = False arg = "off" try: - # This can fail, depending on the target, so catch the error - # and report to our caller. We can't use exec_and_log because - # that does not propagate exceptions. - gdb.execute("set scheduler-locking " + arg, from_tty=True, to_string=True) - except gdb.error: + # This can fail, depending on the target, so catch any error. + exec_and_log("set scheduler-locking " + arg) + except DAPException: result = False # Other DAP code may select a frame, and the "finish" command uses # the selected frame. @@ -73,19 +71,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) + exec_and_expect_stop("finish &") -# 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/scopes.py b/gdb/python/lib/gdb/dap/scopes.py index f499dcb..7ce3a7f 100644 --- a/gdb/python/lib/gdb/dap/scopes.py +++ b/gdb/python/lib/gdb/dap/scopes.py @@ -120,7 +120,6 @@ class _FinishScopeReference(_ScopeReference): def fetch_one_child(self, idx): assert idx == 0 - global _last_return_value return ("(return)", _last_return_value) @@ -145,8 +144,6 @@ class _RegisterReference(_ScopeReference): @request("scopes") def scopes(*, frameId: int, **extra): - global _last_return_value - global frame_to_scope if frameId in frame_to_scope: scopes = frame_to_scope[frameId] else: diff --git a/gdb/python/lib/gdb/dap/server.py b/gdb/python/lib/gdb/dap/server.py index d4314e8..98a8084 100644 --- a/gdb/python/lib/gdb/dap/server.py +++ b/gdb/python/lib/gdb/dap/server.py @@ -74,6 +74,13 @@ class DeferredRequest: self._result = result @in_dap_thread + def defer_events(self): + """Return True if events should be deferred during execution. + + This may be overridden by subclasses.""" + return True + + @in_dap_thread def invoke(self): """Implement the deferred request. @@ -95,7 +102,10 @@ class DeferredRequest: """ with _server.canceller.current_request(self._req): + if self.defer_events(): + _server.set_defer_events() _server.invoke_request(self._req, self._result, self.invoke) + _server.emit_pending_events() # A subclass of Exception that is used solely for reporting that a @@ -230,7 +240,7 @@ class Server: self._out_stream = out_stream self._child_stream = child_stream self._delayed_fns_lock = threading.Lock() - self.defer_stop_events = False + 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 @@ -307,7 +317,6 @@ class Server: args = {} def fn(): - global _commands return _commands[params["command"]](**args) self.invoke_request(req, result, fn) @@ -317,7 +326,7 @@ class Server: def _read_inferior_output(self): while True: line = self._child_stream.readline() - self.send_event( + self.send_event_maybe_later( "output", { "category": "stdout", @@ -357,6 +366,17 @@ class Server: 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): """The main loop of the DAP server.""" # Before looping, start the thread that writes JSON to the @@ -372,28 +392,22 @@ class Server: req = cmd["seq"] with self.canceller.current_request(req): self._handle_command(cmd) - fns = None - with self._delayed_fns_lock: - fns = self._delayed_fns - self._delayed_fns = [] - self.defer_stop_events = False - for fn in fns: - fn() + self.emit_pending_events() # Got the terminate request. This is handled by the # JSON-writing thread, so that we can ensure that all # responses are flushed to the client before exiting. self._write_queue.put(None) json_writer.join() - send_gdb("quit") + send_gdb(lambda: exec_and_log("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.""" + def set_defer_events(self): + """Defer any events until the current request has completed.""" with self._delayed_fns_lock: - self._delayed_fns.append(lambda: self.send_event(event, body)) + 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, @@ -402,10 +416,10 @@ class Server: with self.canceller.lock: if self.canceller.in_flight_dap_thread: with self._delayed_fns_lock: - if self.defer_stop_events: - self._delayed_fns.append(lambda: self.send_event(event, body)) + 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): @@ -416,7 +430,7 @@ class Server: # 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.""" @@ -440,22 +454,11 @@ def send_event(event, body=None): """Send an event to the DAP client. EVENT is the name of the event, a string. BODY is the body of the event, an arbitrary object.""" - global _server - _server.send_event(event, body) - - -def send_event_maybe_later(event, body=None): - """Send a DAP event back to the client, but if a request is in-flight - within the dap thread and that request is configured to delay the event, - wait until the response has been sent until the event is sent back to - the client.""" - global _server _server.send_event_maybe_later(event, body) def call_function_later(fn): """Call FN later -- after the current request's response has been sent.""" - global _server _server.call_function_later(fn) @@ -480,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. @@ -502,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. @@ -527,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 @@ -555,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 @@ -568,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 @@ -581,7 +588,6 @@ def client_bool_capability(name, default=False): If the capability was not specified, or did not have boolean type, DEFAULT is returned. DEFAULT defaults to False.""" - global _server if name in _server.config and isinstance(_server.config[name], bool): return _server.config[name] return default @@ -589,9 +595,8 @@ def client_bool_capability(name, default=False): @request("initialize", on_dap_thread=True) def initialize(**args): - global _server, _capabilities _server.config = args - _server.send_event_later("initialized") + _server.send_event_maybe_later("initialized") global _lines_start_at_1 _lines_start_at_1 = client_bool_capability("linesStartAt1", True) global _columns_start_at_1 @@ -609,7 +614,7 @@ def terminate(**args): @capability("supportTerminateDebuggee") def disconnect(*, terminateDebuggee: bool = False, **args): if terminateDebuggee: - send_gdb_with_response("kill") + send_gdb_with_response(lambda: exec_and_log("kill")) _server.shutdown() @@ -628,18 +633,6 @@ def cancel(**args): return None -class Invoker(object): - """A simple class that can invoke a gdb command.""" - - def __init__(self, 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) - - class Cancellable(object): def __init__(self, fn, result_q=None): @@ -672,25 +665,16 @@ class Cancellable(object): def send_gdb(cmd): """Send CMD to the gdb thread. - CMD can be either a function or a string. - If it is a string, it is passed to gdb.execute.""" - if isinstance(cmd, str): - cmd = Invoker(cmd) - + CMD is a function.""" # Post the event and don't wait for the result. gdb.post_event(Cancellable(cmd)) def send_gdb_with_response(fn): """Send FN to the gdb thread and return its result. - If FN is a string, it is passed to gdb.execute and None is - returned as the result. If FN throws an exception, this function will throw the same exception in the calling thread. """ - if isinstance(fn, str): - fn = Invoker(fn) - # Post the event and wait for the result in result_q. result_q = DAPQueue() gdb.post_event(Cancellable(fn, result_q)) @@ -705,7 +689,6 @@ def export_line(line: int) -> int: """Rewrite LINE according to client capability. This applies the linesStartAt1 capability as needed, when sending a line number from gdb to the client.""" - global _lines_start_at_1 if not _lines_start_at_1: # In gdb, lines start at 1, so we only need to change this if # the client starts at 0. @@ -717,7 +700,6 @@ def import_line(line: int) -> int: """Rewrite LINE according to client capability. This applies the linesStartAt1 capability as needed, when the client sends a line number to gdb.""" - global _lines_start_at_1 if not _lines_start_at_1: # In gdb, lines start at 1, so we only need to change this if # the client starts at 0. diff --git a/gdb/python/lib/gdb/dap/sources.py b/gdb/python/lib/gdb/dap/sources.py index 938c93a..efcd799 100644 --- a/gdb/python/lib/gdb/dap/sources.py +++ b/gdb/python/lib/gdb/dap/sources.py @@ -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 ab3e8fd..0c95ada 100644 --- a/gdb/python/lib/gdb/dap/startup.py +++ b/gdb/python/lib/gdb/dap/startup.py @@ -204,7 +204,7 @@ def log_stack(level=LogLevel.DEFAULT): @in_gdb_thread -def exec_and_log(cmd, propagate_exception=False): +def exec_and_log(cmd): """Execute the gdb command CMD. If logging is enabled, log the command and its output.""" log("+++ " + cmd) @@ -213,10 +213,10 @@ def exec_and_log(cmd, propagate_exception=False): if output != "": log(">>> " + output) except gdb.error as e: - if propagate_exception: - raise DAPException(str(e)) from e - else: - log_stack() + # Don't normally want to see this, as it interferes with the + # test suite. + log_stack(LogLevel.FULL) + raise DAPException(str(e)) from e @in_gdb_thread diff --git a/gdb/python/lib/gdb/dap/threads.py b/gdb/python/lib/gdb/dap/threads.py index c271961..89046a8 100644 --- a/gdb/python/lib/gdb/dap/threads.py +++ b/gdb/python/lib/gdb/dap/threads.py @@ -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/varref.py b/gdb/python/lib/gdb/dap/varref.py index 42b5302..d18197b 100644 --- a/gdb/python/lib/gdb/dap/varref.py +++ b/gdb/python/lib/gdb/dap/varref.py @@ -69,7 +69,6 @@ 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 @@ -147,6 +146,10 @@ class BaseReference(ABC): if self._children is None: self._children = [None] * self.child_count() for idx in range(start, start + count): + if idx >= len(self._children): + raise DAPException( + f"requested child {idx} outside range of variable {self._ref}" + ) if self._children[idx] is None: (name, value) = self.fetch_one_child(idx) name = self._compute_name(name) @@ -243,7 +246,11 @@ 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: + if ( + self._value.type.strip_typedefs().code == gdb.TYPE_CODE_PTR + and not self._value.is_optimized_out + and not self._value.is_unavailable + ): result["memoryReference"] = hex(int(self._value)) if client_bool_capability("supportsVariableType"): result["type"] = str(self._value.type) @@ -267,7 +274,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 8f8e768..5956678 100644 --- a/gdb/python/lib/gdb/disassembler.py +++ b/gdb/python/lib/gdb/disassembler.py @@ -155,9 +155,21 @@ class maint_info_py_disassemblers_cmd(gdb.Command): # Now print the dictionary of registered disassemblers out to # the user. match_tag = "\t(Matches current architecture)" - fmt_len = max(longest_arch_name, len("Architecture")) - format_string = "{:" + str(fmt_len) + "s} {:s}" - print(format_string.format("Architecture", "Disassember Name")) + arch_title = "Architecture" + fmt_len = max(longest_arch_name, len(arch_title)) + format_string = "{:" + str(fmt_len) + "s} {:s}" + padding_string = " " * (fmt_len - len(arch_title)) + title_style = gdb.Style("title") + # We cannot use FORMAT_STRING to layout the title line, as + # Python is unable to calculate the length of a styled string. + # Instead use PADDING_STRING to manually layout the columns. + print( + "{:s}{:s} {:s}".format( + title_style.apply(arch_title), + padding_string, + title_style.apply("Disassember Name"), + ) + ) for architecture in _disassemblers_dict: if architecture is not None: name = _disassemblers_dict[architecture].name diff --git a/gdb/python/lib/gdb/printing.py b/gdb/python/lib/gdb/printing.py index cba27d2..f1ac19d 100644 --- a/gdb/python/lib/gdb/printing.py +++ b/gdb/python/lib/gdb/printing.py @@ -415,10 +415,21 @@ def make_visualizer(value): result = NoOpArrayPrinter(ty, value) elif ty.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION): result = NoOpStructPrinter(ty, value) - elif ty.code in ( - gdb.TYPE_CODE_PTR, - gdb.TYPE_CODE_REF, - gdb.TYPE_CODE_RVALUE_REF, + elif ( + ty.code + in ( + gdb.TYPE_CODE_PTR, + gdb.TYPE_CODE_REF, + gdb.TYPE_CODE_RVALUE_REF, + ) + # Avoid "void *" here because those pointers can't be + # dereferenced without a cast. + and ty.target().code != gdb.TYPE_CODE_VOID + # An optimized-out or unavailable pointer should just be + # treated as a scalar, since there's no way to dereference + # it. + and not value.is_optimized_out + and not value.is_unavailable ): result = NoOpPointerReferencePrinter(value) else: diff --git a/gdb/python/lib/gdb/styling.py b/gdb/python/lib/gdb/styling.py index e9387e3..60c470f 100644 --- a/gdb/python/lib/gdb/styling.py +++ b/gdb/python/lib/gdb/styling.py @@ -80,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()) |