@@ -185,6 +185,10 @@ def __init__(self, context, *args):
185
185
self .file_to_bp = defaultdict (list )
186
186
# { dex_breakpoint_id -> (file, line, condition) }
187
187
self .bp_info = {}
188
+ # { dex_breakpoint_id -> function_name }
189
+ self .function_bp_info = {}
190
+ # { dex_breakpoint_id -> instruction_reference }
191
+ self .instruction_bp_info = {}
188
192
# We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and
189
193
# maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints
190
194
# in the debug adapter itself until necessary.
@@ -193,6 +197,8 @@ def __init__(self, context, *args):
193
197
self .dex_id_to_dap_id = {}
194
198
self .dap_id_to_dex_ids = {}
195
199
self .pending_breakpoints : bool = False
200
+ self .pending_function_breakpoints : bool = False
201
+ self .pending_instruction_breakpoints : bool = False
196
202
# List of breakpoints, indexed by BP ID
197
203
# Each entry has the source file (for use in referencing desired_bps), and the DA-assigned
198
204
# ID for that breakpoint if it has one (if it has been removed or not yet created then it will be None).
@@ -255,6 +261,26 @@ def make_set_breakpoint_request(source: str, bps) -> dict:
255
261
{"source" : {"path" : source }, "breakpoints" : [bp .toDict () for bp in bps ]},
256
262
)
257
263
264
+ @staticmethod
265
+ def make_set_function_breakpoint_request (function_names : list [str ]) -> dict :
266
+ # Function breakpoints may specify conditions and hit counts, though we
267
+ # don't use those here (though perhaps we should use native hit count,
268
+ # rather than emulating it ConditionalController, now that we have a
269
+ # shared interface (DAP)).
270
+ return DAP .make_request (
271
+ "setFunctionBreakpoints" ,
272
+ {"breakpoints" : [{"name" : f } for f in function_names ]},
273
+ )
274
+
275
+ @staticmethod
276
+ def make_set_instruction_breakpoint_request (addrs : list [str ]) -> dict :
277
+ # Instruction breakpoints have additional fields we're ignoring for the
278
+ # moment.
279
+ return DAP .make_request (
280
+ "setInstructionBreakpoints" ,
281
+ {"breakpoints" : [{"instructionReference" : a } for a in addrs ]},
282
+ )
283
+
258
284
############################################################################
259
285
## DAP communication & state-handling functions
260
286
@@ -524,45 +550,98 @@ def clear_breakpoints(self):
524
550
def _add_breakpoint (self , file , line ):
525
551
return self ._add_conditional_breakpoint (file , line , None )
526
552
553
+ def add_function_breakpoint (self , name : str ):
554
+ if not self ._debugger_state .capabilities .supportsFunctionBreakpoints :
555
+ raise DebuggerException ("Debugger does not support function breakpoints" )
556
+ new_id = self .get_next_bp_id ()
557
+ self .function_bp_info [new_id ] = name
558
+ self .pending_function_breakpoints = True
559
+ return new_id
560
+
561
+ def add_instruction_breakpoint (self , addr : str ):
562
+ if not self ._debugger_state .capabilities .supportsInstructionBreakpoints :
563
+ raise DebuggerException ("Debugger does not support instruction breakpoints" )
564
+ new_id = self .get_next_bp_id ()
565
+ self .instruction_bp_info [new_id ] = addr
566
+ self .pending_instruction_breakpoints = True
567
+ return new_id
568
+
527
569
def _add_conditional_breakpoint (self , file , line , condition ):
528
570
new_id = self .get_next_bp_id ()
529
571
self .file_to_bp [file ].append (new_id )
530
572
self .bp_info [new_id ] = (file , line , condition )
531
573
self .pending_breakpoints = True
532
574
return new_id
533
575
576
+ def _update_breakpoint_ids_after_request (
577
+ self , dex_bp_ids : list [int ], response : dict
578
+ ):
579
+ dap_bp_ids = [bp ["id" ] for bp in response ["body" ]["breakpoints" ]]
580
+ if len (dex_bp_ids ) != len (dap_bp_ids ):
581
+ self .context .logger .error (
582
+ f"Sent request to set { len (dex_bp_ids )} breakpoints, but received { len (dap_bp_ids )} in response."
583
+ )
584
+ visited_dap_ids = set ()
585
+ for i , dex_bp_id in enumerate (dex_bp_ids ):
586
+ dap_bp_id = dap_bp_ids [i ]
587
+ self .dex_id_to_dap_id [dex_bp_id ] = dap_bp_id
588
+ # We take the mappings in the response as the canonical mapping, meaning that if the debug server has
589
+ # simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
590
+ # it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
591
+ if dap_bp_id in visited_dap_ids :
592
+ self .dap_id_to_dex_ids [dap_bp_id ].append (dex_bp_id )
593
+ else :
594
+ self .dap_id_to_dex_ids [dap_bp_id ] = [dex_bp_id ]
595
+ visited_dap_ids .add (dap_bp_id )
596
+
534
597
def _flush_breakpoints (self ):
535
- if not self .pending_breakpoints :
536
- return
537
- for file in self .file_to_bp .keys ():
538
- desired_bps = self ._get_desired_bps (file )
598
+ # Normal and conditional breakpoints.
599
+ if self .pending_breakpoints :
600
+ self .pending_breakpoints = False
601
+ for file in self .file_to_bp .keys ():
602
+ desired_bps = self ._get_desired_bps (file )
603
+ request_id = self .send_message (
604
+ self .make_set_breakpoint_request (file , desired_bps )
605
+ )
606
+ result = self ._await_response (request_id , 10 )
607
+ if not result ["success" ]:
608
+ raise DebuggerException (f"could not set breakpoints for '{ file } '" )
609
+ # The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
610
+ # handle them so that our internal bookkeeping is correct.
611
+ dex_bp_ids = self .get_current_bps (file )
612
+ self ._update_breakpoint_ids_after_request (dex_bp_ids , result )
613
+
614
+ # Funciton breakpoints.
615
+ if self .pending_function_breakpoints :
616
+ self .pending_function_breakpoints = False
617
+ desired_bps = list (self .function_bp_info .values ())
539
618
request_id = self .send_message (
540
- self .make_set_breakpoint_request ( file , desired_bps )
619
+ self .make_set_function_breakpoint_request ( desired_bps )
541
620
)
542
621
result = self ._await_response (request_id , 10 )
543
622
if not result ["success" ]:
544
- raise DebuggerException (f"could not set breakpoints for '{ file } '" )
545
- # The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and
546
- # handle them so that our internal bookkeeping is correct.
547
- dex_bp_ids = self .get_current_bps (file )
548
- dap_bp_ids = [bp ["id" ] for bp in result ["body" ]["breakpoints" ]]
549
- if len (dex_bp_ids ) != len (dap_bp_ids ):
550
- self .context .logger .error (
551
- f"Sent request to set { len (dex_bp_ids )} breakpoints, but received { len (dap_bp_ids )} in response."
623
+ raise DebuggerException (
624
+ f"could not set function breakpoints: '{ desired_bps } '"
552
625
)
553
- visited_dap_ids = set ()
554
- for i , dex_bp_id in enumerate (dex_bp_ids ):
555
- dap_bp_id = dap_bp_ids [i ]
556
- self .dex_id_to_dap_id [dex_bp_id ] = dap_bp_id
557
- # We take the mappings in the response as the canonical mapping, meaning that if the debug server has
558
- # simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to
559
- # it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping.
560
- if dap_bp_id in visited_dap_ids :
561
- self .dap_id_to_dex_ids [dap_bp_id ].append (dex_bp_id )
562
- else :
563
- self .dap_id_to_dex_ids [dap_bp_id ] = [dex_bp_id ]
564
- visited_dap_ids .add (dap_bp_id )
565
- self .pending_breakpoints = False
626
+ # Is this right? Are we guarenteed the order of the outgoing/incoming lists?
627
+ dex_bp_ids = list (self .function_bp_info .keys ())
628
+ self ._update_breakpoint_ids_after_request (dex_bp_ids , result )
629
+
630
+ # Address / instruction breakpoints.
631
+ if self .pending_instruction_breakpoints :
632
+ self .pending_instruction_breakpoints = False
633
+ desired_bps = list (self .instruction_bp_info .values ())
634
+ request_id = self .send_message (
635
+ self .make_set_instruction_breakpoint_request (desired_bps )
636
+ )
637
+ result = self ._await_response (request_id , 10 )
638
+ if not result ["success" ]:
639
+ raise DebuggerException (
640
+ f"could not set instruction breakpoints: '{ desired_bps } '"
641
+ )
642
+ # Is this right? Are we guarenteed the order of the outgoing/incoming lists?
643
+ dex_bp_ids = list (self .instruction_bp_info .keys ())
644
+ self ._update_breakpoint_ids_after_request (dex_bp_ids , result )
566
645
567
646
def _confirm_triggered_breakpoint_ids (self , dex_bp_ids ):
568
647
"""Can be overridden for any specific implementations that need further processing from the debug server's
@@ -587,8 +666,16 @@ def get_triggered_breakpoint_ids(self):
587
666
def delete_breakpoints (self , ids ):
588
667
per_file_deletions = defaultdict (list )
589
668
for dex_bp_id in ids :
590
- source , _ , _ = self .bp_info [dex_bp_id ]
591
- per_file_deletions [source ].append (dex_bp_id )
669
+ if dex_bp_id in self .bp_info :
670
+ source , _ , _ = self .bp_info [dex_bp_id ]
671
+ per_file_deletions [source ].append (dex_bp_id )
672
+ elif dex_bp_id in self .function_bp_info :
673
+ del self .function_bp_info [dex_bp_id ]
674
+ self .pending_function_breakpoints = True
675
+ elif dex_bp_id in self .instruction_bp_info :
676
+ del self .instruction_bp_info [dex_bp_id ]
677
+ self .pending_instruction_breakpoints = True
678
+
592
679
for file , deleted_ids in per_file_deletions .items ():
593
680
old_len = len (self .file_to_bp [file ])
594
681
self .file_to_bp [file ] = [
@@ -606,7 +693,13 @@ def _get_launch_params(self, cmdline):
606
693
""" "Set the debugger-specific params used in a launch request."""
607
694
608
695
def launch (self , cmdline ):
609
- assert len (self .file_to_bp .keys ()) > 0
696
+ # FIXME: This should probably not a warning, not an assert.
697
+ assert (
698
+ len (self .file_to_bp )
699
+ + len (self .function_bp_info )
700
+ + len (self .instruction_bp_info )
701
+ > 0
702
+ ), "Expected at least one breakpoint before launching"
610
703
611
704
if self .context .options .target_run_args :
612
705
cmdline += shlex .split (self .context .options .target_run_args )
0 commit comments