-
Notifications
You must be signed in to change notification settings - Fork 15.3k
[Dexter] Add DAP instruction and function breakpoint handling #152718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -185,6 +185,10 @@ def __init__(self, context, *args): | |
| self.file_to_bp = defaultdict(list) | ||
| # { dex_breakpoint_id -> (file, line, condition) } | ||
| self.bp_info = {} | ||
| # { dex_breakpoint_id -> function_name } | ||
| self.function_bp_info = {} | ||
| # { dex_breakpoint_id -> instruction_reference } | ||
| self.instruction_bp_info = {} | ||
| # We don't rely on IDs returned directly from the debug adapter. Instead, we use dexter breakpoint IDs, and | ||
| # maintain a two-way-mapping of dex_bp_id<->dap_bp_id. This also allows us to defer the setting of breakpoints | ||
| # in the debug adapter itself until necessary. | ||
|
|
@@ -193,6 +197,8 @@ def __init__(self, context, *args): | |
| self.dex_id_to_dap_id = {} | ||
| self.dap_id_to_dex_ids = {} | ||
| self.pending_breakpoints: bool = False | ||
| self.pending_function_breakpoints: bool = False | ||
| self.pending_instruction_breakpoints: bool = False | ||
| # List of breakpoints, indexed by BP ID | ||
| # Each entry has the source file (for use in referencing desired_bps), and the DA-assigned | ||
| # 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: | |
| {"source": {"path": source}, "breakpoints": [bp.toDict() for bp in bps]}, | ||
| ) | ||
|
|
||
| @staticmethod | ||
| def make_set_function_breakpoint_request(function_names: list) -> dict: | ||
| # Function breakpoints may specify conditions and hit counts, though we | ||
| # don't use those here (though perhaps we should use native hit count, | ||
| # rather than emulating it ConditionalController, now that we have a | ||
| # shared interface (DAP)). | ||
| return DAP.make_request( | ||
| "setFunctionBreakpoints", | ||
| {"breakpoints": [{"name": f} for f in function_names]}, | ||
| ) | ||
|
|
||
| @staticmethod | ||
| def make_set_instruction_breakpoint_request(addrs: list) -> dict: | ||
| # Instruction breakpoints have additional fields we're ignoring for the | ||
| # moment. | ||
| return DAP.make_request( | ||
| "setInstructionBreakpoints", | ||
| {"breakpoints": [{"instructionReference": a} for a in addrs]}, | ||
| ) | ||
|
|
||
| ############################################################################ | ||
| ## DAP communication & state-handling functions | ||
|
|
||
|
|
@@ -524,45 +550,98 @@ def clear_breakpoints(self): | |
| def _add_breakpoint(self, file, line): | ||
| return self._add_conditional_breakpoint(file, line, None) | ||
|
|
||
| def add_function_breakpoint(self, name: str): | ||
| if not self._debugger_state.capabilities.supportsFunctionBreakpoints: | ||
| raise DebuggerException("Debugger does not support function breakpoints") | ||
| new_id = self.get_next_bp_id() | ||
| self.function_bp_info[new_id] = name | ||
| self.pending_function_breakpoints = True | ||
| return new_id | ||
|
|
||
| def add_instruction_breakpoint(self, addr: str): | ||
| if not self._debugger_state.capabilities.supportsInstructionBreakpoints: | ||
| raise DebuggerException("Debugger does not support instruction breakpoints") | ||
| new_id = self.get_next_bp_id() | ||
| self.instruction_bp_info[new_id] = addr | ||
| self.pending_instruction_breakpoints = True | ||
| return new_id | ||
|
|
||
| def _add_conditional_breakpoint(self, file, line, condition): | ||
| new_id = self.get_next_bp_id() | ||
| self.file_to_bp[file].append(new_id) | ||
| self.bp_info[new_id] = (file, line, condition) | ||
| self.pending_breakpoints = True | ||
| return new_id | ||
|
|
||
| def _update_breakpoint_ids_after_request( | ||
| self, dex_bp_ids: list, response: dict | ||
| ): | ||
| dap_bp_ids = [bp["id"] for bp in response["body"]["breakpoints"]] | ||
| if len(dex_bp_ids) != len(dap_bp_ids): | ||
| self.context.logger.error( | ||
| f"Sent request to set {len(dex_bp_ids)} breakpoints, but received {len(dap_bp_ids)} in response." | ||
| ) | ||
| visited_dap_ids = set() | ||
| for i, dex_bp_id in enumerate(dex_bp_ids): | ||
| dap_bp_id = dap_bp_ids[i] | ||
| self.dex_id_to_dap_id[dex_bp_id] = dap_bp_id | ||
| # We take the mappings in the response as the canonical mapping, meaning that if the debug server has | ||
| # simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to | ||
| # it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping. | ||
| if dap_bp_id in visited_dap_ids: | ||
| self.dap_id_to_dex_ids[dap_bp_id].append(dex_bp_id) | ||
| else: | ||
| self.dap_id_to_dex_ids[dap_bp_id] = [dex_bp_id] | ||
| visited_dap_ids.add(dap_bp_id) | ||
|
|
||
| def _flush_breakpoints(self): | ||
| if not self.pending_breakpoints: | ||
| return | ||
| for file in self.file_to_bp.keys(): | ||
| desired_bps = self._get_desired_bps(file) | ||
| # Normal and conditional breakpoints. | ||
| if self.pending_breakpoints: | ||
| self.pending_breakpoints = False | ||
| for file in self.file_to_bp.keys(): | ||
| desired_bps = self._get_desired_bps(file) | ||
| request_id = self.send_message( | ||
| self.make_set_breakpoint_request(file, desired_bps) | ||
| ) | ||
| result = self._await_response(request_id, 10) | ||
| if not result["success"]: | ||
| raise DebuggerException(f"could not set breakpoints for '{file}'") | ||
| # The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and | ||
| # handle them so that our internal bookkeeping is correct. | ||
| dex_bp_ids = self.get_current_bps(file) | ||
| self._update_breakpoint_ids_after_request(dex_bp_ids, result) | ||
|
|
||
| # Funciton breakpoints. | ||
| if self.pending_function_breakpoints: | ||
| self.pending_function_breakpoints = False | ||
| desired_bps = list(self.function_bp_info.values()) | ||
| request_id = self.send_message( | ||
| self.make_set_breakpoint_request(file, desired_bps) | ||
| self.make_set_function_breakpoint_request(desired_bps) | ||
| ) | ||
| result = self._await_response(request_id, 10) | ||
| if not result["success"]: | ||
| raise DebuggerException(f"could not set breakpoints for '{file}'") | ||
| # The debug adapter may have chosen to merge our breakpoints. From here we need to identify such cases and | ||
| # handle them so that our internal bookkeeping is correct. | ||
| dex_bp_ids = self.get_current_bps(file) | ||
| dap_bp_ids = [bp["id"] for bp in result["body"]["breakpoints"]] | ||
| if len(dex_bp_ids) != len(dap_bp_ids): | ||
| self.context.logger.error( | ||
| f"Sent request to set {len(dex_bp_ids)} breakpoints, but received {len(dap_bp_ids)} in response." | ||
| raise DebuggerException( | ||
| f"could not set function breakpoints: '{desired_bps}'" | ||
| ) | ||
| visited_dap_ids = set() | ||
| for i, dex_bp_id in enumerate(dex_bp_ids): | ||
| dap_bp_id = dap_bp_ids[i] | ||
| self.dex_id_to_dap_id[dex_bp_id] = dap_bp_id | ||
| # We take the mappings in the response as the canonical mapping, meaning that if the debug server has | ||
| # simply *changed* the DAP ID for a breakpoint we overwrite the existing mapping rather than adding to | ||
| # it, but if we receive the same DAP ID for multiple Dex IDs *then* we store a one-to-many mapping. | ||
| if dap_bp_id in visited_dap_ids: | ||
| self.dap_id_to_dex_ids[dap_bp_id].append(dex_bp_id) | ||
| else: | ||
| self.dap_id_to_dex_ids[dap_bp_id] = [dex_bp_id] | ||
| visited_dap_ids.add(dap_bp_id) | ||
| self.pending_breakpoints = False | ||
| # Is this right? Are we guarenteed the order of the outgoing/incoming lists? | ||
|
||
| dex_bp_ids = list(self.function_bp_info.keys()) | ||
| self._update_breakpoint_ids_after_request(dex_bp_ids, result) | ||
|
|
||
| # Address / instruction breakpoints. | ||
| if self.pending_instruction_breakpoints: | ||
| self.pending_instruction_breakpoints = False | ||
| desired_bps = list(self.instruction_bp_info.values()) | ||
| request_id = self.send_message( | ||
| self.make_set_instruction_breakpoint_request(desired_bps) | ||
| ) | ||
| result = self._await_response(request_id, 10) | ||
| if not result["success"]: | ||
| raise DebuggerException( | ||
| f"could not set instruction breakpoints: '{desired_bps}'" | ||
| ) | ||
| # Is this right? Are we guarenteed the order of the outgoing/incoming lists? | ||
| dex_bp_ids = list(self.instruction_bp_info.keys()) | ||
| self._update_breakpoint_ids_after_request(dex_bp_ids, result) | ||
|
|
||
| def _confirm_triggered_breakpoint_ids(self, dex_bp_ids): | ||
| """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): | |
| def delete_breakpoints(self, ids): | ||
| per_file_deletions = defaultdict(list) | ||
| for dex_bp_id in ids: | ||
| source, _, _ = self.bp_info[dex_bp_id] | ||
| per_file_deletions[source].append(dex_bp_id) | ||
| if dex_bp_id in self.bp_info: | ||
| source, _, _ = self.bp_info[dex_bp_id] | ||
| per_file_deletions[source].append(dex_bp_id) | ||
| elif dex_bp_id in self.function_bp_info: | ||
| del self.function_bp_info[dex_bp_id] | ||
| self.pending_function_breakpoints = True | ||
| elif dex_bp_id in self.instruction_bp_info: | ||
| del self.instruction_bp_info[dex_bp_id] | ||
| self.pending_instruction_breakpoints = True | ||
|
|
||
| for file, deleted_ids in per_file_deletions.items(): | ||
| old_len = len(self.file_to_bp[file]) | ||
| self.file_to_bp[file] = [ | ||
|
|
@@ -606,7 +693,13 @@ def _get_launch_params(self, cmdline): | |
| """ "Set the debugger-specific params used in a launch request.""" | ||
|
|
||
| def launch(self, cmdline): | ||
| assert len(self.file_to_bp.keys()) > 0 | ||
| # FIXME: This should probably not a warning, not an assert. | ||
|
||
| assert ( | ||
| len(self.file_to_bp) | ||
| + len(self.function_bp_info) | ||
| + len(self.instruction_bp_info) | ||
| > 0 | ||
| ), "Expected at least one breakpoint before launching" | ||
|
|
||
| if self.context.options.target_run_args: | ||
| cmdline += shlex.split(self.context.options.target_run_args) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.