blob: aca9ace045f381df81568edcf47563b43984ecfd [file] [log] [blame]
# Copyright 2014 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Connect to a DUT in firmware via remote GDB, install custom GDB commands."""
from __future__ import print_function
import errno
import os
import re
import signal
import socket
import time
from chromite.cbuildbot import constants
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import osutils
from chromite.lib import timeout_util
# Need to do this before Servo import
# pylint: disable=import-error
from servo import client
from servo import multiservo
# pylint: enable=import-error
_SRC_ROOT = os.path.join(constants.CHROOT_SOURCE_ROOT, 'src')
_SRC_DC = os.path.join(_SRC_ROOT, 'platform/depthcharge')
_SRC_VB = os.path.join(_SRC_ROOT, 'platform/vboot_reference')
_SRC_LP = os.path.join(_SRC_ROOT, 'third_party/coreboot/payloads/libpayload')
_PTRN_DEVMODE = 'Entering VbBootDeveloper()'
_PTRN_GDB = 'Ready for GDB connection'
_PTRN_BOARD = 'Starting(?: read-only| read/write)? depthcharge on ([a-z_]+)...'
class TerminalFreezer(object):
"""SIGSTOP all processes (and their parents) that have the TTY open."""
def __init__(self, tty):
self._tty = tty
self._processes = None
if 'cros_sdk' in osutils.ReadFile('/proc/1/cmdline'):
raise OSError('You must run this tool in a chroot that was entered with '
'"cros_sdk --no-ns-pid" (see for details)')
def __enter__(self):
lsof = cros_build_lib.RunCommand(
['lsof', '-FR', self._tty],
capture_output=True, log_output=True, error_code_ok=True)
self._processes = re.findall(r'^(?:R|p)(\d+)$', lsof.output, re.MULTILINE)
# SIGSTOP parents before children
for p in reversed(self._processes):'Sending SIGSTOP to process %s!', p)
os.kill(int(p), signal.SIGSTOP)
except OSError:
self.__exit__(None, None, None)
def __exit__(self, _t, _v, _b):
# ...and wake 'em up again in reverse order
for p in self._processes:'Sending SIGCONT to process %s!', p)
os.kill(int(p), signal.SIGCONT)
except OSError as e:
logging.error('Error when trying to unfreeze process %s: %s', p, e)
def ParsePortage(board):
"""Parse some data from portage files. equery takes ages in comparison."""
with open(os.path.join('/build', board, 'packages/Packages'), 'r') as f:
chost = None
use = None
for line in f:
if line[:7] == 'CHOST: ':
chost = line[7:].strip()
if line[:5] == 'USE: ':
use = line[5:].strip()
if chost and use:
return (chost, use)
def ParseArgs(argv):
"""Parse and validate command line arguments."""
parser = commandline.ArgumentParser(default_log_level='warning')
parser.add_argument('-b', '--board',
help='The board overlay name (auto-detect by default)')
parser.add_argument('-s', '--symbols',
help='Root directory or complete path to symbolized ELF '
'(defaults to /build/<BOARD>/firmware)')
parser.add_argument('-r', '--reboot', choices=['yes', 'no', 'auto'],
help='Reboot the DUT before connect (default: reboot if '
'the remote and is unreachable)', default='auto')
parser.add_argument('-e', '--execute', action='append', default=[],
help='GDB command to run after connect (can be supplied '
'multiple times)')
parser.add_argument('-n', '--servod-name', dest='name')
parser.add_argument('--servod-rcfile', default=multiservo.DEFAULT_RC_FILE)
parser.add_argument('-p', '--servod-port', type=int, dest='port')
parser.add_argument('-t', '--tty',
help='TTY file to connect to (defaults to cpu_uart_pty)')
opts = parser.parse_args(argv)
multiservo.get_env_options(logging, opts)
rc = multiservo.parse_rc(logging, opts.servod_rcfile)
if not in rc:
raise parser.error('%s not in %s' % (, opts.servod_rcfile))
if not opts.servod_server:
opts.servod_server = rc[]['sn']
if not opts.port:
opts.port = rc[].get('port', client.DEFAULT_PORT)
if not opts.board and 'board' in rc[]:
opts.board = rc[]['board']
logging.warning('Inferring board %s from %s; make sure this is correct!',
opts.board, opts.servod_rcfile)
if not opts.servod_server:
opts.servod_server = client.DEFAULT_HOST
if not opts.port:
opts.port = client.DEFAULT_PORT
return opts
def FindSymbols(firmware_dir, board, use):
"""Find the symbolized depthcharge ELF (may be supplied by -s flag)."""
if not firmware_dir:
firmware_dir = os.path.join(cros_build_lib.GetSysroot(board), 'firmware')
# Allow overriding the file directly just in case our detection screws up
if firmware_dir.endswith('.elf'):
return firmware_dir
if 'unified_depthcharge' in use:
basename = 'dev.elf'
basename = ''
path = os.path.join(firmware_dir, 'depthcharge', basename)
if not os.path.exists(path):
path = os.path.join(firmware_dir, basename)
if os.path.exists(path):
logging.warning('Auto-detected symbol file at %s... make sure that this '
'matches the image on your DUT!', path)
return path
raise ValueError('Could not find %s symbol file!' % basename)
# TODO(jwerner): Fine tune |wait| delay or maybe even make it configurable if
# this causes problems due to load on the host. The callers where this is
# critical should all have their own timeouts now, though, so it's questionable
# whether the delay here is even needed at all anymore.
def ReadAll(fd, wait=0.03):
"""Read from |fd| until no more data has come for at least |wait| seconds."""
data = ''
while True:
data +=, 4096)
except OSError as e:
if e.errno == errno.EAGAIN:
return data
def GdbChecksum(message):
"""Calculate a remote-GDB style checksum."""
chksum = sum([ord(x) for x in message])
return ('%.2x' % chksum)[-2:]
def TestConnection(fd):
"""Return True iff there is a resposive GDB stub on the other end of 'fd'."""
cmd = 'vUnknownCommand'
for _ in xrange(3):
os.write(fd, '$%s#%s\n' % (cmd, GdbChecksum(cmd)))
reply = ReadAll(fd)
if '+$#00' in reply:
os.write(fd, '+')'TestConnection: Could successfully connect to remote end.')
return True'TestConnection: Remote end does not respond.')
return False
def main(argv):
opts = ParseArgs(argv)
servo = client.ServoClient(host=opts.servod_server, port=opts.port)
if not opts.tty:
opts.tty = servo.get('cpu_uart_pty')
except (client.ServoClientError, socket.error):
logging.error('Cannot auto-detect TTY file without servod. Use the --tty '
with TerminalFreezer(opts.tty):
fd =, os.O_RDWR | os.O_NONBLOCK)
data = ReadAll(fd)
if opts.reboot == 'auto':
if TestConnection(fd):
opts.reboot = 'no'
opts.reboot = 'yes'
if opts.reboot == 'yes':'Rebooting DUT...')
servo.set('warm_reset', 'on')
servo.set('warm_reset', 'off')
except (client.ServoClientError, socket.error):
logging.error('Cannot reboot without a Servo board. You have to boot '
'into developer mode and press CTRL+G manually before '
'running fwgdb.')
# Throw away old data to avoid confusion from messages before the reboot
data = ''
msg = ('Could not reboot into developer mode! '
'(Confirm that you have GBB_FLAG_FORCE_DEV_SWITCH_ON (0x8) set.)')
with timeout_util.Timeout(10, msg):
while _PTRN_DEVMODE not in data:
data += ReadAll(fd)
# Send a CTRL+G'Developer mode detected, pressing CTRL+G...')
os.write(fd, chr(ord('G') & 0x1f))
msg = ('Could not enter GDB mode with CTRL+G! '
'(Confirm that you flashed an "" image to this DUT.)')
with timeout_util.Timeout(1, msg):
while _PTRN_GDB not in data:
data += ReadAll(fd)
if not opts.board:
matches = re.findall(_PTRN_BOARD, data)
if not matches:
raise ValueError('Could not auto-detect board! Please use -b option.')
opts.board = matches[-1]'Auto-detected board as %s from DUT console output.',
if not TestConnection(fd):
raise IOError('Could not connect to remote end! Confirm that your DUT is '
'running in GDB mode on %s.' % opts.tty)
# Eat up leftover data or it will spill back to terminal
opts.execute.insert(0, 'target remote %s' % opts.tty)
ex_args = sum([['--ex', cmd] for cmd in opts.execute], [])
chost, use = ParsePortage(opts.board)'Launching GDB...')
[chost + '-gdb',
'--symbols', FindSymbols(opts.symbols, opts.board, use),
'--directory', _SRC_DC,
'--directory', _SRC_VB,
'--directory', _SRC_LP] + ex_args,
ignore_sigint=True, debug_level=logging.WARNING)