Home | History | Annotate | Download | only in android
      1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 # pylint: disable=W0212
      6 
      7 import fcntl
      8 import logging
      9 import os
     10 import psutil
     11 
     12 from devil import base_error
     13 from devil import devil_env
     14 from devil.android.constants import file_system
     15 from devil.android.valgrind_tools import base_tool
     16 from devil.utils import cmd_helper
     17 
     18 
     19 def _GetProcessStartTime(pid):
     20   return psutil.Process(pid).create_time
     21 
     22 
     23 class _FileLock(object):
     24   """With statement-aware implementation of a file lock.
     25 
     26   File locks are needed for cross-process synchronization when the
     27   multiprocessing Python module is used.
     28   """
     29 
     30   def __init__(self, path):
     31     self._fd = -1
     32     self._path = path
     33 
     34   def __enter__(self):
     35     self._fd = os.open(self._path, os.O_RDONLY | os.O_CREAT)
     36     if self._fd < 0:
     37       raise Exception('Could not open file %s for reading' % self._path)
     38     fcntl.flock(self._fd, fcntl.LOCK_EX)
     39 
     40   def __exit__(self, _exception_type, _exception_value, traceback):
     41     fcntl.flock(self._fd, fcntl.LOCK_UN)
     42     os.close(self._fd)
     43 
     44 
     45 class HostForwarderError(base_error.BaseError):
     46   """Exception for failures involving host_forwarder."""
     47 
     48   def __init__(self, message):
     49     super(HostForwarderError, self).__init__(message)
     50 
     51 
     52 class Forwarder(object):
     53   """Thread-safe class to manage port forwards from the device to the host."""
     54 
     55   _DEVICE_FORWARDER_FOLDER = (file_system.TEST_EXECUTABLE_DIR +
     56                               '/forwarder/')
     57   _DEVICE_FORWARDER_PATH = (file_system.TEST_EXECUTABLE_DIR +
     58                             '/forwarder/device_forwarder')
     59   _LOCK_PATH = '/tmp/chrome.forwarder.lock'
     60   # Defined in host_forwarder_main.cc
     61   _HOST_FORWARDER_LOG = '/tmp/host_forwarder_log'
     62 
     63   _instance = None
     64 
     65   @staticmethod
     66   def Map(port_pairs, device, tool=None):
     67     """Runs the forwarder.
     68 
     69     Args:
     70       port_pairs: A list of tuples (device_port, host_port) to forward. Note
     71                  that you can specify 0 as a device_port, in which case a
     72                  port will by dynamically assigned on the device. You can
     73                  get the number of the assigned port using the
     74                  DevicePortForHostPort method.
     75       device: A DeviceUtils instance.
     76       tool: Tool class to use to get wrapper, if necessary, for executing the
     77             forwarder (see valgrind_tools.py).
     78 
     79     Raises:
     80       Exception on failure to forward the port.
     81     """
     82     if not tool:
     83       tool = base_tool.BaseTool()
     84     with _FileLock(Forwarder._LOCK_PATH):
     85       instance = Forwarder._GetInstanceLocked(tool)
     86       instance._InitDeviceLocked(device, tool)
     87 
     88       device_serial = str(device)
     89       redirection_commands = [
     90           ['--adb=' + devil_env.config.FetchPath('adb'),
     91            '--serial-id=' + device_serial,
     92            '--map', str(device_port), str(host_port)]
     93           for device_port, host_port in port_pairs]
     94       logging.info('Forwarding using commands: %s', redirection_commands)
     95 
     96       for redirection_command in redirection_commands:
     97         try:
     98           (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
     99               [instance._host_forwarder_path] + redirection_command)
    100         except OSError as e:
    101           if e.errno == 2:
    102             raise HostForwarderError(
    103                 'Unable to start host forwarder. '
    104                 'Make sure you have built host_forwarder.')
    105           else: raise
    106         if exit_code != 0:
    107           Forwarder._KillDeviceLocked(device, tool)
    108           # Log alive forwarders
    109           ps_out = device.RunShellCommand(['ps'])
    110           logging.info('Currently running device_forwarders:')
    111           for line in ps_out:
    112             if 'device_forwarder' in line:
    113               logging.info('    %s', line)
    114           raise HostForwarderError(
    115               '%s exited with %d:\n%s' % (instance._host_forwarder_path,
    116                                           exit_code, '\n'.join(output)))
    117         tokens = output.split(':')
    118         if len(tokens) != 2:
    119           raise HostForwarderError(
    120               'Unexpected host forwarder output "%s", '
    121               'expected "device_port:host_port"' % output)
    122         device_port = int(tokens[0])
    123         host_port = int(tokens[1])
    124         serial_with_port = (device_serial, device_port)
    125         instance._device_to_host_port_map[serial_with_port] = host_port
    126         instance._host_to_device_port_map[host_port] = serial_with_port
    127         logging.info('Forwarding device port: %d to host port: %d.',
    128                      device_port, host_port)
    129 
    130   @staticmethod
    131   def UnmapDevicePort(device_port, device):
    132     """Unmaps a previously forwarded device port.
    133 
    134     Args:
    135       device: A DeviceUtils instance.
    136       device_port: A previously forwarded port (through Map()).
    137     """
    138     with _FileLock(Forwarder._LOCK_PATH):
    139       Forwarder._UnmapDevicePortLocked(device_port, device)
    140 
    141   @staticmethod
    142   def UnmapAllDevicePorts(device):
    143     """Unmaps all the previously forwarded ports for the provided device.
    144 
    145     Args:
    146       device: A DeviceUtils instance.
    147       port_pairs: A list of tuples (device_port, host_port) to unmap.
    148     """
    149     with _FileLock(Forwarder._LOCK_PATH):
    150       if not Forwarder._instance:
    151         return
    152       adb_serial = str(device)
    153       if adb_serial not in Forwarder._instance._initialized_devices:
    154         return
    155       port_map = Forwarder._GetInstanceLocked(
    156           None)._device_to_host_port_map
    157       for (device_serial, device_port) in port_map.keys():
    158         if adb_serial == device_serial:
    159           Forwarder._UnmapDevicePortLocked(device_port, device)
    160       # There are no more ports mapped, kill the device_forwarder.
    161       tool = base_tool.BaseTool()
    162       Forwarder._KillDeviceLocked(device, tool)
    163 
    164   @staticmethod
    165   def DevicePortForHostPort(host_port):
    166     """Returns the device port that corresponds to a given host port."""
    167     with _FileLock(Forwarder._LOCK_PATH):
    168       _, device_port = Forwarder._GetInstanceLocked(
    169           None)._host_to_device_port_map.get(host_port)
    170       return device_port
    171 
    172   @staticmethod
    173   def RemoveHostLog():
    174     if os.path.exists(Forwarder._HOST_FORWARDER_LOG):
    175       os.unlink(Forwarder._HOST_FORWARDER_LOG)
    176 
    177   @staticmethod
    178   def GetHostLog():
    179     if not os.path.exists(Forwarder._HOST_FORWARDER_LOG):
    180       return ''
    181     with file(Forwarder._HOST_FORWARDER_LOG, 'r') as f:
    182       return f.read()
    183 
    184   @staticmethod
    185   def _GetInstanceLocked(tool):
    186     """Returns the singleton instance.
    187 
    188     Note that the global lock must be acquired before calling this method.
    189 
    190     Args:
    191       tool: Tool class to use to get wrapper, if necessary, for executing the
    192             forwarder (see valgrind_tools.py).
    193     """
    194     if not Forwarder._instance:
    195       Forwarder._instance = Forwarder(tool)
    196     return Forwarder._instance
    197 
    198   def __init__(self, tool):
    199     """Constructs a new instance of Forwarder.
    200 
    201     Note that Forwarder is a singleton therefore this constructor should be
    202     called only once.
    203 
    204     Args:
    205       tool: Tool class to use to get wrapper, if necessary, for executing the
    206             forwarder (see valgrind_tools.py).
    207     """
    208     assert not Forwarder._instance
    209     self._tool = tool
    210     self._initialized_devices = set()
    211     self._device_to_host_port_map = dict()
    212     self._host_to_device_port_map = dict()
    213     self._host_forwarder_path = devil_env.config.FetchPath('forwarder_host')
    214     assert os.path.exists(self._host_forwarder_path), 'Please build forwarder2'
    215     self._InitHostLocked()
    216 
    217   @staticmethod
    218   def _UnmapDevicePortLocked(device_port, device):
    219     """Internal method used by UnmapDevicePort().
    220 
    221     Note that the global lock must be acquired before calling this method.
    222     """
    223     instance = Forwarder._GetInstanceLocked(None)
    224     serial = str(device)
    225     serial_with_port = (serial, device_port)
    226     if not serial_with_port in instance._device_to_host_port_map:
    227       logging.error('Trying to unmap non-forwarded port %d', device_port)
    228       return
    229     redirection_command = ['--adb=' + devil_env.config.FetchPath('adb'),
    230                            '--serial-id=' + serial,
    231                            '--unmap', str(device_port)]
    232     logging.info('Undo forwarding using command: %s', redirection_command)
    233     (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
    234         [instance._host_forwarder_path] + redirection_command)
    235     if exit_code != 0:
    236       logging.error(
    237           '%s exited with %d:\n%s',
    238           instance._host_forwarder_path, exit_code, '\n'.join(output))
    239     host_port = instance._device_to_host_port_map[serial_with_port]
    240     del instance._device_to_host_port_map[serial_with_port]
    241     del instance._host_to_device_port_map[host_port]
    242 
    243   @staticmethod
    244   def _GetPidForLock():
    245     """Returns the PID used for host_forwarder initialization.
    246 
    247     The PID of the "sharder" is used to handle multiprocessing. The "sharder"
    248     is the initial process that forks that is the parent process.
    249     """
    250     return os.getpgrp()
    251 
    252   def _InitHostLocked(self):
    253     """Initializes the host forwarder daemon.
    254 
    255     Note that the global lock must be acquired before calling this method. This
    256     method kills any existing host_forwarder process that could be stale.
    257     """
    258     # See if the host_forwarder daemon was already initialized by a concurrent
    259     # process or thread (in case multi-process sharding is not used).
    260     pid_for_lock = Forwarder._GetPidForLock()
    261     fd = os.open(Forwarder._LOCK_PATH, os.O_RDWR | os.O_CREAT)
    262     with os.fdopen(fd, 'r+') as pid_file:
    263       pid_with_start_time = pid_file.readline()
    264       if pid_with_start_time:
    265         (pid, process_start_time) = pid_with_start_time.split(':')
    266         if pid == str(pid_for_lock):
    267           if process_start_time == str(_GetProcessStartTime(pid_for_lock)):
    268             return
    269       self._KillHostLocked()
    270       pid_file.seek(0)
    271       pid_file.write(
    272           '%s:%s' % (pid_for_lock, str(_GetProcessStartTime(pid_for_lock))))
    273       pid_file.truncate()
    274 
    275   def _InitDeviceLocked(self, device, tool):
    276     """Initializes the device_forwarder daemon for a specific device (once).
    277 
    278     Note that the global lock must be acquired before calling this method. This
    279     method kills any existing device_forwarder daemon on the device that could
    280     be stale, pushes the latest version of the daemon (to the device) and starts
    281     it.
    282 
    283     Args:
    284       device: A DeviceUtils instance.
    285       tool: Tool class to use to get wrapper, if necessary, for executing the
    286             forwarder (see valgrind_tools.py).
    287     """
    288     device_serial = str(device)
    289     if device_serial in self._initialized_devices:
    290       return
    291     Forwarder._KillDeviceLocked(device, tool)
    292     forwarder_device_path_on_host = devil_env.config.FetchPath(
    293         'forwarder_device', device=device)
    294     forwarder_device_path_on_device = (
    295         Forwarder._DEVICE_FORWARDER_FOLDER
    296         if os.path.isdir(forwarder_device_path_on_host)
    297         else Forwarder._DEVICE_FORWARDER_PATH)
    298     device.PushChangedFiles([(
    299         forwarder_device_path_on_host,
    300         forwarder_device_path_on_device)])
    301 
    302     cmd = '%s %s' % (tool.GetUtilWrapper(), Forwarder._DEVICE_FORWARDER_PATH)
    303     device.RunShellCommand(
    304         cmd, env={'LD_LIBRARY_PATH': Forwarder._DEVICE_FORWARDER_FOLDER},
    305         check_return=True)
    306     self._initialized_devices.add(device_serial)
    307 
    308   def _KillHostLocked(self):
    309     """Kills the forwarder process running on the host.
    310 
    311     Note that the global lock must be acquired before calling this method.
    312     """
    313     logging.info('Killing host_forwarder.')
    314     (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
    315         [self._host_forwarder_path, '--kill-server'])
    316     if exit_code != 0:
    317       (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
    318           ['pkill', '-9', 'host_forwarder'])
    319       if exit_code != 0:
    320         raise HostForwarderError(
    321             '%s exited with %d:\n%s' % (self._host_forwarder_path, exit_code,
    322                                         '\n'.join(output)))
    323 
    324   @staticmethod
    325   def _KillDeviceLocked(device, tool):
    326     """Kills the forwarder process running on the device.
    327 
    328     Note that the global lock must be acquired before calling this method.
    329 
    330     Args:
    331       device: Instance of DeviceUtils for talking to the device.
    332       tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
    333             forwarder (see valgrind_tools.py).
    334     """
    335     logging.info('Killing device_forwarder.')
    336     Forwarder._instance._initialized_devices.discard(str(device))
    337     if not device.FileExists(Forwarder._DEVICE_FORWARDER_PATH):
    338       return
    339 
    340     cmd = '%s %s --kill-server' % (tool.GetUtilWrapper(),
    341                                    Forwarder._DEVICE_FORWARDER_PATH)
    342     device.RunShellCommand(
    343         cmd, env={'LD_LIBRARY_PATH': Forwarder._DEVICE_FORWARDER_FOLDER},
    344         check_return=True)
    345