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('Expected bus_id value: %r, seen %r', bus_id, m.group(1)) 89 if m.group(2) != dev_id: 90 logger.warning('Expected dev_id value: %r, seen %r', dev_id, m.group(2)) 91 device['desc'] = m.group(3) 92 continue 93 94 # Skip any lines that aren't indented, as they're not part of the 95 # device descriptor. 96 indent_match = _INDENTATION_RE.match(line) 97 if not indent_match: 98 continue 99 100 # Determine the indentation depth. 101 depth = 1 + len(indent_match.group(1)) / 2 102 if depth > len(depth_stack): 103 logger.error('lsusb parsing error: unexpected indentation: "%s"', line) 104 continue 105 106 # Pop everything off the depth stack that isn't a parent of 107 # this element. 108 while depth < len(depth_stack): 109 depth_stack.pop() 110 111 cur = depth_stack[-1] 112 113 m = _LSUSB_GROUP_RE.match(line) 114 if m: 115 new_group = {} 116 cur[m.group(1)] = new_group 117 depth_stack.append(new_group) 118 continue 119 120 m = _LSUSB_ENTRY_RE.match(line) 121 if m: 122 new_entry = { 123 '_value': m.group(2), 124 '_desc': m.group(3), 125 } 126 cur[m.group(1)] = new_entry 127 depth_stack.append(new_entry) 128 continue 129 130 logger.error('lsusb parsing error: unrecognized line: "%s"', line) 131 132 return device 133 134 135def lsusb(): 136 """Call lsusb and return the parsed output.""" 137 _, lsusb_list_output = cmd_helper.GetCmdStatusAndOutputWithTimeout(['lsusb'], 138 timeout=10) 139 devices = [] 140 for line in lsusb_list_output.splitlines(): 141 m = _LSUSB_BUS_DEVICE_RE.match(line) 142 if m: 143 bus_num = m.group(1) 144 dev_num = m.group(2) 145 try: 146 devices.append(_lsusbv_on_device(bus_num, dev_num)) 147 except cmd_helper.TimeoutError: 148 # Will be denylisted if it is in expected device file, but times out. 149 logger.info('lsusb -v %s:%s timed out.', bus_num, dev_num) 150 return devices 151 152 153def raw_lsusb(): 154 return cmd_helper.GetCmdOutput(['lsusb']) 155 156 157def get_lsusb_serial(device): 158 try: 159 return device['Device Descriptor']['iSerial']['_desc'] 160 except KeyError: 161 return None 162 163 164def _is_android_device(device): 165 try: 166 # Hubs are not android devices. 167 if device['Device Descriptor']['bDeviceClass']['_value'] == '9': 168 return False 169 except KeyError: 170 pass 171 return get_lsusb_serial(device) is not None 172 173 174def get_android_devices(): 175 android_devices = (d for d in lsusb() if _is_android_device(d)) 176 return [get_lsusb_serial(d) for d in android_devices] 177