1 #!/usr/bin/env python 2 # 3 # Copyright 2017 - The Android Open Source Project 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 import io 18 import os 19 import subprocess 20 21 import sys 22 23 from metrics.metric import Metric 24 from utils import job 25 from utils import time_limit 26 27 28 def _get_output(stdout): 29 if sys.version_info[0] == 2: 30 return iter(stdout.readline, '') 31 else: 32 return io.TextIOWrapper(stdout, encoding="utf-8") 33 34 35 class UsbMetric(Metric): 36 """Class to determine all USB Device traffic over a timeframe.""" 37 USB_IO_COMMAND = 'cat /sys/kernel/debug/usb/usbmon/0u | grep -v \'S Ci\'' 38 USBMON_CHECK_COMMAND = 'grep usbmon /proc/modules' 39 USBMON_INSTALL_COMMAND = 'modprobe usbmon' 40 DEVICES = 'devices' 41 42 def is_privileged(self): 43 """Checks if this module is being ran as the necessary root user. 44 45 Returns: 46 T if being run as root, F if not. 47 """ 48 49 return os.getuid() == 0 50 51 def check_usbmon(self): 52 """Checks if the kernel module 'usbmon' is installed. 53 54 Runs the command using shell.py. 55 56 Raises: 57 job.Error: When the module could not be loaded. 58 """ 59 try: 60 self._shell.run(self.USBMON_CHECK_COMMAND) 61 except job.Error: 62 print('Kernel module not loaded, attempting to load usbmon') 63 try: 64 self._shell.run(self.USBMON_INSTALL_COMMAND) 65 except job.Error as error: 66 raise job.Error('Cannot load usbmon: %s' % error.result.stderr) 67 68 def get_bytes(self, time=5): 69 """Gathers data about USB Busses in a given timeframe. 70 71 When ran, must have super user privileges as well as having the module 72 'usbmon' installed. Since .../0u is a stream-file, we must read it in 73 as a stream in the off chance of reading in too much data to buffer. 74 75 Args: 76 time: The amount of time data will be gathered in seconds. 77 78 Returns: 79 A dictionary where the key is the device's bus and device number, 80 and value is the amount of bytes transferred in the timeframe. 81 """ 82 bytes_sent = {} 83 with time_limit.TimeLimit(time): 84 # Lines matching 'S Ci' do not match output, and only 4 bytes/sec 85 process = subprocess.Popen( 86 self.USB_IO_COMMAND, 87 stdout=subprocess.PIPE, 88 stderr=subprocess.STDOUT, 89 shell=True) 90 91 for line in _get_output(process.stdout): 92 spl_line = line.split(' ') 93 # Example line spl_line[3] " "[5] 94 # ffff88080bb00780 2452973093 C Ii:2:003:1 0:8 8 = 00000000 95 96 # Splits from whole line, into Ii:2:003:1, and then cuts it 97 # down to 2:003, this is for consistency as keys in dicts. 98 dev_id = ':'.join(spl_line[3].split(':')[1:3]) 99 if dev_id in bytes_sent: 100 # spl_line[5] is the number of bytes transferred from a 101 # device, in the example line, spl_line[5] == 8 102 bytes_sent[dev_id] += int(spl_line[5]) 103 else: 104 bytes_sent[dev_id] = int(spl_line[5]) 105 return bytes_sent 106 107 def match_device_id(self): 108 """ Matches a device's id with its name according to lsusb. 109 110 Returns: 111 A dictionary with the devices 'bus:device' as key, and name of the 112 device as a string. 'bus:device', the bus number is stripped of 113 leading 0's because that is how 'usbmon' formats it. 114 """ 115 devices = {} 116 result = self._shell.run('lsusb').stdout 117 118 if result: 119 # Example line 120 # Bus 003 Device 048: ID 18d1:4ee7 Device Name 121 for line in result.split('\n'): 122 line_list = line.split(' ') 123 # Gets bus number, strips leading 0's, adds a ':', and then adds 124 # the device, without its ':'. Example line output: 3:048 125 dev_id = line_list[1].lstrip('0') + ':' + line_list[3].strip( 126 ':') 127 # Parses the device name, example line output: 'Device Name' 128 dev_name = ' '.join(line_list[6:]) 129 devices[dev_id] = dev_name 130 return devices 131 132 def gen_output(self, dev_name_dict, dev_byte_dict): 133 """ Combines all information about device for returning. 134 135 Args: 136 dev_name_dict: A dictionary with the key as 'bus:device', leading 137 0's stripped from bus, and value as the device's name. 138 dev_byte_dict: A dictionary with the key as 'bus:device', leading 139 0's stripped from bus, and value as the number of bytes transferred. 140 Returns: 141 List of populated Device objects. 142 """ 143 devices = [] 144 for dev in dev_name_dict: 145 if dev in dev_byte_dict: 146 devices.append( 147 Device(dev, dev_byte_dict[dev], dev_name_dict[dev])) 148 else: 149 devices.append(Device(dev, 0, dev_name_dict[dev])) 150 return devices 151 152 def gather_metric(self): 153 """ Gathers the usb bus metric 154 155 Returns: 156 A dictionary, with a single entry, 'devices', and the value of a 157 list of Device objects. This is to fit with the formatting of other 158 metrics. 159 """ 160 if self.is_privileged(): 161 self.check_usbmon() 162 dev_byte_dict = self.get_bytes() 163 dev_name_dict = self.match_device_id() 164 return { 165 self.DEVICES: self.gen_output(dev_name_dict, dev_byte_dict) 166 } 167 else: 168 return {self.DEVICES: None} 169 170 171 class Device: 172 """USB Device Information 173 174 Contains information about bytes transferred in timeframe for a device. 175 176 Attributes: 177 dev_id: The device id, usuall in form BUS:DEVICE 178 trans_bytes: The number of bytes transferred in timeframe. 179 name: The device's name according to lsusb. 180 """ 181 182 def __init__(self, dev_id, trans_bytes, name): 183 self.dev_id = dev_id 184 self.trans_bytes = trans_bytes 185 self.name = name 186 187 def __eq__(self, other): 188 return isinstance(other, Device) and \ 189 self.dev_id == other.dev_id and \ 190 self.trans_bytes == other.trans_bytes and \ 191 self.name == other.name 192