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 5import logging 6import re 7 8from devil.utils import cmd_helper 9 10logger = 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 19def _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 137def 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 154def raw_lsusb(): 155 return cmd_helper.GetCmdOutput(['lsusb']) 156 157def get_lsusb_serial(device): 158 try: 159 return device['Device Descriptor']['iSerial']['_desc'] 160 except KeyError: 161 return None 162 163def _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 172def 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