gdb/python: new events.corefile_changed event

Add a new Python event registry, events.corefile_changed.  This event
is emitted each time the corefile within an inferior changes.

The event object has a single 'inferior' attribute which is the
gdb.Inferior object for which the core file changed.  The user can
then inspect Inferior.corefile to see details about the new core file,
or this will be None if the core file was removed from the inferior.

I've updated the existing test to cover this new event.

The new test covers both the corefile_changed event, but also monitors
the exited event.  This ties into the work done in the previous
commit where we use whether the inferior has exited or not as a guard
for whether core_target::exit_core_file_inferior should be called.
Unloading a core file should result in a single corefile_changed event
and a single exited event.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
This commit is contained in:
Andrew Burgess 2026-03-27 09:52:55 +00:00
parent 3780b9993c
commit 4ecaac39c2
7 changed files with 326 additions and 21 deletions

View file

@ -244,6 +244,11 @@ qExecAndArgs
a tuple of pairs each representing a single range. Contiguous blocks
have only one range.
** New event registry gdb.events.corefile_changed, which emits a
CorefileChangedEvent whenever the core file associated with an
inferior changes. The event has an 'inferior' attribute which is
the gdb.Inferior in which the core file has changed.
* Guile API
** Procedures 'memory-port-read-buffer-size',

View file

@ -4147,6 +4147,17 @@ file is updated first, so when this event is emitted, the executable
filename will have changed, but the symbol filename might still hold
its previous value.
@item events.corefile_changed
Emits @code{gdb.CorefileChangedEvent} which indicates that the core
file associated with a @code{gdb.Inferior} has changed, either a new
core file has been loaded, or the existing core file has been
unloaded (@pxref{Core Files In Python}).
@defvar CorefileChangedEvent.inferior
The @code{gdb.Inferior} in which the core file has changed
(@pxref{Inferiors In Python}).
@end defvar
@item events.new_progspace
This is emitted when @value{GDBN} adds a new program space
(@pxref{Progspaces In Python,,Program Spaces In Python}). The event

View file

@ -47,3 +47,4 @@ GDB_PY_DEFINE_EVENT(new_progspace)
GDB_PY_DEFINE_EVENT(free_progspace)
GDB_PY_DEFINE_EVENT(tui_enabled)
GDB_PY_DEFINE_EVENT(selected_context)
GDB_PY_DEFINE_EVENT(corefile_changed)

View file

@ -23,6 +23,7 @@
#include "inferior.h"
#include "gdbcore.h"
#include "gdbsupport/rsp-low.h"
#include "py-event.h"
/* A gdb.Corefile object. */
@ -320,13 +321,47 @@ cfpy_mapped_files (PyObject *self, PyObject *args)
return obj->mapped_files;
}
/* Callback from gdb::observers::core_file_changed. The core file in
PSPACE has been changed. */
/* Emit a CorefileChangedEvent event, INF is the inferior in which the core
file changed. Return 0 on success, or a negative value on error. */
static int
emit_corefile_changed_event (inferior *inf)
{
/* If there are no listeners then we are done. */
if (evregpy_no_listeners_p (gdb_py_events.corefile_changed))
return 0;
gdbpy_ref<> event_obj
= create_event_object (&corefile_changed_event_object_type);
if (event_obj == nullptr)
return -1;
gdbpy_ref<inferior_object> inf_obj = inferior_to_inferior_object (inf);
if (inf_obj == nullptr
|| evpy_add_attribute (event_obj.get (), "inferior",
inf_obj.get ()) < 0)
return -1;
return evpy_emit_event (event_obj.get (), gdb_py_events.corefile_changed);
}
/* Callback from gdb::observers::core_file_changed. The core file for
INF has been changed. */
static void
cfpy_corefile_changed (inferior *inf)
{
/* It's safe to do this even if Python is not initialized, but there
should be nothing to clear in that case. */
cfpy_inferior_corefile_data_key.clear (inf);
if (!gdb_python_initialized)
return;
gdbpy_enter enter_py;
if (emit_corefile_changed_event (inf) < 0)
gdbpy_print_stack ();
}
/* Called when a gdb.Corefile is destroyed. */

View file

@ -150,3 +150,8 @@ GDB_PY_DEFINE_EVENT_TYPE (selected_context,
"SelectedContextEvent",
"GDB user selected context event object",
event_object_type);
GDB_PY_DEFINE_EVENT_TYPE (corefile_changed,
"CorefileChangedEvent",
"GDB corefile changed event",
event_object_type);

View file

@ -17,6 +17,7 @@
# support in Python.
require isnative
require {!is_remote host}
load_lib gdb-python.exp
@ -28,17 +29,108 @@ if {[build_executable "build executable" $testfile $srcfile] == -1} {
return
}
set remote_python_file \
[gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
set corefile [core_find $binfile]
if {$corefile == ""} {
unsupported "couldn't create or find corefile"
return
}
# Helper proc to run the 'core-file' command. Takes optional arguments:
#
# -corefile FILENAME : Load FILENAME as the new core file. If this
# argument is not given then the current core
# file will be unloaded.
#
# -inferior NUM : The inferior in which the corefile is being changed.
# This is used to match the corefile_changed events
# that will be emitted.
#
# -prefix STRING : A test prefix, to make test names unique.
#
# -replacement : There's already a core file loaded when this command
# is being run.
proc core_file_cmd { args } {
parse_some_args {
{corefile ""}
{inferior 1}
{prefix ""}
{replacement}
}
if { $prefix eq "" } {
if { $corefile eq "" } {
set prefix "unload corefile"
} else {
set prefix "load corefile"
}
}
with_test_prefix $prefix {
gdb_test "events corefile_changed check" \
"^No corefile_changed event has been seen\\." \
"no corefile event has been seen"
gdb_test "events exited check" \
"^No exited event has been seen\\." \
"no exited event has been seen"
if { $corefile eq "" } {
gdb_test "core-file" "^No core file now\\." "unload current core file"
gdb_test "events corefile_changed check" \
"Event 1/1, Inferior $inferior, Corefile None" \
"expected corefile event has been seen"
gdb_test "events exited check" \
"Event 1/1, Inferior $inferior, Exit Code None" \
"expected exited event has been seen"
} else {
gdb_test "core-file $corefile" ".*" "load core file"
if { $replacement } {
gdb_test "events corefile_changed check" \
[multi_line \
"Event 1/2, Inferior $inferior, Corefile None" \
"Event 2/2, Inferior $inferior, Corefile [string_to_regexp $corefile]"] \
"expected corefile event has been seen"
gdb_test "events exited check" \
"Event 1/1, Inferior $inferior, Exit Code None" \
"expected exited event has been seen"
} else {
gdb_test "events corefile_changed check" \
"Event 1/1, Inferior $inferior, Corefile [string_to_regexp $corefile]" \
"expected corefile event has been seen"
gdb_test "events exited check" \
"^No exited event has been seen\\." \
"no exited event was emitted"
}
}
}
gdb_test_no_output -nopass "events corefile_changed reset"
gdb_test_no_output -nopass "events exited reset"
}
# A helper proc runs clean_restart passing through ARGS, and then loads the
# test's Python script.
proc clean_restart_and_load_py_script { args } {
clean_restart {*}$args
# Load the Python script into GDB.
gdb_test "source $::remote_python_file" "^Success" \
"source python script"
}
# Create a copy of the corefile.
set other_corefile [standard_output_file ${testfile}-other.core]
remote_exec build "cp $corefile $other_corefile"
clean_restart
clean_restart_and_load_py_script
gdb_test_no_output "python inf = gdb.selected_inferior()" \
"capture current inferior"
@ -46,8 +138,7 @@ gdb_test_no_output "python inf = gdb.selected_inferior()" \
gdb_test "python print(inf.corefile)" "^None" \
"Inferior.corefile is None before loading a core file"
gdb_test "core-file $corefile" ".*" \
"load core file"
core_file_cmd -corefile $corefile
set file_re [string_to_regexp $corefile]
gdb_test "python print(inf.corefile)" "^<gdb\\.Corefile inferior=1 filename='$file_re'>" \
@ -73,7 +164,7 @@ gdb_test "python print(core1.filename)" "^$file_re" \
gdb_test "python print(core1.is_valid())" "^True" \
"Corefile.is_valid() is True while corefile is loaded"
gdb_test "core-file" "^No core file now\\." "unload current core file"
core_file_cmd
gdb_test "python print(core1.is_valid())" "^False" \
"Corefile.is_valid() is False after corefile is unloaded"
@ -101,8 +192,7 @@ gdb_test "add-inferior"
gdb_test "inferior 2"
with_test_prefix "in second inferior" {
gdb_test "core-file $corefile" ".*" \
"load core file"
core_file_cmd -corefile $corefile -inferior 2
gdb_test "python print(inf.corefile)" "^None" \
"first inferior still has no core file"
@ -128,8 +218,8 @@ gdb_test "python print(core2.filename)" "^$file_re" \
"Corefile.filename attribute works from different progspace"
# Load the other corefile into the first inferior.
gdb_test "core $other_corefile" ".*" \
"load other corefile into inferior 1"
core_file_cmd -corefile $other_corefile \
-prefix "load other corefile into inferior 1"
# Delete the second inferior. We need to switch to the second
# inferior and unload its corefile before we can do that. Then,
@ -152,7 +242,7 @@ with_test_prefix "remove second inferior" {
"AttributeError.*: 'gdb\\.Corefile' object has no attribute '_my_attribute'" \
"try to read attribute that doesn't exist"
gdb_test "core-file"
core_file_cmd -inferior 2
gdb_test "python print(core2.filename)" \
[multi_line \
@ -182,18 +272,10 @@ with_test_prefix "remove second inferior" {
# mapped_files API. The output from the built-in command, and the
# Python command should be identical.
with_test_prefix "test mapped files data" {
clean_restart
set remote_python_file \
[gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
# Load the Python script into GDB.
gdb_test "source $remote_python_file" "^Success" \
"source python script"
clean_restart_and_load_py_script
# Load the core file.
gdb_test "core-file $corefile" ".*" \
"load core file"
core_file_cmd -corefile $corefile
# Two files to write the output to.
set out_1 [standard_output_file ${gdb_test_file_name}-out-1.txt]
@ -279,3 +361,52 @@ with_test_prefix "test mapped files data" {
gdb_test "python regions\[0\] = None" \
"'tuple' object does not support item assignment"
}
# Load a core file. GDB should figure out which file is being debugged.
# Then use 'start' to run this executable, this will replace the core file
# target. At least on Linux, this replacement is done without calling
# target_detach. This test checks that the expected core file changed and
# inferior exited events are still seen.
with_test_prefix "start from corefile" {
if { [gdb_protocol_is_native] } {
clean_restart_and_load_py_script
# Load the core file.
core_file_cmd -corefile $corefile
# Check GDB figured out the executable.
gdb_test "info inferiors 1" \
[multi_line \
"\[^\r\n\]+[string_to_regexp $binfile]\\s*" \
"\[^\r\n\]+[string_to_regexp $corefile]\\s*"] \
"check executable was detected correctly"
gdb_test "start" \
"Temporary breakpoint $::decimal, main \\(\\).*" \
gdb_test "events corefile_changed check" \
"Event 1/1, Inferior 1, Corefile None" \
"expected corefile event has been seen"
gdb_test "events exited check" \
"Event 1/1, Inferior 1, Exit Code None" \
"expected exited event has been seen"
gdb_test_no_output -nopass "events corefile_changed reset"
gdb_test_no_output -nopass "events exited reset"
}
}
# Load a core file, then load a different core file to replace it.
# Check that the events that are emitted are as expected.
with_test_prefix "load one core file over another" {
clean_restart_and_load_py_script
# Load the core file.
core_file_cmd -corefile $corefile \
-prefix "load first corefile"
core_file_cmd -corefile $other_corefile \
-prefix "load second corefile" \
-replacement
}

View file

@ -196,4 +196,121 @@ class CheckMainExec(gdb.Command):
CheckMainExec()
# An 'events' prefix command.
class events_cmd(gdb.Command):
"""Information about recent Python events."""
def __init__(self):
gdb.Command.__init__(self, "events", gdb.COMMAND_USER, prefix=True)
# An 'events corefile_changed' sub-command.
class events_corefile_changed_cmd(gdb.Command):
"""Check recent corefile_changed events.
Requires a single argument either 'check' or 'reset'. With
'check', print details of every recent corefile_changed event.
With 'reset' clear the list of recent corefile_changed events."""
def __init__(self):
gdb.Command.__init__(self, "events corefile_changed", gdb.COMMAND_USER)
self._events = []
gdb.events.corefile_changed.connect(lambda e: self._corefile_changed_handler(e))
def _corefile_changed_handler(self, event):
assert isinstance(event, gdb.CorefileChangedEvent)
inf = event.inferior
assert isinstance(inf, gdb.Inferior)
corefile = inf.corefile
if corefile is not None:
assert corefile.is_valid()
corefile = corefile.filename
obj = {"inferior": inf.num, "corefile": corefile}
self._events.append(obj)
def invoke(self, args, from_tty):
if args == "check":
if len(self._events) == 0:
print("No corefile_changed event has been seen.")
else:
total = len(self._events)
for idx, obj in enumerate(self._events, start=1):
inf_num = obj["inferior"]
corefile = obj["corefile"]
if corefile is None:
msg = "None"
else:
msg = corefile
print(
"Event {}/{}, Inferior {}, Corefile {}".format(
idx, total, inf_num, msg
)
)
elif args == "reset":
self._events = []
else:
raise gdb.GdbError("Unknown command args: {}".format(args))
# An 'events exited' sub-command.
class events_exited_cmd(gdb.Command):
"""Check recent exited events.
Requires a single argument either 'check' or 'reset'. With
'check', print details of every recent exited event. With 'reset'
clear the list of recent exited events."""
def __init__(self):
gdb.Command.__init__(self, "events exited", gdb.COMMAND_USER)
self._events = []
gdb.events.exited.connect(lambda e: self._exited_handler(e))
def _exited_handler(self, event):
assert isinstance(event, gdb.ExitedEvent)
inf = event.inferior
assert isinstance(inf, gdb.Inferior)
if hasattr(event, "exit_code"):
assert isinstance(event.exit_code, int)
exit_code = event.exit_code
else:
exit_code = None
obj = {"inferior": inf.num, "exit_code": exit_code}
self._events.append(obj)
def invoke(self, args, from_tty):
if args == "check":
if len(self._events) == 0:
print("No exited event has been seen.")
else:
total = len(self._events)
for idx, obj in enumerate(self._events, start=1):
inf_num = obj["inferior"]
exit_code = obj["exit_code"]
if exit_code is None:
msg = "None"
else:
msg = exit_code
print(
"Event {}/{}, Inferior {}, Exit Code {}".format(
idx, total, inf_num, msg
)
)
elif args == "reset":
self._events = []
else:
raise gdb.GdbError("Unknown command args: {}".format(args))
events_cmd()
events_corefile_changed_cmd()
events_exited_cmd()
print("Success")