Home | History | Annotate | Download | only in metrics
      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