@@ -2614,12 +2614,21 @@ async def set_trace_async(*, header=None, commands=None):
26142614# Remote PDB
26152615
26162616class _PdbServer (Pdb ):
2617- def __init__ (self , sockfile , owns_sockfile = True , ** kwargs ):
2617+ def __init__ (
2618+ self ,
2619+ sockfile ,
2620+ signal_server = None ,
2621+ owns_sockfile = True ,
2622+ ** kwargs ,
2623+ ):
26182624 self ._owns_sockfile = owns_sockfile
26192625 self ._interact_state = None
26202626 self ._sockfile = sockfile
26212627 self ._command_name_cache = []
26222628 self ._write_failed = False
2629+ if signal_server :
2630+ # Only started by the top level _PdbServer, not recursive ones.
2631+ self ._start_signal_listener (signal_server )
26232632 super ().__init__ (** kwargs )
26242633
26252634 @staticmethod
@@ -2675,6 +2684,39 @@ def _ensure_valid_message(self, msg):
26752684 f"PDB message doesn't follow the schema! { msg } "
26762685 )
26772686
2687+ @classmethod
2688+ def _start_signal_listener (cls , address ):
2689+ def listener (sock ):
2690+ with closing (sock ):
2691+ # Check if the interpreter is finalizing every quarter of a second.
2692+ # Clean up and exit if so.
2693+ sock .settimeout (0.25 )
2694+ sock .shutdown (socket .SHUT_WR )
2695+ while not shut_down .is_set ():
2696+ try :
2697+ data = sock .recv (1024 )
2698+ except socket .timeout :
2699+ continue
2700+ if data == b"" :
2701+ return # EOF
2702+ signal .raise_signal (signal .SIGINT )
2703+
2704+ def stop_thread ():
2705+ shut_down .set ()
2706+ thread .join ()
2707+
2708+ # Use a daemon thread so that we don't detach until after all non-daemon
2709+ # threads are done. Use an atexit handler to stop gracefully at that point,
2710+ # so that our thread is stopped before the interpreter is torn down.
2711+ shut_down = threading .Event ()
2712+ thread = threading .Thread (
2713+ target = listener ,
2714+ args = [socket .create_connection (address , timeout = 5 )],
2715+ daemon = True ,
2716+ )
2717+ atexit .register (stop_thread )
2718+ thread .start ()
2719+
26782720 def _send (self , ** kwargs ):
26792721 self ._ensure_valid_message (kwargs )
26802722 json_payload = json .dumps (kwargs )
@@ -3132,17 +3174,13 @@ def cmdloop(self):
31323174 self .process_payload (payload )
31333175
31343176 def send_interrupt (self ):
3135- if sys .platform != "win32" :
3136- # On Unix, send a SIGINT to the remote process, which interrupts IO
3137- # and makes it raise a KeyboardInterrupt on the main thread when
3138- # PyErr_CheckSignals is called or the eval loop regains control.
3139- os .kill (self .pid , signal .SIGINT )
3140- else :
3141- # On Windows, write to a socket that the PDB server listens on.
3142- # This triggers the remote to raise a SIGINT for itself. We do this
3143- # because Windows doesn't allow triggering SIGINT remotely.
3144- # See https://stackoverflow.com/a/35792192 for many more details.
3145- self .interrupt_sock .sendall (signal .SIGINT .to_bytes ())
3177+ # Write to a socket that the PDB server listens on. This triggers
3178+ # the remote to raise a SIGINT for itself. We do this because
3179+ # Windows doesn't allow triggering SIGINT remotely.
3180+ # See https://stackoverflow.com/a/35792192 for many more details.
3181+ # We could directly send SIGINT to the remote process on Unix, but
3182+ # doing the same thing on all platforms simplifies maintenance.
3183+ self .interrupt_sock .sendall (signal .SIGINT .to_bytes ())
31463184
31473185 def process_payload (self , payload ):
31483186 match payload :
@@ -3225,46 +3263,19 @@ def complete(self, text, state):
32253263 return None
32263264
32273265
3228- def _start_interrupt_listener (host , port ):
3229- def sigint_listener (host , port ):
3230- with closing (
3231- socket .create_connection ((host , port ), timeout = 5 )
3232- ) as sock :
3233- # Check if the interpreter is finalizing every quarter of a second.
3234- # Clean up and exit if so.
3235- sock .settimeout (0.25 )
3236- sock .shutdown (socket .SHUT_WR )
3237- while not shut_down .is_set ():
3238- try :
3239- data = sock .recv (1024 )
3240- except socket .timeout :
3241- continue
3242- if data == b"" :
3243- return # EOF
3244- signal .raise_signal (signal .SIGINT )
3245-
3246- def stop_thread ():
3247- shut_down .set ()
3248- thread .join ()
3249-
3250- # Use a daemon thread so that we don't detach until after all non-daemon
3251- # threads are done. Use an atexit handler to stop gracefully at that point,
3252- # so that our thread is stopped before the interpreter is torn down.
3253- shut_down = threading .Event ()
3254- thread = threading .Thread (
3255- target = sigint_listener ,
3256- args = (host , port ),
3257- daemon = True ,
3258- )
3259- atexit .register (stop_thread )
3260- thread .start ()
3261-
3262-
3263- def _connect (host , port , frame , commands , version ):
3266+ def _connect (host , port , frame , commands , version , signal_raising_thread ):
32643267 with closing (socket .create_connection ((host , port ))) as conn :
32653268 sockfile = conn .makefile ("rwb" )
32663269
3267- remote_pdb = _PdbServer (sockfile )
3270+ # Starting a signal raising thread is optional to allow us the flexibility
3271+ # to switch to sending signals directly on Unix platforms in the future
3272+ # without breaking backwards compatibility. This also makes tests simpler.
3273+ if signal_raising_thread :
3274+ signal_server = (host , port )
3275+ else :
3276+ signal_server = None
3277+
3278+ remote_pdb = _PdbServer (sockfile , signal_server = signal_server )
32683279 weakref .finalize (remote_pdb , sockfile .close )
32693280
32703281 if Pdb ._last_pdb_instance is not None :
@@ -3291,12 +3302,6 @@ def attach(pid, commands=()):
32913302 )
32923303 port = server .getsockname ()[1 ]
32933304
3294- if sys .platform == "win32" :
3295- commands = [
3296- f"__import__('pdb')._start_interrupt_listener('localhost', { port } )" ,
3297- * commands ,
3298- ]
3299-
33003305 connect_script = stack .enter_context (
33013306 tempfile .NamedTemporaryFile ("w" , delete_on_close = False )
33023307 )
@@ -3311,6 +3316,7 @@ def attach(pid, commands=()):
33113316 frame=sys._getframe(1),
33123317 commands={ json .dumps ("\n " .join (commands ))} ,
33133318 version={ _PdbServer .protocol_version ()} ,
3319+ signal_raising_thread=True,
33143320 )
33153321 """
33163322 )
@@ -3322,12 +3328,9 @@ def attach(pid, commands=()):
33223328 client_sock , _ = server .accept ()
33233329 stack .enter_context (closing (client_sock ))
33243330
3325- if sys .platform == "win32" :
3326- interrupt_sock , _ = server .accept ()
3327- stack .enter_context (closing (interrupt_sock ))
3328- interrupt_sock .setblocking (False )
3329- else :
3330- interrupt_sock = None
3331+ interrupt_sock , _ = server .accept ()
3332+ stack .enter_context (closing (interrupt_sock ))
3333+ interrupt_sock .setblocking (False )
33313334
33323335 _PdbClient (pid , client_sock , interrupt_sock ).cmdloop ()
33333336
0 commit comments