Home | History | Annotate | Download | only in utils
      1 # Copyright 2015 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import logging
      6 import re
      7 
      8 from devil.utils import cmd_helper
      9 
     10 logger = logging.getLogger(__name__)
     11 
     12 _COULDNT_OPEN_ERROR_RE = re.compile(r'Couldn\'t open device.*')
     13 _INDENTATION_RE = re.compile(r'^( *)')
     14 _LSUSB_BUS_DEVICE_RE = re.compile(r'^Bus (\d{3}) Device (\d{3}): (.*)')
     15 _LSUSB_ENTRY_RE = re.compile(r'^ *([^ ]+) +([^ ]+) *([^ ].*)?$')
     16 _LSUSB_GROUP_RE = re.compile(r'^ *([^ ]+.*):$')
     17 
     18 
     19 def _lsusbv_on_device(bus_id, dev_id):
     20   """Calls lsusb -v on device."""
     21   _, raw_output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
     22       ['lsusb', '-v', '-s', '%s:%s' % (bus_id, dev_id)], timeout=10)
     23 
     24   device = {'bus': bus_id, 'device': dev_id}
     25   depth_stack = [device]
     26 
     27   # This builds a nested dict -- a tree, basically -- that corresponds
     28   # to the lsusb output. It looks first for a line containing
     29   #
     30   #   "Bus <bus number> Device <device number>: ..."
     31   #
     32   # and uses that to create the root node. It then parses all remaining
     33   # lines as a tree, with the indentation level determining the
     34   # depth of the new node.
     35   #
     36   # This expects two kinds of lines:
     37   #   - "groups", which take the form
     38   #       "<Group name>:"
     39   #     and typically have children, and
     40   #   - "entries", which take the form
     41   #       "<entry name>   <entry value>  <possible entry description>"
     42   #     and typically do not have children (but can).
     43   #
     44   # This maintains a stack containing all current ancestor nodes in
     45   # order to add new nodes to the proper place in the tree.
     46   # The stack is added to when a new node is parsed. Nodes are removed
     47   # from the stack when they are either at the same indentation level as
     48   # or a deeper indentation level than the current line.
     49   #
     50   # e.g. the following lsusb output:
     51   #
     52   # Bus 123 Device 456: School bus
     53   # Device Descriptor:
     54   #   bDeviceClass 5 Actual School Bus
     55   #   Configuration Descriptor:
     56   #     bLength 20 Rows
     57   #
     58   # would produce the following dict:
     59   #
     60   # {
     61   #   'bus': 123,
     62   #   'device': 456,
     63   #   'desc': 'School bus',
     64   #   'Device Descriptor': {
     65   #     'bDeviceClass': {
     66   #       '_value': '5',
     67   #       '_desc': 'Actual School Bus',
     68   #     },
     69   #     'Configuration Descriptor': {
     70   #       'bLength': {
     71   #         '_value': '20',
     72   #         '_desc': 'Rows',
     73   #       },
     74   #     },
     75   #   }
     76   # }
     77   for line in raw_output.splitlines():
     78     # Ignore blank lines.
     79     if not line:
     80       continue
     81     # Filter out error mesage about opening device.
     82     if _COULDNT_OPEN_ERROR_RE.match(line):
     83       continue
     84     # Find start of device information.
     85     m = _LSUSB_BUS_DEVICE_RE.match(line)
     86     if m:
     87       if m.group(1) != bus_id:
     88         logger.warning(
     89             'Expected bus_id value: %r, seen %r', bus_id, m.group(1))
     90       if m.group(2) != dev_id:
     91         logger.warning(
     92             'Expected dev_id value: %r, seen %r', dev_id, m.group(2))
     93       device['desc'] = m.group(3)
     94       continue
     95 
     96     # Skip any lines that aren't indented, as they're not part of the
     97     # device descriptor.
     98     indent_match = _INDENTATION_RE.match(line)
     99     if not indent_match:
    100       continue
    101 
    102     # Determine the indentation depth.
    103     depth = 1 + len(indent_match.group(1)) / 2
    104     if depth > len(depth_stack):
    105       logger.error(
    106           'lsusb parsing error: unexpected indentation: "%s"', line)
    107       continue
    108 
    109     # Pop everything off the depth stack that isn't a parent of
    110     # this element.
    111     while depth < len(depth_stack):
    112       depth_stack.pop()
    113 
    114     cur = depth_stack[-1]
    115 
    116     m = _LSUSB_GROUP_RE.match(line)
    117     if m:
    118       new_group = {}
    119       cur[m.group(1)] = new_group
    120       depth_stack.append(new_group)
    121       continue
    122 
    123     m = _LSUSB_ENTRY_RE.match(line)
    124     if m:
    125       new_entry = {
    126         '_value': m.group(2),
    127         '_desc': m.group(3),
    128       }
    129       cur[m.group(1)] = new_entry
    130       depth_stack.append(new_entry)
    131       continue
    132 
    133     logger.error('lsusb parsing error: unrecognized line: "%s"', line)
    134 
    135   return device
    136 
    137 def lsusb():
    138   """Call lsusb and return the parsed output."""
    139   _, lsusb_list_output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
    140       ['lsusb'], timeout=10)
    141   devices = []
    142   for line in lsusb_list_output.splitlines():
    143     m = _LSUSB_BUS_DEVICE_RE.match(line)
    144     if m:
    145       bus_num = m.group(1)
    146       dev_num = m.group(2)
    147       try:
    148         devices.append(_lsusbv_on_device(bus_num, dev_num))
    149       except cmd_helper.TimeoutError:
    150         # Will be blacklisted if it is in expected device file, but times out.
    151         logger.info('lsusb -v %s:%s timed out.', bus_num, dev_num)
    152   return devices
    153 
    154 def raw_lsusb():
    155   return cmd_helper.GetCmdOutput(['lsusb'])
    156 
    157 def get_lsusb_serial(device):
    158   try:
    159     return device['Device Descriptor']['iSerial']['_desc']
    160   except KeyError:
    161     return None
    162 
    163 def _is_android_device(device):
    164   try:
    165     # Hubs are not android devices.
    166     if device['Device Descriptor']['bDeviceClass']['_value'] == '9':
    167       return False
    168   except KeyError:
    169     pass
    170   return get_lsusb_serial(device) is not None
    171 
    172 def get_android_devices():
    173   android_devices = (d for d in lsusb() if _is_android_device(d))
    174   return [get_lsusb_serial(d) for d in android_devices]
    175