@@ -102,6 +102,11 @@ def __init__(self, debug_config=None):
102
102
self .adapter = None
103
103
"""psutil.Popen instance for the adapter process."""
104
104
105
+ self .expected_adapter_sockets = {
106
+ "client" : {"host" : some .str , "port" : some .int , "internal" : False },
107
+ }
108
+ """The sockets which the adapter is expected to report."""
109
+
105
110
self .adapter_endpoints = None
106
111
"""Name of the file that contains the adapter endpoints information.
107
112
@@ -128,6 +133,10 @@ def __init__(self, debug_config=None):
128
133
self .scratchpad = comms .ScratchPad (self )
129
134
"""The ScratchPad object to talk to the debuggee."""
130
135
136
+ self .start_command = None
137
+ """Set to either "launch" or "attach" just before the corresponding request is sent.
138
+ """
139
+
131
140
self .start_request = None
132
141
"""The "launch" or "attach" request that started executing code in this session.
133
142
"""
@@ -183,6 +192,7 @@ def __init__(self, debug_config=None):
183
192
timeline .Event ("module" ),
184
193
timeline .Event ("continued" ),
185
194
timeline .Event ("debugpyWaitingForServer" ),
195
+ timeline .Event ("debugpySockets" ),
186
196
timeline .Event ("thread" , some .dict .containing ({"reason" : "started" })),
187
197
timeline .Event ("thread" , some .dict .containing ({"reason" : "exited" })),
188
198
timeline .Event ("output" , some .dict .containing ({"category" : "stdout" })),
@@ -296,6 +306,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
296
306
@property
297
307
def ignore_unobserved (self ):
298
308
return self .timeline .ignore_unobserved
309
+
310
+ @property
311
+ def is_subprocess (self ):
312
+ return "subProcessId" in self .config
299
313
300
314
def open_backchannel (self ):
301
315
assert self .backchannel is None
@@ -352,7 +366,9 @@ def _make_env(self, base_env, codecov=True):
352
366
return env
353
367
354
368
def _make_python_cmdline (self , exe , * args ):
355
- return [str (s .strpath if isinstance (s , py .path .local ) else s ) for s in [exe , * args ]]
369
+ return [
370
+ str (s .strpath if isinstance (s , py .path .local ) else s ) for s in [exe , * args ]
371
+ ]
356
372
357
373
def spawn_debuggee (self , args , cwd = None , exe = sys .executable , setup = None ):
358
374
assert self .debuggee is None
@@ -406,7 +422,9 @@ def spawn_adapter(self, args=()):
406
422
assert self .adapter is None
407
423
assert self .channel is None
408
424
409
- args = self ._make_python_cmdline (sys .executable , os .path .dirname (debugpy .adapter .__file__ ), * args )
425
+ args = self ._make_python_cmdline (
426
+ sys .executable , os .path .dirname (debugpy .adapter .__file__ ), * args
427
+ )
410
428
env = self ._make_env (self .spawn_adapter .env )
411
429
412
430
log .info (
@@ -430,12 +448,22 @@ def spawn_adapter(self, args=()):
430
448
stream = messaging .JsonIOStream .from_process (self .adapter , name = self .adapter_id )
431
449
self ._start_channel (stream )
432
450
451
+ def expect_server_socket (self , port = some .int ):
452
+ self .expected_adapter_sockets ["server" ] = {
453
+ "host" : some .str ,
454
+ "port" : port ,
455
+ "internal" : True ,
456
+ }
457
+
433
458
def connect_to_adapter (self , address ):
434
459
assert self .channel is None
435
460
436
461
self .before_connect (address )
437
462
host , port = address
438
463
log .info ("Connecting to {0} at {1}:{2}" , self .adapter_id , host , port )
464
+
465
+ self .expected_adapter_sockets ["client" ]["port" ] = port
466
+
439
467
sock = sockets .create_client ()
440
468
sock .connect (address )
441
469
@@ -470,8 +498,12 @@ def send_request(self, command, arguments=None, proceed=True):
470
498
if self .timeline .is_frozen and proceed :
471
499
self .proceed ()
472
500
501
+ if command in ("launch" , "attach" ):
502
+ self .start_command = command
503
+
473
504
message = self .channel .send_request (command , arguments )
474
505
request = self .timeline .record_request (message )
506
+
475
507
if command in ("launch" , "attach" ):
476
508
self .start_request = request
477
509
@@ -483,16 +515,52 @@ def send_request(self, command, arguments=None, proceed=True):
483
515
484
516
def _process_event (self , event ):
485
517
occ = self .timeline .record_event (event , block = False )
518
+
486
519
if event .event == "exited" :
487
520
self .observe (occ )
488
521
self .exit_code = event ("exitCode" , int )
489
522
self .exit_reason = event ("reason" , str , optional = True )
490
523
assert self .exit_code == self .expected_exit_code
524
+
525
+ elif event .event == "terminated" :
526
+ # Server socket should be closed next.
527
+ self .expected_adapter_sockets .pop ("server" , None )
528
+
491
529
elif event .event == "debugpyAttach" :
492
530
self .observe (occ )
493
531
pid = event ("subProcessId" , int )
494
532
watchdog .register_spawn (pid , f"{ self .debuggee_id } -subprocess-{ pid } " )
495
533
534
+ elif event .event == "debugpySockets" :
535
+ assert not self .is_subprocess
536
+ sockets = list (event ("sockets" , json .array (json .object ())))
537
+ for purpose , expected_socket in self .expected_adapter_sockets .items ():
538
+ if expected_socket is None :
539
+ continue
540
+ socket = None
541
+ for socket in sockets :
542
+ if socket == expected_socket :
543
+ break
544
+ assert (
545
+ socket is not None
546
+ ), f"Expected { purpose } socket { expected_socket } not reported by adapter"
547
+ sockets .remove (socket )
548
+ assert not sockets , f"Unexpected sockets reported by adapter: { sockets } "
549
+
550
+ if self .start_command == "launch" :
551
+ if "launcher" in self .expected_adapter_sockets :
552
+ # If adapter has just reported the launcher socket, it shouldn't be
553
+ # reported thereafter.
554
+ self .expected_adapter_sockets ["launcher" ] = None
555
+ elif "server" in self .expected_adapter_sockets :
556
+ # If adapter just reported the server socket, the next event should
557
+ # report the launcher socket.
558
+ self .expected_adapter_sockets ["launcher" ] = {
559
+ "host" : some .str ,
560
+ "port" : some .int ,
561
+ "internal" : False ,
562
+ }
563
+
496
564
def run_in_terminal (self , args , cwd , env ):
497
565
exe = args .pop (0 )
498
566
self .spawn_debuggee .env .update (env )
@@ -514,10 +582,12 @@ def _process_request(self, request):
514
582
except Exception as exc :
515
583
log .swallow_exception ('"runInTerminal" failed:' )
516
584
raise request .cant_handle (str (exc ))
585
+
517
586
elif request .command == "startDebugging" :
518
587
pid = request ("configuration" , dict )("subProcessId" , int )
519
588
watchdog .register_spawn (pid , f"{ self .debuggee_id } -subprocess-{ pid } " )
520
589
return {}
590
+
521
591
else :
522
592
raise request .isnt_valid ("not supported" )
523
593
@@ -567,6 +637,9 @@ def _start_channel(self, stream):
567
637
)
568
638
)
569
639
640
+ if not self .is_subprocess :
641
+ self .wait_for_next (timeline .Event ("debugpySockets" ))
642
+
570
643
self .request ("initialize" , self .capabilities )
571
644
572
645
def all_events (self , event , body = some .object ):
@@ -632,9 +705,20 @@ def request_launch(self):
632
705
# If specified, launcher will use it in lieu of PYTHONPATH it inherited
633
706
# from the adapter when spawning debuggee, so we need to adjust again.
634
707
self .config .env .prepend_to ("PYTHONPATH" , DEBUGGEE_PYTHONPATH .strpath )
708
+
709
+ # Adapter is going to start listening for server and spawn the launcher at
710
+ # this point. Server socket gets reported first.
711
+ self .expect_server_socket ()
712
+
635
713
return self ._request_start ("launch" )
636
714
637
715
def request_attach (self ):
716
+ # In attach(listen) scenario, adapter only starts listening for server
717
+ # after receiving the "attach" request.
718
+ listen = self .config .get ("listen" , None )
719
+ if listen is not None :
720
+ assert "server" not in self .expected_adapter_sockets
721
+ self .expect_server_socket (listen ["port" ])
638
722
return self ._request_start ("attach" )
639
723
640
724
def request_continue (self ):
@@ -787,7 +871,9 @@ def wait_for_stop(
787
871
return StopInfo (stopped , frames , tid , fid )
788
872
789
873
def wait_for_next_subprocess (self ):
790
- message = self .timeline .wait_for_next (timeline .Event ("debugpyAttach" ) | timeline .Request ("startDebugging" ))
874
+ message = self .timeline .wait_for_next (
875
+ timeline .Event ("debugpyAttach" ) | timeline .Request ("startDebugging" )
876
+ )
791
877
if isinstance (message , timeline .EventOccurrence ):
792
878
config = message .body
793
879
assert "request" in config
0 commit comments