@@ -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
@@ -183,6 +188,7 @@ def __init__(self, debug_config=None):
183
188
timeline .Event ("module" ),
184
189
timeline .Event ("continued" ),
185
190
timeline .Event ("debugpyWaitingForServer" ),
191
+ timeline .Event ("debugpySockets" ),
186
192
timeline .Event ("thread" , some .dict .containing ({"reason" : "started" })),
187
193
timeline .Event ("thread" , some .dict .containing ({"reason" : "exited" })),
188
194
timeline .Event ("output" , some .dict .containing ({"category" : "stdout" })),
@@ -296,6 +302,10 @@ def __exit__(self, exc_type, exc_val, exc_tb):
296
302
@property
297
303
def ignore_unobserved (self ):
298
304
return self .timeline .ignore_unobserved
305
+
306
+ @property
307
+ def is_subprocess (self ):
308
+ return "subProcessId" in self .config
299
309
300
310
def open_backchannel (self ):
301
311
assert self .backchannel is None
@@ -352,7 +362,9 @@ def _make_env(self, base_env, codecov=True):
352
362
return env
353
363
354
364
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 ]]
365
+ return [
366
+ str (s .strpath if isinstance (s , py .path .local ) else s ) for s in [exe , * args ]
367
+ ]
356
368
357
369
def spawn_debuggee (self , args , cwd = None , exe = sys .executable , setup = None ):
358
370
assert self .debuggee is None
@@ -406,7 +418,9 @@ def spawn_adapter(self, args=()):
406
418
assert self .adapter is None
407
419
assert self .channel is None
408
420
409
- args = self ._make_python_cmdline (sys .executable , os .path .dirname (debugpy .adapter .__file__ ), * args )
421
+ args = self ._make_python_cmdline (
422
+ sys .executable , os .path .dirname (debugpy .adapter .__file__ ), * args
423
+ )
410
424
env = self ._make_env (self .spawn_adapter .env )
411
425
412
426
log .info (
@@ -430,12 +444,22 @@ def spawn_adapter(self, args=()):
430
444
stream = messaging .JsonIOStream .from_process (self .adapter , name = self .adapter_id )
431
445
self ._start_channel (stream )
432
446
447
+ def expect_server_socket (self , port = some .int ):
448
+ self .expected_adapter_sockets ["server" ] = {
449
+ "host" : some .str ,
450
+ "port" : port ,
451
+ "internal" : True ,
452
+ }
453
+
433
454
def connect_to_adapter (self , address ):
434
455
assert self .channel is None
435
456
436
457
self .before_connect (address )
437
458
host , port = address
438
459
log .info ("Connecting to {0} at {1}:{2}" , self .adapter_id , host , port )
460
+
461
+ self .expected_adapter_sockets ["client" ]["port" ] = port
462
+
439
463
sock = sockets .create_client ()
440
464
sock .connect (address )
441
465
@@ -483,16 +507,55 @@ def send_request(self, command, arguments=None, proceed=True):
483
507
484
508
def _process_event (self , event ):
485
509
occ = self .timeline .record_event (event , block = False )
510
+
486
511
if event .event == "exited" :
487
512
self .observe (occ )
488
513
self .exit_code = event ("exitCode" , int )
489
514
self .exit_reason = event ("reason" , str , optional = True )
490
515
assert self .exit_code == self .expected_exit_code
516
+
517
+ elif event .event == "terminated" :
518
+ # Server socket should be closed next.
519
+ self .expected_adapter_sockets .pop ("server" , None )
520
+
491
521
elif event .event == "debugpyAttach" :
492
522
self .observe (occ )
493
523
pid = event ("subProcessId" , int )
494
524
watchdog .register_spawn (pid , f"{ self .debuggee_id } -subprocess-{ pid } " )
495
525
526
+ elif event .event == "debugpySockets" :
527
+ assert not self .is_subprocess
528
+ sockets = list (event ("sockets" , json .array (json .object ())))
529
+ for purpose , expected_socket in self .expected_adapter_sockets .items ():
530
+ if expected_socket is None :
531
+ continue
532
+ socket = None
533
+ for socket in sockets :
534
+ if socket == expected_socket :
535
+ break
536
+ assert (
537
+ socket is not None
538
+ ), f"Expected { purpose } socket { expected_socket } not reported by adapter"
539
+ sockets .remove (socket )
540
+ assert not sockets , f"Unexpected sockets reported by adapter: { sockets } "
541
+
542
+ if (
543
+ self .start_request is not None
544
+ and self .start_request .command == "launch"
545
+ ):
546
+ if "launcher" in self .expected_adapter_sockets :
547
+ # If adapter has just reported the launcher socket, it shouldn't be
548
+ # reported thereafter.
549
+ self .expected_adapter_sockets ["launcher" ] = None
550
+ elif "server" in self .expected_adapter_sockets :
551
+ # If adapter just reported the server socket, the next event should
552
+ # report the launcher socket.
553
+ self .expected_adapter_sockets ["launcher" ] = {
554
+ "host" : some .str ,
555
+ "port" : some .int ,
556
+ "internal" : False ,
557
+ }
558
+
496
559
def run_in_terminal (self , args , cwd , env ):
497
560
exe = args .pop (0 )
498
561
self .spawn_debuggee .env .update (env )
@@ -514,10 +577,12 @@ def _process_request(self, request):
514
577
except Exception as exc :
515
578
log .swallow_exception ('"runInTerminal" failed:' )
516
579
raise request .cant_handle (str (exc ))
580
+
517
581
elif request .command == "startDebugging" :
518
582
pid = request ("configuration" , dict )("subProcessId" , int )
519
583
watchdog .register_spawn (pid , f"{ self .debuggee_id } -subprocess-{ pid } " )
520
584
return {}
585
+
521
586
else :
522
587
raise request .isnt_valid ("not supported" )
523
588
@@ -567,6 +632,9 @@ def _start_channel(self, stream):
567
632
)
568
633
)
569
634
635
+ if not self .is_subprocess :
636
+ self .wait_for_next (timeline .Event ("debugpySockets" ))
637
+
570
638
self .request ("initialize" , self .capabilities )
571
639
572
640
def all_events (self , event , body = some .object ):
@@ -632,9 +700,20 @@ def request_launch(self):
632
700
# If specified, launcher will use it in lieu of PYTHONPATH it inherited
633
701
# from the adapter when spawning debuggee, so we need to adjust again.
634
702
self .config .env .prepend_to ("PYTHONPATH" , DEBUGGEE_PYTHONPATH .strpath )
703
+
704
+ # Adapter is going to start listening for server and spawn the launcher at
705
+ # this point. Server socket gets reported first.
706
+ self .expect_server_socket ()
707
+
635
708
return self ._request_start ("launch" )
636
709
637
710
def request_attach (self ):
711
+ # In attach(listen) scenario, adapter only starts listening for server
712
+ # after receiving the "attach" request.
713
+ listen = self .config .get ("listen" , None )
714
+ if listen is not None :
715
+ assert "server" not in self .expected_adapter_sockets
716
+ self .expect_server_socket (listen ["port" ])
638
717
return self ._request_start ("attach" )
639
718
640
719
def request_continue (self ):
@@ -787,7 +866,9 @@ def wait_for_stop(
787
866
return StopInfo (stopped , frames , tid , fid )
788
867
789
868
def wait_for_next_subprocess (self ):
790
- message = self .timeline .wait_for_next (timeline .Event ("debugpyAttach" ) | timeline .Request ("startDebugging" ))
869
+ message = self .timeline .wait_for_next (
870
+ timeline .Event ("debugpyAttach" ) | timeline .Request ("startDebugging" )
871
+ )
791
872
if isinstance (message , timeline .EventOccurrence ):
792
873
config = message .body
793
874
assert "request" in config
0 commit comments