• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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