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