gdb/python: add a new gdb_exiting event

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.
This commit is contained in:
Andrew Burgess 2021-09-07 11:45:55 +01:00
parent 1cb56ad3f3
commit b1f0f28418
10 changed files with 141 additions and 5 deletions

View file

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

View 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

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

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

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

View 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);

View 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 ();

View 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
}
}
}

View 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()

View file

@ -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. */