| #!/usr/bin/env python2 |
| # Copyright (c) 2012 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. |
| """Python version of Servo hardware debug & control board server.""" |
| |
| # pylint: disable=g-bad-import-order |
| # pkg_resources is erroneously suggested to be in the 3rd party segment |
| from __future__ import print_function |
| import collections |
| import errno |
| import logging |
| import os |
| import pkg_resources |
| import select |
| import signal |
| try: |
| from SimpleXMLRPCServer import SimpleXMLRPCServer |
| except ImportError: |
| from xmlrpc.server import SimpleXMLRPCServer |
| # TODO(crbug.com/999878): This is for python3 compatibility. |
| # Remove once fully moved to python3. |
| import socket |
| import sys |
| import threading |
| import time |
| |
| import interface.ftdi_common |
| import recovery |
| import servo_interfaces |
| import servo_logging |
| import servo_parsing |
| import servo_postinit |
| import servo_server |
| import system_config |
| import terminal_freezer |
| import usb |
| import utils.scratch as scratch |
| import watchdog |
| |
| MAX_ISERIAL_STR = 128 |
| |
| # If user does not specify a log directory, use this one. |
| DEFAULT_LOG_DIR = '/var/log' |
| |
| # If user does not specify a port to use, try ports in this range. Traverse |
| # the range from high to low addresses to maintain backwards compatibility |
| # (the first checked default port is 9999, the range is such that all possible |
| # port numbers are 4 digits). |
| DEFAULT_PORT_RANGE = (9980, 9999) |
| |
| |
| def usb_get_iserial(device): |
| """Get USB device's iSerial string. |
| |
| Args: |
| device: usb.Device object |
| |
| Returns: |
| iserial: USB devices iSerial string or empty string if the device has |
| no serial number. |
| """ |
| # pylint: disable=broad-except |
| device_handle = device.open() |
| # The device has no serial number string descriptor. |
| if device.iSerialNumber == 0: |
| return '' |
| iserial = '' |
| try: |
| iserial = device_handle.getString(device.iSerialNumber, MAX_ISERIAL_STR) |
| except usb.USBError: |
| # TODO(tbroch) other non-FTDI devices on my host cause following msg |
| # usb.USBError: error sending control message: Broken pipe |
| # Need to investigate further |
| pass |
| except Exception: |
| # This was causing servod to fail to start in the presence of |
| # a broken usb interface. |
| logging.exception('usb_get_iserial failed in an unknown way') |
| return iserial |
| |
| |
| def usb_find(vendor, product, serialname): |
| """Find USB devices based on vendor, product and serial identifiers. |
| |
| Locates all USB devices that match the criteria of the arguments. In the |
| case where input arguments are 'None' that argument is a don't care |
| |
| Args: |
| vendor: USB vendor id (integer) |
| product: USB product id (integer) |
| serialname: USB serial id (string) |
| |
| Returns: |
| matched_devices : list of pyusb devices matching input args |
| """ |
| matched_devices = [] |
| for bus in usb.busses(): |
| for device in bus.devices: |
| if (not vendor or device.idVendor == vendor) and \ |
| (not product or device.idProduct == product) and \ |
| (not serialname or usb_get_iserial(device).endswith(serialname)): |
| matched_devices.append(device) |
| return matched_devices |
| |
| |
| # pylint: disable=g-bad-exception-name |
| class ServodError(Exception): |
| """Exception class for servod server.""" |
| pass |
| |
| |
| class ServodStarter(object): |
| """Class to manage servod instance and rpc server its being served on.""" |
| |
| # Timeout period after which to just turn down, regardless of threads |
| # needing to clean up. |
| EXIT_TIMEOUT_S = 20 |
| |
| def __init__(self, cmdline): |
| """Prepare servod invocation. |
| |
| Parse cmdline and prompt user for missing information if necessary to start |
| servod. Prepare servod instance & thread for it to be served from. |
| |
| Args: |
| cmdline: list, cmdline components to parse |
| |
| Raises: |
| ServodError: if automatic config cannot be found |
| """ |
| # The scratch initialization here ensures that potentially stale entries |
| # are removed from the scratch before attempting to create a new one. |
| self._scratchutil = scratch.Scratch() |
| # Initialize logging up here first to ensure log messages from parsing |
| # can go through. |
| loglevel, fmt = servo_logging.LOGLEVEL_MAP[servo_logging.DEFAULT_LOGLEVEL] |
| logging.basicConfig(level=loglevel, format=fmt) |
| self._logger = logging.getLogger(os.path.basename(sys.argv[0])) |
| sopts, devopts = self._parse_args(cmdline) |
| self._host = sopts.host |
| |
| # Turn on recovery mode if requested. |
| if sopts.recovery_mode: |
| recovery.set_recovery_active() |
| |
| if servo_parsing.ArgMarkedAsUserSupplied(sopts, 'port'): |
| start_port = sopts.port |
| end_port = sopts.port |
| else: |
| end_port, start_port = DEFAULT_PORT_RANGE |
| for self._servo_port in range(start_port, end_port - 1, -1): |
| try: |
| self._server = SimpleXMLRPCServer((self._host, self._servo_port), |
| logRequests=False) |
| break |
| except socket.error as e: |
| if e.errno == errno.EADDRINUSE: |
| continue # Port taken, see if there is another one next to it. |
| self._logger.fatal("Problem opening Server's socket: %s", e) |
| sys.exit(-1) |
| else: |
| if start_port == end_port: |
| # This condition indicates that a specific port was being requested. |
| # Report that the port itself is busy. |
| err_msg = ('Port %d is busy' % sopts.port) |
| else: |
| err_msg = ('Could not find a free port in %d..%d range' % (end_port, |
| start_port)) |
| |
| self._logger.fatal(err_msg) |
| sys.exit(-1) |
| servo_logging.setup(logdir=sopts.log_dir, port=self._servo_port, |
| debug_stdout=sopts.debug, |
| backup_count=sopts.log_dir_backup_count) |
| |
| if sopts.dual_v4: |
| # Leave the right breadcrumbs for servo_postinit to know whether to setup |
| # a dual instance or not. |
| os.environ[servo_postinit.DUAL_V4_VAR] = servo_postinit.DUAL_V4_VAR_EMPTY |
| |
| # Servod needs to be running in the chroot without PID namespaces in order |
| # to freeze terminals when reading from the UARTs. |
| terminal_freezer.CheckForPIDNamespace() |
| |
| self._logger.info('Start') |
| |
| servo_device = self.discover_servo(devopts) |
| if not servo_device: |
| sys.exit(-1) |
| |
| lot_id = self.get_lot_id(servo_device) |
| board_version = self.get_board_version(lot_id, servo_device.idProduct) |
| self._logger.debug('board_version = %s', board_version) |
| all_configs = [] |
| if not devopts.noautoconfig: |
| all_configs += self.get_auto_configs(board_version) |
| |
| if devopts.config: |
| for config in devopts.config: |
| # quietly ignore duplicate configs for backwards compatibility |
| if config not in all_configs: |
| all_configs.append(config) |
| |
| if not all_configs: |
| raise ServodError('No automatic config found,' |
| ' and no config specified with -c <file>') |
| |
| scfg = system_config.SystemConfig() |
| |
| if devopts.board: |
| # Handle differentiated model case. |
| board_config = None |
| if devopts.model: |
| board_config = 'servo_%s_%s_overlay.xml' % ( |
| devopts.board, devopts.model) |
| |
| if not scfg.find_cfg_file(board_config): |
| self._logger.info('No XML overlay for model ' |
| '%s, falling back to board %s default', |
| devopts.model, devopts.board) |
| board_config = None |
| else: |
| self._logger.info('Found XML overlay for model %s:%s', |
| devopts.board, devopts.model) |
| |
| # Handle generic board config. |
| if not board_config: |
| board_config = 'servo_' + devopts.board + '_overlay.xml' |
| if not scfg.find_cfg_file(board_config): |
| self._logger.error('No XML overlay for board %s', devopts.board) |
| sys.exit(-1) |
| |
| self._logger.info('Found XML overlay for board %s', devopts.board) |
| |
| all_configs.append(board_config) |
| scfg.set_board_cfg(board_config) |
| |
| for cfg_file in all_configs: |
| scfg.add_cfg_file(cfg_file) |
| |
| self._logger.debug('\n%s', scfg.display_config()) |
| |
| self._logger.debug('Servo is vid:0x%04x pid:0x%04x sid:%s', |
| servo_device.idVendor, servo_device.idProduct, |
| usb_get_iserial(servo_device)) |
| |
| self._servod = servo_server.Servod( |
| scfg, vendor=servo_device.idVendor, product=servo_device.idProduct, |
| serialname=usb_get_iserial(servo_device), |
| interfaces=devopts.interfaces.split(), board=devopts.board, |
| model=devopts.model, version=board_version, usbkm232=devopts.usbkm232) |
| |
| # Small timeout to allow interface threads to initialize. |
| time.sleep(0.5) |
| |
| self._servod.hwinit(verbose=True) |
| self._server.register_introspection_functions() |
| self._server.register_multicall_functions() |
| self._server.register_instance(self._servod) |
| self._server_thread = threading.Thread(target=self._serve) |
| self._server_thread.daemon = True |
| self._turndown_initiated = False |
| # pylint: disable=protected-access |
| # Needs access to the servod instance. |
| self._watchdog_thread = watchdog.DeviceWatchdog(self._servod) |
| self._exit_status = 0 |
| |
| def handle_sig(self, signum): |
| """Handle a signal by turning off the server & cleaning up servod.""" |
| if not self._turndown_initiated: |
| self._turndown_initiated = True |
| self._logger.info('Received signal: %d. Attempting to turn off', signum) |
| self._server.shutdown() |
| self._server.server_close() |
| self._servod.close() |
| self._logger.info('Successfully turned off') |
| |
| def _parse_args(self, cmdline): |
| """Parse commandline arguments. |
| |
| Args: |
| cmdline: list of cmdline arguments |
| |
| Returns: |
| tuple: (server, dev) args Namespaces after parsing & processing cmdline |
| server: holds --port, --host, --log-dir, --allow-dual-v4, --debug flags |
| dev: holds all the device flags (serialname, interfaces, configs etc - |
| see below) necessary to configure a servo device. |
| """ |
| description = ( |
| '%(prog)s is server to interact with servo debug & control board. ' |
| 'This server communicates to the board via USB and the client via ' |
| 'xmlrpc library. Launcher most specify at least one --config <file> ' |
| 'in order for the server to provide any functionality. In most cases, ' |
| 'multiple configs will be needed to expose complete functionality ' |
| 'between debug & DUT board.') |
| examples = [('-c <path>/data/servo.xml', |
| 'Launch server on default host:port with native servo config'), |
| ('-c <file> -p 8888', 'Launch server listening on port 8888'), |
| ('-c <file> --vendor 0x18d1 --product 0x5001', |
| 'Launch targetting usb device with vid:pid == 0x18d1:0x5001 ' |
| '(Google/Servo)')] |
| # BaseServodParser adds port, host, debug args. |
| server_pars = servo_parsing.BaseServodParser(add_help=False) |
| log_dir = server_pars.add_mutually_exclusive_group() |
| log_dir.add_argument('--no-log-dir', default=False, action='store_true', |
| help='Turn off log dir functionality.') |
| log_dir.add_argument('--log-dir', type=str, default=DEFAULT_LOG_DIR, |
| help='path where to dump servod debug logs as a file. ' |
| 'If flag omitted default path is used') |
| server_pars.add_argument('--log-dir-backup-count', type=int, |
| default=servo_logging.LOG_BACKUP_COUNT, |
| help='Max number of backup logs that will be ' |
| 'kept per loglevel for one servod port. Reminder: ' |
| 'files get rotated on new instance, by user ' |
| 'request or when they grow past %d bytes. After ' |
| 'the newest %d files they are compressed. ' |
| 'inactive when no log dir requested.' % |
| (servo_logging.MAX_LOG_BYTES, |
| servo_logging.UNCOMPRESSED_BACKUP_COUNT)) |
| server_pars.add_argument('--allow-dual-v4', dest='dual_v4', default=False, |
| action='store_true', |
| help='Allow dual micro and ccd on servo v4.') |
| server_pars.add_argument('--recovery_mode', default=False, |
| action='store_true', |
| help='Start servod through issues to allow for ' |
| 'inspection and recovery mechanisms.') |
| # ServodRCParser adds configs for -name/-rcfile & serialname & parses them. |
| dev_pars = servo_parsing.ServodRCParser(add_help=False) |
| dev_pars.add_argument('--vendor', default=None, type=lambda x: int(x, 0), |
| help='vendor id of device to interface to') |
| dev_pars.add_argument('--product', default=None, type=lambda x: int(x, 0), |
| help='USB product id of device to interface with') |
| dev_pars.add_argument('-c', '--config', default=None, type=str, |
| action='append', help='system config file (XML) to ' |
| 'read') |
| dev_pars.add_argument('-b', '--board', default='', type=str, |
| action='store', help='include config file (XML) for ' |
| 'given board') |
| dev_pars.add_argument('-m', '--model', default='', type=str, action='store', |
| help='optional config for a model of the given board,' |
| ' requires --board') |
| dev_pars.add_argument('--noautoconfig', action='store_true', default=False, |
| help='Disable automatic determination of config ' |
| 'files') |
| dev_pars.add_argument('-i', '--interfaces', type=str, default='', |
| help='ordered space-delimited list of interfaces. ' |
| 'Valid choices are gpio|i2c|uart|gpiouart|empty') |
| dev_pars.add_argument('-u', '--usbkm232', type=str, |
| help='path to USB-KM232 device which allow for ' |
| 'sending keyboard commands to DUTs that do not ' |
| 'have built in keyboards. Used in FAFT tests. ' |
| '(Optional), e.g. /dev/ttyUSB0') |
| # Create a unified parser with both server & device arguments to display |
| # meaningful help messages to the user. |
| # pylint: disable=protected-access |
| # The parser here is used for its base ability to format examples. |
| help_displayer = servo_parsing._BaseServodParser(description=description, |
| examples=examples, |
| parents=[server_pars, |
| dev_pars]) |
| if any([True for argstr in cmdline if argstr in ['-h', '--help']]): |
| help_displayer.print_help() |
| help_displayer.exit() |
| # Both parsers should display the same usage information when an |
| # argument is not found. Fix it here by pointing both of their methods |
| # to the help_displayer. |
| server_pars.format_usage = help_displayer.format_usage |
| dev_pars.format_usage = help_displayer.format_usage |
| server_args, dev_cmdline = server_pars.parse_known_args(cmdline) |
| # Adjust log-dir to be None if no_log_dir is requested. |
| if server_args.no_log_dir: |
| server_args.log_dir = None |
| dev_args = dev_pars.parse_args(dev_cmdline) |
| return (server_args, dev_args) |
| |
| def choose_servo(self, all_servos): |
| """Let user choose a servo from available list of unique devices. |
| |
| Args: |
| all_servos: a list of servod objects corresponding to discovered servo |
| devices |
| |
| Returns: |
| servo object for the matching (or single) device, otherwise None |
| """ |
| self._logger.info('') |
| for i, servo in enumerate(all_servos): |
| self._logger.info("Press '%d' for servo, vid: 0x%04x pid: 0x%04x sid: %s", |
| i, servo.idVendor, servo.idProduct, |
| usb_get_iserial(servo)) |
| |
| (rlist, _, _) = select.select([sys.stdin], [], [], 10) |
| if not rlist: |
| self._logger.warn('Timed out waiting for your choice\n') |
| return None |
| |
| rsp = rlist[0].readline().strip() |
| try: |
| rsp = int(rsp) |
| except ValueError: |
| self._logger.warn('%s not a valid choice ... ignoring', rsp) |
| return None |
| |
| if rsp < 0 or rsp >= len(all_servos): |
| self._logger.warn('%s outside of choice range ... ignoring', rsp) |
| return None |
| |
| logging.info('') |
| servo = all_servos[rsp] |
| logging.info('Chose %d ... starting servod on servo ' |
| 'vid: 0x%04x pid: 0x%04x sid: %s', rsp, servo.idVendor, |
| servo.idProduct, usb_get_iserial(servo)) |
| logging.info('') |
| return servo |
| |
| def discover_servo(self, options): |
| """Find a servo USB device to use. |
| |
| First, find all servo devices matching command line options, this may result |
| in discovering none, one or more devices. |
| |
| If there is a match - return the matching device. |
| |
| If there is only one servo connected - return it. |
| If there is no match found and multiple servos are connected - report an |
| error and return None. |
| |
| Args: |
| options: the options object returned by arg_parse |
| |
| Returns: |
| servo object for the matching (or single) device, otherwise None |
| """ |
| |
| vendor, product, serialname = (options.vendor, options.product, |
| options.serialname) |
| all_servos = [] |
| for (vid, pid) in servo_interfaces.SERVO_ID_DEFAULTS: |
| if (vendor and vendor != vid) or \ |
| (product and product != pid): |
| continue |
| all_servos.extend(usb_find(vid, pid, serialname)) |
| |
| if not all_servos: |
| self._logger.error('No servos found') |
| return None |
| |
| if len(all_servos) == 1: |
| return all_servos[0] |
| |
| # See if only one primary servo. Filter secondary servos. |
| secondary_servos = ( |
| servo_interfaces.SERVO_MICRO_DEFAULTS + |
| servo_interfaces.CCD_DEFAULTS + |
| servo_interfaces.C2D2_DEFAULTS) |
| all_primary_servos = [ |
| servo for servo in all_servos |
| if (servo.idVendor, servo.idProduct) not in secondary_servos |
| ] |
| if len(all_primary_servos) == 1: |
| return all_primary_servos[0] |
| |
| # Let user choose a servo |
| matching_servo = self.choose_servo(all_servos) |
| if matching_servo: |
| return matching_servo |
| |
| self._logger.error('Use --vendor, --product or --serialname switches to ' |
| 'identify servo uniquely, or create a servodrc file ' |
| 'and use the --name switch') |
| |
| return None |
| |
| def get_board_version(self, lot_id, product_id): |
| """Get board version string. |
| |
| Typically this will be a string of format <boardname>_<version>. |
| For example, servo_v2. |
| |
| Args: |
| lot_id: string, identifying which lot device was fabbed from or None |
| product_id: integer, USB product id |
| |
| Returns: |
| board_version: string, board & version or None if not found |
| """ |
| if lot_id: |
| for (board_version, lot_ids) in \ |
| interface.ftdi_common.SERVO_LOT_ID_DEFAULTS.items(): |
| if lot_id in lot_ids: |
| return board_version |
| |
| for (board_version, vids) in \ |
| interface.ftdi_common.SERVO_PID_DEFAULTS.items(): |
| if product_id in vids: |
| return board_version |
| |
| return None |
| |
| def get_lot_id(self, servo): |
| """Get lot_id for a given servo. |
| |
| Args: |
| servo: usb.Device object |
| |
| Returns: |
| lot_id of the servo device. |
| """ |
| lot_id = None |
| iserial = usb_get_iserial(servo) |
| self._logger.debug('iserial = %s', iserial) |
| if not iserial: |
| self._logger.warn('Servo device has no iserial value') |
| else: |
| try: |
| (lot_id, _) = iserial.split('-') |
| except ValueError: |
| self._logger.warn("Servo device's iserial was unrecognized.") |
| return lot_id |
| |
| def get_auto_configs(self, board_version): |
| """Get xml configs that should be loaded. |
| |
| Args: |
| board_version: string, board & version |
| |
| Returns: |
| configs: list of XML config files that should be loaded |
| """ |
| if board_version not in interface.ftdi_common.SERVO_CONFIG_DEFAULTS: |
| self._logger.warning('Unable to determine configs to load for board ' |
| 'version = %s', board_version) |
| return [] |
| return interface.ftdi_common.SERVO_CONFIG_DEFAULTS[board_version] |
| |
| def cleanup(self): |
| """Perform any cleanup related work after servod server shut down.""" |
| self._scratchutil.RemoveEntry(self._servo_port) |
| self._logger.info('Server on %s port %s turned down', self._host, |
| self._servo_port) |
| servo_logging.cleanup() |
| |
| def _serve(self): |
| """Wrapper around rpc server's serve_forever to catch server errors.""" |
| # pylint: disable=broad-except |
| self._logger.info('Listening on %s port %s', self._host, self._servo_port) |
| try: |
| self._server.serve_forever() |
| except Exception: |
| self._exit_status = 1 |
| |
| def serve(self): |
| """Add signal handlers, start servod on its own thread & wait for signal. |
| |
| Intercepts and handles stop signals so shutdown is handled. |
| """ |
| handler = lambda signal, unused, starter=self: starter.handle_sig(signal) |
| stop_signals = [signal.SIGHUP, signal.SIGINT, signal.SIGQUIT, |
| signal.SIGTERM, signal.SIGTSTP] |
| for ss in stop_signals: |
| signal.signal(ss, handler) |
| serials = set(self._servod.get_servo_serials().values()) |
| try: |
| self._scratchutil.AddEntry(self._servo_port, serials, os.getpid()) |
| except scratch.ScratchError: |
| self._servod.close() |
| sys.exit(1) |
| self._watchdog_thread.start() |
| self._server_thread.start() |
| # Indicate that servod is running for any process waiting to know. |
| self._scratchutil.MarkActive(self._servo_port) |
| signal.pause() |
| # Set watchdog thread to end |
| self._watchdog_thread.deactivate() |
| # Collect servo and watchdog threads |
| self._server_thread.join(self.EXIT_TIMEOUT_S) |
| if self._server_thread.isAlive(): |
| self._logger.error('Server thread not turned down after %s s.', |
| self.EXIT_TIMEOUT_S) |
| self._watchdog_thread.join(self.EXIT_TIMEOUT_S) |
| if self._watchdog_thread.isAlive(): |
| self._logger.error('Watchdog thread not turned down after %s s.', |
| self.EXIT_TIMEOUT_S) |
| self.cleanup() |
| sys.exit(self._exit_status) |
| |
| |
| # pylint: disable=dangerous-default-value |
| # Ability to pass an arbitrary or artifical cmdline for testing is desirable. |
| def main(cmdline=sys.argv[1:]): |
| try: |
| starter = ServodStarter(cmdline) |
| except ServodError as e: |
| print('Error: ', e.message) |
| sys.exit(1) |
| starter.serve() |
| |
| if __name__ == '__main__': |
| main() |