[6/6] gdb/tui: fix debuginfod prompt using 'C-x C-a' to enter TUI
Commit Message
This commit ties closely into the previous commit. The previous
commit looks at issues that can arise when using 'tui enable' to enter
TUI mode if a debuginfod prompt is triggered. This commit looks at
the problems that can arise when a multi-key combination is used to
enter TUI mode, e.g. 'C-x C-a'. Bug PR gdb/33794 discusses this
issue.
There has been a previous attempt to address this issue here:
https://inbox.sourceware.org/gdb-patches/20260417075719.852558-5-tdevries@suse.de
The approach taken in that patch was to prevent switching to TUI mode
if debuginfod is still in ASK mode, this means the switch could
potentially trigger a secondary prompt.
While the previous commit is relatively simple, the complexity in this
case arises from how multi-key combinations are handled by readline.
Currently global readline state is used to track the multi-key press
situation, and when the multi-key is dispatched back to application
(GDB) code, the globals are still live.
If GDB then triggers reentry into readline, e.g. by triggering a
secondary prompt, the call into readline for this prompt will cause
the global state to be released. When the secondary prompt is
finished and we return back to readline the global state will be
accessed, and undefined behaviour occurs, including crashes.
The core idea of my proposed solution to this is to move handling of
the multi-key actions out of the readline callback, and into GDB's
normal event loop.
When the user presses a combination like 'C-x C-a' this will call a
templated tui_rl_keybinding function as it currently does, but instead
of immediately forwarding to another function to carry out the TUI
changes, we instead schedule a callback with the event loop and then
return.
As far as readline is concerned the multi-key action has now been
dealt with, however, no interface changes have yet occurred. As
readline has now finished handling this key press, readline returns to
the event loop to get the next user input.
At the event loop the pending callback is seen and dispatched. This
callback triggers the actual UI changes, e.g. entering TUI mode. As
we are not inside readline at this point we are free to create
secondary prompts if needed.
In tui_rl_keybinding we use run_on_main_thread to schedule a callback
with the event loop, but there are some additional changes needed:
1. If we changed the tui_active state then we need to call
reinitialize_more_filter. Previously tui_rl_switch_mode would call
rl_newline which would make readline think that a command had been
fully entered, this would trigger a call to GDB's
command_line_handler, which calls command_handler, which then calls
reinitialize_more_filter.
For reasons explained below tui_rl_switch_mode can no longer call
rl_newline, so the reinitialize_more_filter is never reached. This
means that especially when switching CLI to TUI, when the `cmd`
window is smaller than the CLI terminal, GDB might enter TUI mode
thinking that the TUI is already full. This leads to incorrect
pager prompts appearing. Resolve this by explicitly resetting the
pager.
2. After changing the tui_active state (i.e. entering or leaving TUI
mode), there will not be a GDB prompt displayed. Under the old
scheme, the rl_newline call in tui_rl_switch_mode would trick GDB
into thinking an empty command had just been completed, this would
then trigger a prompt redisplay.
Under the new scheme we need to explicitly call display_gdb_prompt
or tui_redisplay_readline to redraw the prompt. However, as we
were at a GDB prompt already when the user pressed a key like 'C-x
C-a', the current_ui will not think that a prompt is needed, if we
plan to call display_gdb_prompt then we'll need to change the
prompt_state to PROMPT_NEEDED before calling display_gdb_prompt.
When possible we prefer calling tui_redisplay_readline, as this
preserves the current readline input line buffer contents, so if
the user types something at the prompt and then does 'C-x o' to
change window focus, the partially typed text is preserved.
Both of these additional actions need to be performed for both the
normal exit path, and the exception path in order that the prompt be
correctly displayed, so this code is done in a SCOPE_EXIT block.
The other set of changes are in tui_rl_switch_mode:
1. The calls to rl_prep_terminal are no longer needed as
display_gdb_prompt will take care of calling this for us if
appropriate (e.g. we are not in TUI mode).
2. The gdb_exception_forced_quit handling can now just propagate the
exception, We are no longer within a readline callback, and so can
throw this exception further up the stack.
3. Likewise with gdb_exception, we can re-throw this. As the
run_on_main_thread mechanism silently swallows all gdb_exceptions
except the gdb_exception_forced_quit sub-class, we do need to print
the exception ourselves first though. This is why we had to
separate out the gdb_exception_forced_quit handling.
4. The rl_kill_text call is no longer needed as the following
rl_newline call is going to be removed.
5. The rl_newline call was a neat trick to force a prompt redisplay,
but this only works when we are within a readline callback, it
injects a newline so that when we return from this callback
readline will see the pending newline character, process the now
empty line (thanks to the rl_kill_text call), and the print the
prompt. This is replaced by the display_gdb_prompt call that was
added to tui_rl_keybinding.
6. The dont_repeat call was needed because the rl_kill_text and
rl_newline calls were tricking readline into thinking the user had
pressed Enter on an empty line, this was done to force a prompt
redisplay.
However, pressing Enter on an empty line repeats the previous
command unless dont_repeat has been called. Now we don't use the
rl_newline trick, the dont_repeat call is not needed.
The gdb.tui/debuginfod-query.exp test is updated to include tests that
switch using multi-key combinations.
Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=33794
---
gdb/testsuite/gdb.tui/debuginfod-query.exp | 5 +-
.../gdb.tui/tui-enable-failure-lib.c | 31 ++++
gdb/testsuite/gdb.tui/tui-enable-failure.c | 22 +++
gdb/testsuite/gdb.tui/tui-enable-failure.exp | 136 ++++++++++++++++++
gdb/tui/tui.c | 120 +++++++++++-----
5 files changed, 278 insertions(+), 36 deletions(-)
create mode 100644 gdb/testsuite/gdb.tui/tui-enable-failure-lib.c
create mode 100644 gdb/testsuite/gdb.tui/tui-enable-failure.c
create mode 100644 gdb/testsuite/gdb.tui/tui-enable-failure.exp
@@ -213,10 +213,7 @@ file copy -force $binfile $debugdir/
# Create CACHE and DB directories ready for debuginfod to use.
prepare_for_debuginfod cache db
-# Can add 'keys' to MODES list, but this doesn't currently
-# work due to PR gdb/33794. This will be fixed in the next
-# commit, and this comment removed.
-set modes {command}
+set modes {command keys}
with_debuginfod_env $cache {
save_vars { env(DEBUGINFOD_URLS) } {
new file mode 100644
@@ -0,0 +1,31 @@
+/* Copyright 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ 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/>. */
+
+/* Preload library that overrides ncurses newterm to always return NULL,
+ causing tui_enable to fail. */
+
+#include <stddef.h>
+
+/* Define this type so we can override newterm. We only plan to
+ return NULL, so the details of this type are not important. */
+typedef struct screen_dummy SCREEN;
+
+SCREEN *
+newterm (const char *type, void *outfd, void *infd)
+{
+ return NULL;
+}
new file mode 100644
@@ -0,0 +1,22 @@
+/* Copyright 2026 Free Software Foundation, Inc.
+
+ This file is part of GDB.
+
+ 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/>. */
+
+int
+main (void)
+{
+ return 0;
+}
new file mode 100644
@@ -0,0 +1,136 @@
+# 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/>.
+
+# Test that GDB handles tui_enable failure gracefully.
+#
+# Use an LD_PRELOAD library to override ncurses' newterm function,
+# making it always return NULL. This causes tui_enable to fail with
+# an error.
+#
+# Test both the "tui enable" command and the multi-key combination
+# (C-x C-a) paths. The multi-key path is particularly important as it
+# exercises the tui_rl_keybinding SCOPE_EXIT error handling.
+
+require allow_tui_tests
+require {!is_remote host}
+require {istarget *-linux*}
+
+tuiterm_env
+
+standard_testfile .c -lib.c
+
+set libfile ${testfile}-lib
+set libobj [standard_output_file ${libfile}.so]
+
+# Compile the preload library.
+if { [build_executable "build preload lib" $libobj $srcfile2 \
+ {debug shlib}] == -1 } {
+ return
+}
+
+# Compile the test executable.
+if { [build_executable "build executable" $testfile $srcfile] == -1 } {
+ return
+}
+
+# Send 'print NUM' to GDB and check the output appear on LINE.
+proc check_responsiveness { num line } {
+ gdb_assert { [Term::command "print $num"] } \
+ "send print command"
+
+ gdb_assert { [Term::check_region_contents_p 0 $line 80 1 " = $num"] } \
+ "check print output is in expected location"
+}
+
+# Try to enable TUI mode using HOW, which is either 'command' or
+# 'keys'. The preload library ensures that newterm returns NULL, so
+# tui_enable will fail. Check that an error is produced and that GDB
+# remains responsive.
+#
+# The first failure sets tui_finish_init to TRIBOOL_UNKNOWN, so a
+# second attempt produces a different (generic) error message. Try
+# the second attempt too, and check that GDB is still responsive.
+#
+# The 'keys' path (C-x C-a) is particularly important as it exercises
+# the tui_rl_keybinding SCOPE_EXIT error handling.
+proc_with_prefix run_test { how } {
+ Term::clean_restart 24 80 $::testfile
+
+ if {![Term::prepare_for_tui]} {
+ return
+ }
+
+ Term::gen_prompt
+
+ # First attempt to enable TUI.
+ if { $how eq "command" } {
+ send_gdb "tui enable\n"
+ set line_offset 1
+ } elseif { $how eq "keys" } {
+ send_gdb "\030\001"
+ set line_offset 0
+ } else {
+ perror "unknown test mode: $how"
+ }
+
+ gdb_assert { [Term::wait_for "Cannot enable the TUI: error opening terminal"] } \
+ "error from first tui enable attempt"
+
+ # Check that GDB is still responsive.
+ with_test_prefix "responsive after first failure" {
+ check_responsiveness 1 [expr {2 + $line_offset}]
+ }
+
+ # Second attempt using 'tui enable' command should also fail.
+ send_gdb "tui enable\n"
+ gdb_assert { [Term::wait_for "Cannot enable the TUI"] } \
+ "error from second tui enable attempt"
+
+ # Check that GDB is still responsive.
+ with_test_prefix "responsive after second failure" {
+ check_responsiveness 2 [expr {6 + $line_offset}]
+ }
+
+ # Third attempt using multi-key combo should also fail.
+ send_gdb "\030\001"
+ gdb_assert { [Term::wait_for "Cannot enable the TUI"] } \
+ "error from third tui enable attempt"
+
+ # Check that GDB is still responsive.
+ with_test_prefix "responsive after third failure" {
+ check_responsiveness 3 [expr {9 + $line_offset}]
+ }
+
+ verbose -log "\n\n\nAPB: Dump:\n\n"
+ Term::dump_screen
+}
+
+set modes {command keys}
+
+save_vars { env(LD_PRELOAD) env(ASAN_OPTIONS) } {
+ if { ![info exists env(LD_PRELOAD)]
+ || $env(LD_PRELOAD) == "" } {
+ set env(LD_PRELOAD) "$libobj"
+ } else {
+ append env(LD_PRELOAD) ":$libobj"
+ }
+
+ # Prevent address sanitizer error about library ordering.
+ append_environment_default ASAN_OPTIONS verify_asan_link_order 0
+
+ foreach_with_prefix how $modes {
+ run_test $how
+ }
+}
@@ -42,6 +42,7 @@
#include "top.h"
#include "ui.h"
#include "observable.h"
+#include "run-on-main-thread.h"
#include <fcntl.h>
@@ -119,15 +120,20 @@ tui_rl_switch_mode (int notused1 = 0, int notused2 = 0)
{
gdb_assert (!gdb_in_secondary_prompt_p (current_ui));
- /* Don't let exceptions escape. We're in the middle of a readline
- callback that isn't prepared for that. */
+ /* This function is called through the run_on_main_thread event loop
+ callback mechanism. That mechanism propagates
+ gdb_exception_forced_quit exceptions, but silently discards
+ gdb_exception exceptions. We catch and print gdb_exception
+ exceptions before propagating them, the run_on_main_thread
+ mechanism will then discard these and return to the event loop.
+ Any RAII cleanup between here and there will have been done,
+ which is important. For gdb_exception_forced_quit exceptions we
+ just propagate these up the stack without printing, these will be
+ handled when they are caught by the event loop. */
try
{
if (tui_active)
- {
- tui_disable ();
- rl_prep_terminal (0);
- }
+ tui_disable ();
else
{
/* If we type "foo", entering it into the readline buffer
@@ -143,43 +149,24 @@ tui_rl_switch_mode (int notused1 = 0, int notused2 = 0)
TUI. */
rl_clear_visible_line ();
- /* If tui_enable throws, we'll re-prep below. */
+ /* Disable readline state ahead of enabling TUI mode. If
+ tui_enable fails then the next display_gdb_prompt will
+ re-prep the terminal for us. */
rl_deprep_terminal ();
+
tui_enable ();
}
}
catch (const gdb_exception_forced_quit &ex)
{
- /* Ideally, we'd do a 'throw' here, but as noted above, we can't
- do that, so, instead, we'll set the necessary flags so that
- a later QUIT check will restart the forced quit. */
- set_force_quit_flag ();
+ throw;
}
catch (const gdb_exception &ex)
{
exception_print (gdb_stderr, ex);
-
- if (!tui_active)
- rl_prep_terminal (0);
+ throw;
}
- /* Clear the readline in case switching occurred in middle of
- something. */
- if (rl_end)
- rl_kill_text (0, rl_end);
-
- /* Since we left the curses mode, the terminal mode is restored to
- some previous state. That state may not be suitable for readline
- to work correctly (it may be restored in line mode). We force an
- exit of the current readline so that readline is re-entered and
- it will be able to setup the terminal for its needs. By
- re-entering in readline, we also redisplay its prompt in the
- non-curses mode. */
- rl_newline (1, '\n');
-
- /* Make sure the \n we are returning does not repeat the last
- command. */
- dont_repeat ();
return 0;
}
@@ -334,7 +321,76 @@ tui_rl_keybinding (int count, int key)
if (gdb_in_secondary_prompt_p (current_ui))
return 0;
- return FPTR (count, key);
+ run_on_main_thread ([=] () {
+ bool was_active = tui_active;
+
+ /* Cleanup required even on the exception path. */
+ SCOPE_EXIT {
+ /* If we switched from CLI to TUI (or back) then we should
+ reinitialize the pager in order to avoid spurious pagination
+ prompts. This is especially important going from CLI to TUI
+ where the command window is usually smaller than the full
+ terminal, so the pager might already think that we have more
+ lines printed than will fit in the window, despite the window
+ starting empty after a mode switch. */
+ if (was_active != tui_active)
+ reinitialize_more_filter ();
+
+ /* The user was at a prompt and pressed a multi-key combination
+ (e.g. C-x C-a). As a result this callback was invoked from
+ the event loop. We're now exiting this callback and want to
+ ensure that the prompt is drawn correctly.
+
+ We have two approaches, full display_gdb_prompt, or a light
+ weight tui_redisplay_readline. The former will clear the
+ readline input buffer and redisplay the prompt, while the
+ second will redraw the prompt along with anything in the
+ input buffer. We want the light weight option where
+ possible, but there are times when this isn't an option:
+
+ 1. A successful switch between CLI and TUI, in either
+ direction, changes how we update the prompt. We need to
+ call display_gdb_prompt the first time to ensure
+ everything is done correctly. We also need to consider
+ the case where tui_enable fails, leaving us in CLI mode,
+ this also requires a call to display_gdb_prompt as the
+ light weight tui_redisplay_readline is not appropriate for
+ CLI use.
+
+ 2. Usually the TUI will have the RL_STATE_CALLBACK state flag
+ set because of when this event callback is called. If a
+ secondary prompt has been displayed, then once the
+ secondary prompt completed, the RL_STATE_CALLBACK flag
+ will have been cleared. This is good for us, because
+ after a secondary prompt rl_prompt will still hold the
+ secondary prompt string, so we need display_gdb_prompt to
+ set the correct top-level prompt. */
+ if (!was_active || !tui_active || !RL_ISSTATE (RL_STATE_CALLBACK))
+ {
+ current_ui->prompt_state = PROMPT_NEEDED;
+ display_gdb_prompt (nullptr);
+ }
+ else
+ {
+ /* If we switched TUI to CLI then we took the above block.
+ If we're in CLI mode then these callbacks will switch us
+ back to TUI mode. Either way, if we get here then the
+ TUI must be active. */
+ gdb_assert (tui_active);
+
+ /* The only time rl_prompt will be NULL is when we switch
+ from CLI to TUI, but that will be handled by the block
+ above. */
+ gdb_assert (rl_prompt != nullptr);
+
+ tui_redisplay_readline ();
+ }
+ };
+
+ (void) FPTR (count, key);
+ });
+
+ return 0;
}
/* Initialize readline and configure the keymap for the switching