@@ -219,6 +219,10 @@ def __init__(self, context, *args):
219
219
self .file_to_bp : dict [str , list [int ]] = defaultdict (list )
220
220
# { dex_breakpoint_id -> (file, line, condition) }
221
221
self .bp_info : dict [int , (str , int , str )] = {}
222
+ # { dex_breakpoint_id -> function_name }
223
+ self .function_bp_info : dict [int , str ] = {}
224
+ # { dex_breakpoint_id -> instruction_reference }
225
+ self .instruction_bp_info : dict [int , str ] = {}
222
226
# We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and
223
227
# maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints
224
228
# in the debug adapter itself until necessary.
@@ -227,6 +231,8 @@ def __init__(self, context, *args):
227
231
self .dex_id_to_dap_id : dict [int , int ] = {}
228
232
self .dap_id_to_dex_ids : dict [int , list [int ]] = {}
229
233
self .pending_breakpoints : bool = False
234
+ self .pending_function_breakpoints : bool = False
235
+ self .pending_instruction_breakpoints : bool = False
230
236
# List of breakpoints, indexed by BP ID
231
237
# Each entry has the source file (for use in referencing desired_bps), and the DA-assigned
232
238
# ID for that breakpoint if it has one (if it has been removed or not yet created then it will be None).
@@ -289,6 +295,26 @@ def make_set_breakpoint_request(source: str, bps: list[BreakpointRequest]) -> di
289
295
{"source" : {"path" : source }, "breakpoints" : [bp .toDict () for bp in bps ]},
290
296
)
291
297
298
+ @staticmethod
299
+ def make_set_function_breakpoint_request (function_names : list [str ]) -> dict :
300
+ # Function breakpoints may specify conditions and hit counts, though we
301
+ # don't use those here (though perhaps we should use native hit count,
302
+ # rather than emulating it ConditionalController, now that we have a
303
+ # shared interface (DAP)).
304
+ return DAP .make_request (
305
+ "setFunctionBreakpoints" ,
306
+ {"breakpoints" : [{"name" : f } for f in function_names ]},
307
+ )
308
+
309
+ @staticmethod
310
+ def make_set_instruction_breakpoint_request (addrs : list [str ]) -> dict :
311
+ # Instruction breakpoints have additional fields we're ignoring for the
312
+ # moment.
313
+ return DAP .make_request (
314
+ "setInstructionBreakpoints" ,
315
+ {"breakpoints" : [{"instructionReference" : a } for a in addrs ]},
316
+ )
317
+
292
318
############################################################################
293
319
## DAP communication & state-handling functions
294
320
@@ -575,45 +601,98 @@ def clear_breakpoints(self):
575
601
def _add_breakpoint (self , file , line ):
576
602
return self ._add_conditional_breakpoint (file , line , None )
577
603
604
+ def add_function_breakpoint (self , name : str ):
605
+ if not self ._debugger_state .capabilities .supportsFunctionBreakpoints :
606
+ raise DebuggerException ("Debugger does not support function breakpoints" )
607
+ new_id = self .get_next_bp_id ()
608
+ self .function_bp_info [new_id ] = name
609
+ self .pending_function_breakpoints = True
610
+ return new_id
611
+
612
+ def add_instruction_breakpoint (self , addr : str ):
613
+ if not self ._debugger_state .capabilities .supportsInstructionBreakpoints :
614
+ raise DebuggerException ("Debugger does not support instruction breakpoints" )
615
+ new_id = self .get_next_bp_id ()
616
+ self .instruction_bp_info [new_id ] = addr
617
+ self .pending_instruction_breakpoints = True
618
+ return new_id
619
+
578
620
def _add_conditional_breakpoint (self , file , line , condition ):
579
621
new_id = self .get_next_bp_id ()
580
622
self .file_to_bp [file ].append (new_id )
581
623
self .bp_info [new_id ] = (file , line , condition )
582
624
self .pending_breakpoints = True
583
625
return new_id
584
626
627
+ def _update_breakpoint_ids_after_request (
628
+ self , dex_bp_ids : list [int ], response : dict
629
+ ):
630
+ dap_bp_ids = [bp ["id" ] for bp in response ["body" ]["breakpoints" ]]
631
+ if len (dex_bp_ids ) != len (dap_bp_ids ):
632
+ self .context .logger .error (
633
+ f"Sent request to set { len (dex_bp_ids )} breakpoints, but received { len (dap_bp_ids )} in response."
634
+ )
635
+ visited_dap_ids = set ()
636
+ for i , dex_bp_id in enumerate (dex_bp_ids ):
637
+ dap_bp_id = dap_bp_ids [i ]
638
+ self .dex_id_to_dap_id [dex_bp_id ] = dap_bp_id
639
+ # We take the mappings in the response as the canonical mapping, meaning that if the debug server has
640
+ # simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
641
+ # it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
642
+ if dap_bp_id in visited_dap_ids :
643
+ self .dap_id_to_dex_ids [dap_bp_id ].append (dex_bp_id )
644
+ else :
645
+ self .dap_id_to_dex_ids [dap_bp_id ] = [dex_bp_id ]
646
+ visited_dap_ids .add (dap_bp_id )
647
+
585
648
def _flush_breakpoints (self ):
586
- if not self .pending_breakpoints :
587
- return
588
- for file in self .file_to_bp .keys ():
589
- desired_bps = self ._get_desired_bps (file )
649
+ # Normal and conditional breakpoints.
650
+ if self .pending_breakpoints :
651
+ self .pending_breakpoints = False
652
+ for file in self .file_to_bp .keys ():
653
+ desired_bps = self ._get_desired_bps (file )
654
+ request_id = self .send_message (
655
+ self .make_set_breakpoint_request (file , desired_bps )
656
+ )
657
+ result = self ._await_response (request_id , 10 )
658
+ if not result ["success" ]:
659
+ raise DebuggerException (f"could not set breakpoints for '{ file } '" )
660
+ # The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
661
+ # handle them so that our internal bookkeeping is correct.
662
+ dex_bp_ids = self .get_current_bps (file )
663
+ self ._update_breakpoint_ids_after_request (dex_bp_ids , result )
664
+
665
+ # Funciton breakpoints.
666
+ if self .pending_function_breakpoints :
667
+ self .pending_function_breakpoints = False
668
+ desired_bps = list (self .function_bp_info .values ())
590
669
request_id = self .send_message (
591
- self .make_set_breakpoint_request ( file , desired_bps )
670
+ self .make_set_function_breakpoint_request ( desired_bps )
592
671
)
593
672
result = self ._await_response (request_id , 10 )
594
673
if not result ["success" ]:
595
- raise DebuggerException (f"could not set breakpoints for '{ file } '" )
596
- # The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
597
- # handle them so that our internal bookkeeping is correct.
598
- dex_bp_ids = self .get_current_bps (file )
599
- dap_bp_ids = [bp ["id" ] for bp in result ["body" ]["breakpoints" ]]
600
- if len (dex_bp_ids ) != len (dap_bp_ids ):
601
- self .context .logger .error (
602
- f"Sent request to set { len (dex_bp_ids )} breakpoints, but received { len (dap_bp_ids )} in response."
674
+ raise DebuggerException (
675
+ f"could not set function breakpoints: '{ desired_bps } '"
603
676
)
604
- visited_dap_ids = set ()
605
- for i , dex_bp_id in enumerate (dex_bp_ids ):
606
- dap_bp_id = dap_bp_ids [i ]
607
- self .dex_id_to_dap_id [dex_bp_id ] = dap_bp_id
608
- # We take the mappings in the response as the canonical mapping, meaning that if the debug server has
609
- # simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
610
- # it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
611
- if dap_bp_id in visited_dap_ids :
612
- self .dap_id_to_dex_ids [dap_bp_id ].append (dex_bp_id )
613
- else :
614
- self .dap_id_to_dex_ids [dap_bp_id ] = [dex_bp_id ]
615
- visited_dap_ids .add (dap_bp_id )
616
- self .pending_breakpoints = False
677
+ # Is this right? Are we guarenteed the order of the outgoing/incoming lists?
678
+ dex_bp_ids = list (self .function_bp_info .keys ())
679
+ self ._update_breakpoint_ids_after_request (dex_bp_ids , result )
680
+
681
+ # Address / instruction breakpoints.
682
+ if self .pending_instruction_breakpoints :
683
+ self .pending_instruction_breakpoints = False
684
+ desired_bps = list (self .instruction_bp_info .values ())
685
+ request_id = self .send_message (
686
+ self .make_set_instruction_breakpoint_request (desired_bps )
687
+ )
688
+ result = self ._await_response (request_id , 10 )
689
+ if not result ["success" ]:
690
+ raise DebuggerException (
691
+ f"could not set instruction breakpoints: '{ desired_bps } '"
692
+ )
693
+ # Is this right? Are we guarenteed the order of the outgoing/incoming lists?
694
+ dex_bp_ids = list (self .instruction_bp_info .keys ())
695
+ self ._update_breakpoint_ids_after_request (dex_bp_ids , result )
617
696
618
697
def _confirm_triggered_breakpoint_ids (self , dex_bp_ids ):
619
698
"""Can be overridden for any specific implementations that need further processing from the debug server's
@@ -638,8 +717,16 @@ def get_triggered_breakpoint_ids(self):
638
717
def delete_breakpoints (self , ids ):
639
718
per_file_deletions : dict [str , list [int ]] = defaultdict (list )
640
719
for dex_bp_id in ids :
641
- source , _ , _ = self .bp_info [dex_bp_id ]
642
- per_file_deletions [source ].append (dex_bp_id )
720
+ if dex_bp_id in self .bp_info :
721
+ source , _ , _ = self .bp_info [dex_bp_id ]
722
+ per_file_deletions [source ].append (dex_bp_id )
723
+ elif dex_bp_id in self .function_bp_info :
724
+ del self .function_bp_info [dex_bp_id ]
725
+ self .pending_function_breakpoints = True
726
+ elif dex_bp_id in self .instruction_bp_info :
727
+ del self .instruction_bp_info [dex_bp_id ]
728
+ self .pending_instruction_breakpoints = True
729
+
643
730
for file , deleted_ids in per_file_deletions .items ():
644
731
old_len = len (self .file_to_bp [file ])
645
732
self .file_to_bp [file ] = [
@@ -657,7 +744,13 @@ def _get_launch_params(self, cmdline):
657
744
""" "Set the debugger-specific params used in a launch request."""
658
745
659
746
def launch (self , cmdline ):
660
- assert len (self .file_to_bp .keys ()) > 0
747
+ # FIXME: This should probably not a warning, not an assert.
748
+ assert (
749
+ len (self .file_to_bp )
750
+ + len (self .function_bp_info )
751
+ + len (self .instruction_bp_info )
752
+ > 0
753
+ ), "Expected at least one breakpoint before launching"
661
754
662
755
if self .context .options .target_run_args :
663
756
cmdline += shlex .split (self .context .options .target_run_args )
0 commit comments