* [PATCH 0/3] Python API for target connections, and packet sending @ 2021-09-11 16:03 Andrew Burgess 2021-09-11 16:03 ` [PATCH 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess ` (3 more replies) 0 siblings, 4 replies; 52+ messages in thread From: Andrew Burgess @ 2021-09-11 16:03 UTC (permalink / raw) To: gdb-patches My goal with the series was to add a Python API for sending packets to remote targets, similar to the existing 'maint packet' CLI command. I did consider just adding such a function at the top level, as in 'gdb.send_remote_packet (...)', but in a multi-connection setup this would require the user to ensure that the correct inferior was selected, and that didn't feel great. So then I thought maybe having 'gdb.Inferior.send_remote_packet' would be better, but that doesn't really feel right, packet sending is not really a property of the inferior, but of the connection between GDB and the target. And so, I've added a whole new object type, gdb.TargetConnection, which represents the connections between GDB and the target, as seen in 'info connections'. This is the first patch in the series. The second patch refactors 'maint packet' to prepare the code for being used from multiple locations. And finally, in the third patch, I add gdb.TargetConnection.send_remote_packet, which is what I needed. All feedback welcome, Thanks, Andrew --- Andrew Burgess (3): gdb/python: introduce gdb.TargetConnection object type gdb: make packet_command function available outside remote.c gdb/python: add TargetConnection.send_remote_packet method gdb/Makefile.in | 1 + gdb/NEWS | 22 + gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 113 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 455 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/remote.c | 67 ++- gdb/remote.h | 34 ++ gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 +++ gdb/testsuite/gdb.python/py-connection.c | 22 + gdb/testsuite/gdb.python/py-connection.exp | 63 +++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- gdb/testsuite/gdb.python/py-send-packet.c | 22 + gdb/testsuite/gdb.python/py-send-packet.exp | 55 +++ gdb/testsuite/gdb.python/py-send-packet.py | 82 ++++ 23 files changed, 1073 insertions(+), 26 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCH 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-09-11 16:03 [PATCH 0/3] Python API for target connections, and packet sending Andrew Burgess @ 2021-09-11 16:03 ` Andrew Burgess 2021-09-11 16:19 ` Eli Zaretskii 2021-09-11 16:03 ` [PATCH 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess ` (2 subsequent siblings) 3 siblings, 1 reply; 52+ messages in thread From: Andrew Burgess @ 2021-09-11 16:03 UTC (permalink / raw) To: gdb-patches This commit adds a new object type gdb.TargetConnection. This new type represents a connection within GDB (a connection as displayed by 'info connections'). There's three ways to find a gdb.TargetConnection, there's a new 'gdb.connections()' function, which returns a list of all currently active connections. Or you can read the new 'connection' property on the gdb.Inferior object type, this contains the connection for that inferior (or None if the inferior has no connection, for example, it is exited). Finally, there's a new gdb.events.connection_removed event registry, this emits a new gdb.ConnectionEvent whenever a connection is removed from GDB (this happens when all inferiors using a connection exit). The gdb.ConnectionEvent has a 'connection' property, which is the gdb.TargetConnection being removed from GDB. The gdb.TargetConnection has a 'is_valid()' method. A connection object becomes invalid when the underlying connection is removed from GDB (all inferiors using the connection exit). The gdb.TargetConnection has the following read-only properties: 'num': The number for this connection, 'type': e.g. 'native', 'remove', 'sim', etc 'description': The longer description as seen in the 'info connections' command output. 'details': A string or None. Extra details for the connection, for example, a remote connection's details might be 'hostname:port'. --- gdb/Makefile.in | 1 + gdb/NEWS | 16 + gdb/doc/python.texi | 92 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 365 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 +++ gdb/testsuite/gdb.python/py-connection.c | 22 ++ gdb/testsuite/gdb.python/py-connection.exp | 63 +++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- 17 files changed, 717 insertions(+), 4 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp diff --git a/gdb/Makefile.in b/gdb/Makefile.in index 320d3326a81..1d56d091e2d 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -388,6 +388,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-breakpoint.c \ python/py-cmd.c \ python/py-continueevent.c \ + python/py-connection.c \ python/py-event.c \ python/py-evtregistry.c \ python/py-evts.c \ diff --git a/gdb/NEWS b/gdb/NEWS index f9485520438..6d8739573b3 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -17,6 +17,22 @@ maint show backtrace-on-fatal-signal integer, the index of the new item in the history list, is returned. + ** New gdb.TargetConnection object type that represents a connection + (as displayed by the 'info connections' command). + + ** The gdb.Inferior type now has a 'connection' property which is an + instance of gdb.TargetConnection, the connection used by this + inferior. This can be None if the inferior has no connection + (for example, when exited). + + ** New 'gdb.events.connection_removed' event registry, which emits a + 'gdb.ConnectionEvent' when a connection is removed from GDB. + This event has a 'connection' property, a gdb.TargetConnection + object for the connection being removed. + + ** New gdb.connections() function that returns a list of all + currently active connections. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index d8f682a091c..9f92aa791c4 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -220,6 +220,7 @@ * Lazy Strings In Python:: Python representation of lazy strings. * Architectures In Python:: Python representation of architectures. * Registers In Python:: Python representation of registers. +* Connections In Python:: Python representation of connections. * TUI Windows In Python:: Implementing new TUI windows. @end menu @@ -557,6 +558,13 @@ related prompts are prohibited from being changed. @end defun +@anchor{gdbpy_connections} +@defun gdb.connections +Return a list of @code{gdb.TargetConnection} objects, one for each +currently active connection (@pxref{Connections In Python}). The +connection objects are in no particular order in the returned list. +@end defun + @node Exception Handling @subsubsection Exception Handling @cindex python exceptions @@ -3084,10 +3092,18 @@ ID of inferior, as assigned by GDB. @end defvar +@anchor{gdbpy_inferior_connection} +@defvar Inferior.connection +The @code{gdb.TargetConnection} for this inferior (@pxref{Connections +In Python}), or @code{None} if this inferior has no connection. +@end defvar + @defvar Inferior.connection_num ID of inferior's connection as assigned by @value{GDBN}, or None if -the inferior is not connected to a target. -@xref{Inferiors Connections and Programs}. +the inferior is not connected to a target. @xref{Inferiors Connections +and Programs}. This is equivalent to +@code{gdb.Inferior.connection.num} in the case where +@code{gdb.Inferior.connection} is not @code{None}. @end defvar @defvar Inferior.pid @@ -3415,6 +3431,15 @@ The new thread. @end defvar +@item events.connection_removed +This is emitted when @value{GDBN} removes a connection +(@pxref{Connections In Python}). The event is of type +@code{gdb.ConnectionEvent}. This has a single read-only attribute: + +@defvar ConnectionEvent.connection +The @code{gdb.TargetConnection} that is being removed. +@end defvar + @end table @node Threads In Python @@ -5933,6 +5958,69 @@ A string that is the name of this register group. @end defvar +@node Connections In Python +@subsubsection Connections In Python +@cindex Connections In Python +@value{GDBN} lets you run and debug multiple programs in a single +session. Each program being debugged has a connection, the connection +describes how @value{GDBN} controls the program being debugged. +Examples of different connection types are @samp{native} and +@samp{remote}. @xref{Inferiors Connections and Programs}. + +@value{GDBN} uses the @code{gdb.TargetConnection} object type to +represent a connection in Python code. To get a list of all +connections use @code{gdb.connections()} +(@pxref{gdbpy_connections,,gdb.connections}). + +To get the connection for a single @code{gdb.Inferior} read its +@code{gdb.Inferior.connection} attribute +(@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). + +A @code{gdb.TargetConnection} has the following method: + +@defun TargetConnection.is_valid () +Return @code{True} if the @code{gdb.TargetConnection} object is valid, +@code{False} if not. A @code{gdb.TargetConnection} will become +invalid if the connection no longer exists within @value{GDBN}, this +will happen when all the inferiors using that connection exit. + +Reading any of the @code{gdb.TargetConnection} properties will throw +an exception if the connection is invalid. +@end defun + +A @code{gdb.TargetConnection} has the following read-only properties: + +@defvar TargetConnection.num +An integer assigned by @value{GDBN} to uniquely identify this +connection. This is the same value as displayed in the @samp{Num} +column of the @code{info connections} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.type +A string that describes what type of connection this is. This string +will be one of the valid names that can be passed to the @code{target} +command (@pxref{Target Commands,,target command}). +@end defvar + +@defvar TargetConnection.description +A string that gives a short description of this target type. This is +the same string that is displayed in the @samp{Description} column of +the @code{info connection} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.details +An optional string that gives additional information about this +connection. This attribute can be @code{None} if there are no +additional details for this connection. + +An example of a connection type that might have additional details is +the @samp{remote} connection, in this case the details string can +contain the @samp{hostname:port} that was used to connect to the +remote target. +@end defvar + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/observable.c b/gdb/observable.c index 51f5edb0a1f..30678c11cae 100644 --- a/gdb/observable.c +++ b/gdb/observable.c @@ -77,6 +77,7 @@ DEFINE_OBSERVABLE (register_changed); DEFINE_OBSERVABLE (user_selected_context_changed); DEFINE_OBSERVABLE (source_styling_changed); DEFINE_OBSERVABLE (current_source_symtab_and_line_changed); +DEFINE_OBSERVABLE (connection_removed); } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/observable.h b/gdb/observable.h index 915770ff363..cb6dc8744f6 100644 --- a/gdb/observable.h +++ b/gdb/observable.h @@ -251,6 +251,9 @@ extern observable<> source_styling_changed; extern observable<> current_source_symtab_and_line_changed; +/* When a connection is removed. */ +extern observable<process_stratum_target */* target */> connection_removed; + } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def index d12a2103e80..6e8c28030d0 100644 --- a/gdb/python/py-all-events.def +++ b/gdb/python/py-all-events.def @@ -38,3 +38,4 @@ GDB_PY_DEFINE_EVENT(breakpoint_created) GDB_PY_DEFINE_EVENT(breakpoint_deleted) GDB_PY_DEFINE_EVENT(breakpoint_modified) GDB_PY_DEFINE_EVENT(before_prompt) +GDB_PY_DEFINE_EVENT(connection_removed) diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c new file mode 100644 index 00000000000..f75a8735148 --- /dev/null +++ b/gdb/python/py-connection.c @@ -0,0 +1,365 @@ +/* Python interface to inferiors. + + Copyright (C) 2009-2021 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/>. */ + +#include "defs.h" +#include "python-internal.h" +#include "process-stratum-target.h" +#include "inferior.h" +#include "observable.h" +#include "target-connection.h" +#include "py-events.h" +#include "py-event.h" +#include "arch-utils.h" + +#include <map> + +/* The Python object that represents a connection. */ + +struct connection_object +{ + PyObject_HEAD + + /* The process target that represents this connection. When a + connection_object is created this field will always point at a valid + target. Later, if GDB stops using this target (the target is popped + from all target stacks) then this field is set to nullptr, which + indicates that this Python object is now in the invalid state (see + the is_valid() method below). */ + struct process_stratum_target *target; +}; + +extern PyTypeObject connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); + +/* Require that CONNECTION be valid. */ +#define CONNPY_REQUIRE_VALID(connection) \ + do { \ + if (connection->target == nullptr) \ + { \ + PyErr_SetString (PyExc_RuntimeError, \ + _("Connection no longer exists.")); \ + return nullptr; \ + } \ + } while (0) + +/* A map between process_stratum targets and the Python object representing + them. We actually hold a gdbpy_ref around the Python object so that + reference counts are handled correctly when entries are deleted. */ +static std::map<process_stratum_target *, + gdbpy_ref<connection_object>> all_connection_objects; + +/* Return a reference to a gdb.TargetConnection object for TARGET. If + TARGET is nullptr then a reference to None is returned. + + Previously created gdb.TargetConnection objects are cached, and + additional references to the same connection object can be returned with + later calls to this function. */ + +gdbpy_ref<> +target_to_connection_object (process_stratum_target *target) +{ + if (target == nullptr) + return gdbpy_ref<>::new_reference (Py_None); + + gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; + if (conn_obj == nullptr) + { + conn_obj.reset (PyObject_New (connection_object, + &connection_object_type)); + if (conn_obj == nullptr) + return nullptr; + + conn_obj->target = target; + + /* PyObject_New initializes the new object with a refcount of 1. This + counts for the reference we are keeping in the target_ops data. */ + all_connection_objects[target] = conn_obj; + } + + gdb_assert (conn_obj != nullptr); + + /* Repackage the result as a PyObject reference. */ + return gdbpy_ref<> ((PyObject *) conn_obj.release ()); +} + +/* Return a list of gdb.TargetConnection objects, one for each currently + active connection. The returned list is in no particular order. */ + +PyObject * +gdbpy_connections (PyObject *self, PyObject *args) +{ + gdbpy_ref<> list (PyList_New (0)); + if (list == nullptr) + return nullptr; + + for (process_stratum_target *target : all_non_exited_process_targets ()) + { + gdb_assert (target != nullptr); + + gdbpy_ref<> conn = target_to_connection_object (target); + if (conn == nullptr) + return nullptr; + gdb_assert (conn.get () != Py_None); + + if (PyList_Append (list.get (), conn.get ()) < 0) + return nullptr; + } + + return list.release (); +} + +/* Emit a connection event for TARGET to REGISTRY. Return 0 on success, or + a negative value on error. */ + +static int +emit_connection_event (process_stratum_target *target, + eventregistry_object *registry) +{ + gdbpy_ref<> event_obj + = create_event_object (&connection_event_object_type); + if (event_obj == nullptr) + return -1; + + gdbpy_ref<> conn = target_to_connection_object (target); + if (evpy_add_attribute (event_obj.get (), "connection", conn.get ()) < 0) + return -1; + + return evpy_emit_event (event_obj.get (), registry); +} + +/* Callback for the connection_removed observer. */ + +static void +connpy_connection_removed (process_stratum_target *target) +{ + if (!gdb_python_initialized) + return; + + gdbpy_enter enter_py (get_current_arch (), current_language); + + if (!evregpy_no_listeners_p (gdb_py_events.connection_removed)) + if (emit_connection_event (target, gdb_py_events.connection_removed) < 0) + gdbpy_print_stack (); + + gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; + if (conn_obj != nullptr) + { + conn_obj->target = nullptr; + all_connection_objects.erase (target); + } +} + +/* Called when a gdb.TargetConnection object is deallocated. */ + +static void +connpy_connection_dealloc (PyObject *obj) +{ + connection_object *conn_obj = (connection_object *) obj; + + /* As the all_connection_objects map holds a reference to each connection + object we can only enter the dealloc function when the reference in + all_connection_objects has been erased. + + As we always set the target pointer back to nullptr before we erase + items from all_connection_objects then, when we get here, the target + pointer must be nullptr. */ + gdb_assert (conn_obj->target == nullptr); + + Py_TYPE (obj)->tp_free (obj); +} + +/* Implement repr() for gdb.TargetConnection. */ + +static PyObject * +connpy_repr (PyObject *obj) +{ + connection_object *self = (connection_object *) obj; + process_stratum_target *target = self->target; + + if (target == nullptr) + return PyString_FromString ("<gdb.TargetConnection (invalid)>"); + + return PyString_FromFormat ("<gdb.TargetConnection num=%d, what=\"%s\">", + target->connection_number, + make_target_connection_string (target).c_str ()); +} + +/* Implementation of gdb.TargetConnection.is_valid() -> Boolean. Returns + True if this connection object is still associated with a + process_stratum_target, otherwise, returns False. */ + +static PyObject * +connpy_is_valid (PyObject *self, PyObject *args) +{ + connection_object *conn = (connection_object *) self; + + if (conn->target == nullptr) + Py_RETURN_FALSE; + + Py_RETURN_TRUE; +} + +/* Return the id number of this connection. */ + +static PyObject * +connpy_get_connection_num (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + auto num = conn->target->connection_number; + return gdb_py_object_from_longest (num).release (); +} + +/* Return a string that gives the short name for this connection type. */ + +static PyObject * +connpy_get_connection_type (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *shortname = conn->target->shortname (); + return host_string_to_python_string (shortname).release (); +} + +/* Return a string that gives a longer description of this connection type. */ + +static PyObject * +connpy_get_description (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *longname = conn->target->longname (); + return host_string_to_python_string (longname).release (); +} + +/* Return a string that gives additional details about this connection, or + None, if there are no additional details for this connection type. */ + +static PyObject * +connpy_get_connection_details (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *details = conn->target->connection_string (); + if (details != nullptr) + return host_string_to_python_string (details).release (); + else + Py_RETURN_NONE; +} + +/* Python specific initialization for this file. */ + +int +gdbpy_initialize_connection (void) +{ + if (PyType_Ready (&connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "TargetConnection", + (PyObject *) &connection_object_type) < 0) + return -1; + + return 0; +} + +/* Global initialization for this file. */ + +void _initialize_py_connection (); +void +_initialize_py_connection () +{ + gdb::observers::connection_removed.attach (connpy_connection_removed, + "py-connection"); +} + +/* Methods for the gdb.TargetConnection object type. */ + +static PyMethodDef connection_object_methods[] = +{ + { "is_valid", connpy_is_valid, METH_NOARGS, + "is_valid () -> Boolean.\n\ +Return true if this TargetConnection is valid, false if not." }, + { NULL } +}; + +/* Attributes for the gdb.TargetConnection object type. */ + +static gdb_PyGetSetDef connection_object_getset[] = +{ + { "num", connpy_get_connection_num, NULL, + "ID number of this connection, as assigned by GDB.", NULL }, + { "type", connpy_get_connection_type, NULL, + "A short string that is the name for this connection type.", NULL }, + { "description", connpy_get_description, NULL, + "A longer string describing this connection type.", NULL }, + { "details", connpy_get_connection_details, NULL, + "A string containing additional connection details.", NULL }, + { NULL } +}; + +/* Define the gdb.TargetConnection object type. */ + +PyTypeObject connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.TargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + connection_object_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def index 70df4804fc4..8db7ae5f47c 100644 --- a/gdb/python/py-event-types.def +++ b/gdb/python/py-event-types.def @@ -105,3 +105,8 @@ GDB_PY_DEFINE_EVENT_TYPE (thread, "ThreadEvent", "GDB thread event object", event_object_type); + +GDB_PY_DEFINE_EVENT_TYPE (connection, + "ConnectionEvent", + "GDB connection added or removed object", + event_object_type); diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c index 0659c28ea9c..6f328b2a9af 100644 --- a/gdb/python/py-inferior.c +++ b/gdb/python/py-inferior.c @@ -427,6 +427,20 @@ infpy_get_num (PyObject *self, void *closure) return gdb_py_object_from_longest (inf->inferior->num).release (); } +/* Return the gdb.TargetConnection object for this inferior, or None if a + connection does not exist. */ + +static PyObject * +infpy_get_connection (PyObject *self, void *closure) +{ + inferior_object *inf = (inferior_object *) self; + + INFPY_REQUIRE_VALID (inf); + + process_stratum_target *target = inf->inferior->process_target (); + return target_to_connection_object (target).release (); +} + /* Return the connection number of the given inferior, or None if a connection does not exist. */ @@ -960,6 +974,8 @@ gdbpy_initialize_inferior (void) static gdb_PyGetSetDef inferior_object_getset[] = { { "num", infpy_get_num, NULL, "ID of inferior, as assigned by GDB.", NULL }, + { "connection", infpy_get_connection, NULL, + "The gdb.TargetConnection for this inferior.", NULL }, { "connection_num", infpy_get_connection_num, NULL, "ID of inferior's connection, as assigned by GDB.", NULL }, { "pid", infpy_get_pid, NULL, "PID of inferior, as assigned by the OS.", diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h index 368681b797c..c99fe270b9e 100644 --- a/gdb/python/python-internal.h +++ b/gdb/python/python-internal.h @@ -478,6 +478,10 @@ gdbpy_ref<thread_object> create_thread_object (struct thread_info *tp); gdbpy_ref<> thread_to_thread_object (thread_info *thr);; gdbpy_ref<inferior_object> inferior_to_inferior_object (inferior *inf); +struct process_stratum_target; +gdbpy_ref<> target_to_connection_object (process_stratum_target *target); +PyObject *gdbpy_connections (PyObject *self, PyObject *args); + const struct block *block_object_to_block (PyObject *obj); struct symbol *symbol_object_to_symbol (PyObject *obj); struct value *value_object_to_value (PyObject *self); @@ -549,6 +553,8 @@ int gdbpy_initialize_unwind (void) CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; int gdbpy_initialize_tui () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; +int gdbpy_initialize_connection () + CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; /* A wrapper for PyErr_Fetch that handles reference counting for the caller. */ diff --git a/gdb/python/python.c b/gdb/python/python.c index 6af9c722e7b..e226634af9e 100644 --- a/gdb/python/python.c +++ b/gdb/python/python.c @@ -1839,6 +1839,7 @@ do_start_initialization () || gdbpy_initialize_registers () < 0 || gdbpy_initialize_xmethods () < 0 || gdbpy_initialize_unwind () < 0 + || gdbpy_initialize_connection () < 0 || gdbpy_initialize_tui () < 0) return false; @@ -2213,6 +2214,10 @@ Set the value of the convenience variable $NAME." }, Register a TUI window constructor." }, #endif /* TUI */ + { "connections", gdbpy_connections, METH_NOARGS, + "connections () -> List.\n\ +Return a list of gdb.TargetConnection objects." }, + {NULL, NULL, 0, NULL} }; diff --git a/gdb/target-connection.c b/gdb/target-connection.c index a649423e07e..da1c9da74dc 100644 --- a/gdb/target-connection.c +++ b/gdb/target-connection.c @@ -24,6 +24,7 @@ #include "inferior.h" #include "target.h" +#include "observable.h" /* A map between connection number and representative process_stratum target. */ @@ -49,6 +50,9 @@ connection_list_add (process_stratum_target *t) void connection_list_remove (process_stratum_target *t) { + /* Notify about the connection being removed before we reset the + connection number to zero. */ + gdb::observers::connection_removed.notify (t); process_targets.erase (t->connection_number); t->connection_number = 0; } diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp index 3fd6b15cdd9..3f74a47de31 100644 --- a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp @@ -15,18 +15,27 @@ # Test "info inferiors" and "info connections" with multiple targets. +load_lib gdb-python.exp + source $srcdir/$subdir/multi-target.exp.tcl if {![multi_target_prepare]} { return } +# Cache the result of calling skip_python_tests into a local variable. +set run_python_tests [expr ! [skip_python_tests]] + # Test "info inferiors" and "info connections". MULTI_PROCESS # indicates whether the multi-process feature of remote targets is # turned off or on. proc test_info_inferiors {multi_process} { setup "off" + if { $::run_python_tests } { + gdb_test_no_output "source ${::remote_python_file}" "load python file" + } + gdb_test_no_output \ "set remote multiprocess-feature-packet $multi_process" @@ -86,6 +95,18 @@ proc test_info_inferiors {multi_process} { "[connection_num 5 $inf]${ws}core${ws}Local core dump file${ws}" \ ] + if { $::run_python_tests } { + gdb_test "python info_connections()" \ + [multi_line \ + "Num${ws}What${ws}Description" \ + "[connection_num 1 $inf]${ws}native${ws}Native process" \ + "[connection_num 2 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 3 $inf]${ws}core${ws}Local core dump file" \ + "[connection_num 4 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 5 $inf]${ws}core${ws}Local core dump file" \ + ] + } + gdb_test "info inferiors" \ [multi_line \ "Num${ws}Description${ws}Connection${ws}Executable${ws}" \ @@ -96,10 +117,27 @@ proc test_info_inferiors {multi_process} { "[inf_desc 5 $inf]4 \\(extended-remote localhost:$decimal\\)${ws}${binfile}${ws}" \ "[inf_desc 6 $inf]5 \\(core\\)${ws}${binfile}${ws}" \ ] + + if { $::run_python_tests } { + gdb_test "python info_inferiors()" \ + [multi_line \ + "Inferior 1, Connection #1: native" \ + "Inferior 2, Connection #2: extended-remote localhost:$decimal" \ + "Inferior 3, Connection #3: core" \ + "Inferior 4, Connection #1: native" \ + "Inferior 5, Connection #4: extended-remote localhost:$decimal" \ + "Inferior 6, Connection #5: core" \ + ] + } } } } +if { $run_python_tests } { + set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +} + # Test "info inferiors" and "info connections" commands. with_test_prefix "info-inferiors" { foreach_with_prefix multi_process {"on" "off"} { diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py new file mode 100644 index 00000000000..aac561ae683 --- /dev/null +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py @@ -0,0 +1,63 @@ +# Copyright (C) 2021 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 + +# Take a gdb.TargetConnection and return the connection number. +def conn_num(c): + return c.num + +# Takes a gdb.TargetConnection and return a string that is either the +# type, or the type and details (if the details are not None). +def make_target_connection_string(c): + if c.details is None: + return c.type + else: + return "%s %s" % (c.type, c.details) + +# A Python implementation of 'info connections'. Produce output that +# is identical to the output of 'info connections' so we can check +# that aspects of gdb.TargetConnection work correctly. +def info_connections(): + all_connections = sorted(gdb.connections(), key=conn_num) + current_conn = gdb.selected_inferior().connection + what_width = 0 + for c in all_connections: + s = make_target_connection_string(c) + if len(s) > what_width: + what_width = len(s) + + fmt = " Num %%-%ds Description" % what_width + print(fmt % "What") + fmt = "%%s%%-3d %%-%ds %%s" % what_width + for c in all_connections: + if c == current_conn: + prefix = "* " + else: + prefix = " " + + print(fmt % (prefix, c.num, make_target_connection_string(c), + c.description)) + +def inf_num(i): + return i.num + +# .... +def info_inferiors(): + all_inferiors = sorted(gdb.inferiors(), key=inf_num) + for i in gdb.inferiors(): + print("Inferior %d, Connection #%d: %s" % + (i.num, i.connection_num, + make_target_connection_string(i.connection))) diff --git a/gdb/testsuite/gdb.python/py-connection.c b/gdb/testsuite/gdb.python/py-connection.c new file mode 100644 index 00000000000..4b32e704476 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp new file mode 100644 index 00000000000..b994cec01eb --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -0,0 +1,63 @@ +# Copyright (C) 2021 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/>. + +# This file is for testing the gdb.TargetConnection API. This API is +# already tested in gdb.multi/multi-target-info-inferiors.exp and +# gdb.python/py-inferior.exp, this file just covers some edge cases +# that are not tested in other places. + +load_lib gdb-python.exp + +standard_testfile + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +# Skip all tests if Python scripting is not enabled. +if { [skip_python_tests] } { continue } + +if ![runto_main] then { + fail "can't run to main" + return 0 +} + +# Create a gdb.TargetConnection object and check it becomes invalid +# once the connection has gone away. +gdb_test_no_output "python conn = gdb.selected_inferior().connection" +gdb_test "python print(conn)" \ + "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ + "print gdb.TargetConnection while it is still valid" +gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" +gdb_test "info connections" "\r\n\\* 1 .*" \ + "info connections while the connection is still around" +gdb_test "kill" "" "kill the inferior" \ + "Kill the program being debugged.*y or n. $" "y" +gdb_test "info connections" "No connections\\." \ + "info connections now all the connections have gone" +gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ + "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" + +# Now check that accessing properties of the invalid connection cases +# an error. +gdb_test "python print(conn.num)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.type)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.description)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.details)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" diff --git a/gdb/testsuite/gdb.python/py-inferior.exp b/gdb/testsuite/gdb.python/py-inferior.exp index 2252215b721..2060a715367 100644 --- a/gdb/testsuite/gdb.python/py-inferior.exp +++ b/gdb/testsuite/gdb.python/py-inferior.exp @@ -52,6 +52,8 @@ gdb_py_test_silent_cmd "python i0 = inferiors\[0\]" "get first inferior" 0 gdb_test "python print ('result = %s' % (i0 == inferiors\[0\]))" " = True" "test equality comparison (true)" gdb_test "python print ('result = %s' % i0.num)" " = \[0-9\]+" "test Inferior.num" gdb_test "python print ('result = %s' % i0.connection_num)" " = \[0-9\]+" "test Inferior.connection_num" +gdb_test "python print ('result = %s' % (i0.connection_num == i0.connection.num))" " = True" \ + "Inferior.connection_num equals Inferior.connection.num" gdb_test "python print ('result = %s' % i0.pid)" " = \[0-9\]+" "test Inferior.pid" gdb_test "python print ('result = %s' % i0.was_attached)" " = False" "test Inferior.was_attached" gdb_test "python print (i0.threads ())" "\\(<gdb.InferiorThread object at 0x\[\[:xdigit:\]\]+>,\\)" "test Inferior.threads" @@ -265,6 +267,8 @@ with_test_prefix "is_valid" { "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].connection_num)" \ "RuntimeError: Inferior no longer exists.*" + gdb_test "python print (inf_list\[1\].connection)" \ + "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].pid)" \ "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].was_attached)" \ @@ -282,7 +286,9 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 1" ".*" "switch to first inferior" gdb_test "py print (gdb.selected_inferior().num)" "1" "first inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "1" \ - "first inferior's connection" + "first inferior's connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" "1" \ + "first inferior's connection number, though connection object" # Figure out if inf 1 has a native target. set inf_1_is_native [gdb_is_target_native] @@ -290,6 +296,8 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 3" ".*" "switch to third inferior" gdb_test "py print (gdb.selected_inferior().num)" "3" "third inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "None" \ + "third inferior's None connection number" + gdb_test "py print (gdb.selected_inferior().connection)" "None" \ "third inferior's None connection" gdb_test "target native" "Done. Use the \"run\" command to start a process." \ "target for the third inferior" @@ -303,7 +311,15 @@ with_test_prefix "selected_inferior" { } gdb_test "py print (gdb.selected_inferior().connection_num)" \ "$expected_connection_num" \ - "third inferior's native connection" + "third inferior's native connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" \ + "$expected_connection_num" \ + "third inferior's native connection number, though connection object" + + # Test printing of gdb.TargetConnection object. + gdb_test "py print (gdb.selected_inferior().connection)" \ + "<gdb.TargetConnection num=${expected_connection_num}, what=\"\[^\"\]+\">" \ + "print a connection object" gdb_test "inferior 1" ".*" "switch back to first inferior" gdb_test_no_output "remove-inferiors 3" "remove second inferior" -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-09-11 16:03 ` [PATCH 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess @ 2021-09-11 16:19 ` Eli Zaretskii 0 siblings, 0 replies; 52+ messages in thread From: Eli Zaretskii @ 2021-09-11 16:19 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches > From: Andrew Burgess <andrew.burgess@embecosm.com> > Date: Sat, 11 Sep 2021 17:03:23 +0100 > > diff --git a/gdb/NEWS b/gdb/NEWS > index f9485520438..6d8739573b3 100644 > --- a/gdb/NEWS > +++ b/gdb/NEWS > @@ -17,6 +17,22 @@ maint show backtrace-on-fatal-signal > integer, the index of the new item in the history list, is > returned. > > + ** New gdb.TargetConnection object type that represents a connection > + (as displayed by the 'info connections' command). > + > + ** The gdb.Inferior type now has a 'connection' property which is an > + instance of gdb.TargetConnection, the connection used by this > + inferior. This can be None if the inferior has no connection > + (for example, when exited). > + > + ** New 'gdb.events.connection_removed' event registry, which emits a > + 'gdb.ConnectionEvent' when a connection is removed from GDB. > + This event has a 'connection' property, a gdb.TargetConnection > + object for the connection being removed. > + > + ** New gdb.connections() function that returns a list of all > + currently active connections. > + > *** Changes in GDB 11 This part is OK. > +@node Connections In Python > +@subsubsection Connections In Python > +@cindex Connections In Python Index entries should not generally use capital letters (because then sorting the index will produce different orders in different locales). > +@value{GDBN} uses the @code{gdb.TargetConnection} object type to > +represent a connection in Python code. To get a list of all > +connections use @code{gdb.connections()} Please remove the "()" parentheses, there's no need to use them here. > +An example of a connection type that might have additional details is > +the @samp{remote} connection, in this case the details string can > +contain the @samp{hostname:port} that was used to connect to the Either "the host name and the port" (without @samp), or "@samp{@var{hostname}:@var{port}}", since these are meta-syntactic variables. Thanks. ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCH 2/3] gdb: make packet_command function available outside remote.c 2021-09-11 16:03 [PATCH 0/3] Python API for target connections, and packet sending Andrew Burgess 2021-09-11 16:03 ` [PATCH 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess @ 2021-09-11 16:03 ` Andrew Burgess 2021-09-11 16:03 ` [PATCH 3/3] gdb/python: add TargetConnection.send_remote_packet method Andrew Burgess 2021-10-18 9:45 ` [PATCHv2 0/3] Python API for target connections, and packet sending Andrew Burgess 3 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-09-11 16:03 UTC (permalink / raw) To: gdb-patches In a later commit I will add a Python API to access the 'maint packet' functionality, that is, sending a user specified packet to the target. To make implementing this easier, this commit refactors how this command is currently implemented so that the packet_command function is now global. The new global send_remote_packet function takes an object that is an implementation of an abstract interface. Two functions within this interface are then called, one just before a packet is sent to the remote target, and one when the reply has been received from the remote target. Using an interface object in this way allows (1) for the error checking to be done before the first callback is made, this means we only print out what packet it being sent once we know we are going to actually send it, and (2) we don't need to make a copy of the reply if all we want to do is print it. The only user visible changes after this commit are the error messages, which I've changed to be less 'maint packet' command focused, this will make them (I hope) better for when send_remote_packet can be called from Python code. So: "command can only be used with remote target" Becomes: "packets can only be sent to a remote target" And: "remote-packet command requires packet text as argument" Becomes: "a remote packet must not be empty" --- gdb/remote.c | 67 +++++++++++++++++++++++++++++++++++----------------- gdb/remote.h | 34 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/gdb/remote.c b/gdb/remote.c index b6da6b086a2..21a7ff18442 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -956,8 +956,6 @@ class remote_target : public process_stratum_target bool vcont_r_supported (); - void packet_command (const char *args, int from_tty); - private: /* data fields */ /* The remote state. Don't reference this directly. Use the @@ -11605,34 +11603,59 @@ remote_target::memory_map () return result; } -static void -packet_command (const char *args, int from_tty) +/* Set of callbacks used to implement the 'maint packet' command. */ + +struct cli_packet_command_callbacks : public send_remote_packet_callbacks { - remote_target *remote = get_current_remote_target (); + /* Called before the packet is sent. PACKET_STR is the packet content + before the protocol specific prefix, suffix, and escaping is added. */ - if (remote == nullptr) - error (_("command can only be used with remote target")); + void sending (const char *packet_str) override + { + puts_filtered ("sending: "); + print_packet (packet_str); + puts_filtered ("\n"); + } - remote->packet_command (args, from_tty); -} + /* Called with BUF, the reply from the remote target. */ + + void received (const gdb::char_vector &buf) override + { + puts_filtered ("received: "); + print_packet (buf.data ()); + puts_filtered ("\n"); + } +}; + +/* See remote.h. */ void -remote_target::packet_command (const char *args, int from_tty) +send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks) { - if (!args) - error (_("remote-packet command requires packet text as argument")); + if (packet_str == nullptr || *packet_str == '\0') + error (_("a remote packet must not be empty")); - puts_filtered ("sending: "); - print_packet (args); - puts_filtered ("\n"); - putpkt (args); + remote_target *remote = get_current_remote_target (); + if (remote == nullptr) + error (_("packets can only be sent to a remote target")); - remote_state *rs = get_remote_state (); + callbacks->sending (packet_str); - getpkt (&rs->buf, 0); - puts_filtered ("received: "); - print_packet (rs->buf.data ()); - puts_filtered ("\n"); + remote->putpkt (packet_str); + remote_state *rs = remote->get_remote_state (); + remote->getpkt (&rs->buf, 0); + + callbacks->received (rs->buf); +} + +/* Entry point for the 'maint packet' command. */ + +static void +cli_packet_command (const char *args, int from_tty) +{ + cli_packet_command_callbacks cb; + send_remote_packet (args, &cb); } #if 0 @@ -14911,7 +14934,7 @@ Argument is a single section name (default: all loaded sections).\n\ To compare only read-only loaded sections, specify the -r option."), &cmdlist); - add_cmd ("packet", class_maintenance, packet_command, _("\ + add_cmd ("packet", class_maintenance, cli_packet_command, _("\ Send an arbitrary packet to a remote target.\n\ maintenance packet TEXT\n\ If GDB is talking to an inferior via the GDB serial protocol, then\n\ diff --git a/gdb/remote.h b/gdb/remote.h index 46bfa01fc79..cd91be8decb 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -78,4 +78,38 @@ extern int remote_register_number_and_offset (struct gdbarch *gdbarch, extern void remote_notif_get_pending_events (remote_target *remote, struct notif_client *np); extern bool remote_target_is_non_stop_p (remote_target *t); + +/* An abstract class that represents the set of callbacks that are made + from the send_remote_packet function (declared below). */ + +struct send_remote_packet_callbacks +{ + /* The SENDING callback is called once send_remote_packet has performed + its error checking and setup, just before the packet is sent to the + remote target. PACKET_STR is the content of the packet that will be + sent (before any of the protocol specific prefix, suffix, or escaping + is applied). */ + + virtual void sending (const char *packet_str) = 0; + + /* The RECEIVED callback is called once a reply has been received from + the remote target. The content of the reply is in BUF which can't be + modified, and which is not guaranteed to remain valid after the + RECEIVED call has returned. If you need to preserve the contents of + BUF then a copy should be taken. */ + + virtual void received (const gdb::char_vector &buf) = 0; +}; + +/* Send PACKET_STR the current remote target. If PACKET_STR is nullptr, or + is the empty string, then an error is thrown. If the current target is + not a remote target then an error is thrown. + + Calls CALLBACKS->sending() just before the packet is sent to the remote + target, and calls CALLBACKS->received() with the reply once this is + received from the remote target. */ + +extern void send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks); + #endif -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCH 3/3] gdb/python: add TargetConnection.send_remote_packet method 2021-09-11 16:03 [PATCH 0/3] Python API for target connections, and packet sending Andrew Burgess 2021-09-11 16:03 ` [PATCH 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-09-11 16:03 ` [PATCH 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess @ 2021-09-11 16:03 ` Andrew Burgess 2021-09-11 16:10 ` Eli Zaretskii 2021-10-18 9:45 ` [PATCHv2 0/3] Python API for target connections, and packet sending Andrew Burgess 3 siblings, 1 reply; 52+ messages in thread From: Andrew Burgess @ 2021-09-11 16:03 UTC (permalink / raw) To: gdb-patches This commit adds a new method to the gdb.TargetConnection object type: 'send_remote_packet'. This new method is equivalent to the 'maint packet' CLI command, it allows a custom packet to be sent to a remote target. Not all gdb.TargetConnection instances will support send_remote_packet, e.g. the 'native' target doesn't. If the send_remote_packet method is invoked on a TargetConnection that is not 'remote' or 'extended-remote' then an exception is thrown. The result of calling TargetConnection.send_remote_packet is a string containing the reply that came from the remote. --- gdb/NEWS | 6 ++ gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 23 +++++- gdb/python/py-connection.c | 90 +++++++++++++++++++++ gdb/testsuite/gdb.python/py-send-packet.c | 22 +++++ gdb/testsuite/gdb.python/py-send-packet.exp | 55 +++++++++++++ gdb/testsuite/gdb.python/py-send-packet.py | 82 +++++++++++++++++++ 7 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py diff --git a/gdb/NEWS b/gdb/NEWS index 6d8739573b3..86adbb9ae96 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -33,6 +33,12 @@ maint show backtrace-on-fatal-signal ** New gdb.connections() function that returns a list of all currently active connections. + ** New gdb.TargetConnection.send_remote_packet(STRING) method. This + is equivalent to the existing 'maint packet' CLI command; it + allows a user specified packet to be sent to the remote target. + If this method is called on a TargetConnection that is not of + type 'remote' or 'extended-remote' then an exception is thrown. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index 58479ef3ed6..2bfcefd028c 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -39231,6 +39231,7 @@ disabled. @end table +@anchor{maint packet} @kindex maint packet @item maint packet @var{text} If @value{GDBN} is talking to an inferior via the serial protocol, diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 9f92aa791c4..6ecd82edb94 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -5976,7 +5976,7 @@ @code{gdb.Inferior.connection} attribute (@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). -A @code{gdb.TargetConnection} has the following method: +A @code{gdb.TargetConnection} has the following methods: @defun TargetConnection.is_valid () Return @code{True} if the @code{gdb.TargetConnection} object is valid, @@ -5988,6 +5988,27 @@ an exception if the connection is invalid. @end defun +@defun TargetConnection.send_remote_packet (@var{packet}) +When the connection object is using @value{GDBN}'s remote serial +protocol (@pxref{Remote Protocol}), i.e. the +@code{TargetConnection.type} is @samp{remote} or +@samp{extended-remote}, then this method sends @var{packet}, which +should be a string, to the remote target and returns the response +packet as a string. + +This is equivalent to the @code{maintenance packet} command +(@pxref{maint packet,,maintenance packet command}). + +If the connection is of any other type then an exception of type +@code{gdb.error} is thrown. If @var{packet} is not a string, or is +the empty string, then an exception of type @code{gdb.error} is +thrown. + +The prefix, suffix, and checksum (as required by the remote serial +protocol) are automatically added to the outgoing packet, and removed +from the incoming packet before the reply is returned. +@end defun + A @code{gdb.TargetConnection} has the following read-only properties: @defvar TargetConnection.num diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c index f75a8735148..ddc4e05d63f 100644 --- a/gdb/python/py-connection.c +++ b/gdb/python/py-connection.c @@ -26,6 +26,8 @@ #include "py-events.h" #include "py-event.h" #include "arch-utils.h" +#include "remote.h" +#include "charset.h" #include <map> @@ -286,6 +288,90 @@ gdbpy_initialize_connection (void) return 0; } +/* Set of callbacks used to implement gdb.send_remote_packet. */ + +struct py_send_packet_callbacks : public send_remote_packet_callbacks +{ + /* Constructor, initialise the result to None. */ + + py_send_packet_callbacks () + : m_result (Py_None) + { /* Nothing. */ } + + /* There's nothing to do when the packet is sent. */ + + void sending (const char *args) override + { /* Nothing. */ } + + /* When the result is returned create a Python string and assign this + into the result member variable. */ + + void received (const gdb::char_vector &buf) override + { + /* Return None when we don't get back a useful result. */ + if (buf.data ()[0] != '\0') + m_result = gdbpy_ref<> (PyUnicode_Decode (buf.data (), + strlen (buf.data ()), + host_charset (), NULL)); + } + + /* Get a reference to the result as a Python object. */ + + gdbpy_ref<> result () const + { + return m_result; + } + +private: + + /* A reference to a valid result value. This is initialized in the + constructor, and so will always point to a valid value, even if this + is just None. */ + + gdbpy_ref<> m_result; +}; + +/* Implement TargetConnection.send_remote_packet function. Send a packet + to the target identified by SELF. The connection must still be valid, + and must be a remote or extended-remote connection otherwise an + exception will be thrown. */ + +static PyObject * +connpy_send_remote_packet (PyObject *self, PyObject *args, PyObject *kw) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + static const char *keywords[] = {"packet", nullptr}; + const char *packet_str = nullptr; + + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s", keywords, + &packet_str)) + return nullptr; + + if (packet_str == nullptr || *packet_str == '\0') + { + PyErr_SetString (PyExc_ValueError, _("Invalid remote packet")); + return nullptr; + } + + try + { + scoped_restore_current_thread restore_thread; + switch_to_target_no_thread (conn->target); + + py_send_packet_callbacks callbacks; + send_remote_packet (packet_str, &callbacks); + return callbacks.result ().release (); + } + catch (const gdb_exception &except) + { + gdbpy_convert_exception (except); + return NULL; + } +} + /* Global initialization for this file. */ void _initialize_py_connection (); @@ -303,6 +389,10 @@ static PyMethodDef connection_object_methods[] = { "is_valid", connpy_is_valid, METH_NOARGS, "is_valid () -> Boolean.\n\ Return true if this TargetConnection is valid, false if not." }, + { "send_remote_packet", (PyCFunction) connpy_send_remote_packet, + METH_VARARGS | METH_KEYWORDS, + "send_remote_packet (PACKET) -> String\n\ +Send PACKET to a remote target, return the reply as a string." }, { NULL } }; diff --git a/gdb/testsuite/gdb.python/py-send-packet.c b/gdb/testsuite/gdb.python/py-send-packet.c new file mode 100644 index 00000000000..9811b15f06d --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main () +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp new file mode 100644 index 00000000000..81001343f0c --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.exp @@ -0,0 +1,55 @@ +# Copyright (C) 2021 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 the gdb.TargetConnection.send_remote_packet API. This is done +# by connecting to a remote target and fetching the thread list in two +# ways, first, we manually send the packets required to read the +# thread list using gdb.TargetConnection.send_remote_packet, then we +# compare the results to the thread list using the standard API calls. + +load_lib gdb-python.exp +load_lib gdbserver-support.exp + +standard_testfile + +if {[skip_gdbserver_tests]} { + return 0 +} + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +gdb_exit +gdb_start + +if { [skip_python_tests] } { + return 0 +} + +# Make sure we're disconnected, in case we're testing with an +# extended-remote board, therefore already connected. +gdb_test "disconnect" ".*" + +gdbserver_run "" + +# Source the python script. +set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +gdb_test "source $remote_python_file" "Sourcing complete\\." \ + "source ${testfile}.py script" + +# The test is actually written in the Python script. Run it now. +gdb_test "python run_send_packet_test()" "Send packet test passed" diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py new file mode 100644 index 00000000000..f4dc5734057 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.py @@ -0,0 +1,82 @@ +# Copyright (C) 2021 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 xml.etree.ElementTree as ET +import gdb + +# Make use of gdb.TargetConnection.send_remote_packet to fetch the +# thread list from the remote target. +# +# Sending existing serial protocol packets like this is not a good +# idea, there should be better ways to get this information using an +# official API, this is just being used as a test case. +# +# Really, the send_remote_packet API would be used to send target +# specific packets to the target, but these are, by definition, target +# specific, so hard to test in a general testsuite. +def get_thread_list_str(): + start_pos = 0 + thread_desc = "" + try: + while True: + conn = gdb.selected_inferior().connection + str = conn.send_remote_packet("qXfer:threads:read::%d,200" % start_pos) + start_pos += 200 + c = str[0] + str = str[1:] + thread_desc += str + if c == "l": + break + return thread_desc + except: + return None + + +# Use gdb.TargetConnection.send_remote_packet to manually fetch the +# thread list, then extract the thread list using the gdb.Inferior and +# gdb.InferiorThread API. Compare the two results to ensure we +# managed to successfully read the thread list from the remote. +def run_send_packet_test(): + # Find the IDs of all current threads. + all_threads = {} + for inf in gdb.inferiors(): + for thr in inf.threads(): + id = "p%x.%x" % (thr.ptid[0], thr.ptid[1]) + all_threads[id] = False + + # Now fetch the thread list from the remote, and parse the XML. + str = get_thread_list_str() + threads_xml = ET.fromstring(str) + + # Look over all threads in the XML list and check we expected to + # find them, mark the ones we do find. + for thr in threads_xml: + id = thr.get("id") + if not id in all_threads: + raise "found unexpected thread in remote thread list" + else: + all_threads[id] = True + + # Check that all the threads were found in the XML list. + for id in all_threads: + if not all_threads[id]: + raise "thread missingt from remote thread list" + + # Test complete. + print("Send packet test passed") + + +# Just to indicate the file was sourced correctly. +print("Sourcing complete.") -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCH 3/3] gdb/python: add TargetConnection.send_remote_packet method 2021-09-11 16:03 ` [PATCH 3/3] gdb/python: add TargetConnection.send_remote_packet method Andrew Burgess @ 2021-09-11 16:10 ` Eli Zaretskii 0 siblings, 0 replies; 52+ messages in thread From: Eli Zaretskii @ 2021-09-11 16:10 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches > From: Andrew Burgess <andrew.burgess@embecosm.com> > Date: Sat, 11 Sep 2021 17:03:25 +0100 > > diff --git a/gdb/NEWS b/gdb/NEWS > index 6d8739573b3..86adbb9ae96 100644 > --- a/gdb/NEWS > +++ b/gdb/NEWS > @@ -33,6 +33,12 @@ maint show backtrace-on-fatal-signal > ** New gdb.connections() function that returns a list of all > currently active connections. > > + ** New gdb.TargetConnection.send_remote_packet(STRING) method. This > + is equivalent to the existing 'maint packet' CLI command; it > + allows a user specified packet to be sent to the remote target. > + If this method is called on a TargetConnection that is not of > + type 'remote' or 'extended-remote' then an exception is thrown. This part is OK. > +@defun TargetConnection.send_remote_packet (@var{packet}) > +When the connection object is using @value{GDBN}'s remote serial > +protocol (@pxref{Remote Protocol}), i.e. the ^ There should be either a comma or @: after "i.e.", to prevent TeX from handling that as the end of a sentence. > +If the connection is of any other type then an exception of type > +@code{gdb.error} is thrown. Can you try rewording this so it doesn't use passive tense? Passive tense generally makes text less clear and harder to read. Thanks. ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv2 0/3] Python API for target connections, and packet sending 2021-09-11 16:03 [PATCH 0/3] Python API for target connections, and packet sending Andrew Burgess ` (2 preceding siblings ...) 2021-09-11 16:03 ` [PATCH 3/3] gdb/python: add TargetConnection.send_remote_packet method Andrew Burgess @ 2021-10-18 9:45 ` Andrew Burgess 2021-10-18 9:45 ` [PATCHv2 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess ` (3 more replies) 3 siblings, 4 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-18 9:45 UTC (permalink / raw) To: gdb-patches Since v1: - Rebased onto current upstream master and resolved the merge conflicts. - Addressed the documentation feedback from Eli. I'm not 100% sure that I have correctly addressed the concerns about patch #3, so this is probably worth rechecking. --- Andrew Burgess (3): gdb/python: introduce gdb.TargetConnection object type gdb: make packet_command function available outside remote.c gdb/python: add TargetConnection.send_remote_packet method gdb/Makefile.in | 1 + gdb/NEWS | 22 + gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 112 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 455 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/remote.c | 67 ++- gdb/remote.h | 34 ++ gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 +++ gdb/testsuite/gdb.python/py-connection.c | 22 + gdb/testsuite/gdb.python/py-connection.exp | 63 +++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- gdb/testsuite/gdb.python/py-send-packet.c | 22 + gdb/testsuite/gdb.python/py-send-packet.exp | 55 +++ gdb/testsuite/gdb.python/py-send-packet.py | 82 ++++ 23 files changed, 1072 insertions(+), 26 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv2 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-10-18 9:45 ` [PATCHv2 0/3] Python API for target connections, and packet sending Andrew Burgess @ 2021-10-18 9:45 ` Andrew Burgess 2021-10-18 12:44 ` Eli Zaretskii 2021-10-18 15:53 ` Simon Marchi 2021-10-18 9:45 ` [PATCHv2 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess ` (2 subsequent siblings) 3 siblings, 2 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-18 9:45 UTC (permalink / raw) To: gdb-patches This commit adds a new object type gdb.TargetConnection. This new type represents a connection within GDB (a connection as displayed by 'info connections'). There's three ways to find a gdb.TargetConnection, there's a new 'gdb.connections()' function, which returns a list of all currently active connections. Or you can read the new 'connection' property on the gdb.Inferior object type, this contains the connection for that inferior (or None if the inferior has no connection, for example, it is exited). Finally, there's a new gdb.events.connection_removed event registry, this emits a new gdb.ConnectionEvent whenever a connection is removed from GDB (this happens when all inferiors using a connection exit). The gdb.ConnectionEvent has a 'connection' property, which is the gdb.TargetConnection being removed from GDB. The gdb.TargetConnection has a 'is_valid()' method. A connection object becomes invalid when the underlying connection is removed from GDB (all inferiors using the connection exit). The gdb.TargetConnection has the following read-only properties: 'num': The number for this connection, 'type': e.g. 'native', 'remove', 'sim', etc 'description': The longer description as seen in the 'info connections' command output. 'details': A string or None. Extra details for the connection, for example, a remote connection's details might be 'hostname:port'. --- gdb/Makefile.in | 1 + gdb/NEWS | 16 + gdb/doc/python.texi | 92 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 365 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 +++ gdb/testsuite/gdb.python/py-connection.c | 22 ++ gdb/testsuite/gdb.python/py-connection.exp | 63 +++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- 17 files changed, 717 insertions(+), 4 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp diff --git a/gdb/Makefile.in b/gdb/Makefile.in index 4201f65e68d..5ec58aba439 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -392,6 +392,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-breakpoint.c \ python/py-cmd.c \ python/py-continueevent.c \ + python/py-connection.c \ python/py-event.c \ python/py-evtregistry.c \ python/py-evts.c \ diff --git a/gdb/NEWS b/gdb/NEWS index bd26d2b1ec2..43b81df526d 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -45,6 +45,22 @@ maint show internal-warning backtrace event is triggered once GDB decides it is going to exit, but before GDB starts to clean up its internal state. + ** New gdb.TargetConnection object type that represents a connection + (as displayed by the 'info connections' command). + + ** The gdb.Inferior type now has a 'connection' property which is an + instance of gdb.TargetConnection, the connection used by this + inferior. This can be None if the inferior has no connection + (for example, when exited). + + ** New 'gdb.events.connection_removed' event registry, which emits a + 'gdb.ConnectionEvent' when a connection is removed from GDB. + This event has a 'connection' property, a gdb.TargetConnection + object for the connection being removed. + + ** New gdb.connections() function that returns a list of all + currently active connections. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 15bf9dc3e21..f81707eb002 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -220,6 +220,7 @@ * Lazy Strings In Python:: Python representation of lazy strings. * Architectures In Python:: Python representation of architectures. * Registers In Python:: Python representation of registers. +* Connections In Python:: Python representation of connections. * TUI Windows In Python:: Implementing new TUI windows. @end menu @@ -557,6 +558,13 @@ related prompts are prohibited from being changed. @end defun +@anchor{gdbpy_connections} +@defun gdb.connections +Return a list of @code{gdb.TargetConnection} objects, one for each +currently active connection (@pxref{Connections In Python}). The +connection objects are in no particular order in the returned list. +@end defun + @node Exception Handling @subsubsection Exception Handling @cindex python exceptions @@ -3084,10 +3092,18 @@ ID of inferior, as assigned by GDB. @end defvar +@anchor{gdbpy_inferior_connection} +@defvar Inferior.connection +The @code{gdb.TargetConnection} for this inferior (@pxref{Connections +In Python}), or @code{None} if this inferior has no connection. +@end defvar + @defvar Inferior.connection_num ID of inferior's connection as assigned by @value{GDBN}, or None if -the inferior is not connected to a target. -@xref{Inferiors Connections and Programs}. +the inferior is not connected to a target. @xref{Inferiors Connections +and Programs}. This is equivalent to +@code{gdb.Inferior.connection.num} in the case where +@code{gdb.Inferior.connection} is not @code{None}. @end defvar @defvar Inferior.pid @@ -3428,6 +3444,15 @@ An integer, the value of the exit code @value{GDBN} will return. @end defvar +@item events.connection_removed +This is emitted when @value{GDBN} removes a connection +(@pxref{Connections In Python}). The event is of type +@code{gdb.ConnectionEvent}. This has a single read-only attribute: + +@defvar ConnectionEvent.connection +The @code{gdb.TargetConnection} that is being removed. +@end defvar + @end table @node Threads In Python @@ -5946,6 +5971,69 @@ A string that is the name of this register group. @end defvar +@node Connections In Python +@subsubsection Connections In Python +@cindex connections in python +@value{GDBN} lets you run and debug multiple programs in a single +session. Each program being debugged has a connection, the connection +describes how @value{GDBN} controls the program being debugged. +Examples of different connection types are @samp{native} and +@samp{remote}. @xref{Inferiors Connections and Programs}. + +@value{GDBN} uses the @code{gdb.TargetConnection} object type to +represent a connection in Python code. To get a list of all +connections use @code{gdb.connections} +(@pxref{gdbpy_connections,,gdb.connections}). + +To get the connection for a single @code{gdb.Inferior} read its +@code{gdb.Inferior.connection} attribute +(@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). + +A @code{gdb.TargetConnection} has the following method: + +@defun TargetConnection.is_valid () +Return @code{True} if the @code{gdb.TargetConnection} object is valid, +@code{False} if not. A @code{gdb.TargetConnection} will become +invalid if the connection no longer exists within @value{GDBN}, this +will happen when all the inferiors using that connection exit. + +Reading any of the @code{gdb.TargetConnection} properties will throw +an exception if the connection is invalid. +@end defun + +A @code{gdb.TargetConnection} has the following read-only properties: + +@defvar TargetConnection.num +An integer assigned by @value{GDBN} to uniquely identify this +connection. This is the same value as displayed in the @samp{Num} +column of the @code{info connections} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.type +A string that describes what type of connection this is. This string +will be one of the valid names that can be passed to the @code{target} +command (@pxref{Target Commands,,target command}). +@end defvar + +@defvar TargetConnection.description +A string that gives a short description of this target type. This is +the same string that is displayed in the @samp{Description} column of +the @code{info connection} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.details +An optional string that gives additional information about this +connection. This attribute can be @code{None} if there are no +additional details for this connection. + +An example of a connection type that might have additional details is +the @samp{remote} connection, in this case the details string can +contain the @samp{@var{hostname}:@var{port}} that was used to connect +to the remote target. +@end defvar + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/observable.c b/gdb/observable.c index b020076cf26..6e91d7bbc4a 100644 --- a/gdb/observable.c +++ b/gdb/observable.c @@ -78,6 +78,7 @@ DEFINE_OBSERVABLE (user_selected_context_changed); DEFINE_OBSERVABLE (source_styling_changed); DEFINE_OBSERVABLE (current_source_symtab_and_line_changed); DEFINE_OBSERVABLE (gdb_exiting); +DEFINE_OBSERVABLE (connection_removed); } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/observable.h b/gdb/observable.h index f20f532870f..707f6c91c12 100644 --- a/gdb/observable.h +++ b/gdb/observable.h @@ -253,6 +253,9 @@ extern observable<> current_source_symtab_and_line_changed; /* Called when GDB is about to exit. */ extern observable<int> gdb_exiting; +/* When a connection is removed. */ +extern observable<process_stratum_target */* target */> connection_removed; + } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def index 83f10989e4a..ed22a3335cd 100644 --- a/gdb/python/py-all-events.def +++ b/gdb/python/py-all-events.def @@ -39,3 +39,4 @@ GDB_PY_DEFINE_EVENT(breakpoint_deleted) GDB_PY_DEFINE_EVENT(breakpoint_modified) GDB_PY_DEFINE_EVENT(before_prompt) GDB_PY_DEFINE_EVENT(gdb_exiting) +GDB_PY_DEFINE_EVENT(connection_removed) diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c new file mode 100644 index 00000000000..f75a8735148 --- /dev/null +++ b/gdb/python/py-connection.c @@ -0,0 +1,365 @@ +/* Python interface to inferiors. + + Copyright (C) 2009-2021 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/>. */ + +#include "defs.h" +#include "python-internal.h" +#include "process-stratum-target.h" +#include "inferior.h" +#include "observable.h" +#include "target-connection.h" +#include "py-events.h" +#include "py-event.h" +#include "arch-utils.h" + +#include <map> + +/* The Python object that represents a connection. */ + +struct connection_object +{ + PyObject_HEAD + + /* The process target that represents this connection. When a + connection_object is created this field will always point at a valid + target. Later, if GDB stops using this target (the target is popped + from all target stacks) then this field is set to nullptr, which + indicates that this Python object is now in the invalid state (see + the is_valid() method below). */ + struct process_stratum_target *target; +}; + +extern PyTypeObject connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); + +/* Require that CONNECTION be valid. */ +#define CONNPY_REQUIRE_VALID(connection) \ + do { \ + if (connection->target == nullptr) \ + { \ + PyErr_SetString (PyExc_RuntimeError, \ + _("Connection no longer exists.")); \ + return nullptr; \ + } \ + } while (0) + +/* A map between process_stratum targets and the Python object representing + them. We actually hold a gdbpy_ref around the Python object so that + reference counts are handled correctly when entries are deleted. */ +static std::map<process_stratum_target *, + gdbpy_ref<connection_object>> all_connection_objects; + +/* Return a reference to a gdb.TargetConnection object for TARGET. If + TARGET is nullptr then a reference to None is returned. + + Previously created gdb.TargetConnection objects are cached, and + additional references to the same connection object can be returned with + later calls to this function. */ + +gdbpy_ref<> +target_to_connection_object (process_stratum_target *target) +{ + if (target == nullptr) + return gdbpy_ref<>::new_reference (Py_None); + + gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; + if (conn_obj == nullptr) + { + conn_obj.reset (PyObject_New (connection_object, + &connection_object_type)); + if (conn_obj == nullptr) + return nullptr; + + conn_obj->target = target; + + /* PyObject_New initializes the new object with a refcount of 1. This + counts for the reference we are keeping in the target_ops data. */ + all_connection_objects[target] = conn_obj; + } + + gdb_assert (conn_obj != nullptr); + + /* Repackage the result as a PyObject reference. */ + return gdbpy_ref<> ((PyObject *) conn_obj.release ()); +} + +/* Return a list of gdb.TargetConnection objects, one for each currently + active connection. The returned list is in no particular order. */ + +PyObject * +gdbpy_connections (PyObject *self, PyObject *args) +{ + gdbpy_ref<> list (PyList_New (0)); + if (list == nullptr) + return nullptr; + + for (process_stratum_target *target : all_non_exited_process_targets ()) + { + gdb_assert (target != nullptr); + + gdbpy_ref<> conn = target_to_connection_object (target); + if (conn == nullptr) + return nullptr; + gdb_assert (conn.get () != Py_None); + + if (PyList_Append (list.get (), conn.get ()) < 0) + return nullptr; + } + + return list.release (); +} + +/* Emit a connection event for TARGET to REGISTRY. Return 0 on success, or + a negative value on error. */ + +static int +emit_connection_event (process_stratum_target *target, + eventregistry_object *registry) +{ + gdbpy_ref<> event_obj + = create_event_object (&connection_event_object_type); + if (event_obj == nullptr) + return -1; + + gdbpy_ref<> conn = target_to_connection_object (target); + if (evpy_add_attribute (event_obj.get (), "connection", conn.get ()) < 0) + return -1; + + return evpy_emit_event (event_obj.get (), registry); +} + +/* Callback for the connection_removed observer. */ + +static void +connpy_connection_removed (process_stratum_target *target) +{ + if (!gdb_python_initialized) + return; + + gdbpy_enter enter_py (get_current_arch (), current_language); + + if (!evregpy_no_listeners_p (gdb_py_events.connection_removed)) + if (emit_connection_event (target, gdb_py_events.connection_removed) < 0) + gdbpy_print_stack (); + + gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; + if (conn_obj != nullptr) + { + conn_obj->target = nullptr; + all_connection_objects.erase (target); + } +} + +/* Called when a gdb.TargetConnection object is deallocated. */ + +static void +connpy_connection_dealloc (PyObject *obj) +{ + connection_object *conn_obj = (connection_object *) obj; + + /* As the all_connection_objects map holds a reference to each connection + object we can only enter the dealloc function when the reference in + all_connection_objects has been erased. + + As we always set the target pointer back to nullptr before we erase + items from all_connection_objects then, when we get here, the target + pointer must be nullptr. */ + gdb_assert (conn_obj->target == nullptr); + + Py_TYPE (obj)->tp_free (obj); +} + +/* Implement repr() for gdb.TargetConnection. */ + +static PyObject * +connpy_repr (PyObject *obj) +{ + connection_object *self = (connection_object *) obj; + process_stratum_target *target = self->target; + + if (target == nullptr) + return PyString_FromString ("<gdb.TargetConnection (invalid)>"); + + return PyString_FromFormat ("<gdb.TargetConnection num=%d, what=\"%s\">", + target->connection_number, + make_target_connection_string (target).c_str ()); +} + +/* Implementation of gdb.TargetConnection.is_valid() -> Boolean. Returns + True if this connection object is still associated with a + process_stratum_target, otherwise, returns False. */ + +static PyObject * +connpy_is_valid (PyObject *self, PyObject *args) +{ + connection_object *conn = (connection_object *) self; + + if (conn->target == nullptr) + Py_RETURN_FALSE; + + Py_RETURN_TRUE; +} + +/* Return the id number of this connection. */ + +static PyObject * +connpy_get_connection_num (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + auto num = conn->target->connection_number; + return gdb_py_object_from_longest (num).release (); +} + +/* Return a string that gives the short name for this connection type. */ + +static PyObject * +connpy_get_connection_type (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *shortname = conn->target->shortname (); + return host_string_to_python_string (shortname).release (); +} + +/* Return a string that gives a longer description of this connection type. */ + +static PyObject * +connpy_get_description (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *longname = conn->target->longname (); + return host_string_to_python_string (longname).release (); +} + +/* Return a string that gives additional details about this connection, or + None, if there are no additional details for this connection type. */ + +static PyObject * +connpy_get_connection_details (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *details = conn->target->connection_string (); + if (details != nullptr) + return host_string_to_python_string (details).release (); + else + Py_RETURN_NONE; +} + +/* Python specific initialization for this file. */ + +int +gdbpy_initialize_connection (void) +{ + if (PyType_Ready (&connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "TargetConnection", + (PyObject *) &connection_object_type) < 0) + return -1; + + return 0; +} + +/* Global initialization for this file. */ + +void _initialize_py_connection (); +void +_initialize_py_connection () +{ + gdb::observers::connection_removed.attach (connpy_connection_removed, + "py-connection"); +} + +/* Methods for the gdb.TargetConnection object type. */ + +static PyMethodDef connection_object_methods[] = +{ + { "is_valid", connpy_is_valid, METH_NOARGS, + "is_valid () -> Boolean.\n\ +Return true if this TargetConnection is valid, false if not." }, + { NULL } +}; + +/* Attributes for the gdb.TargetConnection object type. */ + +static gdb_PyGetSetDef connection_object_getset[] = +{ + { "num", connpy_get_connection_num, NULL, + "ID number of this connection, as assigned by GDB.", NULL }, + { "type", connpy_get_connection_type, NULL, + "A short string that is the name for this connection type.", NULL }, + { "description", connpy_get_description, NULL, + "A longer string describing this connection type.", NULL }, + { "details", connpy_get_connection_details, NULL, + "A string containing additional connection details.", NULL }, + { NULL } +}; + +/* Define the gdb.TargetConnection object type. */ + +PyTypeObject connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.TargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + connection_object_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def index aeaee02e8bb..6a49f2aa896 100644 --- a/gdb/python/py-event-types.def +++ b/gdb/python/py-event-types.def @@ -110,3 +110,8 @@ GDB_PY_DEFINE_EVENT_TYPE (gdb_exiting, "GdbExitingEvent", "GDB is about to exit", event_object_type); + +GDB_PY_DEFINE_EVENT_TYPE (connection, + "ConnectionEvent", + "GDB connection added or removed object", + event_object_type); diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c index c8de41dd009..143f3492928 100644 --- a/gdb/python/py-inferior.c +++ b/gdb/python/py-inferior.c @@ -427,6 +427,20 @@ infpy_get_num (PyObject *self, void *closure) return gdb_py_object_from_longest (inf->inferior->num).release (); } +/* Return the gdb.TargetConnection object for this inferior, or None if a + connection does not exist. */ + +static PyObject * +infpy_get_connection (PyObject *self, void *closure) +{ + inferior_object *inf = (inferior_object *) self; + + INFPY_REQUIRE_VALID (inf); + + process_stratum_target *target = inf->inferior->process_target (); + return target_to_connection_object (target).release (); +} + /* Return the connection number of the given inferior, or None if a connection does not exist. */ @@ -968,6 +982,8 @@ gdbpy_initialize_inferior (void) static gdb_PyGetSetDef inferior_object_getset[] = { { "num", infpy_get_num, NULL, "ID of inferior, as assigned by GDB.", NULL }, + { "connection", infpy_get_connection, NULL, + "The gdb.TargetConnection for this inferior.", NULL }, { "connection_num", infpy_get_connection_num, NULL, "ID of inferior's connection, as assigned by GDB.", NULL }, { "pid", infpy_get_pid, NULL, "PID of inferior, as assigned by the OS.", diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h index 022d4a67172..e0c8d867e0d 100644 --- a/gdb/python/python-internal.h +++ b/gdb/python/python-internal.h @@ -478,6 +478,10 @@ gdbpy_ref<thread_object> create_thread_object (struct thread_info *tp); gdbpy_ref<> thread_to_thread_object (thread_info *thr);; gdbpy_ref<inferior_object> inferior_to_inferior_object (inferior *inf); +struct process_stratum_target; +gdbpy_ref<> target_to_connection_object (process_stratum_target *target); +PyObject *gdbpy_connections (PyObject *self, PyObject *args); + const struct block *block_object_to_block (PyObject *obj); struct symbol *symbol_object_to_symbol (PyObject *obj); struct value *value_object_to_value (PyObject *self); @@ -549,6 +553,8 @@ int gdbpy_initialize_unwind (void) CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; int gdbpy_initialize_tui () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; +int gdbpy_initialize_connection () + CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; /* A wrapper for PyErr_Fetch that handles reference counting for the caller. */ diff --git a/gdb/python/python.c b/gdb/python/python.c index 264f7c88ed6..c12e1aade18 100644 --- a/gdb/python/python.c +++ b/gdb/python/python.c @@ -1876,6 +1876,7 @@ do_start_initialization () || gdbpy_initialize_registers () < 0 || gdbpy_initialize_xmethods () < 0 || gdbpy_initialize_unwind () < 0 + || gdbpy_initialize_connection () < 0 || gdbpy_initialize_tui () < 0) return false; @@ -2299,6 +2300,10 @@ Set the value of the convenience variable $NAME." }, Register a TUI window constructor." }, #endif /* TUI */ + { "connections", gdbpy_connections, METH_NOARGS, + "connections () -> List.\n\ +Return a list of gdb.TargetConnection objects." }, + {NULL, NULL, 0, NULL} }; diff --git a/gdb/target-connection.c b/gdb/target-connection.c index a649423e07e..da1c9da74dc 100644 --- a/gdb/target-connection.c +++ b/gdb/target-connection.c @@ -24,6 +24,7 @@ #include "inferior.h" #include "target.h" +#include "observable.h" /* A map between connection number and representative process_stratum target. */ @@ -49,6 +50,9 @@ connection_list_add (process_stratum_target *t) void connection_list_remove (process_stratum_target *t) { + /* Notify about the connection being removed before we reset the + connection number to zero. */ + gdb::observers::connection_removed.notify (t); process_targets.erase (t->connection_number); t->connection_number = 0; } diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp index 3fd6b15cdd9..3f74a47de31 100644 --- a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp @@ -15,18 +15,27 @@ # Test "info inferiors" and "info connections" with multiple targets. +load_lib gdb-python.exp + source $srcdir/$subdir/multi-target.exp.tcl if {![multi_target_prepare]} { return } +# Cache the result of calling skip_python_tests into a local variable. +set run_python_tests [expr ! [skip_python_tests]] + # Test "info inferiors" and "info connections". MULTI_PROCESS # indicates whether the multi-process feature of remote targets is # turned off or on. proc test_info_inferiors {multi_process} { setup "off" + if { $::run_python_tests } { + gdb_test_no_output "source ${::remote_python_file}" "load python file" + } + gdb_test_no_output \ "set remote multiprocess-feature-packet $multi_process" @@ -86,6 +95,18 @@ proc test_info_inferiors {multi_process} { "[connection_num 5 $inf]${ws}core${ws}Local core dump file${ws}" \ ] + if { $::run_python_tests } { + gdb_test "python info_connections()" \ + [multi_line \ + "Num${ws}What${ws}Description" \ + "[connection_num 1 $inf]${ws}native${ws}Native process" \ + "[connection_num 2 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 3 $inf]${ws}core${ws}Local core dump file" \ + "[connection_num 4 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 5 $inf]${ws}core${ws}Local core dump file" \ + ] + } + gdb_test "info inferiors" \ [multi_line \ "Num${ws}Description${ws}Connection${ws}Executable${ws}" \ @@ -96,10 +117,27 @@ proc test_info_inferiors {multi_process} { "[inf_desc 5 $inf]4 \\(extended-remote localhost:$decimal\\)${ws}${binfile}${ws}" \ "[inf_desc 6 $inf]5 \\(core\\)${ws}${binfile}${ws}" \ ] + + if { $::run_python_tests } { + gdb_test "python info_inferiors()" \ + [multi_line \ + "Inferior 1, Connection #1: native" \ + "Inferior 2, Connection #2: extended-remote localhost:$decimal" \ + "Inferior 3, Connection #3: core" \ + "Inferior 4, Connection #1: native" \ + "Inferior 5, Connection #4: extended-remote localhost:$decimal" \ + "Inferior 6, Connection #5: core" \ + ] + } } } } +if { $run_python_tests } { + set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +} + # Test "info inferiors" and "info connections" commands. with_test_prefix "info-inferiors" { foreach_with_prefix multi_process {"on" "off"} { diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py new file mode 100644 index 00000000000..aac561ae683 --- /dev/null +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py @@ -0,0 +1,63 @@ +# Copyright (C) 2021 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 + +# Take a gdb.TargetConnection and return the connection number. +def conn_num(c): + return c.num + +# Takes a gdb.TargetConnection and return a string that is either the +# type, or the type and details (if the details are not None). +def make_target_connection_string(c): + if c.details is None: + return c.type + else: + return "%s %s" % (c.type, c.details) + +# A Python implementation of 'info connections'. Produce output that +# is identical to the output of 'info connections' so we can check +# that aspects of gdb.TargetConnection work correctly. +def info_connections(): + all_connections = sorted(gdb.connections(), key=conn_num) + current_conn = gdb.selected_inferior().connection + what_width = 0 + for c in all_connections: + s = make_target_connection_string(c) + if len(s) > what_width: + what_width = len(s) + + fmt = " Num %%-%ds Description" % what_width + print(fmt % "What") + fmt = "%%s%%-3d %%-%ds %%s" % what_width + for c in all_connections: + if c == current_conn: + prefix = "* " + else: + prefix = " " + + print(fmt % (prefix, c.num, make_target_connection_string(c), + c.description)) + +def inf_num(i): + return i.num + +# .... +def info_inferiors(): + all_inferiors = sorted(gdb.inferiors(), key=inf_num) + for i in gdb.inferiors(): + print("Inferior %d, Connection #%d: %s" % + (i.num, i.connection_num, + make_target_connection_string(i.connection))) diff --git a/gdb/testsuite/gdb.python/py-connection.c b/gdb/testsuite/gdb.python/py-connection.c new file mode 100644 index 00000000000..4b32e704476 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp new file mode 100644 index 00000000000..b994cec01eb --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -0,0 +1,63 @@ +# Copyright (C) 2021 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/>. + +# This file is for testing the gdb.TargetConnection API. This API is +# already tested in gdb.multi/multi-target-info-inferiors.exp and +# gdb.python/py-inferior.exp, this file just covers some edge cases +# that are not tested in other places. + +load_lib gdb-python.exp + +standard_testfile + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +# Skip all tests if Python scripting is not enabled. +if { [skip_python_tests] } { continue } + +if ![runto_main] then { + fail "can't run to main" + return 0 +} + +# Create a gdb.TargetConnection object and check it becomes invalid +# once the connection has gone away. +gdb_test_no_output "python conn = gdb.selected_inferior().connection" +gdb_test "python print(conn)" \ + "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ + "print gdb.TargetConnection while it is still valid" +gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" +gdb_test "info connections" "\r\n\\* 1 .*" \ + "info connections while the connection is still around" +gdb_test "kill" "" "kill the inferior" \ + "Kill the program being debugged.*y or n. $" "y" +gdb_test "info connections" "No connections\\." \ + "info connections now all the connections have gone" +gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ + "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" + +# Now check that accessing properties of the invalid connection cases +# an error. +gdb_test "python print(conn.num)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.type)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.description)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.details)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" diff --git a/gdb/testsuite/gdb.python/py-inferior.exp b/gdb/testsuite/gdb.python/py-inferior.exp index 894b90a85fc..d5d284a763e 100644 --- a/gdb/testsuite/gdb.python/py-inferior.exp +++ b/gdb/testsuite/gdb.python/py-inferior.exp @@ -51,6 +51,8 @@ gdb_py_test_silent_cmd "python i0 = inferiors\[0\]" "get first inferior" 0 gdb_test "python print ('result = %s' % (i0 == inferiors\[0\]))" " = True" "test equality comparison (true)" gdb_test "python print ('result = %s' % i0.num)" " = \[0-9\]+" "test Inferior.num" gdb_test "python print ('result = %s' % i0.connection_num)" " = \[0-9\]+" "test Inferior.connection_num" +gdb_test "python print ('result = %s' % (i0.connection_num == i0.connection.num))" " = True" \ + "Inferior.connection_num equals Inferior.connection.num" gdb_test "python print ('result = %s' % i0.pid)" " = \[0-9\]+" "test Inferior.pid" gdb_test "python print ('result = %s' % i0.was_attached)" " = False" "test Inferior.was_attached" gdb_test "python print (i0.threads ())" "\\(<gdb.InferiorThread object at 0x\[\[:xdigit:\]\]+>,\\)" "test Inferior.threads" @@ -264,6 +266,8 @@ with_test_prefix "is_valid" { "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].connection_num)" \ "RuntimeError: Inferior no longer exists.*" + gdb_test "python print (inf_list\[1\].connection)" \ + "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].pid)" \ "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].was_attached)" \ @@ -281,7 +285,9 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 1" ".*" "switch to first inferior" gdb_test "py print (gdb.selected_inferior().num)" "1" "first inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "1" \ - "first inferior's connection" + "first inferior's connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" "1" \ + "first inferior's connection number, though connection object" # Figure out if inf 1 has a native target. set inf_1_is_native [gdb_is_target_native] @@ -289,6 +295,8 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 3" ".*" "switch to third inferior" gdb_test "py print (gdb.selected_inferior().num)" "3" "third inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "None" \ + "third inferior's None connection number" + gdb_test "py print (gdb.selected_inferior().connection)" "None" \ "third inferior's None connection" gdb_test "target native" "Done. Use the \"run\" command to start a process." \ "target for the third inferior" @@ -302,7 +310,15 @@ with_test_prefix "selected_inferior" { } gdb_test "py print (gdb.selected_inferior().connection_num)" \ "$expected_connection_num" \ - "third inferior's native connection" + "third inferior's native connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" \ + "$expected_connection_num" \ + "third inferior's native connection number, though connection object" + + # Test printing of gdb.TargetConnection object. + gdb_test "py print (gdb.selected_inferior().connection)" \ + "<gdb.TargetConnection num=${expected_connection_num}, what=\"\[^\"\]+\">" \ + "print a connection object" gdb_test "inferior 1" ".*" "switch back to first inferior" gdb_test_no_output "remove-inferiors 3" "remove second inferior" -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv2 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-10-18 9:45 ` [PATCHv2 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess @ 2021-10-18 12:44 ` Eli Zaretskii 2021-10-18 15:53 ` Simon Marchi 1 sibling, 0 replies; 52+ messages in thread From: Eli Zaretskii @ 2021-10-18 12:44 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches > From: Andrew Burgess <andrew.burgess@embecosm.com> > Cc: Eli Zaretskii <eliz@gnu.org>, > Andrew Burgess <andrew.burgess@embecosm.com> > Date: Mon, 18 Oct 2021 10:45:52 +0100 > > This commit adds a new object type gdb.TargetConnection. This new > type represents a connection within GDB (a connection as displayed by > 'info connections'). Thanks, the documentation parts are OK, with this one gotcha: > @defvar Inferior.connection_num > ID of inferior's connection as assigned by @value{GDBN}, or None if > -the inferior is not connected to a target. > -@xref{Inferiors Connections and Programs}. > +the inferior is not connected to a target. @xref{Inferiors Connections ^^ Two spaces there, please. ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv2 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-10-18 9:45 ` [PATCHv2 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-18 12:44 ` Eli Zaretskii @ 2021-10-18 15:53 ` Simon Marchi 1 sibling, 0 replies; 52+ messages in thread From: Simon Marchi @ 2021-10-18 15:53 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2021-10-18 5:45 a.m., Andrew Burgess wrote: > This commit adds a new object type gdb.TargetConnection. This new > type represents a connection within GDB (a connection as displayed by > 'info connections'). > > There's three ways to find a gdb.TargetConnection, there's a new > 'gdb.connections()' function, which returns a list of all currently > active connections. > > Or you can read the new 'connection' property on the gdb.Inferior > object type, this contains the connection for that inferior (or None > if the inferior has no connection, for example, it is exited). > > Finally, there's a new gdb.events.connection_removed event registry, > this emits a new gdb.ConnectionEvent whenever a connection is removed > from GDB (this happens when all inferiors using a connection exit). > The gdb.ConnectionEvent has a 'connection' property, which is the > gdb.TargetConnection being removed from GDB. > > The gdb.TargetConnection has a 'is_valid()' method. A connection > object becomes invalid when the underlying connection is removed from > GDB (all inferiors using the connection exit). > > The gdb.TargetConnection has the following read-only properties: > > 'num': The number for this connection, > > 'type': e.g. 'native', 'remove', 'sim', etc > > 'description': The longer description as seen in the 'info > connections' command output. > > 'details': A string or None. Extra details for the connection, for > example, a remote connection's details might be > 'hostname:port'. I don't know if I'll have time to read the code, so I at least wanted to say that just looking at the commit message, I think that looks good. As mentioned in my reply to 3/3, I think that we should have subclasses for different target kinds, if we want to provide extra information for some targets. For example, to provide hostname / port for a target connection. But I am not against having also the `details` property that just generates a useful human-readable string (or that could be the __str__ for these objects). Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv2 2/3] gdb: make packet_command function available outside remote.c 2021-10-18 9:45 ` [PATCHv2 0/3] Python API for target connections, and packet sending Andrew Burgess 2021-10-18 9:45 ` [PATCHv2 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess @ 2021-10-18 9:45 ` Andrew Burgess 2021-10-18 9:45 ` [PATCHv2 3/3] gdb/python: add TargetConnection.send_remote_packet method Andrew Burgess 2021-10-19 10:17 ` [PATCHv3 0/3] Python API for target connections, and packet sending Andrew Burgess 3 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-18 9:45 UTC (permalink / raw) To: gdb-patches In a later commit I will add a Python API to access the 'maint packet' functionality, that is, sending a user specified packet to the target. To make implementing this easier, this commit refactors how this command is currently implemented so that the packet_command function is now global. The new global send_remote_packet function takes an object that is an implementation of an abstract interface. Two functions within this interface are then called, one just before a packet is sent to the remote target, and one when the reply has been received from the remote target. Using an interface object in this way allows (1) for the error checking to be done before the first callback is made, this means we only print out what packet it being sent once we know we are going to actually send it, and (2) we don't need to make a copy of the reply if all we want to do is print it. The only user visible changes after this commit are the error messages, which I've changed to be less 'maint packet' command focused, this will make them (I hope) better for when send_remote_packet can be called from Python code. So: "command can only be used with remote target" Becomes: "packets can only be sent to a remote target" And: "remote-packet command requires packet text as argument" Becomes: "a remote packet must not be empty" --- gdb/remote.c | 67 +++++++++++++++++++++++++++++++++++----------------- gdb/remote.h | 34 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/gdb/remote.c b/gdb/remote.c index d5eb40ce578..237b1b21c41 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -956,8 +956,6 @@ class remote_target : public process_stratum_target bool vcont_r_supported (); - void packet_command (const char *args, int from_tty); - private: /* data fields */ /* The remote state. Don't reference this directly. Use the @@ -11615,34 +11613,59 @@ remote_target::memory_map () return result; } -static void -packet_command (const char *args, int from_tty) +/* Set of callbacks used to implement the 'maint packet' command. */ + +struct cli_packet_command_callbacks : public send_remote_packet_callbacks { - remote_target *remote = get_current_remote_target (); + /* Called before the packet is sent. PACKET_STR is the packet content + before the protocol specific prefix, suffix, and escaping is added. */ - if (remote == nullptr) - error (_("command can only be used with remote target")); + void sending (const char *packet_str) override + { + puts_filtered ("sending: "); + print_packet (packet_str); + puts_filtered ("\n"); + } - remote->packet_command (args, from_tty); -} + /* Called with BUF, the reply from the remote target. */ + + void received (const gdb::char_vector &buf) override + { + puts_filtered ("received: "); + print_packet (buf.data ()); + puts_filtered ("\n"); + } +}; + +/* See remote.h. */ void -remote_target::packet_command (const char *args, int from_tty) +send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks) { - if (!args) - error (_("remote-packet command requires packet text as argument")); + if (packet_str == nullptr || *packet_str == '\0') + error (_("a remote packet must not be empty")); - puts_filtered ("sending: "); - print_packet (args); - puts_filtered ("\n"); - putpkt (args); + remote_target *remote = get_current_remote_target (); + if (remote == nullptr) + error (_("packets can only be sent to a remote target")); - remote_state *rs = get_remote_state (); + callbacks->sending (packet_str); - getpkt (&rs->buf, 0); - puts_filtered ("received: "); - print_packet (rs->buf.data ()); - puts_filtered ("\n"); + remote->putpkt (packet_str); + remote_state *rs = remote->get_remote_state (); + remote->getpkt (&rs->buf, 0); + + callbacks->received (rs->buf); +} + +/* Entry point for the 'maint packet' command. */ + +static void +cli_packet_command (const char *args, int from_tty) +{ + cli_packet_command_callbacks cb; + send_remote_packet (args, &cb); } #if 0 @@ -14921,7 +14944,7 @@ Argument is a single section name (default: all loaded sections).\n\ To compare only read-only loaded sections, specify the -r option."), &cmdlist); - add_cmd ("packet", class_maintenance, packet_command, _("\ + add_cmd ("packet", class_maintenance, cli_packet_command, _("\ Send an arbitrary packet to a remote target.\n\ maintenance packet TEXT\n\ If GDB is talking to an inferior via the GDB serial protocol, then\n\ diff --git a/gdb/remote.h b/gdb/remote.h index 46bfa01fc79..cd91be8decb 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -78,4 +78,38 @@ extern int remote_register_number_and_offset (struct gdbarch *gdbarch, extern void remote_notif_get_pending_events (remote_target *remote, struct notif_client *np); extern bool remote_target_is_non_stop_p (remote_target *t); + +/* An abstract class that represents the set of callbacks that are made + from the send_remote_packet function (declared below). */ + +struct send_remote_packet_callbacks +{ + /* The SENDING callback is called once send_remote_packet has performed + its error checking and setup, just before the packet is sent to the + remote target. PACKET_STR is the content of the packet that will be + sent (before any of the protocol specific prefix, suffix, or escaping + is applied). */ + + virtual void sending (const char *packet_str) = 0; + + /* The RECEIVED callback is called once a reply has been received from + the remote target. The content of the reply is in BUF which can't be + modified, and which is not guaranteed to remain valid after the + RECEIVED call has returned. If you need to preserve the contents of + BUF then a copy should be taken. */ + + virtual void received (const gdb::char_vector &buf) = 0; +}; + +/* Send PACKET_STR the current remote target. If PACKET_STR is nullptr, or + is the empty string, then an error is thrown. If the current target is + not a remote target then an error is thrown. + + Calls CALLBACKS->sending() just before the packet is sent to the remote + target, and calls CALLBACKS->received() with the reply once this is + received from the remote target. */ + +extern void send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks); + #endif -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv2 3/3] gdb/python: add TargetConnection.send_remote_packet method 2021-10-18 9:45 ` [PATCHv2 0/3] Python API for target connections, and packet sending Andrew Burgess 2021-10-18 9:45 ` [PATCHv2 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-18 9:45 ` [PATCHv2 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess @ 2021-10-18 9:45 ` Andrew Burgess 2021-10-18 12:57 ` Eli Zaretskii 2021-10-18 15:46 ` Simon Marchi 2021-10-19 10:17 ` [PATCHv3 0/3] Python API for target connections, and packet sending Andrew Burgess 3 siblings, 2 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-18 9:45 UTC (permalink / raw) To: gdb-patches This commit adds a new method to the gdb.TargetConnection object type: 'send_remote_packet'. This new method is equivalent to the 'maint packet' CLI command, it allows a custom packet to be sent to a remote target. Not all gdb.TargetConnection instances will support send_remote_packet, e.g. the 'native' target doesn't. If the send_remote_packet method is invoked on a TargetConnection that is not 'remote' or 'extended-remote' then an exception is thrown. The result of calling TargetConnection.send_remote_packet is a string containing the reply that came from the remote. --- gdb/NEWS | 6 ++ gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 22 ++++- gdb/python/py-connection.c | 90 +++++++++++++++++++++ gdb/testsuite/gdb.python/py-send-packet.c | 22 +++++ gdb/testsuite/gdb.python/py-send-packet.exp | 55 +++++++++++++ gdb/testsuite/gdb.python/py-send-packet.py | 82 +++++++++++++++++++ 7 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py diff --git a/gdb/NEWS b/gdb/NEWS index 43b81df526d..a567098d13a 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -61,6 +61,12 @@ maint show internal-warning backtrace ** New gdb.connections() function that returns a list of all currently active connections. + ** New gdb.TargetConnection.send_remote_packet(STRING) method. This + is equivalent to the existing 'maint packet' CLI command; it + allows a user specified packet to be sent to the remote target. + If this method is called on a TargetConnection that is not of + type 'remote' or 'extended-remote' then an exception is thrown. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index 631a7c03b31..89cf86210ab 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -39269,6 +39269,7 @@ error stream. This is @samp{on} by default for @code{internal-error} and @samp{off} by default for @code{internal-warning}. +@anchor{maint packet} @kindex maint packet @item maint packet @var{text} If @value{GDBN} is talking to an inferior via the serial protocol, diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index f81707eb002..6e8ab3a5a5c 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -5989,7 +5989,7 @@ @code{gdb.Inferior.connection} attribute (@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). -A @code{gdb.TargetConnection} has the following method: +A @code{gdb.TargetConnection} has the following methods: @defun TargetConnection.is_valid () Return @code{True} if the @code{gdb.TargetConnection} object is valid, @@ -6001,6 +6001,26 @@ an exception if the connection is invalid. @end defun +@kindex maint packet +@defun TargetConnection.send_remote_packet (@var{packet}) +When the connection object is using @value{GDBN}'s remote serial +protocol (@pxref{Remote Protocol}), i.e.@: the +@code{TargetConnection.type} is @samp{remote} or +@samp{extended-remote}, then this method sends @var{packet}, which +should be a non-empty string, to the remote target and returns the +response packet as a string. For any other connection type, calling +this method will throw a @code{gdb.error} exception. If @var{packet} +is not a string, or is the empty string, then an exception of type +@code{gdb.error} is thrown. + +The prefix, suffix, and checksum (as required by the remote serial +protocol) are automatically added to the outgoing packet, and removed +from the incoming packet before the reply is returned. + +This is equivalent to the @code{maintenance packet} command +(@pxref{maint packet}). +@end defun + A @code{gdb.TargetConnection} has the following read-only properties: @defvar TargetConnection.num diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c index f75a8735148..43797d48a94 100644 --- a/gdb/python/py-connection.c +++ b/gdb/python/py-connection.c @@ -26,6 +26,8 @@ #include "py-events.h" #include "py-event.h" #include "arch-utils.h" +#include "remote.h" +#include "charset.h" #include <map> @@ -286,6 +288,90 @@ gdbpy_initialize_connection (void) return 0; } +/* Set of callbacks used to implement gdb.send_remote_packet. */ + +struct py_send_packet_callbacks : public send_remote_packet_callbacks +{ + /* Constructor, initialise the result to None. */ + + py_send_packet_callbacks () + : m_result (Py_None) + { /* Nothing. */ } + + /* There's nothing to do when the packet is sent. */ + + void sending (const char *args) override + { /* Nothing. */ } + + /* When the result is returned create a Python string and assign this + into the result member variable. */ + + void received (const gdb::char_vector &buf) override + { + /* Return None when we don't get back a useful result. */ + if (buf.data ()[0] != '\0') + m_result = gdbpy_ref<> (PyUnicode_Decode (buf.data (), + strlen (buf.data ()), + host_charset (), nullptr)); + } + + /* Get a reference to the result as a Python object. */ + + gdbpy_ref<> result () const + { + return m_result; + } + +private: + + /* A reference to a valid result value. This is initialized in the + constructor, and so will always point to a valid value, even if this + is just None. */ + + gdbpy_ref<> m_result; +}; + +/* Implement TargetConnection.send_remote_packet function. Send a packet + to the target identified by SELF. The connection must still be valid, + and must be a remote or extended-remote connection otherwise an + exception will be thrown. */ + +static PyObject * +connpy_send_remote_packet (PyObject *self, PyObject *args, PyObject *kw) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + static const char *keywords[] = {"packet", nullptr}; + const char *packet_str = nullptr; + + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s", keywords, + &packet_str)) + return nullptr; + + if (packet_str == nullptr || *packet_str == '\0') + { + PyErr_SetString (PyExc_ValueError, _("Invalid remote packet")); + return nullptr; + } + + try + { + scoped_restore_current_thread restore_thread; + switch_to_target_no_thread (conn->target); + + py_send_packet_callbacks callbacks; + send_remote_packet (packet_str, &callbacks); + return callbacks.result ().release (); + } + catch (const gdb_exception &except) + { + gdbpy_convert_exception (except); + return nullptr; + } +} + /* Global initialization for this file. */ void _initialize_py_connection (); @@ -303,6 +389,10 @@ static PyMethodDef connection_object_methods[] = { "is_valid", connpy_is_valid, METH_NOARGS, "is_valid () -> Boolean.\n\ Return true if this TargetConnection is valid, false if not." }, + { "send_remote_packet", (PyCFunction) connpy_send_remote_packet, + METH_VARARGS | METH_KEYWORDS, + "send_remote_packet (PACKET) -> String\n\ +Send PACKET to a remote target, return the reply as a string." }, { NULL } }; diff --git a/gdb/testsuite/gdb.python/py-send-packet.c b/gdb/testsuite/gdb.python/py-send-packet.c new file mode 100644 index 00000000000..9811b15f06d --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main () +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp new file mode 100644 index 00000000000..81001343f0c --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.exp @@ -0,0 +1,55 @@ +# Copyright (C) 2021 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 the gdb.TargetConnection.send_remote_packet API. This is done +# by connecting to a remote target and fetching the thread list in two +# ways, first, we manually send the packets required to read the +# thread list using gdb.TargetConnection.send_remote_packet, then we +# compare the results to the thread list using the standard API calls. + +load_lib gdb-python.exp +load_lib gdbserver-support.exp + +standard_testfile + +if {[skip_gdbserver_tests]} { + return 0 +} + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +gdb_exit +gdb_start + +if { [skip_python_tests] } { + return 0 +} + +# Make sure we're disconnected, in case we're testing with an +# extended-remote board, therefore already connected. +gdb_test "disconnect" ".*" + +gdbserver_run "" + +# Source the python script. +set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +gdb_test "source $remote_python_file" "Sourcing complete\\." \ + "source ${testfile}.py script" + +# The test is actually written in the Python script. Run it now. +gdb_test "python run_send_packet_test()" "Send packet test passed" diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py new file mode 100644 index 00000000000..f4dc5734057 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.py @@ -0,0 +1,82 @@ +# Copyright (C) 2021 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 xml.etree.ElementTree as ET +import gdb + +# Make use of gdb.TargetConnection.send_remote_packet to fetch the +# thread list from the remote target. +# +# Sending existing serial protocol packets like this is not a good +# idea, there should be better ways to get this information using an +# official API, this is just being used as a test case. +# +# Really, the send_remote_packet API would be used to send target +# specific packets to the target, but these are, by definition, target +# specific, so hard to test in a general testsuite. +def get_thread_list_str(): + start_pos = 0 + thread_desc = "" + try: + while True: + conn = gdb.selected_inferior().connection + str = conn.send_remote_packet("qXfer:threads:read::%d,200" % start_pos) + start_pos += 200 + c = str[0] + str = str[1:] + thread_desc += str + if c == "l": + break + return thread_desc + except: + return None + + +# Use gdb.TargetConnection.send_remote_packet to manually fetch the +# thread list, then extract the thread list using the gdb.Inferior and +# gdb.InferiorThread API. Compare the two results to ensure we +# managed to successfully read the thread list from the remote. +def run_send_packet_test(): + # Find the IDs of all current threads. + all_threads = {} + for inf in gdb.inferiors(): + for thr in inf.threads(): + id = "p%x.%x" % (thr.ptid[0], thr.ptid[1]) + all_threads[id] = False + + # Now fetch the thread list from the remote, and parse the XML. + str = get_thread_list_str() + threads_xml = ET.fromstring(str) + + # Look over all threads in the XML list and check we expected to + # find them, mark the ones we do find. + for thr in threads_xml: + id = thr.get("id") + if not id in all_threads: + raise "found unexpected thread in remote thread list" + else: + all_threads[id] = True + + # Check that all the threads were found in the XML list. + for id in all_threads: + if not all_threads[id]: + raise "thread missingt from remote thread list" + + # Test complete. + print("Send packet test passed") + + +# Just to indicate the file was sourced correctly. +print("Sourcing complete.") -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv2 3/3] gdb/python: add TargetConnection.send_remote_packet method 2021-10-18 9:45 ` [PATCHv2 3/3] gdb/python: add TargetConnection.send_remote_packet method Andrew Burgess @ 2021-10-18 12:57 ` Eli Zaretskii 2021-10-18 15:46 ` Simon Marchi 1 sibling, 0 replies; 52+ messages in thread From: Eli Zaretskii @ 2021-10-18 12:57 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches, andrew.burgess > From: Andrew Burgess <andrew.burgess@embecosm.com> > Cc: Eli Zaretskii <eliz@gnu.org>, > Andrew Burgess <andrew.burgess@embecosm.com> > Date: Mon, 18 Oct 2021 10:45:54 +0100 > > This commit adds a new method to the gdb.TargetConnection object type: > 'send_remote_packet'. This new method is equivalent to the 'maint > packet' CLI command, it allows a custom packet to be sent to a remote > target. Thanks, the documentation parts are OK. ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv2 3/3] gdb/python: add TargetConnection.send_remote_packet method 2021-10-18 9:45 ` [PATCHv2 3/3] gdb/python: add TargetConnection.send_remote_packet method Andrew Burgess 2021-10-18 12:57 ` Eli Zaretskii @ 2021-10-18 15:46 ` Simon Marchi 1 sibling, 0 replies; 52+ messages in thread From: Simon Marchi @ 2021-10-18 15:46 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2021-10-18 5:45 a.m., Andrew Burgess wrote: > This commit adds a new method to the gdb.TargetConnection object type: > 'send_remote_packet'. This new method is equivalent to the 'maint > packet' CLI command, it allows a custom packet to be sent to a remote > target. > > Not all gdb.TargetConnection instances will support > send_remote_packet, e.g. the 'native' target doesn't. If the > send_remote_packet method is invoked on a TargetConnection that is not > 'remote' or 'extended-remote' then an exception is thrown. > > The result of calling TargetConnection.send_remote_packet is a string > containing the reply that came from the remote. Another way of doing this (that I think fits well with Python) would be to have the send_remote_packet method only on RemoteTargetConnection objects. The RemoteTargetConnection class would inherit from TargetConnection. Users could check the type of the connection with: if isinstance(conn, gdb.RemoteTargetConnection): conn.send_remote_packet(...) If they try to call send_remote_packet on a target connection that isn't a RemoteTargetConnection, they just get a "'TargetConnection' object has no attribute 'send_remote_packet'" error. Doing it like this would allow us to add more target-specific information to RemoteTargetConnection, like the protocol used, address, port, without polluting the common interface. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv3 0/3] Python API for target connections, and packet sending 2021-10-18 9:45 ` [PATCHv2 0/3] Python API for target connections, and packet sending Andrew Burgess ` (2 preceding siblings ...) 2021-10-18 9:45 ` [PATCHv2 3/3] gdb/python: add TargetConnection.send_remote_packet method Andrew Burgess @ 2021-10-19 10:17 ` Andrew Burgess 2021-10-19 10:17 ` [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess ` (3 more replies) 3 siblings, 4 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-19 10:17 UTC (permalink / raw) To: gdb-patches Since v2: - Added a gdb.RemoteTargetConnection sub-class, which is used for 'remote' and 'extended-remote' connections. The previous 'send_remote_packet' has become 'send_packet' on this new sub-class. - Tests and documentation have been updated to reflect the above change. - In the first patch, connpy_repr has been modified in order to prepare it for the sub-class that will appear in the later patch. Since v1: - Rebased onto current upstream master and resolved the merge conflicts. - Addressed the documentation feedback from Eli. I'm not 100% sure that I have correctly addressed the concerns about patch #3, so this is probably worth rechecking. --- Andrew Burgess (3): gdb/python: introduce gdb.TargetConnection object type gdb: make packet_command function available outside remote.c gdb/python: add gdb.RemoteTargetConnection.send_packet gdb/Makefile.in | 1 + gdb/NEWS | 22 + gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 114 +++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 523 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/remote.c | 67 ++- gdb/remote.h | 34 ++ gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 +++ gdb/testsuite/gdb.python/py-connection.c | 22 + gdb/testsuite/gdb.python/py-connection.exp | 63 +++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- gdb/testsuite/gdb.python/py-send-packet.c | 22 + gdb/testsuite/gdb.python/py-send-packet.exp | 59 ++ gdb/testsuite/gdb.python/py-send-packet.py | 84 +++ 23 files changed, 1148 insertions(+), 26 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-10-19 10:17 ` [PATCHv3 0/3] Python API for target connections, and packet sending Andrew Burgess @ 2021-10-19 10:17 ` Andrew Burgess 2021-10-19 12:26 ` Eli Zaretskii ` (2 more replies) 2021-10-19 10:17 ` [PATCHv3 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess ` (2 subsequent siblings) 3 siblings, 3 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-19 10:17 UTC (permalink / raw) To: gdb-patches This commit adds a new object type gdb.TargetConnection. This new type represents a connection within GDB (a connection as displayed by 'info connections'). There's three ways to find a gdb.TargetConnection, there's a new 'gdb.connections()' function, which returns a list of all currently active connections. Or you can read the new 'connection' property on the gdb.Inferior object type, this contains the connection for that inferior (or None if the inferior has no connection, for example, it is exited). Finally, there's a new gdb.events.connection_removed event registry, this emits a new gdb.ConnectionEvent whenever a connection is removed from GDB (this happens when all inferiors using a connection exit). The gdb.ConnectionEvent has a 'connection' property, which is the gdb.TargetConnection being removed from GDB. The gdb.TargetConnection has a 'is_valid()' method. A connection object becomes invalid when the underlying connection is removed from GDB (all inferiors using the connection exit). The gdb.TargetConnection has the following read-only properties: 'num': The number for this connection, 'type': e.g. 'native', 'remove', 'sim', etc 'description': The longer description as seen in the 'info connections' command output. 'details': A string or None. Extra details for the connection, for example, a remote connection's details might be 'hostname:port'. --- gdb/Makefile.in | 1 + gdb/NEWS | 16 + gdb/doc/python.texi | 92 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 366 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 +++ gdb/testsuite/gdb.python/py-connection.c | 22 ++ gdb/testsuite/gdb.python/py-connection.exp | 63 +++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- 17 files changed, 718 insertions(+), 4 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp diff --git a/gdb/Makefile.in b/gdb/Makefile.in index 4201f65e68d..5ec58aba439 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -392,6 +392,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-breakpoint.c \ python/py-cmd.c \ python/py-continueevent.c \ + python/py-connection.c \ python/py-event.c \ python/py-evtregistry.c \ python/py-evts.c \ diff --git a/gdb/NEWS b/gdb/NEWS index bd26d2b1ec2..43b81df526d 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -45,6 +45,22 @@ maint show internal-warning backtrace event is triggered once GDB decides it is going to exit, but before GDB starts to clean up its internal state. + ** New gdb.TargetConnection object type that represents a connection + (as displayed by the 'info connections' command). + + ** The gdb.Inferior type now has a 'connection' property which is an + instance of gdb.TargetConnection, the connection used by this + inferior. This can be None if the inferior has no connection + (for example, when exited). + + ** New 'gdb.events.connection_removed' event registry, which emits a + 'gdb.ConnectionEvent' when a connection is removed from GDB. + This event has a 'connection' property, a gdb.TargetConnection + object for the connection being removed. + + ** New gdb.connections() function that returns a list of all + currently active connections. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 15bf9dc3e21..599c411b11d 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -220,6 +220,7 @@ * Lazy Strings In Python:: Python representation of lazy strings. * Architectures In Python:: Python representation of architectures. * Registers In Python:: Python representation of registers. +* Connections In Python:: Python representation of connections. * TUI Windows In Python:: Implementing new TUI windows. @end menu @@ -557,6 +558,13 @@ related prompts are prohibited from being changed. @end defun +@anchor{gdbpy_connections} +@defun gdb.connections +Return a list of @code{gdb.TargetConnection} objects, one for each +currently active connection (@pxref{Connections In Python}). The +connection objects are in no particular order in the returned list. +@end defun + @node Exception Handling @subsubsection Exception Handling @cindex python exceptions @@ -3084,10 +3092,18 @@ ID of inferior, as assigned by GDB. @end defvar +@anchor{gdbpy_inferior_connection} +@defvar Inferior.connection +The @code{gdb.TargetConnection} for this inferior (@pxref{Connections +In Python}), or @code{None} if this inferior has no connection. +@end defvar + @defvar Inferior.connection_num ID of inferior's connection as assigned by @value{GDBN}, or None if -the inferior is not connected to a target. -@xref{Inferiors Connections and Programs}. +the inferior is not connected to a target. @xref{Inferiors Connections +and Programs}. This is equivalent to +@code{gdb.Inferior.connection.num} in the case where +@code{gdb.Inferior.connection} is not @code{None}. @end defvar @defvar Inferior.pid @@ -3428,6 +3444,15 @@ An integer, the value of the exit code @value{GDBN} will return. @end defvar +@item events.connection_removed +This is emitted when @value{GDBN} removes a connection +(@pxref{Connections In Python}). The event is of type +@code{gdb.ConnectionEvent}. This has a single read-only attribute: + +@defvar ConnectionEvent.connection +The @code{gdb.TargetConnection} that is being removed. +@end defvar + @end table @node Threads In Python @@ -5946,6 +5971,69 @@ A string that is the name of this register group. @end defvar +@node Connections In Python +@subsubsection Connections In Python +@cindex connections in python +@value{GDBN} lets you run and debug multiple programs in a single +session. Each program being debugged has a connection, the connection +describes how @value{GDBN} controls the program being debugged. +Examples of different connection types are @samp{native} and +@samp{remote}. @xref{Inferiors Connections and Programs}. + +@value{GDBN} uses the @code{gdb.TargetConnection} object type to +represent a connection in Python code. To get a list of all +connections use @code{gdb.connections} +(@pxref{gdbpy_connections,,gdb.connections}). + +To get the connection for a single @code{gdb.Inferior} read its +@code{gdb.Inferior.connection} attribute +(@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). + +A @code{gdb.TargetConnection} has the following method: + +@defun TargetConnection.is_valid () +Return @code{True} if the @code{gdb.TargetConnection} object is valid, +@code{False} if not. A @code{gdb.TargetConnection} will become +invalid if the connection no longer exists within @value{GDBN}, this +will happen when all the inferiors using that connection exit. + +Reading any of the @code{gdb.TargetConnection} properties will throw +an exception if the connection is invalid. +@end defun + +A @code{gdb.TargetConnection} has the following read-only properties: + +@defvar TargetConnection.num +An integer assigned by @value{GDBN} to uniquely identify this +connection. This is the same value as displayed in the @samp{Num} +column of the @code{info connections} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.type +A string that describes what type of connection this is. This string +will be one of the valid names that can be passed to the @code{target} +command (@pxref{Target Commands,,target command}). +@end defvar + +@defvar TargetConnection.description +A string that gives a short description of this target type. This is +the same string that is displayed in the @samp{Description} column of +the @code{info connection} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.details +An optional string that gives additional information about this +connection. This attribute can be @code{None} if there are no +additional details for this connection. + +An example of a connection type that might have additional details is +the @samp{remote} connection, in this case the details string can +contain the @samp{@var{hostname}:@var{port}} that was used to connect +to the remote target. +@end defvar + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/observable.c b/gdb/observable.c index b020076cf26..6e91d7bbc4a 100644 --- a/gdb/observable.c +++ b/gdb/observable.c @@ -78,6 +78,7 @@ DEFINE_OBSERVABLE (user_selected_context_changed); DEFINE_OBSERVABLE (source_styling_changed); DEFINE_OBSERVABLE (current_source_symtab_and_line_changed); DEFINE_OBSERVABLE (gdb_exiting); +DEFINE_OBSERVABLE (connection_removed); } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/observable.h b/gdb/observable.h index f20f532870f..707f6c91c12 100644 --- a/gdb/observable.h +++ b/gdb/observable.h @@ -253,6 +253,9 @@ extern observable<> current_source_symtab_and_line_changed; /* Called when GDB is about to exit. */ extern observable<int> gdb_exiting; +/* When a connection is removed. */ +extern observable<process_stratum_target */* target */> connection_removed; + } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def index 83f10989e4a..ed22a3335cd 100644 --- a/gdb/python/py-all-events.def +++ b/gdb/python/py-all-events.def @@ -39,3 +39,4 @@ GDB_PY_DEFINE_EVENT(breakpoint_deleted) GDB_PY_DEFINE_EVENT(breakpoint_modified) GDB_PY_DEFINE_EVENT(before_prompt) GDB_PY_DEFINE_EVENT(gdb_exiting) +GDB_PY_DEFINE_EVENT(connection_removed) diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c new file mode 100644 index 00000000000..52779030f19 --- /dev/null +++ b/gdb/python/py-connection.c @@ -0,0 +1,366 @@ +/* Python interface to inferiors. + + Copyright (C) 2009-2021 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/>. */ + +#include "defs.h" +#include "python-internal.h" +#include "process-stratum-target.h" +#include "inferior.h" +#include "observable.h" +#include "target-connection.h" +#include "py-events.h" +#include "py-event.h" +#include "arch-utils.h" + +#include <map> + +/* The Python object that represents a connection. */ + +struct connection_object +{ + PyObject_HEAD + + /* The process target that represents this connection. When a + connection_object is created this field will always point at a valid + target. Later, if GDB stops using this target (the target is popped + from all target stacks) then this field is set to nullptr, which + indicates that this Python object is now in the invalid state (see + the is_valid() method below). */ + struct process_stratum_target *target; +}; + +extern PyTypeObject connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); + +/* Require that CONNECTION be valid. */ +#define CONNPY_REQUIRE_VALID(connection) \ + do { \ + if (connection->target == nullptr) \ + { \ + PyErr_SetString (PyExc_RuntimeError, \ + _("Connection no longer exists.")); \ + return nullptr; \ + } \ + } while (0) + +/* A map between process_stratum targets and the Python object representing + them. We actually hold a gdbpy_ref around the Python object so that + reference counts are handled correctly when entries are deleted. */ +static std::map<process_stratum_target *, + gdbpy_ref<connection_object>> all_connection_objects; + +/* Return a reference to a gdb.TargetConnection object for TARGET. If + TARGET is nullptr then a reference to None is returned. + + Previously created gdb.TargetConnection objects are cached, and + additional references to the same connection object can be returned with + later calls to this function. */ + +gdbpy_ref<> +target_to_connection_object (process_stratum_target *target) +{ + if (target == nullptr) + return gdbpy_ref<>::new_reference (Py_None); + + gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; + if (conn_obj == nullptr) + { + conn_obj.reset (PyObject_New (connection_object, + &connection_object_type)); + if (conn_obj == nullptr) + return nullptr; + + conn_obj->target = target; + + /* PyObject_New initializes the new object with a refcount of 1. This + counts for the reference we are keeping in the target_ops data. */ + all_connection_objects[target] = conn_obj; + } + + gdb_assert (conn_obj != nullptr); + + /* Repackage the result as a PyObject reference. */ + return gdbpy_ref<> ((PyObject *) conn_obj.release ()); +} + +/* Return a list of gdb.TargetConnection objects, one for each currently + active connection. The returned list is in no particular order. */ + +PyObject * +gdbpy_connections (PyObject *self, PyObject *args) +{ + gdbpy_ref<> list (PyList_New (0)); + if (list == nullptr) + return nullptr; + + for (process_stratum_target *target : all_non_exited_process_targets ()) + { + gdb_assert (target != nullptr); + + gdbpy_ref<> conn = target_to_connection_object (target); + if (conn == nullptr) + return nullptr; + gdb_assert (conn.get () != Py_None); + + if (PyList_Append (list.get (), conn.get ()) < 0) + return nullptr; + } + + return list.release (); +} + +/* Emit a connection event for TARGET to REGISTRY. Return 0 on success, or + a negative value on error. */ + +static int +emit_connection_event (process_stratum_target *target, + eventregistry_object *registry) +{ + gdbpy_ref<> event_obj + = create_event_object (&connection_event_object_type); + if (event_obj == nullptr) + return -1; + + gdbpy_ref<> conn = target_to_connection_object (target); + if (evpy_add_attribute (event_obj.get (), "connection", conn.get ()) < 0) + return -1; + + return evpy_emit_event (event_obj.get (), registry); +} + +/* Callback for the connection_removed observer. */ + +static void +connpy_connection_removed (process_stratum_target *target) +{ + if (!gdb_python_initialized) + return; + + gdbpy_enter enter_py (get_current_arch (), current_language); + + if (!evregpy_no_listeners_p (gdb_py_events.connection_removed)) + if (emit_connection_event (target, gdb_py_events.connection_removed) < 0) + gdbpy_print_stack (); + + gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; + if (conn_obj != nullptr) + { + conn_obj->target = nullptr; + all_connection_objects.erase (target); + } +} + +/* Called when a gdb.TargetConnection object is deallocated. */ + +static void +connpy_connection_dealloc (PyObject *obj) +{ + connection_object *conn_obj = (connection_object *) obj; + + /* As the all_connection_objects map holds a reference to each connection + object we can only enter the dealloc function when the reference in + all_connection_objects has been erased. + + As we always set the target pointer back to nullptr before we erase + items from all_connection_objects then, when we get here, the target + pointer must be nullptr. */ + gdb_assert (conn_obj->target == nullptr); + + Py_TYPE (obj)->tp_free (obj); +} + +/* Implement repr() for gdb.TargetConnection. */ + +static PyObject * +connpy_repr (PyObject *obj) +{ + connection_object *self = (connection_object *) obj; + process_stratum_target *target = self->target; + + if (target == nullptr) + return PyString_FromFormat ("<%s (invalid)>", Py_TYPE (obj)->tp_name); + + return PyString_FromFormat ("<%s num=%d, what=\"%s\">", + Py_TYPE (obj)->tp_name, + target->connection_number, + make_target_connection_string (target).c_str ()); +} + +/* Implementation of gdb.TargetConnection.is_valid() -> Boolean. Returns + True if this connection object is still associated with a + process_stratum_target, otherwise, returns False. */ + +static PyObject * +connpy_is_valid (PyObject *self, PyObject *args) +{ + connection_object *conn = (connection_object *) self; + + if (conn->target == nullptr) + Py_RETURN_FALSE; + + Py_RETURN_TRUE; +} + +/* Return the id number of this connection. */ + +static PyObject * +connpy_get_connection_num (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + auto num = conn->target->connection_number; + return gdb_py_object_from_longest (num).release (); +} + +/* Return a string that gives the short name for this connection type. */ + +static PyObject * +connpy_get_connection_type (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *shortname = conn->target->shortname (); + return host_string_to_python_string (shortname).release (); +} + +/* Return a string that gives a longer description of this connection type. */ + +static PyObject * +connpy_get_description (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *longname = conn->target->longname (); + return host_string_to_python_string (longname).release (); +} + +/* Return a string that gives additional details about this connection, or + None, if there are no additional details for this connection type. */ + +static PyObject * +connpy_get_connection_details (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *details = conn->target->connection_string (); + if (details != nullptr) + return host_string_to_python_string (details).release (); + else + Py_RETURN_NONE; +} + +/* Python specific initialization for this file. */ + +int +gdbpy_initialize_connection (void) +{ + if (PyType_Ready (&connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "TargetConnection", + (PyObject *) &connection_object_type) < 0) + return -1; + + return 0; +} + +/* Global initialization for this file. */ + +void _initialize_py_connection (); +void +_initialize_py_connection () +{ + gdb::observers::connection_removed.attach (connpy_connection_removed, + "py-connection"); +} + +/* Methods for the gdb.TargetConnection object type. */ + +static PyMethodDef connection_object_methods[] = +{ + { "is_valid", connpy_is_valid, METH_NOARGS, + "is_valid () -> Boolean.\n\ +Return true if this TargetConnection is valid, false if not." }, + { NULL } +}; + +/* Attributes for the gdb.TargetConnection object type. */ + +static gdb_PyGetSetDef connection_object_getset[] = +{ + { "num", connpy_get_connection_num, NULL, + "ID number of this connection, as assigned by GDB.", NULL }, + { "type", connpy_get_connection_type, NULL, + "A short string that is the name for this connection type.", NULL }, + { "description", connpy_get_description, NULL, + "A longer string describing this connection type.", NULL }, + { "details", connpy_get_connection_details, NULL, + "A string containing additional connection details.", NULL }, + { NULL } +}; + +/* Define the gdb.TargetConnection object type. */ + +PyTypeObject connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.TargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + connection_object_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def index aeaee02e8bb..6a49f2aa896 100644 --- a/gdb/python/py-event-types.def +++ b/gdb/python/py-event-types.def @@ -110,3 +110,8 @@ GDB_PY_DEFINE_EVENT_TYPE (gdb_exiting, "GdbExitingEvent", "GDB is about to exit", event_object_type); + +GDB_PY_DEFINE_EVENT_TYPE (connection, + "ConnectionEvent", + "GDB connection added or removed object", + event_object_type); diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c index c8de41dd009..143f3492928 100644 --- a/gdb/python/py-inferior.c +++ b/gdb/python/py-inferior.c @@ -427,6 +427,20 @@ infpy_get_num (PyObject *self, void *closure) return gdb_py_object_from_longest (inf->inferior->num).release (); } +/* Return the gdb.TargetConnection object for this inferior, or None if a + connection does not exist. */ + +static PyObject * +infpy_get_connection (PyObject *self, void *closure) +{ + inferior_object *inf = (inferior_object *) self; + + INFPY_REQUIRE_VALID (inf); + + process_stratum_target *target = inf->inferior->process_target (); + return target_to_connection_object (target).release (); +} + /* Return the connection number of the given inferior, or None if a connection does not exist. */ @@ -968,6 +982,8 @@ gdbpy_initialize_inferior (void) static gdb_PyGetSetDef inferior_object_getset[] = { { "num", infpy_get_num, NULL, "ID of inferior, as assigned by GDB.", NULL }, + { "connection", infpy_get_connection, NULL, + "The gdb.TargetConnection for this inferior.", NULL }, { "connection_num", infpy_get_connection_num, NULL, "ID of inferior's connection, as assigned by GDB.", NULL }, { "pid", infpy_get_pid, NULL, "PID of inferior, as assigned by the OS.", diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h index 022d4a67172..e0c8d867e0d 100644 --- a/gdb/python/python-internal.h +++ b/gdb/python/python-internal.h @@ -478,6 +478,10 @@ gdbpy_ref<thread_object> create_thread_object (struct thread_info *tp); gdbpy_ref<> thread_to_thread_object (thread_info *thr);; gdbpy_ref<inferior_object> inferior_to_inferior_object (inferior *inf); +struct process_stratum_target; +gdbpy_ref<> target_to_connection_object (process_stratum_target *target); +PyObject *gdbpy_connections (PyObject *self, PyObject *args); + const struct block *block_object_to_block (PyObject *obj); struct symbol *symbol_object_to_symbol (PyObject *obj); struct value *value_object_to_value (PyObject *self); @@ -549,6 +553,8 @@ int gdbpy_initialize_unwind (void) CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; int gdbpy_initialize_tui () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; +int gdbpy_initialize_connection () + CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; /* A wrapper for PyErr_Fetch that handles reference counting for the caller. */ diff --git a/gdb/python/python.c b/gdb/python/python.c index 264f7c88ed6..c12e1aade18 100644 --- a/gdb/python/python.c +++ b/gdb/python/python.c @@ -1876,6 +1876,7 @@ do_start_initialization () || gdbpy_initialize_registers () < 0 || gdbpy_initialize_xmethods () < 0 || gdbpy_initialize_unwind () < 0 + || gdbpy_initialize_connection () < 0 || gdbpy_initialize_tui () < 0) return false; @@ -2299,6 +2300,10 @@ Set the value of the convenience variable $NAME." }, Register a TUI window constructor." }, #endif /* TUI */ + { "connections", gdbpy_connections, METH_NOARGS, + "connections () -> List.\n\ +Return a list of gdb.TargetConnection objects." }, + {NULL, NULL, 0, NULL} }; diff --git a/gdb/target-connection.c b/gdb/target-connection.c index a649423e07e..da1c9da74dc 100644 --- a/gdb/target-connection.c +++ b/gdb/target-connection.c @@ -24,6 +24,7 @@ #include "inferior.h" #include "target.h" +#include "observable.h" /* A map between connection number and representative process_stratum target. */ @@ -49,6 +50,9 @@ connection_list_add (process_stratum_target *t) void connection_list_remove (process_stratum_target *t) { + /* Notify about the connection being removed before we reset the + connection number to zero. */ + gdb::observers::connection_removed.notify (t); process_targets.erase (t->connection_number); t->connection_number = 0; } diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp index 3fd6b15cdd9..3f74a47de31 100644 --- a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp @@ -15,18 +15,27 @@ # Test "info inferiors" and "info connections" with multiple targets. +load_lib gdb-python.exp + source $srcdir/$subdir/multi-target.exp.tcl if {![multi_target_prepare]} { return } +# Cache the result of calling skip_python_tests into a local variable. +set run_python_tests [expr ! [skip_python_tests]] + # Test "info inferiors" and "info connections". MULTI_PROCESS # indicates whether the multi-process feature of remote targets is # turned off or on. proc test_info_inferiors {multi_process} { setup "off" + if { $::run_python_tests } { + gdb_test_no_output "source ${::remote_python_file}" "load python file" + } + gdb_test_no_output \ "set remote multiprocess-feature-packet $multi_process" @@ -86,6 +95,18 @@ proc test_info_inferiors {multi_process} { "[connection_num 5 $inf]${ws}core${ws}Local core dump file${ws}" \ ] + if { $::run_python_tests } { + gdb_test "python info_connections()" \ + [multi_line \ + "Num${ws}What${ws}Description" \ + "[connection_num 1 $inf]${ws}native${ws}Native process" \ + "[connection_num 2 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 3 $inf]${ws}core${ws}Local core dump file" \ + "[connection_num 4 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 5 $inf]${ws}core${ws}Local core dump file" \ + ] + } + gdb_test "info inferiors" \ [multi_line \ "Num${ws}Description${ws}Connection${ws}Executable${ws}" \ @@ -96,10 +117,27 @@ proc test_info_inferiors {multi_process} { "[inf_desc 5 $inf]4 \\(extended-remote localhost:$decimal\\)${ws}${binfile}${ws}" \ "[inf_desc 6 $inf]5 \\(core\\)${ws}${binfile}${ws}" \ ] + + if { $::run_python_tests } { + gdb_test "python info_inferiors()" \ + [multi_line \ + "Inferior 1, Connection #1: native" \ + "Inferior 2, Connection #2: extended-remote localhost:$decimal" \ + "Inferior 3, Connection #3: core" \ + "Inferior 4, Connection #1: native" \ + "Inferior 5, Connection #4: extended-remote localhost:$decimal" \ + "Inferior 6, Connection #5: core" \ + ] + } } } } +if { $run_python_tests } { + set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +} + # Test "info inferiors" and "info connections" commands. with_test_prefix "info-inferiors" { foreach_with_prefix multi_process {"on" "off"} { diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py new file mode 100644 index 00000000000..aac561ae683 --- /dev/null +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py @@ -0,0 +1,63 @@ +# Copyright (C) 2021 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 + +# Take a gdb.TargetConnection and return the connection number. +def conn_num(c): + return c.num + +# Takes a gdb.TargetConnection and return a string that is either the +# type, or the type and details (if the details are not None). +def make_target_connection_string(c): + if c.details is None: + return c.type + else: + return "%s %s" % (c.type, c.details) + +# A Python implementation of 'info connections'. Produce output that +# is identical to the output of 'info connections' so we can check +# that aspects of gdb.TargetConnection work correctly. +def info_connections(): + all_connections = sorted(gdb.connections(), key=conn_num) + current_conn = gdb.selected_inferior().connection + what_width = 0 + for c in all_connections: + s = make_target_connection_string(c) + if len(s) > what_width: + what_width = len(s) + + fmt = " Num %%-%ds Description" % what_width + print(fmt % "What") + fmt = "%%s%%-3d %%-%ds %%s" % what_width + for c in all_connections: + if c == current_conn: + prefix = "* " + else: + prefix = " " + + print(fmt % (prefix, c.num, make_target_connection_string(c), + c.description)) + +def inf_num(i): + return i.num + +# .... +def info_inferiors(): + all_inferiors = sorted(gdb.inferiors(), key=inf_num) + for i in gdb.inferiors(): + print("Inferior %d, Connection #%d: %s" % + (i.num, i.connection_num, + make_target_connection_string(i.connection))) diff --git a/gdb/testsuite/gdb.python/py-connection.c b/gdb/testsuite/gdb.python/py-connection.c new file mode 100644 index 00000000000..4b32e704476 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp new file mode 100644 index 00000000000..b994cec01eb --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -0,0 +1,63 @@ +# Copyright (C) 2021 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/>. + +# This file is for testing the gdb.TargetConnection API. This API is +# already tested in gdb.multi/multi-target-info-inferiors.exp and +# gdb.python/py-inferior.exp, this file just covers some edge cases +# that are not tested in other places. + +load_lib gdb-python.exp + +standard_testfile + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +# Skip all tests if Python scripting is not enabled. +if { [skip_python_tests] } { continue } + +if ![runto_main] then { + fail "can't run to main" + return 0 +} + +# Create a gdb.TargetConnection object and check it becomes invalid +# once the connection has gone away. +gdb_test_no_output "python conn = gdb.selected_inferior().connection" +gdb_test "python print(conn)" \ + "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ + "print gdb.TargetConnection while it is still valid" +gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" +gdb_test "info connections" "\r\n\\* 1 .*" \ + "info connections while the connection is still around" +gdb_test "kill" "" "kill the inferior" \ + "Kill the program being debugged.*y or n. $" "y" +gdb_test "info connections" "No connections\\." \ + "info connections now all the connections have gone" +gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ + "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" + +# Now check that accessing properties of the invalid connection cases +# an error. +gdb_test "python print(conn.num)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.type)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.description)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.details)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" diff --git a/gdb/testsuite/gdb.python/py-inferior.exp b/gdb/testsuite/gdb.python/py-inferior.exp index 894b90a85fc..d5d284a763e 100644 --- a/gdb/testsuite/gdb.python/py-inferior.exp +++ b/gdb/testsuite/gdb.python/py-inferior.exp @@ -51,6 +51,8 @@ gdb_py_test_silent_cmd "python i0 = inferiors\[0\]" "get first inferior" 0 gdb_test "python print ('result = %s' % (i0 == inferiors\[0\]))" " = True" "test equality comparison (true)" gdb_test "python print ('result = %s' % i0.num)" " = \[0-9\]+" "test Inferior.num" gdb_test "python print ('result = %s' % i0.connection_num)" " = \[0-9\]+" "test Inferior.connection_num" +gdb_test "python print ('result = %s' % (i0.connection_num == i0.connection.num))" " = True" \ + "Inferior.connection_num equals Inferior.connection.num" gdb_test "python print ('result = %s' % i0.pid)" " = \[0-9\]+" "test Inferior.pid" gdb_test "python print ('result = %s' % i0.was_attached)" " = False" "test Inferior.was_attached" gdb_test "python print (i0.threads ())" "\\(<gdb.InferiorThread object at 0x\[\[:xdigit:\]\]+>,\\)" "test Inferior.threads" @@ -264,6 +266,8 @@ with_test_prefix "is_valid" { "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].connection_num)" \ "RuntimeError: Inferior no longer exists.*" + gdb_test "python print (inf_list\[1\].connection)" \ + "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].pid)" \ "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].was_attached)" \ @@ -281,7 +285,9 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 1" ".*" "switch to first inferior" gdb_test "py print (gdb.selected_inferior().num)" "1" "first inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "1" \ - "first inferior's connection" + "first inferior's connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" "1" \ + "first inferior's connection number, though connection object" # Figure out if inf 1 has a native target. set inf_1_is_native [gdb_is_target_native] @@ -289,6 +295,8 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 3" ".*" "switch to third inferior" gdb_test "py print (gdb.selected_inferior().num)" "3" "third inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "None" \ + "third inferior's None connection number" + gdb_test "py print (gdb.selected_inferior().connection)" "None" \ "third inferior's None connection" gdb_test "target native" "Done. Use the \"run\" command to start a process." \ "target for the third inferior" @@ -302,7 +310,15 @@ with_test_prefix "selected_inferior" { } gdb_test "py print (gdb.selected_inferior().connection_num)" \ "$expected_connection_num" \ - "third inferior's native connection" + "third inferior's native connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" \ + "$expected_connection_num" \ + "third inferior's native connection number, though connection object" + + # Test printing of gdb.TargetConnection object. + gdb_test "py print (gdb.selected_inferior().connection)" \ + "<gdb.TargetConnection num=${expected_connection_num}, what=\"\[^\"\]+\">" \ + "print a connection object" gdb_test "inferior 1" ".*" "switch back to first inferior" gdb_test_no_output "remove-inferiors 3" "remove second inferior" -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-10-19 10:17 ` [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess @ 2021-10-19 12:26 ` Eli Zaretskii 2021-10-20 22:33 ` Lancelot SIX 2021-10-21 2:00 ` Simon Marchi 2 siblings, 0 replies; 52+ messages in thread From: Eli Zaretskii @ 2021-10-19 12:26 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches > From: Andrew Burgess <andrew.burgess@embecosm.com> > Cc: Eli Zaretskii <eliz@gnu.org>, > Andrew Burgess <andrew.burgess@embecosm.com> > Date: Tue, 19 Oct 2021 11:17:07 +0100 > > This commit adds a new object type gdb.TargetConnection. This new > type represents a connection within GDB (a connection as displayed by > 'info connections'). Thanks, the documentation parts are okay. ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-10-19 10:17 ` [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-19 12:26 ` Eli Zaretskii @ 2021-10-20 22:33 ` Lancelot SIX 2021-10-21 2:00 ` Simon Marchi 2 siblings, 0 replies; 52+ messages in thread From: Lancelot SIX @ 2021-10-20 22:33 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches Hi, There is a tiny typo in the commit message I guess On Tue, Oct 19, 2021 at 11:17:07AM +0100, Andrew Burgess wrote: > This commit adds a new object type gdb.TargetConnection. This new > type represents a connection within GDB (a connection as displayed by > 'info connections'). > > There's three ways to find a gdb.TargetConnection, there's a new > 'gdb.connections()' function, which returns a list of all currently > active connections. > > Or you can read the new 'connection' property on the gdb.Inferior > object type, this contains the connection for that inferior (or None > if the inferior has no connection, for example, it is exited). > > Finally, there's a new gdb.events.connection_removed event registry, > this emits a new gdb.ConnectionEvent whenever a connection is removed > from GDB (this happens when all inferiors using a connection exit). > The gdb.ConnectionEvent has a 'connection' property, which is the > gdb.TargetConnection being removed from GDB. > > The gdb.TargetConnection has a 'is_valid()' method. A connection > object becomes invalid when the underlying connection is removed from > GDB (all inferiors using the connection exit). > > The gdb.TargetConnection has the following read-only properties: > > 'num': The number for this connection, > > 'type': e.g. 'native', 'remove', 'sim', etc remove -> remote > > 'description': The longer description as seen in the 'info > connections' command output. > > 'details': A string or None. Extra details for the connection, for > example, a remote connection's details might be > 'hostname:port'. Best, Lancelot. ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-10-19 10:17 ` [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-19 12:26 ` Eli Zaretskii 2021-10-20 22:33 ` Lancelot SIX @ 2021-10-21 2:00 ` Simon Marchi 2 siblings, 0 replies; 52+ messages in thread From: Simon Marchi @ 2021-10-21 2:00 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2021-10-19 06:17, Andrew Burgess wrote: > This commit adds a new object type gdb.TargetConnection. This new > type represents a connection within GDB (a connection as displayed by > 'info connections'). > > There's three ways to find a gdb.TargetConnection, there's a new > 'gdb.connections()' function, which returns a list of all currently > active connections. > > Or you can read the new 'connection' property on the gdb.Inferior > object type, this contains the connection for that inferior (or None > if the inferior has no connection, for example, it is exited). > > Finally, there's a new gdb.events.connection_removed event registry, > this emits a new gdb.ConnectionEvent whenever a connection is removed > from GDB (this happens when all inferiors using a connection exit). > The gdb.ConnectionEvent has a 'connection' property, which is the > gdb.TargetConnection being removed from GDB. > > The gdb.TargetConnection has a 'is_valid()' method. A connection > object becomes invalid when the underlying connection is removed from > GDB (all inferiors using the connection exit). > > The gdb.TargetConnection has the following read-only properties: > > 'num': The number for this connection, > > 'type': e.g. 'native', 'remove', 'sim', etc > > 'description': The longer description as seen in the 'info > connections' command output. > > 'details': A string or None. Extra details for the connection, for > example, a remote connection's details might be > 'hostname:port'. > --- > gdb/Makefile.in | 1 + > gdb/NEWS | 16 + > gdb/doc/python.texi | 92 ++++- > gdb/observable.c | 1 + > gdb/observable.h | 3 + > gdb/python/py-all-events.def | 1 + > gdb/python/py-connection.c | 366 ++++++++++++++++++ > gdb/python/py-event-types.def | 5 + > gdb/python/py-inferior.c | 16 + > gdb/python/python-internal.h | 6 + > gdb/python/python.c | 5 + > gdb/target-connection.c | 4 + > .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ > .../gdb.multi/multi-target-info-inferiors.py | 63 +++ > gdb/testsuite/gdb.python/py-connection.c | 22 ++ > gdb/testsuite/gdb.python/py-connection.exp | 63 +++ > gdb/testsuite/gdb.python/py-inferior.exp | 20 +- > 17 files changed, 718 insertions(+), 4 deletions(-) > create mode 100644 gdb/python/py-connection.c > create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py > create mode 100644 gdb/testsuite/gdb.python/py-connection.c > create mode 100644 gdb/testsuite/gdb.python/py-connection.exp > > diff --git a/gdb/Makefile.in b/gdb/Makefile.in > index 4201f65e68d..5ec58aba439 100644 > --- a/gdb/Makefile.in > +++ b/gdb/Makefile.in > @@ -392,6 +392,7 @@ SUBDIR_PYTHON_SRCS = \ > python/py-breakpoint.c \ > python/py-cmd.c \ > python/py-continueevent.c \ > + python/py-connection.c \ This is not in alphabetical order :). > python/py-event.c \ > python/py-evtregistry.c \ > python/py-evts.c \ > diff --git a/gdb/NEWS b/gdb/NEWS > index bd26d2b1ec2..43b81df526d 100644 > --- a/gdb/NEWS > +++ b/gdb/NEWS > @@ -45,6 +45,22 @@ maint show internal-warning backtrace > event is triggered once GDB decides it is going to exit, but > before GDB starts to clean up its internal state. > > + ** New gdb.TargetConnection object type that represents a connection > + (as displayed by the 'info connections' command). > + > + ** The gdb.Inferior type now has a 'connection' property which is an > + instance of gdb.TargetConnection, the connection used by this > + inferior. This can be None if the inferior has no connection > + (for example, when exited). "when exited" is not completely accurate (see below, I wrote that other comment before). > +A @code{gdb.TargetConnection} has the following method: > + > +@defun TargetConnection.is_valid () > +Return @code{True} if the @code{gdb.TargetConnection} object is valid, > +@code{False} if not. A @code{gdb.TargetConnection} will become > +invalid if the connection no longer exists within @value{GDBN}, this > +will happen when all the inferiors using that connection exit. I don't think the last part is completely accurate. In some cases, an inferior will keep its process target (and therefore keep the connection alive) after exiting. For instance, if you push the native target explicitly, or if you connect using the extended-remote protocol. You could say "this will happen when no inferior use the connection" or something along those lines. > +gdbpy_ref<> > +target_to_connection_object (process_stratum_target *target) > +{ > + if (target == nullptr) > + return gdbpy_ref<>::new_reference (Py_None); > + > + gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; > + if (conn_obj == nullptr) > + { > + conn_obj.reset (PyObject_New (connection_object, > + &connection_object_type)); > + if (conn_obj == nullptr) > + return nullptr; > + > + conn_obj->target = target; > + > + /* PyObject_New initializes the new object with a refcount of 1. This > + counts for the reference we are keeping in the target_ops data. */ > + all_connection_objects[target] = conn_obj; I find this comment more confusing than helpful. The strong reference created by PyObject_New belongs to conn_obj, which is then returned by this function. The strong reference belonging to the map (what I presume you mean by "target_ops data") is created when copying conn_obj into the map. In the end all references are the same, I just find it a bit confusing. > +/* Callback for the connection_removed observer. */ > + > +static void > +connpy_connection_removed (process_stratum_target *target) > +{ > + if (!gdb_python_initialized) > + return; > + > + gdbpy_enter enter_py (get_current_arch (), current_language); > + > + if (!evregpy_no_listeners_p (gdb_py_events.connection_removed)) > + if (emit_connection_event (target, gdb_py_events.connection_removed) < 0) > + gdbpy_print_stack (); > + > + gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; > + if (conn_obj != nullptr) > + { > + conn_obj->target = nullptr; > + all_connection_objects.erase (target); > + } Watch out with using operator[], that will insert an item for `target` with value nullptr in the map, if there isn't one already. Maybe it doesn't matter in the end, but it brings the situation where a target without a corresponding Python object can exist in two different states: no entry in the map, and an entry with value nullptr. I think it would be easier to understand / less error prone if all entries in the map had non-nullptr values, and targets without corresponding Python object had no entries in the map. > diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp > new file mode 100644 > index 00000000000..b994cec01eb > --- /dev/null > +++ b/gdb/testsuite/gdb.python/py-connection.exp > @@ -0,0 +1,63 @@ > +# Copyright (C) 2021 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/>. > + > +# This file is for testing the gdb.TargetConnection API. This API is > +# already tested in gdb.multi/multi-target-info-inferiors.exp and > +# gdb.python/py-inferior.exp, this file just covers some edge cases > +# that are not tested in other places. > + > +load_lib gdb-python.exp > + > +standard_testfile > + > +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { > + return -1 > +} > + > +# Skip all tests if Python scripting is not enabled. > +if { [skip_python_tests] } { continue } > + > +if ![runto_main] then { > + fail "can't run to main" > + return 0 > +} Since 4dfef5be6812 ("gdb/testsuite: make runto_main not pass no-message to runto"), you don't have to make an explicit fail when runto_main fails. > + > +# Create a gdb.TargetConnection object and check it becomes invalid > +# once the connection has gone away. > +gdb_test_no_output "python conn = gdb.selected_inferior().connection" > +gdb_test "python print(conn)" \ > + "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ > + "print gdb.TargetConnection while it is still valid" > +gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" > +gdb_test "info connections" "\r\n\\* 1 .*" \ > + "info connections while the connection is still around" > +gdb_test "kill" "" "kill the inferior" \ > + "Kill the program being debugged.*y or n. $" "y" > +gdb_test "info connections" "No connections\\." \ > + "info connections now all the connections have gone" The above will fail with the native-extended-gdbserver board, as the connection will not be gone. You could maybe use "disconnect", which should pop the process target, whatever it is. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv3 2/3] gdb: make packet_command function available outside remote.c 2021-10-19 10:17 ` [PATCHv3 0/3] Python API for target connections, and packet sending Andrew Burgess 2021-10-19 10:17 ` [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess @ 2021-10-19 10:17 ` Andrew Burgess 2021-10-21 2:23 ` Simon Marchi 2021-10-19 10:17 ` [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 0/4] Python API for target connections, and packet sending Andrew Burgess 3 siblings, 1 reply; 52+ messages in thread From: Andrew Burgess @ 2021-10-19 10:17 UTC (permalink / raw) To: gdb-patches In a later commit I will add a Python API to access the 'maint packet' functionality, that is, sending a user specified packet to the target. To make implementing this easier, this commit refactors how this command is currently implemented so that the packet_command function is now global. The new global send_remote_packet function takes an object that is an implementation of an abstract interface. Two functions within this interface are then called, one just before a packet is sent to the remote target, and one when the reply has been received from the remote target. Using an interface object in this way allows (1) for the error checking to be done before the first callback is made, this means we only print out what packet it being sent once we know we are going to actually send it, and (2) we don't need to make a copy of the reply if all we want to do is print it. The only user visible changes after this commit are the error messages, which I've changed to be less 'maint packet' command focused, this will make them (I hope) better for when send_remote_packet can be called from Python code. So: "command can only be used with remote target" Becomes: "packets can only be sent to a remote target" And: "remote-packet command requires packet text as argument" Becomes: "a remote packet must not be empty" --- gdb/remote.c | 67 +++++++++++++++++++++++++++++++++++----------------- gdb/remote.h | 34 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/gdb/remote.c b/gdb/remote.c index d5eb40ce578..237b1b21c41 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -956,8 +956,6 @@ class remote_target : public process_stratum_target bool vcont_r_supported (); - void packet_command (const char *args, int from_tty); - private: /* data fields */ /* The remote state. Don't reference this directly. Use the @@ -11615,34 +11613,59 @@ remote_target::memory_map () return result; } -static void -packet_command (const char *args, int from_tty) +/* Set of callbacks used to implement the 'maint packet' command. */ + +struct cli_packet_command_callbacks : public send_remote_packet_callbacks { - remote_target *remote = get_current_remote_target (); + /* Called before the packet is sent. PACKET_STR is the packet content + before the protocol specific prefix, suffix, and escaping is added. */ - if (remote == nullptr) - error (_("command can only be used with remote target")); + void sending (const char *packet_str) override + { + puts_filtered ("sending: "); + print_packet (packet_str); + puts_filtered ("\n"); + } - remote->packet_command (args, from_tty); -} + /* Called with BUF, the reply from the remote target. */ + + void received (const gdb::char_vector &buf) override + { + puts_filtered ("received: "); + print_packet (buf.data ()); + puts_filtered ("\n"); + } +}; + +/* See remote.h. */ void -remote_target::packet_command (const char *args, int from_tty) +send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks) { - if (!args) - error (_("remote-packet command requires packet text as argument")); + if (packet_str == nullptr || *packet_str == '\0') + error (_("a remote packet must not be empty")); - puts_filtered ("sending: "); - print_packet (args); - puts_filtered ("\n"); - putpkt (args); + remote_target *remote = get_current_remote_target (); + if (remote == nullptr) + error (_("packets can only be sent to a remote target")); - remote_state *rs = get_remote_state (); + callbacks->sending (packet_str); - getpkt (&rs->buf, 0); - puts_filtered ("received: "); - print_packet (rs->buf.data ()); - puts_filtered ("\n"); + remote->putpkt (packet_str); + remote_state *rs = remote->get_remote_state (); + remote->getpkt (&rs->buf, 0); + + callbacks->received (rs->buf); +} + +/* Entry point for the 'maint packet' command. */ + +static void +cli_packet_command (const char *args, int from_tty) +{ + cli_packet_command_callbacks cb; + send_remote_packet (args, &cb); } #if 0 @@ -14921,7 +14944,7 @@ Argument is a single section name (default: all loaded sections).\n\ To compare only read-only loaded sections, specify the -r option."), &cmdlist); - add_cmd ("packet", class_maintenance, packet_command, _("\ + add_cmd ("packet", class_maintenance, cli_packet_command, _("\ Send an arbitrary packet to a remote target.\n\ maintenance packet TEXT\n\ If GDB is talking to an inferior via the GDB serial protocol, then\n\ diff --git a/gdb/remote.h b/gdb/remote.h index 46bfa01fc79..cd91be8decb 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -78,4 +78,38 @@ extern int remote_register_number_and_offset (struct gdbarch *gdbarch, extern void remote_notif_get_pending_events (remote_target *remote, struct notif_client *np); extern bool remote_target_is_non_stop_p (remote_target *t); + +/* An abstract class that represents the set of callbacks that are made + from the send_remote_packet function (declared below). */ + +struct send_remote_packet_callbacks +{ + /* The SENDING callback is called once send_remote_packet has performed + its error checking and setup, just before the packet is sent to the + remote target. PACKET_STR is the content of the packet that will be + sent (before any of the protocol specific prefix, suffix, or escaping + is applied). */ + + virtual void sending (const char *packet_str) = 0; + + /* The RECEIVED callback is called once a reply has been received from + the remote target. The content of the reply is in BUF which can't be + modified, and which is not guaranteed to remain valid after the + RECEIVED call has returned. If you need to preserve the contents of + BUF then a copy should be taken. */ + + virtual void received (const gdb::char_vector &buf) = 0; +}; + +/* Send PACKET_STR the current remote target. If PACKET_STR is nullptr, or + is the empty string, then an error is thrown. If the current target is + not a remote target then an error is thrown. + + Calls CALLBACKS->sending() just before the packet is sent to the remote + target, and calls CALLBACKS->received() with the reply once this is + received from the remote target. */ + +extern void send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks); + #endif -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv3 2/3] gdb: make packet_command function available outside remote.c 2021-10-19 10:17 ` [PATCHv3 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess @ 2021-10-21 2:23 ` Simon Marchi 0 siblings, 0 replies; 52+ messages in thread From: Simon Marchi @ 2021-10-21 2:23 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2021-10-19 06:17, Andrew Burgess wrote: > In a later commit I will add a Python API to access the 'maint packet' > functionality, that is, sending a user specified packet to the target. > > To make implementing this easier, this commit refactors how this > command is currently implemented so that the packet_command function > is now global. > > The new global send_remote_packet function takes an object that is an > implementation of an abstract interface. Two functions within this > interface are then called, one just before a packet is sent to the > remote target, and one when the reply has been received from the > remote target. Using an interface object in this way allows (1) for > the error checking to be done before the first callback is made, this > means we only print out what packet it being sent once we know we are > going to actually send it, and (2) we don't need to make a copy of the > reply if all we want to do is print it. > > The only user visible changes after this commit are the error > messages, which I've changed to be less 'maint packet' command > focused, this will make them (I hope) better for when > send_remote_packet can be called from Python code. > > So: "command can only be used with remote target" > Becomes: "packets can only be sent to a remote target" > > And: "remote-packet command requires packet text as argument" > Becomes: "a remote packet must not be empty" This LGTM. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-10-19 10:17 ` [PATCHv3 0/3] Python API for target connections, and packet sending Andrew Burgess 2021-10-19 10:17 ` [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-19 10:17 ` [PATCHv3 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess @ 2021-10-19 10:17 ` Andrew Burgess 2021-10-19 12:28 ` Eli Zaretskii 2021-10-21 2:43 ` Simon Marchi 2021-10-22 10:58 ` [PATCHv4 0/4] Python API for target connections, and packet sending Andrew Burgess 3 siblings, 2 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-19 10:17 UTC (permalink / raw) To: gdb-patches This commits adds a new sub-class of gdb.TargetConnection, gdb.RemoteTargetConnection. This sub-class is created for all 'remote' and 'extended-remote' targets. This new sub-class has one additional method over its base class, 'send_packet'. This new method is equivalent to the 'maint packet' CLI command, it allows a custom packet to be sent to a remote target. The result of calling RemoteTargetConnection.send_packet is a string containing the reply that came from the remote. --- gdb/NEWS | 8 +- gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 28 +++- gdb/python/py-connection.c | 163 +++++++++++++++++++- gdb/testsuite/gdb.python/py-send-packet.c | 22 +++ gdb/testsuite/gdb.python/py-send-packet.exp | 59 +++++++ gdb/testsuite/gdb.python/py-send-packet.py | 84 ++++++++++ 7 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py diff --git a/gdb/NEWS b/gdb/NEWS index 43b81df526d..78ac8beb7fd 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -46,7 +46,9 @@ maint show internal-warning backtrace before GDB starts to clean up its internal state. ** New gdb.TargetConnection object type that represents a connection - (as displayed by the 'info connections' command). + (as displayed by the 'info connections' command). A sub-class, + gdb.RemoteTargetConnection, is used to represent 'remote' and + 'extended-remote' connections. ** The gdb.Inferior type now has a 'connection' property which is an instance of gdb.TargetConnection, the connection used by this @@ -61,6 +63,10 @@ maint show internal-warning backtrace ** New gdb.connections() function that returns a list of all currently active connections. + ** New gdb.RemoteTargetConnection.send_packet(STRING) method. This + is equivalent to the existing 'maint packet' CLI command; it + allows a user specified packet to be sent to the remote target. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index 631a7c03b31..89cf86210ab 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -39269,6 +39269,7 @@ error stream. This is @samp{on} by default for @code{internal-error} and @samp{off} by default for @code{internal-warning}. +@anchor{maint packet} @kindex maint packet @item maint packet @var{text} If @value{GDBN} is talking to an inferior via the serial protocol, diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 599c411b11d..97c1dd595bf 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -5980,9 +5980,9 @@ Examples of different connection types are @samp{native} and @samp{remote}. @xref{Inferiors Connections and Programs}. -@value{GDBN} uses the @code{gdb.TargetConnection} object type to -represent a connection in Python code. To get a list of all -connections use @code{gdb.connections} +Connections in @value{GDBN} are represented as instances of +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a +list of all connections use @code{gdb.connections} (@pxref{gdbpy_connections,,gdb.connections}). To get the connection for a single @code{gdb.Inferior} read its @@ -6034,6 +6034,28 @@ to the remote target. @end defvar +The @code{gdb.RemoteTargetConnection} class is a sub-class of +@code{gdb.TargetConnection}, and is used to represent @samp{remote} +and @samp{extended-remote} connections. In addition to the attributes +and methods available from the @code{gdb.TargetConnection} base class, +a @code{gdb.RemoteTargetConnection} has the following additional method: + +@kindex maint packet +@defun RemoteTargetConnection.send_packet (@var{packet}) +This method sends @var{packet}, which should be a non-empty string, to +the remote target and returns the response as a string. If +@var{packet} is not a string, or is the empty string, then an +exception of type @code{gdb.error} is thrown. + +The prefix, suffix, and checksum (as required by the remote serial +protocol) are automatically added to the outgoing packet, and removed +from the incoming packet before the contents of the reply are +returned. + +This is equivalent to the @code{maintenance packet} command +(@pxref{maint packet}). +@end defun + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c index 52779030f19..8d3d40b0078 100644 --- a/gdb/python/py-connection.c +++ b/gdb/python/py-connection.c @@ -26,6 +26,8 @@ #include "py-events.h" #include "py-event.h" #include "arch-utils.h" +#include "remote.h" +#include "charset.h" #include <map> @@ -47,6 +49,9 @@ struct connection_object extern PyTypeObject connection_object_type CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); +extern PyTypeObject remote_connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("remote_connection_object"); + /* Require that CONNECTION be valid. */ #define CONNPY_REQUIRE_VALID(connection) \ do { \ @@ -80,8 +85,15 @@ target_to_connection_object (process_stratum_target *target) gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; if (conn_obj == nullptr) { - conn_obj.reset (PyObject_New (connection_object, - &connection_object_type)); + PyTypeObject *type; + + std::string connection_type = target->shortname (); + if (connection_type == "remote" || connection_type == "extended-remote") + type = &remote_connection_object_type; + else + type = &connection_object_type; + + conn_obj.reset (PyObject_New (connection_object, type)); if (conn_obj == nullptr) return nullptr; @@ -284,9 +296,100 @@ gdbpy_initialize_connection (void) (PyObject *) &connection_object_type) < 0) return -1; + if (PyType_Ready (&remote_connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "RemoteTargetConnection", + (PyObject *) &remote_connection_object_type) < 0) + return -1; + return 0; } +/* Set of callbacks used to implement gdb.send_packet. */ + +struct py_send_packet_callbacks : public send_remote_packet_callbacks +{ + /* Constructor, initialise the result to None. */ + + py_send_packet_callbacks () + : m_result (Py_None) + { /* Nothing. */ } + + /* There's nothing to do when the packet is sent. */ + + void sending (const char *args) override + { /* Nothing. */ } + + /* When the result is returned create a Python string and assign this + into the result member variable. */ + + void received (const gdb::char_vector &buf) override + { + /* Return None when we don't get back a useful result. */ + if (buf.data ()[0] != '\0') + m_result = gdbpy_ref<> (PyUnicode_Decode (buf.data (), + strlen (buf.data ()), + host_charset (), nullptr)); + } + + /* Get a reference to the result as a Python object. */ + + gdbpy_ref<> result () const + { + return m_result; + } + +private: + + /* A reference to a valid result value. This is initialized in the + constructor, and so will always point to a valid value, even if this + is just None. */ + + gdbpy_ref<> m_result; +}; + +/* Implement RemoteTargetConnection.send_packet function. Send a packet to + the target identified by SELF. The connection must still be valid, and + the packet to be sent must be non-empty, otherwise an exception will be + thrown. */ + +static PyObject * +connpy_send_packet (PyObject *self, PyObject *args, PyObject *kw) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + static const char *keywords[] = {"packet", nullptr}; + const char *packet_str = nullptr; + + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s", keywords, + &packet_str)) + return nullptr; + + if (packet_str == nullptr || *packet_str == '\0') + { + PyErr_SetString (PyExc_ValueError, _("Invalid remote packet")); + return nullptr; + } + + try + { + scoped_restore_current_thread restore_thread; + switch_to_target_no_thread (conn->target); + + py_send_packet_callbacks callbacks; + send_remote_packet (packet_str, &callbacks); + return callbacks.result ().release (); + } + catch (const gdb_exception &except) + { + gdbpy_convert_exception (except); + return nullptr; + } +} + /* Global initialization for this file. */ void _initialize_py_connection (); @@ -307,6 +410,17 @@ Return true if this TargetConnection is valid, false if not." }, { NULL } }; +/* Methods for the gdb.RemoteTargetConnection object type. */ + +static PyMethodDef remote_connection_object_methods[] = +{ + { "send_packet", (PyCFunction) connpy_send_packet, + METH_VARARGS | METH_KEYWORDS, + "send_packet (PACKET) -> String\n\ +Send PACKET to a remote target, return the reply as a string." }, + { NULL } +}; + /* Attributes for the gdb.TargetConnection object type. */ static gdb_PyGetSetDef connection_object_getset[] = @@ -345,7 +459,7 @@ PyTypeObject connection_object_type = 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ "GDB target connection object", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ @@ -364,3 +478,46 @@ PyTypeObject connection_object_type = 0, /* tp_init */ 0 /* tp_alloc */ }; + +/* Define the gdb.RemoteTargetConnection object type. */ + +PyTypeObject remote_connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.RemoteTargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB remote target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + remote_connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + &connection_object_type, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/testsuite/gdb.python/py-send-packet.c b/gdb/testsuite/gdb.python/py-send-packet.c new file mode 100644 index 00000000000..9811b15f06d --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main () +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp new file mode 100644 index 00000000000..89ab28d003b --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.exp @@ -0,0 +1,59 @@ +# Copyright (C) 2021 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 the gdb.RemoteTargetConnection.send_packet API. This is done +# by connecting to a remote target and fetching the thread list in two +# ways, first, we manually send the packets required to read the +# thread list using gdb.TargetConnection.send_packet, then we compare +# the results to the thread list using the standard API calls. + +load_lib gdb-python.exp +load_lib gdbserver-support.exp + +standard_testfile + +if {[skip_gdbserver_tests]} { + return 0 +} + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +gdb_exit +gdb_start + +if { [skip_python_tests] } { + return 0 +} + +# Make sure we're disconnected, in case we're testing with an +# extended-remote board, therefore already connected. +gdb_test "disconnect" ".*" + +gdbserver_run "" + +# Source the python script. +set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +gdb_test "source $remote_python_file" "Sourcing complete\\." \ + "source ${testfile}.py script" + +# The test is actually written in the Python script. Run it now. +gdb_test "python run_send_packet_test()" "Send packet test passed" + +# Check the string representation of a remote target connection. +gdb_test "python print(gdb.selected_inferior().connection)" \ + "<gdb.RemoteTargetConnection num=$decimal, what=\".*\">" diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py new file mode 100644 index 00000000000..8478dfe622a --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.py @@ -0,0 +1,84 @@ +# Copyright (C) 2021 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 xml.etree.ElementTree as ET +import gdb + +# Make use of gdb.RemoteTargetConnection.send_packet to fetch the +# thread list from the remote target. +# +# Sending existing serial protocol packets like this is not a good +# idea, there should be better ways to get this information using an +# official API, this is just being used as a test case. +# +# Really, the send_packet API would be used to send target +# specific packets to the target, but these are, by definition, target +# specific, so hard to test in a general testsuite. +def get_thread_list_str(): + start_pos = 0 + thread_desc = "" + conn = gdb.selected_inferior().connection + if not isinstance(conn, gdb.RemoteTargetConnection): + raise gdb.GdbError("connection is the wrong type") + try: + while True: + str = conn.send_packet("qXfer:threads:read::%d,200" % start_pos) + start_pos += 200 + c = str[0] + str = str[1:] + thread_desc += str + if c == "l": + break + return thread_desc + except: + return None + + +# Use gdb.RemoteTargetConnection.send_packet to manually fetch the +# thread list, then extract the thread list using the gdb.Inferior and +# gdb.InferiorThread API. Compare the two results to ensure we +# managed to successfully read the thread list from the remote. +def run_send_packet_test(): + # Find the IDs of all current threads. + all_threads = {} + for inf in gdb.inferiors(): + for thr in inf.threads(): + id = "p%x.%x" % (thr.ptid[0], thr.ptid[1]) + all_threads[id] = False + + # Now fetch the thread list from the remote, and parse the XML. + str = get_thread_list_str() + threads_xml = ET.fromstring(str) + + # Look over all threads in the XML list and check we expected to + # find them, mark the ones we do find. + for thr in threads_xml: + id = thr.get("id") + if not id in all_threads: + raise "found unexpected thread in remote thread list" + else: + all_threads[id] = True + + # Check that all the threads were found in the XML list. + for id in all_threads: + if not all_threads[id]: + raise "thread missingt from remote thread list" + + # Test complete. + print("Send packet test passed") + + +# Just to indicate the file was sourced correctly. +print("Sourcing complete.") -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-10-19 10:17 ` [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess @ 2021-10-19 12:28 ` Eli Zaretskii 2021-10-21 2:43 ` Simon Marchi 1 sibling, 0 replies; 52+ messages in thread From: Eli Zaretskii @ 2021-10-19 12:28 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches, andrew.burgess > From: Andrew Burgess <andrew.burgess@embecosm.com> > Cc: Eli Zaretskii <eliz@gnu.org>, > Andrew Burgess <andrew.burgess@embecosm.com> > Date: Tue, 19 Oct 2021 11:17:09 +0100 > > This commits adds a new sub-class of gdb.TargetConnection, > gdb.RemoteTargetConnection. This sub-class is created for all > 'remote' and 'extended-remote' targets. Thanks. > diff --git a/gdb/NEWS b/gdb/NEWS > index 43b81df526d..78ac8beb7fd 100644 > --- a/gdb/NEWS > +++ b/gdb/NEWS > @@ -46,7 +46,9 @@ maint show internal-warning backtrace > before GDB starts to clean up its internal state. > > ** New gdb.TargetConnection object type that represents a connection > - (as displayed by the 'info connections' command). > + (as displayed by the 'info connections' command). A sub-class, > + gdb.RemoteTargetConnection, is used to represent 'remote' and > + 'extended-remote' connections. > > ** The gdb.Inferior type now has a 'connection' property which is an > instance of gdb.TargetConnection, the connection used by this > @@ -61,6 +63,10 @@ maint show internal-warning backtrace > ** New gdb.connections() function that returns a list of all > currently active connections. > > + ** New gdb.RemoteTargetConnection.send_packet(STRING) method. This > + is equivalent to the existing 'maint packet' CLI command; it > + allows a user specified packet to be sent to the remote target. > + > *** Changes in GDB 11 This part is OK. > +The @code{gdb.RemoteTargetConnection} class is a sub-class of > +@code{gdb.TargetConnection}, and is used to represent @samp{remote} > +and @samp{extended-remote} connections. In addition to the attributes > +and methods available from the @code{gdb.TargetConnection} base class, > +a @code{gdb.RemoteTargetConnection} has the following additional method: "In addition, ... has the following additional method" -- one of the "additional" parts is redundant (probably the second one). The documentation parts are okay with this one nit fixed. ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-10-19 10:17 ` [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2021-10-19 12:28 ` Eli Zaretskii @ 2021-10-21 2:43 ` Simon Marchi 2021-10-22 11:08 ` Andrew Burgess 1 sibling, 1 reply; 52+ messages in thread From: Simon Marchi @ 2021-10-21 2:43 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2021-10-19 06:17, Andrew Burgess wrote: > This commits adds a new sub-class of gdb.TargetConnection, > gdb.RemoteTargetConnection. This sub-class is created for all > 'remote' and 'extended-remote' targets. > > This new sub-class has one additional method over its base class, > 'send_packet'. This new method is equivalent to the 'maint > packet' CLI command, it allows a custom packet to be sent to a remote > target. > > The result of calling RemoteTargetConnection.send_packet is a string > containing the reply that came from the remote. > --- > gdb/NEWS | 8 +- > gdb/doc/gdb.texinfo | 1 + > gdb/doc/python.texi | 28 +++- > gdb/python/py-connection.c | 163 +++++++++++++++++++- > gdb/testsuite/gdb.python/py-send-packet.c | 22 +++ > gdb/testsuite/gdb.python/py-send-packet.exp | 59 +++++++ > gdb/testsuite/gdb.python/py-send-packet.py | 84 ++++++++++ > 7 files changed, 358 insertions(+), 7 deletions(-) > create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c > create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp > create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py > > diff --git a/gdb/NEWS b/gdb/NEWS > index 43b81df526d..78ac8beb7fd 100644 > --- a/gdb/NEWS > +++ b/gdb/NEWS > @@ -46,7 +46,9 @@ maint show internal-warning backtrace > before GDB starts to clean up its internal state. > > ** New gdb.TargetConnection object type that represents a connection > - (as displayed by the 'info connections' command). > + (as displayed by the 'info connections' command). A sub-class, > + gdb.RemoteTargetConnection, is used to represent 'remote' and > + 'extended-remote' connections. > > ** The gdb.Inferior type now has a 'connection' property which is an > instance of gdb.TargetConnection, the connection used by this > @@ -61,6 +63,10 @@ maint show internal-warning backtrace > ** New gdb.connections() function that returns a list of all > currently active connections. > > + ** New gdb.RemoteTargetConnection.send_packet(STRING) method. This > + is equivalent to the existing 'maint packet' CLI command; it > + allows a user specified packet to be sent to the remote target. > + > *** Changes in GDB 11 > > * The 'set disassembler-options' command now supports specifying options > diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo > index 631a7c03b31..89cf86210ab 100644 > --- a/gdb/doc/gdb.texinfo > +++ b/gdb/doc/gdb.texinfo > @@ -39269,6 +39269,7 @@ > error stream. This is @samp{on} by default for @code{internal-error} > and @samp{off} by default for @code{internal-warning}. > > +@anchor{maint packet} > @kindex maint packet > @item maint packet @var{text} > If @value{GDBN} is talking to an inferior via the serial protocol, > diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi > index 599c411b11d..97c1dd595bf 100644 > --- a/gdb/doc/python.texi > +++ b/gdb/doc/python.texi > @@ -5980,9 +5980,9 @@ > Examples of different connection types are @samp{native} and > @samp{remote}. @xref{Inferiors Connections and Programs}. > > -@value{GDBN} uses the @code{gdb.TargetConnection} object type to > -represent a connection in Python code. To get a list of all > -connections use @code{gdb.connections} > +Connections in @value{GDBN} are represented as instances of > +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a > +list of all connections use @code{gdb.connections} > (@pxref{gdbpy_connections,,gdb.connections}). > > To get the connection for a single @code{gdb.Inferior} read its > @@ -6034,6 +6034,28 @@ > to the remote target. > @end defvar > > +The @code{gdb.RemoteTargetConnection} class is a sub-class of > +@code{gdb.TargetConnection}, and is used to represent @samp{remote} > +and @samp{extended-remote} connections. In addition to the attributes > +and methods available from the @code{gdb.TargetConnection} base class, > +a @code{gdb.RemoteTargetConnection} has the following additional method: > + > +@kindex maint packet > +@defun RemoteTargetConnection.send_packet (@var{packet}) > +This method sends @var{packet}, which should be a non-empty string, to > +the remote target and returns the response as a string. If > +@var{packet} is not a string, or is the empty string, then an > +exception of type @code{gdb.error} is thrown. I think that the standard TypeError and ValueError would be appropriate here. Can the remote return some bytes that can't be decoded as a string (like the reply of the X packet)? In that case, maybe the return value of send_packet should be `bytes` (at least for Python 3). > @@ -80,8 +85,15 @@ target_to_connection_object (process_stratum_target *target) > gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; > if (conn_obj == nullptr) > { > - conn_obj.reset (PyObject_New (connection_object, > - &connection_object_type)); > + PyTypeObject *type; > + > + std::string connection_type = target->shortname (); > + if (connection_type == "remote" || connection_type == "extended-remote") > + type = &remote_connection_object_type; > + else > + type = &connection_object_type; I imagined that we could use a dynamic cast here, same as we do in get_current_remote_target: dynamic_cast<remote_target *> (target) But, comparing the strings works too, it's not like these names are going to change. > +/* Implement RemoteTargetConnection.send_packet function. Send a packet to > + the target identified by SELF. The connection must still be valid, and > + the packet to be sent must be non-empty, otherwise an exception will be > + thrown. */ > + > +static PyObject * > +connpy_send_packet (PyObject *self, PyObject *args, PyObject *kw) > +{ > + connection_object *conn = (connection_object *) self; > + > + CONNPY_REQUIRE_VALID (conn); > + > + static const char *keywords[] = {"packet", nullptr}; > + const char *packet_str = nullptr; > + > + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s", keywords, > + &packet_str)) > + return nullptr; > + > + if (packet_str == nullptr || *packet_str == '\0') > + { > + PyErr_SetString (PyExc_ValueError, _("Invalid remote packet")); > + return nullptr; > + } > + > + try > + { > + scoped_restore_current_thread restore_thread; > + switch_to_target_no_thread (conn->target); > + > + py_send_packet_callbacks callbacks; > + send_remote_packet (packet_str, &callbacks); > + return callbacks.result ().release (); Looking at py_send_packet_callbacks::received, it looks like m_result can stay nullptr. So here, we would return nullptr, which indicates to the Python interpreter that we are throwing an exception. But are we sure the error indicator has been set? If PyUnicode_Decode fails, I suppose it will. But if m_result stays nullptr because buf is empty, I guess we'll return nullptr without setting the error indicator. > diff --git a/gdb/testsuite/gdb.python/py-send-packet.c b/gdb/testsuite/gdb.python/py-send-packet.c > new file mode 100644 > index 00000000000..9811b15f06d > --- /dev/null > +++ b/gdb/testsuite/gdb.python/py-send-packet.c > @@ -0,0 +1,22 @@ > +/* This testcase is part of GDB, the GNU debugger. > + > + Copyright 2021 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/>. */ > + > +int > +main () main (void) > +{ > + return 0; > +} > diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp > new file mode 100644 > index 00000000000..89ab28d003b > --- /dev/null > +++ b/gdb/testsuite/gdb.python/py-send-packet.exp > @@ -0,0 +1,59 @@ > +# Copyright (C) 2021 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 the gdb.RemoteTargetConnection.send_packet API. This is done > +# by connecting to a remote target and fetching the thread list in two > +# ways, first, we manually send the packets required to read the > +# thread list using gdb.TargetConnection.send_packet, then we compare > +# the results to the thread list using the standard API calls. > + > +load_lib gdb-python.exp > +load_lib gdbserver-support.exp > + > +standard_testfile > + > +if {[skip_gdbserver_tests]} { > + return 0 > +} > + > +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { > + return -1 > +} > + > +gdb_exit > +gdb_start You could avoid an unnecessary starting and exiting GDB by using build_executable instead of prepare_for_testing. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-10-21 2:43 ` Simon Marchi @ 2021-10-22 11:08 ` Andrew Burgess 2021-10-22 11:18 ` Simon Marchi 0 siblings, 1 reply; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 11:08 UTC (permalink / raw) To: Simon Marchi; +Cc: gdb-patches Thanks for the review. I've posted a new version of the patch, but I wanted to reply to one comment specifically, which I've done inline below. * Simon Marchi <simon.marchi@polymtl.ca> [2021-10-20 22:43:01 -0400]: > > > On 2021-10-19 06:17, Andrew Burgess wrote: > > This commits adds a new sub-class of gdb.TargetConnection, > > gdb.RemoteTargetConnection. This sub-class is created for all > > 'remote' and 'extended-remote' targets. > > > > This new sub-class has one additional method over its base class, > > 'send_packet'. This new method is equivalent to the 'maint > > packet' CLI command, it allows a custom packet to be sent to a remote > > target. > > > > The result of calling RemoteTargetConnection.send_packet is a string > > containing the reply that came from the remote. > > --- > > gdb/NEWS | 8 +- > > gdb/doc/gdb.texinfo | 1 + > > gdb/doc/python.texi | 28 +++- > > gdb/python/py-connection.c | 163 +++++++++++++++++++- > > gdb/testsuite/gdb.python/py-send-packet.c | 22 +++ > > gdb/testsuite/gdb.python/py-send-packet.exp | 59 +++++++ > > gdb/testsuite/gdb.python/py-send-packet.py | 84 ++++++++++ > > 7 files changed, 358 insertions(+), 7 deletions(-) > > create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c > > create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp > > create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py > > > > diff --git a/gdb/NEWS b/gdb/NEWS > > index 43b81df526d..78ac8beb7fd 100644 > > --- a/gdb/NEWS > > +++ b/gdb/NEWS > > @@ -46,7 +46,9 @@ maint show internal-warning backtrace > > before GDB starts to clean up its internal state. > > > > ** New gdb.TargetConnection object type that represents a connection > > - (as displayed by the 'info connections' command). > > + (as displayed by the 'info connections' command). A sub-class, > > + gdb.RemoteTargetConnection, is used to represent 'remote' and > > + 'extended-remote' connections. > > > > ** The gdb.Inferior type now has a 'connection' property which is an > > instance of gdb.TargetConnection, the connection used by this > > @@ -61,6 +63,10 @@ maint show internal-warning backtrace > > ** New gdb.connections() function that returns a list of all > > currently active connections. > > > > + ** New gdb.RemoteTargetConnection.send_packet(STRING) method. This > > + is equivalent to the existing 'maint packet' CLI command; it > > + allows a user specified packet to be sent to the remote target. > > + > > *** Changes in GDB 11 > > > > * The 'set disassembler-options' command now supports specifying options > > diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo > > index 631a7c03b31..89cf86210ab 100644 > > --- a/gdb/doc/gdb.texinfo > > +++ b/gdb/doc/gdb.texinfo > > @@ -39269,6 +39269,7 @@ > > error stream. This is @samp{on} by default for @code{internal-error} > > and @samp{off} by default for @code{internal-warning}. > > > > +@anchor{maint packet} > > @kindex maint packet > > @item maint packet @var{text} > > If @value{GDBN} is talking to an inferior via the serial protocol, > > diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi > > index 599c411b11d..97c1dd595bf 100644 > > --- a/gdb/doc/python.texi > > +++ b/gdb/doc/python.texi > > @@ -5980,9 +5980,9 @@ > > Examples of different connection types are @samp{native} and > > @samp{remote}. @xref{Inferiors Connections and Programs}. > > > > -@value{GDBN} uses the @code{gdb.TargetConnection} object type to > > -represent a connection in Python code. To get a list of all > > -connections use @code{gdb.connections} > > +Connections in @value{GDBN} are represented as instances of > > +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a > > +list of all connections use @code{gdb.connections} > > (@pxref{gdbpy_connections,,gdb.connections}). > > > > To get the connection for a single @code{gdb.Inferior} read its > > @@ -6034,6 +6034,28 @@ > > to the remote target. > > @end defvar > > > > +The @code{gdb.RemoteTargetConnection} class is a sub-class of > > +@code{gdb.TargetConnection}, and is used to represent @samp{remote} > > +and @samp{extended-remote} connections. In addition to the attributes > > +and methods available from the @code{gdb.TargetConnection} base class, > > +a @code{gdb.RemoteTargetConnection} has the following additional method: > > + > > +@kindex maint packet > > +@defun RemoteTargetConnection.send_packet (@var{packet}) > > +This method sends @var{packet}, which should be a non-empty string, to > > +the remote target and returns the response as a string. If > > +@var{packet} is not a string, or is the empty string, then an > > +exception of type @code{gdb.error} is thrown. > > I think that the standard TypeError and ValueError would be appropriate > here. > > Can the remote return some bytes that can't be decoded as a string (like > the reply of the X packet)? In that case, maybe the return value of > send_packet should be `bytes` (at least for Python 3). > > > > @@ -80,8 +85,15 @@ target_to_connection_object (process_stratum_target *target) > > gdbpy_ref <connection_object> conn_obj = all_connection_objects[target]; > > if (conn_obj == nullptr) > > { > > - conn_obj.reset (PyObject_New (connection_object, > > - &connection_object_type)); > > + PyTypeObject *type; > > + > > + std::string connection_type = target->shortname (); > > + if (connection_type == "remote" || connection_type == "extended-remote") > > + type = &remote_connection_object_type; > > + else > > + type = &connection_object_type; > > I imagined that we could use a dynamic cast here, same as we do in > get_current_remote_target: > > dynamic_cast<remote_target *> (target) This is what I originally wanted to do. The problem is that this requires that the remote_target type be visible inside py-connection.c, and currently remote_target is defined in remote.c. We could move the declaration of remote_target, but in the end I figured that the string comparison was just as good, and I didn't see the names changing - and even if they did, we can fix py-connections.c with the new names, and this wouldn't break the external API. Thanks, Andrew > > But, comparing the strings works too, it's not like these names are > going to change. > > > +/* Implement RemoteTargetConnection.send_packet function. Send a packet to > > + the target identified by SELF. The connection must still be valid, and > > + the packet to be sent must be non-empty, otherwise an exception will be > > + thrown. */ > > + > > +static PyObject * > > +connpy_send_packet (PyObject *self, PyObject *args, PyObject *kw) > > +{ > > + connection_object *conn = (connection_object *) self; > > + > > + CONNPY_REQUIRE_VALID (conn); > > + > > + static const char *keywords[] = {"packet", nullptr}; > > + const char *packet_str = nullptr; > > + > > + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s", keywords, > > + &packet_str)) > > + return nullptr; > > + > > + if (packet_str == nullptr || *packet_str == '\0') > > + { > > + PyErr_SetString (PyExc_ValueError, _("Invalid remote packet")); > > + return nullptr; > > + } > > + > > + try > > + { > > + scoped_restore_current_thread restore_thread; > > + switch_to_target_no_thread (conn->target); > > + > > + py_send_packet_callbacks callbacks; > > + send_remote_packet (packet_str, &callbacks); > > + return callbacks.result ().release (); > > Looking at py_send_packet_callbacks::received, it looks like m_result > can stay nullptr. So here, we would return nullptr, which indicates to > the Python interpreter that we are throwing an exception. But are we > sure the error indicator has been set? If PyUnicode_Decode fails, I > suppose it will. But if m_result stays nullptr because buf is empty, I > guess we'll return nullptr without setting the error indicator. > > > diff --git a/gdb/testsuite/gdb.python/py-send-packet.c b/gdb/testsuite/gdb.python/py-send-packet.c > > new file mode 100644 > > index 00000000000..9811b15f06d > > --- /dev/null > > +++ b/gdb/testsuite/gdb.python/py-send-packet.c > > @@ -0,0 +1,22 @@ > > +/* This testcase is part of GDB, the GNU debugger. > > + > > + Copyright 2021 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/>. */ > > + > > +int > > +main () > > main (void) > > > +{ > > + return 0; > > +} > > diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp > > new file mode 100644 > > index 00000000000..89ab28d003b > > --- /dev/null > > +++ b/gdb/testsuite/gdb.python/py-send-packet.exp > > @@ -0,0 +1,59 @@ > > +# Copyright (C) 2021 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 the gdb.RemoteTargetConnection.send_packet API. This is done > > +# by connecting to a remote target and fetching the thread list in two > > +# ways, first, we manually send the packets required to read the > > +# thread list using gdb.TargetConnection.send_packet, then we compare > > +# the results to the thread list using the standard API calls. > > + > > +load_lib gdb-python.exp > > +load_lib gdbserver-support.exp > > + > > +standard_testfile > > + > > +if {[skip_gdbserver_tests]} { > > + return 0 > > +} > > + > > +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { > > + return -1 > > +} > > + > > +gdb_exit > > +gdb_start > > You could avoid an unnecessary starting and exiting GDB by using > build_executable instead of prepare_for_testing. > > Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-10-22 11:08 ` Andrew Burgess @ 2021-10-22 11:18 ` Simon Marchi 2021-10-22 17:11 ` Andrew Burgess 0 siblings, 1 reply; 52+ messages in thread From: Simon Marchi @ 2021-10-22 11:18 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches On 2021-10-22 07:08, Andrew Burgess wrote:> This is what I originally wanted to do. The problem is that this > requires that the remote_target type be visible inside > py-connection.c, and currently remote_target is defined in remote.c. > > We could move the declaration of remote_target, but in the end I > figured that the string comparison was just as good, and I didn't see > the names changing - and even if they did, we can fix py-connections.c > with the new names, and this wouldn't break the external API. Well, you could add a: bool is_remote_target (process_stratum_target *target); function implemented in remote.c. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-10-22 11:18 ` Simon Marchi @ 2021-10-22 17:11 ` Andrew Burgess 0 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 17:11 UTC (permalink / raw) To: Simon Marchi; +Cc: gdb-patches * Simon Marchi <simon.marchi@polymtl.ca> [2021-10-22 07:18:35 -0400]: > On 2021-10-22 07:08, Andrew Burgess wrote:> This is what I originally wanted to do. The problem is that this > > requires that the remote_target type be visible inside > > py-connection.c, and currently remote_target is defined in remote.c. > > > > We could move the declaration of remote_target, but in the end I > > figured that the string comparison was just as good, and I didn't see > > the names changing - and even if they did, we can fix py-connections.c > > with the new names, and this wouldn't break the external API. > > Well, you could add a: > > bool is_remote_target (process_stratum_target *target); > > function implemented in remote.c. Good idea. Done in a v5 of this series. Thanks, Andrew ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv4 0/4] Python API for target connections, and packet sending 2021-10-19 10:17 ` [PATCHv3 0/3] Python API for target connections, and packet sending Andrew Burgess ` (2 preceding siblings ...) 2021-10-19 10:17 ` [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess @ 2021-10-22 10:58 ` Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 1/4] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess ` (4 more replies) 3 siblings, 5 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 10:58 UTC (permalink / raw) To: gdb-patches Since v3: - Addressed all review comments. - The RemoteTargetConnection.send_packet method now returns a bytes object for Py3, and a bytesarray object for Py2. This change is in patch #3. - New patch #4 fixes GDB so that we can correctly return packets containing null bytes, this fixes both 'maint packet' and the Python API. Since v2: - Added a gdb.RemoteTargetConnection sub-class, which is used for 'remote' and 'extended-remote' connections. The previous 'send_remote_packet' has become 'send_packet' on this new sub-class. - Tests and documentation have been updated to reflect the above change. - In the first patch, connpy_repr has been modified in order to prepare it for the sub-class that will appear in the later patch. Since v1: - Rebased onto current upstream master and resolved the merge conflicts. - Addressed the documentation feedback from Eli. I'm not 100% sure that I have correctly addressed the concerns about patch #3, so this is probably worth rechecking. --- Andrew Burgess (4): gdb/python: introduce gdb.TargetConnection object type gdb: make packet_command function available outside remote.c gdb/python: add gdb.RemoteTargetConnection.send_packet gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet gdb/Makefile.in | 1 + gdb/NEWS | 21 + gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 127 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 537 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/remote.c | 104 ++-- gdb/remote.h | 34 ++ gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 ++ gdb/testsuite/gdb.python/py-connection.c | 22 + gdb/testsuite/gdb.python/py-connection.exp | 75 +++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- gdb/testsuite/gdb.python/py-send-packet.c | 22 + gdb/testsuite/gdb.python/py-send-packet.exp | 89 +++ gdb/testsuite/gdb.python/py-send-packet.py | 116 ++++ 23 files changed, 1272 insertions(+), 39 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv4 1/4] gdb/python: introduce gdb.TargetConnection object type 2021-10-22 10:58 ` [PATCHv4 0/4] Python API for target connections, and packet sending Andrew Burgess @ 2021-10-22 10:58 ` Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 2/4] gdb: make packet_command function available outside remote.c Andrew Burgess ` (3 subsequent siblings) 4 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 10:58 UTC (permalink / raw) To: gdb-patches This commit adds a new object type gdb.TargetConnection. This new type represents a connection within GDB (a connection as displayed by 'info connections'). There's three ways to find a gdb.TargetConnection, there's a new 'gdb.connections()' function, which returns a list of all currently active connections. Or you can read the new 'connection' property on the gdb.Inferior object type, this contains the connection for that inferior (or None if the inferior has no connection, for example, it is exited). Finally, there's a new gdb.events.connection_removed event registry, this emits a new gdb.ConnectionEvent whenever a connection is removed from GDB (this can happen when all inferiors using a connection exit, though this is not always the case, depending on the connection type). The gdb.ConnectionEvent has a 'connection' property, which is the gdb.TargetConnection being removed from GDB. The gdb.TargetConnection has an 'is_valid()' method. A connection object becomes invalid when the underlying connection is removed from GDB (as discussed above, this might be when all inferiors using a connection exit, or it might be when the user explicitly replaces a connection in GDB by issuing another 'target' command). The gdb.TargetConnection has the following read-only properties: 'num': The number for this connection, 'type': e.g. 'native', 'remote', 'sim', etc 'description': The longer description as seen in the 'info connections' command output. 'details': A string or None. Extra details for the connection, for example, a remote connection's details might be 'hostname:port'. --- gdb/Makefile.in | 1 + gdb/NEWS | 15 + gdb/doc/python.texi | 93 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 366 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 +++ gdb/testsuite/gdb.python/py-connection.c | 22 ++ gdb/testsuite/gdb.python/py-connection.exp | 69 ++++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- 17 files changed, 724 insertions(+), 4 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp diff --git a/gdb/Makefile.in b/gdb/Makefile.in index 4201f65e68d..0e7ab2d2b7a 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -391,6 +391,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-bpevent.c \ python/py-breakpoint.c \ python/py-cmd.c \ + python/py-connection.c \ python/py-continueevent.c \ python/py-event.c \ python/py-evtregistry.c \ diff --git a/gdb/NEWS b/gdb/NEWS index bd26d2b1ec2..3bd13993005 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -45,6 +45,21 @@ maint show internal-warning backtrace event is triggered once GDB decides it is going to exit, but before GDB starts to clean up its internal state. + ** New gdb.TargetConnection object type that represents a connection + (as displayed by the 'info connections' command). + + ** The gdb.Inferior type now has a 'connection' property which is an + instance of gdb.TargetConnection, the connection used by this + inferior. This can be None if the inferior has no connection. + + ** New 'gdb.events.connection_removed' event registry, which emits a + 'gdb.ConnectionEvent' when a connection is removed from GDB. + This event has a 'connection' property, a gdb.TargetConnection + object for the connection being removed. + + ** New gdb.connections() function that returns a list of all + currently active connections. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 15bf9dc3e21..e62e2703b85 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -220,6 +220,7 @@ * Lazy Strings In Python:: Python representation of lazy strings. * Architectures In Python:: Python representation of architectures. * Registers In Python:: Python representation of registers. +* Connections In Python:: Python representation of connections. * TUI Windows In Python:: Implementing new TUI windows. @end menu @@ -557,6 +558,13 @@ related prompts are prohibited from being changed. @end defun +@anchor{gdbpy_connections} +@defun gdb.connections +Return a list of @code{gdb.TargetConnection} objects, one for each +currently active connection (@pxref{Connections In Python}). The +connection objects are in no particular order in the returned list. +@end defun + @node Exception Handling @subsubsection Exception Handling @cindex python exceptions @@ -3084,10 +3092,18 @@ ID of inferior, as assigned by GDB. @end defvar +@anchor{gdbpy_inferior_connection} +@defvar Inferior.connection +The @code{gdb.TargetConnection} for this inferior (@pxref{Connections +In Python}), or @code{None} if this inferior has no connection. +@end defvar + @defvar Inferior.connection_num ID of inferior's connection as assigned by @value{GDBN}, or None if -the inferior is not connected to a target. -@xref{Inferiors Connections and Programs}. +the inferior is not connected to a target. @xref{Inferiors Connections +and Programs}. This is equivalent to +@code{gdb.Inferior.connection.num} in the case where +@code{gdb.Inferior.connection} is not @code{None}. @end defvar @defvar Inferior.pid @@ -3428,6 +3444,15 @@ An integer, the value of the exit code @value{GDBN} will return. @end defvar +@item events.connection_removed +This is emitted when @value{GDBN} removes a connection +(@pxref{Connections In Python}). The event is of type +@code{gdb.ConnectionEvent}. This has a single read-only attribute: + +@defvar ConnectionEvent.connection +The @code{gdb.TargetConnection} that is being removed. +@end defvar + @end table @node Threads In Python @@ -5946,6 +5971,70 @@ A string that is the name of this register group. @end defvar +@node Connections In Python +@subsubsection Connections In Python +@cindex connections in python +@value{GDBN} lets you run and debug multiple programs in a single +session. Each program being debugged has a connection, the connection +describes how @value{GDBN} controls the program being debugged. +Examples of different connection types are @samp{native} and +@samp{remote}. @xref{Inferiors Connections and Programs}. + +@value{GDBN} uses the @code{gdb.TargetConnection} object type to +represent a connection in Python code. To get a list of all +connections use @code{gdb.connections} +(@pxref{gdbpy_connections,,gdb.connections}). + +To get the connection for a single @code{gdb.Inferior} read its +@code{gdb.Inferior.connection} attribute +(@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). + +A @code{gdb.TargetConnection} has the following method: + +@defun TargetConnection.is_valid () +Return @code{True} if the @code{gdb.TargetConnection} object is valid, +@code{False} if not. A @code{gdb.TargetConnection} will become +invalid if the connection no longer exists within @value{GDBN}, this +might happen when no inferiors are using the connection, but could be +delayed until the user replaces the current target. + +Reading any of the @code{gdb.TargetConnection} properties will throw +an exception if the connection is invalid. +@end defun + +A @code{gdb.TargetConnection} has the following read-only properties: + +@defvar TargetConnection.num +An integer assigned by @value{GDBN} to uniquely identify this +connection. This is the same value as displayed in the @samp{Num} +column of the @code{info connections} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.type +A string that describes what type of connection this is. This string +will be one of the valid names that can be passed to the @code{target} +command (@pxref{Target Commands,,target command}). +@end defvar + +@defvar TargetConnection.description +A string that gives a short description of this target type. This is +the same string that is displayed in the @samp{Description} column of +the @code{info connection} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.details +An optional string that gives additional information about this +connection. This attribute can be @code{None} if there are no +additional details for this connection. + +An example of a connection type that might have additional details is +the @samp{remote} connection, in this case the details string can +contain the @samp{@var{hostname}:@var{port}} that was used to connect +to the remote target. +@end defvar + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/observable.c b/gdb/observable.c index b020076cf26..6e91d7bbc4a 100644 --- a/gdb/observable.c +++ b/gdb/observable.c @@ -78,6 +78,7 @@ DEFINE_OBSERVABLE (user_selected_context_changed); DEFINE_OBSERVABLE (source_styling_changed); DEFINE_OBSERVABLE (current_source_symtab_and_line_changed); DEFINE_OBSERVABLE (gdb_exiting); +DEFINE_OBSERVABLE (connection_removed); } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/observable.h b/gdb/observable.h index f20f532870f..707f6c91c12 100644 --- a/gdb/observable.h +++ b/gdb/observable.h @@ -253,6 +253,9 @@ extern observable<> current_source_symtab_and_line_changed; /* Called when GDB is about to exit. */ extern observable<int> gdb_exiting; +/* When a connection is removed. */ +extern observable<process_stratum_target */* target */> connection_removed; + } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def index 83f10989e4a..ed22a3335cd 100644 --- a/gdb/python/py-all-events.def +++ b/gdb/python/py-all-events.def @@ -39,3 +39,4 @@ GDB_PY_DEFINE_EVENT(breakpoint_deleted) GDB_PY_DEFINE_EVENT(breakpoint_modified) GDB_PY_DEFINE_EVENT(before_prompt) GDB_PY_DEFINE_EVENT(gdb_exiting) +GDB_PY_DEFINE_EVENT(connection_removed) diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c new file mode 100644 index 00000000000..f1dfa26e39c --- /dev/null +++ b/gdb/python/py-connection.c @@ -0,0 +1,366 @@ +/* Python interface to inferiors. + + Copyright (C) 2009-2021 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/>. */ + +#include "defs.h" +#include "python-internal.h" +#include "process-stratum-target.h" +#include "inferior.h" +#include "observable.h" +#include "target-connection.h" +#include "py-events.h" +#include "py-event.h" +#include "arch-utils.h" + +#include <map> + +/* The Python object that represents a connection. */ + +struct connection_object +{ + PyObject_HEAD + + /* The process target that represents this connection. When a + connection_object is created this field will always point at a valid + target. Later, if GDB stops using this target (the target is popped + from all target stacks) then this field is set to nullptr, which + indicates that this Python object is now in the invalid state (see + the is_valid() method below). */ + struct process_stratum_target *target; +}; + +extern PyTypeObject connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); + +/* Require that CONNECTION be valid. */ +#define CONNPY_REQUIRE_VALID(connection) \ + do { \ + if (connection->target == nullptr) \ + { \ + PyErr_SetString (PyExc_RuntimeError, \ + _("Connection no longer exists.")); \ + return nullptr; \ + } \ + } while (0) + +/* A map between process_stratum targets and the Python object representing + them. We actually hold a gdbpy_ref around the Python object so that + reference counts are handled correctly when entries are deleted. */ +static std::map<process_stratum_target *, + gdbpy_ref<connection_object>> all_connection_objects; + +/* Return a reference to a gdb.TargetConnection object for TARGET. If + TARGET is nullptr then a reference to None is returned. + + Previously created gdb.TargetConnection objects are cached, and + additional references to the same connection object can be returned with + later calls to this function. */ + +gdbpy_ref<> +target_to_connection_object (process_stratum_target *target) +{ + if (target == nullptr) + return gdbpy_ref<>::new_reference (Py_None); + + gdbpy_ref <connection_object> conn_obj; + auto conn_obj_iter = all_connection_objects.find (target); + if (conn_obj_iter == all_connection_objects.end ()) + { + conn_obj.reset (PyObject_New (connection_object, + &connection_object_type)); + if (conn_obj == nullptr) + return nullptr; + conn_obj->target = target; + all_connection_objects.emplace (target, conn_obj); + } + else + conn_obj = conn_obj_iter->second; + + gdb_assert (conn_obj != nullptr); + + /* Repackage the result as a PyObject reference. */ + return gdbpy_ref<> ((PyObject *) conn_obj.release ()); +} + +/* Return a list of gdb.TargetConnection objects, one for each currently + active connection. The returned list is in no particular order. */ + +PyObject * +gdbpy_connections (PyObject *self, PyObject *args) +{ + gdbpy_ref<> list (PyList_New (0)); + if (list == nullptr) + return nullptr; + + for (process_stratum_target *target : all_non_exited_process_targets ()) + { + gdb_assert (target != nullptr); + + gdbpy_ref<> conn = target_to_connection_object (target); + if (conn == nullptr) + return nullptr; + gdb_assert (conn.get () != Py_None); + + if (PyList_Append (list.get (), conn.get ()) < 0) + return nullptr; + } + + return list.release (); +} + +/* Emit a connection event for TARGET to REGISTRY. Return 0 on success, or + a negative value on error. */ + +static int +emit_connection_event (process_stratum_target *target, + eventregistry_object *registry) +{ + gdbpy_ref<> event_obj + = create_event_object (&connection_event_object_type); + if (event_obj == nullptr) + return -1; + + gdbpy_ref<> conn = target_to_connection_object (target); + if (evpy_add_attribute (event_obj.get (), "connection", conn.get ()) < 0) + return -1; + + return evpy_emit_event (event_obj.get (), registry); +} + +/* Callback for the connection_removed observer. */ + +static void +connpy_connection_removed (process_stratum_target *target) +{ + if (!gdb_python_initialized) + return; + + gdbpy_enter enter_py (get_current_arch (), current_language); + + if (!evregpy_no_listeners_p (gdb_py_events.connection_removed)) + if (emit_connection_event (target, gdb_py_events.connection_removed) < 0) + gdbpy_print_stack (); + + auto conn_obj_iter = all_connection_objects.find (target); + if (conn_obj_iter != all_connection_objects.end ()) + { + gdbpy_ref <connection_object> conn_obj = conn_obj_iter->second; + conn_obj->target = nullptr; + all_connection_objects.erase (target); + } +} + +/* Called when a gdb.TargetConnection object is deallocated. */ + +static void +connpy_connection_dealloc (PyObject *obj) +{ + connection_object *conn_obj = (connection_object *) obj; + + /* As the all_connection_objects map holds a reference to each connection + object we can only enter the dealloc function when the reference in + all_connection_objects has been erased. + + As we always set the target pointer back to nullptr before we erase + items from all_connection_objects then, when we get here, the target + pointer must be nullptr. */ + gdb_assert (conn_obj->target == nullptr); + + Py_TYPE (obj)->tp_free (obj); +} + +/* Implement repr() for gdb.TargetConnection. */ + +static PyObject * +connpy_repr (PyObject *obj) +{ + connection_object *self = (connection_object *) obj; + process_stratum_target *target = self->target; + + if (target == nullptr) + return PyString_FromFormat ("<%s (invalid)>", Py_TYPE (obj)->tp_name); + + return PyString_FromFormat ("<%s num=%d, what=\"%s\">", + Py_TYPE (obj)->tp_name, + target->connection_number, + make_target_connection_string (target).c_str ()); +} + +/* Implementation of gdb.TargetConnection.is_valid() -> Boolean. Returns + True if this connection object is still associated with a + process_stratum_target, otherwise, returns False. */ + +static PyObject * +connpy_is_valid (PyObject *self, PyObject *args) +{ + connection_object *conn = (connection_object *) self; + + if (conn->target == nullptr) + Py_RETURN_FALSE; + + Py_RETURN_TRUE; +} + +/* Return the id number of this connection. */ + +static PyObject * +connpy_get_connection_num (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + auto num = conn->target->connection_number; + return gdb_py_object_from_longest (num).release (); +} + +/* Return a string that gives the short name for this connection type. */ + +static PyObject * +connpy_get_connection_type (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *shortname = conn->target->shortname (); + return host_string_to_python_string (shortname).release (); +} + +/* Return a string that gives a longer description of this connection type. */ + +static PyObject * +connpy_get_description (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *longname = conn->target->longname (); + return host_string_to_python_string (longname).release (); +} + +/* Return a string that gives additional details about this connection, or + None, if there are no additional details for this connection type. */ + +static PyObject * +connpy_get_connection_details (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *details = conn->target->connection_string (); + if (details != nullptr) + return host_string_to_python_string (details).release (); + else + Py_RETURN_NONE; +} + +/* Python specific initialization for this file. */ + +int +gdbpy_initialize_connection (void) +{ + if (PyType_Ready (&connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "TargetConnection", + (PyObject *) &connection_object_type) < 0) + return -1; + + return 0; +} + +/* Global initialization for this file. */ + +void _initialize_py_connection (); +void +_initialize_py_connection () +{ + gdb::observers::connection_removed.attach (connpy_connection_removed, + "py-connection"); +} + +/* Methods for the gdb.TargetConnection object type. */ + +static PyMethodDef connection_object_methods[] = +{ + { "is_valid", connpy_is_valid, METH_NOARGS, + "is_valid () -> Boolean.\n\ +Return true if this TargetConnection is valid, false if not." }, + { NULL } +}; + +/* Attributes for the gdb.TargetConnection object type. */ + +static gdb_PyGetSetDef connection_object_getset[] = +{ + { "num", connpy_get_connection_num, NULL, + "ID number of this connection, as assigned by GDB.", NULL }, + { "type", connpy_get_connection_type, NULL, + "A short string that is the name for this connection type.", NULL }, + { "description", connpy_get_description, NULL, + "A longer string describing this connection type.", NULL }, + { "details", connpy_get_connection_details, NULL, + "A string containing additional connection details.", NULL }, + { NULL } +}; + +/* Define the gdb.TargetConnection object type. */ + +PyTypeObject connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.TargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + connection_object_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def index aeaee02e8bb..6a49f2aa896 100644 --- a/gdb/python/py-event-types.def +++ b/gdb/python/py-event-types.def @@ -110,3 +110,8 @@ GDB_PY_DEFINE_EVENT_TYPE (gdb_exiting, "GdbExitingEvent", "GDB is about to exit", event_object_type); + +GDB_PY_DEFINE_EVENT_TYPE (connection, + "ConnectionEvent", + "GDB connection added or removed object", + event_object_type); diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c index c8de41dd009..143f3492928 100644 --- a/gdb/python/py-inferior.c +++ b/gdb/python/py-inferior.c @@ -427,6 +427,20 @@ infpy_get_num (PyObject *self, void *closure) return gdb_py_object_from_longest (inf->inferior->num).release (); } +/* Return the gdb.TargetConnection object for this inferior, or None if a + connection does not exist. */ + +static PyObject * +infpy_get_connection (PyObject *self, void *closure) +{ + inferior_object *inf = (inferior_object *) self; + + INFPY_REQUIRE_VALID (inf); + + process_stratum_target *target = inf->inferior->process_target (); + return target_to_connection_object (target).release (); +} + /* Return the connection number of the given inferior, or None if a connection does not exist. */ @@ -968,6 +982,8 @@ gdbpy_initialize_inferior (void) static gdb_PyGetSetDef inferior_object_getset[] = { { "num", infpy_get_num, NULL, "ID of inferior, as assigned by GDB.", NULL }, + { "connection", infpy_get_connection, NULL, + "The gdb.TargetConnection for this inferior.", NULL }, { "connection_num", infpy_get_connection_num, NULL, "ID of inferior's connection, as assigned by GDB.", NULL }, { "pid", infpy_get_pid, NULL, "PID of inferior, as assigned by the OS.", diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h index 022d4a67172..e0c8d867e0d 100644 --- a/gdb/python/python-internal.h +++ b/gdb/python/python-internal.h @@ -478,6 +478,10 @@ gdbpy_ref<thread_object> create_thread_object (struct thread_info *tp); gdbpy_ref<> thread_to_thread_object (thread_info *thr);; gdbpy_ref<inferior_object> inferior_to_inferior_object (inferior *inf); +struct process_stratum_target; +gdbpy_ref<> target_to_connection_object (process_stratum_target *target); +PyObject *gdbpy_connections (PyObject *self, PyObject *args); + const struct block *block_object_to_block (PyObject *obj); struct symbol *symbol_object_to_symbol (PyObject *obj); struct value *value_object_to_value (PyObject *self); @@ -549,6 +553,8 @@ int gdbpy_initialize_unwind (void) CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; int gdbpy_initialize_tui () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; +int gdbpy_initialize_connection () + CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; /* A wrapper for PyErr_Fetch that handles reference counting for the caller. */ diff --git a/gdb/python/python.c b/gdb/python/python.c index 264f7c88ed6..c12e1aade18 100644 --- a/gdb/python/python.c +++ b/gdb/python/python.c @@ -1876,6 +1876,7 @@ do_start_initialization () || gdbpy_initialize_registers () < 0 || gdbpy_initialize_xmethods () < 0 || gdbpy_initialize_unwind () < 0 + || gdbpy_initialize_connection () < 0 || gdbpy_initialize_tui () < 0) return false; @@ -2299,6 +2300,10 @@ Set the value of the convenience variable $NAME." }, Register a TUI window constructor." }, #endif /* TUI */ + { "connections", gdbpy_connections, METH_NOARGS, + "connections () -> List.\n\ +Return a list of gdb.TargetConnection objects." }, + {NULL, NULL, 0, NULL} }; diff --git a/gdb/target-connection.c b/gdb/target-connection.c index a649423e07e..da1c9da74dc 100644 --- a/gdb/target-connection.c +++ b/gdb/target-connection.c @@ -24,6 +24,7 @@ #include "inferior.h" #include "target.h" +#include "observable.h" /* A map between connection number and representative process_stratum target. */ @@ -49,6 +50,9 @@ connection_list_add (process_stratum_target *t) void connection_list_remove (process_stratum_target *t) { + /* Notify about the connection being removed before we reset the + connection number to zero. */ + gdb::observers::connection_removed.notify (t); process_targets.erase (t->connection_number); t->connection_number = 0; } diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp index 3fd6b15cdd9..3f74a47de31 100644 --- a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp @@ -15,18 +15,27 @@ # Test "info inferiors" and "info connections" with multiple targets. +load_lib gdb-python.exp + source $srcdir/$subdir/multi-target.exp.tcl if {![multi_target_prepare]} { return } +# Cache the result of calling skip_python_tests into a local variable. +set run_python_tests [expr ! [skip_python_tests]] + # Test "info inferiors" and "info connections". MULTI_PROCESS # indicates whether the multi-process feature of remote targets is # turned off or on. proc test_info_inferiors {multi_process} { setup "off" + if { $::run_python_tests } { + gdb_test_no_output "source ${::remote_python_file}" "load python file" + } + gdb_test_no_output \ "set remote multiprocess-feature-packet $multi_process" @@ -86,6 +95,18 @@ proc test_info_inferiors {multi_process} { "[connection_num 5 $inf]${ws}core${ws}Local core dump file${ws}" \ ] + if { $::run_python_tests } { + gdb_test "python info_connections()" \ + [multi_line \ + "Num${ws}What${ws}Description" \ + "[connection_num 1 $inf]${ws}native${ws}Native process" \ + "[connection_num 2 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 3 $inf]${ws}core${ws}Local core dump file" \ + "[connection_num 4 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 5 $inf]${ws}core${ws}Local core dump file" \ + ] + } + gdb_test "info inferiors" \ [multi_line \ "Num${ws}Description${ws}Connection${ws}Executable${ws}" \ @@ -96,10 +117,27 @@ proc test_info_inferiors {multi_process} { "[inf_desc 5 $inf]4 \\(extended-remote localhost:$decimal\\)${ws}${binfile}${ws}" \ "[inf_desc 6 $inf]5 \\(core\\)${ws}${binfile}${ws}" \ ] + + if { $::run_python_tests } { + gdb_test "python info_inferiors()" \ + [multi_line \ + "Inferior 1, Connection #1: native" \ + "Inferior 2, Connection #2: extended-remote localhost:$decimal" \ + "Inferior 3, Connection #3: core" \ + "Inferior 4, Connection #1: native" \ + "Inferior 5, Connection #4: extended-remote localhost:$decimal" \ + "Inferior 6, Connection #5: core" \ + ] + } } } } +if { $run_python_tests } { + set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +} + # Test "info inferiors" and "info connections" commands. with_test_prefix "info-inferiors" { foreach_with_prefix multi_process {"on" "off"} { diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py new file mode 100644 index 00000000000..aac561ae683 --- /dev/null +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py @@ -0,0 +1,63 @@ +# Copyright (C) 2021 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 + +# Take a gdb.TargetConnection and return the connection number. +def conn_num(c): + return c.num + +# Takes a gdb.TargetConnection and return a string that is either the +# type, or the type and details (if the details are not None). +def make_target_connection_string(c): + if c.details is None: + return c.type + else: + return "%s %s" % (c.type, c.details) + +# A Python implementation of 'info connections'. Produce output that +# is identical to the output of 'info connections' so we can check +# that aspects of gdb.TargetConnection work correctly. +def info_connections(): + all_connections = sorted(gdb.connections(), key=conn_num) + current_conn = gdb.selected_inferior().connection + what_width = 0 + for c in all_connections: + s = make_target_connection_string(c) + if len(s) > what_width: + what_width = len(s) + + fmt = " Num %%-%ds Description" % what_width + print(fmt % "What") + fmt = "%%s%%-3d %%-%ds %%s" % what_width + for c in all_connections: + if c == current_conn: + prefix = "* " + else: + prefix = " " + + print(fmt % (prefix, c.num, make_target_connection_string(c), + c.description)) + +def inf_num(i): + return i.num + +# .... +def info_inferiors(): + all_inferiors = sorted(gdb.inferiors(), key=inf_num) + for i in gdb.inferiors(): + print("Inferior %d, Connection #%d: %s" % + (i.num, i.connection_num, + make_target_connection_string(i.connection))) diff --git a/gdb/testsuite/gdb.python/py-connection.c b/gdb/testsuite/gdb.python/py-connection.c new file mode 100644 index 00000000000..4b32e704476 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp new file mode 100644 index 00000000000..b805b052f73 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -0,0 +1,69 @@ +# Copyright (C) 2021 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/>. + +# This file is for testing the gdb.TargetConnection API. This API is +# already tested in gdb.multi/multi-target-info-inferiors.exp and +# gdb.python/py-inferior.exp, this file just covers some edge cases +# that are not tested in other places. + +load_lib gdb-python.exp + +standard_testfile + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +# Skip all tests if Python scripting is not enabled. +if { [skip_python_tests] } { continue } + +if ![runto_main] then { + return 0 +} + +# Create a gdb.TargetConnection object and check it is initially +# valid. +gdb_test_no_output "python conn = gdb.selected_inferior().connection" +gdb_test "python print(conn)" \ + "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ + "print gdb.TargetConnection while it is still valid" +gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" + +# Get the connection again, and ensure we get the exact same object. +gdb_test_no_output "python conn2 = gdb.selected_inferior().connection" +gdb_test "python print('Same object: %s' % (conn is conn2))" "True" + +# Now invalidate the connection, and ensure that the is_valid method +# starts to return False. +gdb_test "info connections" "\r\n\\* 1 .*" \ + "info connections while the connection is still around" +gdb_test "disconnect" "" "kill the inferior" \ + "A program is being debugged already\\. Kill it\\? .*y or n. $" "y" +gdb_test "info connections" "No connections\\." \ + "info connections now all the connections have gone" +gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ + "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" + +# Now check that accessing properties of the invalid connection cases +# an error. +gdb_test "python print(conn.num)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.type)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.description)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.details)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" diff --git a/gdb/testsuite/gdb.python/py-inferior.exp b/gdb/testsuite/gdb.python/py-inferior.exp index 894b90a85fc..d5d284a763e 100644 --- a/gdb/testsuite/gdb.python/py-inferior.exp +++ b/gdb/testsuite/gdb.python/py-inferior.exp @@ -51,6 +51,8 @@ gdb_py_test_silent_cmd "python i0 = inferiors\[0\]" "get first inferior" 0 gdb_test "python print ('result = %s' % (i0 == inferiors\[0\]))" " = True" "test equality comparison (true)" gdb_test "python print ('result = %s' % i0.num)" " = \[0-9\]+" "test Inferior.num" gdb_test "python print ('result = %s' % i0.connection_num)" " = \[0-9\]+" "test Inferior.connection_num" +gdb_test "python print ('result = %s' % (i0.connection_num == i0.connection.num))" " = True" \ + "Inferior.connection_num equals Inferior.connection.num" gdb_test "python print ('result = %s' % i0.pid)" " = \[0-9\]+" "test Inferior.pid" gdb_test "python print ('result = %s' % i0.was_attached)" " = False" "test Inferior.was_attached" gdb_test "python print (i0.threads ())" "\\(<gdb.InferiorThread object at 0x\[\[:xdigit:\]\]+>,\\)" "test Inferior.threads" @@ -264,6 +266,8 @@ with_test_prefix "is_valid" { "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].connection_num)" \ "RuntimeError: Inferior no longer exists.*" + gdb_test "python print (inf_list\[1\].connection)" \ + "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].pid)" \ "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].was_attached)" \ @@ -281,7 +285,9 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 1" ".*" "switch to first inferior" gdb_test "py print (gdb.selected_inferior().num)" "1" "first inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "1" \ - "first inferior's connection" + "first inferior's connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" "1" \ + "first inferior's connection number, though connection object" # Figure out if inf 1 has a native target. set inf_1_is_native [gdb_is_target_native] @@ -289,6 +295,8 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 3" ".*" "switch to third inferior" gdb_test "py print (gdb.selected_inferior().num)" "3" "third inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "None" \ + "third inferior's None connection number" + gdb_test "py print (gdb.selected_inferior().connection)" "None" \ "third inferior's None connection" gdb_test "target native" "Done. Use the \"run\" command to start a process." \ "target for the third inferior" @@ -302,7 +310,15 @@ with_test_prefix "selected_inferior" { } gdb_test "py print (gdb.selected_inferior().connection_num)" \ "$expected_connection_num" \ - "third inferior's native connection" + "third inferior's native connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" \ + "$expected_connection_num" \ + "third inferior's native connection number, though connection object" + + # Test printing of gdb.TargetConnection object. + gdb_test "py print (gdb.selected_inferior().connection)" \ + "<gdb.TargetConnection num=${expected_connection_num}, what=\"\[^\"\]+\">" \ + "print a connection object" gdb_test "inferior 1" ".*" "switch back to first inferior" gdb_test_no_output "remove-inferiors 3" "remove second inferior" -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv4 2/4] gdb: make packet_command function available outside remote.c 2021-10-22 10:58 ` [PATCHv4 0/4] Python API for target connections, and packet sending Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 1/4] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess @ 2021-10-22 10:58 ` Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess ` (2 subsequent siblings) 4 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 10:58 UTC (permalink / raw) To: gdb-patches In a later commit I will add a Python API to access the 'maint packet' functionality, that is, sending a user specified packet to the target. To make implementing this easier, this commit refactors how this command is currently implemented so that the packet_command function is now global. The new global send_remote_packet function takes an object that is an implementation of an abstract interface. Two functions within this interface are then called, one just before a packet is sent to the remote target, and one when the reply has been received from the remote target. Using an interface object in this way allows (1) for the error checking to be done before the first callback is made, this means we only print out what packet it being sent once we know we are going to actually send it, and (2) we don't need to make a copy of the reply if all we want to do is print it. The only user visible changes after this commit are the error messages, which I've changed to be less 'maint packet' command focused, this will make them (I hope) better for when send_remote_packet can be called from Python code. So: "command can only be used with remote target" Becomes: "packets can only be sent to a remote target" And: "remote-packet command requires packet text as argument" Becomes: "a remote packet must not be empty" --- gdb/remote.c | 67 +++++++++++++++++++++++++++++++++++----------------- gdb/remote.h | 34 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/gdb/remote.c b/gdb/remote.c index d5eb40ce578..237b1b21c41 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -956,8 +956,6 @@ class remote_target : public process_stratum_target bool vcont_r_supported (); - void packet_command (const char *args, int from_tty); - private: /* data fields */ /* The remote state. Don't reference this directly. Use the @@ -11615,34 +11613,59 @@ remote_target::memory_map () return result; } -static void -packet_command (const char *args, int from_tty) +/* Set of callbacks used to implement the 'maint packet' command. */ + +struct cli_packet_command_callbacks : public send_remote_packet_callbacks { - remote_target *remote = get_current_remote_target (); + /* Called before the packet is sent. PACKET_STR is the packet content + before the protocol specific prefix, suffix, and escaping is added. */ - if (remote == nullptr) - error (_("command can only be used with remote target")); + void sending (const char *packet_str) override + { + puts_filtered ("sending: "); + print_packet (packet_str); + puts_filtered ("\n"); + } - remote->packet_command (args, from_tty); -} + /* Called with BUF, the reply from the remote target. */ + + void received (const gdb::char_vector &buf) override + { + puts_filtered ("received: "); + print_packet (buf.data ()); + puts_filtered ("\n"); + } +}; + +/* See remote.h. */ void -remote_target::packet_command (const char *args, int from_tty) +send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks) { - if (!args) - error (_("remote-packet command requires packet text as argument")); + if (packet_str == nullptr || *packet_str == '\0') + error (_("a remote packet must not be empty")); - puts_filtered ("sending: "); - print_packet (args); - puts_filtered ("\n"); - putpkt (args); + remote_target *remote = get_current_remote_target (); + if (remote == nullptr) + error (_("packets can only be sent to a remote target")); - remote_state *rs = get_remote_state (); + callbacks->sending (packet_str); - getpkt (&rs->buf, 0); - puts_filtered ("received: "); - print_packet (rs->buf.data ()); - puts_filtered ("\n"); + remote->putpkt (packet_str); + remote_state *rs = remote->get_remote_state (); + remote->getpkt (&rs->buf, 0); + + callbacks->received (rs->buf); +} + +/* Entry point for the 'maint packet' command. */ + +static void +cli_packet_command (const char *args, int from_tty) +{ + cli_packet_command_callbacks cb; + send_remote_packet (args, &cb); } #if 0 @@ -14921,7 +14944,7 @@ Argument is a single section name (default: all loaded sections).\n\ To compare only read-only loaded sections, specify the -r option."), &cmdlist); - add_cmd ("packet", class_maintenance, packet_command, _("\ + add_cmd ("packet", class_maintenance, cli_packet_command, _("\ Send an arbitrary packet to a remote target.\n\ maintenance packet TEXT\n\ If GDB is talking to an inferior via the GDB serial protocol, then\n\ diff --git a/gdb/remote.h b/gdb/remote.h index 46bfa01fc79..cd91be8decb 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -78,4 +78,38 @@ extern int remote_register_number_and_offset (struct gdbarch *gdbarch, extern void remote_notif_get_pending_events (remote_target *remote, struct notif_client *np); extern bool remote_target_is_non_stop_p (remote_target *t); + +/* An abstract class that represents the set of callbacks that are made + from the send_remote_packet function (declared below). */ + +struct send_remote_packet_callbacks +{ + /* The SENDING callback is called once send_remote_packet has performed + its error checking and setup, just before the packet is sent to the + remote target. PACKET_STR is the content of the packet that will be + sent (before any of the protocol specific prefix, suffix, or escaping + is applied). */ + + virtual void sending (const char *packet_str) = 0; + + /* The RECEIVED callback is called once a reply has been received from + the remote target. The content of the reply is in BUF which can't be + modified, and which is not guaranteed to remain valid after the + RECEIVED call has returned. If you need to preserve the contents of + BUF then a copy should be taken. */ + + virtual void received (const gdb::char_vector &buf) = 0; +}; + +/* Send PACKET_STR the current remote target. If PACKET_STR is nullptr, or + is the empty string, then an error is thrown. If the current target is + not a remote target then an error is thrown. + + Calls CALLBACKS->sending() just before the packet is sent to the remote + target, and calls CALLBACKS->received() with the reply once this is + received from the remote target. */ + +extern void send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks); + #endif -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv4 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-10-22 10:58 ` [PATCHv4 0/4] Python API for target connections, and packet sending Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 1/4] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 2/4] gdb: make packet_command function available outside remote.c Andrew Burgess @ 2021-10-22 10:58 ` Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess 4 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 10:58 UTC (permalink / raw) To: gdb-patches This commits adds a new sub-class of gdb.TargetConnection, gdb.RemoteTargetConnection. This sub-class is created for all 'remote' and 'extended-remote' targets. This new sub-class has one additional method over its base class, 'send_packet'. This new method is equivalent to the 'maint packet' CLI command, it allows a custom packet to be sent to a remote target. The result of calling RemoteTargetConnection.send_packet is a string containing the reply that came from the remote. --- gdb/NEWS | 8 +- gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 40 ++++- gdb/python/py-connection.c | 174 +++++++++++++++++++- gdb/testsuite/gdb.python/py-connection.exp | 14 +- gdb/testsuite/gdb.python/py-send-packet.c | 22 +++ gdb/testsuite/gdb.python/py-send-packet.exp | 56 +++++++ gdb/testsuite/gdb.python/py-send-packet.py | 81 +++++++++ 8 files changed, 385 insertions(+), 11 deletions(-) create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py diff --git a/gdb/NEWS b/gdb/NEWS index 3bd13993005..a2d3505d14e 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -46,7 +46,9 @@ maint show internal-warning backtrace before GDB starts to clean up its internal state. ** New gdb.TargetConnection object type that represents a connection - (as displayed by the 'info connections' command). + (as displayed by the 'info connections' command). A sub-class, + gdb.RemoteTargetConnection, is used to represent 'remote' and + 'extended-remote' connections. ** The gdb.Inferior type now has a 'connection' property which is an instance of gdb.TargetConnection, the connection used by this @@ -60,6 +62,10 @@ maint show internal-warning backtrace ** New gdb.connections() function that returns a list of all currently active connections. + ** New gdb.RemoteTargetConnection.send_packet(STRING) method. This + is equivalent to the existing 'maint packet' CLI command; it + allows a user specified packet to be sent to the remote target. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index 631a7c03b31..89cf86210ab 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -39269,6 +39269,7 @@ error stream. This is @samp{on} by default for @code{internal-error} and @samp{off} by default for @code{internal-warning}. +@anchor{maint packet} @kindex maint packet @item maint packet @var{text} If @value{GDBN} is talking to an inferior via the serial protocol, diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index e62e2703b85..587cc934ca4 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -5980,9 +5980,9 @@ Examples of different connection types are @samp{native} and @samp{remote}. @xref{Inferiors Connections and Programs}. -@value{GDBN} uses the @code{gdb.TargetConnection} object type to -represent a connection in Python code. To get a list of all -connections use @code{gdb.connections} +Connections in @value{GDBN} are represented as instances of +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a +list of all connections use @code{gdb.connections} (@pxref{gdbpy_connections,,gdb.connections}). To get the connection for a single @code{gdb.Inferior} read its @@ -6035,6 +6035,40 @@ to the remote target. @end defvar +The @code{gdb.RemoteTargetConnection} class is a sub-class of +@code{gdb.TargetConnection}, and is used to represent @samp{remote} +and @samp{extended-remote} connections. In addition to the attributes +and methods available from the @code{gdb.TargetConnection} base class, +a @code{gdb.RemoteTargetConnection} has the following method: + +@kindex maint packet +@defun RemoteTargetConnection.send_packet (@var{packet}) +This method sends @var{packet}, which should be a non-empty string, to +the remote target and returns the response. If @var{packet} is not a +string, or is the empty string, then an exception of type +@code{ValueError} is thrown. + +In Python 3, the response is returned as a @code{bytes} buffer, if it +is known that the response can be represented as a string then this +can be decoded from the buffer. For example, if it is known that the +response is an @code{"ascii"} string: + +@smallexample +remote_connection.send_packet("some_packet").decode("ascii") +@end smallexample + +In Python 2, the response is returned as a @code{str}, which may +contain non-printable characters. + +The prefix, suffix, and checksum (as required by the remote serial +protocol) are automatically added to the outgoing packet, and removed +from the incoming packet before the contents of the reply are +returned. + +This is equivalent to the @code{maintenance packet} command +(@pxref{maint packet}). +@end defun + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c index f1dfa26e39c..f47182d720e 100644 --- a/gdb/python/py-connection.c +++ b/gdb/python/py-connection.c @@ -26,6 +26,8 @@ #include "py-events.h" #include "py-event.h" #include "arch-utils.h" +#include "remote.h" +#include "charset.h" #include <map> @@ -47,6 +49,9 @@ struct connection_object extern PyTypeObject connection_object_type CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); +extern PyTypeObject remote_connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("remote_connection_object"); + /* Require that CONNECTION be valid. */ #define CONNPY_REQUIRE_VALID(connection) \ do { \ @@ -81,8 +86,15 @@ target_to_connection_object (process_stratum_target *target) auto conn_obj_iter = all_connection_objects.find (target); if (conn_obj_iter == all_connection_objects.end ()) { - conn_obj.reset (PyObject_New (connection_object, - &connection_object_type)); + PyTypeObject *type; + + std::string connection_type = target->shortname (); + if (connection_type == "remote" || connection_type == "extended-remote") + type = &remote_connection_object_type; + else + type = &connection_object_type; + + conn_obj.reset (PyObject_New (connection_object, type)); if (conn_obj == nullptr) return nullptr; conn_obj->target = target; @@ -284,9 +296,111 @@ gdbpy_initialize_connection (void) (PyObject *) &connection_object_type) < 0) return -1; + if (PyType_Ready (&remote_connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "RemoteTargetConnection", + (PyObject *) &remote_connection_object_type) < 0) + return -1; + return 0; } +/* Set of callbacks used to implement gdb.send_packet. */ + +struct py_send_packet_callbacks : public send_remote_packet_callbacks +{ + /* Constructor, initialise the result to None. */ + + py_send_packet_callbacks () + : m_result (Py_None) + { /* Nothing. */ } + + /* There's nothing to do when the packet is sent. */ + + void sending (const char *args) override + { /* Nothing. */ } + + /* When the result is returned create a Python string and assign this + into the result member variable. */ + + void received (const gdb::char_vector &buf) override + { + /* m_result is initialized to None, leave it untouched unless we got + back a useful result. */ + if (buf.data ()[0] != '\0') + { + PyObject *result; + +#ifdef IS_PY3K + result = Py_BuildValue("y#", buf.data (), strlen (buf.data ())); +#else + result = PyString_FromStringAndSize (buf.data (), + strlen (buf.data ())); +#endif + + if (result != nullptr) + m_result = gdbpy_ref<> (result); + } + } + + /* Get a reference to the result as a Python object. */ + + gdbpy_ref<> result () const + { + return m_result; + } + +private: + + /* A reference to a valid result value. This is initialized in the + constructor, and so will always point to a valid value, even if this + is just None. */ + + gdbpy_ref<> m_result; +}; + +/* Implement RemoteTargetConnection.send_packet function. Send a packet to + the target identified by SELF. The connection must still be valid, and + the packet to be sent must be non-empty, otherwise an exception will be + thrown. */ + +static PyObject * +connpy_send_packet (PyObject *self, PyObject *args, PyObject *kw) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + static const char *keywords[] = {"packet", nullptr}; + const char *packet_str = nullptr; + + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s", keywords, + &packet_str)) + return nullptr; + + if (packet_str == nullptr || *packet_str == '\0') + { + PyErr_SetString (PyExc_ValueError, _("Invalid remote packet")); + return nullptr; + } + + try + { + scoped_restore_current_thread restore_thread; + switch_to_target_no_thread (conn->target); + + py_send_packet_callbacks callbacks; + send_remote_packet (packet_str, &callbacks); + return callbacks.result ().release (); + } + catch (const gdb_exception &except) + { + gdbpy_convert_exception (except); + return nullptr; + } +} + /* Global initialization for this file. */ void _initialize_py_connection (); @@ -307,6 +421,17 @@ Return true if this TargetConnection is valid, false if not." }, { NULL } }; +/* Methods for the gdb.RemoteTargetConnection object type. */ + +static PyMethodDef remote_connection_object_methods[] = +{ + { "send_packet", (PyCFunction) connpy_send_packet, + METH_VARARGS | METH_KEYWORDS, + "send_packet (PACKET) -> String\n\ +Send PACKET to a remote target, return the reply as a string." }, + { NULL } +}; + /* Attributes for the gdb.TargetConnection object type. */ static gdb_PyGetSetDef connection_object_getset[] = @@ -345,7 +470,7 @@ PyTypeObject connection_object_type = 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ "GDB target connection object", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ @@ -364,3 +489,46 @@ PyTypeObject connection_object_type = 0, /* tp_init */ 0 /* tp_alloc */ }; + +/* Define the gdb.RemoteTargetConnection object type. */ + +PyTypeObject remote_connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.RemoteTargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB remote target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + remote_connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + &connection_object_type, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp index b805b052f73..96c83781839 100644 --- a/gdb/testsuite/gdb.python/py-connection.exp +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -33,12 +33,18 @@ if ![runto_main] then { return 0 } +if { [target_info exists gdb_protocol] } { + set connection_type "RemoteTargetConnection" +} else { + set connection_type "TargetConnection" +} + # Create a gdb.TargetConnection object and check it is initially # valid. gdb_test_no_output "python conn = gdb.selected_inferior().connection" gdb_test "python print(conn)" \ - "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ - "print gdb.TargetConnection while it is still valid" + "<gdb.${connection_type} num=1, what=\"\[^\"\]+\">" \ + "print gdb.${connection_type} while it is still valid" gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" # Get the connection again, and ensure we get the exact same object. @@ -53,8 +59,8 @@ gdb_test "disconnect" "" "kill the inferior" \ "A program is being debugged already\\. Kill it\\? .*y or n. $" "y" gdb_test "info connections" "No connections\\." \ "info connections now all the connections have gone" -gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ - "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn)" "<gdb.${connection_type} \\(invalid\\)>" \ + "print gdb.${connection_type} now its invalid" gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" # Now check that accessing properties of the invalid connection cases diff --git a/gdb/testsuite/gdb.python/py-send-packet.c b/gdb/testsuite/gdb.python/py-send-packet.c new file mode 100644 index 00000000000..bfe52c018d4 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp new file mode 100644 index 00000000000..dde5c270f20 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.exp @@ -0,0 +1,56 @@ +# Copyright (C) 2021 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 the gdb.RemoteTargetConnection.send_packet API. This is done +# by connecting to a remote target and fetching the thread list in two +# ways, first, we manually send the packets required to read the +# thread list using gdb.TargetConnection.send_packet, then we compare +# the results to the thread list using the standard API calls. + +load_lib gdb-python.exp +load_lib gdbserver-support.exp + +standard_testfile + +if {[skip_gdbserver_tests]} { + return 0 +} + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +if { [skip_python_tests] } { + return 0 +} + +# Make sure we're disconnected, in case we're testing with an +# extended-remote board, therefore already connected. +gdb_test "disconnect" ".*" + +gdbserver_run "" + +# Source the python script. +set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +gdb_test "source $remote_python_file" "Sourcing complete\\." \ + "source ${testfile}.py script" + +# The test is actually written in the Python script. Run it now. +gdb_test "python run_send_packet_test()" "Send packet test passed" + +# Check the string representation of a remote target connection. +gdb_test "python print(gdb.selected_inferior().connection)" \ + "<gdb.RemoteTargetConnection num=$decimal, what=\".*\">" diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py new file mode 100644 index 00000000000..35754164a98 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.py @@ -0,0 +1,81 @@ +# Copyright (C) 2021 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 xml.etree.ElementTree as ET +import gdb + +# Make use of gdb.RemoteTargetConnection.send_packet to fetch the +# thread list from the remote target. +# +# Sending existing serial protocol packets like this is not a good +# idea, there should be better ways to get this information using an +# official API, this is just being used as a test case. +# +# Really, the send_packet API would be used to send target +# specific packets to the target, but these are, by definition, target +# specific, so hard to test in a general testsuite. +def get_thread_list_str(): + start_pos = 0 + thread_desc = "" + conn = gdb.selected_inferior().connection + if not isinstance(conn, gdb.RemoteTargetConnection): + raise gdb.GdbError("connection is the wrong type") + while True: + str = conn.send_packet("qXfer:threads:read::%d,200" % start_pos).decode("ascii") + start_pos += 200 + c = str[0] + str = str[1:] + thread_desc += str + if c == "l": + break + return thread_desc + + +# Use gdb.RemoteTargetConnection.send_packet to manually fetch the +# thread list, then extract the thread list using the gdb.Inferior and +# gdb.InferiorThread API. Compare the two results to ensure we +# managed to successfully read the thread list from the remote. +def run_send_packet_test(): + # Find the IDs of all current threads. + all_threads = {} + for inf in gdb.inferiors(): + for thr in inf.threads(): + id = "p%x.%x" % (thr.ptid[0], thr.ptid[1]) + all_threads[id] = False + + # Now fetch the thread list from the remote, and parse the XML. + str = get_thread_list_str() + threads_xml = ET.fromstring(str) + + # Look over all threads in the XML list and check we expected to + # find them, mark the ones we do find. + for thr in threads_xml: + id = thr.get("id") + if not id in all_threads: + raise "found unexpected thread in remote thread list" + else: + all_threads[id] = True + + # Check that all the threads were found in the XML list. + for id in all_threads: + if not all_threads[id]: + raise "thread missingt from remote thread list" + + # Test complete. + print("Send packet test passed") + + +# Just to indicate the file was sourced correctly. +print("Sourcing complete.") -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv4 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet 2021-10-22 10:58 ` [PATCHv4 0/4] Python API for target connections, and packet sending Andrew Burgess ` (2 preceding siblings ...) 2021-10-22 10:58 ` [PATCHv4 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess @ 2021-10-22 10:58 ` Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess 4 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 10:58 UTC (permalink / raw) To: gdb-patches Previous the 'maint packet' command, and the recently added Python API RemoteTargetConnection.send_packet, could only cope with replies that contained printable string characters. The code implementing these two features treated the reply buffer as a string, so any null-bytes would terminate the output, and if some other non-printable character was encountered, GDB would just try to print it anyway... The only way I've found to reproduce this easily is to try and examine the auxv data, which is binary in nature, here's a GDB session before my patch: (gdb) target remote :54321 ... (gdb) maint packet qXfer:auxv:read::0,1000 sending: "qXfer:auxv:read::0,1000" received: "l!" (gdb) And after my patch: (gdb) target remote :54321 ... (gdb) maint packet qXfer:auxv:read::0,1000 sending: "qXfer:auxv:read::0,1000" received: "l!\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xfc\xf7\xff\x7f\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xfb\xeb\xbf\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00@\x00@\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x10\xfd\xf7\xff\x7f\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00P\x10@\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00y\xba\xff\xff\xff\x7f\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\xdf\xef\xff\xff\xff\x7f\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x89\xba\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" (gdb) The binary contents of the reply are now printed as escaped hex. Similarly, here's the same packet fetched, and printed, using this Python script: inf = gdb.selected_inferior() conn = inf.connection res = conn.send_packet("qXfer:auxv:read::0,1000") print ("Got: %s" % str(res)) The GDB session: (gdb) source fetch-auxv.py Got: b'l!\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xfc\xf7\xff\x7f\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xfb\xeb\xbf\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00@\x00@\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x10\xfd\xf7\xff\x7f\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00P\x10@\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00y\xba\xff\xff\xff\x7f\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\xdf\xef\xff\xff\xff\x7f\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x89\xba\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' The testing for this change is pretty weak right now, I just fetch the auxv data using 'maint packet' and from Python, and check that the two results match. I also check that the results, when converted to a string, contains some hex-encoded characters (e.g. \xf0, etc). --- gdb/python/py-connection.c | 21 ++++---- gdb/remote.c | 57 ++++++++++++--------- gdb/remote.h | 10 ++-- gdb/testsuite/gdb.python/py-send-packet.exp | 33 ++++++++++++ gdb/testsuite/gdb.python/py-send-packet.py | 35 +++++++++++++ 5 files changed, 119 insertions(+), 37 deletions(-) diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c index f47182d720e..74f0ac45e83 100644 --- a/gdb/python/py-connection.c +++ b/gdb/python/py-connection.c @@ -321,22 +321,25 @@ struct py_send_packet_callbacks : public send_remote_packet_callbacks void sending (const char *args) override { /* Nothing. */ } - /* When the result is returned create a Python string and assign this - into the result member variable. */ + /* When the result is returned create a Python object and assign this + into M_RESULT. If for any reason we can't create a Python object to + represent the result then M_RESULT is left untouched (it is + initialised to reference Py_None), so after this call M_RESULT will + either be an object representing the reply, or None. */ - void received (const gdb::char_vector &buf) override + void received (const gdb::char_vector &buf, int bytes) override { - /* m_result is initialized to None, leave it untouched unless we got - back a useful result. */ - if (buf.data ()[0] != '\0') + gdb_assert (bytes >= 0); + + if (bytes > 0 && buf.data ()[0] != '\0') { PyObject *result; #ifdef IS_PY3K - result = Py_BuildValue("y#", buf.data (), strlen (buf.data ())); + result = Py_BuildValue("y#", buf.data (), bytes); #else - result = PyString_FromStringAndSize (buf.data (), - strlen (buf.data ())); + //result = PyString_FromStringAndSize (buf.data (), bytes); + result = PyByteArray_FromStringAndSize (buf.data (), bytes); #endif if (result != nullptr) diff --git a/gdb/remote.c b/gdb/remote.c index 237b1b21c41..81d3eabb065 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -1028,8 +1028,6 @@ static int hexnumnstr (char *, ULONGEST, int); static CORE_ADDR remote_address_masked (CORE_ADDR); -static void print_packet (const char *); - static int stub_unpack_int (const char *buff, int fieldlength); struct packet_config; @@ -9516,17 +9514,6 @@ escape_buffer (const char *buf, int n) return std::move (stb.string ()); } -/* Display a null-terminated packet on stdout, for debugging, using C - string notation. */ - -static void -print_packet (const char *buf) -{ - puts_filtered ("\""); - fputstr_filtered (buf, '"', gdb_stdout); - puts_filtered ("\""); -} - int remote_target::putpkt (const char *buf) { @@ -11622,18 +11609,39 @@ struct cli_packet_command_callbacks : public send_remote_packet_callbacks void sending (const char *packet_str) override { - puts_filtered ("sending: "); - print_packet (packet_str); - puts_filtered ("\n"); + printf_filtered ("sending: \"%s\"\n", packet_str); } - /* Called with BUF, the reply from the remote target. */ + /* Called with BUF, the reply from the remote target, which is BYTES in + length. Print the contents of BUF to gdb_stdout. */ - void received (const gdb::char_vector &buf) override + void received (const gdb::char_vector &buf, int bytes) override { - puts_filtered ("received: "); - print_packet (buf.data ()); - puts_filtered ("\n"); + puts_filtered ("received: \""); + print_packet (buf, bytes); + puts_filtered ("\"\n"); + } + +private: + /* Print BUF, which contains BYTES bytes to gdb_stdout. Any + non-printable bytes in BUF are printed as '\x??' with '??' replaced + by the hexadecimal value of the byte. */ + + static void + print_packet (const gdb::char_vector &buf, int bytes) + { + string_file stb; + + for (int i = 0; i < bytes; ++i) + { + gdb_byte c = buf[i]; + if (isprint (c)) + fputc_unfiltered (c, &stb); + else + fprintf_unfiltered (&stb, "\\x%02x", (unsigned char) c); + } + + puts_filtered (stb.string ().c_str ()); } }; @@ -11654,9 +11662,12 @@ send_remote_packet (const char *packet_str, remote->putpkt (packet_str); remote_state *rs = remote->get_remote_state (); - remote->getpkt (&rs->buf, 0); + int bytes = remote->getpkt_sane (&rs->buf, 0); + + if (bytes < 0) + error (_("error while fetching packet from remote target")); - callbacks->received (rs->buf); + callbacks->received (rs->buf, bytes); } /* Entry point for the 'maint packet' command. */ diff --git a/gdb/remote.h b/gdb/remote.h index cd91be8decb..fb29d2cfa46 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -93,12 +93,12 @@ struct send_remote_packet_callbacks virtual void sending (const char *packet_str) = 0; /* The RECEIVED callback is called once a reply has been received from - the remote target. The content of the reply is in BUF which can't be - modified, and which is not guaranteed to remain valid after the - RECEIVED call has returned. If you need to preserve the contents of - BUF then a copy should be taken. */ + the remote target. The content of the reply is in BUF, which is BYTES + long, which can't be modified, and which is not guaranteed to remain + valid after the RECEIVED call has returned. If you need to preserve + the contents of BUF then a copy should be taken. */ - virtual void received (const gdb::char_vector &buf) = 0; + virtual void received (const gdb::char_vector &buf, int bytes) = 0; }; /* Send PACKET_STR the current remote target. If PACKET_STR is nullptr, or diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp index dde5c270f20..63a7080e3bc 100644 --- a/gdb/testsuite/gdb.python/py-send-packet.exp +++ b/gdb/testsuite/gdb.python/py-send-packet.exp @@ -54,3 +54,36 @@ gdb_test "python run_send_packet_test()" "Send packet test passed" # Check the string representation of a remote target connection. gdb_test "python print(gdb.selected_inferior().connection)" \ "<gdb.RemoteTargetConnection num=$decimal, what=\".*\">" + +# Check to see if there's any auxv data for this target. +gdb_test_multiple "info auxv" "" { + -re -wrap "The program has no auxiliary information now\\. " { + set skip_auxv_test true + } + -re -wrap "0\\s+AT_NULL\\s+End of vector\\s+0x0" { + set skip_auxv_test false + } +} + +if { ! $skip_auxv_test } { + # Use 'maint packet' to fetch the auxv data. + set reply_data "" + gdb_test_multiple "maint packet qXfer:auxv:read::0,1000" "" { + -re "sending: \"qXfer:auxv:read::0,1000\"\r\n" { + exp_continue + } + -re -wrap "received: \"(.*)\"" { + set reply_data $expect_out(1,string) + } + } + + # Expand the '\x' in the output, so we can pass a string through + # to Python. + set reply_data [string map {\x \\x} $reply_data] + gdb_assert { ![string equal "$reply_data" ""] } + + # Run the test, fetches the auxv data in Python and confirm it + # matches the expected results. + gdb_test "python run_auxv_send_packet_test(\"$reply_data\")" \ + "auxv send packet test passed" +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py index 35754164a98..3e1ee45864c 100644 --- a/gdb/testsuite/gdb.python/py-send-packet.py +++ b/gdb/testsuite/gdb.python/py-send-packet.py @@ -77,5 +77,40 @@ def run_send_packet_test(): print("Send packet test passed") +# Convert a bytes or bytearray object to a string. This masks the +# differences in how this is achieved between Python 2 and 3. +def bytes_to_string(byte_array): + if sys.version_info[0] == 2: + # It feels like there must be a better way to do this on + # Python 2; but for now, just build the result character by + # character. + res = "" + for b in byte_array: + b = int(b) + if b >= 32 and b <= 126: + res = res + ("%c" % b) + else: + res = res + ("\\x%02x" % b) + return res + else: + # This is easy for Python 3. + return str(byte_array) + + +# A very simple test for sending the packet that reads the auxv data. +# We convert the result to a string and expect to find some +# hex-encoded bytes in the output. This test will only work on +# targets that actually supply auxv data. +def run_auxv_send_packet_test(expected_result): + inf = gdb.selected_inferior() + conn = inf.connection + assert isinstance(conn, gdb.RemoteTargetConnection) + res = conn.send_packet("qXfer:auxv:read::0,1000") + string = bytes_to_string(res) + assert string.count("\\x") > 0 + assert string == expected_result + print("auxv send packet test passed") + + # Just to indicate the file was sourced correctly. print("Sourcing complete.") -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv5 0/4] Python API for target connections, and packet sending 2021-10-22 10:58 ` [PATCHv4 0/4] Python API for target connections, and packet sending Andrew Burgess ` (3 preceding siblings ...) 2021-10-22 10:58 ` [PATCHv4 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet Andrew Burgess @ 2021-10-22 17:10 ` Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 1/4] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess ` (5 more replies) 4 siblings, 6 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 17:10 UTC (permalink / raw) To: gdb-patches Since v4: - Updated to latest upstream/master. - Replaced string comparison on process_stratum_target::shortname() with a new helper function is_remote_target declared in remote.h, and defined in remote.c. - Fixed a bug in py-send-packet.exp (patch #4) when running the test using Python 3. Since v3: - Addressed all review comments. - The RemoteTargetConnection.send_packet method now returns a bytes object for Py3, and a bytesarray object for Py2. This change is in patch #3. - New patch #4 fixes GDB so that we can correctly return packets containing null bytes, this fixes both 'maint packet' and the Python API. Since v2: - Added a gdb.RemoteTargetConnection sub-class, which is used for 'remote' and 'extended-remote' connections. The previous 'send_remote_packet' has become 'send_packet' on this new sub-class. - Tests and documentation have been updated to reflect the above change. - In the first patch, connpy_repr has been modified in order to prepare it for the sub-class that will appear in the later patch. Since v1: - Rebased onto current upstream master and resolved the merge conflicts. - Addressed the documentation feedback from Eli. I'm not 100% sure that I have correctly addressed the concerns about patch #3, so this is probably worth rechecking. --- Andrew Burgess (4): gdb/python: introduce gdb.TargetConnection object type gdb: make packet_command function available outside remote.c gdb/python: add gdb.RemoteTargetConnection.send_packet gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet gdb/Makefile.in | 1 + gdb/NEWS | 21 + gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 127 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 536 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/remote.c | 113 ++-- gdb/remote.h | 42 ++ gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 ++ gdb/testsuite/gdb.python/py-connection.c | 22 + gdb/testsuite/gdb.python/py-connection.exp | 75 +++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- gdb/testsuite/gdb.python/py-send-packet.c | 22 + gdb/testsuite/gdb.python/py-send-packet.exp | 89 +++ gdb/testsuite/gdb.python/py-send-packet.py | 109 ++++ 23 files changed, 1281 insertions(+), 39 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv5 1/4] gdb/python: introduce gdb.TargetConnection object type 2021-10-22 17:10 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess @ 2021-10-22 17:10 ` Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 2/4] gdb: make packet_command function available outside remote.c Andrew Burgess ` (4 subsequent siblings) 5 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 17:10 UTC (permalink / raw) To: gdb-patches This commit adds a new object type gdb.TargetConnection. This new type represents a connection within GDB (a connection as displayed by 'info connections'). There's three ways to find a gdb.TargetConnection, there's a new 'gdb.connections()' function, which returns a list of all currently active connections. Or you can read the new 'connection' property on the gdb.Inferior object type, this contains the connection for that inferior (or None if the inferior has no connection, for example, it is exited). Finally, there's a new gdb.events.connection_removed event registry, this emits a new gdb.ConnectionEvent whenever a connection is removed from GDB (this can happen when all inferiors using a connection exit, though this is not always the case, depending on the connection type). The gdb.ConnectionEvent has a 'connection' property, which is the gdb.TargetConnection being removed from GDB. The gdb.TargetConnection has an 'is_valid()' method. A connection object becomes invalid when the underlying connection is removed from GDB (as discussed above, this might be when all inferiors using a connection exit, or it might be when the user explicitly replaces a connection in GDB by issuing another 'target' command). The gdb.TargetConnection has the following read-only properties: 'num': The number for this connection, 'type': e.g. 'native', 'remote', 'sim', etc 'description': The longer description as seen in the 'info connections' command output. 'details': A string or None. Extra details for the connection, for example, a remote connection's details might be 'hostname:port'. --- gdb/Makefile.in | 1 + gdb/NEWS | 15 + gdb/doc/python.texi | 93 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 366 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 +++ gdb/testsuite/gdb.python/py-connection.c | 22 ++ gdb/testsuite/gdb.python/py-connection.exp | 69 ++++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- 17 files changed, 724 insertions(+), 4 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp diff --git a/gdb/Makefile.in b/gdb/Makefile.in index ec5d332c145..40895d4da71 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -391,6 +391,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-bpevent.c \ python/py-breakpoint.c \ python/py-cmd.c \ + python/py-connection.c \ python/py-continueevent.c \ python/py-event.c \ python/py-evtregistry.c \ diff --git a/gdb/NEWS b/gdb/NEWS index d001a03145d..fd1ca0efbe5 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -49,6 +49,21 @@ maint show internal-warning backtrace containing all of the possible Architecture.name() values. Each entry is a string. + ** New gdb.TargetConnection object type that represents a connection + (as displayed by the 'info connections' command). + + ** The gdb.Inferior type now has a 'connection' property which is an + instance of gdb.TargetConnection, the connection used by this + inferior. This can be None if the inferior has no connection. + + ** New 'gdb.events.connection_removed' event registry, which emits a + 'gdb.ConnectionEvent' when a connection is removed from GDB. + This event has a 'connection' property, a gdb.TargetConnection + object for the connection being removed. + + ** New gdb.connections() function that returns a list of all + currently active connections. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 90214f24238..2234bbe1891 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -220,6 +220,7 @@ * Lazy Strings In Python:: Python representation of lazy strings. * Architectures In Python:: Python representation of architectures. * Registers In Python:: Python representation of registers. +* Connections In Python:: Python representation of connections. * TUI Windows In Python:: Implementing new TUI windows. @end menu @@ -565,6 +566,13 @@ (@pxref{gdbpy_architecture_name,,Architecture.name}). @end defun +@anchor{gdbpy_connections} +@defun gdb.connections +Return a list of @code{gdb.TargetConnection} objects, one for each +currently active connection (@pxref{Connections In Python}). The +connection objects are in no particular order in the returned list. +@end defun + @node Exception Handling @subsubsection Exception Handling @cindex python exceptions @@ -3092,10 +3100,18 @@ ID of inferior, as assigned by GDB. @end defvar +@anchor{gdbpy_inferior_connection} +@defvar Inferior.connection +The @code{gdb.TargetConnection} for this inferior (@pxref{Connections +In Python}), or @code{None} if this inferior has no connection. +@end defvar + @defvar Inferior.connection_num ID of inferior's connection as assigned by @value{GDBN}, or None if -the inferior is not connected to a target. -@xref{Inferiors Connections and Programs}. +the inferior is not connected to a target. @xref{Inferiors Connections +and Programs}. This is equivalent to +@code{gdb.Inferior.connection.num} in the case where +@code{gdb.Inferior.connection} is not @code{None}. @end defvar @defvar Inferior.pid @@ -3436,6 +3452,15 @@ An integer, the value of the exit code @value{GDBN} will return. @end defvar +@item events.connection_removed +This is emitted when @value{GDBN} removes a connection +(@pxref{Connections In Python}). The event is of type +@code{gdb.ConnectionEvent}. This has a single read-only attribute: + +@defvar ConnectionEvent.connection +The @code{gdb.TargetConnection} that is being removed. +@end defvar + @end table @node Threads In Python @@ -5955,6 +5980,70 @@ A string that is the name of this register group. @end defvar +@node Connections In Python +@subsubsection Connections In Python +@cindex connections in python +@value{GDBN} lets you run and debug multiple programs in a single +session. Each program being debugged has a connection, the connection +describes how @value{GDBN} controls the program being debugged. +Examples of different connection types are @samp{native} and +@samp{remote}. @xref{Inferiors Connections and Programs}. + +@value{GDBN} uses the @code{gdb.TargetConnection} object type to +represent a connection in Python code. To get a list of all +connections use @code{gdb.connections} +(@pxref{gdbpy_connections,,gdb.connections}). + +To get the connection for a single @code{gdb.Inferior} read its +@code{gdb.Inferior.connection} attribute +(@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). + +A @code{gdb.TargetConnection} has the following method: + +@defun TargetConnection.is_valid () +Return @code{True} if the @code{gdb.TargetConnection} object is valid, +@code{False} if not. A @code{gdb.TargetConnection} will become +invalid if the connection no longer exists within @value{GDBN}, this +might happen when no inferiors are using the connection, but could be +delayed until the user replaces the current target. + +Reading any of the @code{gdb.TargetConnection} properties will throw +an exception if the connection is invalid. +@end defun + +A @code{gdb.TargetConnection} has the following read-only properties: + +@defvar TargetConnection.num +An integer assigned by @value{GDBN} to uniquely identify this +connection. This is the same value as displayed in the @samp{Num} +column of the @code{info connections} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.type +A string that describes what type of connection this is. This string +will be one of the valid names that can be passed to the @code{target} +command (@pxref{Target Commands,,target command}). +@end defvar + +@defvar TargetConnection.description +A string that gives a short description of this target type. This is +the same string that is displayed in the @samp{Description} column of +the @code{info connection} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.details +An optional string that gives additional information about this +connection. This attribute can be @code{None} if there are no +additional details for this connection. + +An example of a connection type that might have additional details is +the @samp{remote} connection, in this case the details string can +contain the @samp{@var{hostname}:@var{port}} that was used to connect +to the remote target. +@end defvar + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/observable.c b/gdb/observable.c index b020076cf26..6e91d7bbc4a 100644 --- a/gdb/observable.c +++ b/gdb/observable.c @@ -78,6 +78,7 @@ DEFINE_OBSERVABLE (user_selected_context_changed); DEFINE_OBSERVABLE (source_styling_changed); DEFINE_OBSERVABLE (current_source_symtab_and_line_changed); DEFINE_OBSERVABLE (gdb_exiting); +DEFINE_OBSERVABLE (connection_removed); } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/observable.h b/gdb/observable.h index f20f532870f..707f6c91c12 100644 --- a/gdb/observable.h +++ b/gdb/observable.h @@ -253,6 +253,9 @@ extern observable<> current_source_symtab_and_line_changed; /* Called when GDB is about to exit. */ extern observable<int> gdb_exiting; +/* When a connection is removed. */ +extern observable<process_stratum_target */* target */> connection_removed; + } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def index 83f10989e4a..ed22a3335cd 100644 --- a/gdb/python/py-all-events.def +++ b/gdb/python/py-all-events.def @@ -39,3 +39,4 @@ GDB_PY_DEFINE_EVENT(breakpoint_deleted) GDB_PY_DEFINE_EVENT(breakpoint_modified) GDB_PY_DEFINE_EVENT(before_prompt) GDB_PY_DEFINE_EVENT(gdb_exiting) +GDB_PY_DEFINE_EVENT(connection_removed) diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c new file mode 100644 index 00000000000..f1dfa26e39c --- /dev/null +++ b/gdb/python/py-connection.c @@ -0,0 +1,366 @@ +/* Python interface to inferiors. + + Copyright (C) 2009-2021 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/>. */ + +#include "defs.h" +#include "python-internal.h" +#include "process-stratum-target.h" +#include "inferior.h" +#include "observable.h" +#include "target-connection.h" +#include "py-events.h" +#include "py-event.h" +#include "arch-utils.h" + +#include <map> + +/* The Python object that represents a connection. */ + +struct connection_object +{ + PyObject_HEAD + + /* The process target that represents this connection. When a + connection_object is created this field will always point at a valid + target. Later, if GDB stops using this target (the target is popped + from all target stacks) then this field is set to nullptr, which + indicates that this Python object is now in the invalid state (see + the is_valid() method below). */ + struct process_stratum_target *target; +}; + +extern PyTypeObject connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); + +/* Require that CONNECTION be valid. */ +#define CONNPY_REQUIRE_VALID(connection) \ + do { \ + if (connection->target == nullptr) \ + { \ + PyErr_SetString (PyExc_RuntimeError, \ + _("Connection no longer exists.")); \ + return nullptr; \ + } \ + } while (0) + +/* A map between process_stratum targets and the Python object representing + them. We actually hold a gdbpy_ref around the Python object so that + reference counts are handled correctly when entries are deleted. */ +static std::map<process_stratum_target *, + gdbpy_ref<connection_object>> all_connection_objects; + +/* Return a reference to a gdb.TargetConnection object for TARGET. If + TARGET is nullptr then a reference to None is returned. + + Previously created gdb.TargetConnection objects are cached, and + additional references to the same connection object can be returned with + later calls to this function. */ + +gdbpy_ref<> +target_to_connection_object (process_stratum_target *target) +{ + if (target == nullptr) + return gdbpy_ref<>::new_reference (Py_None); + + gdbpy_ref <connection_object> conn_obj; + auto conn_obj_iter = all_connection_objects.find (target); + if (conn_obj_iter == all_connection_objects.end ()) + { + conn_obj.reset (PyObject_New (connection_object, + &connection_object_type)); + if (conn_obj == nullptr) + return nullptr; + conn_obj->target = target; + all_connection_objects.emplace (target, conn_obj); + } + else + conn_obj = conn_obj_iter->second; + + gdb_assert (conn_obj != nullptr); + + /* Repackage the result as a PyObject reference. */ + return gdbpy_ref<> ((PyObject *) conn_obj.release ()); +} + +/* Return a list of gdb.TargetConnection objects, one for each currently + active connection. The returned list is in no particular order. */ + +PyObject * +gdbpy_connections (PyObject *self, PyObject *args) +{ + gdbpy_ref<> list (PyList_New (0)); + if (list == nullptr) + return nullptr; + + for (process_stratum_target *target : all_non_exited_process_targets ()) + { + gdb_assert (target != nullptr); + + gdbpy_ref<> conn = target_to_connection_object (target); + if (conn == nullptr) + return nullptr; + gdb_assert (conn.get () != Py_None); + + if (PyList_Append (list.get (), conn.get ()) < 0) + return nullptr; + } + + return list.release (); +} + +/* Emit a connection event for TARGET to REGISTRY. Return 0 on success, or + a negative value on error. */ + +static int +emit_connection_event (process_stratum_target *target, + eventregistry_object *registry) +{ + gdbpy_ref<> event_obj + = create_event_object (&connection_event_object_type); + if (event_obj == nullptr) + return -1; + + gdbpy_ref<> conn = target_to_connection_object (target); + if (evpy_add_attribute (event_obj.get (), "connection", conn.get ()) < 0) + return -1; + + return evpy_emit_event (event_obj.get (), registry); +} + +/* Callback for the connection_removed observer. */ + +static void +connpy_connection_removed (process_stratum_target *target) +{ + if (!gdb_python_initialized) + return; + + gdbpy_enter enter_py (get_current_arch (), current_language); + + if (!evregpy_no_listeners_p (gdb_py_events.connection_removed)) + if (emit_connection_event (target, gdb_py_events.connection_removed) < 0) + gdbpy_print_stack (); + + auto conn_obj_iter = all_connection_objects.find (target); + if (conn_obj_iter != all_connection_objects.end ()) + { + gdbpy_ref <connection_object> conn_obj = conn_obj_iter->second; + conn_obj->target = nullptr; + all_connection_objects.erase (target); + } +} + +/* Called when a gdb.TargetConnection object is deallocated. */ + +static void +connpy_connection_dealloc (PyObject *obj) +{ + connection_object *conn_obj = (connection_object *) obj; + + /* As the all_connection_objects map holds a reference to each connection + object we can only enter the dealloc function when the reference in + all_connection_objects has been erased. + + As we always set the target pointer back to nullptr before we erase + items from all_connection_objects then, when we get here, the target + pointer must be nullptr. */ + gdb_assert (conn_obj->target == nullptr); + + Py_TYPE (obj)->tp_free (obj); +} + +/* Implement repr() for gdb.TargetConnection. */ + +static PyObject * +connpy_repr (PyObject *obj) +{ + connection_object *self = (connection_object *) obj; + process_stratum_target *target = self->target; + + if (target == nullptr) + return PyString_FromFormat ("<%s (invalid)>", Py_TYPE (obj)->tp_name); + + return PyString_FromFormat ("<%s num=%d, what=\"%s\">", + Py_TYPE (obj)->tp_name, + target->connection_number, + make_target_connection_string (target).c_str ()); +} + +/* Implementation of gdb.TargetConnection.is_valid() -> Boolean. Returns + True if this connection object is still associated with a + process_stratum_target, otherwise, returns False. */ + +static PyObject * +connpy_is_valid (PyObject *self, PyObject *args) +{ + connection_object *conn = (connection_object *) self; + + if (conn->target == nullptr) + Py_RETURN_FALSE; + + Py_RETURN_TRUE; +} + +/* Return the id number of this connection. */ + +static PyObject * +connpy_get_connection_num (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + auto num = conn->target->connection_number; + return gdb_py_object_from_longest (num).release (); +} + +/* Return a string that gives the short name for this connection type. */ + +static PyObject * +connpy_get_connection_type (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *shortname = conn->target->shortname (); + return host_string_to_python_string (shortname).release (); +} + +/* Return a string that gives a longer description of this connection type. */ + +static PyObject * +connpy_get_description (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *longname = conn->target->longname (); + return host_string_to_python_string (longname).release (); +} + +/* Return a string that gives additional details about this connection, or + None, if there are no additional details for this connection type. */ + +static PyObject * +connpy_get_connection_details (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *details = conn->target->connection_string (); + if (details != nullptr) + return host_string_to_python_string (details).release (); + else + Py_RETURN_NONE; +} + +/* Python specific initialization for this file. */ + +int +gdbpy_initialize_connection (void) +{ + if (PyType_Ready (&connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "TargetConnection", + (PyObject *) &connection_object_type) < 0) + return -1; + + return 0; +} + +/* Global initialization for this file. */ + +void _initialize_py_connection (); +void +_initialize_py_connection () +{ + gdb::observers::connection_removed.attach (connpy_connection_removed, + "py-connection"); +} + +/* Methods for the gdb.TargetConnection object type. */ + +static PyMethodDef connection_object_methods[] = +{ + { "is_valid", connpy_is_valid, METH_NOARGS, + "is_valid () -> Boolean.\n\ +Return true if this TargetConnection is valid, false if not." }, + { NULL } +}; + +/* Attributes for the gdb.TargetConnection object type. */ + +static gdb_PyGetSetDef connection_object_getset[] = +{ + { "num", connpy_get_connection_num, NULL, + "ID number of this connection, as assigned by GDB.", NULL }, + { "type", connpy_get_connection_type, NULL, + "A short string that is the name for this connection type.", NULL }, + { "description", connpy_get_description, NULL, + "A longer string describing this connection type.", NULL }, + { "details", connpy_get_connection_details, NULL, + "A string containing additional connection details.", NULL }, + { NULL } +}; + +/* Define the gdb.TargetConnection object type. */ + +PyTypeObject connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.TargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + connection_object_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def index aeaee02e8bb..6a49f2aa896 100644 --- a/gdb/python/py-event-types.def +++ b/gdb/python/py-event-types.def @@ -110,3 +110,8 @@ GDB_PY_DEFINE_EVENT_TYPE (gdb_exiting, "GdbExitingEvent", "GDB is about to exit", event_object_type); + +GDB_PY_DEFINE_EVENT_TYPE (connection, + "ConnectionEvent", + "GDB connection added or removed object", + event_object_type); diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c index aec8c0f73cb..e95ed8b4452 100644 --- a/gdb/python/py-inferior.c +++ b/gdb/python/py-inferior.c @@ -415,6 +415,20 @@ infpy_get_num (PyObject *self, void *closure) return gdb_py_object_from_longest (inf->inferior->num).release (); } +/* Return the gdb.TargetConnection object for this inferior, or None if a + connection does not exist. */ + +static PyObject * +infpy_get_connection (PyObject *self, void *closure) +{ + inferior_object *inf = (inferior_object *) self; + + INFPY_REQUIRE_VALID (inf); + + process_stratum_target *target = inf->inferior->process_target (); + return target_to_connection_object (target).release (); +} + /* Return the connection number of the given inferior, or None if a connection does not exist. */ @@ -849,6 +863,8 @@ gdbpy_initialize_inferior (void) static gdb_PyGetSetDef inferior_object_getset[] = { { "num", infpy_get_num, NULL, "ID of inferior, as assigned by GDB.", NULL }, + { "connection", infpy_get_connection, NULL, + "The gdb.TargetConnection for this inferior.", NULL }, { "connection_num", infpy_get_connection_num, NULL, "ID of inferior's connection, as assigned by GDB.", NULL }, { "pid", infpy_get_pid, NULL, "PID of inferior, as assigned by the OS.", diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h index 24e28bc4767..b462872da71 100644 --- a/gdb/python/python-internal.h +++ b/gdb/python/python-internal.h @@ -482,6 +482,10 @@ gdbpy_ref<inferior_object> inferior_to_inferior_object (inferior *inf); PyObject *gdbpy_buffer_to_membuf (gdb::unique_xmalloc_ptr<gdb_byte> buffer, CORE_ADDR address, ULONGEST length); +struct process_stratum_target; +gdbpy_ref<> target_to_connection_object (process_stratum_target *target); +PyObject *gdbpy_connections (PyObject *self, PyObject *args); + const struct block *block_object_to_block (PyObject *obj); struct symbol *symbol_object_to_symbol (PyObject *obj); struct value *value_object_to_value (PyObject *self); @@ -555,6 +559,8 @@ int gdbpy_initialize_tui () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; int gdbpy_initialize_membuf () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; +int gdbpy_initialize_connection () + CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; /* A wrapper for PyErr_Fetch that handles reference counting for the caller. */ diff --git a/gdb/python/python.c b/gdb/python/python.c index c81814c557b..005ee5966ac 100644 --- a/gdb/python/python.c +++ b/gdb/python/python.c @@ -1877,6 +1877,7 @@ do_start_initialization () || gdbpy_initialize_xmethods () < 0 || gdbpy_initialize_unwind () < 0 || gdbpy_initialize_membuf () < 0 + || gdbpy_initialize_connection () < 0 || gdbpy_initialize_tui () < 0) return false; @@ -2329,6 +2330,10 @@ Register a TUI window constructor." }, "architecture_names () -> List.\n\ Return a list of all the architecture names GDB understands." }, + { "connections", gdbpy_connections, METH_NOARGS, + "connections () -> List.\n\ +Return a list of gdb.TargetConnection objects." }, + {NULL, NULL, 0, NULL} }; diff --git a/gdb/target-connection.c b/gdb/target-connection.c index a649423e07e..da1c9da74dc 100644 --- a/gdb/target-connection.c +++ b/gdb/target-connection.c @@ -24,6 +24,7 @@ #include "inferior.h" #include "target.h" +#include "observable.h" /* A map between connection number and representative process_stratum target. */ @@ -49,6 +50,9 @@ connection_list_add (process_stratum_target *t) void connection_list_remove (process_stratum_target *t) { + /* Notify about the connection being removed before we reset the + connection number to zero. */ + gdb::observers::connection_removed.notify (t); process_targets.erase (t->connection_number); t->connection_number = 0; } diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp index 3fd6b15cdd9..3f74a47de31 100644 --- a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp @@ -15,18 +15,27 @@ # Test "info inferiors" and "info connections" with multiple targets. +load_lib gdb-python.exp + source $srcdir/$subdir/multi-target.exp.tcl if {![multi_target_prepare]} { return } +# Cache the result of calling skip_python_tests into a local variable. +set run_python_tests [expr ! [skip_python_tests]] + # Test "info inferiors" and "info connections". MULTI_PROCESS # indicates whether the multi-process feature of remote targets is # turned off or on. proc test_info_inferiors {multi_process} { setup "off" + if { $::run_python_tests } { + gdb_test_no_output "source ${::remote_python_file}" "load python file" + } + gdb_test_no_output \ "set remote multiprocess-feature-packet $multi_process" @@ -86,6 +95,18 @@ proc test_info_inferiors {multi_process} { "[connection_num 5 $inf]${ws}core${ws}Local core dump file${ws}" \ ] + if { $::run_python_tests } { + gdb_test "python info_connections()" \ + [multi_line \ + "Num${ws}What${ws}Description" \ + "[connection_num 1 $inf]${ws}native${ws}Native process" \ + "[connection_num 2 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 3 $inf]${ws}core${ws}Local core dump file" \ + "[connection_num 4 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 5 $inf]${ws}core${ws}Local core dump file" \ + ] + } + gdb_test "info inferiors" \ [multi_line \ "Num${ws}Description${ws}Connection${ws}Executable${ws}" \ @@ -96,10 +117,27 @@ proc test_info_inferiors {multi_process} { "[inf_desc 5 $inf]4 \\(extended-remote localhost:$decimal\\)${ws}${binfile}${ws}" \ "[inf_desc 6 $inf]5 \\(core\\)${ws}${binfile}${ws}" \ ] + + if { $::run_python_tests } { + gdb_test "python info_inferiors()" \ + [multi_line \ + "Inferior 1, Connection #1: native" \ + "Inferior 2, Connection #2: extended-remote localhost:$decimal" \ + "Inferior 3, Connection #3: core" \ + "Inferior 4, Connection #1: native" \ + "Inferior 5, Connection #4: extended-remote localhost:$decimal" \ + "Inferior 6, Connection #5: core" \ + ] + } } } } +if { $run_python_tests } { + set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +} + # Test "info inferiors" and "info connections" commands. with_test_prefix "info-inferiors" { foreach_with_prefix multi_process {"on" "off"} { diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py new file mode 100644 index 00000000000..aac561ae683 --- /dev/null +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py @@ -0,0 +1,63 @@ +# Copyright (C) 2021 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 + +# Take a gdb.TargetConnection and return the connection number. +def conn_num(c): + return c.num + +# Takes a gdb.TargetConnection and return a string that is either the +# type, or the type and details (if the details are not None). +def make_target_connection_string(c): + if c.details is None: + return c.type + else: + return "%s %s" % (c.type, c.details) + +# A Python implementation of 'info connections'. Produce output that +# is identical to the output of 'info connections' so we can check +# that aspects of gdb.TargetConnection work correctly. +def info_connections(): + all_connections = sorted(gdb.connections(), key=conn_num) + current_conn = gdb.selected_inferior().connection + what_width = 0 + for c in all_connections: + s = make_target_connection_string(c) + if len(s) > what_width: + what_width = len(s) + + fmt = " Num %%-%ds Description" % what_width + print(fmt % "What") + fmt = "%%s%%-3d %%-%ds %%s" % what_width + for c in all_connections: + if c == current_conn: + prefix = "* " + else: + prefix = " " + + print(fmt % (prefix, c.num, make_target_connection_string(c), + c.description)) + +def inf_num(i): + return i.num + +# .... +def info_inferiors(): + all_inferiors = sorted(gdb.inferiors(), key=inf_num) + for i in gdb.inferiors(): + print("Inferior %d, Connection #%d: %s" % + (i.num, i.connection_num, + make_target_connection_string(i.connection))) diff --git a/gdb/testsuite/gdb.python/py-connection.c b/gdb/testsuite/gdb.python/py-connection.c new file mode 100644 index 00000000000..4b32e704476 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp new file mode 100644 index 00000000000..b805b052f73 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -0,0 +1,69 @@ +# Copyright (C) 2021 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/>. + +# This file is for testing the gdb.TargetConnection API. This API is +# already tested in gdb.multi/multi-target-info-inferiors.exp and +# gdb.python/py-inferior.exp, this file just covers some edge cases +# that are not tested in other places. + +load_lib gdb-python.exp + +standard_testfile + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +# Skip all tests if Python scripting is not enabled. +if { [skip_python_tests] } { continue } + +if ![runto_main] then { + return 0 +} + +# Create a gdb.TargetConnection object and check it is initially +# valid. +gdb_test_no_output "python conn = gdb.selected_inferior().connection" +gdb_test "python print(conn)" \ + "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ + "print gdb.TargetConnection while it is still valid" +gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" + +# Get the connection again, and ensure we get the exact same object. +gdb_test_no_output "python conn2 = gdb.selected_inferior().connection" +gdb_test "python print('Same object: %s' % (conn is conn2))" "True" + +# Now invalidate the connection, and ensure that the is_valid method +# starts to return False. +gdb_test "info connections" "\r\n\\* 1 .*" \ + "info connections while the connection is still around" +gdb_test "disconnect" "" "kill the inferior" \ + "A program is being debugged already\\. Kill it\\? .*y or n. $" "y" +gdb_test "info connections" "No connections\\." \ + "info connections now all the connections have gone" +gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ + "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" + +# Now check that accessing properties of the invalid connection cases +# an error. +gdb_test "python print(conn.num)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.type)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.description)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.details)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" diff --git a/gdb/testsuite/gdb.python/py-inferior.exp b/gdb/testsuite/gdb.python/py-inferior.exp index 894b90a85fc..d5d284a763e 100644 --- a/gdb/testsuite/gdb.python/py-inferior.exp +++ b/gdb/testsuite/gdb.python/py-inferior.exp @@ -51,6 +51,8 @@ gdb_py_test_silent_cmd "python i0 = inferiors\[0\]" "get first inferior" 0 gdb_test "python print ('result = %s' % (i0 == inferiors\[0\]))" " = True" "test equality comparison (true)" gdb_test "python print ('result = %s' % i0.num)" " = \[0-9\]+" "test Inferior.num" gdb_test "python print ('result = %s' % i0.connection_num)" " = \[0-9\]+" "test Inferior.connection_num" +gdb_test "python print ('result = %s' % (i0.connection_num == i0.connection.num))" " = True" \ + "Inferior.connection_num equals Inferior.connection.num" gdb_test "python print ('result = %s' % i0.pid)" " = \[0-9\]+" "test Inferior.pid" gdb_test "python print ('result = %s' % i0.was_attached)" " = False" "test Inferior.was_attached" gdb_test "python print (i0.threads ())" "\\(<gdb.InferiorThread object at 0x\[\[:xdigit:\]\]+>,\\)" "test Inferior.threads" @@ -264,6 +266,8 @@ with_test_prefix "is_valid" { "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].connection_num)" \ "RuntimeError: Inferior no longer exists.*" + gdb_test "python print (inf_list\[1\].connection)" \ + "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].pid)" \ "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].was_attached)" \ @@ -281,7 +285,9 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 1" ".*" "switch to first inferior" gdb_test "py print (gdb.selected_inferior().num)" "1" "first inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "1" \ - "first inferior's connection" + "first inferior's connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" "1" \ + "first inferior's connection number, though connection object" # Figure out if inf 1 has a native target. set inf_1_is_native [gdb_is_target_native] @@ -289,6 +295,8 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 3" ".*" "switch to third inferior" gdb_test "py print (gdb.selected_inferior().num)" "3" "third inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "None" \ + "third inferior's None connection number" + gdb_test "py print (gdb.selected_inferior().connection)" "None" \ "third inferior's None connection" gdb_test "target native" "Done. Use the \"run\" command to start a process." \ "target for the third inferior" @@ -302,7 +310,15 @@ with_test_prefix "selected_inferior" { } gdb_test "py print (gdb.selected_inferior().connection_num)" \ "$expected_connection_num" \ - "third inferior's native connection" + "third inferior's native connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" \ + "$expected_connection_num" \ + "third inferior's native connection number, though connection object" + + # Test printing of gdb.TargetConnection object. + gdb_test "py print (gdb.selected_inferior().connection)" \ + "<gdb.TargetConnection num=${expected_connection_num}, what=\"\[^\"\]+\">" \ + "print a connection object" gdb_test "inferior 1" ".*" "switch back to first inferior" gdb_test_no_output "remove-inferiors 3" "remove second inferior" -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv5 2/4] gdb: make packet_command function available outside remote.c 2021-10-22 17:10 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 1/4] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess @ 2021-10-22 17:10 ` Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess ` (3 subsequent siblings) 5 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 17:10 UTC (permalink / raw) To: gdb-patches In a later commit I will add a Python API to access the 'maint packet' functionality, that is, sending a user specified packet to the target. To make implementing this easier, this commit refactors how this command is currently implemented so that the packet_command function is now global. The new global send_remote_packet function takes an object that is an implementation of an abstract interface. Two functions within this interface are then called, one just before a packet is sent to the remote target, and one when the reply has been received from the remote target. Using an interface object in this way allows (1) for the error checking to be done before the first callback is made, this means we only print out what packet it being sent once we know we are going to actually send it, and (2) we don't need to make a copy of the reply if all we want to do is print it. The only user visible changes after this commit are the error messages, which I've changed to be less 'maint packet' command focused, this will make them (I hope) better for when send_remote_packet can be called from Python code. So: "command can only be used with remote target" Becomes: "packets can only be sent to a remote target" And: "remote-packet command requires packet text as argument" Becomes: "a remote packet must not be empty" --- gdb/remote.c | 67 +++++++++++++++++++++++++++++++++++----------------- gdb/remote.h | 34 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/gdb/remote.c b/gdb/remote.c index 0fb42753596..57f35892527 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -956,8 +956,6 @@ class remote_target : public process_stratum_target bool vcont_r_supported (); - void packet_command (const char *args, int from_tty); - private: /* data fields */ /* The remote state. Don't reference this directly. Use the @@ -11593,34 +11591,59 @@ remote_target::memory_map () return result; } -static void -packet_command (const char *args, int from_tty) +/* Set of callbacks used to implement the 'maint packet' command. */ + +struct cli_packet_command_callbacks : public send_remote_packet_callbacks { - remote_target *remote = get_current_remote_target (); + /* Called before the packet is sent. PACKET_STR is the packet content + before the protocol specific prefix, suffix, and escaping is added. */ - if (remote == nullptr) - error (_("command can only be used with remote target")); + void sending (const char *packet_str) override + { + puts_filtered ("sending: "); + print_packet (packet_str); + puts_filtered ("\n"); + } - remote->packet_command (args, from_tty); -} + /* Called with BUF, the reply from the remote target. */ + + void received (const gdb::char_vector &buf) override + { + puts_filtered ("received: "); + print_packet (buf.data ()); + puts_filtered ("\n"); + } +}; + +/* See remote.h. */ void -remote_target::packet_command (const char *args, int from_tty) +send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks) { - if (!args) - error (_("remote-packet command requires packet text as argument")); + if (packet_str == nullptr || *packet_str == '\0') + error (_("a remote packet must not be empty")); - puts_filtered ("sending: "); - print_packet (args); - puts_filtered ("\n"); - putpkt (args); + remote_target *remote = get_current_remote_target (); + if (remote == nullptr) + error (_("packets can only be sent to a remote target")); - remote_state *rs = get_remote_state (); + callbacks->sending (packet_str); - getpkt (&rs->buf, 0); - puts_filtered ("received: "); - print_packet (rs->buf.data ()); - puts_filtered ("\n"); + remote->putpkt (packet_str); + remote_state *rs = remote->get_remote_state (); + remote->getpkt (&rs->buf, 0); + + callbacks->received (rs->buf); +} + +/* Entry point for the 'maint packet' command. */ + +static void +cli_packet_command (const char *args, int from_tty) +{ + cli_packet_command_callbacks cb; + send_remote_packet (args, &cb); } #if 0 @@ -14899,7 +14922,7 @@ Argument is a single section name (default: all loaded sections).\n\ To compare only read-only loaded sections, specify the -r option."), &cmdlist); - add_cmd ("packet", class_maintenance, packet_command, _("\ + add_cmd ("packet", class_maintenance, cli_packet_command, _("\ Send an arbitrary packet to a remote target.\n\ maintenance packet TEXT\n\ If GDB is talking to an inferior via the GDB serial protocol, then\n\ diff --git a/gdb/remote.h b/gdb/remote.h index 46bfa01fc79..cd91be8decb 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -78,4 +78,38 @@ extern int remote_register_number_and_offset (struct gdbarch *gdbarch, extern void remote_notif_get_pending_events (remote_target *remote, struct notif_client *np); extern bool remote_target_is_non_stop_p (remote_target *t); + +/* An abstract class that represents the set of callbacks that are made + from the send_remote_packet function (declared below). */ + +struct send_remote_packet_callbacks +{ + /* The SENDING callback is called once send_remote_packet has performed + its error checking and setup, just before the packet is sent to the + remote target. PACKET_STR is the content of the packet that will be + sent (before any of the protocol specific prefix, suffix, or escaping + is applied). */ + + virtual void sending (const char *packet_str) = 0; + + /* The RECEIVED callback is called once a reply has been received from + the remote target. The content of the reply is in BUF which can't be + modified, and which is not guaranteed to remain valid after the + RECEIVED call has returned. If you need to preserve the contents of + BUF then a copy should be taken. */ + + virtual void received (const gdb::char_vector &buf) = 0; +}; + +/* Send PACKET_STR the current remote target. If PACKET_STR is nullptr, or + is the empty string, then an error is thrown. If the current target is + not a remote target then an error is thrown. + + Calls CALLBACKS->sending() just before the packet is sent to the remote + target, and calls CALLBACKS->received() with the reply once this is + received from the remote target. */ + +extern void send_remote_packet (const char *packet_str, + send_remote_packet_callbacks *callbacks); + #endif -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv5 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-10-22 17:10 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 1/4] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 2/4] gdb: make packet_command function available outside remote.c Andrew Burgess @ 2021-10-22 17:10 ` Andrew Burgess 2021-11-15 2:08 ` Simon Marchi 2021-10-22 17:10 ` [PATCHv5 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet Andrew Burgess ` (2 subsequent siblings) 5 siblings, 1 reply; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 17:10 UTC (permalink / raw) To: gdb-patches This commits adds a new sub-class of gdb.TargetConnection, gdb.RemoteTargetConnection. This sub-class is created for all 'remote' and 'extended-remote' targets. This new sub-class has one additional method over its base class, 'send_packet'. This new method is equivalent to the 'maint packet' CLI command, it allows a custom packet to be sent to a remote target. The result of calling RemoteTargetConnection.send_packet is a string containing the reply that came from the remote. --- gdb/NEWS | 8 +- gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 40 ++++- gdb/python/py-connection.c | 173 +++++++++++++++++++- gdb/remote.c | 9 + gdb/remote.h | 8 + gdb/testsuite/gdb.python/py-connection.exp | 14 +- gdb/testsuite/gdb.python/py-send-packet.c | 22 +++ gdb/testsuite/gdb.python/py-send-packet.exp | 56 +++++++ gdb/testsuite/gdb.python/py-send-packet.py | 81 +++++++++ 10 files changed, 401 insertions(+), 11 deletions(-) create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py diff --git a/gdb/NEWS b/gdb/NEWS index fd1ca0efbe5..205cb740a52 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -50,7 +50,9 @@ maint show internal-warning backtrace entry is a string. ** New gdb.TargetConnection object type that represents a connection - (as displayed by the 'info connections' command). + (as displayed by the 'info connections' command). A sub-class, + gdb.RemoteTargetConnection, is used to represent 'remote' and + 'extended-remote' connections. ** The gdb.Inferior type now has a 'connection' property which is an instance of gdb.TargetConnection, the connection used by this @@ -64,6 +66,10 @@ maint show internal-warning backtrace ** New gdb.connections() function that returns a list of all currently active connections. + ** New gdb.RemoteTargetConnection.send_packet(STRING) method. This + is equivalent to the existing 'maint packet' CLI command; it + allows a user specified packet to be sent to the remote target. + *** Changes in GDB 11 * The 'set disassembler-options' command now supports specifying options diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index 631a7c03b31..89cf86210ab 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -39269,6 +39269,7 @@ error stream. This is @samp{on} by default for @code{internal-error} and @samp{off} by default for @code{internal-warning}. +@anchor{maint packet} @kindex maint packet @item maint packet @var{text} If @value{GDBN} is talking to an inferior via the serial protocol, diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 2234bbe1891..0fd31ca6cb3 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -5989,9 +5989,9 @@ Examples of different connection types are @samp{native} and @samp{remote}. @xref{Inferiors Connections and Programs}. -@value{GDBN} uses the @code{gdb.TargetConnection} object type to -represent a connection in Python code. To get a list of all -connections use @code{gdb.connections} +Connections in @value{GDBN} are represented as instances of +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a +list of all connections use @code{gdb.connections} (@pxref{gdbpy_connections,,gdb.connections}). To get the connection for a single @code{gdb.Inferior} read its @@ -6044,6 +6044,40 @@ to the remote target. @end defvar +The @code{gdb.RemoteTargetConnection} class is a sub-class of +@code{gdb.TargetConnection}, and is used to represent @samp{remote} +and @samp{extended-remote} connections. In addition to the attributes +and methods available from the @code{gdb.TargetConnection} base class, +a @code{gdb.RemoteTargetConnection} has the following method: + +@kindex maint packet +@defun RemoteTargetConnection.send_packet (@var{packet}) +This method sends @var{packet}, which should be a non-empty string, to +the remote target and returns the response. If @var{packet} is not a +string, or is the empty string, then an exception of type +@code{ValueError} is thrown. + +In Python 3, the response is returned as a @code{bytes} buffer, if it +is known that the response can be represented as a string then this +can be decoded from the buffer. For example, if it is known that the +response is an @code{"ascii"} string: + +@smallexample +remote_connection.send_packet("some_packet").decode("ascii") +@end smallexample + +In Python 2, the response is returned as a @code{str}, which may +contain non-printable characters. + +The prefix, suffix, and checksum (as required by the remote serial +protocol) are automatically added to the outgoing packet, and removed +from the incoming packet before the contents of the reply are +returned. + +This is equivalent to the @code{maintenance packet} command +(@pxref{maint packet}). +@end defun + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c index f1dfa26e39c..d282c225e9c 100644 --- a/gdb/python/py-connection.c +++ b/gdb/python/py-connection.c @@ -26,6 +26,8 @@ #include "py-events.h" #include "py-event.h" #include "arch-utils.h" +#include "remote.h" +#include "charset.h" #include <map> @@ -47,6 +49,9 @@ struct connection_object extern PyTypeObject connection_object_type CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); +extern PyTypeObject remote_connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("remote_connection_object"); + /* Require that CONNECTION be valid. */ #define CONNPY_REQUIRE_VALID(connection) \ do { \ @@ -81,8 +86,14 @@ target_to_connection_object (process_stratum_target *target) auto conn_obj_iter = all_connection_objects.find (target); if (conn_obj_iter == all_connection_objects.end ()) { - conn_obj.reset (PyObject_New (connection_object, - &connection_object_type)); + PyTypeObject *type; + + if (is_remote_target (target)) + type = &remote_connection_object_type; + else + type = &connection_object_type; + + conn_obj.reset (PyObject_New (connection_object, type)); if (conn_obj == nullptr) return nullptr; conn_obj->target = target; @@ -284,9 +295,111 @@ gdbpy_initialize_connection (void) (PyObject *) &connection_object_type) < 0) return -1; + if (PyType_Ready (&remote_connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "RemoteTargetConnection", + (PyObject *) &remote_connection_object_type) < 0) + return -1; + return 0; } +/* Set of callbacks used to implement gdb.send_packet. */ + +struct py_send_packet_callbacks : public send_remote_packet_callbacks +{ + /* Constructor, initialise the result to None. */ + + py_send_packet_callbacks () + : m_result (Py_None) + { /* Nothing. */ } + + /* There's nothing to do when the packet is sent. */ + + void sending (const char *args) override + { /* Nothing. */ } + + /* When the result is returned create a Python string and assign this + into the result member variable. */ + + void received (const gdb::char_vector &buf) override + { + /* m_result is initialized to None, leave it untouched unless we got + back a useful result. */ + if (buf.data ()[0] != '\0') + { + PyObject *result; + +#ifdef IS_PY3K + result = Py_BuildValue("y#", buf.data (), strlen (buf.data ())); +#else + result = PyString_FromStringAndSize (buf.data (), + strlen (buf.data ())); +#endif + + if (result != nullptr) + m_result = gdbpy_ref<> (result); + } + } + + /* Get a reference to the result as a Python object. */ + + gdbpy_ref<> result () const + { + return m_result; + } + +private: + + /* A reference to a valid result value. This is initialized in the + constructor, and so will always point to a valid value, even if this + is just None. */ + + gdbpy_ref<> m_result; +}; + +/* Implement RemoteTargetConnection.send_packet function. Send a packet to + the target identified by SELF. The connection must still be valid, and + the packet to be sent must be non-empty, otherwise an exception will be + thrown. */ + +static PyObject * +connpy_send_packet (PyObject *self, PyObject *args, PyObject *kw) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + static const char *keywords[] = {"packet", nullptr}; + const char *packet_str = nullptr; + + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s", keywords, + &packet_str)) + return nullptr; + + if (packet_str == nullptr || *packet_str == '\0') + { + PyErr_SetString (PyExc_ValueError, _("Invalid remote packet")); + return nullptr; + } + + try + { + scoped_restore_current_thread restore_thread; + switch_to_target_no_thread (conn->target); + + py_send_packet_callbacks callbacks; + send_remote_packet (packet_str, &callbacks); + return callbacks.result ().release (); + } + catch (const gdb_exception &except) + { + gdbpy_convert_exception (except); + return nullptr; + } +} + /* Global initialization for this file. */ void _initialize_py_connection (); @@ -307,6 +420,17 @@ Return true if this TargetConnection is valid, false if not." }, { NULL } }; +/* Methods for the gdb.RemoteTargetConnection object type. */ + +static PyMethodDef remote_connection_object_methods[] = +{ + { "send_packet", (PyCFunction) connpy_send_packet, + METH_VARARGS | METH_KEYWORDS, + "send_packet (PACKET) -> String\n\ +Send PACKET to a remote target, return the reply as a string." }, + { NULL } +}; + /* Attributes for the gdb.TargetConnection object type. */ static gdb_PyGetSetDef connection_object_getset[] = @@ -345,7 +469,7 @@ PyTypeObject connection_object_type = 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ "GDB target connection object", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ @@ -364,3 +488,46 @@ PyTypeObject connection_object_type = 0, /* tp_init */ 0 /* tp_alloc */ }; + +/* Define the gdb.RemoteTargetConnection object type. */ + +PyTypeObject remote_connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.RemoteTargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB remote target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + remote_connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + &connection_object_type, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/remote.c b/gdb/remote.c index 57f35892527..27af76d8dd2 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -994,6 +994,15 @@ class extended_remote_target final : public remote_target bool supports_disable_randomization () override; }; +/* See remote.h. */ + +bool +is_remote_target (process_stratum_target *target) +{ + remote_target *rt = dynamic_cast<remote_target *> (target); + return rt != nullptr; +} + /* Per-program-space data key. */ static const struct program_space_key<char, gdb::xfree_deleter<char>> remote_pspace_data; diff --git a/gdb/remote.h b/gdb/remote.h index cd91be8decb..22455e8f8d5 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -24,6 +24,8 @@ struct target_desc; struct remote_target; +class process_stratum_target; + /* True when printing "remote" debug statements is enabled. */ extern bool remote_debug; @@ -112,4 +114,10 @@ struct send_remote_packet_callbacks extern void send_remote_packet (const char *packet_str, send_remote_packet_callbacks *callbacks); + +/* Return true if TARGET is a remote, or extended-remote target, otherwise, + return false. */ + +extern bool is_remote_target (process_stratum_target *target); + #endif diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp index b805b052f73..96c83781839 100644 --- a/gdb/testsuite/gdb.python/py-connection.exp +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -33,12 +33,18 @@ if ![runto_main] then { return 0 } +if { [target_info exists gdb_protocol] } { + set connection_type "RemoteTargetConnection" +} else { + set connection_type "TargetConnection" +} + # Create a gdb.TargetConnection object and check it is initially # valid. gdb_test_no_output "python conn = gdb.selected_inferior().connection" gdb_test "python print(conn)" \ - "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ - "print gdb.TargetConnection while it is still valid" + "<gdb.${connection_type} num=1, what=\"\[^\"\]+\">" \ + "print gdb.${connection_type} while it is still valid" gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" # Get the connection again, and ensure we get the exact same object. @@ -53,8 +59,8 @@ gdb_test "disconnect" "" "kill the inferior" \ "A program is being debugged already\\. Kill it\\? .*y or n. $" "y" gdb_test "info connections" "No connections\\." \ "info connections now all the connections have gone" -gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ - "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn)" "<gdb.${connection_type} \\(invalid\\)>" \ + "print gdb.${connection_type} now its invalid" gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" # Now check that accessing properties of the invalid connection cases diff --git a/gdb/testsuite/gdb.python/py-send-packet.c b/gdb/testsuite/gdb.python/py-send-packet.c new file mode 100644 index 00000000000..bfe52c018d4 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp new file mode 100644 index 00000000000..dde5c270f20 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.exp @@ -0,0 +1,56 @@ +# Copyright (C) 2021 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 the gdb.RemoteTargetConnection.send_packet API. This is done +# by connecting to a remote target and fetching the thread list in two +# ways, first, we manually send the packets required to read the +# thread list using gdb.TargetConnection.send_packet, then we compare +# the results to the thread list using the standard API calls. + +load_lib gdb-python.exp +load_lib gdbserver-support.exp + +standard_testfile + +if {[skip_gdbserver_tests]} { + return 0 +} + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +if { [skip_python_tests] } { + return 0 +} + +# Make sure we're disconnected, in case we're testing with an +# extended-remote board, therefore already connected. +gdb_test "disconnect" ".*" + +gdbserver_run "" + +# Source the python script. +set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +gdb_test "source $remote_python_file" "Sourcing complete\\." \ + "source ${testfile}.py script" + +# The test is actually written in the Python script. Run it now. +gdb_test "python run_send_packet_test()" "Send packet test passed" + +# Check the string representation of a remote target connection. +gdb_test "python print(gdb.selected_inferior().connection)" \ + "<gdb.RemoteTargetConnection num=$decimal, what=\".*\">" diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py new file mode 100644 index 00000000000..35754164a98 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.py @@ -0,0 +1,81 @@ +# Copyright (C) 2021 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 xml.etree.ElementTree as ET +import gdb + +# Make use of gdb.RemoteTargetConnection.send_packet to fetch the +# thread list from the remote target. +# +# Sending existing serial protocol packets like this is not a good +# idea, there should be better ways to get this information using an +# official API, this is just being used as a test case. +# +# Really, the send_packet API would be used to send target +# specific packets to the target, but these are, by definition, target +# specific, so hard to test in a general testsuite. +def get_thread_list_str(): + start_pos = 0 + thread_desc = "" + conn = gdb.selected_inferior().connection + if not isinstance(conn, gdb.RemoteTargetConnection): + raise gdb.GdbError("connection is the wrong type") + while True: + str = conn.send_packet("qXfer:threads:read::%d,200" % start_pos).decode("ascii") + start_pos += 200 + c = str[0] + str = str[1:] + thread_desc += str + if c == "l": + break + return thread_desc + + +# Use gdb.RemoteTargetConnection.send_packet to manually fetch the +# thread list, then extract the thread list using the gdb.Inferior and +# gdb.InferiorThread API. Compare the two results to ensure we +# managed to successfully read the thread list from the remote. +def run_send_packet_test(): + # Find the IDs of all current threads. + all_threads = {} + for inf in gdb.inferiors(): + for thr in inf.threads(): + id = "p%x.%x" % (thr.ptid[0], thr.ptid[1]) + all_threads[id] = False + + # Now fetch the thread list from the remote, and parse the XML. + str = get_thread_list_str() + threads_xml = ET.fromstring(str) + + # Look over all threads in the XML list and check we expected to + # find them, mark the ones we do find. + for thr in threads_xml: + id = thr.get("id") + if not id in all_threads: + raise "found unexpected thread in remote thread list" + else: + all_threads[id] = True + + # Check that all the threads were found in the XML list. + for id in all_threads: + if not all_threads[id]: + raise "thread missingt from remote thread list" + + # Test complete. + print("Send packet test passed") + + +# Just to indicate the file was sourced correctly. +print("Sourcing complete.") -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv5 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-10-22 17:10 ` [PATCHv5 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess @ 2021-11-15 2:08 ` Simon Marchi 2021-11-15 9:25 ` Andrew Burgess 0 siblings, 1 reply; 52+ messages in thread From: Simon Marchi @ 2021-11-15 2:08 UTC (permalink / raw) To: Andrew Burgess, gdb-patches Just some minor comments (one about a possible bug), but otherwise this looks fine to me. > diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi > index 2234bbe1891..0fd31ca6cb3 100644 > --- a/gdb/doc/python.texi > +++ b/gdb/doc/python.texi > @@ -5989,9 +5989,9 @@ > Examples of different connection types are @samp{native} and > @samp{remote}. @xref{Inferiors Connections and Programs}. > > -@value{GDBN} uses the @code{gdb.TargetConnection} object type to > -represent a connection in Python code. To get a list of all > -connections use @code{gdb.connections} > +Connections in @value{GDBN} are represented as instances of > +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a > +list of all connections use @code{gdb.connections} > (@pxref{gdbpy_connections,,gdb.connections}). > > To get the connection for a single @code{gdb.Inferior} read its > @@ -6044,6 +6044,40 @@ > to the remote target. > @end defvar > > +The @code{gdb.RemoteTargetConnection} class is a sub-class of > +@code{gdb.TargetConnection}, and is used to represent @samp{remote} > +and @samp{extended-remote} connections. In addition to the attributes > +and methods available from the @code{gdb.TargetConnection} base class, > +a @code{gdb.RemoteTargetConnection} has the following method: In your opinion, would it be ok (backwards-compatible-wise) to introduce a gdb.ExtendedRemoteTargetConnection class in the future, should we ever need that? The only case that I think of that would break is if somebody does: type(my_target) is gdb.RemoteTargetConnection where my_target is an extended-remote connection. That expression would return True today, and would return False after adding gdb.ExtendedRemoteTargetConnection. But if that could would use isinstance, then it would be fine. > @@ -284,9 +295,111 @@ gdbpy_initialize_connection (void) > (PyObject *) &connection_object_type) < 0) > return -1; > > + if (PyType_Ready (&remote_connection_object_type) < 0) > + return -1; > + > + if (gdb_pymodule_addobject (gdb_module, "RemoteTargetConnection", > + (PyObject *) &remote_connection_object_type) < 0) > + return -1; > + > return 0; > } > > +/* Set of callbacks used to implement gdb.send_packet. */ > + > +struct py_send_packet_callbacks : public send_remote_packet_callbacks > +{ > + /* Constructor, initialise the result to None. */ > + > + py_send_packet_callbacks () > + : m_result (Py_None) > + { /* Nothing. */ } I think you might need to incref Py_None here. the gdbpy_ref will decref on destruction or when its value changes, but you never incref it. > + > + /* There's nothing to do when the packet is sent. */ > + > + void sending (const char *args) override > + { /* Nothing. */ } > + > + /* When the result is returned create a Python string and assign this > + into the result member variable. */ > + > + void received (const gdb::char_vector &buf) override > + { > + /* m_result is initialized to None, leave it untouched unless we got > + back a useful result. */ > + if (buf.data ()[0] != '\0') > + { > + PyObject *result; > + > +#ifdef IS_PY3K > + result = Py_BuildValue("y#", buf.data (), strlen (buf.data ())); Missing space. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv5 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-11-15 2:08 ` Simon Marchi @ 2021-11-15 9:25 ` Andrew Burgess 2021-11-15 13:16 ` Simon Marchi 0 siblings, 1 reply; 52+ messages in thread From: Andrew Burgess @ 2021-11-15 9:25 UTC (permalink / raw) To: Simon Marchi; +Cc: Andrew Burgess, gdb-patches * Simon Marchi via Gdb-patches <gdb-patches@sourceware.org> [2021-11-14 21:08:40 -0500]: > Just some minor comments (one about a possible bug), but otherwise this > looks fine to me. > > > diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi > > index 2234bbe1891..0fd31ca6cb3 100644 > > --- a/gdb/doc/python.texi > > +++ b/gdb/doc/python.texi > > @@ -5989,9 +5989,9 @@ > > Examples of different connection types are @samp{native} and > > @samp{remote}. @xref{Inferiors Connections and Programs}. > > > > -@value{GDBN} uses the @code{gdb.TargetConnection} object type to > > -represent a connection in Python code. To get a list of all > > -connections use @code{gdb.connections} > > +Connections in @value{GDBN} are represented as instances of > > +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a > > +list of all connections use @code{gdb.connections} > > (@pxref{gdbpy_connections,,gdb.connections}). > > > > To get the connection for a single @code{gdb.Inferior} read its > > @@ -6044,6 +6044,40 @@ > > to the remote target. > > @end defvar > > > > +The @code{gdb.RemoteTargetConnection} class is a sub-class of > > +@code{gdb.TargetConnection}, and is used to represent @samp{remote} > > +and @samp{extended-remote} connections. In addition to the attributes > > +and methods available from the @code{gdb.TargetConnection} base class, > > +a @code{gdb.RemoteTargetConnection} has the following method: > > In your opinion, would it be ok (backwards-compatible-wise) to introduce > a gdb.ExtendedRemoteTargetConnection class in the future, should we ever need > that? The only case that I think of that would break is if somebody > does: > > type(my_target) is gdb.RemoteTargetConnection > > where my_target is an extended-remote connection. That expression would > return True today, and would return False after adding > gdb.ExtendedRemoteTargetConnection. But if that could would use > isinstance, then it would be fine. I did worry about this too. But I don't think we should limit ourselves to just worrying about remote/extended-remote targets, what if, in the future, we wanted special handling for native targets, or simulator targets, or any other target type.... I did consider creating different object types for each possible target right up front, but figured that might be overkill. Given you're also concerned about this, maybe we can avoid the problem by adding this text to the manual: Currently there is only a single sub-class of @code{gdb.TargetConnection}, @code{gdb.RemoteTargetConnection}, however, additional sub-classes may be added in future releases of @value{GDBN}. As a result you should avoid writing code like: @smallexample conn = gdb.selected_inferior().connection if type(conn) is gdb.RemoteTargetConnection: print("This is a remote target connection") @end smallexample @noindent As this may fail when more connection types are added. Instead, you should write: @smallexample conn = gdb.selected_inferior().connection if isinstance(conn, gdb.RemoteTargetConnection): print("This is a remote target connection") @end smallexample We basically reserve the right to add additional connection type classes in the future, and tell folk not to use 'type(object)' but always use 'isinstance(object, type)'. If they do break the rules it's still not a massive task to fix their code, and if they do follow the rules, they'll be fine. What do you think? Thanks, Andrew > > > @@ -284,9 +295,111 @@ gdbpy_initialize_connection (void) > > (PyObject *) &connection_object_type) < 0) > > return -1; > > > > + if (PyType_Ready (&remote_connection_object_type) < 0) > > + return -1; > > + > > + if (gdb_pymodule_addobject (gdb_module, "RemoteTargetConnection", > > + (PyObject *) &remote_connection_object_type) < 0) > > + return -1; > > + > > return 0; > > } > > > > +/* Set of callbacks used to implement gdb.send_packet. */ > > + > > +struct py_send_packet_callbacks : public send_remote_packet_callbacks > > +{ > > + /* Constructor, initialise the result to None. */ > > + > > + py_send_packet_callbacks () > > + : m_result (Py_None) > > + { /* Nothing. */ } > > I think you might need to incref Py_None here. the gdbpy_ref will > decref on destruction or when its value changes, but you never incref > it. > > > + > > + /* There's nothing to do when the packet is sent. */ > > + > > + void sending (const char *args) override > > + { /* Nothing. */ } > > + > > + /* When the result is returned create a Python string and assign this > > + into the result member variable. */ > > + > > + void received (const gdb::char_vector &buf) override > > + { > > + /* m_result is initialized to None, leave it untouched unless we got > > + back a useful result. */ > > + if (buf.data ()[0] != '\0') > > + { > > + PyObject *result; > > + > > +#ifdef IS_PY3K > > + result = Py_BuildValue("y#", buf.data (), strlen (buf.data ())); > > Missing space. > > Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv5 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-11-15 9:25 ` Andrew Burgess @ 2021-11-15 13:16 ` Simon Marchi 0 siblings, 0 replies; 52+ messages in thread From: Simon Marchi @ 2021-11-15 13:16 UTC (permalink / raw) To: Andrew Burgess; +Cc: Andrew Burgess, gdb-patches On 2021-11-15 04:25, Andrew Burgess wrote: > * Simon Marchi via Gdb-patches <gdb-patches@sourceware.org> [2021-11-14 21:08:40 -0500]: > >> Just some minor comments (one about a possible bug), but otherwise this >> looks fine to me. >> >>> diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi >>> index 2234bbe1891..0fd31ca6cb3 100644 >>> --- a/gdb/doc/python.texi >>> +++ b/gdb/doc/python.texi >>> @@ -5989,9 +5989,9 @@ >>> Examples of different connection types are @samp{native} and >>> @samp{remote}. @xref{Inferiors Connections and Programs}. >>> >>> -@value{GDBN} uses the @code{gdb.TargetConnection} object type to >>> -represent a connection in Python code. To get a list of all >>> -connections use @code{gdb.connections} >>> +Connections in @value{GDBN} are represented as instances of >>> +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a >>> +list of all connections use @code{gdb.connections} >>> (@pxref{gdbpy_connections,,gdb.connections}). >>> >>> To get the connection for a single @code{gdb.Inferior} read its >>> @@ -6044,6 +6044,40 @@ >>> to the remote target. >>> @end defvar >>> >>> +The @code{gdb.RemoteTargetConnection} class is a sub-class of >>> +@code{gdb.TargetConnection}, and is used to represent @samp{remote} >>> +and @samp{extended-remote} connections. In addition to the attributes >>> +and methods available from the @code{gdb.TargetConnection} base class, >>> +a @code{gdb.RemoteTargetConnection} has the following method: >> >> In your opinion, would it be ok (backwards-compatible-wise) to introduce >> a gdb.ExtendedRemoteTargetConnection class in the future, should we ever need >> that? The only case that I think of that would break is if somebody >> does: >> >> type(my_target) is gdb.RemoteTargetConnection >> >> where my_target is an extended-remote connection. That expression would >> return True today, and would return False after adding >> gdb.ExtendedRemoteTargetConnection. But if that could would use >> isinstance, then it would be fine. > > I did worry about this too. But I don't think we should limit > ourselves to just worrying about remote/extended-remote targets, what > if, in the future, we wanted special handling for native targets, or > simulator targets, or any other target type.... > > I did consider creating different object types for each possible > target right up front, but figured that might be overkill. Given > you're also concerned about this, maybe we can avoid the problem by > adding this text to the manual: > > Currently there is only a single sub-class of > @code{gdb.TargetConnection}, @code{gdb.RemoteTargetConnection}, > however, additional sub-classes may be added in future releases of > @value{GDBN}. As a result you should avoid writing code like: > > @smallexample > conn = gdb.selected_inferior().connection > if type(conn) is gdb.RemoteTargetConnection: > print("This is a remote target connection") > @end smallexample > > @noindent > As this may fail when more connection types are added. Instead, you I would say "As this may fail if @code{gdb.RemoteTargetConnection} sub-classes are added in the future". > should write: > > @smallexample > conn = gdb.selected_inferior().connection > if isinstance(conn, gdb.RemoteTargetConnection): > print("This is a remote target connection") > @end smallexample > > We basically reserve the right to add additional connection type > classes in the future, and tell folk not to use 'type(object)' but > always use 'isinstance(object, type)'. If they do break the rules > it's still not a massive task to fix their code, and if they do follow > the rules, they'll be fine. > > What do you think? That sounds good to me. I also thought about creating all types up front, but that would just cover the target connection types that exist today, what if a new one is introduced in the future? Not that likely, but still. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv5 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet 2021-10-22 17:10 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess ` (2 preceding siblings ...) 2021-10-22 17:10 ` [PATCHv5 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess @ 2021-10-22 17:10 ` Andrew Burgess 2021-11-15 2:44 ` Simon Marchi 2021-11-09 10:04 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 0/3] " Andrew Burgess 5 siblings, 1 reply; 52+ messages in thread From: Andrew Burgess @ 2021-10-22 17:10 UTC (permalink / raw) To: gdb-patches Previous the 'maint packet' command, and the recently added Python API RemoteTargetConnection.send_packet, could only cope with replies that contained printable string characters. The code implementing these two features treated the reply buffer as a string, so any null-bytes would terminate the output, and if some other non-printable character was encountered, GDB would just try to print it anyway... The only way I've found to reproduce this easily is to try and examine the auxv data, which is binary in nature, here's a GDB session before my patch: (gdb) target remote :54321 ... (gdb) maint packet qXfer:auxv:read::0,1000 sending: "qXfer:auxv:read::0,1000" received: "l!" (gdb) And after my patch: (gdb) target remote :54321 ... (gdb) maint packet qXfer:auxv:read::0,1000 sending: "qXfer:auxv:read::0,1000" received: "l!\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xfc\xf7\xff\x7f\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xfb\xeb\xbf\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00@\x00@\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x10\xfd\xf7\xff\x7f\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00P\x10@\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00y\xba\xff\xff\xff\x7f\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\xdf\xef\xff\xff\xff\x7f\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x89\xba\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" (gdb) The binary contents of the reply are now printed as escaped hex. Similarly, here's the same packet fetched, and printed, using this Python script: inf = gdb.selected_inferior() conn = inf.connection res = conn.send_packet("qXfer:auxv:read::0,1000") print ("Got: %s" % str(res)) The GDB session: (gdb) source fetch-auxv.py Got: b'l!\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xfc\xf7\xff\x7f\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xfb\xeb\xbf\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00@\x00@\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x10\xfd\xf7\xff\x7f\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00P\x10@\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00y\xba\xff\xff\xff\x7f\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\xdf\xef\xff\xff\xff\x7f\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x89\xba\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' The testing for this change is pretty weak right now, I just fetch the auxv data using 'maint packet' and from Python, and check that the two results match. I also check that the results, when converted to a string, contains some hex-encoded characters (e.g. \xf0, etc). --- gdb/python/py-connection.c | 21 ++++---- gdb/remote.c | 57 ++++++++++++--------- gdb/remote.h | 10 ++-- gdb/testsuite/gdb.python/py-send-packet.exp | 33 ++++++++++++ gdb/testsuite/gdb.python/py-send-packet.py | 28 ++++++++++ 5 files changed, 112 insertions(+), 37 deletions(-) diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c index d282c225e9c..a0bea11694d 100644 --- a/gdb/python/py-connection.c +++ b/gdb/python/py-connection.c @@ -320,22 +320,25 @@ struct py_send_packet_callbacks : public send_remote_packet_callbacks void sending (const char *args) override { /* Nothing. */ } - /* When the result is returned create a Python string and assign this - into the result member variable. */ + /* When the result is returned create a Python object and assign this + into M_RESULT. If for any reason we can't create a Python object to + represent the result then M_RESULT is left untouched (it is + initialised to reference Py_None), so after this call M_RESULT will + either be an object representing the reply, or None. */ - void received (const gdb::char_vector &buf) override + void received (const gdb::char_vector &buf, int bytes) override { - /* m_result is initialized to None, leave it untouched unless we got - back a useful result. */ - if (buf.data ()[0] != '\0') + gdb_assert (bytes >= 0); + + if (bytes > 0 && buf.data ()[0] != '\0') { PyObject *result; #ifdef IS_PY3K - result = Py_BuildValue("y#", buf.data (), strlen (buf.data ())); + result = Py_BuildValue("y#", buf.data (), bytes); #else - result = PyString_FromStringAndSize (buf.data (), - strlen (buf.data ())); + //result = PyString_FromStringAndSize (buf.data (), bytes); + result = PyByteArray_FromStringAndSize (buf.data (), bytes); #endif if (result != nullptr) diff --git a/gdb/remote.c b/gdb/remote.c index 27af76d8dd2..a7c495a0e25 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -1037,8 +1037,6 @@ static int hexnumnstr (char *, ULONGEST, int); static CORE_ADDR remote_address_masked (CORE_ADDR); -static void print_packet (const char *); - static int stub_unpack_int (const char *buff, int fieldlength); struct packet_config; @@ -9503,17 +9501,6 @@ escape_buffer (const char *buf, int n) return std::move (stb.string ()); } -/* Display a null-terminated packet on stdout, for debugging, using C - string notation. */ - -static void -print_packet (const char *buf) -{ - puts_filtered ("\""); - fputstr_filtered (buf, '"', gdb_stdout); - puts_filtered ("\""); -} - int remote_target::putpkt (const char *buf) { @@ -11609,18 +11596,39 @@ struct cli_packet_command_callbacks : public send_remote_packet_callbacks void sending (const char *packet_str) override { - puts_filtered ("sending: "); - print_packet (packet_str); - puts_filtered ("\n"); + printf_filtered ("sending: \"%s\"\n", packet_str); } - /* Called with BUF, the reply from the remote target. */ + /* Called with BUF, the reply from the remote target, which is BYTES in + length. Print the contents of BUF to gdb_stdout. */ - void received (const gdb::char_vector &buf) override + void received (const gdb::char_vector &buf, int bytes) override { - puts_filtered ("received: "); - print_packet (buf.data ()); - puts_filtered ("\n"); + puts_filtered ("received: \""); + print_packet (buf, bytes); + puts_filtered ("\"\n"); + } + +private: + /* Print BUF, which contains BYTES bytes to gdb_stdout. Any + non-printable bytes in BUF are printed as '\x??' with '??' replaced + by the hexadecimal value of the byte. */ + + static void + print_packet (const gdb::char_vector &buf, int bytes) + { + string_file stb; + + for (int i = 0; i < bytes; ++i) + { + gdb_byte c = buf[i]; + if (isprint (c)) + fputc_unfiltered (c, &stb); + else + fprintf_unfiltered (&stb, "\\x%02x", (unsigned char) c); + } + + puts_filtered (stb.string ().c_str ()); } }; @@ -11641,9 +11649,12 @@ send_remote_packet (const char *packet_str, remote->putpkt (packet_str); remote_state *rs = remote->get_remote_state (); - remote->getpkt (&rs->buf, 0); + int bytes = remote->getpkt_sane (&rs->buf, 0); + + if (bytes < 0) + error (_("error while fetching packet from remote target")); - callbacks->received (rs->buf); + callbacks->received (rs->buf, bytes); } /* Entry point for the 'maint packet' command. */ diff --git a/gdb/remote.h b/gdb/remote.h index 22455e8f8d5..957dfcc5f5b 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -95,12 +95,12 @@ struct send_remote_packet_callbacks virtual void sending (const char *packet_str) = 0; /* The RECEIVED callback is called once a reply has been received from - the remote target. The content of the reply is in BUF which can't be - modified, and which is not guaranteed to remain valid after the - RECEIVED call has returned. If you need to preserve the contents of - BUF then a copy should be taken. */ + the remote target. The content of the reply is in BUF, which is BYTES + long, which can't be modified, and which is not guaranteed to remain + valid after the RECEIVED call has returned. If you need to preserve + the contents of BUF then a copy should be taken. */ - virtual void received (const gdb::char_vector &buf) = 0; + virtual void received (const gdb::char_vector &buf, int bytes) = 0; }; /* Send PACKET_STR the current remote target. If PACKET_STR is nullptr, or diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp index dde5c270f20..63a7080e3bc 100644 --- a/gdb/testsuite/gdb.python/py-send-packet.exp +++ b/gdb/testsuite/gdb.python/py-send-packet.exp @@ -54,3 +54,36 @@ gdb_test "python run_send_packet_test()" "Send packet test passed" # Check the string representation of a remote target connection. gdb_test "python print(gdb.selected_inferior().connection)" \ "<gdb.RemoteTargetConnection num=$decimal, what=\".*\">" + +# Check to see if there's any auxv data for this target. +gdb_test_multiple "info auxv" "" { + -re -wrap "The program has no auxiliary information now\\. " { + set skip_auxv_test true + } + -re -wrap "0\\s+AT_NULL\\s+End of vector\\s+0x0" { + set skip_auxv_test false + } +} + +if { ! $skip_auxv_test } { + # Use 'maint packet' to fetch the auxv data. + set reply_data "" + gdb_test_multiple "maint packet qXfer:auxv:read::0,1000" "" { + -re "sending: \"qXfer:auxv:read::0,1000\"\r\n" { + exp_continue + } + -re -wrap "received: \"(.*)\"" { + set reply_data $expect_out(1,string) + } + } + + # Expand the '\x' in the output, so we can pass a string through + # to Python. + set reply_data [string map {\x \\x} $reply_data] + gdb_assert { ![string equal "$reply_data" ""] } + + # Run the test, fetches the auxv data in Python and confirm it + # matches the expected results. + gdb_test "python run_auxv_send_packet_test(\"$reply_data\")" \ + "auxv send packet test passed" +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py index 35754164a98..226c3e9477f 100644 --- a/gdb/testsuite/gdb.python/py-send-packet.py +++ b/gdb/testsuite/gdb.python/py-send-packet.py @@ -77,5 +77,33 @@ def run_send_packet_test(): print("Send packet test passed") +# Convert a bytes or bytearray object to a string. This follows the +# same rules as the 'maint packet' command so that the output from the +# two sources can be compared. +def bytes_to_string(byte_array): + res = "" + for b in byte_array: + b = int(b) + if b >= 32 and b <= 126: + res = res + ("%c" % b) + else: + res = res + ("\\x%02x" % b) + return res + +# A very simple test for sending the packet that reads the auxv data. +# We convert the result to a string and expect to find some +# hex-encoded bytes in the output. This test will only work on +# targets that actually supply auxv data. +def run_auxv_send_packet_test(expected_result): + inf = gdb.selected_inferior() + conn = inf.connection + assert isinstance(conn, gdb.RemoteTargetConnection) + res = conn.send_packet("qXfer:auxv:read::0,1000") + string = bytes_to_string(res) + assert string.count("\\x") > 0 + assert string == expected_result + print("auxv send packet test passed") + + # Just to indicate the file was sourced correctly. print("Sourcing complete.") -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv5 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet 2021-10-22 17:10 ` [PATCHv5 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet Andrew Burgess @ 2021-11-15 2:44 ` Simon Marchi 0 siblings, 0 replies; 52+ messages in thread From: Simon Marchi @ 2021-11-15 2:44 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2021-10-22 13:10, Andrew Burgess wrote: > Previous the 'maint packet' command, and the recently added Python API Previous -> Previously? > RemoteTargetConnection.send_packet, could only cope with replies that > contained printable string characters. > > The code implementing these two features treated the reply buffer as a > string, so any null-bytes would terminate the output, and if some > other non-printable character was encountered, GDB would just try to > print it anyway... > > The only way I've found to reproduce this easily is to try and examine > the auxv data, which is binary in nature, here's a GDB session before > my patch: > > (gdb) target remote :54321 > ... > (gdb) maint packet qXfer:auxv:read::0,1000 > sending: "qXfer:auxv:read::0,1000" > received: "l!" > (gdb) > > And after my patch: > > (gdb) target remote :54321 > ... > (gdb) maint packet qXfer:auxv:read::0,1000 > sending: "qXfer:auxv:read::0,1000" > received: "l!\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xfc\xf7\xff\x7f\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xfb\xeb\xbf\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00@\x00@\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x10\xfd\xf7\xff\x7f\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00P\x10@\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0d\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00y\xba\xff\xff\xff\x7f\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\xdf\xef\xff\xff\xff\x7f\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x89\xba\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" > (gdb) > > The binary contents of the reply are now printed as escaped hex. > > Similarly, here's the same packet fetched, and printed, using this > Python script: > > inf = gdb.selected_inferior() > conn = inf.connection > res = conn.send_packet("qXfer:auxv:read::0,1000") > print ("Got: %s" % str(res)) > > The GDB session: > > (gdb) source fetch-auxv.py > Got: b'l!\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xfc\xf7\xff\x7f\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xfb\xeb\xbf\x00\x00\x00\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x00\x00\x00\x00d\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00@\x00@\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00\x00\x10\xfd\xf7\xff\x7f\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\t\x00\x00\x00\x00\x00\x00\x00P\x10@\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\r\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x0e\x00\x00\x00\x00\x00\x00\x00\xe8\x03\x00\x00\x00\x00\x00\x00\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x00\x00\x00\x00\x00\x00\x00y\xba\xff\xff\xff\x7f\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\xdf\xef\xff\xff\xff\x7f\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x89\xba\xff\xff\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' > > The testing for this change is pretty weak right now, I just fetch the > auxv data using 'maint packet' and from Python, and check that the two > results match. I also check that the results, when converted to a > string, contains some hex-encoded characters (e.g. \xf0, etc). Reading this made me think that the input side of things should probably use bytes and not str as well. See this example using the X packet to write memory using binary data: inf = gdb.selected_inferior() conn = inf.connection res = conn.send_packet('X555555558028,4:\xff\x03\x02\x01') print(res) The result: (gdb) source s.py [remote] Sending packet: $X555555558028,4:ÿ\003\002\001#f4 [remote] Packet received: E01 b'E01' And on GDBserver side: gdbserver: Received too much data from the target. The ff gets interpreted as this unicode character: https://www.fileformat.info/info/unicode/char/00ff/index.htm and encoded in UTF-8 as two bytes (C3 BF). > --- > gdb/python/py-connection.c | 21 ++++---- > gdb/remote.c | 57 ++++++++++++--------- > gdb/remote.h | 10 ++-- > gdb/testsuite/gdb.python/py-send-packet.exp | 33 ++++++++++++ > gdb/testsuite/gdb.python/py-send-packet.py | 28 ++++++++++ > 5 files changed, 112 insertions(+), 37 deletions(-) > > diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c > index d282c225e9c..a0bea11694d 100644 > --- a/gdb/python/py-connection.c > +++ b/gdb/python/py-connection.c > @@ -320,22 +320,25 @@ struct py_send_packet_callbacks : public send_remote_packet_callbacks > void sending (const char *args) override > { /* Nothing. */ } > > - /* When the result is returned create a Python string and assign this > - into the result member variable. */ > + /* When the result is returned create a Python object and assign this > + into M_RESULT. If for any reason we can't create a Python object to > + represent the result then M_RESULT is left untouched (it is > + initialised to reference Py_None), so after this call M_RESULT will > + either be an object representing the reply, or None. */ > > - void received (const gdb::char_vector &buf) override > + void received (const gdb::char_vector &buf, int bytes) override I was confused about why you needed to pass both BUF and BYTES. The reason is that BUF refers to the whole remote_state::buf vectors, but only a subset of that is valid. In that case, it would perhaps be better to replace the parameter with a gdb::array_view (instead of adding BYTES). > { > - /* m_result is initialized to None, leave it untouched unless we got > - back a useful result. */ > - if (buf.data ()[0] != '\0') > + gdb_assert (bytes >= 0); > + > + if (bytes > 0 && buf.data ()[0] != '\0') > { > PyObject *result; > > #ifdef IS_PY3K > - result = Py_BuildValue("y#", buf.data (), strlen (buf.data ())); > + result = Py_BuildValue("y#", buf.data (), bytes); > #else > - result = PyString_FromStringAndSize (buf.data (), > - strlen (buf.data ())); > + //result = PyString_FromStringAndSize (buf.data (), bytes); > + result = PyByteArray_FromStringAndSize (buf.data (), bytes); Leftover comment. I think that for Python 2, string was the right type actually. Strings were just plain byte sequences. Python 2.6 and 2.7 actually make bytes an alias of str, because they are the same thing: >>> bytes <type 'str'> >>> bytes == str True You might be able to use PyBytes_FromStringAndSize for both Python 2 and 3 actually. See the note at the top here: https://docs.python.org/2.7/c-api/string.html I didn't test though, as I don't have a Python 2 build on hand. I could make one though if we need to discuss this more. > diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py > index 35754164a98..226c3e9477f 100644 > --- a/gdb/testsuite/gdb.python/py-send-packet.py > +++ b/gdb/testsuite/gdb.python/py-send-packet.py > @@ -77,5 +77,33 @@ def run_send_packet_test(): > print("Send packet test passed") > > > +# Convert a bytes or bytearray object to a string. This follows the > +# same rules as the 'maint packet' command so that the output from the > +# two sources can be compared. > +def bytes_to_string(byte_array): > + res = "" > + for b in byte_array: > + b = int(b) > + if b >= 32 and b <= 126: > + res = res + ("%c" % b) > + else: > + res = res + ("\\x%02x" % b) > + return res > + > +# A very simple test for sending the packet that reads the auxv data. > +# We convert the result to a string and expect to find some > +# hex-encoded bytes in the output. This test will only work on > +# targets that actually supply auxv data. > +def run_auxv_send_packet_test(expected_result): > + inf = gdb.selected_inferior() > + conn = inf.connection > + assert isinstance(conn, gdb.RemoteTargetConnection) > + res = conn.send_packet("qXfer:auxv:read::0,1000") Can you assert that type(res) is the right thing, to verify that what we document is true? Following what I said above, it could be assert type(res) is bytes for both Python 2 and 3. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv5 0/4] Python API for target connections, and packet sending 2021-10-22 17:10 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess ` (3 preceding siblings ...) 2021-10-22 17:10 ` [PATCHv5 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet Andrew Burgess @ 2021-11-09 10:04 ` Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 0/3] " Andrew Burgess 5 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-11-09 10:04 UTC (permalink / raw) To: gdb-patches Ping! I think I addressed all the issues from the last round of feedback. Thanks, Andrew Andrew Burgess <andrew.burgess@embecosm.com> writes: > Since v4: > > - Updated to latest upstream/master. > > - Replaced string comparison on process_stratum_target::shortname() > with a new helper function is_remote_target declared in remote.h, > and defined in remote.c. > > - Fixed a bug in py-send-packet.exp (patch #4) when running the test > using Python 3. > > Since v3: > > - Addressed all review comments. > > - The RemoteTargetConnection.send_packet method now returns a bytes > object for Py3, and a bytesarray object for Py2. This change is > in patch #3. > > - New patch #4 fixes GDB so that we can correctly return packets > containing null bytes, this fixes both 'maint packet' and the > Python API. > > Since v2: > > - Added a gdb.RemoteTargetConnection sub-class, which is used for > 'remote' and 'extended-remote' connections. The previous > 'send_remote_packet' has become 'send_packet' on this new > sub-class. > > - Tests and documentation have been updated to reflect the above > change. > > - In the first patch, connpy_repr has been modified in order to > prepare it for the sub-class that will appear in the later > patch. > > Since v1: > > - Rebased onto current upstream master and resolved the merge > conflicts. > > - Addressed the documentation feedback from Eli. I'm not 100% sure > that I have correctly addressed the concerns about patch #3, so > this is probably worth rechecking. > > > --- > > Andrew Burgess (4): > gdb/python: introduce gdb.TargetConnection object type > gdb: make packet_command function available outside remote.c > gdb/python: add gdb.RemoteTargetConnection.send_packet > gdb: handle binary data in 'maint packet' and > RemoteTargetConnection.send_packet > > gdb/Makefile.in | 1 + > gdb/NEWS | 21 + > gdb/doc/gdb.texinfo | 1 + > gdb/doc/python.texi | 127 ++++- > gdb/observable.c | 1 + > gdb/observable.h | 3 + > gdb/python/py-all-events.def | 1 + > gdb/python/py-connection.c | 536 ++++++++++++++++++ > gdb/python/py-event-types.def | 5 + > gdb/python/py-inferior.c | 16 + > gdb/python/python-internal.h | 6 + > gdb/python/python.c | 5 + > gdb/remote.c | 113 ++-- > gdb/remote.h | 42 ++ > gdb/target-connection.c | 4 + > .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ > .../gdb.multi/multi-target-info-inferiors.py | 63 ++ > gdb/testsuite/gdb.python/py-connection.c | 22 + > gdb/testsuite/gdb.python/py-connection.exp | 75 +++ > gdb/testsuite/gdb.python/py-inferior.exp | 20 +- > gdb/testsuite/gdb.python/py-send-packet.c | 22 + > gdb/testsuite/gdb.python/py-send-packet.exp | 89 +++ > gdb/testsuite/gdb.python/py-send-packet.py | 109 ++++ > 23 files changed, 1281 insertions(+), 39 deletions(-) > create mode 100644 gdb/python/py-connection.c > create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py > create mode 100644 gdb/testsuite/gdb.python/py-connection.c > create mode 100644 gdb/testsuite/gdb.python/py-connection.exp > create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c > create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp > create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py > > -- > 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv6 0/3] Python API for target connections, and packet sending 2021-10-22 17:10 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess ` (4 preceding siblings ...) 2021-11-09 10:04 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess @ 2021-11-15 17:40 ` Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess ` (2 more replies) 5 siblings, 3 replies; 52+ messages in thread From: Andrew Burgess @ 2021-11-15 17:40 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess Since v5: - Updated to latest upstream/master. - Significant changes in patches #2 and #3. - In patch #2, the old patch #4 is now merged into this commit. I'm now using gdb::array_view as part of the new API added in this commit. By the time I'd done this adding support for results containing binary data was trivial, so I just added it here. I've added a NEWS entry, and updated the docs to cover this new functionality. - In patch #2, as mentioned above, I'm now using gdb::array_view for passing data around, this includes taking an array_view for the data passed in that the user wants to send. This makes no difference for 'maint packet', but will be critical in the next commit, as it allows the user to send packets containing binary data from Python code. - In patch #3, replaced the use of Py_None in py_send_packet_callbacks with just using nullptr. The previous approach would leave a pending error if the Py* calls in ::received failed. I don't have a test for this as I can't imagine how this would ever actually fail, but, if it does, the new approach should handle it just fine. - In patch #3, now using PyBytes_FromStringAndSize for Py2 and Py3, this seems to work fine, there was a small compatibility issue that I needed to work around in the test, look for use of ord vs int. - In patch #3, I added a paragraph to the docs advising against the use of type(connection), but to use isinstance(connection, ...) instead, as this should be future proof. Since v4: - Updated to latest upstream/master. - Replaced string comparison on process_stratum_target::shortname() with a new helper function is_remote_target declared in remote.h, and defined in remote.c. - Fixed a bug in py-send-packet.exp (patch #4) when running the test using Python 3. Since v3: - Addressed all review comments. - The RemoteTargetConnection.send_packet method now returns a bytes object for Py3, and a bytesarray object for Py2. This change is in patch #3. - New patch #4 fixes GDB so that we can correctly return packets containing null bytes, this fixes both 'maint packet' and the Python API. Since v2: - Added a gdb.RemoteTargetConnection sub-class, which is used for 'remote' and 'extended-remote' connections. The previous 'send_remote_packet' has become 'send_packet' on this new sub-class. - Tests and documentation have been updated to reflect the above change. - In the first patch, connpy_repr has been modified in order to prepare it for the sub-class that will appear in the later patch. Since v1: - Rebased onto current upstream master and resolved the merge conflicts. - Addressed the documentation feedback from Eli. I'm not 100% sure that I have correctly addressed the concerns about patch #3, so this is probably worth rechecking. --- Andrew Burgess (3): gdb/python: introduce gdb.TargetConnection object type gdb: make packet_command function available outside remote.c gdb/python: add gdb.RemoteTargetConnection.send_packet gdb/Makefile.in | 1 + gdb/NEWS | 29 + gdb/doc/gdb.texinfo | 4 + gdb/doc/python.texi | 150 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 541 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/remote.c | 117 ++-- gdb/remote.h | 43 ++ gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 ++ gdb/testsuite/gdb.python/py-connection.c | 22 + gdb/testsuite/gdb.python/py-connection.exp | 75 +++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- gdb/testsuite/gdb.python/py-send-packet.c | 22 + gdb/testsuite/gdb.python/py-send-packet.exp | 89 +++ gdb/testsuite/gdb.python/py-send-packet.py | 117 ++++ 23 files changed, 1333 insertions(+), 39 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv6 1/3] gdb/python: introduce gdb.TargetConnection object type 2021-11-15 17:40 ` [PATCHv6 0/3] " Andrew Burgess @ 2021-11-15 17:40 ` Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-11-15 17:40 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess From: Andrew Burgess <andrew.burgess@embecosm.com> This commit adds a new object type gdb.TargetConnection. This new type represents a connection within GDB (a connection as displayed by 'info connections'). There's three ways to find a gdb.TargetConnection, there's a new 'gdb.connections()' function, which returns a list of all currently active connections. Or you can read the new 'connection' property on the gdb.Inferior object type, this contains the connection for that inferior (or None if the inferior has no connection, for example, it is exited). Finally, there's a new gdb.events.connection_removed event registry, this emits a new gdb.ConnectionEvent whenever a connection is removed from GDB (this can happen when all inferiors using a connection exit, though this is not always the case, depending on the connection type). The gdb.ConnectionEvent has a 'connection' property, which is the gdb.TargetConnection being removed from GDB. The gdb.TargetConnection has an 'is_valid()' method. A connection object becomes invalid when the underlying connection is removed from GDB (as discussed above, this might be when all inferiors using a connection exit, or it might be when the user explicitly replaces a connection in GDB by issuing another 'target' command). The gdb.TargetConnection has the following read-only properties: 'num': The number for this connection, 'type': e.g. 'native', 'remote', 'sim', etc 'description': The longer description as seen in the 'info connections' command output. 'details': A string or None. Extra details for the connection, for example, a remote connection's details might be 'hostname:port'. --- gdb/Makefile.in | 1 + gdb/NEWS | 15 + gdb/doc/python.texi | 93 ++++- gdb/observable.c | 1 + gdb/observable.h | 3 + gdb/python/py-all-events.def | 1 + gdb/python/py-connection.c | 366 ++++++++++++++++++ gdb/python/py-event-types.def | 5 + gdb/python/py-inferior.c | 16 + gdb/python/python-internal.h | 6 + gdb/python/python.c | 5 + gdb/target-connection.c | 4 + .../gdb.multi/multi-target-info-inferiors.exp | 38 ++ .../gdb.multi/multi-target-info-inferiors.py | 63 +++ gdb/testsuite/gdb.python/py-connection.c | 22 ++ gdb/testsuite/gdb.python/py-connection.exp | 69 ++++ gdb/testsuite/gdb.python/py-inferior.exp | 20 +- 17 files changed, 724 insertions(+), 4 deletions(-) create mode 100644 gdb/python/py-connection.c create mode 100644 gdb/testsuite/gdb.multi/multi-target-info-inferiors.py create mode 100644 gdb/testsuite/gdb.python/py-connection.c create mode 100644 gdb/testsuite/gdb.python/py-connection.exp diff --git a/gdb/Makefile.in b/gdb/Makefile.in index f71537f6595..56b1d4f5372 100644 --- a/gdb/Makefile.in +++ b/gdb/Makefile.in @@ -391,6 +391,7 @@ SUBDIR_PYTHON_SRCS = \ python/py-bpevent.c \ python/py-breakpoint.c \ python/py-cmd.c \ + python/py-connection.c \ python/py-continueevent.c \ python/py-event.c \ python/py-evtregistry.c \ diff --git a/gdb/NEWS b/gdb/NEWS index 9e950d2f80d..56128905229 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -52,6 +52,21 @@ maint show internal-warning backtrace ** New function gdb.Architecture.integer_type(), which returns an integer type given a size and a signed-ness. + ** New gdb.TargetConnection object type that represents a connection + (as displayed by the 'info connections' command). + + ** The gdb.Inferior type now has a 'connection' property which is an + instance of gdb.TargetConnection, the connection used by this + inferior. This can be None if the inferior has no connection. + + ** New 'gdb.events.connection_removed' event registry, which emits a + 'gdb.ConnectionEvent' when a connection is removed from GDB. + This event has a 'connection' property, a gdb.TargetConnection + object for the connection being removed. + + ** New gdb.connections() function that returns a list of all + currently active connections. + * 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 9a768133f4c..33748eeb9f3 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -220,6 +220,7 @@ * Lazy Strings In Python:: Python representation of lazy strings. * Architectures In Python:: Python representation of architectures. * Registers In Python:: Python representation of registers. +* Connections In Python:: Python representation of connections. * TUI Windows In Python:: Implementing new TUI windows. @end menu @@ -565,6 +566,13 @@ (@pxref{gdbpy_architecture_name,,Architecture.name}). @end defun +@anchor{gdbpy_connections} +@defun gdb.connections +Return a list of @code{gdb.TargetConnection} objects, one for each +currently active connection (@pxref{Connections In Python}). The +connection objects are in no particular order in the returned list. +@end defun + @node Exception Handling @subsubsection Exception Handling @cindex python exceptions @@ -3095,10 +3103,18 @@ ID of inferior, as assigned by GDB. @end defvar +@anchor{gdbpy_inferior_connection} +@defvar Inferior.connection +The @code{gdb.TargetConnection} for this inferior (@pxref{Connections +In Python}), or @code{None} if this inferior has no connection. +@end defvar + @defvar Inferior.connection_num ID of inferior's connection as assigned by @value{GDBN}, or None if -the inferior is not connected to a target. -@xref{Inferiors Connections and Programs}. +the inferior is not connected to a target. @xref{Inferiors Connections +and Programs}. This is equivalent to +@code{gdb.Inferior.connection.num} in the case where +@code{gdb.Inferior.connection} is not @code{None}. @end defvar @defvar Inferior.pid @@ -3439,6 +3455,15 @@ An integer, the value of the exit code @value{GDBN} will return. @end defvar +@item events.connection_removed +This is emitted when @value{GDBN} removes a connection +(@pxref{Connections In Python}). The event is of type +@code{gdb.ConnectionEvent}. This has a single read-only attribute: + +@defvar ConnectionEvent.connection +The @code{gdb.TargetConnection} that is being removed. +@end defvar + @end table @node Threads In Python @@ -5973,6 +5998,70 @@ A string that is the name of this register group. @end defvar +@node Connections In Python +@subsubsection Connections In Python +@cindex connections in python +@value{GDBN} lets you run and debug multiple programs in a single +session. Each program being debugged has a connection, the connection +describes how @value{GDBN} controls the program being debugged. +Examples of different connection types are @samp{native} and +@samp{remote}. @xref{Inferiors Connections and Programs}. + +@value{GDBN} uses the @code{gdb.TargetConnection} object type to +represent a connection in Python code. To get a list of all +connections use @code{gdb.connections} +(@pxref{gdbpy_connections,,gdb.connections}). + +To get the connection for a single @code{gdb.Inferior} read its +@code{gdb.Inferior.connection} attribute +(@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). + +A @code{gdb.TargetConnection} has the following method: + +@defun TargetConnection.is_valid () +Return @code{True} if the @code{gdb.TargetConnection} object is valid, +@code{False} if not. A @code{gdb.TargetConnection} will become +invalid if the connection no longer exists within @value{GDBN}, this +might happen when no inferiors are using the connection, but could be +delayed until the user replaces the current target. + +Reading any of the @code{gdb.TargetConnection} properties will throw +an exception if the connection is invalid. +@end defun + +A @code{gdb.TargetConnection} has the following read-only properties: + +@defvar TargetConnection.num +An integer assigned by @value{GDBN} to uniquely identify this +connection. This is the same value as displayed in the @samp{Num} +column of the @code{info connections} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.type +A string that describes what type of connection this is. This string +will be one of the valid names that can be passed to the @code{target} +command (@pxref{Target Commands,,target command}). +@end defvar + +@defvar TargetConnection.description +A string that gives a short description of this target type. This is +the same string that is displayed in the @samp{Description} column of +the @code{info connection} command output (@pxref{Inferiors +Connections and Programs,,info connections}). +@end defvar + +@defvar TargetConnection.details +An optional string that gives additional information about this +connection. This attribute can be @code{None} if there are no +additional details for this connection. + +An example of a connection type that might have additional details is +the @samp{remote} connection, in this case the details string can +contain the @samp{@var{hostname}:@var{port}} that was used to connect +to the remote target. +@end defvar + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/observable.c b/gdb/observable.c index b020076cf26..6e91d7bbc4a 100644 --- a/gdb/observable.c +++ b/gdb/observable.c @@ -78,6 +78,7 @@ DEFINE_OBSERVABLE (user_selected_context_changed); DEFINE_OBSERVABLE (source_styling_changed); DEFINE_OBSERVABLE (current_source_symtab_and_line_changed); DEFINE_OBSERVABLE (gdb_exiting); +DEFINE_OBSERVABLE (connection_removed); } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/observable.h b/gdb/observable.h index 1de746a691e..9ab584adbd9 100644 --- a/gdb/observable.h +++ b/gdb/observable.h @@ -253,6 +253,9 @@ extern observable<> current_source_symtab_and_line_changed; /* Called when GDB is about to exit. */ extern observable<int> gdb_exiting; +/* When a connection is removed. */ +extern observable<process_stratum_target */* target */> connection_removed; + } /* namespace observers */ } /* namespace gdb */ diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def index 83f10989e4a..ed22a3335cd 100644 --- a/gdb/python/py-all-events.def +++ b/gdb/python/py-all-events.def @@ -39,3 +39,4 @@ GDB_PY_DEFINE_EVENT(breakpoint_deleted) GDB_PY_DEFINE_EVENT(breakpoint_modified) GDB_PY_DEFINE_EVENT(before_prompt) GDB_PY_DEFINE_EVENT(gdb_exiting) +GDB_PY_DEFINE_EVENT(connection_removed) diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c new file mode 100644 index 00000000000..f1dfa26e39c --- /dev/null +++ b/gdb/python/py-connection.c @@ -0,0 +1,366 @@ +/* Python interface to inferiors. + + Copyright (C) 2009-2021 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/>. */ + +#include "defs.h" +#include "python-internal.h" +#include "process-stratum-target.h" +#include "inferior.h" +#include "observable.h" +#include "target-connection.h" +#include "py-events.h" +#include "py-event.h" +#include "arch-utils.h" + +#include <map> + +/* The Python object that represents a connection. */ + +struct connection_object +{ + PyObject_HEAD + + /* The process target that represents this connection. When a + connection_object is created this field will always point at a valid + target. Later, if GDB stops using this target (the target is popped + from all target stacks) then this field is set to nullptr, which + indicates that this Python object is now in the invalid state (see + the is_valid() method below). */ + struct process_stratum_target *target; +}; + +extern PyTypeObject connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); + +/* Require that CONNECTION be valid. */ +#define CONNPY_REQUIRE_VALID(connection) \ + do { \ + if (connection->target == nullptr) \ + { \ + PyErr_SetString (PyExc_RuntimeError, \ + _("Connection no longer exists.")); \ + return nullptr; \ + } \ + } while (0) + +/* A map between process_stratum targets and the Python object representing + them. We actually hold a gdbpy_ref around the Python object so that + reference counts are handled correctly when entries are deleted. */ +static std::map<process_stratum_target *, + gdbpy_ref<connection_object>> all_connection_objects; + +/* Return a reference to a gdb.TargetConnection object for TARGET. If + TARGET is nullptr then a reference to None is returned. + + Previously created gdb.TargetConnection objects are cached, and + additional references to the same connection object can be returned with + later calls to this function. */ + +gdbpy_ref<> +target_to_connection_object (process_stratum_target *target) +{ + if (target == nullptr) + return gdbpy_ref<>::new_reference (Py_None); + + gdbpy_ref <connection_object> conn_obj; + auto conn_obj_iter = all_connection_objects.find (target); + if (conn_obj_iter == all_connection_objects.end ()) + { + conn_obj.reset (PyObject_New (connection_object, + &connection_object_type)); + if (conn_obj == nullptr) + return nullptr; + conn_obj->target = target; + all_connection_objects.emplace (target, conn_obj); + } + else + conn_obj = conn_obj_iter->second; + + gdb_assert (conn_obj != nullptr); + + /* Repackage the result as a PyObject reference. */ + return gdbpy_ref<> ((PyObject *) conn_obj.release ()); +} + +/* Return a list of gdb.TargetConnection objects, one for each currently + active connection. The returned list is in no particular order. */ + +PyObject * +gdbpy_connections (PyObject *self, PyObject *args) +{ + gdbpy_ref<> list (PyList_New (0)); + if (list == nullptr) + return nullptr; + + for (process_stratum_target *target : all_non_exited_process_targets ()) + { + gdb_assert (target != nullptr); + + gdbpy_ref<> conn = target_to_connection_object (target); + if (conn == nullptr) + return nullptr; + gdb_assert (conn.get () != Py_None); + + if (PyList_Append (list.get (), conn.get ()) < 0) + return nullptr; + } + + return list.release (); +} + +/* Emit a connection event for TARGET to REGISTRY. Return 0 on success, or + a negative value on error. */ + +static int +emit_connection_event (process_stratum_target *target, + eventregistry_object *registry) +{ + gdbpy_ref<> event_obj + = create_event_object (&connection_event_object_type); + if (event_obj == nullptr) + return -1; + + gdbpy_ref<> conn = target_to_connection_object (target); + if (evpy_add_attribute (event_obj.get (), "connection", conn.get ()) < 0) + return -1; + + return evpy_emit_event (event_obj.get (), registry); +} + +/* Callback for the connection_removed observer. */ + +static void +connpy_connection_removed (process_stratum_target *target) +{ + if (!gdb_python_initialized) + return; + + gdbpy_enter enter_py (get_current_arch (), current_language); + + if (!evregpy_no_listeners_p (gdb_py_events.connection_removed)) + if (emit_connection_event (target, gdb_py_events.connection_removed) < 0) + gdbpy_print_stack (); + + auto conn_obj_iter = all_connection_objects.find (target); + if (conn_obj_iter != all_connection_objects.end ()) + { + gdbpy_ref <connection_object> conn_obj = conn_obj_iter->second; + conn_obj->target = nullptr; + all_connection_objects.erase (target); + } +} + +/* Called when a gdb.TargetConnection object is deallocated. */ + +static void +connpy_connection_dealloc (PyObject *obj) +{ + connection_object *conn_obj = (connection_object *) obj; + + /* As the all_connection_objects map holds a reference to each connection + object we can only enter the dealloc function when the reference in + all_connection_objects has been erased. + + As we always set the target pointer back to nullptr before we erase + items from all_connection_objects then, when we get here, the target + pointer must be nullptr. */ + gdb_assert (conn_obj->target == nullptr); + + Py_TYPE (obj)->tp_free (obj); +} + +/* Implement repr() for gdb.TargetConnection. */ + +static PyObject * +connpy_repr (PyObject *obj) +{ + connection_object *self = (connection_object *) obj; + process_stratum_target *target = self->target; + + if (target == nullptr) + return PyString_FromFormat ("<%s (invalid)>", Py_TYPE (obj)->tp_name); + + return PyString_FromFormat ("<%s num=%d, what=\"%s\">", + Py_TYPE (obj)->tp_name, + target->connection_number, + make_target_connection_string (target).c_str ()); +} + +/* Implementation of gdb.TargetConnection.is_valid() -> Boolean. Returns + True if this connection object is still associated with a + process_stratum_target, otherwise, returns False. */ + +static PyObject * +connpy_is_valid (PyObject *self, PyObject *args) +{ + connection_object *conn = (connection_object *) self; + + if (conn->target == nullptr) + Py_RETURN_FALSE; + + Py_RETURN_TRUE; +} + +/* Return the id number of this connection. */ + +static PyObject * +connpy_get_connection_num (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + auto num = conn->target->connection_number; + return gdb_py_object_from_longest (num).release (); +} + +/* Return a string that gives the short name for this connection type. */ + +static PyObject * +connpy_get_connection_type (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *shortname = conn->target->shortname (); + return host_string_to_python_string (shortname).release (); +} + +/* Return a string that gives a longer description of this connection type. */ + +static PyObject * +connpy_get_description (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *longname = conn->target->longname (); + return host_string_to_python_string (longname).release (); +} + +/* Return a string that gives additional details about this connection, or + None, if there are no additional details for this connection type. */ + +static PyObject * +connpy_get_connection_details (PyObject *self, void *closure) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + const char *details = conn->target->connection_string (); + if (details != nullptr) + return host_string_to_python_string (details).release (); + else + Py_RETURN_NONE; +} + +/* Python specific initialization for this file. */ + +int +gdbpy_initialize_connection (void) +{ + if (PyType_Ready (&connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "TargetConnection", + (PyObject *) &connection_object_type) < 0) + return -1; + + return 0; +} + +/* Global initialization for this file. */ + +void _initialize_py_connection (); +void +_initialize_py_connection () +{ + gdb::observers::connection_removed.attach (connpy_connection_removed, + "py-connection"); +} + +/* Methods for the gdb.TargetConnection object type. */ + +static PyMethodDef connection_object_methods[] = +{ + { "is_valid", connpy_is_valid, METH_NOARGS, + "is_valid () -> Boolean.\n\ +Return true if this TargetConnection is valid, false if not." }, + { NULL } +}; + +/* Attributes for the gdb.TargetConnection object type. */ + +static gdb_PyGetSetDef connection_object_getset[] = +{ + { "num", connpy_get_connection_num, NULL, + "ID number of this connection, as assigned by GDB.", NULL }, + { "type", connpy_get_connection_type, NULL, + "A short string that is the name for this connection type.", NULL }, + { "description", connpy_get_description, NULL, + "A longer string describing this connection type.", NULL }, + { "details", connpy_get_connection_details, NULL, + "A string containing additional connection details.", NULL }, + { NULL } +}; + +/* Define the gdb.TargetConnection object type. */ + +PyTypeObject connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.TargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + connection_object_getset, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def index aeaee02e8bb..6a49f2aa896 100644 --- a/gdb/python/py-event-types.def +++ b/gdb/python/py-event-types.def @@ -110,3 +110,8 @@ GDB_PY_DEFINE_EVENT_TYPE (gdb_exiting, "GdbExitingEvent", "GDB is about to exit", event_object_type); + +GDB_PY_DEFINE_EVENT_TYPE (connection, + "ConnectionEvent", + "GDB connection added or removed object", + event_object_type); diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c index 222e4d9a3d3..3b2d47095ca 100644 --- a/gdb/python/py-inferior.c +++ b/gdb/python/py-inferior.c @@ -415,6 +415,20 @@ infpy_get_num (PyObject *self, void *closure) return gdb_py_object_from_longest (inf->inferior->num).release (); } +/* Return the gdb.TargetConnection object for this inferior, or None if a + connection does not exist. */ + +static PyObject * +infpy_get_connection (PyObject *self, void *closure) +{ + inferior_object *inf = (inferior_object *) self; + + INFPY_REQUIRE_VALID (inf); + + process_stratum_target *target = inf->inferior->process_target (); + return target_to_connection_object (target).release (); +} + /* Return the connection number of the given inferior, or None if a connection does not exist. */ @@ -849,6 +863,8 @@ gdbpy_initialize_inferior (void) static gdb_PyGetSetDef inferior_object_getset[] = { { "num", infpy_get_num, NULL, "ID of inferior, as assigned by GDB.", NULL }, + { "connection", infpy_get_connection, NULL, + "The gdb.TargetConnection for this inferior.", NULL }, { "connection_num", infpy_get_connection_num, NULL, "ID of inferior's connection, as assigned by GDB.", NULL }, { "pid", infpy_get_pid, NULL, "PID of inferior, as assigned by the OS.", diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h index aece7080043..211833e4b2d 100644 --- a/gdb/python/python-internal.h +++ b/gdb/python/python-internal.h @@ -482,6 +482,10 @@ gdbpy_ref<inferior_object> inferior_to_inferior_object (inferior *inf); PyObject *gdbpy_buffer_to_membuf (gdb::unique_xmalloc_ptr<gdb_byte> buffer, CORE_ADDR address, ULONGEST length); +struct process_stratum_target; +gdbpy_ref<> target_to_connection_object (process_stratum_target *target); +PyObject *gdbpy_connections (PyObject *self, PyObject *args); + const struct block *block_object_to_block (PyObject *obj); struct symbol *symbol_object_to_symbol (PyObject *obj); struct value *value_object_to_value (PyObject *self); @@ -555,6 +559,8 @@ int gdbpy_initialize_tui () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; int gdbpy_initialize_membuf () CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; +int gdbpy_initialize_connection () + CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION; /* A wrapper for PyErr_Fetch that handles reference counting for the caller. */ diff --git a/gdb/python/python.c b/gdb/python/python.c index 593fbc1be7a..199ba997dd1 100644 --- a/gdb/python/python.c +++ b/gdb/python/python.c @@ -1877,6 +1877,7 @@ do_start_initialization () || gdbpy_initialize_xmethods () < 0 || gdbpy_initialize_unwind () < 0 || gdbpy_initialize_membuf () < 0 + || gdbpy_initialize_connection () < 0 || gdbpy_initialize_tui () < 0) return false; @@ -2327,6 +2328,10 @@ Register a TUI window constructor." }, "architecture_names () -> List.\n\ Return a list of all the architecture names GDB understands." }, + { "connections", gdbpy_connections, METH_NOARGS, + "connections () -> List.\n\ +Return a list of gdb.TargetConnection objects." }, + {NULL, NULL, 0, NULL} }; diff --git a/gdb/target-connection.c b/gdb/target-connection.c index a649423e07e..da1c9da74dc 100644 --- a/gdb/target-connection.c +++ b/gdb/target-connection.c @@ -24,6 +24,7 @@ #include "inferior.h" #include "target.h" +#include "observable.h" /* A map between connection number and representative process_stratum target. */ @@ -49,6 +50,9 @@ connection_list_add (process_stratum_target *t) void connection_list_remove (process_stratum_target *t) { + /* Notify about the connection being removed before we reset the + connection number to zero. */ + gdb::observers::connection_removed.notify (t); process_targets.erase (t->connection_number); t->connection_number = 0; } diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp index 3fd6b15cdd9..3f74a47de31 100644 --- a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.exp @@ -15,18 +15,27 @@ # Test "info inferiors" and "info connections" with multiple targets. +load_lib gdb-python.exp + source $srcdir/$subdir/multi-target.exp.tcl if {![multi_target_prepare]} { return } +# Cache the result of calling skip_python_tests into a local variable. +set run_python_tests [expr ! [skip_python_tests]] + # Test "info inferiors" and "info connections". MULTI_PROCESS # indicates whether the multi-process feature of remote targets is # turned off or on. proc test_info_inferiors {multi_process} { setup "off" + if { $::run_python_tests } { + gdb_test_no_output "source ${::remote_python_file}" "load python file" + } + gdb_test_no_output \ "set remote multiprocess-feature-packet $multi_process" @@ -86,6 +95,18 @@ proc test_info_inferiors {multi_process} { "[connection_num 5 $inf]${ws}core${ws}Local core dump file${ws}" \ ] + if { $::run_python_tests } { + gdb_test "python info_connections()" \ + [multi_line \ + "Num${ws}What${ws}Description" \ + "[connection_num 1 $inf]${ws}native${ws}Native process" \ + "[connection_num 2 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 3 $inf]${ws}core${ws}Local core dump file" \ + "[connection_num 4 $inf]${ws}extended-remote localhost:$decimal${ws}Extended remote serial target in gdb-specific protocol" \ + "[connection_num 5 $inf]${ws}core${ws}Local core dump file" \ + ] + } + gdb_test "info inferiors" \ [multi_line \ "Num${ws}Description${ws}Connection${ws}Executable${ws}" \ @@ -96,10 +117,27 @@ proc test_info_inferiors {multi_process} { "[inf_desc 5 $inf]4 \\(extended-remote localhost:$decimal\\)${ws}${binfile}${ws}" \ "[inf_desc 6 $inf]5 \\(core\\)${ws}${binfile}${ws}" \ ] + + if { $::run_python_tests } { + gdb_test "python info_inferiors()" \ + [multi_line \ + "Inferior 1, Connection #1: native" \ + "Inferior 2, Connection #2: extended-remote localhost:$decimal" \ + "Inferior 3, Connection #3: core" \ + "Inferior 4, Connection #1: native" \ + "Inferior 5, Connection #4: extended-remote localhost:$decimal" \ + "Inferior 6, Connection #5: core" \ + ] + } } } } +if { $run_python_tests } { + set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +} + # Test "info inferiors" and "info connections" commands. with_test_prefix "info-inferiors" { foreach_with_prefix multi_process {"on" "off"} { diff --git a/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py new file mode 100644 index 00000000000..aac561ae683 --- /dev/null +++ b/gdb/testsuite/gdb.multi/multi-target-info-inferiors.py @@ -0,0 +1,63 @@ +# Copyright (C) 2021 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 + +# Take a gdb.TargetConnection and return the connection number. +def conn_num(c): + return c.num + +# Takes a gdb.TargetConnection and return a string that is either the +# type, or the type and details (if the details are not None). +def make_target_connection_string(c): + if c.details is None: + return c.type + else: + return "%s %s" % (c.type, c.details) + +# A Python implementation of 'info connections'. Produce output that +# is identical to the output of 'info connections' so we can check +# that aspects of gdb.TargetConnection work correctly. +def info_connections(): + all_connections = sorted(gdb.connections(), key=conn_num) + current_conn = gdb.selected_inferior().connection + what_width = 0 + for c in all_connections: + s = make_target_connection_string(c) + if len(s) > what_width: + what_width = len(s) + + fmt = " Num %%-%ds Description" % what_width + print(fmt % "What") + fmt = "%%s%%-3d %%-%ds %%s" % what_width + for c in all_connections: + if c == current_conn: + prefix = "* " + else: + prefix = " " + + print(fmt % (prefix, c.num, make_target_connection_string(c), + c.description)) + +def inf_num(i): + return i.num + +# .... +def info_inferiors(): + all_inferiors = sorted(gdb.inferiors(), key=inf_num) + for i in gdb.inferiors(): + print("Inferior %d, Connection #%d: %s" % + (i.num, i.connection_num, + make_target_connection_string(i.connection))) diff --git a/gdb/testsuite/gdb.python/py-connection.c b/gdb/testsuite/gdb.python/py-connection.c new file mode 100644 index 00000000000..4b32e704476 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp new file mode 100644 index 00000000000..b805b052f73 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -0,0 +1,69 @@ +# Copyright (C) 2021 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/>. + +# This file is for testing the gdb.TargetConnection API. This API is +# already tested in gdb.multi/multi-target-info-inferiors.exp and +# gdb.python/py-inferior.exp, this file just covers some edge cases +# that are not tested in other places. + +load_lib gdb-python.exp + +standard_testfile + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +# Skip all tests if Python scripting is not enabled. +if { [skip_python_tests] } { continue } + +if ![runto_main] then { + return 0 +} + +# Create a gdb.TargetConnection object and check it is initially +# valid. +gdb_test_no_output "python conn = gdb.selected_inferior().connection" +gdb_test "python print(conn)" \ + "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ + "print gdb.TargetConnection while it is still valid" +gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" + +# Get the connection again, and ensure we get the exact same object. +gdb_test_no_output "python conn2 = gdb.selected_inferior().connection" +gdb_test "python print('Same object: %s' % (conn is conn2))" "True" + +# Now invalidate the connection, and ensure that the is_valid method +# starts to return False. +gdb_test "info connections" "\r\n\\* 1 .*" \ + "info connections while the connection is still around" +gdb_test "disconnect" "" "kill the inferior" \ + "A program is being debugged already\\. Kill it\\? .*y or n. $" "y" +gdb_test "info connections" "No connections\\." \ + "info connections now all the connections have gone" +gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ + "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" + +# Now check that accessing properties of the invalid connection cases +# an error. +gdb_test "python print(conn.num)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.type)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.description)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" +gdb_test "python print(conn.details)" \ + "RuntimeError: Connection no longer exists\\.\r\n.*" diff --git a/gdb/testsuite/gdb.python/py-inferior.exp b/gdb/testsuite/gdb.python/py-inferior.exp index 894b90a85fc..d5d284a763e 100644 --- a/gdb/testsuite/gdb.python/py-inferior.exp +++ b/gdb/testsuite/gdb.python/py-inferior.exp @@ -51,6 +51,8 @@ gdb_py_test_silent_cmd "python i0 = inferiors\[0\]" "get first inferior" 0 gdb_test "python print ('result = %s' % (i0 == inferiors\[0\]))" " = True" "test equality comparison (true)" gdb_test "python print ('result = %s' % i0.num)" " = \[0-9\]+" "test Inferior.num" gdb_test "python print ('result = %s' % i0.connection_num)" " = \[0-9\]+" "test Inferior.connection_num" +gdb_test "python print ('result = %s' % (i0.connection_num == i0.connection.num))" " = True" \ + "Inferior.connection_num equals Inferior.connection.num" gdb_test "python print ('result = %s' % i0.pid)" " = \[0-9\]+" "test Inferior.pid" gdb_test "python print ('result = %s' % i0.was_attached)" " = False" "test Inferior.was_attached" gdb_test "python print (i0.threads ())" "\\(<gdb.InferiorThread object at 0x\[\[:xdigit:\]\]+>,\\)" "test Inferior.threads" @@ -264,6 +266,8 @@ with_test_prefix "is_valid" { "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].connection_num)" \ "RuntimeError: Inferior no longer exists.*" + gdb_test "python print (inf_list\[1\].connection)" \ + "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].pid)" \ "RuntimeError: Inferior no longer exists.*" gdb_test "python print (inf_list\[1\].was_attached)" \ @@ -281,7 +285,9 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 1" ".*" "switch to first inferior" gdb_test "py print (gdb.selected_inferior().num)" "1" "first inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "1" \ - "first inferior's connection" + "first inferior's connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" "1" \ + "first inferior's connection number, though connection object" # Figure out if inf 1 has a native target. set inf_1_is_native [gdb_is_target_native] @@ -289,6 +295,8 @@ with_test_prefix "selected_inferior" { gdb_test "inferior 3" ".*" "switch to third inferior" gdb_test "py print (gdb.selected_inferior().num)" "3" "third inferior selected" gdb_test "py print (gdb.selected_inferior().connection_num)" "None" \ + "third inferior's None connection number" + gdb_test "py print (gdb.selected_inferior().connection)" "None" \ "third inferior's None connection" gdb_test "target native" "Done. Use the \"run\" command to start a process." \ "target for the third inferior" @@ -302,7 +310,15 @@ with_test_prefix "selected_inferior" { } gdb_test "py print (gdb.selected_inferior().connection_num)" \ "$expected_connection_num" \ - "third inferior's native connection" + "third inferior's native connection number" + gdb_test "py print (gdb.selected_inferior().connection.num)" \ + "$expected_connection_num" \ + "third inferior's native connection number, though connection object" + + # Test printing of gdb.TargetConnection object. + gdb_test "py print (gdb.selected_inferior().connection)" \ + "<gdb.TargetConnection num=${expected_connection_num}, what=\"\[^\"\]+\">" \ + "print a connection object" gdb_test "inferior 1" ".*" "switch back to first inferior" gdb_test_no_output "remove-inferiors 3" "remove second inferior" -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv6 2/3] gdb: make packet_command function available outside remote.c 2021-11-15 17:40 ` [PATCHv6 0/3] " Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess @ 2021-11-15 17:40 ` Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-11-15 17:40 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess From: Andrew Burgess <andrew.burgess@embecosm.com> In a later commit I will add a Python API to access the 'maint packet' functionality, that is, sending a user specified packet to the target. To make implementing this easier, this commit refactors how this command is currently implemented so that the packet_command function is now global. The new global send_remote_packet function takes an object that is an implementation of an abstract interface. Two functions within this interface are then called, one just before a packet is sent to the remote target, and one when the reply has been received from the remote target. Using an interface object in this way allows (1) for the error checking to be done before the first callback is made, this means we only print out what packet it being sent once we know we are going to actually send it, and (2) we don't need to make a copy of the reply if all we want to do is print it. One user visible changes after this commit are the error messages, which I've changed to be less 'maint packet' command focused, this will make them (I hope) better for when send_remote_packet can be called from Python code. So: "command can only be used with remote target" Becomes: "packets can only be sent to a remote target" And: "remote-packet command requires packet text as argument" Becomes: "a remote packet must not be empty" Additionally, in this commit, I've added support for packet replies that contain binary data. Before this commit, the code that printed the reply treated the reply as a C string, it assumed that the string only contained printable characters, and had a null character only at the end. One way to show the problem with this is if we try to read the auxv data from a remote target, the auxv data is binary, so, before this commit: (gdb) target remote :54321 ... (gdb) maint packet qXfer:auxv:read::0,1000 sending: "qXfer:auxv:read::0,1000" received: "l!" (gdb) And after this commit: (gdb) target remote :54321 ... (gdb) maint packet qXfer:auxv:read::0,1000 sending: "qXfer:auxv:read::0,1000" received: "l!\x00\x00\x00\x00\x00\x00\x00\x00\xf0\xfc\xf7\xff\x7f\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\xff\xf> (gdb) The binary contents of the reply are now printed as escaped hex. --- gdb/NEWS | 8 ++++ gdb/doc/gdb.texinfo | 3 ++ gdb/remote.c | 108 ++++++++++++++++++++++++++++++-------------- gdb/remote.h | 35 ++++++++++++++ 4 files changed, 119 insertions(+), 35 deletions(-) diff --git a/gdb/NEWS b/gdb/NEWS index 56128905229..63e7c118eb1 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -32,6 +32,14 @@ maint show internal-warning backtrace internal-error, or an internal-warning. This is on by default for internal-error and off by default for internal-warning. +* Changed commands + +maint packet + This command can now print a reply, if the reply includes + non-printable characters. Any non-printable characters are printed + as escaped hex, e.g. \x?? where '??' is replaces with the value of + the non-printable character. + * Python API ** New function gdb.add_history(), which takes a gdb.Value object diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index fc8e5bdf3db..c38ff49ec04 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -39288,6 +39288,9 @@ @samp{$} character, the terminating @samp{#} character, and the checksum. +Any non-printable characters in the reply are printed as escaped hex, +e.g. @samp{\x00}, @samp{\x01}, etc. + @kindex maint print architecture @item maint print architecture @r{[}@var{file}@r{]} Print the entire architecture configuration. The optional argument diff --git a/gdb/remote.c b/gdb/remote.c index abf63de622a..466afa1fe58 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -956,8 +956,6 @@ class remote_target : public process_stratum_target bool vcont_r_supported (); - void packet_command (const char *args, int from_tty); - private: /* data fields */ /* The remote state. Don't reference this directly. Use the @@ -1030,8 +1028,6 @@ static int hexnumnstr (char *, ULONGEST, int); static CORE_ADDR remote_address_masked (CORE_ADDR); -static void print_packet (const char *); - static int stub_unpack_int (const char *buff, int fieldlength); struct packet_config; @@ -9500,17 +9496,6 @@ escape_buffer (const char *buf, int n) return std::move (stb.string ()); } -/* Display a null-terminated packet on stdout, for debugging, using C - string notation. */ - -static void -print_packet (const char *buf) -{ - puts_filtered ("\""); - fputstr_filtered (buf, '"', gdb_stdout); - puts_filtered ("\""); -} - int remote_target::putpkt (const char *buf) { @@ -11597,34 +11582,87 @@ remote_target::memory_map () return result; } -static void -packet_command (const char *args, int from_tty) +/* Set of callbacks used to implement the 'maint packet' command. */ + +struct cli_packet_command_callbacks : public send_remote_packet_callbacks { - remote_target *remote = get_current_remote_target (); + /* Called before the packet is sent. BUF is the packet content before + the protocol specific prefix, suffix, and escaping is added. */ - if (remote == nullptr) - error (_("command can only be used with remote target")); + void sending (gdb::array_view<const char> &buf) override + { + puts_filtered ("sending: "); + print_packet (buf); + puts_filtered ("\n"); + } - remote->packet_command (args, from_tty); -} + /* Called with BUF, the reply from the remote target. */ + + void received (gdb::array_view<const char> &buf) override + { + puts_filtered ("received: \""); + print_packet (buf); + puts_filtered ("\"\n"); + } + +private: + + /* Print BUF o gdb_stdout. Any non-printable bytes in BUF are printed as + '\x??' with '??' replaced by the hexadecimal value of the byte. */ + + static void + print_packet (gdb::array_view<const char> &buf) + { + string_file stb; + + for (int i = 0; i < buf.size (); ++i) + { + gdb_byte c = buf[i]; + if (isprint (c)) + fputc_unfiltered (c, &stb); + else + fprintf_unfiltered (&stb, "\\x%02x", (unsigned char) c); + } + + puts_filtered (stb.string ().c_str ()); + } +}; + +/* See remote.h. */ void -remote_target::packet_command (const char *args, int from_tty) +send_remote_packet (gdb::array_view<const char> &buf, + send_remote_packet_callbacks *callbacks) { - if (!args) - error (_("remote-packet command requires packet text as argument")); + if (buf.size () == 0 || buf.data ()[0] == '\0') + error (_("a remote packet must not be empty")); - puts_filtered ("sending: "); - print_packet (args); - puts_filtered ("\n"); - putpkt (args); + remote_target *remote = get_current_remote_target (); + if (remote == nullptr) + error (_("packets can only be sent to a remote target")); - remote_state *rs = get_remote_state (); + callbacks->sending (buf); - getpkt (&rs->buf, 0); - puts_filtered ("received: "); - print_packet (rs->buf.data ()); - puts_filtered ("\n"); + remote->putpkt_binary (buf.data (), buf.size ()); + remote_state *rs = remote->get_remote_state (); + int bytes = remote->getpkt_sane (&rs->buf, 0); + + if (bytes < 0) + error (_("error while fetching packet from remote target")); + + gdb::array_view<const char> view (&rs->buf[0], bytes); + callbacks->received (view); +} + +/* Entry point for the 'maint packet' command. */ + +static void +cli_packet_command (const char *args, int from_tty) +{ + cli_packet_command_callbacks cb; + gdb::array_view<const char> view + = gdb::make_array_view (args, args == nullptr ? 0 : strlen (args)); + send_remote_packet (view, &cb); } #if 0 @@ -14903,7 +14941,7 @@ Argument is a single section name (default: all loaded sections).\n\ To compare only read-only loaded sections, specify the -r option."), &cmdlist); - add_cmd ("packet", class_maintenance, packet_command, _("\ + add_cmd ("packet", class_maintenance, cli_packet_command, _("\ Send an arbitrary packet to a remote target.\n\ maintenance packet TEXT\n\ If GDB is talking to an inferior via the GDB serial protocol, then\n\ diff --git a/gdb/remote.h b/gdb/remote.h index 46bfa01fc79..0178294ab1d 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -78,4 +78,39 @@ extern int remote_register_number_and_offset (struct gdbarch *gdbarch, extern void remote_notif_get_pending_events (remote_target *remote, struct notif_client *np); extern bool remote_target_is_non_stop_p (remote_target *t); + +/* An abstract class that represents the set of callbacks that are made + from the send_remote_packet function (declared below). */ + +struct send_remote_packet_callbacks +{ + /* The SENDING callback is called once send_remote_packet has performed + its error checking and setup, just before the packet is sent to the + remote target. BUF is the content of the packet that will be sent + (before any of the protocol specific prefix, suffix, or escaping is + applied). */ + + virtual void sending (gdb::array_view<const char> &buf) = 0; + + /* The RECEIVED callback is called once a reply has been received from + the remote target. The content of the reply is in BUF which can't be + modified, and which is not guaranteed to remain valid after the + RECEIVED call has returned. If you need to preserve the contents of + BUF then a copy should be taken. */ + + virtual void received (gdb::array_view<const char> &buf) = 0; +}; + +/* Send BUF to the current remote target. If BUF points to an empty + string, either zero length, or the first character is the null + character, then an error is thrown. If the current target is not a + remote target then an error is thrown. + + Calls CALLBACKS->sending() just before the packet is sent to the remote + target, and calls CALLBACKS->received() with the reply once this is + received from the remote target. */ + +extern void send_remote_packet (gdb::array_view<const char> &buf, + send_remote_packet_callbacks *callbacks); + #endif -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-11-15 17:40 ` [PATCHv6 0/3] " Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess @ 2021-11-15 17:40 ` Andrew Burgess 2021-11-15 18:42 ` Eli Zaretskii ` (2 more replies) 2 siblings, 3 replies; 52+ messages in thread From: Andrew Burgess @ 2021-11-15 17:40 UTC (permalink / raw) To: gdb-patches; +Cc: Andrew Burgess From: Andrew Burgess <andrew.burgess@embecosm.com> This commits adds a new sub-class of gdb.TargetConnection, gdb.RemoteTargetConnection. This sub-class is created for all 'remote' and 'extended-remote' targets. This new sub-class has one additional method over its base class, 'send_packet'. This new method is equivalent to the 'maint packet' CLI command, it allows a custom packet to be sent to a remote target. The result of calling RemoteTargetConnection.send_packet is a bytes array containing the reply that came from the remote. --- gdb/NEWS | 8 +- gdb/doc/gdb.texinfo | 1 + gdb/doc/python.texi | 63 ++++++- gdb/python/py-connection.c | 181 +++++++++++++++++++- gdb/remote.c | 9 + gdb/remote.h | 8 + gdb/testsuite/gdb.python/py-connection.exp | 14 +- gdb/testsuite/gdb.python/py-send-packet.c | 22 +++ gdb/testsuite/gdb.python/py-send-packet.exp | 89 ++++++++++ gdb/testsuite/gdb.python/py-send-packet.py | 117 +++++++++++++ 10 files changed, 501 insertions(+), 11 deletions(-) create mode 100644 gdb/testsuite/gdb.python/py-send-packet.c create mode 100644 gdb/testsuite/gdb.python/py-send-packet.exp create mode 100644 gdb/testsuite/gdb.python/py-send-packet.py diff --git a/gdb/NEWS b/gdb/NEWS index 63e7c118eb1..4fdb2c41121 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -61,7 +61,9 @@ maint packet integer type given a size and a signed-ness. ** New gdb.TargetConnection object type that represents a connection - (as displayed by the 'info connections' command). + (as displayed by the 'info connections' command). A sub-class, + gdb.RemoteTargetConnection, is used to represent 'remote' and + 'extended-remote' connections. ** The gdb.Inferior type now has a 'connection' property which is an instance of gdb.TargetConnection, the connection used by this @@ -75,6 +77,10 @@ maint packet ** New gdb.connections() function that returns a list of all currently active connections. + ** New gdb.RemoteTargetConnection.send_packet(PACKET) method. This + is equivalent to the existing 'maint packet' CLI command; it + allows a user specified packet to be sent to the remote target. + * New features in the GDB remote stub, GDBserver ** GDBserver is now supported on OpenRISC GNU/Linux. diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index c38ff49ec04..ae60c341713 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -39280,6 +39280,7 @@ error stream. This is @samp{on} by default for @code{internal-error} and @samp{off} by default for @code{internal-warning}. +@anchor{maint packet} @kindex maint packet @item maint packet @var{text} If @value{GDBN} is talking to an inferior via the serial protocol, diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 33748eeb9f3..7d5398cbb65 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -6007,15 +6007,36 @@ Examples of different connection types are @samp{native} and @samp{remote}. @xref{Inferiors Connections and Programs}. -@value{GDBN} uses the @code{gdb.TargetConnection} object type to -represent a connection in Python code. To get a list of all -connections use @code{gdb.connections} +Connections in @value{GDBN} are represented as instances of +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a +list of all connections use @code{gdb.connections} (@pxref{gdbpy_connections,,gdb.connections}). To get the connection for a single @code{gdb.Inferior} read its @code{gdb.Inferior.connection} attribute (@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). +Currently there is only a single sub-class of +@code{gdb.TargetConnection}, @code{gdb.RemoteTargetConnection}, +however, additional sub-classes may be added in future releases of +@value{GDBN}. As a result you should avoid writing code like: + +@smallexample +conn = gdb.selected_inferior().connection +if type(conn) is gdb.RemoteTargetConnection: + print("This is a remote target connection") +@end smallexample + +@noindent +As this may fail when more connection types are added. Instead, you +should write: + +@smallexample +conn = gdb.selected_inferior().connection +if isinstance(conn, gdb.RemoteTargetConnection): + print("This is a remote target connection") +@end smallexample + A @code{gdb.TargetConnection} has the following method: @defun TargetConnection.is_valid () @@ -6062,6 +6083,42 @@ to the remote target. @end defvar +The @code{gdb.RemoteTargetConnection} class is a sub-class of +@code{gdb.TargetConnection}, and is used to represent @samp{remote} +and @samp{extended-remote} connections. In addition to the attributes +and methods available from the @code{gdb.TargetConnection} base class, +a @code{gdb.RemoteTargetConnection} has the following method: + +@kindex maint packet +@defun RemoteTargetConnection.send_packet (@var{packet}) +This method sends @var{packet}, which should be a non-empty string or +bytes array, to the remote target and returns the response. If +@var{packet} is not a string, a bytes array, or is empty, then an +exception of type @code{ValueError} is thrown. + +The response is returned as a @code{bytes} buffer. For Python 3 if it +is known that the response can be represented as a string then this +can be decoded from the buffer. For example, if it is known that the +response is an @code{"ascii"} string: + +@smallexample +remote_connection.send_packet("some_packet").decode("ascii") +@end smallexample + +In Python 2 @code{bytes} and @code{str} are aliases, so the result is +already a string, if the response includes non-printable characters, +or null characters, then these will be present in the result, care +should be taken when processing the result to handle this case. + +The prefix, suffix, and checksum (as required by the remote serial +protocol) are automatically added to the outgoing packet, and removed +from the incoming packet before the contents of the reply are +returned. + +This is equivalent to the @code{maintenance packet} command +(@pxref{maint packet}). +@end defun + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c index f1dfa26e39c..7641b4d34a6 100644 --- a/gdb/python/py-connection.c +++ b/gdb/python/py-connection.c @@ -26,6 +26,8 @@ #include "py-events.h" #include "py-event.h" #include "arch-utils.h" +#include "remote.h" +#include "charset.h" #include <map> @@ -47,6 +49,9 @@ struct connection_object extern PyTypeObject connection_object_type CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); +extern PyTypeObject remote_connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("remote_connection_object"); + /* Require that CONNECTION be valid. */ #define CONNPY_REQUIRE_VALID(connection) \ do { \ @@ -81,8 +86,14 @@ target_to_connection_object (process_stratum_target *target) auto conn_obj_iter = all_connection_objects.find (target); if (conn_obj_iter == all_connection_objects.end ()) { - conn_obj.reset (PyObject_New (connection_object, - &connection_object_type)); + PyTypeObject *type; + + if (is_remote_target (target)) + type = &remote_connection_object_type; + else + type = &connection_object_type; + + conn_obj.reset (PyObject_New (connection_object, type)); if (conn_obj == nullptr) return nullptr; conn_obj->target = target; @@ -284,9 +295,119 @@ gdbpy_initialize_connection (void) (PyObject *) &connection_object_type) < 0) return -1; + if (PyType_Ready (&remote_connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "RemoteTargetConnection", + (PyObject *) &remote_connection_object_type) < 0) + return -1; + return 0; } +/* Set of callbacks used to implement gdb.send_packet. */ + +struct py_send_packet_callbacks : public send_remote_packet_callbacks +{ + /* Constructor, initialise the result to nullptr. It is invalid to try + and read the result before sending a packet and processing the + reply. */ + + py_send_packet_callbacks () + : m_result (nullptr) + { /* Nothing. */ } + + /* There's nothing to do when the packet is sent. */ + + void sending (gdb::array_view<const char> &buf) override + { /* Nothing. */ } + + /* When the result is returned create a Python object and assign this + into M_RESULT. If for any reason we can't create a Python object to + represent the result then M_RESULT is set to nullptr, and Python's + internal error flags will be set. If the result we got back from the + remote is empty then set the result to None. */ + + void received (gdb::array_view<const char> &buf) override + { + if (buf.size () > 0 && buf.data ()[0] != '\0') + m_result.reset (PyBytes_FromStringAndSize (buf.data (), buf.size ())); + else + { + /* We didn't get back any result data; set the result to None. */ + Py_INCREF (Py_None); + m_result.reset (Py_None); + } + } + + /* Get a reference to the result as a Python object. It is invalid to + call this before sending a packet to the remote and processing the + reply. + + The result value is setup in the RECEIVED call above. If the RECEIVED + call causes an error then the result value will be set to nullptr, + and the error reason is left stored in Python's global error state. + + It is important that the result is inspected immediately after sending + a packet to the remote, and any error fetched, calling any other + Python functions that might clear the error state, or rely on an error + not being set will cause undefined behaviour. */ + + gdbpy_ref<> result () const + { + return m_result; + } + +private: + + /* A reference to the result value. */ + + gdbpy_ref<> m_result; +}; + +/* Implement RemoteTargetConnection.send_packet function. Send a packet to + the target identified by SELF. The connection must still be valid, and + the packet to be sent must be non-empty, otherwise an exception will be + thrown. */ + +static PyObject * +connpy_send_packet (PyObject *self, PyObject *args, PyObject *kw) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + static const char *keywords[] = {"packet", nullptr}; + const char *packet_str = nullptr; + Py_ssize_t packet_len; + + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s#", keywords, + &packet_str, &packet_len)) + return nullptr; + + if (packet_str == nullptr || *packet_str == '\0' || packet_len <= 0) + { + PyErr_SetString (PyExc_ValueError, _("Invalid remote packet")); + return nullptr; + } + + try + { + scoped_restore_current_thread restore_thread; + switch_to_target_no_thread (conn->target); + + gdb::array_view<const char> view (packet_str, packet_len); + py_send_packet_callbacks callbacks; + send_remote_packet (view, &callbacks); + return callbacks.result ().release (); + } + catch (const gdb_exception &except) + { + gdbpy_convert_exception (except); + return nullptr; + } +} + /* Global initialization for this file. */ void _initialize_py_connection (); @@ -307,6 +428,17 @@ Return true if this TargetConnection is valid, false if not." }, { NULL } }; +/* Methods for the gdb.RemoteTargetConnection object type. */ + +static PyMethodDef remote_connection_object_methods[] = +{ + { "send_packet", (PyCFunction) connpy_send_packet, + METH_VARARGS | METH_KEYWORDS, + "send_packet (PACKET) -> Bytes\n\ +Send PACKET to a remote target, return the reply as a bytes array." }, + { NULL } +}; + /* Attributes for the gdb.TargetConnection object type. */ static gdb_PyGetSetDef connection_object_getset[] = @@ -345,7 +477,7 @@ PyTypeObject connection_object_type = 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ "GDB target connection object", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ @@ -364,3 +496,46 @@ PyTypeObject connection_object_type = 0, /* tp_init */ 0 /* tp_alloc */ }; + +/* Define the gdb.RemoteTargetConnection object type. */ + +PyTypeObject remote_connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.RemoteTargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB remote target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + remote_connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + &connection_object_type, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/remote.c b/gdb/remote.c index 466afa1fe58..22fcf8c25ab 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -994,6 +994,15 @@ class extended_remote_target final : public remote_target bool supports_disable_randomization () override; }; +/* See remote.h. */ + +bool +is_remote_target (process_stratum_target *target) +{ + remote_target *rt = dynamic_cast<remote_target *> (target); + return rt != nullptr; +} + /* Per-program-space data key. */ static const struct program_space_key<char, gdb::xfree_deleter<char>> remote_pspace_data; diff --git a/gdb/remote.h b/gdb/remote.h index 0178294ab1d..0ddf55d19d3 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -24,6 +24,8 @@ struct target_desc; struct remote_target; +class process_stratum_target; + /* True when printing "remote" debug statements is enabled. */ extern bool remote_debug; @@ -113,4 +115,10 @@ struct send_remote_packet_callbacks extern void send_remote_packet (gdb::array_view<const char> &buf, send_remote_packet_callbacks *callbacks); + +/* Return true if TARGET is a remote, or extended-remote target, otherwise, + return false. */ + +extern bool is_remote_target (process_stratum_target *target); + #endif diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp index b805b052f73..96c83781839 100644 --- a/gdb/testsuite/gdb.python/py-connection.exp +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -33,12 +33,18 @@ if ![runto_main] then { return 0 } +if { [target_info exists gdb_protocol] } { + set connection_type "RemoteTargetConnection" +} else { + set connection_type "TargetConnection" +} + # Create a gdb.TargetConnection object and check it is initially # valid. gdb_test_no_output "python conn = gdb.selected_inferior().connection" gdb_test "python print(conn)" \ - "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ - "print gdb.TargetConnection while it is still valid" + "<gdb.${connection_type} num=1, what=\"\[^\"\]+\">" \ + "print gdb.${connection_type} while it is still valid" gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" # Get the connection again, and ensure we get the exact same object. @@ -53,8 +59,8 @@ gdb_test "disconnect" "" "kill the inferior" \ "A program is being debugged already\\. Kill it\\? .*y or n. $" "y" gdb_test "info connections" "No connections\\." \ "info connections now all the connections have gone" -gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ - "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn)" "<gdb.${connection_type} \\(invalid\\)>" \ + "print gdb.${connection_type} now its invalid" gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" # Now check that accessing properties of the invalid connection cases diff --git a/gdb/testsuite/gdb.python/py-send-packet.c b/gdb/testsuite/gdb.python/py-send-packet.c new file mode 100644 index 00000000000..bfe52c018d4 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.c @@ -0,0 +1,22 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +int +main (void) +{ + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp new file mode 100644 index 00000000000..63a7080e3bc --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.exp @@ -0,0 +1,89 @@ +# Copyright (C) 2021 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 the gdb.RemoteTargetConnection.send_packet API. This is done +# by connecting to a remote target and fetching the thread list in two +# ways, first, we manually send the packets required to read the +# thread list using gdb.TargetConnection.send_packet, then we compare +# the results to the thread list using the standard API calls. + +load_lib gdb-python.exp +load_lib gdbserver-support.exp + +standard_testfile + +if {[skip_gdbserver_tests]} { + return 0 +} + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +if { [skip_python_tests] } { + return 0 +} + +# Make sure we're disconnected, in case we're testing with an +# extended-remote board, therefore already connected. +gdb_test "disconnect" ".*" + +gdbserver_run "" + +# Source the python script. +set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +gdb_test "source $remote_python_file" "Sourcing complete\\." \ + "source ${testfile}.py script" + +# The test is actually written in the Python script. Run it now. +gdb_test "python run_send_packet_test()" "Send packet test passed" + +# Check the string representation of a remote target connection. +gdb_test "python print(gdb.selected_inferior().connection)" \ + "<gdb.RemoteTargetConnection num=$decimal, what=\".*\">" + +# Check to see if there's any auxv data for this target. +gdb_test_multiple "info auxv" "" { + -re -wrap "The program has no auxiliary information now\\. " { + set skip_auxv_test true + } + -re -wrap "0\\s+AT_NULL\\s+End of vector\\s+0x0" { + set skip_auxv_test false + } +} + +if { ! $skip_auxv_test } { + # Use 'maint packet' to fetch the auxv data. + set reply_data "" + gdb_test_multiple "maint packet qXfer:auxv:read::0,1000" "" { + -re "sending: \"qXfer:auxv:read::0,1000\"\r\n" { + exp_continue + } + -re -wrap "received: \"(.*)\"" { + set reply_data $expect_out(1,string) + } + } + + # Expand the '\x' in the output, so we can pass a string through + # to Python. + set reply_data [string map {\x \\x} $reply_data] + gdb_assert { ![string equal "$reply_data" ""] } + + # Run the test, fetches the auxv data in Python and confirm it + # matches the expected results. + gdb_test "python run_auxv_send_packet_test(\"$reply_data\")" \ + "auxv send packet test passed" +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py new file mode 100644 index 00000000000..dc3aa43b0f5 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.py @@ -0,0 +1,117 @@ +# Copyright (C) 2021 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 xml.etree.ElementTree as ET +import gdb + +# Make use of gdb.RemoteTargetConnection.send_packet to fetch the +# thread list from the remote target. +# +# Sending existing serial protocol packets like this is not a good +# idea, there should be better ways to get this information using an +# official API, this is just being used as a test case. +# +# Really, the send_packet API would be used to send target +# specific packets to the target, but these are, by definition, target +# specific, so hard to test in a general testsuite. +def get_thread_list_str(): + start_pos = 0 + thread_desc = "" + conn = gdb.selected_inferior().connection + if not isinstance(conn, gdb.RemoteTargetConnection): + raise gdb.GdbError("connection is the wrong type") + while True: + str = conn.send_packet("qXfer:threads:read::%d,200" % start_pos).decode("ascii") + start_pos += 200 + c = str[0] + str = str[1:] + thread_desc += str + if c == "l": + break + return thread_desc + + +# Use gdb.RemoteTargetConnection.send_packet to manually fetch the +# thread list, then extract the thread list using the gdb.Inferior and +# gdb.InferiorThread API. Compare the two results to ensure we +# managed to successfully read the thread list from the remote. +def run_send_packet_test(): + # Find the IDs of all current threads. + all_threads = {} + for inf in gdb.inferiors(): + for thr in inf.threads(): + id = "p%x.%x" % (thr.ptid[0], thr.ptid[1]) + all_threads[id] = False + + # Now fetch the thread list from the remote, and parse the XML. + str = get_thread_list_str() + threads_xml = ET.fromstring(str) + + # Look over all threads in the XML list and check we expected to + # find them, mark the ones we do find. + for thr in threads_xml: + id = thr.get("id") + if not id in all_threads: + raise "found unexpected thread in remote thread list" + else: + all_threads[id] = True + + # Check that all the threads were found in the XML list. + for id in all_threads: + if not all_threads[id]: + raise "thread missingt from remote thread list" + + # Test complete. + print("Send packet test passed") + +# Convert a bytes object to a string. This follows the same rules as +# the 'maint packet' command so that the output from the two sources +# can be compared. +def bytes_to_string(byte_array): + + # Python 2/3 compatibility. We need a function that can give us + # the value of a single element in BYTE_ARRAY as an integer. + if sys.version_info[0] > 2: + value_of_single_byte = int + else: + value_of_single_byte = ord + + res = "" + for b in byte_array: + b = value_of_single_byte(b) + if b >= 32 and b <= 126: + res = res + ("%c" % b) + else: + res = res + ("\\x%02x" % b) + return res + +# A very simple test for sending the packet that reads the auxv data. +# We convert the result to a string and expect to find some +# hex-encoded bytes in the output. This test will only work on +# targets that actually supply auxv data. +def run_auxv_send_packet_test(expected_result): + inf = gdb.selected_inferior() + conn = inf.connection + assert isinstance(conn, gdb.RemoteTargetConnection) + res = conn.send_packet("qXfer:auxv:read::0,1000") + assert isinstance(res, bytes) + string = bytes_to_string(res) + assert string.count("\\x") > 0 + assert string == expected_result + print("auxv send packet test passed") + + +# Just to indicate the file was sourced correctly. +print("Sourcing complete.") -- 2.25.4 ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-11-15 17:40 ` [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess @ 2021-11-15 18:42 ` Eli Zaretskii 2021-11-15 19:38 ` Simon Marchi 2021-11-15 19:29 ` Simon Marchi 2021-11-16 12:48 ` Andrew Burgess 2 siblings, 1 reply; 52+ messages in thread From: Eli Zaretskii @ 2021-11-15 18:42 UTC (permalink / raw) To: Andrew Burgess; +Cc: gdb-patches > Date: Mon, 15 Nov 2021 17:40:57 +0000 > From: Andrew Burgess via Gdb-patches <gdb-patches@sourceware.org> > Cc: Andrew Burgess <andrew.burgess@embecosm.com> > > diff --git a/gdb/NEWS b/gdb/NEWS > index 63e7c118eb1..4fdb2c41121 100644 > --- a/gdb/NEWS > +++ b/gdb/NEWS > @@ -61,7 +61,9 @@ maint packet > integer type given a size and a signed-ness. > > ** New gdb.TargetConnection object type that represents a connection > - (as displayed by the 'info connections' command). > + (as displayed by the 'info connections' command). A sub-class, > + gdb.RemoteTargetConnection, is used to represent 'remote' and > + 'extended-remote' connections. > > ** The gdb.Inferior type now has a 'connection' property which is an > instance of gdb.TargetConnection, the connection used by this > @@ -75,6 +77,10 @@ maint packet > ** New gdb.connections() function that returns a list of all > currently active connections. > > + ** New gdb.RemoteTargetConnection.send_packet(PACKET) method. This > + is equivalent to the existing 'maint packet' CLI command; it > + allows a user specified packet to be sent to the remote target. > + > * New features in the GDB remote stub, GDBserver > > ** GDBserver is now supported on OpenRISC GNU/Linux. This part is OK. > +Currently there is only a single sub-class of > +@code{gdb.TargetConnection}, @code{gdb.RemoteTargetConnection}, > +however, additional sub-classes may be added in future releases of > +@value{GDBN}. As a result you should avoid writing code like: > + > +@smallexample > +conn = gdb.selected_inferior().connection > +if type(conn) is gdb.RemoteTargetConnection: > + print("This is a remote target connection") > +@end smallexample > + > +@noindent > +As this may fail when more connection types are added. Instead, you The "As this may fail" should begin with a lower-case "as", since this is the continuation of the sentence before the example. > +This method sends @var{packet}, which should be a non-empty string or > +bytes array, to the remote target and returns the response. If ^^^^^^^^^^^ You meant "byte array", right? Or "array of bytes", perhaps. > +@var{packet} is not a string, a bytes array, or is empty, then an ^^^^^^^^^^^ Likewise. > +can be decoded from the buffer. For example, if it is known that the > +response is an @code{"ascii"} string: ^^^^^^^^^^^^^^ I'd prefer @sc{ascii} instead (and without the quotes), it will look better in print. Thanks. ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-11-15 18:42 ` Eli Zaretskii @ 2021-11-15 19:38 ` Simon Marchi 0 siblings, 0 replies; 52+ messages in thread From: Simon Marchi @ 2021-11-15 19:38 UTC (permalink / raw) To: Eli Zaretskii, Andrew Burgess; +Cc: gdb-patches On 2021-11-15 1:42 p.m., Eli Zaretskii via Gdb-patches wrote: >> +This method sends @var{packet}, which should be a non-empty string or >> +bytes array, to the remote target and returns the response. If > ^^^^^^^^^^^ > You meant "byte array", right? Or "array of bytes", perhaps. I didn't point it out, out of fear of being too nitpicky, but since you mention it... I don't think we should say array at all. We expect a `bytes object` where `bytes` is literally the class name: https://docs.python.org/3/library/stdtypes.html#bytes So, "... which should be a non-empty string or bytes object", something like that. The `bytes` could be in @code{...} to make it clear we refer to the type name. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-11-15 17:40 ` [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2021-11-15 18:42 ` Eli Zaretskii @ 2021-11-15 19:29 ` Simon Marchi 2021-11-16 12:48 ` Andrew Burgess 2 siblings, 0 replies; 52+ messages in thread From: Simon Marchi @ 2021-11-15 19:29 UTC (permalink / raw) To: Andrew Burgess, gdb-patches; +Cc: Andrew Burgess > @@ -6062,6 +6083,42 @@ > to the remote target. > @end defvar > > +The @code{gdb.RemoteTargetConnection} class is a sub-class of > +@code{gdb.TargetConnection}, and is used to represent @samp{remote} > +and @samp{extended-remote} connections. In addition to the attributes > +and methods available from the @code{gdb.TargetConnection} base class, > +a @code{gdb.RemoteTargetConnection} has the following method: > + > +@kindex maint packet > +@defun RemoteTargetConnection.send_packet (@var{packet}) > +This method sends @var{packet}, which should be a non-empty string or > +bytes array, to the remote target and returns the response. If > +@var{packet} is not a string, a bytes array, or is empty, then an > +exception of type @code{ValueError} is thrown. If the type is wrong, a TypeError error is raised in fact. Minor nit: Python uses "raise" and not "throw", for exceptions. I suppose our doc should follow that nomenclature when talking about Python code. > + > +/* Implement RemoteTargetConnection.send_packet function. Send a packet to > + the target identified by SELF. The connection must still be valid, and > + the packet to be sent must be non-empty, otherwise an exception will be > + thrown. */ > + > +static PyObject * > +connpy_send_packet (PyObject *self, PyObject *args, PyObject *kw) > +{ > + connection_object *conn = (connection_object *) self; > + > + CONNPY_REQUIRE_VALID (conn); > + > + static const char *keywords[] = {"packet", nullptr}; > + const char *packet_str = nullptr; > + Py_ssize_t packet_len; > + > + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "s#", keywords, > + &packet_str, &packet_len)) > + return nullptr; I tried my example yet again: res = conn.send_packet('X555555558010,4:\xff\x03\x02\x01') ... and that still produced the error I got with the previous patch series version. This is expected, as the unicode string still gets encoded as UTF-8, and that generates 5 bytes of data instead of 4. What I have to do is pass in a bytes object, not a string: res = conn.send_packet(b'X555555558010,4:\xff\x03\x02\x01') I think we should only accept bytes-like objects here (y#). Accepting string objects and implicitly encoding them to bytes using the utf-8 codec (as the `s#` specifier does) just sets up a trap for people to fall into, like I did. If people really want to send the bytes representing the UTF-8 encoding of some string, they can just encode the bytes themselves, from a string: In [1]: "é".encode() Out[1]: b'\xc3\xa9' This way, there's no misunderstanding about what the user wants. I think that for Python 2 we could use `s#`, as `y#` doesn't exist (again, str in Python 3 is basically Python 3's bytes). The doc [1] says about s#: Convert a Python string or Unicode object to a C pointer to a character string. This variant on s stores into two C variables, the first one a pointer to a character string, the second one its length. In this case the Python string may contain embedded null bytes. Unicode objects pass back a pointer to the default encoded string version of the object if such a conversion is possible. So with Python 2, people could still pass a unicode object and fall into the same trap I fell in. But the difference is that with Python 2, if you want to use a unicode object, you have to do so explicitly. So it won't happen by mistake. So the only difference in the GDB code for Python 2 and 3 would be the format specifier. [1] https://docs.python.org/2.7/c-api/arg.html > + if (packet_str == nullptr || *packet_str == '\0' || packet_len <= 0) I'm pretty sure that packet_str can't be nullptr here, if there's a problem with the type of the argument, gdb_PyArg_ParseTupleAndKeywords will raise and we will return above. I would use a gdb_assert for this instead. I would probably drop the *packet_str == '\0' test. While it's not very useful to send a packet with a '\0' in first position, I don't see why we should discriminate against it specifically. The caller sends what it wants to send. > + { > + PyErr_SetString (PyExc_ValueError, _("Invalid remote packet")); > + return nullptr; ... and that would only leave "packet_len <= 0", which would happen if the passed-in data is empty. I would make the message a bit more precise then: PyErr_SetString (PyExc_ValueError, _("Packet must not be empty")); > + } > + > + try > + { > + scoped_restore_current_thread restore_thread; > + switch_to_target_no_thread (conn->target); > + > + gdb::array_view<const char> view (packet_str, packet_len); > + py_send_packet_callbacks callbacks; > + send_remote_packet (view, &callbacks); > + return callbacks.result ().release (); Assert that callbacks.result != nullptr? Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-11-15 17:40 ` [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2021-11-15 18:42 ` Eli Zaretskii 2021-11-15 19:29 ` Simon Marchi @ 2021-11-16 12:48 ` Andrew Burgess 2021-11-16 15:10 ` Simon Marchi 2 siblings, 1 reply; 52+ messages in thread From: Andrew Burgess @ 2021-11-16 12:48 UTC (permalink / raw) To: gdb-patches; +Cc: Eli Zaretskii, Simon Marchi Simon, Eli, Thanks for the feedback, I think I've addressed everything in this update. Simon, sorry for not quite getting your point the first time through. I believe that this revision should address your concerns. I have not been as restrictive as you suggested, but hopefully this should still be acceptable. So, you can still pass a string, but the documentation is specific that it must be possible to convert the string to a bytes object using the ascii codec. This allows for what I assume would be the most common use case: conn.send_packet("some_ascii_text") But if we revisit your example, we now get: res = conn.send_packet('X555555558028,4:\xff\x03\x02\xff') UnicodeEncodeError: 'ascii' codec can't encode character '\xff' in position 10: ordinal not in range(128) In which case, the solution is, as your suggest, to pass a bytes object: res = conn.send_packet(b'X555555558028,4:\xff\x03\x02\xff') print(res) b'OK' I've tested this code with Python 3.7 and Python 2.7 and it seems to work fine. I've extended the test to include your example and related cases, hopefully that should cover what I've said above. Let me know what you think, Thanks, Andrew --- commit ccb55cdcd64d618276b9b16eade71807eb171c2c Author: Andrew Burgess <andrew.burgess@embecosm.com> Date: Tue Aug 31 14:04:36 2021 +0100 gdb/python: add gdb.RemoteTargetConnection.send_packet This commits adds a new sub-class of gdb.TargetConnection, gdb.RemoteTargetConnection. This sub-class is created for all 'remote' and 'extended-remote' targets. This new sub-class has one additional method over its base class, 'send_packet'. This new method is equivalent to the 'maint packet' CLI command, it allows a custom packet to be sent to a remote target. The outgoing packet can either be a bytes object, or a Unicode string, so long as the Unicode string contains only ASCII characters. The result of calling RemoteTargetConnection.send_packet is a bytes object containing the reply that came from the remote. diff --git a/gdb/NEWS b/gdb/NEWS index 63e7c118eb1..4fdb2c41121 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -61,7 +61,9 @@ maint packet integer type given a size and a signed-ness. ** New gdb.TargetConnection object type that represents a connection - (as displayed by the 'info connections' command). + (as displayed by the 'info connections' command). A sub-class, + gdb.RemoteTargetConnection, is used to represent 'remote' and + 'extended-remote' connections. ** The gdb.Inferior type now has a 'connection' property which is an instance of gdb.TargetConnection, the connection used by this @@ -75,6 +77,10 @@ maint packet ** New gdb.connections() function that returns a list of all currently active connections. + ** New gdb.RemoteTargetConnection.send_packet(PACKET) method. This + is equivalent to the existing 'maint packet' CLI command; it + allows a user specified packet to be sent to the remote target. + * New features in the GDB remote stub, GDBserver ** GDBserver is now supported on OpenRISC GNU/Linux. diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo index c38ff49ec04..ae60c341713 100644 --- a/gdb/doc/gdb.texinfo +++ b/gdb/doc/gdb.texinfo @@ -39280,6 +39280,7 @@ error stream. This is @samp{on} by default for @code{internal-error} and @samp{off} by default for @code{internal-warning}. +@anchor{maint packet} @kindex maint packet @item maint packet @var{text} If @value{GDBN} is talking to an inferior via the serial protocol, diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 33748eeb9f3..4a66c11c19d 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -6007,15 +6007,36 @@ Examples of different connection types are @samp{native} and @samp{remote}. @xref{Inferiors Connections and Programs}. -@value{GDBN} uses the @code{gdb.TargetConnection} object type to -represent a connection in Python code. To get a list of all -connections use @code{gdb.connections} +Connections in @value{GDBN} are represented as instances of +@code{gdb.TargetConnection}, or as one of its sub-classes. To get a +list of all connections use @code{gdb.connections} (@pxref{gdbpy_connections,,gdb.connections}). To get the connection for a single @code{gdb.Inferior} read its @code{gdb.Inferior.connection} attribute (@pxref{gdbpy_inferior_connection,,gdb.Inferior.connection}). +Currently there is only a single sub-class of +@code{gdb.TargetConnection}, @code{gdb.RemoteTargetConnection}, +however, additional sub-classes may be added in future releases of +@value{GDBN}. As a result you should avoid writing code like: + +@smallexample +conn = gdb.selected_inferior().connection +if type(conn) is gdb.RemoteTargetConnection: + print("This is a remote target connection") +@end smallexample + +@noindent +as this may fail when more connection types are added. Instead, you +should write: + +@smallexample +conn = gdb.selected_inferior().connection +if isinstance(conn, gdb.RemoteTargetConnection): + print("This is a remote target connection") +@end smallexample + A @code{gdb.TargetConnection} has the following method: @defun TargetConnection.is_valid () @@ -6062,6 +6083,49 @@ to the remote target. @end defvar +The @code{gdb.RemoteTargetConnection} class is a sub-class of +@code{gdb.TargetConnection}, and is used to represent @samp{remote} +and @samp{extended-remote} connections. In addition to the attributes +and methods available from the @code{gdb.TargetConnection} base class, +a @code{gdb.RemoteTargetConnection} has the following method: + +@kindex maint packet +@defun RemoteTargetConnection.send_packet (@var{packet}) +This method sends @var{packet} to the remote target and returns the +response. The @var{packet} should either be a @code{bytes} object, or +a @code{Unicode} string. + +If @var{packet} is a @code{Unicode} string, then the string is encoded +to a @code{bytes} object using the @sc{ascii} codec. If the string +can't be encoded then an @code{UnicodeError} is raised. + +If @var{packet} is not a @code{bytes} object, or a @code{Unicode} +string, then a @code{TypeError} is raised. If @var{packet} is empty +then a @code{ValueError} is raised. + +The response is returned as a @code{bytes} object. For Python 3 if it +is known that the response can be represented as a string then this +can be decoded from the buffer. For example, if it is known that the +response is an @sc{ascii} string: + +@smallexample +remote_connection.send_packet("some_packet").decode("ascii") +@end smallexample + +In Python 2 @code{bytes} and @code{str} are aliases, so the result is +already a string, if the response includes non-printable characters, +or null characters, then these will be present in the result, care +should be taken when processing the result to handle this case. + +The prefix, suffix, and checksum (as required by the remote serial +protocol) are automatically added to the outgoing packet, and removed +from the incoming packet before the contents of the reply are +returned. + +This is equivalent to the @code{maintenance packet} command +(@pxref{maint packet}). +@end defun + @node TUI Windows In Python @subsubsection Implementing new TUI windows @cindex Python TUI Windows diff --git a/gdb/python/py-connection.c b/gdb/python/py-connection.c index f1dfa26e39c..48e14fed4ad 100644 --- a/gdb/python/py-connection.c +++ b/gdb/python/py-connection.c @@ -26,6 +26,8 @@ #include "py-events.h" #include "py-event.h" #include "arch-utils.h" +#include "remote.h" +#include "charset.h" #include <map> @@ -47,6 +49,9 @@ struct connection_object extern PyTypeObject connection_object_type CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("connection_object"); +extern PyTypeObject remote_connection_object_type + CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("remote_connection_object"); + /* Require that CONNECTION be valid. */ #define CONNPY_REQUIRE_VALID(connection) \ do { \ @@ -81,8 +86,14 @@ target_to_connection_object (process_stratum_target *target) auto conn_obj_iter = all_connection_objects.find (target); if (conn_obj_iter == all_connection_objects.end ()) { - conn_obj.reset (PyObject_New (connection_object, - &connection_object_type)); + PyTypeObject *type; + + if (is_remote_target (target)) + type = &remote_connection_object_type; + else + type = &connection_object_type; + + conn_obj.reset (PyObject_New (connection_object, type)); if (conn_obj == nullptr) return nullptr; conn_obj->target = target; @@ -284,9 +295,148 @@ gdbpy_initialize_connection (void) (PyObject *) &connection_object_type) < 0) return -1; + if (PyType_Ready (&remote_connection_object_type) < 0) + return -1; + + if (gdb_pymodule_addobject (gdb_module, "RemoteTargetConnection", + (PyObject *) &remote_connection_object_type) < 0) + return -1; + return 0; } +/* Set of callbacks used to implement gdb.send_packet. */ + +struct py_send_packet_callbacks : public send_remote_packet_callbacks +{ + /* Constructor, initialise the result to nullptr. It is invalid to try + and read the result before sending a packet and processing the + reply. */ + + py_send_packet_callbacks () + : m_result (nullptr) + { /* Nothing. */ } + + /* There's nothing to do when the packet is sent. */ + + void sending (gdb::array_view<const char> &buf) override + { /* Nothing. */ } + + /* When the result is returned create a Python object and assign this + into M_RESULT. If for any reason we can't create a Python object to + represent the result then M_RESULT is set to nullptr, and Python's + internal error flags will be set. If the result we got back from the + remote is empty then set the result to None. */ + + void received (gdb::array_view<const char> &buf) override + { + if (buf.size () > 0 && buf.data ()[0] != '\0') + m_result.reset (PyBytes_FromStringAndSize (buf.data (), buf.size ())); + else + { + /* We didn't get back any result data; set the result to None. */ + Py_INCREF (Py_None); + m_result.reset (Py_None); + } + } + + /* Get a reference to the result as a Python object. It is invalid to + call this before sending a packet to the remote and processing the + reply. + + The result value is setup in the RECEIVED call above. If the RECEIVED + call causes an error then the result value will be set to nullptr, + and the error reason is left stored in Python's global error state. + + It is important that the result is inspected immediately after sending + a packet to the remote, and any error fetched, calling any other + Python functions that might clear the error state, or rely on an error + not being set will cause undefined behaviour. */ + + gdbpy_ref<> result () const + { + return m_result; + } + +private: + + /* A reference to the result value. */ + + gdbpy_ref<> m_result; +}; + +/* Implement RemoteTargetConnection.send_packet function. Send a packet to + the target identified by SELF. The connection must still be valid, and + the packet to be sent must be non-empty, otherwise an exception will be + thrown. */ + +static PyObject * +connpy_send_packet (PyObject *self, PyObject *args, PyObject *kw) +{ + connection_object *conn = (connection_object *) self; + + CONNPY_REQUIRE_VALID (conn); + + static const char *keywords[] = {"packet", nullptr}; + PyObject *packet_obj; + + if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "O", keywords, + &packet_obj)) + return nullptr; + + /* If the packet is a unicode string then convert it to a bytes object. */ + if (PyUnicode_Check (packet_obj)) + { + /* We encode the string to bytes using the ascii codec, if this fails + then a suitable error will have been set. */ + packet_obj = PyUnicode_AsASCIIString (packet_obj); + if (packet_obj == nullptr) + return nullptr; + } + + /* Check the packet is now a bytes object. */ + if (!PyBytes_Check (packet_obj)) + { + PyErr_SetString (PyExc_TypeError, _("Packet is not a bytes object")); + return nullptr; + } + + Py_ssize_t packet_len = 0; + char *packet_str_nonconst = nullptr; + if (PyBytes_AsStringAndSize (packet_obj, &packet_str_nonconst, + &packet_len) < 0) + return nullptr; + const char *packet_str = packet_str_nonconst; + gdb_assert (packet_str != nullptr); + + if (packet_len == 0) + { + PyErr_SetString (PyExc_ValueError, _("Packet must not be empty")); + return nullptr; + } + + try + { + scoped_restore_current_thread restore_thread; + switch_to_target_no_thread (conn->target); + + gdb::array_view<const char> view (packet_str, packet_len); + py_send_packet_callbacks callbacks; + send_remote_packet (view, &callbacks); + PyObject *result = callbacks.result ().release (); + /* If we encountered an error converting the reply to a Python + object, then the result here can be nullptr. In that case, Python + should be aware that an error occurred. */ + gdb_assert ((result == nullptr) == (PyErr_Occurred () != nullptr)); + return result; + } + catch (const gdb_exception &except) + { + gdbpy_convert_exception (except); + return nullptr; + } +} + /* Global initialization for this file. */ void _initialize_py_connection (); @@ -307,6 +457,17 @@ Return true if this TargetConnection is valid, false if not." }, { NULL } }; +/* Methods for the gdb.RemoteTargetConnection object type. */ + +static PyMethodDef remote_connection_object_methods[] = +{ + { "send_packet", (PyCFunction) connpy_send_packet, + METH_VARARGS | METH_KEYWORDS, + "send_packet (PACKET) -> Bytes\n\ +Send PACKET to a remote target, return the reply as a bytes array." }, + { NULL } +}; + /* Attributes for the gdb.TargetConnection object type. */ static gdb_PyGetSetDef connection_object_getset[] = @@ -345,7 +506,7 @@ PyTypeObject connection_object_type = 0, /* tp_getattro */ 0, /* tp_setattro */ 0, /* tp_as_buffer */ - Py_TPFLAGS_DEFAULT, /* tp_flags */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ "GDB target connection object", /* tp_doc */ 0, /* tp_traverse */ 0, /* tp_clear */ @@ -364,3 +525,46 @@ PyTypeObject connection_object_type = 0, /* tp_init */ 0 /* tp_alloc */ }; + +/* Define the gdb.RemoteTargetConnection object type. */ + +PyTypeObject remote_connection_object_type = +{ + PyVarObject_HEAD_INIT (NULL, 0) + "gdb.RemoteTargetConnection", /* tp_name */ + sizeof (connection_object), /* tp_basicsize */ + 0, /* tp_itemsize */ + connpy_connection_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + connpy_repr, /* 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, /* tp_flags */ + "GDB remote target connection object", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + remote_connection_object_methods, /* tp_methods */ + 0, /* tp_members */ + 0, /* tp_getset */ + &connection_object_type, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + 0, /* tp_init */ + 0 /* tp_alloc */ +}; diff --git a/gdb/remote.c b/gdb/remote.c index 466afa1fe58..22fcf8c25ab 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -994,6 +994,15 @@ class extended_remote_target final : public remote_target bool supports_disable_randomization () override; }; +/* See remote.h. */ + +bool +is_remote_target (process_stratum_target *target) +{ + remote_target *rt = dynamic_cast<remote_target *> (target); + return rt != nullptr; +} + /* Per-program-space data key. */ static const struct program_space_key<char, gdb::xfree_deleter<char>> remote_pspace_data; diff --git a/gdb/remote.h b/gdb/remote.h index 0178294ab1d..0ddf55d19d3 100644 --- a/gdb/remote.h +++ b/gdb/remote.h @@ -24,6 +24,8 @@ struct target_desc; struct remote_target; +class process_stratum_target; + /* True when printing "remote" debug statements is enabled. */ extern bool remote_debug; @@ -113,4 +115,10 @@ struct send_remote_packet_callbacks extern void send_remote_packet (gdb::array_view<const char> &buf, send_remote_packet_callbacks *callbacks); + +/* Return true if TARGET is a remote, or extended-remote target, otherwise, + return false. */ + +extern bool is_remote_target (process_stratum_target *target); + #endif diff --git a/gdb/testsuite/gdb.python/py-connection.exp b/gdb/testsuite/gdb.python/py-connection.exp index b805b052f73..96c83781839 100644 --- a/gdb/testsuite/gdb.python/py-connection.exp +++ b/gdb/testsuite/gdb.python/py-connection.exp @@ -33,12 +33,18 @@ if ![runto_main] then { return 0 } +if { [target_info exists gdb_protocol] } { + set connection_type "RemoteTargetConnection" +} else { + set connection_type "TargetConnection" +} + # Create a gdb.TargetConnection object and check it is initially # valid. gdb_test_no_output "python conn = gdb.selected_inferior().connection" gdb_test "python print(conn)" \ - "<gdb.TargetConnection num=1, what=\"\[^\"\]+\">" \ - "print gdb.TargetConnection while it is still valid" + "<gdb.${connection_type} num=1, what=\"\[^\"\]+\">" \ + "print gdb.${connection_type} while it is still valid" gdb_test "python print(conn.is_valid())" "True" "is_valid returns True" # Get the connection again, and ensure we get the exact same object. @@ -53,8 +59,8 @@ gdb_test "disconnect" "" "kill the inferior" \ "A program is being debugged already\\. Kill it\\? .*y or n. $" "y" gdb_test "info connections" "No connections\\." \ "info connections now all the connections have gone" -gdb_test "python print(conn)" "<gdb.TargetConnection \\(invalid\\)>" \ - "print gdb.TargetConnection now its invalid" +gdb_test "python print(conn)" "<gdb.${connection_type} \\(invalid\\)>" \ + "print gdb.${connection_type} now its invalid" gdb_test "python print(conn.is_valid())" "False" "is_valid returns False" # Now check that accessing properties of the invalid connection cases diff --git a/gdb/testsuite/gdb.python/py-send-packet.c b/gdb/testsuite/gdb.python/py-send-packet.c new file mode 100644 index 00000000000..49fbd79e11b --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.c @@ -0,0 +1,31 @@ +/* This testcase is part of GDB, the GNU debugger. + + Copyright 2021 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/>. */ + +volatile int global_var = 0; + +void +breakpt () +{ + /* Nothing. */ +} + +int +main (void) +{ + breakpt (); + return 0; +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.exp b/gdb/testsuite/gdb.python/py-send-packet.exp new file mode 100644 index 00000000000..59035757414 --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.exp @@ -0,0 +1,99 @@ +# Copyright (C) 2021 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 the gdb.RemoteTargetConnection.send_packet API. This is done +# by connecting to a remote target and fetching the thread list in two +# ways, first, we manually send the packets required to read the +# thread list using gdb.TargetConnection.send_packet, then we compare +# the results to the thread list using the standard API calls. + +load_lib gdb-python.exp +load_lib gdbserver-support.exp + +standard_testfile + +if {[skip_gdbserver_tests]} { + return 0 +} + +if { [prepare_for_testing "failed to prepare" ${testfile} ${srcfile}] } { + return -1 +} + +if { [skip_python_tests] } { + return 0 +} + +# Make sure we're disconnected, in case we're testing with an +# extended-remote board, therefore already connected. +gdb_test "disconnect" ".*" + +gdbserver_run "" + +gdb_breakpoint "breakpt" +gdb_continue_to_breakpoint "breakpt" + +# Source the python script. +set remote_python_file [gdb_remote_download host \ + ${srcdir}/${subdir}/${testfile}.py] +gdb_test "source $remote_python_file" "Sourcing complete\\." \ + "source ${testfile}.py script" + +# The test is actually written in the Python script. Run it now. +gdb_test "python run_send_packet_test()" "Send packet test passed" + +# Check the string representation of a remote target connection. +gdb_test "python print(gdb.selected_inferior().connection)" \ + "<gdb.RemoteTargetConnection num=$decimal, what=\".*\">" + +# Check to see if there's any auxv data for this target. +gdb_test_multiple "info auxv" "" { + -re -wrap "The program has no auxiliary information now\\. " { + set skip_auxv_test true + } + -re -wrap "0\\s+AT_NULL\\s+End of vector\\s+0x0" { + set skip_auxv_test false + } +} + +if { ! $skip_auxv_test } { + # Use 'maint packet' to fetch the auxv data. + set reply_data "" + gdb_test_multiple "maint packet qXfer:auxv:read::0,1000" "" { + -re "sending: \"qXfer:auxv:read::0,1000\"\r\n" { + exp_continue + } + -re -wrap "received: \"(.*)\"" { + set reply_data $expect_out(1,string) + } + } + + # Expand the '\x' in the output, so we can pass a string through + # to Python. + set reply_data [string map {\x \\x} $reply_data] + gdb_assert { ![string equal "$reply_data" ""] } + + # Run the test, fetches the auxv data in Python and confirm it + # matches the expected results. + gdb_test "python run_auxv_send_packet_test(\"$reply_data\")" \ + "auxv send packet test passed" +} + +set sizeof_global_var [get_valueof "/d" "sizeof(global_var)" "UNKNOWN"] +if { $sizeof_global_var == 4 } { + gdb_test_no_output "set debug remote 1" + gdb_test "python run_set_global_var_test()" \ + "set global_var test passed" +} diff --git a/gdb/testsuite/gdb.python/py-send-packet.py b/gdb/testsuite/gdb.python/py-send-packet.py new file mode 100644 index 00000000000..23abc42101c --- /dev/null +++ b/gdb/testsuite/gdb.python/py-send-packet.py @@ -0,0 +1,176 @@ +# Copyright (C) 2021 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 xml.etree.ElementTree as ET +import gdb + +# Make use of gdb.RemoteTargetConnection.send_packet to fetch the +# thread list from the remote target. +# +# Sending existing serial protocol packets like this is not a good +# idea, there should be better ways to get this information using an +# official API, this is just being used as a test case. +# +# Really, the send_packet API would be used to send target +# specific packets to the target, but these are, by definition, target +# specific, so hard to test in a general testsuite. +def get_thread_list_str(): + start_pos = 0 + thread_desc = "" + conn = gdb.selected_inferior().connection + if not isinstance(conn, gdb.RemoteTargetConnection): + raise gdb.GdbError("connection is the wrong type") + while True: + str = conn.send_packet("qXfer:threads:read::%d,200" % start_pos).decode("ascii") + start_pos += 200 + c = str[0] + str = str[1:] + thread_desc += str + if c == "l": + break + return thread_desc + + +# Use gdb.RemoteTargetConnection.send_packet to manually fetch the +# thread list, then extract the thread list using the gdb.Inferior and +# gdb.InferiorThread API. Compare the two results to ensure we +# managed to successfully read the thread list from the remote. +def run_send_packet_test(): + # Find the IDs of all current threads. + all_threads = {} + for inf in gdb.inferiors(): + for thr in inf.threads(): + id = "p%x.%x" % (thr.ptid[0], thr.ptid[1]) + all_threads[id] = False + + # Now fetch the thread list from the remote, and parse the XML. + str = get_thread_list_str() + threads_xml = ET.fromstring(str) + + # Look over all threads in the XML list and check we expected to + # find them, mark the ones we do find. + for thr in threads_xml: + id = thr.get("id") + if not id in all_threads: + raise "found unexpected thread in remote thread list" + else: + all_threads[id] = True + + # Check that all the threads were found in the XML list. + for id in all_threads: + if not all_threads[id]: + raise "thread missingt from remote thread list" + + # Test complete. + print("Send packet test passed") + + +# Convert a bytes object to a string. This follows the same rules as +# the 'maint packet' command so that the output from the two sources +# can be compared. +def bytes_to_string(byte_array): + + # Python 2/3 compatibility. We need a function that can give us + # the value of a single element in BYTE_ARRAY as an integer. + if sys.version_info[0] > 2: + value_of_single_byte = int + else: + value_of_single_byte = ord + + res = "" + for b in byte_array: + b = value_of_single_byte(b) + if b >= 32 and b <= 126: + res = res + ("%c" % b) + else: + res = res + ("\\x%02x" % b) + return res + + +# A very simple test for sending the packet that reads the auxv data. +# We convert the result to a string and expect to find some +# hex-encoded bytes in the output. This test will only work on +# targets that actually supply auxv data. +def run_auxv_send_packet_test(expected_result): + inf = gdb.selected_inferior() + conn = inf.connection + assert isinstance(conn, gdb.RemoteTargetConnection) + res = conn.send_packet("qXfer:auxv:read::0,1000") + assert isinstance(res, bytes) + string = bytes_to_string(res) + assert string.count("\\x") > 0 + assert string == expected_result + print("auxv send packet test passed") + + +# Check that the value of 'global_var' is EXPECTED_VAL. +def check_global_var(expected_val): + val = int(gdb.parse_and_eval("global_var")) + val = val & 0xFFFFFFFF + if val != expected_val: + raise gdb.GdbError("global_var is 0x%x, expected 0x%x" % (val, expected_val)) + + +# Set the 'X' packet to the remote target to set a global variable. +# Checks that we can send byte values. +def run_set_global_var_test(): + inf = gdb.selected_inferior() + conn = inf.connection + assert isinstance(conn, gdb.RemoteTargetConnection) + addr = gdb.parse_and_eval("&global_var") + res = conn.send_packet("X%x,4:\x01\x01\x01\x01" % addr) + assert isinstance(res, bytes) + check_global_var(0x01010101) + res = conn.send_packet(b"X%x,4:\x02\x02\x02\x02" % addr) + assert isinstance(res, bytes) + check_global_var(0x02020202) + if sys.version_info[0] > 2: + # On Python 3 this first attempt will not work as we're + # passing a Unicode string containing non-ascii characters. + saw_error = False + try: + res = conn.send_packet("X%x,4:\xff\xff\xff\xff" % addr) + except UnicodeError: + saw_error = True + except: + assert False + assert saw_error + check_global_var(0x02020202) + # Now we pass a bytes object, which will work. + res = conn.send_packet(b"X%x,4:\xff\xff\xff\xff" % addr) + check_global_var(0xFFFFFFFF) + else: + # On Python 2 we need to force the creation of a Unicode + # string, but, with that done, we expect to see the same error + # as on Python 3; the unicode string contains non-ascii + # characters. + saw_error = False + try: + res = conn.send_packet(unicode("X%x,4:\xff\xff\xff\xff") % addr) + except UnicodeError: + saw_error = True + except: + assert False + assert saw_error + check_global_var(0x02020202) + # Now we pass a plain string, which, on Python 2, is the same + # as a bytes object, this, we expect to work. + res = conn.send_packet("X%x,4:\xff\xff\xff\xff" % addr) + check_global_var(0xFFFFFFFF) + print("set global_var test passed") + + +# Just to indicate the file was sourced correctly. +print("Sourcing complete.") ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-11-16 12:48 ` Andrew Burgess @ 2021-11-16 15:10 ` Simon Marchi 2021-11-30 12:15 ` Andrew Burgess 0 siblings, 1 reply; 52+ messages in thread From: Simon Marchi @ 2021-11-16 15:10 UTC (permalink / raw) To: Andrew Burgess, gdb-patches On 2021-11-16 7:48 a.m., Andrew Burgess via Gdb-patches wrote: > Simon, Eli, > > Thanks for the feedback, I think I've addressed everything in this > update. > > Simon, sorry for not quite getting your point the first time through. > I believe that this revision should address your concerns. I have not > been as restrictive as you suggested, but hopefully this should still > be acceptable. > > So, you can still pass a string, but the documentation is specific > that it must be possible to convert the string to a bytes object using > the ascii codec. This allows for what I assume would be the most > common use case: > > conn.send_packet("some_ascii_text") > > But if we revisit your example, we now get: > > res = conn.send_packet('X555555558028,4:\xff\x03\x02\xff') > UnicodeEncodeError: 'ascii' codec can't encode character '\xff' in position 10: ordinal not in range(128) > > In which case, the solution is, as your suggest, to pass a bytes > object: > > res = conn.send_packet(b'X555555558028,4:\xff\x03\x02\xff') > print(res) > b'OK' > > I've tested this code with Python 3.7 and Python 2.7 and it seems to > work fine. I've extended the test to include your example and related > cases, hopefully that should cover what I've said above. > > Let me know what you think, If I had done it myself, I would have accepted just "bytes" objects, to be more straightforward in the implementation and the doc. But I am still happy with what you have, the user won't have sneaky encoding bugs. So, +1 from me. Simon ^ permalink raw reply [flat|nested] 52+ messages in thread
* Re: [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet 2021-11-16 15:10 ` Simon Marchi @ 2021-11-30 12:15 ` Andrew Burgess 0 siblings, 0 replies; 52+ messages in thread From: Andrew Burgess @ 2021-11-30 12:15 UTC (permalink / raw) To: Simon Marchi; +Cc: gdb-patches * Simon Marchi <simark@simark.ca> [2021-11-16 10:10:23 -0500]: > On 2021-11-16 7:48 a.m., Andrew Burgess via Gdb-patches wrote: > > Simon, Eli, > > > > Thanks for the feedback, I think I've addressed everything in this > > update. > > > > Simon, sorry for not quite getting your point the first time through. > > I believe that this revision should address your concerns. I have not > > been as restrictive as you suggested, but hopefully this should still > > be acceptable. > > > > So, you can still pass a string, but the documentation is specific > > that it must be possible to convert the string to a bytes object using > > the ascii codec. This allows for what I assume would be the most > > common use case: > > > > conn.send_packet("some_ascii_text") > > > > But if we revisit your example, we now get: > > > > res = conn.send_packet('X555555558028,4:\xff\x03\x02\xff') > > UnicodeEncodeError: 'ascii' codec can't encode character '\xff' in position 10: ordinal not in range(128) > > > > In which case, the solution is, as your suggest, to pass a bytes > > object: > > > > res = conn.send_packet(b'X555555558028,4:\xff\x03\x02\xff') > > print(res) > > b'OK' > > > > I've tested this code with Python 3.7 and Python 2.7 and it seems to > > work fine. I've extended the test to include your example and related > > cases, hopefully that should cover what I've said above. > > > > Let me know what you think, > > If I had done it myself, I would have accepted just "bytes" objects, to > be more straightforward in the implementation and the doc. But I am > still happy with what you have, the user won't have sneaky encoding > bugs. So, +1 from me. Thanks for all the reviewing. I've now pushed this series. Thanks, Andrew ^ permalink raw reply [flat|nested] 52+ messages in thread
end of thread, other threads:[~2021-11-30 12:15 UTC | newest] Thread overview: 52+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2021-09-11 16:03 [PATCH 0/3] Python API for target connections, and packet sending Andrew Burgess 2021-09-11 16:03 ` [PATCH 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-09-11 16:19 ` Eli Zaretskii 2021-09-11 16:03 ` [PATCH 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess 2021-09-11 16:03 ` [PATCH 3/3] gdb/python: add TargetConnection.send_remote_packet method Andrew Burgess 2021-09-11 16:10 ` Eli Zaretskii 2021-10-18 9:45 ` [PATCHv2 0/3] Python API for target connections, and packet sending Andrew Burgess 2021-10-18 9:45 ` [PATCHv2 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-18 12:44 ` Eli Zaretskii 2021-10-18 15:53 ` Simon Marchi 2021-10-18 9:45 ` [PATCHv2 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess 2021-10-18 9:45 ` [PATCHv2 3/3] gdb/python: add TargetConnection.send_remote_packet method Andrew Burgess 2021-10-18 12:57 ` Eli Zaretskii 2021-10-18 15:46 ` Simon Marchi 2021-10-19 10:17 ` [PATCHv3 0/3] Python API for target connections, and packet sending Andrew Burgess 2021-10-19 10:17 ` [PATCHv3 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-19 12:26 ` Eli Zaretskii 2021-10-20 22:33 ` Lancelot SIX 2021-10-21 2:00 ` Simon Marchi 2021-10-19 10:17 ` [PATCHv3 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess 2021-10-21 2:23 ` Simon Marchi 2021-10-19 10:17 ` [PATCHv3 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2021-10-19 12:28 ` Eli Zaretskii 2021-10-21 2:43 ` Simon Marchi 2021-10-22 11:08 ` Andrew Burgess 2021-10-22 11:18 ` Simon Marchi 2021-10-22 17:11 ` Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 0/4] Python API for target connections, and packet sending Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 1/4] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 2/4] gdb: make packet_command function available outside remote.c Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2021-10-22 10:58 ` [PATCHv4 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 1/4] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 2/4] gdb: make packet_command function available outside remote.c Andrew Burgess 2021-10-22 17:10 ` [PATCHv5 3/4] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2021-11-15 2:08 ` Simon Marchi 2021-11-15 9:25 ` Andrew Burgess 2021-11-15 13:16 ` Simon Marchi 2021-10-22 17:10 ` [PATCHv5 4/4] gdb: handle binary data in 'maint packet' and RemoteTargetConnection.send_packet Andrew Burgess 2021-11-15 2:44 ` Simon Marchi 2021-11-09 10:04 ` [PATCHv5 0/4] Python API for target connections, and packet sending Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 0/3] " Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 1/3] gdb/python: introduce gdb.TargetConnection object type Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 2/3] gdb: make packet_command function available outside remote.c Andrew Burgess 2021-11-15 17:40 ` [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet Andrew Burgess 2021-11-15 18:42 ` Eli Zaretskii 2021-11-15 19:38 ` Simon Marchi 2021-11-15 19:29 ` Simon Marchi 2021-11-16 12:48 ` Andrew Burgess 2021-11-16 15:10 ` Simon Marchi 2021-11-30 12:15 ` Andrew Burgess
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).