Home | History | Annotate | Download | only in utils
      1 #!/usr/bin/python
      2 # Copyright 2016 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 import argparse
      7 import logging
      8 import os
      9 import re
     10 import sys
     11 
     12 if __name__ == '__main__':
     13   sys.path.append(
     14       os.path.abspath(os.path.join(os.path.dirname(__file__),
     15                                    '..', '..')))
     16 
     17 from devil.utils import cmd_helper
     18 from devil.utils import usb_hubs
     19 from devil.utils import lsusb
     20 
     21 logger = logging.getLogger(__name__)
     22 
     23 # Note: In the documentation below, "virtual port" refers to the port number
     24 # as observed by the system (e.g. by usb-devices) and "physical port" refers
     25 # to the physical numerical label on the physical port e.g. on a USB hub.
     26 # The mapping between virtual and physical ports is not always the identity
     27 # (e.g. the port labeled "1" on a USB hub does not always show up as "port 1"
     28 # when you plug something into it) but, as far as we are aware, the mapping
     29 # between virtual and physical ports is always the same for a given
     30 # model of USB hub. When "port number" is referenced without specifying, it
     31 # means the virtual port number.
     32 
     33 
     34 # Wrapper functions for system commands to get output. These are in wrapper
     35 # functions so that they can be more easily mocked-out for tests.
     36 def _GetParsedLSUSBOutput():
     37   return lsusb.lsusb()
     38 
     39 
     40 def _GetUSBDevicesOutput():
     41   return cmd_helper.GetCmdOutput(['usb-devices'])
     42 
     43 
     44 def _GetTtyUSBInfo(tty_string):
     45   cmd = ['udevadm', 'info', '--name=/dev/' + tty_string, '--attribute-walk']
     46   return cmd_helper.GetCmdOutput(cmd)
     47 
     48 
     49 def _GetCommList():
     50   return cmd_helper.GetCmdOutput('ls /dev', shell=True)
     51 
     52 
     53 def GetTTYList():
     54   return [x for x in _GetCommList().splitlines() if 'ttyUSB' in x]
     55 
     56 
     57 # Class to identify nodes in the USB topology. USB topology is organized as
     58 # a tree.
     59 class USBNode(object):
     60   def __init__(self):
     61     self._port_to_node = {}
     62 
     63   @property
     64   def desc(self):
     65     raise NotImplementedError
     66 
     67   @property
     68   def info(self):
     69     raise NotImplementedError
     70 
     71   @property
     72   def device_num(self):
     73     raise NotImplementedError
     74 
     75   @property
     76   def bus_num(self):
     77     raise NotImplementedError
     78 
     79   def HasPort(self, port):
     80     """Determines if this device has a device connected to the given port."""
     81     return port in self._port_to_node
     82 
     83   def PortToDevice(self, port):
     84     """Gets the device connected to the given port on this device."""
     85     return self._port_to_node[port]
     86 
     87   def Display(self, port_chain='', info=False):
     88     """Displays information about this node and its descendants.
     89 
     90     Output format is, e.g. 1:3:3:Device 42 (ID 1234:5678 Some Device)
     91     meaning that from the bus, if you look at the device connected
     92     to port 1, then the device connected to port 3 of that,
     93     then the device connected to port 3 of that, you get the device
     94     assigned device number 42, which is Some Device. Note that device
     95     numbers will be reassigned whenever a connected device is powercycled
     96     or reinserted, but port numbers stay the same as long as the device
     97     is reinserted back into the same physical port.
     98 
     99     Args:
    100       port_chain: [string] Chain of ports from bus to this node (e.g. '2:4:')
    101       info: [bool] Whether to display detailed info as well.
    102     """
    103     raise NotImplementedError
    104 
    105   def AddChild(self, port, device):
    106     """Adds child to the device tree.
    107 
    108     Args:
    109       port: [int] Port number of the device.
    110       device: [USBDeviceNode] Device to add.
    111 
    112     Raises:
    113       ValueError: If device already has a child at the given port.
    114     """
    115     if self.HasPort(port):
    116       raise ValueError('Duplicate port number')
    117     else:
    118       self._port_to_node[port] = device
    119 
    120   def AllNodes(self):
    121     """Generator that yields this node and all of its descendants.
    122 
    123     Yields:
    124       [USBNode] First this node, then each of its descendants (recursively)
    125     """
    126     yield self
    127     for child_node in self._port_to_node.values():
    128       for descendant_node in child_node.AllNodes():
    129         yield descendant_node
    130 
    131   def FindDeviceNumber(self, findnum):
    132     """Find device with given number in tree
    133 
    134     Searches the portion of the device tree rooted at this node for
    135     a device with the given device number.
    136 
    137     Args:
    138       findnum: [int] Device number to search for.
    139 
    140     Returns:
    141       [USBDeviceNode] Node that is found.
    142     """
    143     for node in self.AllNodes():
    144       if node.device_num == findnum:
    145         return node
    146     return None
    147 
    148 
    149 class USBDeviceNode(USBNode):
    150   def __init__(self, bus_num=0, device_num=0, serial=None, info=None):
    151     """Class that represents a device in USB tree.
    152 
    153     Args:
    154       bus_num: [int] Bus number that this node is attached to.
    155       device_num: [int] Device number of this device (or 0, if this is a bus)
    156       serial: [string] Serial number.
    157       info: [dict] Map giving detailed device info.
    158     """
    159     super(USBDeviceNode, self).__init__()
    160     self._bus_num = bus_num
    161     self._device_num = device_num
    162     self._serial = serial
    163     self._info = {} if info is None else info
    164 
    165   #override
    166   @property
    167   def desc(self):
    168     return self._info.get('desc')
    169 
    170   #override
    171   @property
    172   def info(self):
    173     return self._info
    174 
    175   #override
    176   @property
    177   def device_num(self):
    178     return self._device_num
    179 
    180   #override
    181   @property
    182   def bus_num(self):
    183     return self._bus_num
    184 
    185   @property
    186   def serial(self):
    187     return self._serial
    188 
    189   @serial.setter
    190   def serial(self, serial):
    191     self._serial = serial
    192 
    193   #override
    194   def Display(self, port_chain='', info=False):
    195     logger.info('%s Device %d (%s)', port_chain, self.device_num, self.desc)
    196     if info:
    197       logger.info('%s', self.info)
    198     for (port, device) in self._port_to_node.iteritems():
    199       device.Display('%s%d:' % (port_chain, port), info=info)
    200 
    201 
    202 class USBBusNode(USBNode):
    203   def __init__(self, bus_num=0):
    204     """Class that represents a node (either a bus or device) in USB tree.
    205 
    206     Args:
    207       is_bus: [bool] If true, node is bus; if not, node is device.
    208       bus_num: [int] Bus number that this node is attached to.
    209       device_num: [int] Device number of this device (or 0, if this is a bus)
    210       desc: [string] Short description of device.
    211       serial: [string] Serial number.
    212       info: [dict] Map giving detailed device info.
    213       port_to_dev: [dict(int:USBDeviceNode)]
    214           Maps port # to device connected to port.
    215     """
    216     super(USBBusNode, self).__init__()
    217     self._bus_num = bus_num
    218 
    219   #override
    220   @property
    221   def desc(self):
    222     return 'BUS %d' % self._bus_num
    223 
    224   #override
    225   @property
    226   def info(self):
    227     return {}
    228 
    229   #override
    230   @property
    231   def device_num(self):
    232     return -1
    233 
    234   #override
    235   @property
    236   def bus_num(self):
    237     return self._bus_num
    238 
    239   #override
    240   def Display(self, port_chain='', info=False):
    241     logger.info('=== %s ===', self.desc)
    242     for (port, device) in self._port_to_node.iteritems():
    243       device.Display('%s%d:' % (port_chain, port), info=info)
    244 
    245 
    246 _T_LINE_REGEX = re.compile(r'T:  Bus=(?P<bus>\d{2}) Lev=(?P<lev>\d{2}) '
    247                            r'Prnt=(?P<prnt>\d{2,3}) Port=(?P<port>\d{2}) '
    248                            r'Cnt=(?P<cnt>\d{2}) Dev#=(?P<dev>.{3}) .*')
    249 
    250 _S_LINE_REGEX = re.compile(r'S:  SerialNumber=(?P<serial>.*)')
    251 _LSUSB_BUS_DEVICE_RE = re.compile(r'^Bus (\d{3}) Device (\d{3}): (.*)')
    252 
    253 
    254 def GetBusNumberToDeviceTreeMap(fast=True):
    255   """Gets devices currently attached.
    256 
    257   Args:
    258     fast [bool]: whether to do it fast (only get description, not
    259     the whole dictionary, from lsusb)
    260 
    261   Returns:
    262     map of {bus number: bus object}
    263     where the bus object has all the devices attached to it in a tree.
    264   """
    265   if fast:
    266     info_map = {}
    267     for line in lsusb.raw_lsusb().splitlines():
    268       match = _LSUSB_BUS_DEVICE_RE.match(line)
    269       if match:
    270         info_map[(int(match.group(1)), int(match.group(2)))] = (
    271           {'desc':match.group(3)})
    272   else:
    273     info_map = {((int(line['bus']), int(line['device']))): line
    274                 for line in _GetParsedLSUSBOutput()}
    275 
    276 
    277   tree = {}
    278   bus_num = -1
    279   for line in _GetUSBDevicesOutput().splitlines():
    280     match = _T_LINE_REGEX.match(line)
    281     if match:
    282       bus_num = int(match.group('bus'))
    283       parent_num = int(match.group('prnt'))
    284       # usb-devices starts counting ports from 0, so add 1
    285       port_num = int(match.group('port')) + 1
    286       device_num = int(match.group('dev'))
    287 
    288       # create new bus if necessary
    289       if bus_num not in tree:
    290         tree[bus_num] = USBBusNode(bus_num=bus_num)
    291 
    292       # create the new device
    293       new_device = USBDeviceNode(bus_num=bus_num,
    294                                  device_num=device_num,
    295                                  info=info_map.get((bus_num, device_num),
    296                                                    {'desc': 'NOT AVAILABLE'}))
    297 
    298       # add device to bus
    299       if parent_num != 0:
    300         tree[bus_num].FindDeviceNumber(parent_num).AddChild(
    301             port_num, new_device)
    302       else:
    303         tree[bus_num].AddChild(port_num, new_device)
    304 
    305     match = _S_LINE_REGEX.match(line)
    306     if match:
    307       if bus_num == -1:
    308         raise ValueError('S line appears before T line in input file')
    309       # put the serial number in the device
    310       tree[bus_num].FindDeviceNumber(device_num).serial = match.group('serial')
    311 
    312   return tree
    313 
    314 
    315 def GetHubsOnBus(bus, hub_types):
    316   """Scans for all hubs on a bus of given hub types.
    317 
    318   Args:
    319     bus: [USBNode] Bus object.
    320     hub_types: [iterable(usb_hubs.HubType)] Possible types of hubs.
    321 
    322   Yields:
    323     Sequence of tuples representing (hub, type of hub)
    324   """
    325   for device in bus.AllNodes():
    326     for hub_type in hub_types:
    327       if hub_type.IsType(device):
    328         yield (device, hub_type)
    329 
    330 
    331 def GetPhysicalPortToNodeMap(hub, hub_type):
    332   """Gets physical-port:node mapping for a given hub.
    333   Args:
    334     hub: [USBNode] Hub to get map for.
    335     hub_type: [usb_hubs.HubType] Which type of hub it is.
    336 
    337   Returns:
    338     Dict of {physical port: node}
    339   """
    340   port_device = hub_type.GetPhysicalPortToNodeTuples(hub)
    341   return {port: device for (port, device) in port_device}
    342 
    343 
    344 def GetPhysicalPortToBusDeviceMap(hub, hub_type):
    345   """Gets physical-port:(bus#, device#) mapping for a given hub.
    346   Args:
    347     hub: [USBNode] Hub to get map for.
    348     hub_type: [usb_hubs.HubType] Which type of hub it is.
    349 
    350   Returns:
    351     Dict of {physical port: (bus number, device number)}
    352   """
    353   port_device = hub_type.GetPhysicalPortToNodeTuples(hub)
    354   return {port: (device.bus_num, device.device_num)
    355           for (port, device) in port_device}
    356 
    357 
    358 def GetPhysicalPortToSerialMap(hub, hub_type):
    359   """Gets physical-port:serial# mapping for a given hub.
    360 
    361   Args:
    362     hub: [USBNode] Hub to get map for.
    363     hub_type: [usb_hubs.HubType] Which type of hub it is.
    364 
    365   Returns:
    366     Dict of {physical port: serial number)}
    367   """
    368   port_device = hub_type.GetPhysicalPortToNodeTuples(hub)
    369   return {port: device.serial
    370           for (port, device) in port_device
    371           if device.serial}
    372 
    373 
    374 def GetPhysicalPortToTTYMap(device, hub_type):
    375   """Gets physical-port:tty-string mapping for a given hub.
    376   Args:
    377     hub: [USBNode] Hub to get map for.
    378     hub_type: [usb_hubs.HubType] Which type of hub it is.
    379 
    380   Returns:
    381     Dict of {physical port: tty-string)}
    382   """
    383   port_device = hub_type.GetPhysicalPortToNodeTuples(device)
    384   bus_device_to_tty = GetBusDeviceToTTYMap()
    385   return {port: bus_device_to_tty[(device.bus_num, device.device_num)]
    386           for (port, device) in port_device
    387           if (device.bus_num, device.device_num) in bus_device_to_tty}
    388 
    389 
    390 def CollectHubMaps(hub_types, map_func, device_tree_map=None, fast=False):
    391   """Runs a function on all hubs in the system and collects their output.
    392 
    393   Args:
    394     hub_types: [usb_hubs.HubType] List of possible hub types.
    395     map_func: [string] Function to run on each hub.
    396     device_tree: Previously constructed device tree map, if any.
    397     fast: Whether to construct device tree fast, if not already provided
    398 
    399   Yields:
    400     Sequence of dicts of {physical port: device} where the type of
    401     device depends on the ident keyword. Each dict is a separate hub.
    402   """
    403   if device_tree_map is None:
    404     device_tree_map = GetBusNumberToDeviceTreeMap(fast=fast)
    405   for bus in device_tree_map.values():
    406     for (hub, hub_type) in GetHubsOnBus(bus, hub_types):
    407       yield map_func(hub, hub_type)
    408 
    409 
    410 def GetAllPhysicalPortToNodeMaps(hub_types, **kwargs):
    411   return CollectHubMaps(hub_types, GetPhysicalPortToNodeMap, **kwargs)
    412 
    413 
    414 def GetAllPhysicalPortToBusDeviceMaps(hub_types, **kwargs):
    415   return CollectHubMaps(hub_types, GetPhysicalPortToBusDeviceMap, **kwargs)
    416 
    417 
    418 def GetAllPhysicalPortToSerialMaps(hub_types, **kwargs):
    419   return CollectHubMaps(hub_types, GetPhysicalPortToSerialMap, **kwargs)
    420 
    421 
    422 def GetAllPhysicalPortToTTYMaps(hub_types, **kwargs):
    423   return CollectHubMaps(hub_types, GetPhysicalPortToTTYMap, **kwargs)
    424 
    425 
    426 _BUS_NUM_REGEX = re.compile(r'.*ATTRS{busnum}=="(\d*)".*')
    427 _DEVICE_NUM_REGEX = re.compile(r'.*ATTRS{devnum}=="(\d*)".*')
    428 
    429 
    430 def GetBusDeviceFromTTY(tty_string):
    431   """Gets bus and device number connected to a ttyUSB port.
    432 
    433   Args:
    434     tty_string: [String] Identifier for ttyUSB (e.g. 'ttyUSB0')
    435 
    436   Returns:
    437     Tuple (bus, device) giving device connected to that ttyUSB.
    438 
    439   Raises:
    440     ValueError: If bus and device information could not be found.
    441   """
    442   bus_num = None
    443   device_num = None
    444   # Expected output of GetCmdOutput should be something like:
    445   # looking at device /devices/something/.../.../...
    446   # KERNELS="ttyUSB0"
    447   # SUBSYSTEMS=...
    448   # DRIVERS=...
    449   # ATTRS{foo}=...
    450   # ATTRS{bar}=...
    451   # ...
    452   for line in _GetTtyUSBInfo(tty_string).splitlines():
    453     bus_match = _BUS_NUM_REGEX.match(line)
    454     device_match = _DEVICE_NUM_REGEX.match(line)
    455     if bus_match and bus_num == None:
    456       bus_num = int(bus_match.group(1))
    457     if device_match and device_num == None:
    458       device_num = int(device_match.group(1))
    459   if bus_num is None or device_num is None:
    460     raise ValueError('Info not found')
    461   return (bus_num, device_num)
    462 
    463 
    464 def GetBusDeviceToTTYMap():
    465   """Gets all mappings from (bus, device) to ttyUSB string.
    466 
    467   Gets mapping from (bus, device) to ttyUSB string (e.g. 'ttyUSB0'),
    468   for all ttyUSB strings currently active.
    469 
    470   Returns:
    471     [dict] Dict that maps (bus, device) to ttyUSB string
    472   """
    473   result = {}
    474   for tty in GetTTYList():
    475     result[GetBusDeviceFromTTY(tty)] = tty
    476   return result
    477 
    478 
    479 # This dictionary described the mapping between physical and
    480 # virtual ports on a Plugable 7-Port Hub (model USB2-HUB7BC).
    481 # Keys are the virtual ports, values are the physical port.
    482 # The entry 4:{1:4, 2:3, 3:2, 4:1} indicates that virtual port
    483 # 4 connects to another 'virtual' hub that itself has the
    484 # virtual-to-physical port mapping {1:4, 2:3, 3:2, 4:1}.
    485 
    486 
    487 def TestUSBTopologyScript():
    488   """Test display and hub identification."""
    489   # The following makes logger.info behave pretty much like print
    490   # during this test script.
    491   logging.basicConfig(format='%(message)s', stream=sys.stdout)
    492   logger.setLevel(logging.INFO)
    493 
    494   # Identification criteria for Plugable 7-Port Hub
    495   logger.info('==== USB TOPOLOGY SCRIPT TEST ====')
    496   logger.info('')
    497 
    498   # Display devices
    499   logger.info('==== DEVICE DISPLAY ====')
    500   device_trees = GetBusNumberToDeviceTreeMap()
    501   for device_tree in device_trees.values():
    502     device_tree.Display()
    503   logger.info('')
    504 
    505   # Display TTY information about devices plugged into hubs.
    506   logger.info('==== TTY INFORMATION ====')
    507   for port_map in GetAllPhysicalPortToTTYMaps(
    508       usb_hubs.ALL_HUBS, device_tree_map=device_trees):
    509     logger.info('%s', port_map)
    510   logger.info('')
    511 
    512   # Display serial number information about devices plugged into hubs.
    513   logger.info('==== SERIAL NUMBER INFORMATION ====')
    514   for port_map in GetAllPhysicalPortToSerialMaps(
    515       usb_hubs.ALL_HUBS, device_tree_map=device_trees):
    516     logger.info('%s', port_map)
    517 
    518   return 0
    519 
    520 
    521 def parse_options(argv):
    522   """Parses and checks the command-line options.
    523 
    524   Returns:
    525     A tuple containing the options structure and a list of categories to
    526     be traced.
    527   """
    528   USAGE = '''./find_usb_devices [--help]
    529     This script shows the mapping between USB devices and port numbers.
    530     Clients are not intended to call this script from the command line.
    531     Clients are intended to call the functions in this script directly.
    532     For instance, GetAllPhysicalPortToSerialMaps(...)
    533     Running this script with --help will display this message.
    534     Running this script without --help will display information about
    535     devices attached, TTY mapping, and serial number mapping,
    536     for testing purposes. See design document for API documentation.
    537   '''
    538   parser = argparse.ArgumentParser(usage=USAGE)
    539   return parser.parse_args(argv[1:])
    540 
    541 def main():
    542   parse_options(sys.argv)
    543   TestUSBTopologyScript()
    544 
    545 if __name__ == "__main__":
    546   sys.exit(main())
    547