@@ -218,6 +218,10 @@ def __init__(self, context, *args):
218218 self .file_to_bp = defaultdict (list )
219219 # { dex_breakpoint_id -> (file, line, condition) }
220220 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 = {}
221225 # We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and
222226 # maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints
223227 # in the debug adapter itself until necessary.
@@ -226,6 +230,8 @@ def __init__(self, context, *args):
226230 self .dex_id_to_dap_id = {}
227231 self .dap_id_to_dex_ids = {}
228232 self .pending_breakpoints : bool = False
233+ self .pending_function_breakpoints : bool = False
234+ self .pending_instruction_breakpoints : bool = False
229235 # List of breakpoints, indexed by BP ID
230236 # Each entry has the source file (for use in referencing desired_bps), and the DA-assigned
231237 # 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:
288294 {"source" : {"path" : source }, "breakpoints" : [bp .toDict () for bp in bps ]},
289295 )
290296
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+
291317 ############################################################################
292318 ## DAP communication & state-handling functions
293319
@@ -569,45 +595,96 @@ def clear_breakpoints(self):
569595 def _add_breakpoint (self , file , line ):
570596 return self ._add_conditional_breakpoint (file , line , None )
571597
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+
572614 def _add_conditional_breakpoint (self , file , line , condition ):
573615 new_id = self .get_next_bp_id ()
574616 self .file_to_bp [file ].append (new_id )
575617 self .bp_info [new_id ] = (file , line , condition )
576618 self .pending_breakpoints = True
577619 return new_id
578620
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+
579640 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 ())
584661 request_id = self .send_message (
585- self .make_set_breakpoint_request ( file , desired_bps )
662+ self .make_set_function_breakpoint_request ( desired_bps )
586663 )
587664 result = self ._await_response (request_id , 10 )
588665 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 } '"
597668 )
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 )
611688
612689 def _confirm_triggered_breakpoint_ids (self , dex_bp_ids ):
613690 """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):
632709 def delete_breakpoints (self , ids ):
633710 per_file_deletions = defaultdict (list )
634711 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+
637722 for file , deleted_ids in per_file_deletions .items ():
638723 old_len = len (self .file_to_bp [file ])
639724 self .file_to_bp [file ] = [
@@ -651,7 +736,13 @@ def _get_launch_params(self, cmdline):
651736 """ "Set the debugger-specific params used in a launch request."""
652737
653738 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"
655746
656747 if self .context .options .target_run_args :
657748 cmdline += shlex .split (self .context .options .target_run_args )
0 commit comments