# 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 ()