* [PATCH] gdb/python: new selected_context event
@ 2026-02-22 16:02 Andrew Burgess
2026-02-22 16:47 ` Eli Zaretskii
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Andrew Burgess @ 2026-02-22 16:02 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess
This commit introduces a new Python event, selected_context. This
event is attached to the user_selected_context_changed observer, which
triggers when the user changes the currently selected inferior,
thread, or frame.
Adding this event allows a Python extension to update in response to
user driven changes without having to poll the state from a
before_prompt hook, which is what I currently do to achieve the same
results.
I did consider splitting the user_selected_context_changed observer
into 3 separate Python events, inferior_changed, thread_changed, and
frame_changed, but I couldn't see any significant advantage to doing
this, so in the end I went with just a single event, and the event
object contains the inferior, thread, and frame.
Additionally, the user isn't informed about which aspect of the
context changed. That is, every event carries the inferior, thread,
and frame, so an event triggered when switching frames will looks
identical to an event triggered when switching inferiors. If the user
wants to know what changed then they will have to track the current
state themselves, and then compare the event state to the stored
current state. In many cases though I suspect that just being told
something changed, and then updating everything will be sufficient,
which is why I've not bothered trying to inform the user what changed.
---
gdb/NEWS | 7 +
gdb/doc/python.texi | 35 +++++
gdb/python/py-all-events.def | 1 +
gdb/python/py-event-types.def | 5 +
gdb/python/py-inferior.c | 54 ++++++++
.../gdb.python/py-selected-context.c | 56 ++++++++
.../gdb.python/py-selected-context.exp | 130 ++++++++++++++++++
.../gdb.python/py-selected-context.py | 57 ++++++++
8 files changed, 345 insertions(+)
create mode 100644 gdb/testsuite/gdb.python/py-selected-context.c
create mode 100644 gdb/testsuite/gdb.python/py-selected-context.exp
create mode 100644 gdb/testsuite/gdb.python/py-selected-context.py
diff --git a/gdb/NEWS b/gdb/NEWS
index 47d8189d344..b7337cbb8dd 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -190,6 +190,13 @@ qExecAndArgs
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
+ SelectedContext 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',
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 1f88ea7e9ad..54f128159a6 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -4059,6 +4059,41 @@ Events In Python
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.SelectedContext} and has the following
+attributes:
+
+@defvar SelectedContext.inferior
+The currently selected inferior. This is of type @code{gdb.Inferior}
+(@pxref{Inferiors In Python}).
+@end defvar
+
+@defvar SelectedContext.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 SelectedContext.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
diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def
index b88d11ad4f2..24724038562 100644
--- a/gdb/python/py-all-events.def
+++ b/gdb/python/py-all-events.def
@@ -46,3 +46,4 @@ GDB_PY_DEFINE_EVENT(executable_changed)
GDB_PY_DEFINE_EVENT(new_progspace)
GDB_PY_DEFINE_EVENT(free_progspace)
GDB_PY_DEFINE_EVENT(tui_enabled)
+GDB_PY_DEFINE_EVENT(selected_context)
diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def
index a7644d79b06..b5c3cbff31f 100644
--- a/gdb/python/py-event-types.def
+++ b/gdb/python/py-event-types.def
@@ -145,3 +145,8 @@ GDB_PY_DEFINE_EVENT_TYPE (tui_enabled,
"TuiEnabledEvent",
"GDB TUI enabled event object",
event_object_type);
+
+GDB_PY_DEFINE_EVENT_TYPE (selected_context,
+ "SelectedContext",
+ "GDB user selected context event object",
+ event_object_type);
diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c
index 53f3344429d..8f2badb69f7 100644
--- a/gdb/python/py-inferior.c
+++ b/gdb/python/py-inferior.c
@@ -999,6 +999,58 @@ gdbpy_selected_inferior (PyObject *self, PyObject *args)
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<> (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 ()
{
@@ -1029,6 +1081,8 @@ 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;
}
diff --git a/gdb/testsuite/gdb.python/py-selected-context.c b/gdb/testsuite/gdb.python/py-selected-context.c
new file mode 100644
index 00000000000..b7654829a24
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-selected-context.c
@@ -0,0 +1,56 @@
+/* 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. */
+}
diff --git a/gdb/testsuite/gdb.python/py-selected-context.exp b/gdb/testsuite/gdb.python/py-selected-context.exp
new file mode 100644
index 00000000000..c287cc90d44
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-selected-context.exp
@@ -0,0 +1,130 @@
+# 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"
diff --git a/gdb/testsuite/gdb.python/py-selected-context.py b/gdb/testsuite/gdb.python/py-selected-context.py
new file mode 100644
index 00000000000..8653decb855
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-selected-context.py
@@ -0,0 +1,57 @@
+# 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.SelectedContext)
+
+ 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")
base-commit: 120072c7cdc14f2e6e2ca1fe4a8eb5bec9e6e36b
--
2.25.4
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] gdb/python: new selected_context event
2026-02-22 16:02 [PATCH] gdb/python: new selected_context event Andrew Burgess
@ 2026-02-22 16:47 ` Eli Zaretskii
2026-02-27 18:56 ` Tom Tromey
2026-03-04 20:28 ` [PATCHv2] " Andrew Burgess
2 siblings, 0 replies; 6+ messages in thread
From: Eli Zaretskii @ 2026-02-22 16:47 UTC (permalink / raw)
To: Andrew Burgess; +Cc: gdb-patches
> From: Andrew Burgess <aburgess@redhat.com>
> Cc: Andrew Burgess <aburgess@redhat.com>
> Date: Sun, 22 Feb 2026 16:02:58 +0000
>
> This commit introduces a new Python event, selected_context. This
> event is attached to the user_selected_context_changed observer, which
> triggers when the user changes the currently selected inferior,
> thread, or frame.
>
> Adding this event allows a Python extension to update in response to
> user driven changes without having to poll the state from a
> before_prompt hook, which is what I currently do to achieve the same
> results.
>
> I did consider splitting the user_selected_context_changed observer
> into 3 separate Python events, inferior_changed, thread_changed, and
> frame_changed, but I couldn't see any significant advantage to doing
> this, so in the end I went with just a single event, and the event
> object contains the inferior, thread, and frame.
>
> Additionally, the user isn't informed about which aspect of the
> context changed. That is, every event carries the inferior, thread,
> and frame, so an event triggered when switching frames will looks
> identical to an event triggered when switching inferiors. If the user
> wants to know what changed then they will have to track the current
> state themselves, and then compare the event state to the stored
> current state. In many cases though I suspect that just being told
> something changed, and then updating everything will be sufficient,
> which is why I've not bothered trying to inform the user what changed.
> ---
> gdb/NEWS | 7 +
> gdb/doc/python.texi | 35 +++++
> gdb/python/py-all-events.def | 1 +
> gdb/python/py-event-types.def | 5 +
> gdb/python/py-inferior.c | 54 ++++++++
> .../gdb.python/py-selected-context.c | 56 ++++++++
> .../gdb.python/py-selected-context.exp | 130 ++++++++++++++++++
> .../gdb.python/py-selected-context.py | 57 ++++++++
> 8 files changed, 345 insertions(+)
> create mode 100644 gdb/testsuite/gdb.python/py-selected-context.c
> create mode 100644 gdb/testsuite/gdb.python/py-selected-context.exp
> create mode 100644 gdb/testsuite/gdb.python/py-selected-context.py
The documentation parts are okay, thanks.
Reviewed-By: Eli Zaretskii <eliz@gnu.org>
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH] gdb/python: new selected_context event
2026-02-22 16:02 [PATCH] gdb/python: new selected_context event Andrew Burgess
2026-02-22 16:47 ` Eli Zaretskii
@ 2026-02-27 18:56 ` Tom Tromey
2026-03-04 20:28 ` [PATCHv2] " Andrew Burgess
2 siblings, 0 replies; 6+ messages in thread
From: Tom Tromey @ 2026-02-27 18:56 UTC (permalink / raw)
To: Andrew Burgess; +Cc: gdb-patches
>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
Andrew> This commit introduces a new Python event, selected_context. This
Andrew> event is attached to the user_selected_context_changed observer, which
Andrew> triggers when the user changes the currently selected inferior,
Andrew> thread, or frame.
Thanks. I think this approach seems fine.
FWIW this is https://sourceware.org/bugzilla/show_bug.cgi?id=24482
Andrew> Adding this event allows a Python extension to update in response to
Andrew> user driven changes without having to poll the state from a
Andrew> before_prompt hook, which is what I currently do to achieve the same
Andrew> results.
Yeah, that approach is ok-ish but also problematic.
I think we have similar issues in the TUI, see
https://sourceware.org/bugzilla/show_bug.cgi?id=32865
Andrew> I did consider splitting the user_selected_context_changed observer
Andrew> into 3 separate Python events, inferior_changed, thread_changed, and
Andrew> frame_changed, but I couldn't see any significant advantage to doing
Andrew> this, so in the end I went with just a single event, and the event
Andrew> object contains the inferior, thread, and frame.
This seems totally fine to me.
Andrew> Additionally, the user isn't informed about which aspect of the
Andrew> context changed.
This too.
Andrew> +The event is of type @code{gdb.SelectedContext} and has the following
Andrew> +attributes:
There's been a convention that an event type name ends in "Event".
Andrew> + frame_obj = gdbpy_ref<> (Py_None);
This has to be gdbpy_ref<>::new_reference (Py_None);
Unfortunately even the safety series I am working on won't prevent this
kind of problem :-(
I guess we could do it by forbidding the use of plain "Py_None" somehow,
and then having a gdb variant that is clearly a borrowed reference.
Tom
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCHv2] gdb/python: new selected_context event
2026-02-22 16:02 [PATCH] gdb/python: new selected_context event Andrew Burgess
2026-02-22 16:47 ` Eli Zaretskii
2026-02-27 18:56 ` Tom Tromey
@ 2026-03-04 20:28 ` Andrew Burgess
2026-03-04 20:42 ` Tom Tromey
2 siblings, 1 reply; 6+ messages in thread
From: Andrew Burgess @ 2026-03-04 20:28 UTC (permalink / raw)
To: gdb-patches; +Cc: Andrew Burgess, Eli Zaretskii
In v2:
- Renamed event to SelectedContextEvent, adding the 'Event' suffix.
- Fixed the reference counting bug for Py_None, now use
gdbpy_ref<>::new_reference (Py_None).
- Rebased to upstream HEAD.
---
This commit introduces a new Python event, selected_context. This
event is attached to the user_selected_context_changed observer, which
triggers when the user changes the currently selected inferior,
thread, or frame.
Adding this event allows a Python extension to update in response to
user driven changes without having to poll the state from a
before_prompt hook, which is what I currently do to achieve the same
results.
I did consider splitting the user_selected_context_changed observer
into 3 separate Python events, inferior_changed, thread_changed, and
frame_changed, but I couldn't see any significant advantage to doing
this, so in the end I went with just a single event, and the event
object contains the inferior, thread, and frame.
Additionally, the user isn't informed about which aspect of the
context changed. That is, every event carries the inferior, thread,
and frame, so an event triggered when switching frames will looks
identical to an event triggered when switching inferiors. If the user
wants to know what changed then they will have to track the current
state themselves, and then compare the event state to the stored
current state. In many cases though I suspect that just being told
something changed, and then updating everything will be sufficient,
which is why I've not bothered trying to inform the user what changed.
Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=24482
Reviewed-By: Eli Zaretskii <eliz@gnu.org>
---
gdb/NEWS | 7 +
gdb/doc/python.texi | 35 +++++
gdb/python/py-all-events.def | 1 +
gdb/python/py-event-types.def | 5 +
gdb/python/py-inferior.c | 54 ++++++++
.../gdb.python/py-selected-context.c | 56 ++++++++
.../gdb.python/py-selected-context.exp | 130 ++++++++++++++++++
.../gdb.python/py-selected-context.py | 57 ++++++++
8 files changed, 345 insertions(+)
create mode 100644 gdb/testsuite/gdb.python/py-selected-context.c
create mode 100644 gdb/testsuite/gdb.python/py-selected-context.exp
create mode 100644 gdb/testsuite/gdb.python/py-selected-context.py
diff --git a/gdb/NEWS b/gdb/NEWS
index d70147144e4..72471dbc751 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -200,6 +200,13 @@ qExecAndArgs
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',
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 1f88ea7e9ad..6f1f35101f9 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -4059,6 +4059,41 @@ Events In Python
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
diff --git a/gdb/python/py-all-events.def b/gdb/python/py-all-events.def
index b88d11ad4f2..24724038562 100644
--- a/gdb/python/py-all-events.def
+++ b/gdb/python/py-all-events.def
@@ -46,3 +46,4 @@ GDB_PY_DEFINE_EVENT(executable_changed)
GDB_PY_DEFINE_EVENT(new_progspace)
GDB_PY_DEFINE_EVENT(free_progspace)
GDB_PY_DEFINE_EVENT(tui_enabled)
+GDB_PY_DEFINE_EVENT(selected_context)
diff --git a/gdb/python/py-event-types.def b/gdb/python/py-event-types.def
index a7644d79b06..fe3e0978a55 100644
--- a/gdb/python/py-event-types.def
+++ b/gdb/python/py-event-types.def
@@ -145,3 +145,8 @@ GDB_PY_DEFINE_EVENT_TYPE (tui_enabled,
"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);
diff --git a/gdb/python/py-inferior.c b/gdb/python/py-inferior.c
index ae309620e1f..76e3da9f620 100644
--- a/gdb/python/py-inferior.c
+++ b/gdb/python/py-inferior.c
@@ -997,6 +997,58 @@ gdbpy_selected_inferior (PyObject *self, PyObject *args)
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 ()
{
@@ -1027,6 +1079,8 @@ 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;
}
diff --git a/gdb/testsuite/gdb.python/py-selected-context.c b/gdb/testsuite/gdb.python/py-selected-context.c
new file mode 100644
index 00000000000..b7654829a24
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-selected-context.c
@@ -0,0 +1,56 @@
+/* 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. */
+}
diff --git a/gdb/testsuite/gdb.python/py-selected-context.exp b/gdb/testsuite/gdb.python/py-selected-context.exp
new file mode 100644
index 00000000000..c287cc90d44
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-selected-context.exp
@@ -0,0 +1,130 @@
+# 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"
diff --git a/gdb/testsuite/gdb.python/py-selected-context.py b/gdb/testsuite/gdb.python/py-selected-context.py
new file mode 100644
index 00000000000..e624be71994
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-selected-context.py
@@ -0,0 +1,57 @@
+# 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")
base-commit: c2af35131a7c9fd9f6b3192418baf366b125efa4
--
2.25.4
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCHv2] gdb/python: new selected_context event
2026-03-04 20:28 ` [PATCHv2] " Andrew Burgess
@ 2026-03-04 20:42 ` Tom Tromey
2026-03-05 9:49 ` Andrew Burgess
0 siblings, 1 reply; 6+ messages in thread
From: Tom Tromey @ 2026-03-04 20:42 UTC (permalink / raw)
To: Andrew Burgess; +Cc: gdb-patches, Eli Zaretskii
>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
Andrew> In v2:
Andrew> - Renamed event to SelectedContextEvent, adding the 'Event' suffix.
Andrew> - Fixed the reference counting bug for Py_None, now use
Andrew> gdbpy_ref<>::new_reference (Py_None).
Andrew> - Rebased to upstream HEAD.
Looks good to me, thank you.
Approved-By: Tom Tromey <tom@tromey.com>
Tom
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCHv2] gdb/python: new selected_context event
2026-03-04 20:42 ` Tom Tromey
@ 2026-03-05 9:49 ` Andrew Burgess
0 siblings, 0 replies; 6+ messages in thread
From: Andrew Burgess @ 2026-03-05 9:49 UTC (permalink / raw)
To: Tom Tromey; +Cc: gdb-patches, Eli Zaretskii
Tom Tromey <tom@tromey.com> writes:
>>>>>> "Andrew" == Andrew Burgess <aburgess@redhat.com> writes:
>
> Andrew> In v2:
> Andrew> - Renamed event to SelectedContextEvent, adding the 'Event' suffix.
>
> Andrew> - Fixed the reference counting bug for Py_None, now use
> Andrew> gdbpy_ref<>::new_reference (Py_None).
>
> Andrew> - Rebased to upstream HEAD.
>
> Looks good to me, thank you.
> Approved-By: Tom Tromey <tom@tromey.com>
Pushed.
Thanks,
Andrew
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-03-05 9:50 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-02-22 16:02 [PATCH] gdb/python: new selected_context event Andrew Burgess
2026-02-22 16:47 ` Eli Zaretskii
2026-02-27 18:56 ` Tom Tromey
2026-03-04 20:28 ` [PATCHv2] " Andrew Burgess
2026-03-04 20:42 ` Tom Tromey
2026-03-05 9:49 ` Andrew Burgess
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox