Export gdb.block_signals and create gdb.Thread

While working on an experiment, I realized that I needed the DAP
block_signals function.  I figured other developers may need it as
well, so this patch moves it from DAP to the gdb module and exports
it.

I also added a new subclass of threading.Thread that ensures that
signals are blocked in the new thread.

Finally, this patch slightly rearranges the documentation so that
gdb-side threading issues and functions are all discussed in a single
node.
This commit is contained in:
Tom Tromey 2023-07-04 09:15:54 -06:00
parent 8a9da63e40
commit 560c121c20
4 changed files with 106 additions and 62 deletions

View File

@ -228,6 +228,12 @@ info main
** New function gdb.execute_mi(COMMAND, [ARG]...), that invokes a
GDB/MI command and returns the output as a Python dictionary.
** New function gdb.block_signals(). This returns a context manager
that blocks any signals that GDB needs to handle itself.
** New class gdb.Thread. This is a subclass of threading.Thread
that calls gdb.block_signals in its "start" method.
** gdb.parse_and_eval now has a new "global_context" parameter.
This can be used to request that the parse only examine global
symbols.

View File

@ -190,6 +190,7 @@ optional arguments while skipping others. Example:
@menu
* Basic Python:: Basic Python Functions.
* Threading in GDB:: Using Python threads in GDB.
* Exception Handling:: How Python exceptions are translated.
* Values From Inferior:: Python representation of values.
* Types In Python:: Python representation of types.
@ -436,44 +437,6 @@ will be @code{None} and 0 respectively. This is identical to
historical compatibility.
@end defun
@defun gdb.post_event (event)
Put @var{event}, a callable object taking no arguments, into
@value{GDBN}'s internal event queue. This callable will be invoked at
some later point, during @value{GDBN}'s event processing. Events
posted using @code{post_event} will be run in the order in which they
were posted; however, there is no way to know when they will be
processed relative to other events inside @value{GDBN}.
@value{GDBN} is not thread-safe. If your Python program uses multiple
threads, you must be careful to only call @value{GDBN}-specific
functions in the @value{GDBN} thread. @code{post_event} ensures
this. For example:
@smallexample
(@value{GDBP}) python
>import threading
>
>class Writer():
> def __init__(self, message):
> self.message = message;
> def __call__(self):
> gdb.write(self.message)
>
>class MyThread1 (threading.Thread):
> def run (self):
> gdb.post_event(Writer("Hello "))
>
>class MyThread2 (threading.Thread):
> def run (self):
> gdb.post_event(Writer("World\n"))
>
>MyThread1().start()
>MyThread2().start()
>end
(@value{GDBP}) Hello World
@end smallexample
@end defun
@defun gdb.write (string @r{[}, stream@r{]})
Print a string to @value{GDBN}'s paginated output stream. The
optional @var{stream} determines the stream to print to. The default
@ -669,6 +632,71 @@ In Python}), the @code{language} method might be preferable in some
cases, as that is not affected by the user's language setting.
@end defun
@node Threading in GDB
@subsubsection Threading in GDB
@value{GDBN} is not thread-safe. If your Python program uses multiple
threads, you must be careful to only call @value{GDBN}-specific
functions in the @value{GDBN} thread. @value{GDBN} provides some
functions to help with this.
@defun gdb.block_signals ()
As mentioned earlier (@pxref{Basic Python}), certain signals must be
delivered to the @value{GDBN} main thread. The @code{block_signals}
function returns a context manager that will block these signals on
entry. This can be used when starting a new thread to ensure that the
signals are blocked there, like:
@smallexample
with gdb.block_signals():
start_new_thread()
@end smallexample
@end defun
@deftp {class} gdb.Thread
This is a subclass of Python's @code{threading.Thread} class. It
overrides the @code{start} method to call @code{block_signals}, making
this an easy-to-use drop-in replacement for creating threads that will
work well in @value{GDBN}.
@end deftp
@defun gdb.post_event (event)
Put @var{event}, a callable object taking no arguments, into
@value{GDBN}'s internal event queue. This callable will be invoked at
some later point, during @value{GDBN}'s event processing. Events
posted using @code{post_event} will be run in the order in which they
were posted; however, there is no way to know when they will be
processed relative to other events inside @value{GDBN}.
Unlike most Python APIs in @value{GDBN}, @code{post_event} is
thread-safe. For example:
@smallexample
(@value{GDBP}) python
>import threading
>
>class Writer():
> def __init__(self, message):
> self.message = message;
> def __call__(self):
> gdb.write(self.message)
>
>class MyThread1 (threading.Thread):
> def run (self):
> gdb.post_event(Writer("Hello "))
>
>class MyThread2 (threading.Thread):
> def run (self):
> gdb.post_event(Writer("World\n"))
>
>MyThread1().start()
>MyThread2().start()
>end
(@value{GDBP}) Hello World
@end smallexample
@end defun
@node Exception Handling
@subsubsection Exception Handling
@cindex python exceptions

View File

@ -13,6 +13,8 @@
# 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 signal
import threading
import traceback
import os
import sys
@ -259,3 +261,33 @@ def with_parameter(name, value):
yield None
finally:
set_parameter(name, old_value)
@contextmanager
def blocked_signals():
"""A helper function that blocks and unblocks signals."""
if not hasattr(signal, "pthread_sigmask"):
yield
return
to_block = {signal.SIGCHLD, signal.SIGINT, signal.SIGALRM, signal.SIGWINCH}
signal.pthread_sigmask(signal.SIG_BLOCK, to_block)
try:
yield None
finally:
signal.pthread_sigmask(signal.SIG_UNBLOCK, to_block)
class Thread(threading.Thread):
"""A GDB-specific wrapper around threading.Thread
This wrapper ensures that the new thread blocks any signals that
must be delivered on GDB's main thread."""
def start(self):
# GDB requires that these be delivered to the main thread. We
# do this here to avoid any possible race with the creation of
# the new thread. The thread mask is inherited by new
# threads.
with blocked_signals():
super().start()

View File

@ -18,10 +18,8 @@
import functools
import gdb
import queue
import signal
import threading
import traceback
from contextlib import contextmanager
import sys
@ -33,32 +31,12 @@ _gdb_thread = threading.current_thread()
_dap_thread = None
@contextmanager
def blocked_signals():
"""A helper function that blocks and unblocks signals."""
if not hasattr(signal, "pthread_sigmask"):
yield
return
to_block = {signal.SIGCHLD, signal.SIGINT, signal.SIGALRM, signal.SIGWINCH}
signal.pthread_sigmask(signal.SIG_BLOCK, to_block)
try:
yield None
finally:
signal.pthread_sigmask(signal.SIG_UNBLOCK, to_block)
def start_thread(name, target, args=()):
"""Start a new thread, invoking TARGET with *ARGS there.
This is a helper function that ensures that any GDB signals are
correctly blocked."""
# GDB requires that these be delivered to the gdb thread. We
# do this here to avoid any possible race with the creation of
# the new thread. The thread mask is inherited by new
# threads.
with blocked_signals():
result = threading.Thread(target=target, args=args, daemon=True)
result.start()
result = gdb.Thread(target=target, args=args, daemon=True)
result.start()
def start_dap(target):