• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2# Copyright 2016 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import argparse
7import logging
8import os
9import re
10import sys
11
12if __name__ == '__main__':
13  sys.path.append(
14      os.path.abspath(os.path.join(os.path.dirname(__file__),
15                                   '..', '..')))
16
17from devil.utils import cmd_helper
18from devil.utils import usb_hubs
19from devil.utils import lsusb
20
21logger = logging.getLogger(__name__)
22
23# Note: In the documentation below, "virtual port" refers to the port number
24# as observed by the system (e.g. by usb-devices) and "physical port" refers
25# to the physical numerical label on the physical port e.g. on a USB hub.
26# The mapping between virtual and physical ports is not always the identity
27# (e.g. the port labeled "1" on a USB hub does not always show up as "port 1"
28# when you plug something into it) but, as far as we are aware, the mapping
29# between virtual and physical ports is always the same for a given
30# model of USB hub. When "port number" is referenced without specifying, it
31# means the virtual port number.
32
33
34# Wrapper functions for system commands to get output. These are in wrapper
35# functions so that they can be more easily mocked-out for tests.
36def _GetParsedLSUSBOutput():
37  return lsusb.lsusb()
38
39
40def _GetUSBDevicesOutput():
41  return cmd_helper.GetCmdOutput(['usb-devices'])
42
43
44def _GetTtyUSBInfo(tty_string):
45  cmd = ['udevadm', 'info', '--name=/dev/' + tty_string, '--attribute-walk']
46  return cmd_helper.GetCmdOutput(cmd)
47
48
49def _GetCommList():
50  return cmd_helper.GetCmdOutput('ls /dev', shell=True)
51
52
53def GetTTYList():
54  return [x for x in _GetCommList().splitlines() if 'ttyUSB' in x]
55
56
57# Class to identify nodes in the USB topology. USB topology is organized as
58# a tree.
59class USBNode(object):
60  def __init__(self):
61    self._port_to_node = {}
62
63  @property
64  def desc(self):
65    raise NotImplementedError
66
67  @property
68  def info(self):
69    raise NotImplementedError
70
71  @property
72  def device_num(self):
73    raise NotImplementedError
74
75  @property
76  def bus_num(self):
77    raise NotImplementedError
78
79  def HasPort(self, port):
80    """Determines if this device has a device connected to the given port."""
81    return port in self._port_to_node
82
83  def PortToDevice(self, port):
84    """Gets the device connected to the given port on this device."""
85    return self._port_to_node[port]
86
87  def Display(self, port_chain='', info=False):
88    """Displays information about this node and its descendants.
89
90    Output format is, e.g. 1:3:3:Device 42 (ID 1234:5678 Some Device)
91    meaning that from the bus, if you look at the device connected
92    to port 1, then the device connected to port 3 of that,
93    then the device connected to port 3 of that, you get the device
94    assigned device number 42, which is Some Device. Note that device
95    numbers will be reassigned whenever a connected device is powercycled
96    or reinserted, but port numbers stay the same as long as the device
97    is reinserted back into the same physical port.
98
99    Args:
100      port_chain: [string] Chain of ports from bus to this node (e.g. '2:4:')
101      info: [bool] Whether to display detailed info as well.
102    """
103    raise NotImplementedError
104
105  def AddChild(self, port, device):
106    """Adds child to the device tree.
107
108    Args:
109      port: [int] Port number of the device.
110      device: [USBDeviceNode] Device to add.
111
112    Raises:
113      ValueError: If device already has a child at the given port.
114    """
115    if self.HasPort(port):
116      raise ValueError('Duplicate port number')
117    else:
118      self._port_to_node[port] = device
119
120  def AllNodes(self):
121    """Generator that yields this node and all of its descendants.
122
123    Yields:
124      [USBNode] First this node, then each of its descendants (recursively)
125    """
126    yield self
127    for child_node in self._port_to_node.values():
128      for descendant_node in child_node.AllNodes():
129        yield descendant_node
130
131  def FindDeviceNumber(self, findnum):
132    """Find device with given number in tree
133
134    Searches the portion of the device tree rooted at this node for
135    a device with the given device number.
136
137    Args:
138      findnum: [int] Device number to search for.
139
140    Returns:
141      [USBDeviceNode] Node that is found.
142    """
143    for node in self.AllNodes():
144      if node.device_num == findnum:
145        return node
146    return None
147
148
149class USBDeviceNode(USBNode):
150  def __init__(self, bus_num=0, device_num=0, serial=None, info=None):
151    """Class that represents a device in USB tree.
152
153    Args:
154      bus_num: [int] Bus number that this node is attached to.
155      device_num: [int] Device number of this device (or 0, if this is a bus)
156      serial: [string] Serial number.
157      info: [dict] Map giving detailed device info.
158    """
159    super(USBDeviceNode, self).__init__()
160    self._bus_num = bus_num
161    self._device_num = device_num
162    self._serial = serial
163    self._info = {} if info is None else info
164
165  #override
166  @property
167  def desc(self):
168    return self._info.get('desc')
169
170  #override
171  @property
172  def info(self):
173    return self._info
174
175  #override
176  @property
177  def device_num(self):
178    return self._device_num
179
180  #override
181  @property
182  def bus_num(self):
183    return self._bus_num
184
185  @property
186  def serial(self):
187    return self._serial
188
189  @serial.setter
190  def serial(self, serial):
191    self._serial = serial
192
193  #override
194  def Display(self, port_chain='', info=False):
195    logger.info('%s Device %d (%s)', port_chain, self.device_num, self.desc)
196    if info:
197      logger.info('%s', self.info)
198    for (port, device) in self._port_to_node.iteritems():
199      device.Display('%s%d:' % (port_chain, port), info=info)
200
201
202class USBBusNode(USBNode):
203  def __init__(self, bus_num=0):
204    """Class that represents a node (either a bus or device) in USB tree.
205
206    Args:
207      is_bus: [bool] If true, node is bus; if not, node is device.
208      bus_num: [int] Bus number that this node is attached to.
209      device_num: [int] Device number of this device (or 0, if this is a bus)
210      desc: [string] Short description of device.
211      serial: [string] Serial number.
212      info: [dict] Map giving detailed device info.
213      port_to_dev: [dict(int:USBDeviceNode)]
214          Maps port # to device connected to port.
215    """
216    super(USBBusNode, self).__init__()
217    self._bus_num = bus_num
218
219  #override
220  @property
221  def desc(self):
222    return 'BUS %d' % self._bus_num
223
224  #override
225  @property
226  def info(self):
227    return {}
228
229  #override
230  @property
231  def device_num(self):
232    return -1
233
234  #override
235  @property
236  def bus_num(self):
237    return self._bus_num
238
239  #override
240  def Display(self, port_chain='', info=False):
241    logger.info('=== %s ===', self.desc)
242    for (port, device) in self._port_to_node.iteritems():
243      device.Display('%s%d:' % (port_chain, port), info=info)
244
245
246_T_LINE_REGEX = re.compile(r'T:  Bus=(?P<bus>\d{2}) Lev=(?P<lev>\d{2}) '
247                           r'Prnt=(?P<prnt>\d{2,3}) Port=(?P<port>\d{2}) '
248                           r'Cnt=(?P<cnt>\d{2}) Dev#=(?P<dev>.{3}) .*')
249
250_S_LINE_REGEX = re.compile(r'S:  SerialNumber=(?P<serial>.*)')
251_LSUSB_BUS_DEVICE_RE = re.compile(r'^Bus (\d{3}) Device (\d{3}): (.*)')
252
253
254def GetBusNumberToDeviceTreeMap(fast=True):
255  """Gets devices currently attached.
256
257  Args:
258    fast [bool]: whether to do it fast (only get description, not
259    the whole dictionary, from lsusb)
260
261  Returns:
262    map of {bus number: bus object}
263    where the bus object has all the devices attached to it in a tree.
264  """
265  if fast:
266    info_map = {}
267    for line in lsusb.raw_lsusb().splitlines():
268      match = _LSUSB_BUS_DEVICE_RE.match(line)
269      if match:
270        info_map[(int(match.group(1)), int(match.group(2)))] = (
271          {'desc':match.group(3)})
272  else:
273    info_map = {((int(line['bus']), int(line['device']))): line
274                for line in _GetParsedLSUSBOutput()}
275
276
277  tree = {}
278  bus_num = -1
279  for line in _GetUSBDevicesOutput().splitlines():
280    match = _T_LINE_REGEX.match(line)
281    if match:
282      bus_num = int(match.group('bus'))
283      parent_num = int(match.group('prnt'))
284      # usb-devices starts counting ports from 0, so add 1
285      port_num = int(match.group('port')) + 1
286      device_num = int(match.group('dev'))
287
288      # create new bus if necessary
289      if bus_num not in tree:
290        tree[bus_num] = USBBusNode(bus_num=bus_num)
291
292      # create the new device
293      new_device = USBDeviceNode(bus_num=bus_num,
294                                 device_num=device_num,
295                                 info=info_map.get((bus_num, device_num),
296                                                   {'desc': 'NOT AVAILABLE'}))
297
298      # add device to bus
299      if parent_num != 0:
300        tree[bus_num].FindDeviceNumber(parent_num).AddChild(
301            port_num, new_device)
302      else:
303        tree[bus_num].AddChild(port_num, new_device)
304
305    match = _S_LINE_REGEX.match(line)
306    if match:
307      if bus_num == -1:
308        raise ValueError('S line appears before T line in input file')
309      # put the serial number in the device
310      tree[bus_num].FindDeviceNumber(device_num).serial = match.group('serial')
311
312  return tree
313
314
315def GetHubsOnBus(bus, hub_types):
316  """Scans for all hubs on a bus of given hub types.
317
318  Args:
319    bus: [USBNode] Bus object.
320    hub_types: [iterable(usb_hubs.HubType)] Possible types of hubs.
321
322  Yields:
323    Sequence of tuples representing (hub, type of hub)
324  """
325  for device in bus.AllNodes():
326    for hub_type in hub_types:
327      if hub_type.IsType(device):
328        yield (device, hub_type)
329
330
331def GetPhysicalPortToNodeMap(hub, hub_type):
332  """Gets physical-port:node mapping for a given hub.
333  Args:
334    hub: [USBNode] Hub to get map for.
335    hub_type: [usb_hubs.HubType] Which type of hub it is.
336
337  Returns:
338    Dict of {physical port: node}
339  """
340  port_device = hub_type.GetPhysicalPortToNodeTuples(hub)
341  return {port: device for (port, device) in port_device}
342
343
344def GetPhysicalPortToBusDeviceMap(hub, hub_type):
345  """Gets physical-port:(bus#, device#) mapping for a given hub.
346  Args:
347    hub: [USBNode] Hub to get map for.
348    hub_type: [usb_hubs.HubType] Which type of hub it is.
349
350  Returns:
351    Dict of {physical port: (bus number, device number)}
352  """
353  port_device = hub_type.GetPhysicalPortToNodeTuples(hub)
354  return {port: (device.bus_num, device.device_num)
355          for (port, device) in port_device}
356
357
358def GetPhysicalPortToSerialMap(hub, hub_type):
359  """Gets physical-port:serial# mapping for a given hub.
360
361  Args:
362    hub: [USBNode] Hub to get map for.
363    hub_type: [usb_hubs.HubType] Which type of hub it is.
364
365  Returns:
366    Dict of {physical port: serial number)}
367  """
368  port_device = hub_type.GetPhysicalPortToNodeTuples(hub)
369  return {port: device.serial
370          for (port, device) in port_device
371          if device.serial}
372
373
374def GetPhysicalPortToTTYMap(device, hub_type):
375  """Gets physical-port:tty-string mapping for a given hub.
376  Args:
377    hub: [USBNode] Hub to get map for.
378    hub_type: [usb_hubs.HubType] Which type of hub it is.
379
380  Returns:
381    Dict of {physical port: tty-string)}
382  """
383  port_device = hub_type.GetPhysicalPortToNodeTuples(device)
384  bus_device_to_tty = GetBusDeviceToTTYMap()
385  return {port: bus_device_to_tty[(device.bus_num, device.device_num)]
386          for (port, device) in port_device
387          if (device.bus_num, device.device_num) in bus_device_to_tty}
388
389
390def CollectHubMaps(hub_types, map_func, device_tree_map=None, fast=False):
391  """Runs a function on all hubs in the system and collects their output.
392
393  Args:
394    hub_types: [usb_hubs.HubType] List of possible hub types.
395    map_func: [string] Function to run on each hub.
396    device_tree: Previously constructed device tree map, if any.
397    fast: Whether to construct device tree fast, if not already provided
398
399  Yields:
400    Sequence of dicts of {physical port: device} where the type of
401    device depends on the ident keyword. Each dict is a separate hub.
402  """
403  if device_tree_map is None:
404    device_tree_map = GetBusNumberToDeviceTreeMap(fast=fast)
405  for bus in device_tree_map.values():
406    for (hub, hub_type) in GetHubsOnBus(bus, hub_types):
407      yield map_func(hub, hub_type)
408
409
410def GetAllPhysicalPortToNodeMaps(hub_types, **kwargs):
411  return CollectHubMaps(hub_types, GetPhysicalPortToNodeMap, **kwargs)
412
413
414def GetAllPhysicalPortToBusDeviceMaps(hub_types, **kwargs):
415  return CollectHubMaps(hub_types, GetPhysicalPortToBusDeviceMap, **kwargs)
416
417
418def GetAllPhysicalPortToSerialMaps(hub_types, **kwargs):
419  return CollectHubMaps(hub_types, GetPhysicalPortToSerialMap, **kwargs)
420
421
422def GetAllPhysicalPortToTTYMaps(hub_types, **kwargs):
423  return CollectHubMaps(hub_types, GetPhysicalPortToTTYMap, **kwargs)
424
425
426_BUS_NUM_REGEX = re.compile(r'.*ATTRS{busnum}=="(\d*)".*')
427_DEVICE_NUM_REGEX = re.compile(r'.*ATTRS{devnum}=="(\d*)".*')
428
429
430def GetBusDeviceFromTTY(tty_string):
431  """Gets bus and device number connected to a ttyUSB port.
432
433  Args:
434    tty_string: [String] Identifier for ttyUSB (e.g. 'ttyUSB0')
435
436  Returns:
437    Tuple (bus, device) giving device connected to that ttyUSB.
438
439  Raises:
440    ValueError: If bus and device information could not be found.
441  """
442  bus_num = None
443  device_num = None
444  # Expected output of GetCmdOutput should be something like:
445  # looking at device /devices/something/.../.../...
446  # KERNELS="ttyUSB0"
447  # SUBSYSTEMS=...
448  # DRIVERS=...
449  # ATTRS{foo}=...
450  # ATTRS{bar}=...
451  # ...
452  for line in _GetTtyUSBInfo(tty_string).splitlines():
453    bus_match = _BUS_NUM_REGEX.match(line)
454    device_match = _DEVICE_NUM_REGEX.match(line)
455    if bus_match and bus_num is None:
456      bus_num = int(bus_match.group(1))
457    if device_match and device_num is None:
458      device_num = int(device_match.group(1))
459  if bus_num is None or device_num is None:
460    raise ValueError('Info not found')
461  return (bus_num, device_num)
462
463
464def GetBusDeviceToTTYMap():
465  """Gets all mappings from (bus, device) to ttyUSB string.
466
467  Gets mapping from (bus, device) to ttyUSB string (e.g. 'ttyUSB0'),
468  for all ttyUSB strings currently active.
469
470  Returns:
471    [dict] Dict that maps (bus, device) to ttyUSB string
472  """
473  result = {}
474  for tty in GetTTYList():
475    result[GetBusDeviceFromTTY(tty)] = tty
476  return result
477
478
479# This dictionary described the mapping between physical and
480# virtual ports on a Plugable 7-Port Hub (model USB2-HUB7BC).
481# Keys are the virtual ports, values are the physical port.
482# The entry 4:{1:4, 2:3, 3:2, 4:1} indicates that virtual port
483# 4 connects to another 'virtual' hub that itself has the
484# virtual-to-physical port mapping {1:4, 2:3, 3:2, 4:1}.
485
486
487def TestUSBTopologyScript():
488  """Test display and hub identification."""
489  # The following makes logger.info behave pretty much like print
490  # during this test script.
491  logging.basicConfig(format='%(message)s', stream=sys.stdout)
492  logger.setLevel(logging.INFO)
493
494  # Identification criteria for Plugable 7-Port Hub
495  logger.info('==== USB TOPOLOGY SCRIPT TEST ====')
496  logger.info('')
497
498  # Display devices
499  logger.info('==== DEVICE DISPLAY ====')
500  device_trees = GetBusNumberToDeviceTreeMap()
501  for device_tree in device_trees.values():
502    device_tree.Display()
503  logger.info('')
504
505  # Display TTY information about devices plugged into hubs.
506  logger.info('==== TTY INFORMATION ====')
507  for port_map in GetAllPhysicalPortToTTYMaps(
508      usb_hubs.ALL_HUBS, device_tree_map=device_trees):
509    logger.info('%s', port_map)
510  logger.info('')
511
512  # Display serial number information about devices plugged into hubs.
513  logger.info('==== SERIAL NUMBER INFORMATION ====')
514  for port_map in GetAllPhysicalPortToSerialMaps(
515      usb_hubs.ALL_HUBS, device_tree_map=device_trees):
516    logger.info('%s', port_map)
517
518  return 0
519
520
521def parse_options(argv):
522  """Parses and checks the command-line options.
523
524  Returns:
525    A tuple containing the options structure and a list of categories to
526    be traced.
527  """
528  USAGE = '''./find_usb_devices [--help]
529    This script shows the mapping between USB devices and port numbers.
530    Clients are not intended to call this script from the command line.
531    Clients are intended to call the functions in this script directly.
532    For instance, GetAllPhysicalPortToSerialMaps(...)
533    Running this script with --help will display this message.
534    Running this script without --help will display information about
535    devices attached, TTY mapping, and serial number mapping,
536    for testing purposes. See design document for API documentation.
537  '''
538  parser = argparse.ArgumentParser(usage=USAGE)
539  return parser.parse_args(argv[1:])
540
541def main():
542  parse_options(sys.argv)
543  TestUSBTopologyScript()
544
545if __name__ == "__main__":
546  sys.exit(main())
547