# Configure mgdbserver then open remote target. # Copyright (C) 2011-2013 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """GDB command openning connection to mgdbserver.""" import argparse import os import os.path import random import StringIO import sys import subprocess import tempfile import time import uuid import gdb def raise_gdb_error(msg, logfile): """ Raise gdb.GdbError instance with logfile content as the error message.""" try: with open(logfile, 'r') as f: msg += ':\n' + '\t'.join([line for line in f.readlines() if line.strip() != 'Exiting']) except IOError: pass raise gdb.GdbError(msg) def _get_popen_args(stderr_stream): args = { # redirect mgdbserver stderr to a log file 'stderr':stderr_stream, # ignore stdout to be able to display a coherent error message # if gdbserver fails to start 'stdout':open(os.devnull, 'w') } # prevent mgdbserver subprocess from receiving CTRL-C if sys.platform.startswith('win'): args['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP else: def ignore_sigint(): import signal signal.signal(signal.SIGINT, signal.SIG_IGN) args['preexec_fn'] = ignore_sigint # use shell mode to ease passing arguments to a mgdbserver process args['shell'] = True return args _PORT_START = 22222 _PORT_RANGE = 1000 def _start_server(server, args, logfile, timeout=0.1): # shuffle ports to avoid a contest between multiple GDB instances # for the first numbered port ports = range(_PORT_START, _PORT_START + _PORT_RANGE) random.shuffle(ports) for port in ports: exec_string = '{} --lbc {} :{}'.format(server, args, port) # a process can to continue to write to logstream # after an exit from the scope # so we can't to use a context manager here logstream = open(logfile, 'w') popen_args = _get_popen_args(logstream) srv = subprocess.Popen(exec_string, **popen_args) time.sleep(timeout) return_code = srv.poll() if return_code is None: return port if return_code != 2: logstream.flush() logstream.close() raise_gdb_error( 'Failed to start a mgdbserver with the command line "{}"'.format(exec_string), logfile ) raise gdb.GdbError('Failed to find a free port.') def _get_remotetimeout(): """ Parse remotetimeout value. Unfortunately we are forced to parse remotetimeout value manually because an attempt to execute an expression `gdb.parameter("remotetimeout")` leads to an error: Traceback (most recent call last): File "", line 1, in RuntimeError: Programmer error: unhandled type. Error while executing Python code. """ import re pattern = r'Timeout limit to wait for target to respond is (\d+)\.' result = re.match(pattern, gdb.execute('show remotetimeout', to_string=True).strip() ) if not result: # crash and burn early raise RuntimeError('failed to parse remotetimeout value!') return int(result.group(1)) class OpenRemoteTargetArgParser(argparse.ArgumentParser): """A derived argument parser needed to properly display usage error. By default argparse calls sys.exit() on error. A better way to handle the situation would be raise GdbError instance with the usage string as a message. """ def error(self, message): io = StringIO.StringIO() self.print_usage(io) raise gdb.GdbError(message + '\n' + io.getvalue()) def _get_default_gdbserver_path(): """Calculate default mgdbserver path relative to the path of mgdbserver.py.""" import gdb.mgdbserver return os.path.abspath( os.path.join( os.path.dirname(gdb.__file__), '../../../bin/mgdbserver' + ('.exe' if os.name == 'nt' else '') ) ) def _disconnect(): """Kill previously opened remote target.""" try: gdb.execute('disconnect') except gdb.error: # We have no way of knowing whether the target has been open or not. # In the last case an exception will be raised. # So just swallow it here. pass def _unique_log_filename(basename='mgdbserver'): return os.path.join( tempfile.gettempdir(), basename + '-' + str(uuid.uuid4()) + '.log' ) def _parse_remote_device_descriptor(device_descriptor): """Split a device descriptor into a mjtagserver address and a device name or number.""" delim_index = device_descriptor.rfind('/') if delim_index == -1: return os.environ.get('MJTAGSRV_ADDR', ''), device_descriptor if delim_index + 1 == len(device_descriptor): raise gdb.GdbError( 'An invalid remote device descriptor provided: "{}"'.format(device_descriptor) ) return device_descriptor[0:delim_index], device_descriptor[delim_index+1:] class OpenRemoteTarget (gdb.Command): """Open the extended remote target. Usage: open-remote-target OPTIONS [CORE_NUM [CORE_NUM ...]] Parameter could be one of the following: 1. A simulator config name starting with a symbol '@' (simulator debugging). 2. a device number (emulator debugging). 3. a device name (emulator debugging). 4. a remote device descriptor ([mjtagserver_address/] Provide a path to a gdbserver. --gdbserver_args Pass custom mgdbserver arguments. Should be enclosed by quotes. Use these only if you know what you do. --target Remote target type. Valid values are "remote", "extended-remote". The default value is "extended-remote". See gdb user manual for additional details. """ def __init__ (self): command_name ="open-remote-target" super (OpenRemoteTarget, self).__init__ (command_name, gdb.COMMAND_USER) self.parser = OpenRemoteTargetArgParser(prog=command_name, add_help=False, description='Open remote target') self.parser.add_argument('device_descriptor') self.parser.add_argument('cores', metavar='CORE_NUM', type=int, nargs='*') self.parser.add_argument('--gdbserver_path', default=_get_default_gdbserver_path(), help=argparse.SUPPRESS) # redirect stdout/err to GDB by default self.parser.add_argument('--gdbserver_args', default='--inferriorio', help=argparse.SUPPRESS) self.parser.add_argument('--target', choices=['remote', 'extended-remote'], default='extended-remote') self.__target = 'extended-remote' def invoke (self, arg, from_tty): _disconnect() logfile = _unique_log_filename() print logfile gdbserver_path, gdbserver_args, target = self.__parse_args(arg) port = _start_server(gdbserver_path, gdbserver_args, logfile=logfile) if _get_remotetimeout() < 10: gdb.execute('set remotetimeout 10') try: gdb.execute('target {} :{}'.format(target, port)) except gdb.error: raise_gdb_error('Failed to open remote target', logfile) self.dont_repeat() def __parse_args(self, arg): # arg is an unicode string # convert to byte string to avoid unicode errors on windows str_arg = arg.encode(sys.getfilesystemencoding()) args = self.parser.parse_args(gdb.string_to_argv(str_arg)) # compose gdbserver arguments args_string = '' if args.device_descriptor.startswith('@'): # a simulator config args_string = '-s' + args.device_descriptor else: # a emulator device descriptor args_string += '--bi "connect {}" -d{}'.format( *_parse_remote_device_descriptor(args.device_descriptor) ) if args.cores: args_string += ' --cores {} --'.format(' '.join(map(str, args.cores))) # user provided arguments should be go at the end # to be able to override automatically supplied ones args_string += ' ' + args.gdbserver_args # normpath converts forward slashes to backward slashes on windows gdbserver_path = os.path.normpath(args.gdbserver_path) if not os.path.exists(gdbserver_path): raise gdb.GdbError('The path "{}" does not exists!' .format(gdbserver_path)) return gdbserver_path, args_string, args.target # register open-remote-target command OpenRemoteTarget ()