diff options
-rw-r--r-- | gdb/Makefile.in | 1 | ||||
-rw-r--r-- | gdb/NEWS | 2 | ||||
-rw-r--r-- | gdb/doc/python.texi | 168 | ||||
-rw-r--r-- | gdb/mi/mi-cmds.c | 23 | ||||
-rw-r--r-- | gdb/mi/mi-cmds.h | 18 | ||||
-rw-r--r-- | gdb/python/lib/gdb/__init__.py | 4 | ||||
-rw-r--r-- | gdb/python/py-micmd.c | 812 | ||||
-rw-r--r-- | gdb/python/py-utils.c | 17 | ||||
-rw-r--r-- | gdb/python/python-internal.h | 13 | ||||
-rw-r--r-- | gdb/python/python.c | 3 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-mi-cmd.exp | 390 | ||||
-rw-r--r-- | gdb/testsuite/gdb.python/py-mi-cmd.py | 120 |
12 files changed, 1553 insertions, 18 deletions
diff --git a/gdb/Makefile.in b/gdb/Makefile.in index db0125b..9bca9b0 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -409,6 +409,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-lazy-string.c \ python/py-linetable.c \ python/py-membuf.c \ + python/py-micmd.c \ python/py-newobjfileevent.c \ python/py-objfile.c \ python/py-param.c \ @@ -250,6 +250,8 @@ GNU/Linux/LoongArch loongarch*-*-linux* for signed types, and False for all other types. Attempting to read this attribute for non-scalar types will raise a ValueError. + ** It is now possible to add GDB/MI commands implemented in Python. + * New features in the GDB remote stub, GDBserver ** GDBserver is now supported on OpenRISC GNU/Linux. diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index b6acda4..918418b 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -95,6 +95,7 @@ containing @code{end}. For example: 23 @end smallexample +@anchor{set_python_print_stack} @kindex set python print-stack @item set python print-stack By default, @value{GDBN} will print only the message component of a @@ -204,7 +205,8 @@ optional arguments while skipping others. Example: * Events In Python:: Listening for events from @value{GDBN}. * Threads In Python:: Accessing inferior threads from Python. * Recordings In Python:: Accessing recordings from Python. -* Commands In Python:: Implementing new commands in Python. +* CLI Commands In Python:: Implementing new CLI commands in Python. +* GDB/MI Commands In Python:: Implementing new @sc{GDB/MI} commands in Python. * Parameters In Python:: Adding new @value{GDBN} parameters. * Functions In Python:: Writing new convenience functions. * Progspaces In Python:: Program spaces. @@ -419,7 +421,8 @@ the current language, evaluate it, and return the result as a @code{gdb.Value}. This function can be useful when implementing a new command -(@pxref{Commands In Python}), as it provides a way to parse the +(@pxref{CLI Commands In Python}, @pxref{GDB/MI Commands In Python}), +as it provides a way to parse the command's argument as an expression. It is also useful simply to compute values. @end defun @@ -2162,7 +2165,7 @@ must contain the frames that are being elided wrapped in a suitable frame decorator. If no frames are being elided this function may return an empty iterable, or @code{None}. Elided frames are indented from normal frames in a @code{CLI} backtrace, or in the case of -@code{GDB/MI}, are placed in the @code{children} field of the eliding +@sc{GDB/MI}, are placed in the @code{children} field of the eliding frame. It is the frame filter's task to also filter out the elided frames from @@ -3883,11 +3886,12 @@ def countrange (filename, linerange): return count @end smallexample -@node Commands In Python -@subsubsection Commands In Python +@node CLI Commands In Python +@subsubsection CLI Commands In Python -@cindex commands in python -@cindex python commands +@cindex CLI commands in python +@cindex commands in python, CLI +@cindex python commands, CLI You can implement new @value{GDBN} CLI commands in Python. A CLI command is implemented using an instance of the @code{gdb.Command} class, most commonly using a subclass. @@ -4166,6 +4170,154 @@ registration of the command with @value{GDBN}. Depending on how the Python code is read into @value{GDBN}, you may need to import the @code{gdb} module explicitly. +@node GDB/MI Commands In Python +@subsubsection @sc{GDB/MI} Commands In Python + +@cindex MI commands in python +@cindex commands in python, GDB/MI +@cindex python commands, GDB/MI +It is possible to add @sc{GDB/MI} (@pxref{GDB/MI}) commands +implemented in Python. A @sc{GDB/MI} command is implemented using an +instance of the @code{gdb.MICommand} class, most commonly using a +subclass. + +@defun MICommand.__init__ (name) +The object initializer for @code{MICommand} registers the new command +with @value{GDBN}. This initializer is normally invoked from the +subclass' own @code{__init__} method. + +@var{name} is the name of the command. It must be a valid name of a +@sc{GDB/MI} command, and in particular must start with a hyphen +(@code{-}). Reusing the name of a built-in @sc{GDB/MI} is not +allowed, and a @code{RuntimeError} will be raised. Using the name +of an @sc{GDB/MI} command previously defined in Python is allowed, the +previous command will be replaced with the new command. +@end defun + +@defun MICommand.invoke (arguments) +This method is called by @value{GDBN} when the new MI command is +invoked. + +@var{arguments} is a list of strings. Note, that @code{--thread} +and @code{--frame} arguments are handled by @value{GDBN} itself therefore +they do not show up in @code{arguments}. + +If this method raises an exception, then it is turned into a +@sc{GDB/MI} @code{^error} response. Only @code{gdb.GdbError} +exceptions (or its sub-classes) should be used for reporting errors to +users, any other exception type is treated as a failure of the +@code{invoke} method, and the exception will be printed to the error +stream according to the @kbd{set python print-stack} setting +(@pxref{set_python_print_stack,,@kbd{set python print-stack}}). + +If this method returns @code{None}, then the @sc{GDB/MI} command will +return a @code{^done} response with no additional values. + +Otherwise, the return value must be a dictionary, which is converted +to a @sc{GDB/MI} @var{result-record} (@pxref{GDB/MI Output Syntax}). +The keys of this dictionary must be strings, and are used as +@var{variable} names in the @var{result-record}, these strings must +comply with the naming rules detailed below. The values of this +dictionary are recursively handled as follows: + +@itemize +@item +If the value is Python sequence or iterator, it is converted to +@sc{GDB/MI} @var{list} with elements converted recursively. + +@item +If the value is Python dictionary, it is converted to +@sc{GDB/MI} @var{tuple}. Keys in that dictionary must be strings, +which comply with the @var{variable} naming rules detailed below. +Values are converted recursively. + +@item +Otherwise, value is first converted to a Python string using +@code{str ()} and then converted to @sc{GDB/MI} @var{const}. +@end itemize + +The strings used for @var{variable} names in the @sc{GDB/MI} output +must follow the following rules; the string must be at least one +character long, the first character must be in the set +@code{[a-zA-Z]}, while every subsequent character must be in the set +@code{[-_a-zA-Z0-9]}. +@end defun + +An instance of @code{MICommand} has the following attributes: + +@defvar MICommand.name +A string, the name of this @sc{GDB/MI} command, as was passed to the +@code{__init__} method. This attribute is read-only. +@end defvar + +@defvar MICommand.installed +A boolean value indicating if this command is installed ready for a +user to call from the command line. Commands are automatically +installed when they are instantiated, after which this attribute will +be @code{True}. + +If later, a new command is created with the same name, then the +original command will become uninstalled, and this attribute will be +@code{False}. + +This attribute is read-write, setting this attribute to @code{False} +will uninstall the command, removing it from the set of available +commands. Setting this attribute to @code{True} will install the +command for use. If there is already a Python command with this name +installed, the currently installed command will be uninstalled, and +this command installed in its place. +@end defvar + +The following code snippet shows how a two trivial MI command can be +implemented in Python: + +@smallexample +class MIEcho(gdb.MICommand): + """Echo arguments passed to the command.""" + + def __init__(self, name, mode): + self._mode = mode + super(MIEcho, self).__init__(name) + + def invoke(self, argv): + if self._mode == 'dict': + return @{ 'dict': @{ 'argv' : argv @} @} + elif self._mode == 'list': + return @{ 'list': argv @} + else: + return @{ 'string': ", ".join(argv) @} + + +MIEcho("-echo-dict", "dict") +MIEcho("-echo-list", "list") +MIEcho("-echo-string", "string") +@end smallexample + +The last three lines instantiate the class three times, creating three +new @sc{GDB/MI} commands @code{-echo-dict}, @code{-echo-list}, and +@code{-echo-string}. Each time a subclass of @code{gdb.MICommand} is +instantiated, the new command is automatically registered with +@value{GDBN}. + +Depending on how the Python code is read into @value{GDBN}, you may +need to import the @code{gdb} module explicitly. + +The following example shows a @value{GDBN} session in which the above +commands have been added: + +@smallexample +(@value{GDBP}) +-echo-dict abc def ghi +^done,dict=@{argv=["abc","def","ghi"]@} +(@value{GDBP}) +-echo-list abc def ghi +^done,list=["abc","def","ghi"] +(@value{GDBP}) +-echo-string abc def ghi +^done,string="abc, def, ghi" +(@value{GDBP}) +@end smallexample + @node Parameters In Python @subsubsection Parameters In Python @@ -4203,7 +4355,7 @@ If @var{name} consists of multiple words, and no prefix parameter group can be found, an exception is raised. @var{command-class} should be one of the @samp{COMMAND_} constants -(@pxref{Commands In Python}). This argument tells @value{GDBN} how to +(@pxref{CLI Commands In Python}). This argument tells @value{GDBN} how to categorize the new parameter in the help system. @var{parameter-class} should be one of the @samp{PARAM_} constants diff --git a/gdb/mi/mi-cmds.c b/gdb/mi/mi-cmds.c index cd7cabd..38fbe0d 100644 --- a/gdb/mi/mi-cmds.c +++ b/gdb/mi/mi-cmds.c @@ -26,10 +26,6 @@ #include <map> #include <string> -/* A command held in the MI_CMD_TABLE. */ - -using mi_command_up = std::unique_ptr<struct mi_command>; - /* MI command table (built at run time). */ static std::map<std::string, mi_command_up> mi_cmd_table; @@ -108,12 +104,9 @@ private: bool m_args_p; }; -/* Insert COMMAND into the global mi_cmd_table. Return false if - COMMAND->name already exists in mi_cmd_table, in which case COMMAND will - not have been added to mi_cmd_table. Otherwise, return true, and - COMMAND was added to mi_cmd_table. */ +/* See mi-cmds.h. */ -static bool +bool insert_mi_cmd_entry (mi_command_up command) { gdb_assert (command != nullptr); @@ -127,6 +120,18 @@ insert_mi_cmd_entry (mi_command_up command) return true; } +/* See mi-cmds.h. */ + +bool +remove_mi_cmd_entry (const std::string &name) +{ + if (mi_cmd_table.find (name) == mi_cmd_table.end ()) + return false; + + mi_cmd_table.erase (name); + return true; +} + /* Create and register a new MI command with an MI specific implementation. NAME must name an MI command that does not already exist, otherwise an assertion will trigger. */ diff --git a/gdb/mi/mi-cmds.h b/gdb/mi/mi-cmds.h index 785652e..47b90a2 100644 --- a/gdb/mi/mi-cmds.h +++ b/gdb/mi/mi-cmds.h @@ -199,6 +199,10 @@ private: int *m_suppress_notification; }; +/* A command held in the global mi_cmd_table. */ + +using mi_command_up = std::unique_ptr<struct mi_command>; + /* Lookup a command in the MI command table, returns nullptr if COMMAND is not found. */ @@ -206,4 +210,18 @@ extern mi_command *mi_cmd_lookup (const char *command); extern void mi_execute_command (const char *cmd, int from_tty); +/* Insert COMMAND into the global mi_cmd_table. Return false if + COMMAND->name already exists in mi_cmd_table, in which case COMMAND will + not have been added to mi_cmd_table. Otherwise, return true, and + COMMAND was added to mi_cmd_table. */ + +extern bool insert_mi_cmd_entry (mi_command_up command); + +/* Remove the command called NAME from the global mi_cmd_table. Return + true if the removal was a success, otherwise return false, which + indicates no command called NAME was found in the mi_cmd_table. */ + +extern bool remove_mi_cmd_entry (const std::string &name); + + #endif /* MI_MI_CMDS_H */ diff --git a/gdb/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py index 5f63bce..3294583 100644 --- a/gdb/python/lib/gdb/__init__.py +++ b/gdb/python/lib/gdb/__init__.py @@ -82,6 +82,10 @@ frame_filters = {} # Initial frame unwinders. frame_unwinders = [] +# Dictionary containing all user created MI commands, the key is the +# command name, and the value is the gdb.MICommand object. +_mi_commands = {} + def _execute_unwinders(pending_frame): """Internal function called from GDB to execute all unwinders. diff --git a/gdb/python/py-micmd.c b/gdb/python/py-micmd.c new file mode 100644 index 0000000..4665fcc --- /dev/null +++ b/gdb/python/py-micmd.c @@ -0,0 +1,812 @@ +/* MI Command Set for GDB, the GNU debugger. + + Copyright (C) 2019-2022 Free Software Foundation, Inc. + + This file is part of GDB. + + 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/>. */ + +/* GDB/MI commands implemented in Python. */ + +#include "defs.h" +#include "python-internal.h" +#include "arch-utils.h" +#include "charset.h" +#include "language.h" +#include "mi/mi-cmds.h" +#include "mi/mi-parse.h" +#include "cli/cli-cmds.h" +#include <string> + +/* Debugging of Python MI commands. */ + +static bool pymicmd_debug; + +/* Implementation of "show debug py-micmd". */ + +static void +show_pymicmd_debug (struct ui_file *file, int from_tty, + struct cmd_list_element *c, const char *value) +{ + fprintf_filtered (file, _("Python MI command debugging is %s.\n"), value); +} + +/* Print a "py-micmd" debug statement. */ + +#define pymicmd_debug_printf(fmt, ...) \ + debug_prefixed_printf_cond (pymicmd_debug, "py-micmd", fmt, ##__VA_ARGS__) + +/* Print a "py-micmd" enter/exit debug statements. */ + +#define PYMICMD_SCOPED_DEBUG_ENTER_EXIT \ + scoped_debug_enter_exit (pymicmd_debug, "py-micmd") + +struct mi_command_py; + +/* Representation of a Python gdb.MICommand object. */ + +struct micmdpy_object +{ + PyObject_HEAD + + /* The object representing this command in the MI command table. This + pointer can be nullptr if the command is not currently installed into + the MI command table (see gdb.MICommand.installed property). */ + struct mi_command_py *mi_command; + + /* The string representing the name of this command, without the leading + dash. This string is never nullptr once the Python object has been + initialised. + + The memory for this string was allocated with malloc, and needs to be + deallocated with free when the Python object is deallocated. + + When the MI_COMMAND field is not nullptr, then the mi_command_py + object's name will point back to this string. */ + char *mi_command_name; +}; + +/* The MI command implemented in Python. */ + +struct mi_command_py : public mi_command +{ + /* Constructs a new mi_command_py object. NAME is command name without + leading dash. OBJECT is a reference to a Python object implementing + the command. This object must inherit from gdb.MICommand and must + implement the invoke method. */ + + mi_command_py (const char *name, micmdpy_object *object) + : mi_command (name, nullptr), + m_pyobj (object) + { + pymicmd_debug_printf ("this = %p", this); + } + + ~mi_command_py () + { + /* The Python object representing a MI command contains a pointer back + to this c++ object. We can safely set this pointer back to nullptr + now, to indicate the Python object no longer references a valid c++ + object. + + However, the Python object also holds the storage for our name + string. We can't clear that here as our parent's destructor might + still want to reference that string. Instead we rely on the Python + object deallocator to free that memory, and reset the pointer. */ + m_pyobj->mi_command = nullptr; + + pymicmd_debug_printf ("this = %p", this); + }; + + /* Validate that CMD_OBJ, a non-nullptr pointer, is installed into the MI + command table correctly. This function looks up the command in the MI + command table and checks that the object we get back references + CMD_OBJ. This function is only intended for calling within a + gdb_assert. This function performs many assertions internally, and + then always returns true. */ + static void validate_installation (micmdpy_object *cmd_obj); + + /* Update m_pyobj to NEW_PYOBJ. The pointer from M_PYOBJ that points + back to this object is swapped with the pointer in NEW_PYOBJ, which + must be nullptr, so that NEW_PYOBJ now points back to this object. + Additionally our parent's name string is stored in m_pyobj, so we + swap the name string with NEW_PYOBJ. + + Before this call m_pyobj is the Python object representing this MI + command object. After this call has completed, NEW_PYOBJ now + represents this MI command object. */ + void swap_python_object (micmdpy_object *new_pyobj) + { + gdb_assert (new_pyobj->mi_command == nullptr); + std::swap (new_pyobj->mi_command, m_pyobj->mi_command); + std::swap (new_pyobj->mi_command_name, m_pyobj->mi_command_name); + m_pyobj = new_pyobj; + } + +protected: + /* Called when the MI command is invoked. */ + virtual void do_invoke(struct mi_parse *parse) const override; + +private: + /* The Python object representing this MI command. */ + micmdpy_object *m_pyobj; +}; + +extern PyTypeObject micmdpy_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("micmdpy_object"); + +/* Holds a Python object containing the string 'invoke'. */ + +static PyObject *invoke_cst; + +/* Convert KEY_OBJ into a string that can be used as a field name in MI + output. KEY_OBJ must be a Python string object, and must only contain + characters suitable for use as an MI field name. + + If KEY_OBJ is not a string, or if KEY_OBJ contains invalid characters, + then an error is thrown. Otherwise, KEY_OBJ is converted to a string + and returned. */ + +static gdb::unique_xmalloc_ptr<char> +py_object_to_mi_key (PyObject *key_obj) +{ + /* The key must be a string. */ + if (!PyString_Check (key_obj)) + { + gdbpy_ref<> key_repr (PyObject_Repr (key_obj)); + gdb::unique_xmalloc_ptr<char> key_repr_string; + if (key_repr != nullptr) + key_repr_string = python_string_to_target_string (key_repr.get ()); + if (key_repr_string == nullptr) + gdbpy_handle_exception (); + + gdbpy_error (_("non-string object used as key: %s"), + key_repr_string.get ()); + } + + gdb::unique_xmalloc_ptr<char> key_string + = python_string_to_target_string (key_obj); + if (key_string == nullptr) + gdbpy_handle_exception (); + + /* Predicate function, returns true if NAME is a valid field name for use + in MI result output, otherwise, returns false. */ + auto is_valid_key_name = [] (const char *name) -> bool + { + gdb_assert (name != nullptr); + + if (*name == '\0' || !isalpha (*name)) + return false; + + for (; *name != '\0'; ++name) + if (!isalnum (*name) && *name != '_' && *name != '-') + return false; + + return true; + }; + + if (!is_valid_key_name (key_string.get ())) + { + if (*key_string.get () == '\0') + gdbpy_error (_("Invalid empty key in MI result")); + else + gdbpy_error (_("Invalid key in MI result: %s"), key_string.get ()); + } + + return key_string; +} + +/* Serialize RESULT and print it in MI format to the current_uiout. + FIELD_NAME is used as the name of this result field. + + RESULT can be a dictionary, a sequence, an iterator, or an object that + can be converted to a string, these are converted to the matching MI + output format (dictionaries as tuples, sequences and iterators as lists, + and strings as named fields). + + If anything goes wrong while formatting the output then an error is + thrown. + + This function is the recursive inner core of serialize_mi_result, and + should only be called from that function. */ + +static void +serialize_mi_result_1 (PyObject *result, const char *field_name) +{ + struct ui_out *uiout = current_uiout; + + if (PyDict_Check (result)) + { + PyObject *key, *value; + Py_ssize_t pos = 0; + ui_out_emit_tuple tuple_emitter (uiout, field_name); + while (PyDict_Next (result, &pos, &key, &value)) + { + gdb::unique_xmalloc_ptr<char> key_string + (py_object_to_mi_key (key)); + serialize_mi_result_1 (value, key_string.get ()); + } + } + else if (PySequence_Check (result) && !PyString_Check (result)) + { + ui_out_emit_list list_emitter (uiout, field_name); + Py_ssize_t len = PySequence_Size (result); + if (len == -1) + gdbpy_handle_exception (); + for (Py_ssize_t i = 0; i < len; ++i) + { + gdbpy_ref<> item (PySequence_ITEM (result, i)); + if (item == nullptr) + gdbpy_handle_exception (); + serialize_mi_result_1 (item.get (), nullptr); + } + } + else if (PyIter_Check (result)) + { + gdbpy_ref<> item; + ui_out_emit_list list_emitter (uiout, field_name); + while (true) + { + item.reset (PyIter_Next (result)); + if (item == nullptr) + { + if (PyErr_Occurred () != nullptr) + gdbpy_handle_exception (); + break; + } + serialize_mi_result_1 (item.get (), nullptr); + } + } + else + { + gdb::unique_xmalloc_ptr<char> string (gdbpy_obj_to_string (result)); + if (string == nullptr) + gdbpy_handle_exception (); + uiout->field_string (field_name, string.get ()); + } +} + +/* Serialize RESULT and print it in MI format to the current_uiout. + + This function handles the top-level result initially returned from the + invoke method of the Python command implementation. At the top-level + the result must be a dictionary. The values within this dictionary can + be a wider range of types. Handling the values of the top-level + dictionary is done by serialize_mi_result_1, see that function for more + details. + + If anything goes wrong while parsing and printing the MI output then an + error is thrown. */ + +static void +serialize_mi_result (PyObject *result) +{ + /* At the top-level, the result must be a dictionary. */ + + if (!PyDict_Check (result)) + gdbpy_error (_("Result from invoke must be a dictionary")); + + PyObject *key, *value; + Py_ssize_t pos = 0; + while (PyDict_Next (result, &pos, &key, &value)) + { + gdb::unique_xmalloc_ptr<char> key_string + (py_object_to_mi_key (key)); + serialize_mi_result_1 (value, key_string.get ()); + } +} + +/* Called when the MI command is invoked. PARSE contains the parsed + command line arguments from the user. */ + +void +mi_command_py::do_invoke (struct mi_parse *parse) const +{ + PYMICMD_SCOPED_DEBUG_ENTER_EXIT; + + pymicmd_debug_printf ("this = %p, name = %s", this, name ()); + + mi_parse_argv (parse->args, parse); + + if (parse->argv == nullptr) + error (_("Problem parsing arguments: %s %s"), parse->command, parse->args); + + PyObject *obj = (PyObject *) this->m_pyobj; + gdb_assert (obj != nullptr); + + gdbpy_enter enter_py; + + /* Place all the arguments into a list which we pass as a single argument + to the MI command's invoke method. */ + gdbpy_ref<> argobj (PyList_New (parse->argc)); + if (argobj == nullptr) + gdbpy_handle_exception (); + + for (int i = 0; i < parse->argc; ++i) + { + gdbpy_ref<> str (PyUnicode_Decode (parse->argv[i], + strlen (parse->argv[i]), + host_charset (), nullptr)); + if (PyList_SetItem (argobj.get (), i, str.release ()) < 0) + gdbpy_handle_exception (); + } + + gdb_assert (PyErr_Occurred () == nullptr); + gdbpy_ref<> result (PyObject_CallMethodObjArgs (obj, invoke_cst, + argobj.get (), nullptr)); + if (result == nullptr) + gdbpy_handle_exception (); + + if (result != Py_None) + serialize_mi_result (result.get ()); +} + +/* See declaration above. */ + +void +mi_command_py::validate_installation (micmdpy_object *cmd_obj) +{ + gdb_assert (cmd_obj != nullptr); + mi_command_py *cmd = cmd_obj->mi_command; + gdb_assert (cmd != nullptr); + const char *name = cmd_obj->mi_command_name; + gdb_assert (name != nullptr); + gdb_assert (name == cmd->name ()); + mi_command *mi_cmd = mi_cmd_lookup (name); + gdb_assert (mi_cmd == cmd); + gdb_assert (cmd->m_pyobj == cmd_obj); +} + +/* Return a reference to the gdb._mi_commands dictionary. If the + dictionary can't be found for any reason then nullptr is returned, and + a Python exception will be set. */ + +static gdbpy_ref<> +micmdpy_global_command_dictionary () +{ + if (gdb_python_module == nullptr) + { + PyErr_SetString (PyExc_RuntimeError, _("unable to find gdb module")); + return nullptr; + } + + gdbpy_ref<> mi_cmd_dict (PyObject_GetAttrString (gdb_python_module, + "_mi_commands")); + if (mi_cmd_dict == nullptr) + return nullptr; + + if (!PyDict_Check (mi_cmd_dict.get ())) + { + PyErr_SetString (PyExc_RuntimeError, + _("gdb._mi_commands is not a dictionary as expected")); + return nullptr; + } + + return mi_cmd_dict; +} + +/* Uninstall OBJ, making the MI command represented by OBJ unavailable for + use by the user. On success 0 is returned, otherwise -1 is returned + and a Python exception will be set. */ + +static int +micmdpy_uninstall_command (micmdpy_object *obj) +{ + PYMICMD_SCOPED_DEBUG_ENTER_EXIT; + + gdb_assert (obj->mi_command != nullptr); + gdb_assert (obj->mi_command_name != nullptr); + + pymicmd_debug_printf ("name = %s", obj->mi_command_name); + + /* Remove the command from the internal MI table of commands, this will + cause the c++ object to be deleted, which will clear the mi_command + member variable within the Python object. */ + remove_mi_cmd_entry (obj->mi_command->name ()); + gdb_assert (obj->mi_command == nullptr); + + gdbpy_ref<> mi_cmd_dict = micmdpy_global_command_dictionary (); + if (mi_cmd_dict == nullptr) + return -1; + + /* Grab the name for this command. */ + gdbpy_ref<> name_obj + = host_string_to_python_string (obj->mi_command_name); + if (name_obj == nullptr) + return -1; + + /* Lookup the gdb.MICommand object in the dictionary of all Python MI + commands, this is gdb._mi_command, and remove it. */ + PyObject *curr = PyDict_GetItemWithError (mi_cmd_dict.get (), + name_obj.get ()); + + /* Did we encounter an error? Failing to find the object in the + dictionary isn't an error, that's fine. */ + if (curr == nullptr && PyErr_Occurred ()) + return -1; + + /* Did we find this command in the gdb._mi_commands dictionary? If so, + then remove it. */ + if (curr != nullptr) + { + /* Yes we did! Remove it. */ + if (PyDict_DelItem (mi_cmd_dict.get (), name_obj.get ()) < 0) + return -1; + } + + return 0; +} + +/* Install OBJ as a usable MI command. Return 0 on success, and -1 on + error, in which case, a Python error will have been set. + + After successful completion the command name associated with OBJ will + be installed in the MI command table (so it can be found if the user + enters that command name), additionally, OBJ will have been added to + the gdb._mi_commands dictionary (using the command name as its key), + this will ensure that OBJ remains live even if the user gives up all + references. */ + +static int +micmdpy_install_command (micmdpy_object *obj) +{ + PYMICMD_SCOPED_DEBUG_ENTER_EXIT; + + gdb_assert (obj->mi_command == nullptr); + gdb_assert (obj->mi_command_name != nullptr); + + pymicmd_debug_printf ("name = %s", obj->mi_command_name); + + gdbpy_ref<> mi_cmd_dict = micmdpy_global_command_dictionary (); + if (mi_cmd_dict == nullptr) + return -1; + + /* Look up this command name in the gdb._mi_commands dictionary, a + command with this name may already exist. */ + gdbpy_ref<> name_obj + = host_string_to_python_string (obj->mi_command_name); + + PyObject *curr = PyDict_GetItemWithError (mi_cmd_dict.get (), + name_obj.get ()); + if (curr == nullptr && PyErr_Occurred ()) + return -1; + if (curr != nullptr) + { + /* There is a command with this name already in the gdb._mi_commands + dictionary. First, validate that the object in the dictionary is + of the expected type, just in case something weird has happened. */ + if (!PyObject_IsInstance (curr, (PyObject *) &micmdpy_object_type)) + { + PyErr_SetString (PyExc_RuntimeError, + _("unexpected object in gdb._mi_commands dictionary")); + return -1; + } + + /* To get to this function OBJ should not be installed, which should + mean OBJ is not in the gdb._mi_commands dictionary. If we find + that OBJ is the thing in the dictionary, then something weird is + going on, we should throw an error. */ + micmdpy_object *other = (micmdpy_object *) curr; + if (other == obj || other->mi_command == nullptr) + { + PyErr_SetString (PyExc_RuntimeError, + _("uninstalled command found in gdb._mi_commands dictionary")); + return -1; + } + + /* All Python mi command object should always have a name set. */ + gdb_assert (other->mi_command_name != nullptr); + + /* We always insert commands into the gdb._mi_commands dictionary + using their name as a key, if this check fails then the dictionary + is in some weird state. */ + if (other->mi_command_name != other->mi_command->name () + || strcmp (other->mi_command_name, obj->mi_command_name) != 0) + { + PyErr_SetString (PyExc_RuntimeError, + _("gdb._mi_commands dictionary is corrupted")); + return -1; + } + + /* Switch the state of the c++ object held in the MI command table + so that it now references OBJ. After this action the old Python + object that used to be referenced from the MI command table will + now show as uninstalled, while the new Python object will show as + installed. */ + other->mi_command->swap_python_object (obj); + + gdb_assert (other->mi_command == nullptr); + gdb_assert (obj->mi_command != nullptr); + gdb_assert (obj->mi_command->name () == obj->mi_command_name); + + /* Remove the previous Python object from the gdb._mi_commands + dictionary, we'll install the new object below. */ + if (PyDict_DelItem (mi_cmd_dict.get (), name_obj.get ()) < 0) + return -1; + } + else + { + /* There's no Python object for this command name in the + gdb._mi_commands dictionary from which we can steal an existing + object already held in the MI commands table, and so, we now + create a new c++ object, and install it into the MI table. */ + obj->mi_command = new mi_command_py (obj->mi_command_name, obj); + mi_command_up micommand (obj->mi_command); + + /* Add the command to the gdb internal MI command table. */ + bool result = insert_mi_cmd_entry (std::move (micommand)); + if (!result) + { + PyErr_SetString (PyExc_RuntimeError, + _("unable to add command, name may already be in use")); + return -1; + } + } + + /* Finally, add the Python object to the gdb._mi_commands dictionary. */ + if (PyDict_SetItem (mi_cmd_dict.get (), name_obj.get (), (PyObject *) obj) < 0) + return -1; + + return 0; +} + +/* Implement gdb.MICommand.__init__. The init method takes the name of + the MI command as the first argument, which must be a string, starting + with a single dash. */ + +static int +micmdpy_init (PyObject *self, PyObject *args, PyObject *kwargs) +{ + PYMICMD_SCOPED_DEBUG_ENTER_EXIT; + + micmdpy_object *cmd = (micmdpy_object *) self; + + static const char *keywords[] = { "name", nullptr }; + const char *name; + + if (!gdb_PyArg_ParseTupleAndKeywords (args, kwargs, "s", keywords, + &name)) + return -1; + + /* Validate command name */ + const int name_len = strlen (name); + if (name_len == 0) + { + PyErr_SetString (PyExc_ValueError, _("MI command name is empty.")); + return -1; + } + else if ((name_len < 2) || (name[0] != '-') || !isalnum (name[1])) + { + PyErr_SetString (PyExc_ValueError, + _("MI command name does not start with '-'" + " followed by at least one letter or digit.")); + return -1; + } + else + { + for (int i = 2; i < name_len; i++) + { + if (!isalnum (name[i]) && name[i] != '-') + { + PyErr_Format + (PyExc_ValueError, + _("MI command name contains invalid character: %c."), + name[i]); + return -1; + } + } + + /* Skip over the leading dash. For the rest of this function the + dash is not important. */ + ++name; + } + + /* If this object already has a name set, then this object has been + initialized before. We handle this case a little differently. */ + if (cmd->mi_command_name != nullptr) + { + /* First, we don't allow the user to change the MI command name. + Supporting this would be tricky as we would need to delete the + mi_command_py from the MI command table, however, the user might + be trying to perform this reinitialization from within the very + command we're about to delete... it all gets very messy. + + So, for now at least, we don't allow this. This doesn't seem like + an excessive restriction. */ + if (strcmp (cmd->mi_command_name, name) != 0) + { + PyErr_SetString + (PyExc_ValueError, + _("can't reinitialize object with a different command name")); + return -1; + } + + /* If there's already an object registered with the MI command table, + then we're done. That object must be a mi_command_py, which + should reference back to this micmdpy_object. */ + if (cmd->mi_command != nullptr) + { + mi_command_py::validate_installation (cmd); + return 0; + } + } + else + cmd->mi_command_name = xstrdup (name); + + /* Now we can install this mi_command_py in the MI command table. */ + return micmdpy_install_command (cmd); +} + +/* Called when a gdb.MICommand object is deallocated. */ + +static void +micmdpy_dealloc (PyObject *obj) +{ + PYMICMD_SCOPED_DEBUG_ENTER_EXIT; + + micmdpy_object *cmd = (micmdpy_object *) obj; + + /* If the Python object failed to initialize, then the name field might + be nullptr. */ + pymicmd_debug_printf ("obj = %p, name = %s", cmd, + (cmd->mi_command_name == nullptr + ? "(null)" : cmd->mi_command_name)); + + /* Remove the command from the MI command table if needed. This will + cause the mi_command_py object to be deleted, which, in turn, will + clear the cmd->mi_command member variable, hence the assert. */ + if (cmd->mi_command != nullptr) + remove_mi_cmd_entry (cmd->mi_command->name ()); + gdb_assert (cmd->mi_command == nullptr); + + /* Free the memory that holds the command name. */ + xfree (cmd->mi_command_name); + cmd->mi_command_name = nullptr; + + /* Finally, free the memory for this Python object. */ + Py_TYPE (obj)->tp_free (obj); +} + +/* Python initialization for the MI commands components. */ + +int +gdbpy_initialize_micommands () +{ + micmdpy_object_type.tp_new = PyType_GenericNew; + if (PyType_Ready (&micmdpy_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "MICommand", + (PyObject *) &micmdpy_object_type) + < 0) + return -1; + + invoke_cst = PyString_FromString ("invoke"); + if (invoke_cst == nullptr) + return -1; + + return 0; +} + +/* Get the gdb.MICommand.name attribute, returns a string, the name of this + MI command. */ + +static PyObject * +micmdpy_get_name (PyObject *self, void *closure) +{ + struct micmdpy_object *micmd_obj = (struct micmdpy_object *) self; + + gdb_assert (micmd_obj->mi_command_name != nullptr); + std::string name_str = string_printf ("-%s", micmd_obj->mi_command_name); + return PyString_FromString (name_str.c_str ()); +} + +/* Get the gdb.MICommand.installed property. Returns true if this MI + command is installed into the MI command table, otherwise returns + false. */ + +static PyObject * +micmdpy_get_installed (PyObject *self, void *closure) +{ + struct micmdpy_object *micmd_obj = (struct micmdpy_object *) self; + + if (micmd_obj->mi_command == nullptr) + Py_RETURN_FALSE; + Py_RETURN_TRUE; +} + +/* Set the gdb.MICommand.installed property. The property can be set to + either true or false. Setting the property to true will cause the + command to be installed into the MI command table (if it isn't + already), while setting this property to false will cause the command + to be removed from the MI command table (if it is present). */ + +static int +micmdpy_set_installed (PyObject *self, PyObject *newvalue, void *closure) +{ + struct micmdpy_object *micmd_obj = (struct micmdpy_object *) self; + + bool installed_p = PyObject_IsTrue (newvalue); + if (installed_p == (micmd_obj->mi_command != nullptr)) + return 0; + + if (installed_p) + return micmdpy_install_command (micmd_obj); + else + return micmdpy_uninstall_command (micmd_obj); +} + +/* The gdb.MICommand properties. */ + +static gdb_PyGetSetDef micmdpy_object_getset[] = { + { "name", micmdpy_get_name, nullptr, "The command's name.", nullptr }, + { "installed", micmdpy_get_installed, micmdpy_set_installed, + "Is this command installed for use.", nullptr }, + { nullptr } /* Sentinel. */ +}; + +/* The gdb.MICommand descriptor. */ + +PyTypeObject micmdpy_object_type = { + PyVarObject_HEAD_INIT (nullptr, 0) "gdb.MICommand", /*tp_name */ + sizeof (micmdpy_object), /*tp_basicsize */ + 0, /*tp_itemsize */ + micmdpy_dealloc, /*tp_dealloc */ + 0, /*tp_print */ + 0, /*tp_getattr */ + 0, /*tp_setattr */ + 0, /*tp_compare */ + 0, /*tp_repr */ + 0, /*tp_as_number */ + 0, /*tp_as_sequence */ + 0, /*tp_as_mapping */ + 0, /*tp_hash */ + 0, /*tp_call */ + 0, /*tp_str */ + 0, /*tp_getattro */ + 0, /*tp_setattro */ + 0, /*tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags */ + "GDB mi-command object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + 0, /* tp_methods */ + 0, /* tp_members */ + micmdpy_object_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + micmdpy_init, /* tp_init */ + 0, /* tp_alloc */ +}; + +void _initialize_py_micmd (); +void +_initialize_py_micmd () +{ + add_setshow_boolean_cmd + ("py-micmd", class_maintenance, &pymicmd_debug, + _("Set Python micmd debugging."), + _("Show Python micmd debugging."), + _("When on, Python micmd debugging is enabled."), + nullptr, + show_pymicmd_debug, + &setdebuglist, &showdebuglist); +} diff --git a/gdb/python/py-utils.c b/gdb/python/py-utils.c index 73c860b..838853c 100644 --- a/gdb/python/py-utils.c +++ b/gdb/python/py-utils.c @@ -382,6 +382,23 @@ gdb_pymodule_addobject (PyObject *module, const char *name, PyObject *object) return result; } +/* See python-internal.h. */ + +void +gdbpy_error (const char *fmt, ...) +{ + va_list ap; + va_start (ap, fmt); + std::string str = string_vprintf (fmt, ap); + va_end (ap); + + const char *msg = str.c_str (); + if (msg != nullptr && *msg != '\0') + error (_("Error occurred in Python: %s"), msg); + else + error (_("Error occurred in Python.")); +} + /* Handle a Python exception when the special gdb.GdbError treatment is desired. This should only be called when an exception is set. If the exception is a gdb.GdbError, throw a gdb exception with the diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h index 5e15f62..083c4db 100644 --- a/gdb/python/python-internal.h +++ b/gdb/python/python-internal.h @@ -562,6 +562,8 @@ int gdbpy_initialize_membuf () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; int gdbpy_initialize_connection () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; +int gdbpy_initialize_micommands (void) + CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; /* A wrapper for PyErr_Fetch that handles reference counting for the caller. */ @@ -730,6 +732,17 @@ void gdbpy_print_stack (void); void gdbpy_print_stack_or_quit (); void gdbpy_handle_exception () ATTRIBUTE_NORETURN; +/* A wrapper around calling 'error'. Prefixes the error message with an + 'Error occurred in Python' string. Use this in C++ code if we spot + something wrong with an object returned from Python code. The prefix + string gives the user a hint that the mistake is within Python code, + rather than some other part of GDB. + + This always calls error, and never returns. */ + +void gdbpy_error (const char *fmt, ...) + ATTRIBUTE_NORETURN ATTRIBUTE_PRINTF (1, 2); + gdbpy_ref<> python_string_to_unicode (PyObject *obj); gdb::unique_xmalloc_ptr<char> unicode_to_target_string (PyObject *unicode_str); gdb::unique_xmalloc_ptr<char> python_string_to_target_string (PyObject *obj); diff --git a/gdb/python/python.c b/gdb/python/python.c index 79f9826..9795588 100644 --- a/gdb/python/python.c +++ b/gdb/python/python.c @@ -1983,7 +1983,8 @@ do_start_initialization () || gdbpy_initialize_unwind () < 0 || gdbpy_initialize_membuf () < 0 || gdbpy_initialize_connection () < 0 - || gdbpy_initialize_tui () < 0) + || gdbpy_initialize_tui () < 0 + || gdbpy_initialize_micommands () < 0) return false; #define GDB_PY_DEFINE_EVENT_TYPE(name, py_name, doc, base) \ diff --git a/gdb/testsuite/gdb.python/py-mi-cmd.exp b/gdb/testsuite/gdb.python/py-mi-cmd.exp new file mode 100644 index 0000000..c102efb --- /dev/null +++ b/gdb/testsuite/gdb.python/py-mi-cmd.exp @@ -0,0 +1,390 @@ +# Copyright (C) 2019-2022 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/>. + +# Test custom MI commands implemented in Python. + +load_lib gdb-python.exp +load_lib mi-support.exp +set MIFLAGS "-i=mi" + +gdb_exit +if {[mi_gdb_start]} { + continue +} + +if {[lsearch -exact [mi_get_features] python] < 0} { + unsupported "python support is disabled" + return -1 +} + +standard_testfile + +mi_gdb_test "set python print-stack full" \ + ".*\\^done" \ + "set python print-stack full" + +mi_gdb_test "source ${srcdir}/${subdir}/${testfile}.py" \ + ".*\\^done" \ + "load python file" + +mi_gdb_test "python pycmd1('-pycmd')" \ + ".*\\^done" \ + "define -pycmd MI command" + +mi_gdb_test "-pycmd int" \ + "\\^done,result=\"42\"" \ + "-pycmd int" + +mi_gdb_test "-pycmd str" \ + "\\^done,result=\"Hello world!\"" \ + "-pycmd str" + +mi_gdb_test "-pycmd ary" \ + "\\^done,result=\\\[\"Hello\",\"42\"\\\]" \ + "-pycmd ary" + +mi_gdb_test "-pycmd dct" \ + "\\^done,result={hello=\"world\",times=\"42\"}" \ + "-pycmd dct" + +mi_gdb_test "-pycmd bk1" \ + "\\^error,msg=\"Error occurred in Python: non-string object used as key: Bad Key\"" \ + "-pycmd bk1" + +mi_gdb_test "-pycmd bk2" \ + "\\^error,msg=\"Error occurred in Python: non-string object used as key: 1\"" \ + "-pycmd bk2" + +mi_gdb_test "-pycmd bk3" \ + [multi_line \ + "&\"TypeError: __repr__ returned non-string \\(type BadKey\\)..\"" \ + "\\^error,msg=\"Error occurred in Python: __repr__ returned non-string \\(type BadKey\\)\""] \ + "-pycmd bk3" + +mi_gdb_test "-pycmd tpl" \ + "\\^done,result=\\\[\"42\",\"Hello\"\\\]" \ + "-pycmd tpl" + +mi_gdb_test "-pycmd itr" \ + "\\^done,result=\\\[\"1\",\"2\",\"3\"\\\]" \ + "-pycmd itr" + +mi_gdb_test "-pycmd nn1" \ + "\\^done" \ + "-pycmd nn1" + +mi_gdb_test "-pycmd nn2" \ + "\\^done,result=\\\[\"None\"\\\]" \ + "-pycmd nn2" + +mi_gdb_test "-pycmd bogus" \ + "\\^error,msg=\"Invalid parameter: bogus\"" \ + "-pycmd bogus" + +# Check that the top-level result from 'invoke' must be a dictionary. +foreach test_name { nd1 nd2 nd3 } { + mi_gdb_test "-pycmd ${test_name}" \ + "\\^error,msg=\"Error occurred in Python: Result from invoke must be a dictionary\"" +} + +# Check for invalid strings in the result. +foreach test_desc { {ik1 "xxx yyy"} {ik2 "xxx yyy"} {ik3 "xxx\\+yyy"} \ + {ik4 "xxx\\.yyy"} {ik5 "123xxxyyy"} } { + lassign $test_desc name pattern + + mi_gdb_test "-pycmd ${name}" \ + "\\^error,msg=\"Error occurred in Python: Invalid key in MI result: ${pattern}\"" +} + +mi_gdb_test "-pycmd empty_key" \ + "\\^error,msg=\"Error occurred in Python: Invalid empty key in MI result\"" + +# Check that a dash ('-') can be used in a key name. +mi_gdb_test "-pycmd dash-key" \ + "\\^done,the-key=\"123\"" + +# With this argument the command raises a gdb.GdbError with no message +# string. GDB considers this a bug in the user program, so prints a +# backtrace, and a generic error message. +mi_gdb_test "-pycmd exp" \ + [multi_line ".*&\"Traceback \\(most recent call last\\):..\"" \ + "&\"\[^\r\n\]+${testfile}.py\[^\r\n\]+\"" \ + "&\"\[^\r\n\]+raise gdb.GdbError\\(\\)..\"" \ + "&\"gdb.GdbError..\"" \ + "\\^error,msg=\"Error occurred in Python\\.\""] \ + "-pycmd exp" + +mi_gdb_test "python pycmd2('-pycmd')" \ + ".*\\^done" \ + "redefine -pycmd MI command from CLI command" + +mi_gdb_test "-pycmd str" \ + "\\^done,result=\"Ciao!\"" \ + "-pycmd str - redefined from CLI" + +mi_gdb_test "-pycmd int" \ + "\\^error,msg=\"Invalid parameter: int\"" \ + "-pycmd int - redefined from CLI" + +mi_gdb_test "-pycmd new" \ + "\\^done" \ + "Define new command -pycmd-new MI command from Python MI command" + +mi_gdb_test "-pycmd red" \ + "\\^error,msg=\"Command redefined but we failing anyway\"" \ + "redefine -pycmd MI command from Python MI command" + +mi_gdb_test "-pycmd int" \ + "\\^done,result=\"42\"" \ + "-pycmd int - redefined from MI" + +mi_gdb_test "-pycmd-new int" \ + "\\^done,result=\"42\"" \ + "-pycmd-new int - defined from MI" + +mi_gdb_test "python pycmd1('')" \ + ".*&\"ValueError: MI command name is empty\\...\".*\\^error,msg=\"Error while executing Python code\\.\"" \ + "empty MI command name" + +mi_gdb_test "python pycmd1('-')" \ + [multi_line \ + ".*" \ + "&\"ValueError: MI command name does not start with '-' followed by at least one letter or digit\\...\"" \ + "&\"Error while executing Python code\\...\"" \ + "\\^error,msg=\"Error while executing Python code\\.\""] \ + "invalid MI command name" + +mi_gdb_test "python pycmd1('-bad-character-@')" \ + [multi_line \ + ".*" \ + "&\"ValueError: MI command name contains invalid character: @\\...\"" \ + "&\"Error while executing Python code\\...\"" \ + "\\^error,msg=\"Error while executing Python code\\.\""] \ + "invalid character in MI command name" + +mi_gdb_test "python cmd=pycmd1('-abc')" \ + ".*\\^done" \ + "create command -abc, stored in a python variable" + +mi_gdb_test "python print(cmd.name)" \ + ".*\r\n~\"-abc\\\\n\"\r\n\\^done" \ + "print the name of the stored mi command" + +mi_gdb_test "python print(cmd.installed)" \ + ".*\r\n~\"True\\\\n\"\r\n\\^done" \ + "print the installed status of the stored mi command" + +mi_gdb_test "-abc str" \ + "\\^done,result=\"Hello world!\"" \ + "-abc str" + +mi_gdb_test "python cmd.installed = False" \ + ".*\\^done" \ + "uninstall the mi command" + +mi_gdb_test "-abc str" \ + "\\^error,msg=\"Undefined MI command: abc\",code=\"undefined-command\"" \ + "-abc str, but now the command is gone" + +mi_gdb_test "python cmd.installed = True" \ + ".*\\^done" \ + "re-install the mi command" + +mi_gdb_test "-abc str" \ + "\\^done,result=\"Hello world!\"" \ + "-abc str, the command is back again" + +mi_gdb_test "python other=pycmd2('-abc')" \ + ".*\\^done" \ + "create another command called -abc, stored in a separate python variable" + +mi_gdb_test "python print(other.installed)" \ + ".*\r\n~\"True\\\\n\"\r\n\\^done" \ + "print the installed status of the other stored mi command" + +mi_gdb_test "python print(cmd.installed)" \ + ".*\r\n~\"False\\\\n\"\r\n\\^done" \ + "print the installed status of the original stored mi command" + +mi_gdb_test "-abc str" \ + "\\^done,result=\"Ciao!\"" \ + "-abc str, when the other command is in place" + +mi_gdb_test "python cmd.installed = True" \ + ".*\\^done" \ + "re-install the original mi command" + +mi_gdb_test "-abc str" \ + "\\^done,result=\"Hello world!\"" \ + "-abc str, the original command is back again" + +mi_gdb_test "python print(other.installed)" \ + ".*\r\n~\"False\\\\n\"\r\n\\^done" \ + "the other command is now not installed" + +mi_gdb_test "python print(cmd.installed)" \ + ".*\r\n~\"True\\\\n\"\r\n\\^done" \ + "the original command is now installed" + +mi_gdb_test "python aa = pycmd3('-aa', 'message one', 'xxx')" \ + ".*\\^done" \ + "created a new -aa command" + +mi_gdb_test "-aa" \ + ".*\\^done,xxx={msg=\"message one\"}" \ + "call the -aa command" + +mi_gdb_test "python aa.__init__('-aa', 'message two', 'yyy')" \ + ".*\\^done" \ + "reinitialise -aa command with a new message" + +mi_gdb_test "-aa" \ + ".*\\^done,yyy={msg=\"message two\"}" \ + "call the -aa command, get the new message" + +mi_gdb_test "python aa.__init__('-bb', 'message three', 'zzz')" \ + [multi_line \ + ".*" \ + "&\"ValueError: can't reinitialize object with a different command name..\"" \ + "&\"Error while executing Python code\\...\"" \ + "\\^error,msg=\"Error while executing Python code\\.\""] \ + "attempt to reinitialise aa variable to a new command name" + +mi_gdb_test "-aa" \ + ".*\\^done,yyy={msg=\"message two\"}" \ + "check the aa object has not changed after failed initialization" + +mi_gdb_test "python aa.installed = False" \ + ".*\\^done" \ + "uninstall the -aa command" + +mi_gdb_test "python aa.__init__('-bb', 'message three', 'zzz')" \ + [multi_line \ + ".*" \ + "&\"ValueError: can't reinitialize object with a different command name..\"" \ + "&\"Error while executing Python code\\...\"" \ + "\\^error,msg=\"Error while executing Python code\\.\""] \ + "attempt to reinitialise aa variable to a new command name while uninstalled" + +mi_gdb_test "python aa.__init__('-aa', 'message three', 'zzz')" \ + ".*\\^done" \ + "reinitialise -aa command with a new message while uninstalled" + +mi_gdb_test "python aa.installed = True" \ + ".*\\^done" \ + "install the -aa command" + +mi_gdb_test "-aa" \ + ".*\\^done,zzz={msg=\"message three\"}" \ + "call the -aa command looking for message three" + +# Remove the gdb._mi_commands dictionary, then try to register a new +# command. +mi_gdb_test "python del(gdb._mi_commands)" ".*\\^done" +mi_gdb_test "python pycmd3('-hello', 'Hello', 'msg')" \ + [multi_line \ + ".*" \ + "&\"AttributeError: module 'gdb' has no attribute '_mi_commands'..\"" \ + "&\"Error while executing Python code\\...\"" \ + "\\^error,msg=\"Error while executing Python code\\.\""] \ + "register a command with no gdb._mi_commands available" + +# Set gdb._mi_commands to be something other than a dictionary, and +# try to register a command. +mi_gdb_test "python gdb._mi_commands = 'string'" ".*\\^done" +mi_gdb_test "python pycmd3('-hello', 'Hello', 'msg')" \ + [multi_line \ + ".*" \ + "&\"RuntimeError: gdb._mi_commands is not a dictionary as expected..\"" \ + "&\"Error while executing Python code\\...\"" \ + "\\^error,msg=\"Error while executing Python code\\.\""] \ + "register a command when gdb._mi_commands is not a dictionary" + +# Restore gdb._mi_commands to a dictionary. +mi_gdb_test "python gdb._mi_commands = {}" ".*\\^done" + +# Try to register a command object that is missing an invoke method. +# This is accepted, but will give an error when the user tries to run +# the command. +mi_gdb_test "python no_invoke('-no-invoke')" ".*\\^done" \ + "attempt to register command with no invoke method" +mi_gdb_test "-no-invoke" \ + [multi_line \ + ".*" \ + "&\"AttributeError: 'no_invoke' object has no attribute 'invoke'..\"" \ + "\\^error,msg=\"Error occurred in Python: 'no_invoke' object has no attribute 'invoke'\""] \ + "execute -no-invoke command, which is missing the invoke method" + +# Register a command, then delete its invoke method. What is the user thinking!! +mi_gdb_test "python setattr(no_invoke, 'invoke', free_invoke)" ".*\\^done" +mi_gdb_test "python cmd = no_invoke('-hello')" ".*\\^done" +mi_gdb_test "-hello" ".*\\^done,result=\\\[\\\]" \ + "execute no_invoke command, while it still has an invoke attribute" +mi_gdb_test "python delattr(no_invoke, 'invoke')" ".*\\^done" +mi_gdb_test "-hello" \ + [multi_line \ + ".*" \ + "&\"AttributeError: 'no_invoke' object has no attribute 'invoke'..\"" \ + "\\^error,msg=\"Error occurred in Python: 'no_invoke' object has no attribute 'invoke'\""] \ + "execute -hello command, that had its invoke method removed" +mi_gdb_test "python cmd.invoke = 'string'" ".*\\^done" +mi_gdb_test "-hello" \ + [multi_line \ + ".*" \ + "&\"TypeError: 'str' object is not callable..\"" \ + "\\^error,msg=\"Error occurred in Python: 'str' object is not callable\""] \ + "execute command with invoke set to a string" + +# Further checking of corruption to the gdb._mi_commands dictionary. +# +# First, insert an object of the wrong type, then try to register an +# MI command that will go into that same dictionary slot. +mi_gdb_test "python gdb._mi_commands\['blah'\] = 'blah blah blah'" ".*\\^done" +mi_gdb_test "python pycmd2('-blah')" \ + [multi_line \ + ".*" \ + "&\"RuntimeError: unexpected object in gdb\\._mi_commands dictionary..\"" \ + "&\"Error while executing Python code\\...\"" \ + "\\^error,msg=\"Error while executing Python code\\.\""] \ + "hit unexpected object in gdb._mi_commands dictionary" + +# Next, create a command, uninstall it, then force the command back +# into the dictionary. +mi_gdb_test "python cmd = pycmd2('-foo')" ".*\\^done" +mi_gdb_test "python cmd.installed = False" ".*\\^done" +mi_gdb_test "python gdb._mi_commands\['foo'\] = cmd" ".*\\^done" +mi_gdb_test "python cmd.installed = True" \ + [multi_line \ + ".*" \ + "&\"RuntimeError: uninstalled command found in gdb\\._mi_commands dictionary..\"" \ + "&\"Error while executing Python code\\...\"" \ + "\\^error,msg=\"Error while executing Python code\\.\""] \ + "found uninstalled command in gdb._mi_commands dictionary" + +# Try to create a new MI command that uses the name of a builtin MI command. +mi_gdb_test "python cmd = pycmd2('-data-disassemble')" \ + [multi_line \ + ".*" \ + "&\"RuntimeError: unable to add command, name may already be in use..\"" \ + "&\"Error while executing Python code\\...\"" \ + "\\^error,msg=\"Error while executing Python code\\.\""] \ + "try to register a command that replaces -data-disassemble" + + + +mi_gdb_test "python run_exception_tests()" \ + [multi_line \ + ".*" \ + "~\"PASS..\"" \ + "\\^done"] diff --git a/gdb/testsuite/gdb.python/py-mi-cmd.py b/gdb/testsuite/gdb.python/py-mi-cmd.py new file mode 100644 index 0000000..ffe27c5 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-mi-cmd.py @@ -0,0 +1,120 @@ +# Copyright (C) 2019-2022 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 + + +class BadKey: + def __repr__(self): + return "Bad Key" + + +class ReallyBadKey: + def __repr__(self): + return BadKey() + + +class pycmd1(gdb.MICommand): + def invoke(self, argv): + if argv[0] == "int": + return {"result": 42} + elif argv[0] == "str": + return {"result": "Hello world!"} + elif argv[0] == "ary": + return {"result": ["Hello", 42]} + elif argv[0] == "dct": + return {"result": {"hello": "world", "times": 42}} + elif argv[0] == "bk1": + return {"result": {BadKey(): "world"}} + elif argv[0] == "bk2": + return {"result": {1: "world"}} + elif argv[0] == "bk3": + return {"result": {ReallyBadKey(): "world"}} + elif argv[0] == "tpl": + return {"result": (42, "Hello")} + elif argv[0] == "itr": + return {"result": iter([1, 2, 3])} + elif argv[0] == "nn1": + return None + elif argv[0] == "nn2": + return {"result": [None]} + elif argv[0] == "red": + pycmd2("-pycmd") + return None + elif argv[0] == "nd1": + return [1, 2, 3] + elif argv[0] == "nd2": + return 123 + elif argv[0] == "nd3": + return "abc" + elif argv[0] == "ik1": + return {"xxx yyy": 123} + elif argv[0] == "ik2": + return {"result": {"xxx yyy": 123}} + elif argv[0] == "ik3": + return {"xxx+yyy": 123} + elif argv[0] == "ik4": + return {"xxx.yyy": 123} + elif argv[0] == "ik5": + return {"123xxxyyy": 123} + elif argv[0] == "empty_key": + return {"": 123} + elif argv[0] == "dash-key": + return {"the-key": 123} + elif argv[0] == "exp": + raise gdb.GdbError() + else: + raise gdb.GdbError("Invalid parameter: %s" % argv[0]) + + +class pycmd2(gdb.MICommand): + def invoke(self, argv): + if argv[0] == "str": + return {"result": "Ciao!"} + elif argv[0] == "red": + pycmd1("-pycmd") + raise gdb.GdbError("Command redefined but we failing anyway") + elif argv[0] == "new": + pycmd1("-pycmd-new") + return None + else: + raise gdb.GdbError("Invalid parameter: %s" % argv[0]) + + +# This class creates a command that returns a string, which is passed +# when the command is created. +class pycmd3(gdb.MICommand): + def __init__(self, name, msg, top_level): + super(pycmd3, self).__init__(name) + self._msg = msg + self._top_level = top_level + + def invoke(self, args): + return {self._top_level: {"msg": self._msg}} + + +# A command that is missing it's invoke method. +class no_invoke(gdb.MICommand): + def __init__(self, name): + super(no_invoke, self).__init__(name) + + +def free_invoke(obj, args): + return {"result": args} + + +# Run some test involving catching exceptions. It's easier to write +# these as a Python function which is then called from the exp script. +def run_exception_tests(): + print("PASS") |