public inbox for gdb-patches@sourceware.org
 help / color / mirror / Atom feed
* [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

* [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

* 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

* [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

* [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 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 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

* 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

* [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

* [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

* [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 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 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 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

* 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

* 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

* [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

* 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

* [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

* [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: [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

* 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

* 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 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 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

* [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 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 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
  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).