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