#!/usr/bin/python # Copyright 2016 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import argparse import logging import os import re import sys if __name__ == '__main__': sys.path.append( os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) from devil.utils import cmd_helper from devil.utils import usb_hubs from devil.utils import lsusb logger = logging.getLogger(__name__) # Note: In the documentation below, "virtual port" refers to the port number # as observed by the system (e.g. by usb-devices) and "physical port" refers # to the physical numerical label on the physical port e.g. on a USB hub. # The mapping between virtual and physical ports is not always the identity # (e.g. the port labeled "1" on a USB hub does not always show up as "port 1" # when you plug something into it) but, as far as we are aware, the mapping # between virtual and physical ports is always the same for a given # model of USB hub. When "port number" is referenced without specifying, it # means the virtual port number. # Wrapper functions for system commands to get output. These are in wrapper # functions so that they can be more easily mocked-out for tests. def _GetParsedLSUSBOutput(): return lsusb.lsusb() def _GetUSBDevicesOutput(): return cmd_helper.GetCmdOutput(['usb-devices']) def _GetTtyUSBInfo(tty_string): cmd = ['udevadm', 'info', '--name=/dev/' + tty_string, '--attribute-walk'] return cmd_helper.GetCmdOutput(cmd) def _GetCommList(): return cmd_helper.GetCmdOutput('ls /dev', shell=True) def GetTTYList(): return [x for x in _GetCommList().splitlines() if 'ttyUSB' in x] # Class to identify nodes in the USB topology. USB topology is organized as # a tree. class USBNode(object): def __init__(self): self._port_to_node = {} @property def desc(self): raise NotImplementedError @property def info(self): raise NotImplementedError @property def device_num(self): raise NotImplementedError @property def bus_num(self): raise NotImplementedError def HasPort(self, port): """Determines if this device has a device connected to the given port.""" return port in self._port_to_node def PortToDevice(self, port): """Gets the device connected to the given port on this device.""" return self._port_to_node[port] def Display(self, port_chain='', info=False): """Displays information about this node and its descendants. Output format is, e.g. 1:3:3:Device 42 (ID 1234:5678 Some Device) meaning that from the bus, if you look at the device connected to port 1, then the device connected to port 3 of that, then the device connected to port 3 of that, you get the device assigned device number 42, which is Some Device. Note that device numbers will be reassigned whenever a connected device is powercycled or reinserted, but port numbers stay the same as long as the device is reinserted back into the same physical port. Args: port_chain: [string] Chain of ports from bus to this node (e.g. '2:4:') info: [bool] Whether to display detailed info as well. """ raise NotImplementedError def AddChild(self, port, device): """Adds child to the device tree. Args: port: [int] Port number of the device. device: [USBDeviceNode] Device to add. Raises: ValueError: If device already has a child at the given port. """ if self.HasPort(port): raise ValueError('Duplicate port number') else: self._port_to_node[port] = device def AllNodes(self): """Generator that yields this node and all of its descendants. Yields: [USBNode] First this node, then each of its descendants (recursively) """ yield self for child_node in self._port_to_node.values(): for descendant_node in child_node.AllNodes(): yield descendant_node def FindDeviceNumber(self, findnum): """Find device with given number in tree Searches the portion of the device tree rooted at this node for a device with the given device number. Args: findnum: [int] Device number to search for. Returns: [USBDeviceNode] Node that is found. """ for node in self.AllNodes(): if node.device_num == findnum: return node return None class USBDeviceNode(USBNode): def __init__(self, bus_num=0, device_num=0, serial=None, info=None): """Class that represents a device in USB tree. Args: bus_num: [int] Bus number that this node is attached to. device_num: [int] Device number of this device (or 0, if this is a bus) serial: [string] Serial number. info: [dict] Map giving detailed device info. """ super(USBDeviceNode, self).__init__() self._bus_num = bus_num self._device_num = device_num self._serial = serial self._info = {} if info is None else info #override @property def desc(self): return self._info.get('desc') #override @property def info(self): return self._info #override @property def device_num(self): return self._device_num #override @property def bus_num(self): return self._bus_num @property def serial(self): return self._serial @serial.setter def serial(self, serial): self._serial = serial #override def Display(self, port_chain='', info=False): logger.info('%s Device %d (%s)', port_chain, self.device_num, self.desc) if info: logger.info('%s', self.info) for (port, device) in self._port_to_node.iteritems(): device.Display('%s%d:' % (port_chain, port), info=info) class USBBusNode(USBNode): def __init__(self, bus_num=0): """Class that represents a node (either a bus or device) in USB tree. Args: is_bus: [bool] If true, node is bus; if not, node is device. bus_num: [int] Bus number that this node is attached to. device_num: [int] Device number of this device (or 0, if this is a bus) desc: [string] Short description of device. serial: [string] Serial number. info: [dict] Map giving detailed device info. port_to_dev: [dict(int:USBDeviceNode)] Maps port # to device connected to port. """ super(USBBusNode, self).__init__() self._bus_num = bus_num #override @property def desc(self): return 'BUS %d' % self._bus_num #override @property def info(self): return {} #override @property def device_num(self): return -1 #override @property def bus_num(self): return self._bus_num #override def Display(self, port_chain='', info=False): logger.info('=== %s ===', self.desc) for (port, device) in self._port_to_node.iteritems(): device.Display('%s%d:' % (port_chain, port), info=info) _T_LINE_REGEX = re.compile(r'T: Bus=(?P\d{2}) Lev=(?P\d{2}) ' r'Prnt=(?P\d{2,3}) Port=(?P\d{2}) ' r'Cnt=(?P\d{2}) Dev#=(?P.{3}) .*') _S_LINE_REGEX = re.compile(r'S: SerialNumber=(?P.*)') _LSUSB_BUS_DEVICE_RE = re.compile(r'^Bus (\d{3}) Device (\d{3}): (.*)') def GetBusNumberToDeviceTreeMap(fast=True): """Gets devices currently attached. Args: fast [bool]: whether to do it fast (only get description, not the whole dictionary, from lsusb) Returns: map of {bus number: bus object} where the bus object has all the devices attached to it in a tree. """ if fast: info_map = {} for line in lsusb.raw_lsusb().splitlines(): match = _LSUSB_BUS_DEVICE_RE.match(line) if match: info_map[(int(match.group(1)), int(match.group(2)))] = ( {'desc':match.group(3)}) else: info_map = {((int(line['bus']), int(line['device']))): line for line in _GetParsedLSUSBOutput()} tree = {} bus_num = -1 for line in _GetUSBDevicesOutput().splitlines(): match = _T_LINE_REGEX.match(line) if match: bus_num = int(match.group('bus')) parent_num = int(match.group('prnt')) # usb-devices starts counting ports from 0, so add 1 port_num = int(match.group('port')) + 1 device_num = int(match.group('dev')) # create new bus if necessary if bus_num not in tree: tree[bus_num] = USBBusNode(bus_num=bus_num) # create the new device new_device = USBDeviceNode(bus_num=bus_num, device_num=device_num, info=info_map.get((bus_num, device_num), {'desc': 'NOT AVAILABLE'})) # add device to bus if parent_num != 0: tree[bus_num].FindDeviceNumber(parent_num).AddChild( port_num, new_device) else: tree[bus_num].AddChild(port_num, new_device) match = _S_LINE_REGEX.match(line) if match: if bus_num == -1: raise ValueError('S line appears before T line in input file') # put the serial number in the device tree[bus_num].FindDeviceNumber(device_num).serial = match.group('serial') return tree def GetHubsOnBus(bus, hub_types): """Scans for all hubs on a bus of given hub types. Args: bus: [USBNode] Bus object. hub_types: [iterable(usb_hubs.HubType)] Possible types of hubs. Yields: Sequence of tuples representing (hub, type of hub) """ for device in bus.AllNodes(): for hub_type in hub_types: if hub_type.IsType(device): yield (device, hub_type) def GetPhysicalPortToNodeMap(hub, hub_type): """Gets physical-port:node mapping for a given hub. Args: hub: [USBNode] Hub to get map for. hub_type: [usb_hubs.HubType] Which type of hub it is. Returns: Dict of {physical port: node} """ port_device = hub_type.GetPhysicalPortToNodeTuples(hub) return {port: device for (port, device) in port_device} def GetPhysicalPortToBusDeviceMap(hub, hub_type): """Gets physical-port:(bus#, device#) mapping for a given hub. Args: hub: [USBNode] Hub to get map for. hub_type: [usb_hubs.HubType] Which type of hub it is. Returns: Dict of {physical port: (bus number, device number)} """ port_device = hub_type.GetPhysicalPortToNodeTuples(hub) return {port: (device.bus_num, device.device_num) for (port, device) in port_device} def GetPhysicalPortToSerialMap(hub, hub_type): """Gets physical-port:serial# mapping for a given hub. Args: hub: [USBNode] Hub to get map for. hub_type: [usb_hubs.HubType] Which type of hub it is. Returns: Dict of {physical port: serial number)} """ port_device = hub_type.GetPhysicalPortToNodeTuples(hub) return {port: device.serial for (port, device) in port_device if device.serial} def GetPhysicalPortToTTYMap(device, hub_type): """Gets physical-port:tty-string mapping for a given hub. Args: hub: [USBNode] Hub to get map for. hub_type: [usb_hubs.HubType] Which type of hub it is. Returns: Dict of {physical port: tty-string)} """ port_device = hub_type.GetPhysicalPortToNodeTuples(device) bus_device_to_tty = GetBusDeviceToTTYMap() return {port: bus_device_to_tty[(device.bus_num, device.device_num)] for (port, device) in port_device if (device.bus_num, device.device_num) in bus_device_to_tty} def CollectHubMaps(hub_types, map_func, device_tree_map=None, fast=False): """Runs a function on all hubs in the system and collects their output. Args: hub_types: [usb_hubs.HubType] List of possible hub types. map_func: [string] Function to run on each hub. device_tree: Previously constructed device tree map, if any. fast: Whether to construct device tree fast, if not already provided Yields: Sequence of dicts of {physical port: device} where the type of device depends on the ident keyword. Each dict is a separate hub. """ if device_tree_map is None: device_tree_map = GetBusNumberToDeviceTreeMap(fast=fast) for bus in device_tree_map.values(): for (hub, hub_type) in GetHubsOnBus(bus, hub_types): yield map_func(hub, hub_type) def GetAllPhysicalPortToNodeMaps(hub_types, **kwargs): return CollectHubMaps(hub_types, GetPhysicalPortToNodeMap, **kwargs) def GetAllPhysicalPortToBusDeviceMaps(hub_types, **kwargs): return CollectHubMaps(hub_types, GetPhysicalPortToBusDeviceMap, **kwargs) def GetAllPhysicalPortToSerialMaps(hub_types, **kwargs): return CollectHubMaps(hub_types, GetPhysicalPortToSerialMap, **kwargs) def GetAllPhysicalPortToTTYMaps(hub_types, **kwargs): return CollectHubMaps(hub_types, GetPhysicalPortToTTYMap, **kwargs) _BUS_NUM_REGEX = re.compile(r'.*ATTRS{busnum}=="(\d*)".*') _DEVICE_NUM_REGEX = re.compile(r'.*ATTRS{devnum}=="(\d*)".*') def GetBusDeviceFromTTY(tty_string): """Gets bus and device number connected to a ttyUSB port. Args: tty_string: [String] Identifier for ttyUSB (e.g. 'ttyUSB0') Returns: Tuple (bus, device) giving device connected to that ttyUSB. Raises: ValueError: If bus and device information could not be found. """ bus_num = None device_num = None # Expected output of GetCmdOutput should be something like: # looking at device /devices/something/.../.../... # KERNELS="ttyUSB0" # SUBSYSTEMS=... # DRIVERS=... # ATTRS{foo}=... # ATTRS{bar}=... # ... for line in _GetTtyUSBInfo(tty_string).splitlines(): bus_match = _BUS_NUM_REGEX.match(line) device_match = _DEVICE_NUM_REGEX.match(line) if bus_match and bus_num is None: bus_num = int(bus_match.group(1)) if device_match and device_num is None: device_num = int(device_match.group(1)) if bus_num is None or device_num is None: raise ValueError('Info not found') return (bus_num, device_num) def GetBusDeviceToTTYMap(): """Gets all mappings from (bus, device) to ttyUSB string. Gets mapping from (bus, device) to ttyUSB string (e.g. 'ttyUSB0'), for all ttyUSB strings currently active. Returns: [dict] Dict that maps (bus, device) to ttyUSB string """ result = {} for tty in GetTTYList(): result[GetBusDeviceFromTTY(tty)] = tty return result # This dictionary described the mapping between physical and # virtual ports on a Plugable 7-Port Hub (model USB2-HUB7BC). # Keys are the virtual ports, values are the physical port. # The entry 4:{1:4, 2:3, 3:2, 4:1} indicates that virtual port # 4 connects to another 'virtual' hub that itself has the # virtual-to-physical port mapping {1:4, 2:3, 3:2, 4:1}. def TestUSBTopologyScript(): """Test display and hub identification.""" # The following makes logger.info behave pretty much like print # during this test script. logging.basicConfig(format='%(message)s', stream=sys.stdout) logger.setLevel(logging.INFO) # Identification criteria for Plugable 7-Port Hub logger.info('==== USB TOPOLOGY SCRIPT TEST ====') logger.info('') # Display devices logger.info('==== DEVICE DISPLAY ====') device_trees = GetBusNumberToDeviceTreeMap() for device_tree in device_trees.values(): device_tree.Display() logger.info('') # Display TTY information about devices plugged into hubs. logger.info('==== TTY INFORMATION ====') for port_map in GetAllPhysicalPortToTTYMaps( usb_hubs.ALL_HUBS, device_tree_map=device_trees): logger.info('%s', port_map) logger.info('') # Display serial number information about devices plugged into hubs. logger.info('==== SERIAL NUMBER INFORMATION ====') for port_map in GetAllPhysicalPortToSerialMaps( usb_hubs.ALL_HUBS, device_tree_map=device_trees): logger.info('%s', port_map) return 0 def parse_options(argv): """Parses and checks the command-line options. Returns: A tuple containing the options structure and a list of categories to be traced. """ USAGE = '''./find_usb_devices [--help] This script shows the mapping between USB devices and port numbers. Clients are not intended to call this script from the command line. Clients are intended to call the functions in this script directly. For instance, GetAllPhysicalPortToSerialMaps(...) Running this script with --help will display this message. Running this script without --help will display information about devices attached, TTY mapping, and serial number mapping, for testing purposes. See design document for API documentation. ''' parser = argparse.ArgumentParser(usage=USAGE) return parser.parse_args(argv[1:]) def main(): parse_options(sys.argv) TestUSBTopologyScript() if __name__ == "__main__": sys.exit(main())