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