aboutsummaryrefslogtreecommitdiff
path: root/catapult/devil/devil/utils/lsusb.py
blob: 6cbf2567b984cf14d41409c1c9effdeda42ebcfe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import logging
import re

from devil.utils import cmd_helper

logger = logging.getLogger(__name__)

_COULDNT_OPEN_ERROR_RE = re.compile(r'Couldn\'t open device.*')
_INDENTATION_RE = re.compile(r'^( *)')
_LSUSB_BUS_DEVICE_RE = re.compile(r'^Bus (\d{3}) Device (\d{3}): (.*)')
_LSUSB_ENTRY_RE = re.compile(r'^ *([^ ]+) +([^ ]+) *([^ ].*)?$')
_LSUSB_GROUP_RE = re.compile(r'^ *([^ ]+.*):$')


def _lsusbv_on_device(bus_id, dev_id):
  """Calls lsusb -v on device."""
  _, raw_output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
      ['lsusb', '-v', '-s', '%s:%s' % (bus_id, dev_id)], timeout=10)

  device = {'bus': bus_id, 'device': dev_id}
  depth_stack = [device]

  # This builds a nested dict -- a tree, basically -- that corresponds
  # to the lsusb output. It looks first for a line containing
  #
  #   "Bus <bus number> Device <device number>: ..."
  #
  # and uses that to create the root node. It then parses all remaining
  # lines as a tree, with the indentation level determining the
  # depth of the new node.
  #
  # This expects two kinds of lines:
  #   - "groups", which take the form
  #       "<Group name>:"
  #     and typically have children, and
  #   - "entries", which take the form
  #       "<entry name>   <entry value>  <possible entry description>"
  #     and typically do not have children (but can).
  #
  # This maintains a stack containing all current ancestor nodes in
  # order to add new nodes to the proper place in the tree.
  # The stack is added to when a new node is parsed. Nodes are removed
  # from the stack when they are either at the same indentation level as
  # or a deeper indentation level than the current line.
  #
  # e.g. the following lsusb output:
  #
  # Bus 123 Device 456: School bus
  # Device Descriptor:
  #   bDeviceClass 5 Actual School Bus
  #   Configuration Descriptor:
  #     bLength 20 Rows
  #
  # would produce the following dict:
  #
  # {
  #   'bus': 123,
  #   'device': 456,
  #   'desc': 'School bus',
  #   'Device Descriptor': {
  #     'bDeviceClass': {
  #       '_value': '5',
  #       '_desc': 'Actual School Bus',
  #     },
  #     'Configuration Descriptor': {
  #       'bLength': {
  #         '_value': '20',
  #         '_desc': 'Rows',
  #       },
  #     },
  #   }
  # }
  for line in raw_output.splitlines():
    # Ignore blank lines.
    if not line:
      continue
    # Filter out error mesage about opening device.
    if _COULDNT_OPEN_ERROR_RE.match(line):
      continue
    # Find start of device information.
    m = _LSUSB_BUS_DEVICE_RE.match(line)
    if m:
      if m.group(1) != bus_id:
        logger.warning(
            'Expected bus_id value: %r, seen %r', bus_id, m.group(1))
      if m.group(2) != dev_id:
        logger.warning(
            'Expected dev_id value: %r, seen %r', dev_id, m.group(2))
      device['desc'] = m.group(3)
      continue

    # Skip any lines that aren't indented, as they're not part of the
    # device descriptor.
    indent_match = _INDENTATION_RE.match(line)
    if not indent_match:
      continue

    # Determine the indentation depth.
    depth = 1 + len(indent_match.group(1)) / 2
    if depth > len(depth_stack):
      logger.error(
          'lsusb parsing error: unexpected indentation: "%s"', line)
      continue

    # Pop everything off the depth stack that isn't a parent of
    # this element.
    while depth < len(depth_stack):
      depth_stack.pop()

    cur = depth_stack[-1]

    m = _LSUSB_GROUP_RE.match(line)
    if m:
      new_group = {}
      cur[m.group(1)] = new_group
      depth_stack.append(new_group)
      continue

    m = _LSUSB_ENTRY_RE.match(line)
    if m:
      new_entry = {
        '_value': m.group(2),
        '_desc': m.group(3),
      }
      cur[m.group(1)] = new_entry
      depth_stack.append(new_entry)
      continue

    logger.error('lsusb parsing error: unrecognized line: "%s"', line)

  return device

def lsusb():
  """Call lsusb and return the parsed output."""
  _, lsusb_list_output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
      ['lsusb'], timeout=10)
  devices = []
  for line in lsusb_list_output.splitlines():
    m = _LSUSB_BUS_DEVICE_RE.match(line)
    if m:
      bus_num = m.group(1)
      dev_num = m.group(2)
      try:
        devices.append(_lsusbv_on_device(bus_num, dev_num))
      except cmd_helper.TimeoutError:
        # Will be blacklisted if it is in expected device file, but times out.
        logger.info('lsusb -v %s:%s timed out.', bus_num, dev_num)
  return devices

def raw_lsusb():
  return cmd_helper.GetCmdOutput(['lsusb'])

def get_lsusb_serial(device):
  try:
    return device['Device Descriptor']['iSerial']['_desc']
  except KeyError:
    return None

def _is_android_device(device):
  try:
    # Hubs are not android devices.
    if device['Device Descriptor']['bDeviceClass']['_value'] == '9':
      return False
  except KeyError:
    pass
  return get_lsusb_serial(device) is not None

def get_android_devices():
  android_devices = (d for d in lsusb() if _is_android_device(d))
  return [get_lsusb_serial(d) for d in android_devices]