gdb/python: add a new gdb_exiting event
authorAndrew Burgess <andrew.burgess@embecosm.com>
Tue, 7 Sep 2021 10:45:55 +0000 (11:45 +0100)
committerAndrew Burgess <andrew.burgess@embecosm.com>
Tue, 5 Oct 2021 09:05:40 +0000 (10:05 +0100)
Add a new event, gdb.events.gdb_exiting, which is called once GDB
decides it is going to exit.

This event is not triggered in the case that GDB performs a hard
abort, for example, when handling an internal error and the user
decides to quit the debug session, or if GDB hits an unexpected,
fatal, signal.

This event is triggered if the user just types 'quit' at the command
prompt, or if GDB is run with '-batch' and has processed all of the
required commands.

The new event type is gdb.GdbExitingEvent, and it has a single
attribute exit_code, which is the value that GDB is about to exit
with.

The event is triggered before GDB starts dismantling any of its own
internal state, so, my expectation is that most Python calls should
work just fine at this point.

When considering this functionality I wondered about using the
'atexit' Python module.  However, this is triggered when the Python
environment is shut down, which is done from a final cleanup.  At
this point we don't know for sure what other GDB state has already
been cleaned up.

gdb/NEWS
gdb/doc/python.texi
gdb/observable.c
gdb/observable.h
gdb/python/py-all-events.def
gdb/python/py-event-types.def
gdb/python/python.c
gdb/testsuite/gdb.python/py-events.exp
gdb/testsuite/gdb.python/py-events.py
gdb/top.c

index 1e25cb8cc3694384eb191e02f4fb695ad6eb4536..e0fb006909ef441c463b1e4b1cc42af78746c638 100644 (file)
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -34,6 +34,12 @@ maint show internal-warning backtrace
      integer, the index of the new item in the history list, is
      returned.
 
+  ** New gdb.events.gdb_exiting event.  This event is called with a
+     gdb.GdbExitingEvent object which has the read-only attribute
+     'exit_code', which contains the value of the GDB exit code.  This
+     event is triggered once GDB decides it is going to exit, but
+     before GDB starts to clean up its internal state.
+
 *** Changes in GDB 11
 
 * The 'set disassembler-options' command now supports specifying options
index b123b2409804ab5a5502c38679e1a646269e879d..15bf9dc3e21ae17002153adb6ac6ae3d6fe0e36f 100644 (file)
@@ -3418,6 +3418,16 @@ This has a single attribute:
 The new thread.
 @end defvar
 
+@item events.gdb_exiting
+This is emitted when @value{GDBN} exits.  This event is not emitted if
+@value{GDBN} exits as a result of an internal error, or after an
+unexpected signal.  The event is of type @code{gdb.GdbExitingEvent},
+which has a single attribute:
+
+@defvar GdbExitingEvent.exit_code
+An integer, the value of the exit code @value{GDBN} will return.
+@end defvar
+
 @end table
 
 @node Threads In Python
index 51f5edb0a1f6e6c8433d9dca846a9ff66a621dc8..b020076cf26b9e477a6a38e8f6a7d4f0fbb8582e 100644 (file)
@@ -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 (gdb_exiting);
 
 } /* namespace observers */
 } /* namespace gdb */
index 915770ff3632ca9585d194d00b28ec2f4c726895..f20f532870f58bd82c9a29c520d3bb736b3375ee 100644 (file)
@@ -248,9 +248,11 @@ extern observable<> source_styling_changed;
 /* The CLI's notion of the current source has changed.  This differs
    from user_selected_context_changed in that it is also set by the
    "list" command.  */
-
 extern observable<> current_source_symtab_and_line_changed;
 
+/* Called when GDB is about to exit.  */
+extern observable<int> gdb_exiting;
+
 } /* namespace observers */
 
 } /* namespace gdb */
index d12a2103e8009c3ba75074154a9d08fe876f4e31..83f10989e4aea9978f1923eb357c910e2b8909b2 100644 (file)
@@ -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(gdb_exiting)
index 70df4804fc4a934084a33a6cf66e0d484c1644c5..aeaee02e8bb7f91aea51cdd5b20248b069699428 100644 (file)
@@ -105,3 +105,8 @@ GDB_PY_DEFINE_EVENT_TYPE (thread,
                          "ThreadEvent",
                          "GDB thread event object",
                          event_object_type);
+
+GDB_PY_DEFINE_EVENT_TYPE (gdb_exiting,
+                         "GdbExitingEvent",
+                         "GDB is about to exit",
+                         event_object_type);
index fcd367fe2ec93f6980fc8c02c3c70037cfff5caa..451c5bdfe0b6d5ebddcd8c4176e3f9e4f316a393 100644 (file)
@@ -36,6 +36,7 @@
 #include "location.h"
 #include "run-on-main-thread.h"
 #include "gdbsupport/selftest.h"
+#include "observable.h"
 
 /* Declared constants and enum for python stack printing.  */
 static const char python_excp_none[] = "none";
@@ -1720,6 +1721,38 @@ init__gdb_module (void)
 }
 #endif
 
+/* Emit a gdb.GdbExitingEvent, return a negative value if there are any
+   errors, otherwise, return 0.  */
+
+static int
+emit_exiting_event (int exit_code)
+{
+  gdbpy_ref<> event_obj = create_event_object (&gdb_exiting_event_object_type);
+  if (event_obj == nullptr)
+    return -1;
+
+  gdbpy_ref<> code = gdb_py_object_from_longest (exit_code);
+  if (evpy_add_attribute (event_obj.get (), "exit_code", code.get ()) < 0)
+    return -1;
+
+  return evpy_emit_event (event_obj.get (), gdb_py_events.gdb_exiting);
+}
+
+/* Callback for the gdb_exiting observable.  EXIT_CODE is the value GDB
+   will exit with.  */
+
+static void
+gdbpy_gdb_exiting (int exit_code)
+{
+  if (!gdb_python_initialized)
+    return;
+
+  gdbpy_enter enter_py (python_gdbarch, python_language);
+
+  if (emit_exiting_event (exit_code) < 0)
+    gdbpy_print_stack ();
+}
+
 static bool
 do_start_initialization ()
 {
@@ -1871,6 +1904,8 @@ do_start_initialization ()
   if (gdbpy_value_cst == NULL)
     return false;
 
+  gdb::observers::gdb_exiting.attach (gdbpy_gdb_exiting, "python");
+
   /* Release the GIL while gdb runs.  */
   PyEval_SaveThread ();
 
index 78915241e0f0a50aa0bac05270103dbf23193d38..529eeeb404964b11557a693e96cb160de5ebd316 100644 (file)
@@ -282,21 +282,70 @@ with_test_prefix "inferior continue exit" {
 }
 
 # Check that when GDB exits, we see gdb.ExitedEvent objects with no
-# 'exit_code' attribute.
-with_test_prefix "gdb exiting" {
+# 'exit_code' attribute, and that a gdb.GdbExitingEvent is emitted.
+with_test_prefix "gdb exiting: normal" {
+    gdb_test "test-exiting-event normal" "GDB exiting event registered\\."
+
+    set saw_exiting_event 0
     set saw_inferior_exit 0
     gdb_test_multiple "quit" "" {
        -re "Quit anyway\\? \\(y or n\\) $" {
            send_gdb "y\n"
            exp_continue
        }
+       -re "event type: gdb-exiting\r\nexit code: $decimal" {
+           incr saw_exiting_event
+           exp_continue
+       }
        -re "event type: exit\r\nexit code: not-present\r\nexit inf: $decimal\r\nexit pid: $decimal\r\ndir ok: False\r\n" {
            incr saw_inferior_exit
            exp_continue
        }
        eof {
+           gdb_assert { $saw_exiting_event == 1 }
            gdb_assert { $saw_inferior_exit == 2 }
            pass $gdb_test_name
        }
     }
 }
+
+# Check that if the GdbExitingEvent raises an exception then this
+# doesn't impact GDB's exit process.
+with_test_prefix "gdb exiting: error" {
+    clean_restart ${testfile}
+
+    if ![runto_main] then {
+       fail "cannot run to main."
+       return 0
+    }
+
+    gdb_test_no_output "source ${pyfile}" "load python file"
+    gdb_test "test-exiting-event error" "GDB exiting event registered\\."
+    gdb_test "test-events" "Event testers registered\\."
+
+    set saw_exiting_error 0
+    set saw_inferior_exit 0
+    gdb_test_multiple "quit" "" {
+       -re "Quit anyway\\? \\(y or n\\) $" {
+           send_gdb "y\n"
+           exp_continue
+       }
+       -re "event type: gdb-exiting\r\nexit code: $decimal" {
+           fail "$gdb_test_name XXXX"
+           exp_continue
+       }
+       -re "Python Exception <class 'gdb.GdbError'>: error from gdb_exiting_handler\r\n" {
+           incr saw_exiting_error
+           exp_continue
+       }
+       -re "event type: exit\r\nexit code: not-present\r\nexit inf: $decimal\r\nexit pid: $decimal\r\ndir ok: False\r\n" {
+           incr saw_inferior_exit
+           exp_continue
+       }
+       eof {
+           gdb_assert { $saw_inferior_exit == 1 }
+           gdb_assert { $saw_exiting_error == 1 }
+           pass $gdb_test_name
+       }
+    }
+}
index b21f562bfd3002b39a579a17c7287cae2d2cda88..57b0324598f66e5afdab635fb68a0d94349f1e62 100644 (file)
@@ -132,3 +132,28 @@ class test_newobj_events(gdb.Command):
 
 
 test_newobj_events()
+
+def gdb_exiting_handler(event, throw_error):
+    assert isinstance(event, gdb.GdbExitingEvent)
+    if throw_error:
+        raise gdb.GdbError("error from gdb_exiting_handler")
+    else:
+        print("event type: gdb-exiting")
+        print("exit code: %d" % (event.exit_code))
+
+class test_exiting_event(gdb.Command):
+    """GDB Exiting event."""
+
+    def __init__(self):
+        gdb.Command.__init__(self, "test-exiting-event", gdb.COMMAND_STACK)
+
+    def invoke(self, arg, from_tty):
+        if arg == "normal":
+            gdb.events.gdb_exiting.connect(lambda e: gdb_exiting_handler(e,False))
+        elif arg == "error":
+            gdb.events.gdb_exiting.connect(lambda e: gdb_exiting_handler(e,True))
+        else:
+            raise gdb.GdbError("invalid or missing argument")
+        print("GDB exiting event registered.")
+
+test_exiting_event()
index 75692d34dfa95d48e6b990fafc858d469da077f1..08cdb487df482f82a4d0bd7db2eb149af0ee92e4 100644 (file)
--- a/gdb/top.c
+++ b/gdb/top.c
@@ -1761,8 +1761,6 @@ quit_force (int *exit_arg, int from_tty)
 {
   int exit_code = 0;
 
-  undo_terminal_modifications_before_exit ();
-
   /* An optional expression may be used to cause gdb to terminate with the
      value of that expression.  */
   if (exit_arg)
@@ -1770,6 +1768,10 @@ quit_force (int *exit_arg, int from_tty)
   else if (return_child_result)
     exit_code = return_child_result_value;
 
+  gdb::observers::gdb_exiting.notify (exit_code);
+
+  undo_terminal_modifications_before_exit ();
+
   /* We want to handle any quit errors and exit regardless.  */
 
   /* Get out of tfind mode, and kill or detach all inferiors.  */