Don't stop when removable inferior exits

Message ID 20260103234441.1084415-1-tom@tromey.com
State New
Headers
Series Don't stop when removable inferior exits |

Commit Message

Tom Tromey Jan. 3, 2026, 11:44 p.m. UTC
  A while back I noticed that gdb would issue a stop if an inferior in a
multi-inferior process tree exited.  For instance, if you used "gdb
make" and then "run", if one of the compilations exited, this would
show up as a stop.

However, this seems strange.  Those intermediate processes are
interesting, but (IMO) only if they crash or hit a breakpoint --
exiting is normal.

This patch arranges to suppress these stops, but only for removable
inferiors, with the reasoning being that non-removable inferiors are
the ones that the user explicitly asked to 'run', and so returning to
the prompt when that completes seems sensible.

Regression tested on x86-64 Fedora 40.

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=19471
---
 gdb/inferior.c                                | 16 +++++++
 gdb/inferior.h                                |  3 ++
 gdb/infrun.c                                  | 16 ++++++-
 .../gdb.multi/no-stop-removable-exit.exp      | 45 +++++++++++++++++++
 4 files changed, 78 insertions(+), 2 deletions(-)
 create mode 100644 gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
  

Comments

Andrew Burgess Jan. 6, 2026, 2:01 p.m. UTC | #1
Tom Tromey <tom@tromey.com> writes:

> A while back I noticed that gdb would issue a stop if an inferior in a
> multi-inferior process tree exited.  For instance, if you used "gdb
> make" and then "run", if one of the compilations exited, this would
> show up as a stop.
>
> However, this seems strange.  Those intermediate processes are
> interesting, but (IMO) only if they crash or hit a breakpoint --
> exiting is normal.
>
> This patch arranges to suppress these stops, but only for removable
> inferiors, with the reasoning being that non-removable inferiors are
> the ones that the user explicitly asked to 'run', and so returning to
> the prompt when that completes seems sensible.

That all makes sense.

>
> Regression tested on x86-64 Fedora 40.
>
> Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=19471
> ---
>  gdb/inferior.c                                | 16 +++++++
>  gdb/inferior.h                                |  3 ++
>  gdb/infrun.c                                  | 16 ++++++-
>  .../gdb.multi/no-stop-removable-exit.exp      | 45 +++++++++++++++++++
>  4 files changed, 78 insertions(+), 2 deletions(-)
>  create mode 100644 gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
>
> diff --git a/gdb/inferior.c b/gdb/inferior.c
> index 8131f23b938..8f62ff84a54 100644
> --- a/gdb/inferior.c
> +++ b/gdb/inferior.c
> @@ -477,6 +477,22 @@ have_live_inferiors (void)
>    return number_of_live_inferiors (NULL) > 0;
>  }
>  
> +/* Return true if there is at least one executing thread.  */
> +
> +bool
> +any_executing_threads ()
> +{
> +  for (inferior *inf : all_non_exited_inferiors (nullptr))
> +    if (inf->has_execution ())
> +      for (thread_info &tp ATTRIBUTE_UNUSED : inf->non_exited_threads ())

Is the ATTRIBUTE_UNUSED needed here?  It seems like TP is used.

> +	{
> +	  if (tp.executing ())
> +	    return true;
> +	}
> +
> +  return false;
> +}
> +
>  /* Prune away any unused inferiors, and then prune away no longer used
>     program spaces.  */
>  
> diff --git a/gdb/inferior.h b/gdb/inferior.h
> index 5b499a207b4..9e9633a1905 100644
> --- a/gdb/inferior.h
> +++ b/gdb/inferior.h
> @@ -765,6 +765,9 @@ extern int number_of_live_inferiors (process_stratum_target *proc_target);
>     (not cores, not executables, real live processes).  */
>  extern int have_live_inferiors (void);
>  
> +/* Return true if there is at least one executing thread.  */
> +extern bool any_executing_threads ();
> +
>  /* Save/restore the current inferior.  */
>  
>  class scoped_restore_current_inferior
> diff --git a/gdb/infrun.c b/gdb/infrun.c
> index bd114e16b80..f4e51bcb34e 100644
> --- a/gdb/infrun.c
> +++ b/gdb/infrun.c
> @@ -6343,8 +6343,20 @@ handle_inferior_event (struct execution_control_state *ecs)
>  
>        gdb_flush (gdb_stdout);
>        target_mourn_inferior (inferior_ptid);
> -      stop_print_frame = false;
> -      stop_waiting (ecs);
> +      /* When a removable inferior exits, there's ordinarily no reason
> +	 to report a stop, because this is normally an inferior that
> +	 was started by some other process, and not explicitly managed
> +	 by the user.  However, it's possible for a removable inferior
> +	 to be the last live inferior, and in this case reporting a
> +	 stop is needed, as otherwise the user will see a message
> +	 about "unwaited-for children".  */
> +      if (current_inferior ()->removable && any_executing_threads ())

I removed the `any_executing_threads ()` and the new test still passes.
Would it be possible to extend the test to cover this case too?

> +	prepare_to_wait (ecs);
> +      else
> +	{
> +	  stop_print_frame = false;
> +	  stop_waiting (ecs);
> +	}
>        return;
>  
>      case TARGET_WAITKIND_FORKED:
> diff --git a/gdb/testsuite/gdb.multi/no-stop-removable-exit.exp b/gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
> new file mode 100644
> index 00000000000..111351cf299
> --- /dev/null
> +++ b/gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
> @@ -0,0 +1,45 @@
> +# 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 removable inferiors don't cause a stop when they exit.
> +
> +# This test program conveniently launches another inferior.
> +standard_testfile watchpoint-multi-exit.c
> +
> +if {[prepare_for_testing "failed to build" $testfile $srcfile {debug}]} {
> +    return
> +}
> +
> +gdb_test_no_output "set detach-on-fork off"
> +gdb_test_no_output "set schedule-multiple on"
> +
> +gdb_test_multiple "run" "do not stop when child exits" {

This is going to cause errors when using
--target_board=native-gdbserver.  I'll take a moment to advertise 'make
check-all-boards'; there's some text in testsuite/README on how to set
it up.

Thanks,
Andrew

> +    -re "Starting program: \[^\n\]*\r\n" {
> +	exp_continue
> +    }
> +    -re "\\\[New inferior 2\[^\n\]\\\]\r\n" {
> +	exp_continue
> +    }
> +    -re "\\\[Inferior 2 .*exited normally\\\]\r\n" {
> +	exp_continue
> +    }
> +    -re -wrap "\\\[Inferior 1 .*exited normally\\\]" {
> +	pass $gdb_test_name
> +    }
> +    -re -wrap "" {
> +	# Any other stop is a failure.
> +	fail $gdb_test_name
> +    }
> +}
> -- 
> 2.49.0
  
Aktemur, Tankut Baris Jan. 7, 2026, 12:52 p.m. UTC | #2
On Tuesday, January 6, 2026 3:01 PM, Andrew Burgess wrote:
> Tom Tromey <tom@tromey.com> writes:
> 
> > A while back I noticed that gdb would issue a stop if an inferior in a
> > multi-inferior process tree exited.  For instance, if you used "gdb
> > make" and then "run", if one of the compilations exited, this would
> > show up as a stop.
> >
> > However, this seems strange.  Those intermediate processes are
> > interesting, but (IMO) only if they crash or hit a breakpoint --
> > exiting is normal.
> >
> > This patch arranges to suppress these stops, but only for removable
> > inferiors, with the reasoning being that non-removable inferiors are
> > the ones that the user explicitly asked to 'run', and so returning to
> > the prompt when that completes seems sensible.
> 
> That all makes sense.
> 
> >
> > Regression tested on x86-64 Fedora 40.
> >
> > Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=19471
> > ---
> >  gdb/inferior.c                                | 16 +++++++
> >  gdb/inferior.h                                |  3 ++
> >  gdb/infrun.c                                  | 16 ++++++-
> >  .../gdb.multi/no-stop-removable-exit.exp      | 45
> +++++++++++++++++++
> >  4 files changed, 78 insertions(+), 2 deletions(-)
> >  create mode 100644 gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
> >
> > diff --git a/gdb/inferior.c b/gdb/inferior.c
> > index 8131f23b938..8f62ff84a54 100644
> > --- a/gdb/inferior.c
> > +++ b/gdb/inferior.c
> > @@ -477,6 +477,22 @@ have_live_inferiors (void)
> >    return number_of_live_inferiors (NULL) > 0;
> >  }
> >
> > +/* Return true if there is at least one executing thread.  */
> > +
> > +bool
> > +any_executing_threads ()
> > +{
> > +  for (inferior *inf : all_non_exited_inferiors (nullptr))
> > +    if (inf->has_execution ())
> > +      for (thread_info &tp ATTRIBUTE_UNUSED : inf->non_exited_threads
> ())
> 
> Is the ATTRIBUTE_UNUSED needed here?  It seems like TP is used.
> 
> > +	{
> > +	  if (tp.executing ())
> > +	    return true;
> > +	}
> > +
> > +  return false;
> > +}

Is there a reason we cannot iterate over `all_non_exited_process_targets ()`
and check `threads_executing` instead of checking each thread?

> > +
> >  /* Prune away any unused inferiors, and then prune away no longer
> used
> >     program spaces.  */
> >
> > diff --git a/gdb/inferior.h b/gdb/inferior.h
> > index 5b499a207b4..9e9633a1905 100644
> > --- a/gdb/inferior.h
> > +++ b/gdb/inferior.h
> > @@ -765,6 +765,9 @@ extern int number_of_live_inferiors
> (process_stratum_target *proc_target);
> >     (not cores, not executables, real live processes).  */
> >  extern int have_live_inferiors (void);
> >
> > +/* Return true if there is at least one executing thread.  */
> > +extern bool any_executing_threads ();
> > +
> >  /* Save/restore the current inferior.  */
> >
> >  class scoped_restore_current_inferior
> > diff --git a/gdb/infrun.c b/gdb/infrun.c
> > index bd114e16b80..f4e51bcb34e 100644
> > --- a/gdb/infrun.c
> > +++ b/gdb/infrun.c
> > @@ -6343,8 +6343,20 @@ handle_inferior_event (struct
> execution_control_state *ecs)
> >
> >        gdb_flush (gdb_stdout);
> >        target_mourn_inferior (inferior_ptid);
> > -      stop_print_frame = false;
> > -      stop_waiting (ecs);
> > +      /* When a removable inferior exits, there's ordinarily no
> reason
> > +	 to report a stop, because this is normally an inferior that
> > +	 was started by some other process, and not explicitly managed
> > +	 by the user.  However, it's possible for a removable inferior
> > +	 to be the last live inferior, and in this case reporting a
> > +	 stop is needed, as otherwise the user will see a message
> > +	 about "unwaited-for children".  */
> > +      if (current_inferior ()->removable && any_executing_threads ())
> 
> I removed the `any_executing_threads ()` and the new test still passes.
> Would it be possible to extend the test to cover this case too?
> 
> > +	prepare_to_wait (ecs);
> > +      else
> > +	{
> > +	  stop_print_frame = false;
> > +	  stop_waiting (ecs);
> > +	}
> >        return;
> >
> >      case TARGET_WAITKIND_FORKED:
> > diff --git a/gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
> b/gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
> > new file mode 100644
> > index 00000000000..111351cf299
> > --- /dev/null
> > +++ b/gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
> > @@ -0,0 +1,45 @@
> > +# 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 removable inferiors don't cause a stop when they exit.
> > +
> > +# This test program conveniently launches another inferior.
> > +standard_testfile watchpoint-multi-exit.c
> > +
> > +if {[prepare_for_testing "failed to build" $testfile $srcfile
> {debug}]} {
> > +    return
> > +}
> > +
> > +gdb_test_no_output "set detach-on-fork off"
> > +gdb_test_no_output "set schedule-multiple on"
> > +
> > +gdb_test_multiple "run" "do not stop when child exits" {
> 
> This is going to cause errors when using
> --target_board=native-gdbserver.  I'll take a moment to advertise 'make
> check-all-boards'; there's some text in testsuite/README on how to set
> it up.
> 
> Thanks,
> Andrew
> 
> > +    -re "Starting program: \[^\n\]*\r\n" {

Using `\[^\r\n\]*` is much more common than `\[^\n\]*` in the testsuite.

> > +	exp_continue
> > +    }
> > +    -re "\\\[New inferior 2\[^\n\]\\\]\r\n" {
> > +	exp_continue
> > +    }
> > +    -re "\\\[Inferior 2 .*exited normally\\\]\r\n" {

I'd suggest using `\[^\r\n\]*` instead of `.*`, because otherwise we may
too eagerly match until Inferior 1's "exited normally", causing failures.

Thanks,
-Baris

> > +	exp_continue
> > +    }
> > +    -re -wrap "\\\[Inferior 1 .*exited normally\\\]" {
> > +	pass $gdb_test_name
> > +    }
> > +    -re -wrap "" {
> > +	# Any other stop is a failure.
> > +	fail $gdb_test_name
> > +    }
> > +}
> > --
> > 2.49.0

Intel Deutschland GmbH
Registered Address: Dornacher Straße 1, 85622 Feldkirchen, Germany
Tel: +49 89 991 430, www.intel.de
Managing Directors: Harry Demas, Jeffrey Schneiderman, Yin Chong Sorrell
Chairperson of the Supervisory Board: Nicole Lau
Registered Seat: Munich
Commercial Register: Amtsgericht München HRB 186928
  
Tom Tromey Jan. 13, 2026, 2:02 a.m. UTC | #3
>> +      for (thread_info &tp ATTRIBUTE_UNUSED : inf->non_exited_threads ())

Andrew> Is the ATTRIBUTE_UNUSED needed here?  It seems like TP is used.

Oops, I fixed this.

>> +      if (current_inferior ()->removable && any_executing_threads ())

Andrew> I removed the `any_executing_threads ()` and the new test still passes.
Andrew> Would it be possible to extend the test to cover this case too?

I believe if you remove this, there's another test that fails -- one of
the ones in gdb.python I believe.

>> +gdb_test_multiple "run" "do not stop when child exits" {

Andrew> This is going to cause errors when using
Andrew> --target_board=native-gdbserver.  I'll take a moment to advertise 'make
Andrew> check-all-boards'; there's some text in testsuite/README on how to set
Andrew> it up.

I'll take a look.

Tom
  

Patch

diff --git a/gdb/inferior.c b/gdb/inferior.c
index 8131f23b938..8f62ff84a54 100644
--- a/gdb/inferior.c
+++ b/gdb/inferior.c
@@ -477,6 +477,22 @@  have_live_inferiors (void)
   return number_of_live_inferiors (NULL) > 0;
 }
 
+/* Return true if there is at least one executing thread.  */
+
+bool
+any_executing_threads ()
+{
+  for (inferior *inf : all_non_exited_inferiors (nullptr))
+    if (inf->has_execution ())
+      for (thread_info &tp ATTRIBUTE_UNUSED : inf->non_exited_threads ())
+	{
+	  if (tp.executing ())
+	    return true;
+	}
+
+  return false;
+}
+
 /* Prune away any unused inferiors, and then prune away no longer used
    program spaces.  */
 
diff --git a/gdb/inferior.h b/gdb/inferior.h
index 5b499a207b4..9e9633a1905 100644
--- a/gdb/inferior.h
+++ b/gdb/inferior.h
@@ -765,6 +765,9 @@  extern int number_of_live_inferiors (process_stratum_target *proc_target);
    (not cores, not executables, real live processes).  */
 extern int have_live_inferiors (void);
 
+/* Return true if there is at least one executing thread.  */
+extern bool any_executing_threads ();
+
 /* Save/restore the current inferior.  */
 
 class scoped_restore_current_inferior
diff --git a/gdb/infrun.c b/gdb/infrun.c
index bd114e16b80..f4e51bcb34e 100644
--- a/gdb/infrun.c
+++ b/gdb/infrun.c
@@ -6343,8 +6343,20 @@  handle_inferior_event (struct execution_control_state *ecs)
 
       gdb_flush (gdb_stdout);
       target_mourn_inferior (inferior_ptid);
-      stop_print_frame = false;
-      stop_waiting (ecs);
+      /* When a removable inferior exits, there's ordinarily no reason
+	 to report a stop, because this is normally an inferior that
+	 was started by some other process, and not explicitly managed
+	 by the user.  However, it's possible for a removable inferior
+	 to be the last live inferior, and in this case reporting a
+	 stop is needed, as otherwise the user will see a message
+	 about "unwaited-for children".  */
+      if (current_inferior ()->removable && any_executing_threads ())
+	prepare_to_wait (ecs);
+      else
+	{
+	  stop_print_frame = false;
+	  stop_waiting (ecs);
+	}
       return;
 
     case TARGET_WAITKIND_FORKED:
diff --git a/gdb/testsuite/gdb.multi/no-stop-removable-exit.exp b/gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
new file mode 100644
index 00000000000..111351cf299
--- /dev/null
+++ b/gdb/testsuite/gdb.multi/no-stop-removable-exit.exp
@@ -0,0 +1,45 @@ 
+# 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 removable inferiors don't cause a stop when they exit.
+
+# This test program conveniently launches another inferior.
+standard_testfile watchpoint-multi-exit.c
+
+if {[prepare_for_testing "failed to build" $testfile $srcfile {debug}]} {
+    return
+}
+
+gdb_test_no_output "set detach-on-fork off"
+gdb_test_no_output "set schedule-multiple on"
+
+gdb_test_multiple "run" "do not stop when child exits" {
+    -re "Starting program: \[^\n\]*\r\n" {
+	exp_continue
+    }
+    -re "\\\[New inferior 2\[^\n\]\\\]\r\n" {
+	exp_continue
+    }
+    -re "\\\[Inferior 2 .*exited normally\\\]\r\n" {
+	exp_continue
+    }
+    -re -wrap "\\\[Inferior 1 .*exited normally\\\]" {
+	pass $gdb_test_name
+    }
+    -re -wrap "" {
+	# Any other stop is a failure.
+	fail $gdb_test_name
+    }
+}