@@ -185,6 +185,10 @@ def __init__(self, context, *args):
185185 self .file_to_bp = defaultdict (list )
186186 # { dex_breakpoint_id -> (file, line, condition) }
187187 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 = {}
188192 # We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and
189193 # maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints
190194 # in the debug adapter itself until necessary.
@@ -193,6 +197,8 @@ def __init__(self, context, *args):
193197 self .dex_id_to_dap_id = {}
194198 self .dap_id_to_dex_ids = {}
195199 self .pending_breakpoints : bool = False
200+ self .pending_function_breakpoints : bool = False
201+ self .pending_instruction_breakpoints : bool = False
196202 # List of breakpoints, indexed by BP ID
197203 # Each entry has the source file (for use in referencing desired_bps), and the DA-assigned
198204 # 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:
255261 {"source" : {"path" : source }, "breakpoints" : [bp .toDict () for bp in bps ]},
256262 )
257263
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+
258284 ############################################################################
259285 ## DAP communication & state-handling functions
260286
@@ -524,45 +550,98 @@ def clear_breakpoints(self):
524550 def _add_breakpoint (self , file , line ):
525551 return self ._add_conditional_breakpoint (file , line , None )
526552
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+
527569 def _add_conditional_breakpoint (self , file , line , condition ):
528570 new_id = self .get_next_bp_id ()
529571 self .file_to_bp [file ].append (new_id )
530572 self .bp_info [new_id ] = (file , line , condition )
531573 self .pending_breakpoints = True
532574 return new_id
533575
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+
534597 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 ())
539618 request_id = self .send_message (
540- self .make_set_breakpoint_request ( file , desired_bps )
619+ self .make_set_function_breakpoint_request ( desired_bps )
541620 )
542621 result = self ._await_response (request_id , 10 )
543622 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 } '"
552625 )
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 )
566645
567646 def _confirm_triggered_breakpoint_ids (self , dex_bp_ids ):
568647 """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):
587666 def delete_breakpoints (self , ids ):
588667 per_file_deletions = defaultdict (list )
589668 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+
592679 for file , deleted_ids in per_file_deletions .items ():
593680 old_len = len (self .file_to_bp [file ])
594681 self .file_to_bp [file ] = [
@@ -606,7 +693,13 @@ def _get_launch_params(self, cmdline):
606693 """ "Set the debugger-specific params used in a launch request."""
607694
608695 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"
610703
611704 if self .context .options .target_run_args :
612705 cmdline += shlex .split (self .context .options .target_run_args )
0 commit comments