77
77
import json
78
78
import token
79
79
import types
80
+ import atexit
80
81
import codeop
81
82
import pprint
82
83
import signal
92
93
import itertools
93
94
import traceback
94
95
import linecache
96
+ import selectors
97
+ import threading
95
98
import _colorize
96
99
import _pyrepl .utils
97
100
98
- from contextlib import closing
99
- from contextlib import contextmanager
101
+ from contextlib import ExitStack , closing , contextmanager
100
102
from rlcompleter import Completer
101
103
from types import CodeType
102
104
from warnings import deprecated
@@ -2670,12 +2672,21 @@ async def set_trace_async(*, header=None, commands=None):
2670
2672
# Remote PDB
2671
2673
2672
2674
class _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
+ ):
2674
2682
self ._owns_sockfile = owns_sockfile
2675
2683
self ._interact_state = None
2676
2684
self ._sockfile = sockfile
2677
2685
self ._command_name_cache = []
2678
2686
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 )
2679
2690
super ().__init__ (colorize = False , ** kwargs )
2680
2691
2681
2692
@staticmethod
@@ -2731,15 +2742,49 @@ def _ensure_valid_message(self, msg):
2731
2742
f"PDB message doesn't follow the schema! { msg } "
2732
2743
)
2733
2744
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
+
2734
2778
def _send (self , ** kwargs ):
2735
2779
self ._ensure_valid_message (kwargs )
2736
2780
json_payload = json .dumps (kwargs )
2737
2781
try :
2738
2782
self ._sockfile .write (json_payload .encode () + b"\n " )
2739
2783
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
2743
2788
# of trying to handle it from everywhere _send() may be called.
2744
2789
# Track this with a flag rather than assuming readline() will ever
2745
2790
# return an empty string because the socket may be half-closed.
@@ -2967,10 +3012,15 @@ def default(self, line):
2967
3012
2968
3013
2969
3014
class _PdbClient :
2970
- def __init__ (self , pid , sockfile , interrupt_script ):
3015
+ def __init__ (self , pid , server_socket , interrupt_sock ):
2971
3016
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
2974
3024
self .pdb_instance = Pdb ()
2975
3025
self .pdb_commands = set ()
2976
3026
self .completion_matches = []
@@ -3012,8 +3062,7 @@ def _send(self, **kwargs):
3012
3062
self ._ensure_valid_message (kwargs )
3013
3063
json_payload = json .dumps (kwargs )
3014
3064
try :
3015
- self .sockfile .write (json_payload .encode () + b"\n " )
3016
- self .sockfile .flush ()
3065
+ self .server_socket .sendall (json_payload .encode () + b"\n " )
3017
3066
except OSError :
3018
3067
# This means that the client has abruptly disconnected, but we'll
3019
3068
# handle that the next time we try to read from the client instead
@@ -3022,10 +3071,44 @@ def _send(self, **kwargs):
3022
3071
# return an empty string because the socket may be half-closed.
3023
3072
self .write_failed = True
3024
3073
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 )
3028
3109
3110
+ def read_command (self , prompt ):
3111
+ reply = self .read_input (prompt , multiline_block = False )
3029
3112
if self .state == "dumb" :
3030
3113
# No logic applied whatsoever, just pass the raw reply back.
3031
3114
return reply
@@ -3048,10 +3131,9 @@ def read_command(self, prompt):
3048
3131
return prefix + reply
3049
3132
3050
3133
# 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 ))
3053
3135
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 )
3055
3137
3056
3138
return prefix + reply
3057
3139
@@ -3076,11 +3158,70 @@ def readline_completion(self, completer):
3076
3158
finally :
3077
3159
readline .set_completer (old_completer )
3078
3160
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
+
3079
3217
def cmdloop (self ):
3080
- with self .readline_completion (self .complete ):
3218
+ with (
3219
+ self ._sigint_handler (),
3220
+ self .readline_completion (self .complete ),
3221
+ ):
3081
3222
while not self .write_failed :
3082
3223
try :
3083
- if not (payload_bytes := self .sockfile . readline ()):
3224
+ if not (payload_bytes := self ._readline ()):
3084
3225
break
3085
3226
except KeyboardInterrupt :
3086
3227
self .send_interrupt ()
@@ -3098,11 +3239,17 @@ def cmdloop(self):
3098
3239
self .process_payload (payload )
3099
3240
3100
3241
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 )
3106
3253
3107
3254
def process_payload (self , payload ):
3108
3255
match payload :
@@ -3172,7 +3319,7 @@ def complete(self, text, state):
3172
3319
if self .write_failed :
3173
3320
return None
3174
3321
3175
- payload = self .sockfile . readline ()
3322
+ payload = self ._readline ()
3176
3323
if not payload :
3177
3324
return None
3178
3325
@@ -3189,11 +3336,18 @@ def complete(self, text, state):
3189
3336
return None
3190
3337
3191
3338
3192
- def _connect (host , port , frame , commands , version ):
3339
+ def _connect (* , host , port , frame , commands , version , signal_raising_thread ):
3193
3340
with closing (socket .create_connection ((host , port ))) as conn :
3194
3341
sockfile = conn .makefile ("rwb" )
3195
3342
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 )
3197
3351
weakref .finalize (remote_pdb , sockfile .close )
3198
3352
3199
3353
if Pdb ._last_pdb_instance is not None :
@@ -3214,43 +3368,48 @@ def _connect(host, port, frame, commands, version):
3214
3368
3215
3369
def attach (pid , commands = ()):
3216
3370
"""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
+ )
3218
3375
port = server .getsockname ()[1 ]
3219
3376
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} ,
3233
3394
)
3395
+ """
3234
3396
)
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 )
3240
3400
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 ))
3243
3404
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
3252
3411
3253
- _PdbClient (pid , sockfile , interrupt_script . name ).cmdloop ()
3412
+ _PdbClient (pid , client_sock , interrupt_sock ).cmdloop ()
3254
3413
3255
3414
3256
3415
# Post-Mortem interface
0 commit comments