983 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			983 lines
		
	
	
		
			38 KiB
		
	
	
	
		
			Python
		
	
	
	
| """Module for supporting unit testing of the lldb-server debug monitor exe.
 | |
| """
 | |
| 
 | |
| from __future__ import division, print_function
 | |
| 
 | |
| import binascii
 | |
| import os
 | |
| import os.path
 | |
| import platform
 | |
| import re
 | |
| import six
 | |
| import socket
 | |
| import subprocess
 | |
| from lldbsuite.support import seven
 | |
| from lldbsuite.test.lldbtest import *
 | |
| from lldbsuite.test import configuration
 | |
| from textwrap import dedent
 | |
| import shutil
 | |
| 
 | |
| def _get_support_exe(basename):
 | |
|     support_dir = lldb.SBHostOS.GetLLDBPath(lldb.ePathTypeSupportExecutableDir)
 | |
| 
 | |
|     return shutil.which(basename, path=support_dir.GetDirectory())
 | |
| 
 | |
| 
 | |
| def get_lldb_server_exe():
 | |
|     """Return the lldb-server exe path.
 | |
| 
 | |
|     Returns:
 | |
|         A path to the lldb-server exe if it is found to exist; otherwise,
 | |
|         returns None.
 | |
|     """
 | |
| 
 | |
|     return _get_support_exe("lldb-server")
 | |
| 
 | |
| 
 | |
| def get_debugserver_exe():
 | |
|     """Return the debugserver exe path.
 | |
| 
 | |
|     Returns:
 | |
|         A path to the debugserver exe if it is found to exist; otherwise,
 | |
|         returns None.
 | |
|     """
 | |
|     if configuration.arch and configuration.arch == "x86_64" and \
 | |
|        platform.machine().startswith("arm64"):
 | |
|         return '/Library/Apple/usr/libexec/oah/debugserver'
 | |
| 
 | |
|     return _get_support_exe("debugserver")
 | |
| 
 | |
| _LOG_LINE_REGEX = re.compile(r'^(lldb-server|debugserver)\s+<\s*(\d+)>' +
 | |
|                              '\s+(read|send)\s+packet:\s+(.+)$')
 | |
| 
 | |
| 
 | |
| def _is_packet_lldb_gdbserver_input(packet_type, llgs_input_is_read):
 | |
|     """Return whether a given packet is input for lldb-gdbserver.
 | |
| 
 | |
|     Args:
 | |
|         packet_type: a string indicating 'send' or 'receive', from a
 | |
|             gdbremote packet protocol log.
 | |
| 
 | |
|         llgs_input_is_read: true if lldb-gdbserver input (content sent to
 | |
|             lldb-gdbserver) is listed as 'read' or 'send' in the packet
 | |
|             log entry.
 | |
| 
 | |
|     Returns:
 | |
|         True if the packet should be considered input for lldb-gdbserver; False
 | |
|         otherwise.
 | |
|     """
 | |
|     if packet_type == 'read':
 | |
|         # when llgs is the read side, then a read packet is meant for
 | |
|         # input to llgs (when captured from the llgs/debugserver exe).
 | |
|         return llgs_input_is_read
 | |
|     elif packet_type == 'send':
 | |
|         # when llgs is the send side, then a send packet is meant to
 | |
|         # be input to llgs (when captured from the lldb exe).
 | |
|         return not llgs_input_is_read
 | |
|     else:
 | |
|         # don't understand what type of packet this is
 | |
|         raise "Unknown packet type: {}".format(packet_type)
 | |
| 
 | |
| 
 | |
| def handle_O_packet(context, packet_contents, logger):
 | |
|     """Handle O packets."""
 | |
|     if (not packet_contents) or (len(packet_contents) < 1):
 | |
|         return False
 | |
|     elif packet_contents[0] != "O":
 | |
|         return False
 | |
|     elif packet_contents == "OK":
 | |
|         return False
 | |
| 
 | |
|     new_text = gdbremote_hex_decode_string(packet_contents[1:])
 | |
|     context["O_content"] += new_text
 | |
|     context["O_count"] += 1
 | |
| 
 | |
|     if logger:
 | |
|         logger.debug(
 | |
|             "text: new \"{}\", cumulative: \"{}\"".format(
 | |
|                 new_text, context["O_content"]))
 | |
| 
 | |
|     return True
 | |
| 
 | |
| _STRIP_CHECKSUM_REGEX = re.compile(r'#[0-9a-fA-F]{2}$')
 | |
| _STRIP_COMMAND_PREFIX_REGEX = re.compile(r"^\$")
 | |
| _STRIP_COMMAND_PREFIX_M_REGEX = re.compile(r"^\$m")
 | |
| 
 | |
| 
 | |
| def assert_packets_equal(asserter, actual_packet, expected_packet):
 | |
|     # strip off the checksum digits of the packet.  When we're in
 | |
|     # no-ack mode, the # checksum is ignored, and should not be cause
 | |
|     # for a mismatched packet.
 | |
|     actual_stripped = _STRIP_CHECKSUM_REGEX.sub('', actual_packet)
 | |
|     expected_stripped = _STRIP_CHECKSUM_REGEX.sub('', expected_packet)
 | |
|     asserter.assertEqual(actual_stripped, expected_stripped)
 | |
| 
 | |
| 
 | |
| def expect_lldb_gdbserver_replay(
 | |
|         asserter,
 | |
|         server,
 | |
|         test_sequence,
 | |
|         timeout_seconds,
 | |
|         logger=None):
 | |
|     """Replay socket communication with lldb-gdbserver and verify responses.
 | |
| 
 | |
|     Args:
 | |
|         asserter: the object providing assertEqual(first, second, msg=None), e.g. TestCase instance.
 | |
| 
 | |
|         test_sequence: a GdbRemoteTestSequence instance that describes
 | |
|             the messages sent to the gdb remote and the responses
 | |
|             expected from it.
 | |
| 
 | |
|         timeout_seconds: any response taking more than this number of
 | |
|            seconds will cause an exception to be raised.
 | |
| 
 | |
|         logger: a Python logger instance.
 | |
| 
 | |
|     Returns:
 | |
|         The context dictionary from running the given gdbremote
 | |
|         protocol sequence.  This will contain any of the capture
 | |
|         elements specified to any GdbRemoteEntry instances in
 | |
|         test_sequence.
 | |
| 
 | |
|         The context will also contain an entry, context["O_content"]
 | |
|         which contains the text from the inferior received via $O
 | |
|         packets.  $O packets should not attempt to be matched
 | |
|         directly since they are not entirely deterministic as to
 | |
|         how many arrive and how much text is in each one.
 | |
| 
 | |
|         context["O_count"] will contain an integer of the number of
 | |
|         O packets received.
 | |
|     """
 | |
| 
 | |
|     # Ensure we have some work to do.
 | |
|     if len(test_sequence.entries) < 1:
 | |
|         return {}
 | |
| 
 | |
|     context = {"O_count": 0, "O_content": ""}
 | |
| 
 | |
|     # Grab the first sequence entry.
 | |
|     sequence_entry = test_sequence.entries.pop(0)
 | |
| 
 | |
|     # While we have an active sequence entry, send messages
 | |
|     # destined for the stub and collect/match/process responses
 | |
|     # expected from the stub.
 | |
|     while sequence_entry:
 | |
|         if sequence_entry.is_send_to_remote():
 | |
|             # This is an entry to send to the remote debug monitor.
 | |
|             send_packet = sequence_entry.get_send_packet()
 | |
|             if logger:
 | |
|                 if len(send_packet) == 1 and send_packet[0] == chr(3):
 | |
|                     packet_desc = "^C"
 | |
|                 else:
 | |
|                     packet_desc = send_packet
 | |
|                 logger.info(
 | |
|                     "sending packet to remote: {}".format(packet_desc))
 | |
|             server.send_raw(send_packet.encode())
 | |
|         else:
 | |
|             # This is an entry expecting to receive content from the remote
 | |
|             # debug monitor.
 | |
| 
 | |
|             # We'll pull from (and wait on) the queue appropriate for the type of matcher.
 | |
|             # We keep separate queues for process output (coming from non-deterministic
 | |
|             # $O packet division) and for all other packets.
 | |
|             try:
 | |
|                 if sequence_entry.is_output_matcher():
 | |
|                     # Grab next entry from the output queue.
 | |
|                     content = server.get_raw_output_packet()
 | |
|                 else:
 | |
|                     content = server.get_raw_normal_packet()
 | |
|                 content = seven.bitcast_to_string(content)
 | |
|             except socket.timeout:
 | |
|                 asserter.fail(
 | |
|                         "timed out while waiting for '{}':\n{}".format(sequence_entry, server))
 | |
| 
 | |
|             # Give the sequence entry the opportunity to match the content.
 | |
|             # Output matchers might match or pass after more output accumulates.
 | |
|             # Other packet types generally must match.
 | |
|             asserter.assertIsNotNone(content)
 | |
|             context = sequence_entry.assert_match(
 | |
|                 asserter, content, context=context)
 | |
| 
 | |
|         # Move on to next sequence entry as needed.  Some sequence entries support executing multiple
 | |
|         # times in different states (for looping over query/response
 | |
|         # packets).
 | |
|         if sequence_entry.is_consumed():
 | |
|             if len(test_sequence.entries) > 0:
 | |
|                 sequence_entry = test_sequence.entries.pop(0)
 | |
|             else:
 | |
|                 sequence_entry = None
 | |
| 
 | |
|     # Fill in the O_content entries.
 | |
|     context["O_count"] = 1
 | |
|     context["O_content"] = server.consume_accumulated_output()
 | |
| 
 | |
|     return context
 | |
| 
 | |
| 
 | |
| def gdbremote_hex_encode_string(str):
 | |
|     output = ''
 | |
|     for c in str:
 | |
|         output += '{0:02x}'.format(ord(c))
 | |
|     return output
 | |
| 
 | |
| 
 | |
| def gdbremote_hex_decode_string(str):
 | |
|     return str.decode("hex")
 | |
| 
 | |
| 
 | |
| def gdbremote_packet_encode_string(str):
 | |
|     checksum = 0
 | |
|     for c in str:
 | |
|         checksum += ord(c)
 | |
|     return '$' + str + '#{0:02x}'.format(checksum % 256)
 | |
| 
 | |
| 
 | |
| def build_gdbremote_A_packet(args_list):
 | |
|     """Given a list of args, create a properly-formed $A packet containing each arg.
 | |
|     """
 | |
|     payload = "A"
 | |
| 
 | |
|     # build the arg content
 | |
|     arg_index = 0
 | |
|     for arg in args_list:
 | |
|         # Comma-separate the args.
 | |
|         if arg_index > 0:
 | |
|             payload += ','
 | |
| 
 | |
|         # Hex-encode the arg.
 | |
|         hex_arg = gdbremote_hex_encode_string(arg)
 | |
| 
 | |
|         # Build the A entry.
 | |
|         payload += "{},{},{}".format(len(hex_arg), arg_index, hex_arg)
 | |
| 
 | |
|         # Next arg index, please.
 | |
|         arg_index += 1
 | |
| 
 | |
|     # return the packetized payload
 | |
|     return gdbremote_packet_encode_string(payload)
 | |
| 
 | |
| 
 | |
| def parse_reg_info_response(response_packet):
 | |
|     if not response_packet:
 | |
|         raise Exception("response_packet cannot be None")
 | |
| 
 | |
|     # Strip off prefix $ and suffix #xx if present.
 | |
|     response_packet = _STRIP_COMMAND_PREFIX_REGEX.sub("", response_packet)
 | |
|     response_packet = _STRIP_CHECKSUM_REGEX.sub("", response_packet)
 | |
| 
 | |
|     # Build keyval pairs
 | |
|     values = {}
 | |
|     for kv in response_packet.split(";"):
 | |
|         if len(kv) < 1:
 | |
|             continue
 | |
|         (key, val) = kv.split(':')
 | |
|         values[key] = val
 | |
| 
 | |
|     return values
 | |
| 
 | |
| 
 | |
| def parse_threadinfo_response(response_packet):
 | |
|     if not response_packet:
 | |
|         raise Exception("response_packet cannot be None")
 | |
| 
 | |
|     # Strip off prefix $ and suffix #xx if present.
 | |
|     response_packet = _STRIP_COMMAND_PREFIX_M_REGEX.sub("", response_packet)
 | |
|     response_packet = _STRIP_CHECKSUM_REGEX.sub("", response_packet)
 | |
| 
 | |
|     # Return list of thread ids
 | |
|     return [int(thread_id_hex, 16) for thread_id_hex in response_packet.split(
 | |
|         ",") if len(thread_id_hex) > 0]
 | |
| 
 | |
| 
 | |
| def unpack_endian_binary_string(endian, value_string):
 | |
|     """Unpack a gdb-remote binary (post-unescaped, i.e. not escaped) response to an unsigned int given endianness of the inferior."""
 | |
|     if not endian:
 | |
|         raise Exception("endian cannot be None")
 | |
|     if not value_string or len(value_string) < 1:
 | |
|         raise Exception("value_string cannot be None or empty")
 | |
| 
 | |
|     if endian == 'little':
 | |
|         value = 0
 | |
|         i = 0
 | |
|         while len(value_string) > 0:
 | |
|             value += (ord(value_string[0]) << i)
 | |
|             value_string = value_string[1:]
 | |
|             i += 8
 | |
|         return value
 | |
|     elif endian == 'big':
 | |
|         value = 0
 | |
|         while len(value_string) > 0:
 | |
|             value = (value << 8) + ord(value_string[0])
 | |
|             value_string = value_string[1:]
 | |
|         return value
 | |
|     else:
 | |
|         # pdp is valid but need to add parse code once needed.
 | |
|         raise Exception("unsupported endian:{}".format(endian))
 | |
| 
 | |
| 
 | |
| def unpack_register_hex_unsigned(endian, value_string):
 | |
|     """Unpack a gdb-remote $p-style response to an unsigned int given endianness of inferior."""
 | |
|     if not endian:
 | |
|         raise Exception("endian cannot be None")
 | |
|     if not value_string or len(value_string) < 1:
 | |
|         raise Exception("value_string cannot be None or empty")
 | |
| 
 | |
|     if endian == 'little':
 | |
|         value = 0
 | |
|         i = 0
 | |
|         while len(value_string) > 0:
 | |
|             value += (int(value_string[0:2], 16) << i)
 | |
|             value_string = value_string[2:]
 | |
|             i += 8
 | |
|         return value
 | |
|     elif endian == 'big':
 | |
|         return int(value_string, 16)
 | |
|     else:
 | |
|         # pdp is valid but need to add parse code once needed.
 | |
|         raise Exception("unsupported endian:{}".format(endian))
 | |
| 
 | |
| 
 | |
| def pack_register_hex(endian, value, byte_size=None):
 | |
|     """Unpack a gdb-remote $p-style response to an unsigned int given endianness of inferior."""
 | |
|     if not endian:
 | |
|         raise Exception("endian cannot be None")
 | |
| 
 | |
|     if endian == 'little':
 | |
|         # Create the litt-endian return value.
 | |
|         retval = ""
 | |
|         while value != 0:
 | |
|             retval = retval + "{:02x}".format(value & 0xff)
 | |
|             value = value >> 8
 | |
|         if byte_size:
 | |
|             # Add zero-fill to the right/end (MSB side) of the value.
 | |
|             retval += "00" * (byte_size - len(retval) // 2)
 | |
|         return retval
 | |
| 
 | |
|     elif endian == 'big':
 | |
|         retval = ""
 | |
|         while value != 0:
 | |
|             retval = "{:02x}".format(value & 0xff) + retval
 | |
|             value = value >> 8
 | |
|         if byte_size:
 | |
|             # Add zero-fill to the left/front (MSB side) of the value.
 | |
|             retval = ("00" * (byte_size - len(retval) // 2)) + retval
 | |
|         return retval
 | |
| 
 | |
|     else:
 | |
|         # pdp is valid but need to add parse code once needed.
 | |
|         raise Exception("unsupported endian:{}".format(endian))
 | |
| 
 | |
| 
 | |
| class GdbRemoteEntryBase(object):
 | |
| 
 | |
|     def is_output_matcher(self):
 | |
|         return False
 | |
| 
 | |
| 
 | |
| class GdbRemoteEntry(GdbRemoteEntryBase):
 | |
| 
 | |
|     def __init__(
 | |
|             self,
 | |
|             is_send_to_remote=True,
 | |
|             exact_payload=None,
 | |
|             regex=None,
 | |
|             capture=None,
 | |
|             expect_captures=None):
 | |
|         """Create an entry representing one piece of the I/O to/from a gdb remote debug monitor.
 | |
| 
 | |
|         Args:
 | |
| 
 | |
|             is_send_to_remote: True if this entry is a message to be
 | |
|                 sent to the gdbremote debug monitor; False if this
 | |
|                 entry represents text to be matched against the reply
 | |
|                 from the gdbremote debug monitor.
 | |
| 
 | |
|             exact_payload: if not None, then this packet is an exact
 | |
|                 send (when sending to the remote) or an exact match of
 | |
|                 the response from the gdbremote. The checksums are
 | |
|                 ignored on exact match requests since negotiation of
 | |
|                 no-ack makes the checksum content essentially
 | |
|                 undefined.
 | |
| 
 | |
|             regex: currently only valid for receives from gdbremote.
 | |
|                 When specified (and only if exact_payload is None),
 | |
|                 indicates the gdbremote response must match the given
 | |
|                 regex. Match groups in the regex can be used for two
 | |
|                 different purposes: saving the match (see capture
 | |
|                 arg), or validating that a match group matches a
 | |
|                 previously established value (see expect_captures). It
 | |
|                 is perfectly valid to have just a regex arg and to
 | |
|                 specify neither capture or expect_captures args. This
 | |
|                 arg only makes sense if exact_payload is not
 | |
|                 specified.
 | |
| 
 | |
|             capture: if specified, is a dictionary of regex match
 | |
|                 group indices (should start with 1) to variable names
 | |
|                 that will store the capture group indicated by the
 | |
|                 index. For example, {1:"thread_id"} will store capture
 | |
|                 group 1's content in the context dictionary where
 | |
|                 "thread_id" is the key and the match group value is
 | |
|                 the value. The value stored off can be used later in a
 | |
|                 expect_captures expression. This arg only makes sense
 | |
|                 when regex is specified.
 | |
| 
 | |
|             expect_captures: if specified, is a dictionary of regex
 | |
|                 match group indices (should start with 1) to variable
 | |
|                 names, where the match group should match the value
 | |
|                 existing in the context at the given variable name.
 | |
|                 For example, {2:"thread_id"} indicates that the second
 | |
|                 match group must match the value stored under the
 | |
|                 context's previously stored "thread_id" key. This arg
 | |
|                 only makes sense when regex is specified.
 | |
|         """
 | |
|         self._is_send_to_remote = is_send_to_remote
 | |
|         self.exact_payload = exact_payload
 | |
|         self.regex = regex
 | |
|         self.capture = capture
 | |
|         self.expect_captures = expect_captures
 | |
| 
 | |
|     def is_send_to_remote(self):
 | |
|         return self._is_send_to_remote
 | |
| 
 | |
|     def is_consumed(self):
 | |
|         # For now, all packets are consumed after first use.
 | |
|         return True
 | |
| 
 | |
|     def get_send_packet(self):
 | |
|         if not self.is_send_to_remote():
 | |
|             raise Exception(
 | |
|                 "get_send_packet() called on GdbRemoteEntry that is not a send-to-remote packet")
 | |
|         if not self.exact_payload:
 | |
|             raise Exception(
 | |
|                 "get_send_packet() called on GdbRemoteEntry but it doesn't have an exact payload")
 | |
|         return self.exact_payload
 | |
| 
 | |
|     def _assert_exact_payload_match(self, asserter, actual_packet):
 | |
|         assert_packets_equal(asserter, actual_packet, self.exact_payload)
 | |
|         return None
 | |
| 
 | |
|     def _assert_regex_match(self, asserter, actual_packet, context):
 | |
|         # Ensure the actual packet matches from the start of the actual packet.
 | |
|         match = self.regex.match(actual_packet)
 | |
|         if not match:
 | |
|             asserter.fail(
 | |
|                 "regex '{}' failed to match against content '{}'".format(
 | |
|                     self.regex.pattern, actual_packet))
 | |
| 
 | |
|         if self.capture:
 | |
|             # Handle captures.
 | |
|             for group_index, var_name in list(self.capture.items()):
 | |
|                 capture_text = match.group(group_index)
 | |
|                 # It is okay for capture text to be None - which it will be if it is a group that can match nothing.
 | |
|                 # The user must be okay with it since the regex itself matched
 | |
|                 # above.
 | |
|                 context[var_name] = capture_text
 | |
| 
 | |
|         if self.expect_captures:
 | |
|             # Handle comparing matched groups to context dictionary entries.
 | |
|             for group_index, var_name in list(self.expect_captures.items()):
 | |
|                 capture_text = match.group(group_index)
 | |
|                 if not capture_text:
 | |
|                     raise Exception(
 | |
|                         "No content to expect for group index {}".format(group_index))
 | |
|                 asserter.assertEqual(capture_text, context[var_name])
 | |
| 
 | |
|         return context
 | |
| 
 | |
|     def assert_match(self, asserter, actual_packet, context=None):
 | |
|         # This only makes sense for matching lines coming from the
 | |
|         # remote debug monitor.
 | |
|         if self.is_send_to_remote():
 | |
|             raise Exception(
 | |
|                 "Attempted to match a packet being sent to the remote debug monitor, doesn't make sense.")
 | |
| 
 | |
|         # Create a new context if needed.
 | |
|         if not context:
 | |
|             context = {}
 | |
| 
 | |
|         # If this is an exact payload, ensure they match exactly,
 | |
|         # ignoring the packet checksum which is optional for no-ack
 | |
|         # mode.
 | |
|         if self.exact_payload:
 | |
|             self._assert_exact_payload_match(asserter, actual_packet)
 | |
|             return context
 | |
|         elif self.regex:
 | |
|             return self._assert_regex_match(asserter, actual_packet, context)
 | |
|         else:
 | |
|             raise Exception(
 | |
|                 "Don't know how to match a remote-sent packet when exact_payload isn't specified.")
 | |
| 
 | |
| 
 | |
| class MultiResponseGdbRemoteEntry(GdbRemoteEntryBase):
 | |
|     """Represents a query/response style packet.
 | |
| 
 | |
|     Assumes the first item is sent to the gdb remote.
 | |
|     An end sequence regex indicates the end of the query/response
 | |
|     packet sequence.  All responses up through (but not including) the
 | |
|     end response are stored in a context variable.
 | |
| 
 | |
|     Settings accepted from params:
 | |
| 
 | |
|         next_query or query: required.  The typical query packet without the $ prefix or #xx suffix.
 | |
|             If there is a special first packet to start the iteration query, see the
 | |
|             first_query key.
 | |
| 
 | |
|         first_query: optional. If the first query requires a special query command, specify
 | |
|             it with this key.  Do not specify the $ prefix or #xx suffix.
 | |
| 
 | |
|         append_iteration_suffix: defaults to False.  Specify True if the 0-based iteration
 | |
|             index should be appended as a suffix to the command.  e.g. qRegisterInfo with
 | |
|             this key set true will generate query packets of qRegisterInfo0, qRegisterInfo1,
 | |
|             etc.
 | |
| 
 | |
|         end_regex: required. Specifies a compiled regex object that will match the full text
 | |
|             of any response that signals an end to the iteration.  It must include the
 | |
|             initial $ and ending #xx and must match the whole packet.
 | |
| 
 | |
|         save_key: required.  Specifies the key within the context where an array will be stored.
 | |
|             Each packet received from the gdb remote that does not match the end_regex will get
 | |
|             appended to the array stored within the context at that key.
 | |
| 
 | |
|         runaway_response_count: optional. Defaults to 10000. If this many responses are retrieved,
 | |
|             assume there is something wrong with either the response collection or the ending
 | |
|             detection regex and throw an exception.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, params):
 | |
|         self._next_query = params.get("next_query", params.get("query"))
 | |
|         if not self._next_query:
 | |
|             raise "either next_query or query key must be specified for MultiResponseGdbRemoteEntry"
 | |
| 
 | |
|         self._first_query = params.get("first_query", self._next_query)
 | |
|         self._append_iteration_suffix = params.get(
 | |
|             "append_iteration_suffix", False)
 | |
|         self._iteration = 0
 | |
|         self._end_regex = params["end_regex"]
 | |
|         self._save_key = params["save_key"]
 | |
|         self._runaway_response_count = params.get(
 | |
|             "runaway_response_count", 10000)
 | |
|         self._is_send_to_remote = True
 | |
|         self._end_matched = False
 | |
| 
 | |
|     def is_send_to_remote(self):
 | |
|         return self._is_send_to_remote
 | |
| 
 | |
|     def get_send_packet(self):
 | |
|         if not self.is_send_to_remote():
 | |
|             raise Exception(
 | |
|                 "get_send_packet() called on MultiResponseGdbRemoteEntry that is not in the send state")
 | |
|         if self._end_matched:
 | |
|             raise Exception(
 | |
|                 "get_send_packet() called on MultiResponseGdbRemoteEntry but end of query/response sequence has already been seen.")
 | |
| 
 | |
|         # Choose the first or next query for the base payload.
 | |
|         if self._iteration == 0 and self._first_query:
 | |
|             payload = self._first_query
 | |
|         else:
 | |
|             payload = self._next_query
 | |
| 
 | |
|         # Append the suffix as needed.
 | |
|         if self._append_iteration_suffix:
 | |
|             payload += "%x" % self._iteration
 | |
| 
 | |
|         # Keep track of the iteration.
 | |
|         self._iteration += 1
 | |
| 
 | |
|         # Now that we've given the query packet, flip the mode to
 | |
|         # receive/match.
 | |
|         self._is_send_to_remote = False
 | |
| 
 | |
|         # Return the result, converted to packet form.
 | |
|         return gdbremote_packet_encode_string(payload)
 | |
| 
 | |
|     def is_consumed(self):
 | |
|         return self._end_matched
 | |
| 
 | |
|     def assert_match(self, asserter, actual_packet, context=None):
 | |
|         # This only makes sense for matching lines coming from the remote debug
 | |
|         # monitor.
 | |
|         if self.is_send_to_remote():
 | |
|             raise Exception(
 | |
|                 "assert_match() called on MultiResponseGdbRemoteEntry but state is set to send a query packet.")
 | |
| 
 | |
|         if self._end_matched:
 | |
|             raise Exception(
 | |
|                 "assert_match() called on MultiResponseGdbRemoteEntry but end of query/response sequence has already been seen.")
 | |
| 
 | |
|         # Set up a context as needed.
 | |
|         if not context:
 | |
|             context = {}
 | |
| 
 | |
|         # Check if the packet matches the end condition.
 | |
|         match = self._end_regex.match(actual_packet)
 | |
|         if match:
 | |
|             # We're done iterating.
 | |
|             self._end_matched = True
 | |
|             return context
 | |
| 
 | |
|         # Not done iterating - save the packet.
 | |
|         context[self._save_key] = context.get(self._save_key, [])
 | |
|         context[self._save_key].append(actual_packet)
 | |
| 
 | |
|         # Check for a runaway response cycle.
 | |
|         if len(context[self._save_key]) >= self._runaway_response_count:
 | |
|             raise Exception(
 | |
|                 "runaway query/response cycle detected: %d responses captured so far. Last response: %s" %
 | |
|                 (len(
 | |
|                     context[
 | |
|                         self._save_key]), context[
 | |
|                     self._save_key][
 | |
|                     -1]))
 | |
| 
 | |
|         # Flip the mode to send for generating the query.
 | |
|         self._is_send_to_remote = True
 | |
|         return context
 | |
| 
 | |
| 
 | |
| class MatchRemoteOutputEntry(GdbRemoteEntryBase):
 | |
|     """Waits for output from the debug monitor to match a regex or time out.
 | |
| 
 | |
|     This entry type tries to match each time new gdb remote output is accumulated
 | |
|     using a provided regex.  If the output does not match the regex within the
 | |
|     given timeframe, the command fails the playback session.  If the regex does
 | |
|     match, any capture fields are recorded in the context.
 | |
| 
 | |
|     Settings accepted from params:
 | |
| 
 | |
|         regex: required. Specifies a compiled regex object that must either succeed
 | |
|             with re.match or re.search (see regex_mode below) within the given timeout
 | |
|             (see timeout_seconds below) or cause the playback to fail.
 | |
| 
 | |
|         regex_mode: optional. Available values: "match" or "search". If "match", the entire
 | |
|             stub output as collected so far must match the regex.  If search, then the regex
 | |
|             must match starting somewhere within the output text accumulated thus far.
 | |
|             Default: "match" (i.e. the regex must match the entirety of the accumulated output
 | |
|             buffer, so unexpected text will generally fail the match).
 | |
| 
 | |
|         capture: optional.  If specified, is a dictionary of regex match group indices (should start
 | |
|             with 1) to variable names that will store the capture group indicated by the
 | |
|             index. For example, {1:"thread_id"} will store capture group 1's content in the
 | |
|             context dictionary where "thread_id" is the key and the match group value is
 | |
|             the value. The value stored off can be used later in a expect_captures expression.
 | |
|             This arg only makes sense when regex is specified.
 | |
|     """
 | |
| 
 | |
|     def __init__(self, regex=None, regex_mode="match", capture=None):
 | |
|         self._regex = regex
 | |
|         self._regex_mode = regex_mode
 | |
|         self._capture = capture
 | |
|         self._matched = False
 | |
| 
 | |
|         if not self._regex:
 | |
|             raise Exception("regex cannot be None")
 | |
| 
 | |
|         if not self._regex_mode in ["match", "search"]:
 | |
|             raise Exception(
 | |
|                 "unsupported regex mode \"{}\": must be \"match\" or \"search\"".format(
 | |
|                     self._regex_mode))
 | |
| 
 | |
|     def is_output_matcher(self):
 | |
|         return True
 | |
| 
 | |
|     def is_send_to_remote(self):
 | |
|         # This is always a "wait for remote" command.
 | |
|         return False
 | |
| 
 | |
|     def is_consumed(self):
 | |
|         return self._matched
 | |
| 
 | |
|     def assert_match(self, asserter, accumulated_output, context):
 | |
|         # Validate args.
 | |
|         if not accumulated_output:
 | |
|             raise Exception("accumulated_output cannot be none")
 | |
|         if not context:
 | |
|             raise Exception("context cannot be none")
 | |
| 
 | |
|         # Validate that we haven't already matched.
 | |
|         if self._matched:
 | |
|             raise Exception(
 | |
|                 "invalid state - already matched, attempting to match again")
 | |
| 
 | |
|         # If we don't have any content yet, we don't match.
 | |
|         if len(accumulated_output) < 1:
 | |
|             return context
 | |
| 
 | |
|         # Check if we match
 | |
|         if self._regex_mode == "match":
 | |
|             match = self._regex.match(accumulated_output)
 | |
|         elif self._regex_mode == "search":
 | |
|             match = self._regex.search(accumulated_output)
 | |
|         else:
 | |
|             raise Exception(
 | |
|                 "Unexpected regex mode: {}".format(
 | |
|                     self._regex_mode))
 | |
| 
 | |
|         # If we don't match, wait to try again after next $O content, or time
 | |
|         # out.
 | |
|         if not match:
 | |
|             # print("re pattern \"{}\" did not match against \"{}\"".format(self._regex.pattern, accumulated_output))
 | |
|             return context
 | |
| 
 | |
|         # We do match.
 | |
|         self._matched = True
 | |
|         # print("re pattern \"{}\" matched against \"{}\"".format(self._regex.pattern, accumulated_output))
 | |
| 
 | |
|         # Collect up any captures into the context.
 | |
|         if self._capture:
 | |
|             # Handle captures.
 | |
|             for group_index, var_name in list(self._capture.items()):
 | |
|                 capture_text = match.group(group_index)
 | |
|                 if not capture_text:
 | |
|                     raise Exception(
 | |
|                         "No content for group index {}".format(group_index))
 | |
|                 context[var_name] = capture_text
 | |
| 
 | |
|         return context
 | |
| 
 | |
| 
 | |
| class GdbRemoteTestSequence(object):
 | |
| 
 | |
|     _LOG_LINE_REGEX = re.compile(r'^.*(read|send)\s+packet:\s+(.+)$')
 | |
| 
 | |
|     def __init__(self, logger):
 | |
|         self.entries = []
 | |
|         self.logger = logger
 | |
| 
 | |
|     def __len__(self):
 | |
|         return len(self.entries)
 | |
| 
 | |
|     def add_log_lines(self, log_lines, remote_input_is_read):
 | |
|         for line in log_lines:
 | |
|             if isinstance(line, str):
 | |
|                 # Handle log line import
 | |
|                 # if self.logger:
 | |
|                 #     self.logger.debug("processing log line: {}".format(line))
 | |
|                 match = self._LOG_LINE_REGEX.match(line)
 | |
|                 if match:
 | |
|                     playback_packet = match.group(2)
 | |
|                     direction = match.group(1)
 | |
|                     if _is_packet_lldb_gdbserver_input(
 | |
|                             direction, remote_input_is_read):
 | |
|                         # Handle as something to send to the remote debug monitor.
 | |
|                         # if self.logger:
 | |
|                         #     self.logger.info("processed packet to send to remote: {}".format(playback_packet))
 | |
|                         self.entries.append(
 | |
|                             GdbRemoteEntry(
 | |
|                                 is_send_to_remote=True,
 | |
|                                 exact_payload=playback_packet))
 | |
|                     else:
 | |
|                         # Log line represents content to be expected from the remote debug monitor.
 | |
|                         # if self.logger:
 | |
|                         #     self.logger.info("receiving packet from llgs, should match: {}".format(playback_packet))
 | |
|                         self.entries.append(
 | |
|                             GdbRemoteEntry(
 | |
|                                 is_send_to_remote=False,
 | |
|                                 exact_payload=playback_packet))
 | |
|                 else:
 | |
|                     raise Exception(
 | |
|                         "failed to interpret log line: {}".format(line))
 | |
|             elif isinstance(line, dict):
 | |
|                 entry_type = line.get("type", "regex_capture")
 | |
|                 if entry_type == "regex_capture":
 | |
|                     # Handle more explicit control over details via dictionary.
 | |
|                     direction = line.get("direction", None)
 | |
|                     regex = line.get("regex", None)
 | |
|                     capture = line.get("capture", None)
 | |
|                     expect_captures = line.get("expect_captures", None)
 | |
| 
 | |
|                     # Compile the regex.
 | |
|                     if regex and (isinstance(regex, str)):
 | |
|                         regex = re.compile(regex)
 | |
| 
 | |
|                     if _is_packet_lldb_gdbserver_input(
 | |
|                             direction, remote_input_is_read):
 | |
|                         # Handle as something to send to the remote debug monitor.
 | |
|                         # if self.logger:
 | |
|                         #     self.logger.info("processed dict sequence to send to remote")
 | |
|                         self.entries.append(
 | |
|                             GdbRemoteEntry(
 | |
|                                 is_send_to_remote=True,
 | |
|                                 regex=regex,
 | |
|                                 capture=capture,
 | |
|                                 expect_captures=expect_captures))
 | |
|                     else:
 | |
|                         # Log line represents content to be expected from the remote debug monitor.
 | |
|                         # if self.logger:
 | |
|                         #     self.logger.info("processed dict sequence to match receiving from remote")
 | |
|                         self.entries.append(
 | |
|                             GdbRemoteEntry(
 | |
|                                 is_send_to_remote=False,
 | |
|                                 regex=regex,
 | |
|                                 capture=capture,
 | |
|                                 expect_captures=expect_captures))
 | |
|                 elif entry_type == "multi_response":
 | |
|                     self.entries.append(MultiResponseGdbRemoteEntry(line))
 | |
|                 elif entry_type == "output_match":
 | |
| 
 | |
|                     regex = line.get("regex", None)
 | |
|                     # Compile the regex.
 | |
|                     if regex and (isinstance(regex, str)):
 | |
|                         regex = re.compile(regex, re.DOTALL)
 | |
| 
 | |
|                     regex_mode = line.get("regex_mode", "match")
 | |
|                     capture = line.get("capture", None)
 | |
|                     self.entries.append(
 | |
|                         MatchRemoteOutputEntry(
 | |
|                             regex=regex,
 | |
|                             regex_mode=regex_mode,
 | |
|                             capture=capture))
 | |
|                 else:
 | |
|                     raise Exception("unknown entry type \"%s\"" % entry_type)
 | |
| 
 | |
| 
 | |
| def process_is_running(pid, unknown_value=True):
 | |
|     """If possible, validate that the given pid represents a running process on the local system.
 | |
| 
 | |
|     Args:
 | |
| 
 | |
|         pid: an OS-specific representation of a process id.  Should be an integral value.
 | |
| 
 | |
|         unknown_value: value used when we cannot determine how to check running local
 | |
|         processes on the OS.
 | |
| 
 | |
|     Returns:
 | |
| 
 | |
|         If we can figure out how to check running process ids on the given OS:
 | |
|         return True if the process is running, or False otherwise.
 | |
| 
 | |
|         If we don't know how to check running process ids on the given OS:
 | |
|         return the value provided by the unknown_value arg.
 | |
|     """
 | |
|     if not isinstance(pid, six.integer_types):
 | |
|         raise Exception(
 | |
|             "pid must be an integral type (actual type: %s)" % str(
 | |
|                 type(pid)))
 | |
| 
 | |
|     process_ids = []
 | |
| 
 | |
|     if lldb.remote_platform:
 | |
|         # Don't know how to get list of running process IDs on a remote
 | |
|         # platform
 | |
|         return unknown_value
 | |
|     elif platform.system() in ['Darwin', 'Linux', 'FreeBSD', 'NetBSD']:
 | |
|         # Build the list of running process ids
 | |
|         output = subprocess.check_output(
 | |
|             "ps ax | awk '{ print $1; }'", shell=True).decode("utf-8")
 | |
|         text_process_ids = output.split('\n')[1:]
 | |
|         # Convert text pids to ints
 | |
|         process_ids = [int(text_pid)
 | |
|                        for text_pid in text_process_ids if text_pid != '']
 | |
|     elif platform.system() == 'Windows':
 | |
|         output = subprocess.check_output(
 | |
|             "for /f \"tokens=2 delims=,\" %F in ('tasklist /nh /fi \"PID ne 0\" /fo csv') do @echo %~F", shell=True).decode("utf-8")
 | |
|         text_process_ids = output.split('\n')[1:]
 | |
|         process_ids = [int(text_pid)
 | |
|                        for text_pid in text_process_ids if text_pid != '']
 | |
|     # elif {your_platform_here}:
 | |
|     #   fill in process_ids as a list of int type process IDs running on
 | |
|     #   the local system.
 | |
|     else:
 | |
|         # Don't know how to get list of running process IDs on this
 | |
|         # OS, so return the "don't know" value.
 | |
|         return unknown_value
 | |
| 
 | |
|     # Check if the pid is in the process_ids
 | |
|     return pid in process_ids
 | |
| 
 | |
| def _handle_output_packet_string(packet_contents):
 | |
|     if (not packet_contents) or (len(packet_contents) < 1):
 | |
|         return None
 | |
|     elif packet_contents[0:1] != b"O":
 | |
|         return None
 | |
|     elif packet_contents == b"OK":
 | |
|         return None
 | |
|     else:
 | |
|         return binascii.unhexlify(packet_contents[1:])
 | |
| 
 | |
| class Server(object):
 | |
| 
 | |
|     _GDB_REMOTE_PACKET_REGEX = re.compile(br'^\$([^\#]*)#[0-9a-fA-F]{2}')
 | |
| 
 | |
|     class ChecksumMismatch(Exception):
 | |
|         pass
 | |
| 
 | |
|     def __init__(self, sock, proc = None):
 | |
|         self._accumulated_output = b""
 | |
|         self._receive_buffer = b""
 | |
|         self._normal_queue = []
 | |
|         self._output_queue = []
 | |
|         self._sock = sock
 | |
|         self._proc = proc
 | |
| 
 | |
|     def send_raw(self, frame):
 | |
|         self._sock.sendall(frame)
 | |
| 
 | |
|     def _read(self, q):
 | |
|         while not q:
 | |
|             new_bytes = self._sock.recv(4096)
 | |
|             self._process_new_bytes(new_bytes)
 | |
|         return q.pop(0)
 | |
| 
 | |
|     def _process_new_bytes(self, new_bytes):
 | |
|         # Add new bytes to our accumulated unprocessed packet bytes.
 | |
|         self._receive_buffer += new_bytes
 | |
| 
 | |
|         # Parse fully-formed packets into individual packets.
 | |
|         has_more = len(self._receive_buffer) > 0
 | |
|         while has_more:
 | |
|             if len(self._receive_buffer) <= 0:
 | |
|                 has_more = False
 | |
|             # handle '+' ack
 | |
|             elif self._receive_buffer[0:1] == b"+":
 | |
|                 self._normal_queue += [b"+"]
 | |
|                 self._receive_buffer = self._receive_buffer[1:]
 | |
|             else:
 | |
|                 packet_match = self._GDB_REMOTE_PACKET_REGEX.match(
 | |
|                     self._receive_buffer)
 | |
|                 if packet_match:
 | |
|                     # Our receive buffer matches a packet at the
 | |
|                     # start of the receive buffer.
 | |
|                     new_output_content = _handle_output_packet_string(
 | |
|                         packet_match.group(1))
 | |
|                     if new_output_content:
 | |
|                         # This was an $O packet with new content.
 | |
|                         self._accumulated_output += new_output_content
 | |
|                         self._output_queue += [self._accumulated_output]
 | |
|                     else:
 | |
|                         # Any packet other than $O.
 | |
|                         self._normal_queue += [packet_match.group(0)]
 | |
| 
 | |
|                     # Remove the parsed packet from the receive
 | |
|                     # buffer.
 | |
|                     self._receive_buffer = self._receive_buffer[
 | |
|                         len(packet_match.group(0)):]
 | |
|                 else:
 | |
|                     # We don't have enough in the receive bufferto make a full
 | |
|                     # packet. Stop trying until we read more.
 | |
|                     has_more = False
 | |
| 
 | |
|     def get_raw_output_packet(self):
 | |
|         return self._read(self._output_queue)
 | |
| 
 | |
|     def get_raw_normal_packet(self):
 | |
|         return self._read(self._normal_queue)
 | |
| 
 | |
|     def get_accumulated_output(self):
 | |
|         return self._accumulated_output
 | |
| 
 | |
|     def consume_accumulated_output(self):
 | |
|         output = self._accumulated_output
 | |
|         self._accumulated_output = b""
 | |
|         return output
 | |
| 
 | |
|     def __str__(self):
 | |
|         return dedent("""\
 | |
|             server '{}' on '{}'
 | |
|             _receive_buffer: {}
 | |
|             _normal_queue: {}
 | |
|             _output_queue: {}
 | |
|             _accumulated_output: {}
 | |
|             """).format(self._proc, self._sock, self._receive_buffer,
 | |
|                     self._normal_queue, self._output_queue,
 | |
|                     self._accumulated_output)
 |