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 \
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.
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
* 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.
@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
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
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.
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
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
#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;
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);
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. */
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. */
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 */
# 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.
--- /dev/null
+/* 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);
+}
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
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. */
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);
|| 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) \
--- /dev/null
+# 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"]
--- /dev/null
+# 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")