From patchwork Tue Jul 4 16:51:13 2023 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Tom Tromey X-Patchwork-Id: 72091 Return-Path: X-Original-To: patchwork@sourceware.org Delivered-To: patchwork@sourceware.org Received: from server2.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id C3C913858436 for ; Tue, 4 Jul 2023 16:51:37 +0000 (GMT) X-Original-To: gdb-patches@sourceware.org Delivered-To: gdb-patches@sourceware.org Received: from gproxy4-pub.mail.unifiedlayer.com (gproxy4-pub.mail.unifiedlayer.com [69.89.23.142]) by sourceware.org (Postfix) with ESMTPS id 8620D3858D32 for ; Tue, 4 Jul 2023 16:51:21 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 8620D3858D32 Authentication-Results: sourceware.org; dmarc=none (p=none dis=none) header.from=tromey.com Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=tromey.com Received: from cmgw14.mail.unifiedlayer.com (unknown [10.0.90.129]) by progateway6.mail.pro1.eigbox.com (Postfix) with ESMTP id 3C19510047D6A for ; Tue, 4 Jul 2023 16:51:20 +0000 (UTC) Received: from box5379.bluehost.com ([162.241.216.53]) by cmsmtp with ESMTP id GjFIqJrfgUoBjGjFIqSYHg; Tue, 04 Jul 2023 16:51:20 +0000 X-Authority-Reason: nr=8 X-Authority-Analysis: v=2.4 cv=N7fsq0xB c=1 sm=1 tr=0 ts=64a44e08 a=ApxJNpeYhEAb1aAlGBBbmA==:117 a=ApxJNpeYhEAb1aAlGBBbmA==:17 a=dLZJa+xiwSxG16/P+YVxDGlgEgI=:19 a=ws7JD89P4LkA:10:nop_rcvd_month_year a=Qbun_eYptAEA:10:endurance_base64_authed_username_1 a=mDV3o1hIAAAA:8 a=Cmlc4x5YDMK31Xztei0A:9 a=_FVE-zBwftR9WsbkzFJk:22 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=tromey.com; s=default; h=Content-Transfer-Encoding:MIME-Version:Message-ID:Date:Subject: Cc:To:From:Sender:Reply-To:Content-Type:Content-ID:Content-Description: Resent-Date:Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID: In-Reply-To:References:List-Id:List-Help:List-Unsubscribe:List-Subscribe: List-Post:List-Owner:List-Archive; bh=uIeI+xgdXG08ZiJ+iBGe/sipBhjnwOh4C+0b3YC1pC4=; b=r7lu8KigETMqoiptFBqhKfDe8L PjbScV7UB6/+8eai0jqIHoYELjDq/nRhz+K6bueTFUMtfYMTtgVPnT7gGB3EU5dvjI/heCY6cMUhK wIPOltzHhEJjqlc1Kk31c7qxI; Received: from 71-211-181-162.hlrn.qwest.net ([71.211.181.162]:33080 helo=localhost.localdomain) by box5379.bluehost.com with esmtpsa (TLS1.2) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1qGjFH-001Goc-2e; Tue, 04 Jul 2023 10:51:19 -0600 From: Tom Tromey To: gdb-patches@sourceware.org Cc: Tom Tromey Subject: [PATCH] Export gdb.block_signals and create gdb.Thread Date: Tue, 4 Jul 2023 10:51:13 -0600 Message-ID: <20230704165113.409751-1-tom@tromey.com> X-Mailer: git-send-email 2.41.0 MIME-Version: 1.0 X-AntiAbuse: This header was added to track abuse, please include it with any abuse report X-AntiAbuse: Primary Hostname - box5379.bluehost.com X-AntiAbuse: Original Domain - sourceware.org X-AntiAbuse: Originator/Caller UID/GID - [47 12] / [47 12] X-AntiAbuse: Sender Address Domain - tromey.com X-BWhitelist: no X-Source-IP: 71.211.181.162 X-Source-L: No X-Exim-ID: 1qGjFH-001Goc-2e X-Source: X-Source-Args: X-Source-Dir: X-Source-Sender: 71-211-181-162.hlrn.qwest.net (localhost.localdomain) [71.211.181.162]:33080 X-Source-Auth: tom+tromey.com X-Email-Count: 1 X-Source-Cap: ZWx5bnJvYmk7ZWx5bnJvYmk7Ym94NTM3OS5ibHVlaG9zdC5jb20= X-Local-Domain: yes X-Spam-Status: No, score=-3025.5 required=5.0 tests=BAYES_00, DKIM_SIGNED, DKIM_VALID, GIT_PATCH_0, JMQ_SPF_NEUTRAL, KAM_SHORT, RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H5, RCVD_IN_MSPIKE_WL, SPF_HELO_NONE, SPF_PASS, TXREP, T_SCC_BODY_TEXT_LINE autolearn=ham autolearn_force=no version=3.4.6 X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on server2.sourceware.org X-BeenThere: gdb-patches@sourceware.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: Gdb-patches mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: gdb-patches-bounces+patchwork=sourceware.org@sourceware.org Sender: "Gdb-patches" 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. Reviewed-By: Eli Zaretskii --- gdb/NEWS | 6 ++ gdb/doc/python.texi | 105 +++++++++++++++++++----------- gdb/python/lib/gdb/__init__.py | 32 +++++++++ gdb/python/lib/gdb/dap/startup.py | 26 +------- 4 files changed, 106 insertions(+), 63 deletions(-) diff --git a/gdb/NEWS b/gdb/NEWS index fd42864c692..fe79f1716fd 100644 --- a/gdb/NEWS +++ b/gdb/NEWS @@ -208,6 +208,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. diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi index 9a342f34bf0..75fae3a1895 100644 --- a/gdb/doc/python.texi +++ b/gdb/doc/python.texi @@ -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. @@ -447,45 +448,6 @@ will be @code{None} and 0 respectively. This is identical to historical compatibility. @end defun -@findex gdb.post_event -@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 - @findex gdb.write @defun gdb.write (string @r{[}, stream@r{]}) Print a string to @value{GDBN}'s paginated output stream. The @@ -688,6 +650,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 diff --git a/gdb/python/lib/gdb/__init__.py b/gdb/python/lib/gdb/__init__.py index 6f3f1945f62..98aadb1dfea 100644 --- a/gdb/python/lib/gdb/__init__.py +++ b/gdb/python/lib/gdb/__init__.py @@ -13,6 +13,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +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() diff --git a/gdb/python/lib/gdb/dap/startup.py b/gdb/python/lib/gdb/dap/startup.py index aa834cdb14c..15d1fb9e9e5 100644 --- a/gdb/python/lib/gdb/dap/startup.py +++ b/gdb/python/lib/gdb/dap/startup.py @@ -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):