public inbox for gdb-cvs@sourceware.org
help / color / mirror / Atom feed
* [binutils-gdb] gdb/python: implement support for sending custom MI async notifications
@ 2023-10-10 10:26 Jan Vrany
  0 siblings, 0 replies; only message in thread
From: Jan Vrany @ 2023-10-10 10:26 UTC (permalink / raw)
  To: gdb-cvs

https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=4825fd2d3552a047264a1f3d900ce30047a8c5f6

commit 4825fd2d3552a047264a1f3d900ce30047a8c5f6
Author: Jan Vrany <jan.vrany@labware.com>
Date:   Tue Oct 10 11:22:56 2023 +0100

    gdb/python: implement support for sending custom MI async notifications
    
    This commit adds a new Python function, gdb.notify_mi, that can be used
    to emit custom async notification to MI channel.  This can be used, among
    other things, to implement notifications about events MI does not support,
    such as remote connection closed or register change.
    
    Reviewed-By: Eli Zaretskii <eliz@gnu.org>
    Approved-By: Andrew Burgess <aburgess@redhat.com>

Diff:
---
 gdb/NEWS                                  |  5 +++
 gdb/doc/python.texi                       | 45 +++++++++++++++++++
 gdb/python/py-mi.c                        | 74 +++++++++++++++++++++++++++++++
 gdb/python/python-internal.h              |  5 +++
 gdb/python/python.c                       |  4 ++
 gdb/testsuite/gdb.python/py-mi-notify.exp | 71 +++++++++++++++++++++++++++++
 6 files changed, 204 insertions(+)

diff --git a/gdb/NEWS b/gdb/NEWS
index 0f75abede47..81264c0cfb3 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -6,6 +6,11 @@
 * GDB index now contains information about the main function. This speeds up
   startup when it is being used for some large binaries.
 
+* Python API
+
+  ** New function gdb.notify_mi(NAME, DATA), that emits custom
+     GDB/MI async notification.
+
 *** Changes in GDB 14
 
 * GDB now supports the AArch64 Scalable Matrix Extension 2 (SME2), which
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index a97e4451897..546b4d4b962 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -211,6 +211,7 @@ optional arguments while skipping others.  Example:
 * Recordings In Python::        Accessing recordings from Python.
 * CLI Commands In Python::      Implementing new CLI commands in Python.
 * GDB/MI Commands In Python::   Implementing new @sc{gdb/mi} commands in Python.
+* GDB/MI Notifications In Python:: Implementing new @sc{gdb/mi} notifications in Python.
 * Parameters In Python::        Adding new @value{GDBN} parameters.
 * Functions In Python::         Writing new convenience functions.
 * Progspaces In Python::        Program spaces.
@@ -4804,6 +4805,50 @@ Here is how this works using the commands from the example above:
 @{'string': 'abc, def, ghi'@}
 @end smallexample
 
+@node GDB/MI Notifications In Python
+@subsubsection @sc{gdb/mi} Notifications In Python
+
+@cindex MI notifications in python
+@cindex notifications in python, GDB/MI
+@cindex python notifications, GDB/MI
+
+It is possible to emit @sc{gdb/mi} notifications from
+Python.  Use the @code{gdb.notify_mi} function to do that.
+
+@defun gdb.notify_mi (name @r{[}, data@r{]})
+Emit a @sc{gdb/mi} asynchronous notification.  @var{name} is the name of the
+notification, consisting of alphanumeric characters and a hyphen (@code{-}).
+@var{data} is any additional data to be emitted with the notification, passed
+as a Python dictionary. This argument is optional. The dictionary is converted
+to a @sc{gdb/mi} @var{result} records (@pxref{GDB/MI Output Syntax}) the same way
+as result of Python MI command (@pxref{GDB/MI Commands In Python}).
+
+If @var{data} is @code{None} then no additional values are emitted.
+@end defun
+
+While using existing notification names (@pxref{GDB/MI Async Records}) with
+@code{gdb.notify_mi} is allowed, users are encouraged to prefix user-defined
+notification with a hyphen (@code{-}) to avoid possible conflict.
+@value{GDBN} will never introduce notification starting with hyphen.
+
+Here is how to emit @code{=-connection-removed} whenever a connection to remote
+GDB server is closed (@pxref{Connections In Python}):
+
+@smallexample
+def notify_connection_removed(event):
+    data = @{"id": event.connection.num, "type": event.connection.type@}
+    gdb.notify_mi("-connection-removed", data)
+
+
+gdb.events.connection_removed.connect(notify_connection_removed)
+@end smallexample
+
+Then, each time a connection is closed, there will be a notification on MI channel:
+
+@smallexample
+=-connection-removed,id="1",type="remote"
+@end smallexample
+
 @node Parameters In Python
 @subsubsection Parameters In Python
 
diff --git a/gdb/python/py-mi.c b/gdb/python/py-mi.c
index 36bcb6ceece..a7b4f4fa3cf 100644
--- a/gdb/python/py-mi.c
+++ b/gdb/python/py-mi.c
@@ -19,8 +19,14 @@
 
 #include "defs.h"
 #include "python-internal.h"
+#include "utils.h"
+#include "ui.h"
 #include "ui-out.h"
+#include "interps.h"
+#include "target.h"
 #include "mi/mi-parse.h"
+#include "mi/mi-console.h"
+#include "mi/mi-interp.h"
 
 /* A ui_out subclass that creates a Python object based on the data
    that is passed in.  */
@@ -455,3 +461,71 @@ serialize_mi_results (PyObject *results)
       serialize_mi_result_1 (value, key_string.get ());
     }
 }
+
+/* See python-internal.h.  */
+
+PyObject *
+gdbpy_notify_mi (PyObject *self, PyObject *args, PyObject *kwargs)
+{
+  static const char *keywords[] = { "name", "data", nullptr };
+  char *name = nullptr;
+  PyObject *data = Py_None;
+
+  if (!gdb_PyArg_ParseTupleAndKeywords (args, kwargs, "s|O", keywords,
+					&name, &data))
+    return nullptr;
+
+  /* Validate notification name.  */
+  const int name_len = strlen (name);
+  if (name_len == 0)
+    {
+      PyErr_SetString (PyExc_ValueError, _("MI notification name is empty."));
+      return nullptr;
+    }
+  for (int i = 0; i < name_len; i++)
+    {
+      if (!isalnum (name[i]) && name[i] != '-')
+	{
+	  PyErr_Format
+	    (PyExc_ValueError,
+	     _("MI notification name contains invalid character: %c."),
+	     name[i]);
+	  return nullptr;
+	}
+    }
+
+  /* Validate additional data.  */
+  if (!(data == Py_None || PyDict_Check (data)))
+    {
+      PyErr_Format
+	(PyExc_ValueError,
+	 _("MI notification data must be either None or a dictionary, not %s"),
+	 Py_TYPE (data)->tp_name);
+      return nullptr;
+    }
+
+  SWITCH_THRU_ALL_UIS ()
+    {
+      struct mi_interp *mi = as_mi_interp (top_level_interpreter ());
+
+      if (mi == nullptr)
+	continue;
+
+      target_terminal::scoped_restore_terminal_state term_state;
+      target_terminal::ours_for_output ();
+
+      gdb_printf (mi->event_channel, "%s", name);
+      if (data != Py_None)
+	{
+	  ui_out *mi_uiout = mi->interp_ui_out ();
+	  ui_out_redirect_pop redir (mi_uiout, mi->event_channel);
+	  scoped_restore restore_uiout
+	    = make_scoped_restore (&current_uiout, mi_uiout);
+
+	  serialize_mi_results (data);
+	}
+      gdb_flush (mi->event_channel);
+    }
+
+  Py_RETURN_NONE;
+}
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index 60b795ff98c..847bed84dfe 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -499,6 +499,11 @@ extern PyObject *gdbpy_execute_mi_command (PyObject *self, PyObject *args,
 
 extern void serialize_mi_results (PyObject *results);
 
+/* Implementation of the gdb.notify_mi function.  */
+
+extern PyObject *gdbpy_notify_mi (PyObject *self, PyObject *args,
+				  PyObject *kw);
+
 /* Convert Python object OBJ to a program_space pointer.  OBJ must be a
    gdb.Progspace reference.  Return nullptr if the gdb.Progspace is not
    valid (see gdb.Progspace.is_valid), otherwise return the program_space
diff --git a/gdb/python/python.c b/gdb/python/python.c
index 6a978d632e9..faa7e0c217d 100644
--- a/gdb/python/python.c
+++ b/gdb/python/python.c
@@ -2669,6 +2669,10 @@ Return the name of the currently selected language." },
     "print_options () -> dict\n\
 Return the current print options." },
 
+  { "notify_mi", (PyCFunction) gdbpy_notify_mi,
+    METH_VARARGS | METH_KEYWORDS,
+    "notify_mi (name, data) -> None\n\
+Output async record to MI channels if any." },
   {NULL, NULL, 0, NULL}
 };
 
diff --git a/gdb/testsuite/gdb.python/py-mi-notify.exp b/gdb/testsuite/gdb.python/py-mi-notify.exp
new file mode 100644
index 00000000000..8ba770391c2
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-mi-notify.exp
@@ -0,0 +1,71 @@
+# Copyright (C) 2023 Free Software Foundation, Inc.
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test custom MI notifications implemented in Python.
+
+load_lib gdb-python.exp
+load_lib mi-support.exp
+set MIFLAGS "-i=mi"
+
+gdb_exit
+if {[mi_gdb_start]} {
+    return
+}
+
+if {[lsearch -exact [mi_get_features] python] < 0} {
+    unsupported "python support is disabled"
+    return -1
+}
+
+standard_testfile
+
+mi_gdb_test "set python print-stack full" \
+    ".*\\^done" \
+    "set python print-stack full"
+
+mi_gdb_test "python gdb.notify_mi('-test-notification')" \
+    ".*=-test-notification\r\n\\^done" \
+    "python notification, no additional data parameter"
+
+mi_gdb_test "python gdb.notify_mi('-test-notification', None)" \
+    ".*=-test-notification\r\n\\^done" \
+    "python notification, no additional data"
+
+mi_gdb_test "python gdb.notify_mi('-test-notification', \{ 'data1' : 1 , 'data2' : 2 })" \
+    ".*=-test-notification,data1=\"1\",data2=\"2\"\r\n\\^done" \
+    "python notification, with additional data"
+
+mi_gdb_test "python gdb.notify_mi('-test-notification', 1)" \
+    ".*\\^error,msg=\".*\"" \
+    "python notification, invalid additional data"
+
+mi_gdb_test "python gdb.notify_mi('', None)" \
+    ".*\\^error,msg=\".*\"" \
+    "python notification, empty notification name"
+
+mi_gdb_test "python gdb.notify_mi('**invalid**', None)" \
+    ".*\\^error,msg=\".*\"" \
+    "python notification, invalid notification name"
+
+mi_gdb_test "python gdb.notify_mi(\[1,2,3\], None)" \
+    ".*\\^error,msg=\".*\"" \
+    "python notification, non-string notification name"
+
+mi_gdb_test "python gdb.notify_mi()" \
+    ".*\\^error,msg=\".*\"" \
+    "python notification, no arguments passed"
+
+mi_gdb_test "python gdb.notify_mi('thread-group-added', \{'id' : 'i2'\})" \
+    ".*=thread-group-added,id=\"i2\"\r\n\\^done" \
+    "python notification, using existing internal notification name"

^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2023-10-10 10:26 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-10-10 10:26 [binutils-gdb] gdb/python: implement support for sending custom MI async notifications Jan Vrany

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