contains the gdb.Corefile object if a core file is loaded into
the inferior, otherwise, this contains None.
+ ** New event registry gdb.events.selected_context that emits a
+ SelectedContextEvent event whenever the user changes the inferior
+ context. The context consists of which inferior, thread, and
+ frame are currently selected. The event object has 'inferior',
+ 'thread' and 'frame' attributes containing gdb.Inferior,
+ gdb.InferiorThread, and gdb.Frame objects respectively.
+
* Guile API
** Procedures 'memory-port-read-buffer-size',
The exiting thread.
@end defvar
+@item events.selected_context
+This is emitted when the user directly, or indirectly, causes the
+selected inferior context to change. The context consists of the
+currently selected inferior, thread, and frame. Examples commands
+that trigger this event are @kbd{inferior}, @kbd{thread}, and
+@kbd{frame}.
+
+The event is of type @code{gdb.SelectedContextEvent} and has the
+following attributes:
+
+@defvar SelectedContextEvent.inferior
+The currently selected inferior. This is of type @code{gdb.Inferior}
+(@pxref{Inferiors In Python}).
+@end defvar
+
+@defvar SelectedContextEvent.thread
+The currently selected thread. If not @code{None} then this is of
+type @code{gdb.InferiorThread} (@pxref{Threads In Python}). If
+switching to an inferior that is not yet started, then the
+@code{thread} attribute will be @code{None}.
+@end defvar
+
+@defvar SelectedContextEvent.frame
+The currently selected frame. If not @code{None} then this is of type
+@code{gdb.Frame} (@pxref{Frames In Python}). If switching to an
+inferior that is not yet started, then the @code{frame} attribute will
+be @code{None}.
+@end defvar
+
+In some cases @value{GDBN} might emit the @code{selected_context}
+event even when the context has not changed. The state within the
+event will always reflect the state of the current inferior. These
+unnecessary events could be removed in future releases of
+@value{GDBN}.
+
@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
GDB_PY_DEFINE_EVENT(new_progspace)
GDB_PY_DEFINE_EVENT(free_progspace)
GDB_PY_DEFINE_EVENT(tui_enabled)
+GDB_PY_DEFINE_EVENT(selected_context)
"TuiEnabledEvent",
"GDB TUI enabled event object",
event_object_type);
+
+GDB_PY_DEFINE_EVENT_TYPE (selected_context,
+ "SelectedContextEvent",
+ "GDB user selected context event object",
+ event_object_type);
inferior_to_inferior_object (current_inferior ()).release ());
}
+/* Implement the selected_context event handler. This is called when some
+ aspect of the inferior's context (inferior, thread, or frame) is
+ changed by the user. If there are event listeners in place then create
+ an event object and notify the listeners. */
+
+static void
+python_context_changed (user_selected_what selection)
+{
+ if (!gdb_python_initialized)
+ return;
+
+ gdbpy_enter enter_py (current_inferior ()->arch ());
+
+ if (evregpy_no_listeners_p (gdb_py_events.selected_context))
+ return;
+
+ gdbpy_ref<> inf_obj (gdbpy_selected_inferior (nullptr, nullptr));
+ if (inf_obj == nullptr)
+ {
+ gdbpy_print_stack ();
+ return;
+ }
+
+ gdbpy_ref<> thr_obj (gdbpy_selected_thread (nullptr, nullptr));
+ if (thr_obj == nullptr)
+ {
+ gdbpy_print_stack ();
+ return;
+ }
+
+ gdbpy_ref<> frame_obj;
+ if (has_stack_frames ())
+ frame_obj = gdbpy_ref<> (gdbpy_selected_frame (nullptr, nullptr));
+ else
+ frame_obj = gdbpy_ref<>::new_reference (Py_None);
+
+ if (frame_obj == nullptr)
+ {
+ gdbpy_print_stack ();
+ return;
+ }
+
+ gdbpy_ref<> event
+ = create_event_object (&selected_context_event_object_type);
+ if (event == nullptr
+ || evpy_add_attribute (event.get (), "inferior", inf_obj.get ()) < 0
+ || evpy_add_attribute (event.get (), "thread", thr_obj.get ()) < 0
+ || evpy_add_attribute (event.get (), "frame", frame_obj.get ()) < 0
+ || evpy_emit_event (event.get (), gdb_py_events.selected_context) < 0)
+ gdbpy_print_stack ();
+}
+
static int
gdbpy_initialize_inferior ()
{
gdb::observers::inferior_added.attach (python_new_inferior, "py-inferior");
gdb::observers::inferior_removed.attach (python_inferior_deleted,
"py-inferior");
+ gdb::observers::user_selected_context_changed.attach (python_context_changed,
+ "py-inferior");
return 0;
}
--- /dev/null
+/* This testcase is part of GDB, the GNU debugger.
+
+ Copyright 2026 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/>. */
+
+#include <pthread.h>
+
+volatile int global_var = 0;
+
+/* Thread inner function. */
+
+void
+thread_breakpt (void)
+{
+ global_var = global_var + 1; /* First breakpoint. */
+}
+
+/* The thread entry point. */
+
+void *
+worker_thread (void *unused)
+{
+ thread_breakpt ();
+ return NULL;
+}
+
+/* Create a thread, and wait for it to complete. */
+
+void
+run_thread (void)
+{
+ pthread_t thr;
+
+ pthread_create (&thr, NULL, worker_thread, NULL);
+
+ pthread_join (thr, NULL);
+}
+
+int
+main (void)
+{
+ run_thread ();
+ return 0; /* Second breakpoint. */
+}
--- /dev/null
+# Copyright (C) 2026 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/>.
+
+# Check the Python gdb.selected_context event handling.
+
+require allow_python_tests
+
+load_lib gdb-python.exp
+
+standard_testfile
+
+if { [build_executable "build exec" $testfile $srcfile {debug pthreads}] } {
+ return
+}
+
+clean_restart
+
+# Source the Python script.
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+gdb_test "source ${pyfile}" "^DONE" "load python file"
+gdb_test "test-selected-context-event" \
+ "^GDB selected-context event registered\\."
+
+# Return a regexp for when the selected context event triggers, and
+# runs without error.
+proc event_regexp { inferior {thread "None"} {frame "None"}} {
+ return [multi_line \
+ " Inferior: ${inferior}" \
+ " Thread: [string_to_regexp $thread]" \
+ " Frame: [string_to_regexp $frame]"]
+}
+
+# Use 'info inferiors' to check that INF is the currently selected
+# inferior. INF should be an inferior number, e.g. '1', '2', etc.
+proc check_inferior { inf testname } {
+ gdb_test "info inferiors" \
+ "\r\n\\*\\s+[string_to_regexp $inf]\\s+\[^\r\n\]*(?=\r\n)" \
+ $testname
+}
+
+# Use 'info threads' to check that THR is the currently selected
+# thread. THR should be the thread-id (e.g. '1.1', '2.1') as appears
+# in the 'info threads' output.
+proc check_thread { thr testname } {
+ gdb_test "info threads" \
+ "\r\n\\*\\s+[string_to_regexp $thr]\\s+\[^\r\n\]+(?=\r\n).*" \
+ $testname
+}
+
+# Create a second inferior.
+gdb_test "add-inferior" "Added inferior 2\[^\r\n\]*"
+
+# Switch between inferiors before either inferior is started. The
+# event will include a valid gdb.Inferior, but the thread and frame
+# will both be None.
+gdb_test "inferior 2" [event_regexp 2] \
+ "switch to inferior 2, inferior is not started"
+gdb_test "inferior 1" [event_regexp 1] \
+ "switch to inferior 1, inferior is not started"
+
+# Arrange for the event handler to raise an error. Switch inferior,
+# check the error is printed, then check that the inferior switch was
+# still successful.
+gdb_test_no_output "python event_throws_error = True"
+gdb_test "inferior 2" \
+ [multi_line \
+ "\\\[Switching to inferior 2\[^\r\n\]*\\\]" \
+ "\[^\r\n\]+: error from gdb_selected_context_handler"] \
+ "switch to inferior 2, event raises an error"
+check_inferior 2 "check inferior 2 was selected"
+
+# Switch back to inferior 1.
+gdb_test "inferior 1" ".*" \
+ "return to inferior 1"
+
+# Load the executable and start the inferior.
+gdb_load $binfile
+if {![runto_main]} {
+ return
+}
+
+# Setup breakpoints and continue until the first is reached.
+gdb_breakpoint [gdb_get_line_number "First breakpoint"]
+gdb_breakpoint [gdb_get_line_number "Second breakpoint"]
+gdb_continue_to_breakpoint "first bp"
+
+# Ensure the expected thread is currently selected.
+check_thread 1.2 "confirm expected thread selected"
+
+# Switch thread. The event handler is still configured to raise an
+# error, but the thread switch should still happen.
+gdb_test "thread 1" \
+ [multi_line \
+ "\\\[Switching to thread 1\\.1\[^\r\n\]*\\\]" \
+ "#0\\s+\[^\r\n\]+" \
+ "\[^\r\n\]+: error from gdb_selected_context_handler"] \
+ "switch thread, handler raises an error"
+check_thread 1.1 "thread switched despite handler error"
+
+# Switch frame, ensure event handler raises an error.
+gdb_test "up" \
+ "#1\\s+.*: error from gdb_selected_context_handler" \
+ "error from event handler when switching frames"
+
+# Disable handler errors.
+gdb_test_no_output "python event_throws_error = False"
+
+# Switch thread, ensure event handler triggers.
+gdb_test "thread 2" [event_regexp 1 1.2 #0] \
+ "switch to thread 2, event handler triggers"
+
+# Now switch frames, ensure the event handler triggers.
+gdb_test "up" [event_regexp 1 1.2 #1] \
+ "move up a frame, event handler triggers"
+gdb_test "down" [event_regexp 1 1.2 #0] \
+ "move down a frame, event handler triggers"
+gdb_test "frame 1" [event_regexp 1 1.2 #1] \
+ "select a frame, event handler triggers"
--- /dev/null
+# Copyright (C) 2026 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 gdb
+
+event_throws_error = False
+
+
+def gdb_selected_context_handler(event):
+ assert isinstance(event, gdb.SelectedContextEvent)
+
+ global event_throws_error
+
+ if event_throws_error:
+ raise gdb.GdbError("error from gdb_selected_context_handler")
+ else:
+ print("event type: selected-context")
+ assert isinstance(event.inferior, gdb.Inferior)
+ print(" Inferior: %d" % (event.inferior.num))
+ if event.thread is None:
+ thr = "None"
+ else:
+ assert isinstance(event.thread, gdb.InferiorThread)
+ thr = "%d.%d" % (event.thread.inferior.num, event.thread.num)
+ print(" Thread: %s" % (thr))
+ if event.frame is None:
+ frame = "None"
+ else:
+ assert isinstance(event.frame, gdb.Frame)
+ frame = "#%d" % (event.frame.level())
+ print(" Frame: %s" % (frame))
+
+
+class test_selected_context(gdb.Command):
+ """Test GDB's Selected Context Event."""
+
+ def __init__(self):
+ gdb.Command.__init__(self, "test-selected-context-event", gdb.COMMAND_USER)
+
+ def invoke(self, arg, from_tty):
+ gdb.events.selected_context.connect(gdb_selected_context_handler)
+ print("GDB selected-context event registered.")
+
+
+test_selected_context()
+
+print("DONE")