1# Copyright (c) 2017 Linaro Limited.
2#
3# SPDX-License-Identifier: Apache-2.0
4
5'''Runner for debugging with J-Link.'''
6
7import argparse
8import ipaddress
9import logging
10import os
11import shlex
12import socket
13import subprocess
14import sys
15import tempfile
16import time
17from pathlib import Path
18
19from runners.core import FileType, RunnerCaps, ZephyrBinaryRunner
20
21try:
22    import pylink
23    from pylink.library import Library
24    MISSING_REQUIREMENTS = False
25except ImportError:
26    MISSING_REQUIREMENTS = True
27
28DEFAULT_JLINK_EXE = 'JLink.exe' if sys.platform == 'win32' else 'JLinkExe'
29DEFAULT_JLINK_GDB_PORT = 2331
30DEFAULT_JLINK_RTT_PORT = 19021
31
32def is_ip(ip):
33    if not ip:
34        return False
35    try:
36        ipaddress.ip_address(ip.split(':')[0])
37    except ValueError:
38        return False
39    return True
40
41def is_tunnel(tunnel):
42    return tunnel.startswith("tunnel:") if tunnel else False
43
44class ToggleAction(argparse.Action):
45
46    def __call__(self, parser, args, ignored, option):
47        setattr(args, self.dest, not option.startswith('--no-'))
48
49class JLinkBinaryRunner(ZephyrBinaryRunner):
50    '''Runner front-end for the J-Link GDB server.'''
51
52    def __init__(self, cfg, device, dev_id=None,
53                 commander=DEFAULT_JLINK_EXE,
54                 dt_flash=True, erase=True, reset=False,
55                 iface='swd', speed='auto', flash_script = None,
56                 loader=None,
57                 gdbserver='JLinkGDBServer',
58                 gdb_host='',
59                 gdb_port=DEFAULT_JLINK_GDB_PORT,
60                 rtt_port=DEFAULT_JLINK_RTT_PORT,
61                 tui=False, tool_opt=None):
62        super().__init__(cfg)
63        self.file = cfg.file
64        self.file_type = cfg.file_type
65        self.hex_name = cfg.hex_file
66        self.bin_name = cfg.bin_file
67        self.elf_name = cfg.elf_file
68        self.gdb_cmd = [cfg.gdb] if cfg.gdb else None
69        self.device = device
70        self.dev_id = dev_id
71        self.commander = commander
72        self.flash_script = flash_script
73        self.dt_flash = dt_flash
74        self.erase = erase
75        self.reset = reset
76        self.gdbserver = gdbserver
77        self.iface = iface
78        self.speed = speed
79        self.gdb_host = gdb_host
80        self.gdb_port = gdb_port
81        self.tui_arg = ['-tui'] if tui else []
82        self.loader = loader
83        self.rtt_port = rtt_port
84
85        self.tool_opt = []
86        if tool_opt is not None:
87            for opts in [shlex.split(opt) for opt in tool_opt]:
88                self.tool_opt += opts
89
90    @classmethod
91    def name(cls):
92        return 'jlink'
93
94    @classmethod
95    def capabilities(cls):
96        return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach', 'rtt'},
97                          dev_id=True, flash_addr=True, erase=True, reset=True,
98                          tool_opt=True, file=True, rtt=True)
99
100    @classmethod
101    def dev_id_help(cls) -> str:
102        return '''Device identifier. Use it to select the J-Link Serial Number
103                  of the device connected over USB. If the J-Link is connected over ip,
104                  the Device identifier is the ip.'''
105
106    @classmethod
107    def tool_opt_help(cls) -> str:
108        return "Additional options for JLink Commander, e.g. '-autoconnect 1'"
109
110    @classmethod
111    def do_add_parser(cls, parser):
112        # Required:
113        parser.add_argument('--device', required=True, help='device name')
114
115        # Optional:
116        parser.add_argument('--loader', required=False, dest='loader',
117                            help='specifies a loader type')
118        parser.add_argument('--id', required=False, dest='dev_id',
119                            help='obsolete synonym for -i/--dev-id')
120        parser.add_argument('--iface', default='swd',
121                            help='interface to use, default is swd')
122        parser.add_argument('--speed', default='auto',
123                            help='interface speed, default is autodetect')
124        parser.add_argument('--flash-script', default=None,
125                            help='Custom flashing script, default is None')
126        parser.add_argument('--tui', default=False, action='store_true',
127                            help='if given, GDB uses -tui')
128        parser.add_argument('--gdbserver', default='JLinkGDBServer',
129                            help='GDB server, default is JLinkGDBServer')
130        parser.add_argument('--gdb-host', default='',
131                            help='custom gdb host, defaults to the empty string '
132                            'and runs a gdb server')
133        parser.add_argument('--gdb-port', default=DEFAULT_JLINK_GDB_PORT,
134                            help=f'pyocd gdb port, defaults to {DEFAULT_JLINK_GDB_PORT}')
135        parser.add_argument('--commander', default=DEFAULT_JLINK_EXE,
136                            help=f'''J-Link Commander, default is
137                            {DEFAULT_JLINK_EXE}''')
138        parser.add_argument('--reset-after-load', '--no-reset-after-load',
139                            dest='reset', nargs=0,
140                            action=ToggleAction,
141                            help='obsolete synonym for --reset/--no-reset')
142        parser.add_argument('--rtt-client', default='JLinkRTTClient',
143                            help='RTT client, default is JLinkRTTClient')
144        parser.add_argument('--rtt-port', default=DEFAULT_JLINK_RTT_PORT,
145                            help=f'jlink rtt port, defaults to {DEFAULT_JLINK_RTT_PORT}')
146
147        parser.set_defaults(reset=False)
148
149    @classmethod
150    def do_create(cls, cfg, args):
151        return JLinkBinaryRunner(cfg, args.device,
152                                 dev_id=args.dev_id,
153                                 commander=args.commander,
154                                 dt_flash=args.dt_flash,
155                                 erase=args.erase,
156                                 reset=args.reset,
157                                 iface=args.iface, speed=args.speed,
158                                 flash_script=args.flash_script,
159                                 gdbserver=args.gdbserver,
160                                 loader=args.loader,
161                                 gdb_host=args.gdb_host,
162                                 gdb_port=args.gdb_port,
163                                 rtt_port=args.rtt_port,
164                                 tui=args.tui, tool_opt=args.tool_opt)
165
166    def print_gdbserver_message(self):
167        if not self.thread_info_enabled:
168            thread_msg = '; no thread info available'
169        elif self.supports_thread_info:
170            thread_msg = '; thread info enabled'
171        else:
172            thread_msg = '; update J-Link software for thread info'
173        self.logger.info('J-Link GDB server running on port '
174                         f'{self.gdb_port}{thread_msg}')
175
176    def print_rttserver_message(self):
177        self.logger.info(f'J-Link RTT server running on port {self.rtt_port}')
178
179    @property
180    def jlink_version(self):
181        # Get the J-Link version as a (major, minor, rev) tuple of integers.
182        #
183        # J-Link's command line tools provide neither a standalone
184        # "--version" nor help output that contains the version. Hack
185        # around this deficiency by using the third-party pylink library
186        # to load the shared library distributed with the tools, which
187        # provides an API call for getting the version.
188        if not hasattr(self, '_jlink_version'):
189            # pylink 0.14.0/0.14.1 exposes JLink SDK DLL (libjlinkarm) in
190            # JLINK_SDK_STARTS_WITH, while other versions use JLINK_SDK_NAME
191            if pylink.__version__ in ('0.14.0', '0.14.1'):
192                sdk = Library.JLINK_SDK_STARTS_WITH
193            else:
194                sdk = Library.JLINK_SDK_NAME
195
196            plat = sys.platform
197            if plat.startswith('win32'):
198                libname = Library.get_appropriate_windows_sdk_name() + '.dll'
199            elif plat.startswith('linux'):
200                libname = sdk + '.so'
201            elif plat.startswith('darwin'):
202                libname = sdk + '.dylib'
203            else:
204                self.logger.warning(f'unknown platform {plat}; assuming UNIX')
205                libname = sdk + '.so'
206
207            lib = Library(dllpath=os.fspath(Path(self.commander).parent /
208                                            libname))
209            version = int(lib.dll().JLINKARM_GetDLLVersion())
210            self.logger.debug('JLINKARM_GetDLLVersion()=%s', version)
211            # The return value is an int with 2 decimal digits per
212            # version subfield.
213            self._jlink_version = (version // 10000,
214                                   (version // 100) % 100,
215                                   version % 100)
216
217        return self._jlink_version
218
219    @property
220    def jlink_version_str(self):
221        # Converts the numeric revision tuple to something human-readable.
222        if not hasattr(self, '_jlink_version_str'):
223            major, minor, rev = self.jlink_version
224            rev_str = chr(ord('a') + rev - 1) if rev else ''
225            self._jlink_version_str = f'{major}.{minor:02}{rev_str}'
226        return self._jlink_version_str
227
228    @property
229    def supports_nogui(self):
230        # -nogui was introduced in J-Link Commander v6.80
231        return self.jlink_version >= (6, 80, 0)
232
233    @property
234    def supports_thread_info(self):
235        # RTOSPlugin_Zephyr was introduced in 7.11b
236        return self.jlink_version >= (7, 11, 2)
237
238    @property
239    def supports_loader(self):
240        return self.jlink_version >= (7, 70, 4)
241
242    def do_run(self, command, **kwargs):
243
244        if MISSING_REQUIREMENTS:
245            raise RuntimeError('one or more Python dependencies were missing; '
246                               "see the getting started guide for details on "
247                               "how to fix")
248        # Convert commander to a real absolute path. We need this to
249        # be able to find the shared library that tells us what
250        # version of the tools we're using.
251        self.commander = os.fspath(
252            Path(self.require(self.commander)).resolve())
253        self.logger.info(f'JLink version: {self.jlink_version_str}')
254
255        rtos = self.thread_info_enabled and self.supports_thread_info
256        plugin_dir = os.fspath(Path(self.commander).parent / 'GDBServer' /
257                               'RTOSPlugin_Zephyr')
258        big_endian = self.build_conf.getboolean('CONFIG_BIG_ENDIAN')
259
260        server_cmd = (
261            [self.gdbserver]
262            + [
263                '-select',
264                ('ip' if (is_ip(self.dev_id) or is_tunnel(self.dev_id)) else 'usb')
265                + (f'={self.dev_id}' if self.dev_id else ''),
266            ]
267            + ['-port', str(self.gdb_port)]
268            + ['-if', self.iface]
269            + ['-speed', self.speed]
270            + ['-device', self.device]
271            + ['-silent']
272            + ['-endian' 'big' if big_endian else 'little']
273            + ['-singlerun']
274            + (['-nogui'] if self.supports_nogui else [])
275            + (['-rtos', plugin_dir] if rtos else [])
276            + ['-rtttelnetport', str(self.rtt_port)]
277            + self.tool_opt
278        )
279
280        if command == 'flash':
281            self.flash(**kwargs)
282        elif command == 'debugserver':
283            if self.gdb_host:
284                raise ValueError('Cannot run debugserver with --gdb-host')
285            self.require(self.gdbserver)
286            self.print_gdbserver_message()
287            self.check_call(server_cmd)
288        elif command == 'rtt':
289            self.print_gdbserver_message()
290            self.print_rttserver_message()
291            server_cmd += ['-nohalt']
292            server_proc = self.popen_ignore_int(server_cmd)
293            try:
294                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
295                # wait for the port to be open
296                while server_proc.poll() is None:
297                    try:
298                        sock.connect(('localhost', self.rtt_port))
299                        break
300                    except ConnectionRefusedError:
301                        time.sleep(0.1)
302                sock.shutdown(socket.SHUT_RDWR)
303                time.sleep(0.1)
304                self.run_telnet_client('localhost', self.rtt_port)
305            except Exception as e:
306                self.logger.error(e)
307            finally:
308                server_proc.terminate()
309                server_proc.wait()
310        else:
311            if self.gdb_cmd is None:
312                raise ValueError('Cannot debug; gdb is missing')
313            if self.file is not None:
314                if self.file_type != FileType.ELF:
315                    raise ValueError('Cannot debug; elf file required')
316                elf_name = self.file
317            elif self.elf_name is None:
318                raise ValueError('Cannot debug; elf is missing')
319            else:
320                elf_name = self.elf_name
321            client_cmd = (self.gdb_cmd +
322                          self.tui_arg +
323                          [elf_name] +
324                          ['-ex', f'target remote {self.gdb_host}:{self.gdb_port}'])
325            if command == 'debug':
326                client_cmd += ['-ex', 'monitor halt',
327                               '-ex', 'monitor reset',
328                               '-ex', 'load']
329                if self.reset:
330                    client_cmd += ['-ex', 'monitor reset']
331            if not self.gdb_host:
332                self.require(self.gdbserver)
333                self.print_gdbserver_message()
334                self.run_server_and_client(server_cmd, client_cmd)
335            else:
336                self.run_client(client_cmd)
337
338    def get_default_flash_commands(self):
339        lines = [
340            'ExitOnError 1',  # Treat any command-error as fatal
341            'r',  # Reset and halt the target
342            'BE' if self.build_conf.getboolean('CONFIG_BIG_ENDIAN') else 'LE'
343        ]
344
345        if self.erase:
346            lines.append('erase') # Erase all flash sectors
347
348        # Get the build artifact to flash
349        if self.file is not None:
350            # use file provided by the user
351            if not os.path.isfile(self.file):
352                err = 'Cannot flash; file ({}) not found'
353                raise ValueError(err.format(self.file))
354
355            flash_file = self.file
356
357            if self.file_type == FileType.HEX:
358                flash_cmd = f'loadfile "{self.file}"'
359            elif self.file_type == FileType.BIN:
360                if self.dt_flash:
361                    flash_addr = self.flash_address_from_build_conf(self.build_conf)
362                else:
363                    flash_addr = 0
364                flash_cmd = f'loadfile "{self.file}" 0x{flash_addr:x}'
365            else:
366                err = 'Cannot flash; jlink runner only supports hex and bin files'
367                raise ValueError(err)
368
369        else:
370            # Use hex, bin or elf file provided by the buildsystem.
371            # Preferring .hex over .bin and .elf
372            if self.hex_name is not None and os.path.isfile(self.hex_name):
373                flash_file = self.hex_name
374                flash_cmd = f'loadfile "{self.hex_name}"'
375            # Preferring .bin over .elf
376            elif self.bin_name is not None and os.path.isfile(self.bin_name):
377                if self.dt_flash:
378                    flash_addr = self.flash_address_from_build_conf(self.build_conf)
379                else:
380                    flash_addr = 0
381                flash_file = self.bin_name
382                flash_cmd = f'loadfile "{self.bin_name}" 0x{flash_addr:x}'
383            elif self.elf_name is not None and os.path.isfile(self.elf_name):
384                flash_file = self.elf_name
385                flash_cmd = f'loadfile "{self.elf_name}"'
386            else:
387                err = 'Cannot flash; no hex ({}), bin ({}) or elf ({}) files found.'
388                raise ValueError(err.format(self.hex_name, self.bin_name, self.elf_name))
389
390        # Flash the selected build artifact
391        lines.append(flash_cmd)
392
393        if self.reset:
394            lines.append('r') # Reset and halt the target
395
396        lines.append('g') # Start the CPU
397
398        # Reset the Debug Port CTRL/STAT register
399        # Under normal operation this is done automatically, but if other
400        # JLink tools are running, it is not performed.
401        # The J-Link scripting layer chains commands, meaning that writes are
402        # not actually performed until after the next operation. After writing
403        # the register, read it back to perform this flushing.
404        lines.append('writeDP 1 0')
405        lines.append('readDP 1')
406
407        lines.append('q') # Close the connection and quit
408
409        self.logger.debug('JLink commander script:\n' +
410                          '\n'.join(lines))
411        return flash_file, lines
412
413    def run_flash_cmd(self, fname, flash_file, **kwargs):
414        loader_details = ""
415        if self.supports_loader and self.loader:
416            loader_details = "?" + self.loader
417
418        cmd = (
419            [self.commander]
420            + (
421                ['-IP', f'{self.dev_id}']
422                if (is_ip(self.dev_id) or is_tunnel(self.dev_id))
423                else (['-USB', f'{self.dev_id}'] if self.dev_id else [])
424            )
425            + (['-nogui', '1'] if self.supports_nogui else [])
426            + ['-if', self.iface]
427            + ['-speed', self.speed]
428            + ['-device', self.device + loader_details]
429            + ['-CommanderScript', fname]
430            + (['-nogui', '1'] if self.supports_nogui else [])
431            + self.tool_opt
432        )
433
434        if flash_file:
435            self.logger.info(f'Flashing file: {flash_file}')
436        kwargs = {}
437        if not self.logger.isEnabledFor(logging.DEBUG):
438            kwargs['stdout'] = subprocess.DEVNULL
439        self.check_call(cmd, **kwargs)
440
441    def flash(self, **kwargs):
442        fname = self.flash_script
443        if fname is None:
444            # Don't use NamedTemporaryFile: the resulting file can't be
445            # opened again on Windows.
446            with tempfile.TemporaryDirectory(suffix='jlink') as d:
447                flash_file, lines = self.get_default_flash_commands()
448                fname = os.path.join(d, 'runner.jlink')
449                with open(fname, 'wb') as f:
450                    f.writelines(bytes(line + '\n', 'utf-8') for line in lines)
451
452                self.run_flash_cmd(fname, flash_file, **kwargs)
453        else:
454            self.run_flash_cmd(fname, None, **kwargs)
455