7777import json
7878import token
7979import types
80+ import atexit
8081import codeop
8182import pprint
8283import signal
9293import itertools
9394import traceback
9495import linecache
96+ import selectors
97+ import threading
9598import _colorize
9699import _pyrepl .utils
97100
98- from contextlib import closing
99- from contextlib import contextmanager
101+ from contextlib import ExitStack , closing , contextmanager
100102from rlcompleter import Completer
101103from types import CodeType
102104from warnings import deprecated
@@ -2670,12 +2672,21 @@ async def set_trace_async(*, header=None, commands=None):
26702672# Remote PDB
26712673
26722674class _PdbServer (Pdb ):
2673- def __init__ (self , sockfile , owns_sockfile = True , ** kwargs ):
2675+ def __init__ (
2676+ self ,
2677+ sockfile ,
2678+ signal_server = None ,
2679+ owns_sockfile = True ,
2680+ ** kwargs ,
2681+ ):
26742682 self ._owns_sockfile = owns_sockfile
26752683 self ._interact_state = None
26762684 self ._sockfile = sockfile
26772685 self ._command_name_cache = []
26782686 self ._write_failed = False
2687+ if signal_server :
2688+ # Only started by the top level _PdbServer, not recursive ones.
2689+ self ._start_signal_listener (signal_server )
26792690 super ().__init__ (colorize = False , ** kwargs )
26802691
26812692 @staticmethod
@@ -2731,15 +2742,49 @@ def _ensure_valid_message(self, msg):
27312742 f"PDB message doesn't follow the schema! { msg } "
27322743 )
27332744
2745+ @classmethod
2746+ def _start_signal_listener (cls , address ):
2747+ def listener (sock ):
2748+ with closing (sock ):
2749+ # Check if the interpreter is finalizing every quarter of a second.
2750+ # Clean up and exit if so.
2751+ sock .settimeout (0.25 )
2752+ sock .shutdown (socket .SHUT_WR )
2753+ while not shut_down .is_set ():
2754+ try :
2755+ data = sock .recv (1024 )
2756+ except socket .timeout :
2757+ continue
2758+ if data == b"" :
2759+ return # EOF
2760+ signal .raise_signal (signal .SIGINT )
2761+
2762+ def stop_thread ():
2763+ shut_down .set ()
2764+ thread .join ()
2765+
2766+ # Use a daemon thread so that we don't detach until after all non-daemon
2767+ # threads are done. Use an atexit handler to stop gracefully at that point,
2768+ # so that our thread is stopped before the interpreter is torn down.
2769+ shut_down = threading .Event ()
2770+ thread = threading .Thread (
2771+ target = listener ,
2772+ args = [socket .create_connection (address , timeout = 5 )],
2773+ daemon = True ,
2774+ )
2775+ atexit .register (stop_thread )
2776+ thread .start ()
2777+
27342778 def _send (self , ** kwargs ):
27352779 self ._ensure_valid_message (kwargs )
27362780 json_payload = json .dumps (kwargs )
27372781 try :
27382782 self ._sockfile .write (json_payload .encode () + b"\n " )
27392783 self ._sockfile .flush ()
2740- except OSError :
2741- # This means that the client has abruptly disconnected, but we'll
2742- # handle that the next time we try to read from the client instead
2784+ except (OSError , ValueError ):
2785+ # We get an OSError if the network connection has dropped, and a
2786+ # ValueError if detach() if the sockfile has been closed. We'll
2787+ # handle this the next time we try to read from the client instead
27432788 # of trying to handle it from everywhere _send() may be called.
27442789 # Track this with a flag rather than assuming readline() will ever
27452790 # return an empty string because the socket may be half-closed.
@@ -2967,10 +3012,15 @@ def default(self, line):
29673012
29683013
29693014class _PdbClient :
2970- def __init__ (self , pid , sockfile , interrupt_script ):
3015+ def __init__ (self , pid , server_socket , interrupt_sock ):
29713016 self .pid = pid
2972- self .sockfile = sockfile
2973- self .interrupt_script = interrupt_script
3017+ self .read_buf = b""
3018+ self .signal_read = None
3019+ self .signal_write = None
3020+ self .sigint_received = False
3021+ self .raise_on_sigint = False
3022+ self .server_socket = server_socket
3023+ self .interrupt_sock = interrupt_sock
29743024 self .pdb_instance = Pdb ()
29753025 self .pdb_commands = set ()
29763026 self .completion_matches = []
@@ -3012,8 +3062,7 @@ def _send(self, **kwargs):
30123062 self ._ensure_valid_message (kwargs )
30133063 json_payload = json .dumps (kwargs )
30143064 try :
3015- self .sockfile .write (json_payload .encode () + b"\n " )
3016- self .sockfile .flush ()
3065+ self .server_socket .sendall (json_payload .encode () + b"\n " )
30173066 except OSError :
30183067 # This means that the client has abruptly disconnected, but we'll
30193068 # handle that the next time we try to read from the client instead
@@ -3022,10 +3071,44 @@ def _send(self, **kwargs):
30223071 # return an empty string because the socket may be half-closed.
30233072 self .write_failed = True
30243073
3025- def read_command (self , prompt ):
3026- self .multiline_block = False
3027- reply = input (prompt )
3074+ def _readline (self ):
3075+ if self .sigint_received :
3076+ # There's a pending unhandled SIGINT. Handle it now.
3077+ self .sigint_received = False
3078+ raise KeyboardInterrupt
3079+
3080+ # Wait for either a SIGINT or a line or EOF from the PDB server.
3081+ selector = selectors .DefaultSelector ()
3082+ selector .register (self .signal_read , selectors .EVENT_READ )
3083+ selector .register (self .server_socket , selectors .EVENT_READ )
3084+
3085+ while b"\n " not in self .read_buf :
3086+ for key , _ in selector .select ():
3087+ if key .fileobj == self .signal_read :
3088+ self .signal_read .recv (1024 )
3089+ if self .sigint_received :
3090+ # If not, we're reading wakeup events for sigints that
3091+ # we've previously handled, and can ignore them.
3092+ self .sigint_received = False
3093+ raise KeyboardInterrupt
3094+ elif key .fileobj == self .server_socket :
3095+ data = self .server_socket .recv (16 * 1024 )
3096+ self .read_buf += data
3097+ if not data and b"\n " not in self .read_buf :
3098+ # EOF without a full final line. Drop the partial line.
3099+ self .read_buf = b""
3100+ return b""
3101+
3102+ ret , sep , self .read_buf = self .read_buf .partition (b"\n " )
3103+ return ret + sep
3104+
3105+ def read_input (self , prompt , multiline_block ):
3106+ self .multiline_block = multiline_block
3107+ with self ._sigint_raises_keyboard_interrupt ():
3108+ return input (prompt )
30283109
3110+ def read_command (self , prompt ):
3111+ reply = self .read_input (prompt , multiline_block = False )
30293112 if self .state == "dumb" :
30303113 # No logic applied whatsoever, just pass the raw reply back.
30313114 return reply
@@ -3048,10 +3131,9 @@ def read_command(self, prompt):
30483131 return prefix + reply
30493132
30503133 # Otherwise, valid first line of a multi-line statement
3051- self .multiline_block = True
3052- continue_prompt = "..." .ljust (len (prompt ))
3134+ more_prompt = "..." .ljust (len (prompt ))
30533135 while codeop .compile_command (reply , "<stdin>" , "single" ) is None :
3054- reply += "\n " + input ( continue_prompt )
3136+ reply += "\n " + self . read_input ( more_prompt , multiline_block = True )
30553137
30563138 return prefix + reply
30573139
@@ -3076,11 +3158,70 @@ def readline_completion(self, completer):
30763158 finally :
30773159 readline .set_completer (old_completer )
30783160
3161+ @contextmanager
3162+ def _sigint_handler (self ):
3163+ # Signal handling strategy:
3164+ # - When we call input() we want a SIGINT to raise KeyboardInterrupt
3165+ # - Otherwise we want to write to the wakeup FD and set a flag.
3166+ # We'll break out of select() when the wakeup FD is written to,
3167+ # and we'll check the flag whenever we're about to accept input.
3168+ def handler (signum , frame ):
3169+ self .sigint_received = True
3170+ if self .raise_on_sigint :
3171+ # One-shot; don't raise again until the flag is set again.
3172+ self .raise_on_sigint = False
3173+ self .sigint_received = False
3174+ raise KeyboardInterrupt
3175+
3176+ sentinel = object ()
3177+ old_handler = sentinel
3178+ old_wakeup_fd = sentinel
3179+
3180+ self .signal_read , self .signal_write = socket .socketpair ()
3181+ with (closing (self .signal_read ), closing (self .signal_write )):
3182+ self .signal_read .setblocking (False )
3183+ self .signal_write .setblocking (False )
3184+
3185+ try :
3186+ old_handler = signal .signal (signal .SIGINT , handler )
3187+
3188+ try :
3189+ old_wakeup_fd = signal .set_wakeup_fd (
3190+ self .signal_write .fileno (),
3191+ warn_on_full_buffer = False ,
3192+ )
3193+ yield
3194+ finally :
3195+ # Restore the old wakeup fd if we installed a new one
3196+ if old_wakeup_fd is not sentinel :
3197+ signal .set_wakeup_fd (old_wakeup_fd )
3198+ finally :
3199+ self .signal_read = self .signal_write = None
3200+ if old_handler is not sentinel :
3201+ # Restore the old handler if we installed a new one
3202+ signal .signal (signal .SIGINT , old_handler )
3203+
3204+ @contextmanager
3205+ def _sigint_raises_keyboard_interrupt (self ):
3206+ if self .sigint_received :
3207+ # There's a pending unhandled SIGINT. Handle it now.
3208+ self .sigint_received = False
3209+ raise KeyboardInterrupt
3210+
3211+ try :
3212+ self .raise_on_sigint = True
3213+ yield
3214+ finally :
3215+ self .raise_on_sigint = False
3216+
30793217 def cmdloop (self ):
3080- with self .readline_completion (self .complete ):
3218+ with (
3219+ self ._sigint_handler (),
3220+ self .readline_completion (self .complete ),
3221+ ):
30813222 while not self .write_failed :
30823223 try :
3083- if not (payload_bytes := self .sockfile . readline ()):
3224+ if not (payload_bytes := self ._readline ()):
30843225 break
30853226 except KeyboardInterrupt :
30863227 self .send_interrupt ()
@@ -3098,11 +3239,17 @@ def cmdloop(self):
30983239 self .process_payload (payload )
30993240
31003241 def send_interrupt (self ):
3101- print (
3102- "\n *** Program will stop at the next bytecode instruction."
3103- " (Use 'cont' to resume)."
3104- )
3105- sys .remote_exec (self .pid , self .interrupt_script )
3242+ if self .interrupt_sock is not None :
3243+ # Write to a socket that the PDB server listens on. This triggers
3244+ # the remote to raise a SIGINT for itself. We do this because
3245+ # Windows doesn't allow triggering SIGINT remotely.
3246+ # See https://stackoverflow.com/a/35792192 for many more details.
3247+ self .interrupt_sock .sendall (signal .SIGINT .to_bytes ())
3248+ else :
3249+ # On Unix we can just send a SIGINT to the remote process.
3250+ # This is preferable to using the signal thread approach that we
3251+ # use on Windows because it can interrupt IO in the main thread.
3252+ os .kill (self .pid , signal .SIGINT )
31063253
31073254 def process_payload (self , payload ):
31083255 match payload :
@@ -3172,7 +3319,7 @@ def complete(self, text, state):
31723319 if self .write_failed :
31733320 return None
31743321
3175- payload = self .sockfile . readline ()
3322+ payload = self ._readline ()
31763323 if not payload :
31773324 return None
31783325
@@ -3189,11 +3336,18 @@ def complete(self, text, state):
31893336 return None
31903337
31913338
3192- def _connect (host , port , frame , commands , version ):
3339+ def _connect (* , host , port , frame , commands , version , signal_raising_thread ):
31933340 with closing (socket .create_connection ((host , port ))) as conn :
31943341 sockfile = conn .makefile ("rwb" )
31953342
3196- remote_pdb = _PdbServer (sockfile )
3343+ # The client requests this thread on Windows but not on Unix.
3344+ # Most tests don't request this thread, to keep them simpler.
3345+ if signal_raising_thread :
3346+ signal_server = (host , port )
3347+ else :
3348+ signal_server = None
3349+
3350+ remote_pdb = _PdbServer (sockfile , signal_server = signal_server )
31973351 weakref .finalize (remote_pdb , sockfile .close )
31983352
31993353 if Pdb ._last_pdb_instance is not None :
@@ -3214,43 +3368,48 @@ def _connect(host, port, frame, commands, version):
32143368
32153369def attach (pid , commands = ()):
32163370 """Attach to a running process with the given PID."""
3217- with closing (socket .create_server (("localhost" , 0 ))) as server :
3371+ with ExitStack () as stack :
3372+ server = stack .enter_context (
3373+ closing (socket .create_server (("localhost" , 0 )))
3374+ )
32183375 port = server .getsockname ()[1 ]
32193376
3220- with tempfile .NamedTemporaryFile ("w" , delete_on_close = False ) as connect_script :
3221- connect_script .write (
3222- textwrap .dedent (
3223- f"""
3224- import pdb, sys
3225- pdb._connect(
3226- host="localhost",
3227- port={ port } ,
3228- frame=sys._getframe(1),
3229- commands={ json .dumps ("\n " .join (commands ))} ,
3230- version={ _PdbServer .protocol_version ()} ,
3231- )
3232- """
3377+ connect_script = stack .enter_context (
3378+ tempfile .NamedTemporaryFile ("w" , delete_on_close = False )
3379+ )
3380+
3381+ use_signal_thread = sys .platform == "win32"
3382+
3383+ connect_script .write (
3384+ textwrap .dedent (
3385+ f"""
3386+ import pdb, sys
3387+ pdb._connect(
3388+ host="localhost",
3389+ port={ port } ,
3390+ frame=sys._getframe(1),
3391+ commands={ json .dumps ("\n " .join (commands ))} ,
3392+ version={ _PdbServer .protocol_version ()} ,
3393+ signal_raising_thread={ use_signal_thread !r} ,
32333394 )
3395+ """
32343396 )
3235- connect_script .close ()
3236- sys .remote_exec (pid , connect_script .name )
3237-
3238- # TODO Add a timeout? Or don't bother since the user can ^C?
3239- client_sock , _ = server .accept ()
3397+ )
3398+ connect_script .close ()
3399+ sys .remote_exec (pid , connect_script .name )
32403400
3241- with closing (client_sock ):
3242- sockfile = client_sock .makefile ("rwb" )
3401+ # TODO Add a timeout? Or don't bother since the user can ^C?
3402+ client_sock , _ = server .accept ()
3403+ stack .enter_context (closing (client_sock ))
32433404
3244- with closing (sockfile ):
3245- with tempfile .NamedTemporaryFile ("w" , delete_on_close = False ) as interrupt_script :
3246- interrupt_script .write (
3247- 'import pdb, sys\n '
3248- 'if inst := pdb.Pdb._last_pdb_instance:\n '
3249- ' inst.set_trace(sys._getframe(1))\n '
3250- )
3251- interrupt_script .close ()
3405+ if use_signal_thread :
3406+ interrupt_sock , _ = server .accept ()
3407+ stack .enter_context (closing (interrupt_sock ))
3408+ interrupt_sock .setblocking (False )
3409+ else :
3410+ interrupt_sock = None
32523411
3253- _PdbClient (pid , sockfile , interrupt_script . name ).cmdloop ()
3412+ _PdbClient (pid , client_sock , interrupt_sock ).cmdloop ()
32543413
32553414
32563415# Post-Mortem interface
0 commit comments