Export gdb.block_signals and create gdb.Thread
authorTom Tromey <tom@tromey.com>
Tue, 4 Jul 2023 15:15:54 +0000 (09:15 -0600)
committerTom Tromey <tom@tromey.com>
Sun, 23 Jul 2023 20:33:44 +0000 (14:33 -0600)
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.

gdb/NEWS
gdb/doc/python.texi
gdb/python/lib/gdb/__init__.py
gdb/python/lib/gdb/dap/startup.py

index a44f90e7c1c504b147bc7ca5dd70fa52aa531949..3f414a54fb7c172fe6f7ddcc20d5ee2be73b2314 100644 (file)
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -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.
index b4cdc22f626916812c19e85266cf69d2b87178df..a4ec5bf5ceb27a1432c1ff5e2c728dff5055fec9 100644 (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
index 6f3f1945f6239a06d61ce0283525163bf86ee2ec..98aadb1dfea7a37ff35680f44da662b8a2c7afd6 100644 (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()
index aa834cdb14c59fdabe93e3281bb0d0a4f85fb56f..15d1fb9e9e5468a01bed752c07b59309dc9f949a 100644 (file)
 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):