@@ -219,6 +219,10 @@ def __init__(self, context, *args):
219219 self .file_to_bp : dict [str , list [int ]] = defaultdict (list )
220220 # { dex_breakpoint_id -> (file, line, condition) }
221221 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 ] = {}
222226 # We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and
223227 # maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints
224228 # in the debug adapter itself until necessary.
@@ -227,6 +231,8 @@ def __init__(self, context, *args):
227231 self .dex_id_to_dap_id : dict [int , int ] = {}
228232 self .dap_id_to_dex_ids : dict [int , list [int ]] = {}
229233 self .pending_breakpoints : bool = False
234+ self .pending_function_breakpoints : bool = False
235+ self .pending_instruction_breakpoints : bool = False
230236 # List of breakpoints, indexed by BP ID
231237 # Each entry has the source file (for use in referencing desired_bps), and the DA-assigned
232238 # 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
289295 {"source" : {"path" : source }, "breakpoints" : [bp .toDict () for bp in bps ]},
290296 )
291297
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+
292318 ############################################################################
293319 ## DAP communication & state-handling functions
294320
@@ -575,45 +601,98 @@ def clear_breakpoints(self):
575601 def _add_breakpoint (self , file , line ):
576602 return self ._add_conditional_breakpoint (file , line , None )
577603
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+
578620 def _add_conditional_breakpoint (self , file , line , condition ):
579621 new_id = self .get_next_bp_id ()
580622 self .file_to_bp [file ].append (new_id )
581623 self .bp_info [new_id ] = (file , line , condition )
582624 self .pending_breakpoints = True
583625 return new_id
584626
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+
585648 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 ())
590669 request_id = self .send_message (
591- self .make_set_breakpoint_request ( file , desired_bps )
670+ self .make_set_function_breakpoint_request ( desired_bps )
592671 )
593672 result = self ._await_response (request_id , 10 )
594673 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 } '"
603676 )
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 )
617696
618697 def _confirm_triggered_breakpoint_ids (self , dex_bp_ids ):
619698 """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):
638717 def delete_breakpoints (self , ids ):
639718 per_file_deletions : dict [str , list [int ]] = defaultdict (list )
640719 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+
643730 for file , deleted_ids in per_file_deletions .items ():
644731 old_len = len (self .file_to_bp [file ])
645732 self .file_to_bp [file ] = [
@@ -657,7 +744,13 @@ def _get_launch_params(self, cmdline):
657744 """ "Set the debugger-specific params used in a launch request."""
658745
659746 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"
661754
662755 if self .context .options .target_run_args :
663756 cmdline += shlex .split (self .context .options .target_run_args )
0 commit comments