summaryrefslogtreecommitdiffstats
path: root/lldb/test/tools/lldb-gdbserver/lldbgdbserverutils.py
blob: 0bd44ef7f4587a110b5d264f6fe7ac9875f19607 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
"""Module for supporting unit testing of the lldb-gdbserver debug monitor exe.
"""

import os
import os.path
import re
import select
import time


def _get_debug_monitor_from_lldb(lldb_exe, debug_monitor_basename):
    """Return the debug monitor exe path given the lldb exe path.

    This method attempts to construct a valid debug monitor exe name
    from a given lldb exe name.  It will return None if the synthesized
    debug monitor name is not found to exist.

    The debug monitor exe path is synthesized by taking the directory
    of the lldb exe, and replacing the portion of the base name that
    matches "lldb" (case insensitive) and replacing with the value of
    debug_monitor_basename.

    Args:
        lldb_exe: the path to an lldb executable.

        debug_monitor_basename: the base name portion of the debug monitor
            that will replace 'lldb'.

    Returns:
        A path to the debug monitor exe if it is found to exist; otherwise,
        returns None.

    """

    exe_dir = os.path.dirname(lldb_exe)
    exe_base = os.path.basename(lldb_exe)

    # we'll rebuild the filename by replacing lldb with
    # the debug monitor basename, keeping any prefix or suffix in place.
    regex = re.compile(r"lldb", re.IGNORECASE)
    new_base = regex.sub(debug_monitor_basename, exe_base)

    debug_monitor_exe = os.path.join(exe_dir, new_base)
    if os.path.exists(debug_monitor_exe):
        return debug_monitor_exe
    else:
        return None


def get_lldb_gdbserver_exe():
    """Return the lldb-gdbserver exe path.

    Returns:
        A path to the lldb-gdbserver exe if it is found to exist; otherwise,
        returns None.
    """
    lldb_exe = os.environ["LLDB_EXEC"]
    if not lldb_exe:
        return None
    else:
        return _get_debug_monitor_from_lldb(lldb_exe, "lldb-gdbserver")

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.
    """
    lldb_exe = os.environ["LLDB_EXEC"]
    if not lldb_exe:
        return None
    else:
        return _get_debug_monitor_from_lldb(lldb_exe, "debugserver")


_LOG_LINE_REGEX = re.compile(r'^(lldb-gdbserver|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)


_STRIP_CHECKSUM_REGEX = re.compile(r'#[0-9a-fA-F]{2}$')

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)


_GDB_REMOTE_PACKET_REGEX = re.compile(r'^\$([^\#]*)#[0-9a-fA-F]{2}')

def expect_lldb_gdbserver_replay(
    asserter,
    sock,
    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.

        sock: the TCP socket connected to the lldb-gdbserver exe.

        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:
        None if no issues.  Raises an exception if the expected communication does not
        occur.

    """
    received_lines = []
    receive_buffer = ''
    context = {}

    for sequence_entry in test_sequence.entries:
        if sequence_entry.is_send_to_remote:
            # This is an entry to send to the remote debug monitor.
            if logger:
                logger.info("sending packet to remote: {}".format(sequence_entry.exact_payload))
            sock.sendall(sequence_entry.exact_payload)
        else:
            # This is an entry to expect to receive from the remote debug monitor.
            if logger:
                logger.info("receiving packet from remote, should match: {}".format(sequence_entry.exact_payload))

            start_time = time.time()
            timeout_time = start_time + timeout_seconds

            # while we don't have a complete line of input, wait
            # for it from socket.
            while len(received_lines) < 1:
                # check for timeout
                if time.time() > timeout_time:
                    raise Exception(
                        'timed out after {} seconds while waiting for llgs to respond with: {}, currently received: {}'.format(
                            timeout_seconds, sequence_entry.exact_payload, receive_buffer))
                can_read, _, _ = select.select([sock], [], [], 0)
                if can_read and sock in can_read:
                    new_bytes = sock.recv(4096)
                    if new_bytes and len(new_bytes) > 0:
                        # read the next bits from the socket
                        if logger:
                            logger.debug("llgs responded with bytes: {}".format(new_bytes))
                        receive_buffer += new_bytes

                        # parse fully-formed packets into individual packets
                        has_more = len(receive_buffer) > 0
                        while has_more:
                            if len(receive_buffer) <= 0:
                                has_more = False
                            # handle '+' ack
                            elif receive_buffer[0] == '+':
                                received_lines.append('+')
                                receive_buffer = receive_buffer[1:]
                                if logger:
                                    logger.debug('parsed packet from llgs: +, new receive_buffer: {}'.format(receive_buffer))
                            else:
                                packet_match = _GDB_REMOTE_PACKET_REGEX.match(receive_buffer)
                                if packet_match:
                                    received_lines.append(packet_match.group(0))
                                    receive_buffer = receive_buffer[len(packet_match.group(0)):]
                                    if logger:
                                        logger.debug('parsed packet from llgs: {}, new receive_buffer: {}'.format(packet_match.group(0), receive_buffer))
                                else:
                                    has_more = False
            # got a line - now try to match it against expected line
            if len(received_lines) > 0:
                received_packet = received_lines.pop(0)
                context = sequence_entry.assert_match(asserter, received_packet, context=context)
    return None


def gdbremote_hex_encode_string(str):
    output = ''
    for c in str:
        output += '{0:02x}'.format(ord(c))
    return output


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)

class GdbRemoteEntry(object):

    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 _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)
        asserter.assertIsNotNone(match)

        if self.capture:
            # Handle captures.
            for group_index, var_name in 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

        if self.expect_captures:
            # Handle comparing matched groups to context dictionary entries.
            for group_index, var_name in 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.assertEquals(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 GdbRemoteTestSequence(object):

    _LOG_LINE_REGEX = re.compile(r'^.*(read|send)\s+packet:\s+(.+)$')

    def __init__(self, logger):
        self.entries = []
        self.logger = logger

    def add_log_lines(self, log_lines, remote_input_is_read):
        for line in log_lines:
            if type(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 type(line) == dict:
                # 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 (type(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))


if __name__ == '__main__':
    EXE_PATH = get_lldb_gdbserver_exe()
    if EXE_PATH:
        print "lldb-gdbserver path detected: {}".format(EXE_PATH)
    else:
        print "lldb-gdbserver could not be found"
OpenPOWER on IntegriCloud