1import os
2import re
3import subprocess
4import sys
5import tempfile
6
7from .constants import PANIC_OUTPUT_DECODE_SCRIPT
8from .logger import Logger
9from .output_helpers import normal_print, red_print, yellow_print
10from .web_socket_client import WebSocketClient
11
12
13class GDBHelper:
14    def __init__(self, toolchain_prefix, websocket_client, elf_file, port, baud_rate):
15        # type: (str, WebSocketClient, str, int, int) -> None
16        self._gdb_buffer = b''  # type: bytes
17        self._gdb_exit = False  # type: bool
18        self.toolchain_prefix = toolchain_prefix
19        self.websocket_client = websocket_client
20        self.elf_file = elf_file
21        self.port = port
22        self.baud_rate = baud_rate
23
24    @property
25    def gdb_buffer(self):  # type: () -> bytes
26        return self._gdb_buffer
27
28    @gdb_buffer.setter
29    def gdb_buffer(self, value):  # type: (bytes) -> None
30        self._gdb_buffer = value
31
32    @property
33    def gdb_exit(self):  # type: () -> bool
34        return self._gdb_exit
35
36    @gdb_exit.setter
37    def gdb_exit(self, value):  # type: (bool) -> None
38        self._gdb_exit = value
39
40    def run_gdb(self):
41        # type: () -> None
42        normal_print('')
43        try:
44            cmd = ['%sgdb' % self.toolchain_prefix,
45                   '-ex', 'set serial baud %d' % self.baud_rate,
46                   '-ex', 'target remote %s' % self.port,
47                   self.elf_file]
48            # Here we handling GDB as a process
49            # Open GDB process
50            try:
51                process = subprocess.Popen(cmd, cwd='.')
52            except KeyboardInterrupt:
53                pass
54            # We ignore Ctrl+C interrupt form external process abd wait response util GDB will be finished.
55            while True:
56                try:
57                    process.wait()
58                    break
59                except KeyboardInterrupt:
60                    pass  # We ignore the Ctrl+C
61            self.gdb_exit = True
62        except OSError as e:
63            red_print('%s: %s' % (' '.join(cmd), e))
64        except KeyboardInterrupt:
65            pass  # happens on Windows, maybe other OSes
66        finally:
67            try:
68                # on Linux, maybe other OSes, gdb sometimes seems to be alive even after wait() returns...
69                process.terminate()
70            except Exception:  # noqa
71                pass
72            try:
73                # also on Linux, maybe other OSes, gdb sometimes exits uncleanly and breaks the tty mode
74                subprocess.call(['stty', 'sane'])
75            except Exception:  # noqa
76                pass  # don't care if there's no stty, we tried...
77
78    def check_gdb_stub_trigger(self, line):
79        # type: (bytes) -> bool
80        line = self.gdb_buffer + line
81        self.gdb_buffer = b''
82        m = re.search(b'\\$(T..)#(..)', line)  # look for a gdb "reason" for a break
83        if m is not None:
84            try:
85                chsum = sum(ord(bytes([p])) for p in m.group(1)) & 0xFF
86                calc_chsum = int(m.group(2), 16)
87            except ValueError:  # payload wasn't valid hex digits
88                return False
89            if chsum == calc_chsum:
90                if self.websocket_client:
91                    yellow_print('Communicating through WebSocket')
92                    self.websocket_client.send({'event': 'gdb_stub',
93                                                'port': self.port,
94                                                'prog': self.elf_file})
95                    yellow_print('Waiting for debug finished event')
96                    self.websocket_client.wait([('event', 'debug_finished')])
97                    yellow_print('Communications through WebSocket is finished')
98                else:
99                    return True
100            else:
101                red_print('Malformed gdb message... calculated checksum %02x received %02x' % (chsum, calc_chsum))
102        return False
103
104    def process_panic_output(self, panic_output, logger, target):  # type: (bytes, Logger, str) -> None
105        panic_output_file = None
106        try:
107            # On Windows, the temporary file can't be read unless it is closed.
108            # Set delete=False and delete the file manually later.
109            with tempfile.NamedTemporaryFile(mode='wb', delete=False) as panic_output_file:
110                panic_output_file.write(panic_output)
111                panic_output_file.flush()
112            cmd = [self.toolchain_prefix + 'gdb',
113                   '--batch', '-n',
114                   self.elf_file,
115                   '-ex', "target remote | \"{python}\" \"{script}\" --target {target} \"{output_file}\""
116                       .format(python=sys.executable,
117                               script=PANIC_OUTPUT_DECODE_SCRIPT,
118                               target=target,
119                               output_file=panic_output_file.name),
120                   '-ex', 'bt']
121            output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
122            yellow_print('\nBacktrace:\n\n')
123            logger.print(output)  # noqa: E999
124        except subprocess.CalledProcessError as e:
125            yellow_print('Failed to run gdb_panic_server.py script: {}\n{}\n\n'.format(e, e.output))
126            logger.print(panic_output)
127        finally:
128            if panic_output_file is not None:
129                try:
130                    os.unlink(panic_output_file.name)
131                except OSError as e:
132                    yellow_print('Couldn\'t remove temporary panic output file ({})'.format(e))
133