public inbox for gdb-patches@sourceware.org
 help / color / mirror / Atom feed
From: Andrew Burgess <aburgess@redhat.com>
To: gdb-patches@sourceware.org
Cc: Eli Zaretskii <eliz@gnu.org>, Simon Marchi <simark@simark.ca>
Subject: Re: [PATCHv6 3/3] gdb/python: add gdb.RemoteTargetConnection.send_packet
Date: Tue, 16 Nov 2021 12:48:47 +0000	[thread overview]
Message-ID: <20211116124847.GJ2352@redhat.com> (raw)
In-Reply-To: <1ee3ff1661f4e29adb8cf1daf90f88bd7c282a77.1636997240.git.aburgess@redhat.com>

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.")


  parent reply	other threads:[~2021-11-16 12:48 UTC|newest]

Thread overview: 52+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
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 [this message]
2021-11-16 15:10               ` Simon Marchi
2021-11-30 12:15                 ` Andrew Burgess

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20211116124847.GJ2352@redhat.com \
    --to=aburgess@redhat.com \
    --cc=eliz@gnu.org \
    --cc=gdb-patches@sourceware.org \
    --cc=simark@simark.ca \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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).