Chromium Code Reviews| Index: build/android/pylib/forwarder.py |
| diff --git a/build/android/pylib/forwarder.py b/build/android/pylib/forwarder.py |
| index 5bdb1cd4ce92b2598e9cff38295f19fd3950c572..01d368d1dbca3ce6b13b2073d6377f5d65925df3 100644 |
| --- a/build/android/pylib/forwarder.py |
| +++ b/build/android/pylib/forwarder.py |
| @@ -2,24 +2,49 @@ |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| +import fcntl |
| import logging |
| import os |
| +import psutil |
| import re |
| import sys |
| -import threading |
| import time |
| import android_commands |
| import cmd_helper |
| import constants |
| -from pylib import pexpect |
| +from pylib import valgrind_tools |
| def _MakeBinaryPath(build_type, binary_name): |
| return os.path.join(cmd_helper.OutDirectory.get(), build_type, binary_name) |
| +def _GetProcessStartTime(pid): |
| + return psutil.Process(pid).create_time |
| + |
| + |
| +class _FileLock(object): |
| + """With statement-aware implementation of a file lock. |
| + |
| + File locks are needed for cross-process synchronization when the |
| + multiprocessing Python module is used. |
| + """ |
| + def __init__(self, path): |
| + self._path = path |
| + |
| + def __enter__(self): |
| + self._fd = os.open(self._path, os.O_RDONLY | os.O_CREAT) |
| + if self._fd < 0: |
| + raise Exception('Could not open file %s for reading' % self._path) |
| + fcntl.flock(self._fd, fcntl.LOCK_EX) |
| + |
| + def __exit__(self, type, value, traceback): |
| + fcntl.flock(self._fd, fcntl.LOCK_UN) |
| + os.close(self._fd) |
| + |
| + |
| class Forwarder(object): |
| """Thread-safe class to manage port forwards from the device to the host.""" |
| @@ -28,28 +53,18 @@ class Forwarder(object): |
| _DEVICE_FORWARDER_PATH = (constants.TEST_EXECUTABLE_DIR + |
| '/forwarder/device_forwarder') |
| _LD_LIBRARY_PATH = 'LD_LIBRARY_PATH=%s' % _DEVICE_FORWARDER_FOLDER |
| + _LOCK_PATH = '/tmp/chrome.forwarder.lock' |
| + _MULTIPROCESSING_ENV_VAR = 'CHROME_FORWARDER_USE_MULTIPROCESSING' |
| - def __init__(self, adb, build_type): |
| - """Forwards TCP ports on the device back to the host. |
| - |
| - Works like adb forward, but in reverse. |
| + _instance = None |
| - Args: |
| - adb: Instance of AndroidCommands for talking to the device. |
| - build_type: 'Release' or 'Debug'. |
| - """ |
| - assert build_type in ('Release', 'Debug') |
| - self._adb = adb |
| - self._device_to_host_port_map = dict() |
| - self._host_to_device_port_map = dict() |
| - self._device_initialized = False |
| - self._host_adb_control_port = 0 |
| - self._lock = threading.Lock() |
| - self._host_forwarder_path = _MakeBinaryPath(build_type, 'host_forwarder') |
| - self._device_forwarder_path_on_host = os.path.join( |
| - cmd_helper.OutDirectory.get(), build_type, 'forwarder_dist') |
| + @staticmethod |
| + def UseMultiprocessing(): |
| + """Tells the forwarder that multiprocessing is used.""" |
| + os.environ[Forwarder._MULTIPROCESSING_ENV_VAR] = '1' |
| - def Run(self, port_pairs, tool): |
| + @staticmethod |
| + def Map(port_pairs, adb, build_type='Debug', tool=None): |
| """Runs the forwarder. |
| Args: |
| @@ -58,24 +73,29 @@ class Forwarder(object): |
| port will by dynamically assigned on the device. You can |
| get the number of the assigned port using the |
| DevicePortForHostPort method. |
| + adb: An AndroidCommands instance. |
| tool: Tool class to use to get wrapper, if necessary, for executing the |
| forwarder (see valgrind_tools.py). |
| Raises: |
| Exception on failure to forward the port. |
| """ |
| - with self._lock: |
| - self._InitDeviceLocked(tool) |
| - host_name = '127.0.0.1' |
| + if not tool: |
| + tool = valgrind_tools.CreateTool(None, adb) |
| + with _FileLock(Forwarder._LOCK_PATH): |
| + instance = Forwarder._GetInstanceLocked(build_type, tool) |
| + instance._InitDeviceLocked(adb, tool) |
| + |
| + device_serial = adb.Adb().GetSerialNumber() |
| redirection_commands = [ |
| - ['--serial-id=' + self._adb.Adb().GetSerialNumber(), '--map', |
| - str(device), str(host)] for device, host in port_pairs] |
| + ['--serial-id=' + device_serial, '--map', str(device), |
| + str(host)] for device, host in port_pairs] |
| logging.info('Forwarding using commands: %s', redirection_commands) |
| for redirection_command in redirection_commands: |
| try: |
| (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( |
| - [self._host_forwarder_path] + redirection_command) |
| + [instance._host_forwarder_path] + redirection_command) |
| except OSError as e: |
| if e.errno == 2: |
| raise Exception('Unable to start host forwarder. Make sure you have' |
| @@ -83,83 +103,202 @@ class Forwarder(object): |
| else: raise |
| if exit_code != 0: |
| raise Exception('%s exited with %d:\n%s' % ( |
| - self._host_forwarder_path, exit_code, '\n'.join(output))) |
| + instance._host_forwarder_path, exit_code, '\n'.join(output))) |
| tokens = output.split(':') |
| if len(tokens) != 2: |
| raise Exception(('Unexpected host forwarder output "%s", ' + |
| 'expected "device_port:host_port"') % output) |
| device_port = int(tokens[0]) |
| host_port = int(tokens[1]) |
| - self._device_to_host_port_map[device_port] = host_port |
| - self._host_to_device_port_map[host_port] = device_port |
| + serial_with_port = (device_serial, device_port) |
| + instance._device_to_host_port_map[serial_with_port] = host_port |
| + instance._host_to_device_port_map[host_port] = serial_with_port |
| logging.info('Forwarding device port: %d to host port: %d.', |
| device_port, host_port) |
| - def _InitDeviceLocked(self, tool): |
| - """Initializes the device forwarder process (only once).""" |
| - if self._device_initialized: |
| - return |
| - self._adb.PushIfNeeded( |
| - self._device_forwarder_path_on_host, |
| - Forwarder._DEVICE_FORWARDER_FOLDER) |
| - (exit_code, output) = self._adb.GetShellCommandStatusAndOutput( |
| - '%s %s %s' % (Forwarder._LD_LIBRARY_PATH, tool.GetUtilWrapper(), |
| - Forwarder._DEVICE_FORWARDER_PATH)) |
| - if exit_code != 0: |
| - raise Exception( |
| - 'Failed to start device forwarder:\n%s' % '\n'.join(output)) |
| - self._device_initialized = True |
| - |
| - def UnmapDevicePort(self, device_port): |
| + @staticmethod |
| + def UnmapDevicePort(device_port, adb): |
| """Unmaps a previously forwarded device port. |
| Args: |
| - device_port: A previously forwarded port (through Run()). |
| + adb: An AndroidCommands instance. |
| + device_port: A previously forwarded port (through Map()). |
| + """ |
| + with _FileLock(Forwarder._LOCK_PATH): |
| + Forwarder._UnmapDevicePortLocked(device_port, adb) |
| + |
| + @staticmethod |
| + def UnmapAllDevicePorts(adb): |
| + """Unmaps all the previously forwarded ports for the provided device. |
| + |
| + Args: |
| + adb: An AndroidCommands instance. |
| + port_pairs: A list of tuples (device_port, host_port) to unmap. |
| + """ |
| + with _FileLock(Forwarder._LOCK_PATH): |
| + port_map = Forwarder._GetInstanceLocked( |
| + None, None)._device_to_host_port_map |
| + adb_serial = adb.Adb().GetSerialNumber() |
| + for ((device_serial, device_port), _) in port_map: |
| + if adb_serial == device_serial: |
| + Forwarder._UnmapDevicePortLocked(device_port, adb) |
| + |
| + @staticmethod |
| + def DevicePortForHostPort(host_port): |
| + """Returns the device port that corresponds to a given host port.""" |
| + with _FileLock(Forwarder._LOCK_PATH): |
| + (device_serial, device_port) = Forwarder._GetInstanceLocked( |
| + None, None)._host_to_device_port_map.get(host_port) |
| + return device_port |
| + |
| + @staticmethod |
| + def _GetInstanceLocked(build_type, tool): |
| + """Returns the singleton instance. |
| + |
| + Note that the global lock must be acquired before calling this method. |
| + |
| + Args: |
| + build_type: 'Release' or 'Debug' |
| + tool: Tool class to use to get wrapper, if necessary, for executing the |
| + forwarder (see valgrind_tools.py). |
| """ |
| - with self._lock: |
| - self._UnmapDevicePortInternalLocked(device_port) |
| + if not Forwarder._instance: |
| + Forwarder._instance = Forwarder(build_type, tool) |
| + return Forwarder._instance |
| + |
| + def __init__(self, build_type, tool): |
| + """Constructs a new instance of Forwarder. |
| - def _UnmapDevicePortInternalLocked(self, device_port): |
| - if not device_port in self._device_to_host_port_map: |
| + Note that Forwarder is a singleton therefore this constructor should be |
| + called only once. |
| + |
| + Args: |
| + build_type: 'Release' or 'Debug' |
| + tool: Tool class to use to get wrapper, if necessary, for executing the |
| + forwarder (see valgrind_tools.py). |
| + """ |
| + assert not Forwarder._instance |
| + self._build_type = build_type |
| + self._tool = tool |
| + self._initialized_devices = set() |
| + self._device_to_host_port_map = dict() |
| + self._host_to_device_port_map = dict() |
| + self._host_forwarder_path = _MakeBinaryPath( |
| + self._build_type, 'host_forwarder') |
| + if not os.path.exists(self._host_forwarder_path): |
| + self._build_type = 'Release' if self._build_type == 'Debug' else 'Debug' |
| + self._host_forwarder_path = _MakeBinaryPath( |
| + self._build_type, 'host_forwarder') |
| + assert os.path.exists( |
| + self._host_forwarder_path), 'Please build forwarder2' |
| + self._device_forwarder_path_on_host = os.path.join( |
| + cmd_helper.OutDirectory.get(), self._build_type, 'forwarder_dist') |
| + self._InitHostLocked() |
| + |
| + @staticmethod |
| + def _UnmapDevicePortLocked(device_port, adb): |
| + """Internal method used by UnmapDevicePort(). |
| + |
| + Note that the global lock must be acquired before calling this method. |
| + """ |
| + instance = Forwarder._GetInstanceLocked(None, None) |
| + serial = adb.Adb().GetSerialNumber() |
| + serial_with_port = (serial, device_port) |
| + if not serial_with_port in instance._device_to_host_port_map: |
|
bulach
2013/07/17 10:20:19
nit: this shouldn't happen, so perhaps a logging.e
Philippe
2013/07/17 10:24:39
Done.
|
| return |
| - redirection_command = [ |
| - '--serial-id=' + self._adb.Adb().GetSerialNumber(), '--unmap', |
| - str(device_port)] |
| + redirection_command = ['--serial-id=' + serial, '--unmap', str(device_port)] |
| (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( |
| - [self._host_forwarder_path] + redirection_command) |
| + [instance._host_forwarder_path] + redirection_command) |
| if exit_code != 0: |
| logging.error('%s exited with %d:\n%s' % ( |
| - self._host_forwarder_path, exit_code, '\n'.join(output))) |
| - host_port = self._device_to_host_port_map[device_port] |
| - del self._device_to_host_port_map[device_port] |
| - del self._host_to_device_port_map[host_port] |
| + instance._host_forwarder_path, exit_code, '\n'.join(output))) |
| + host_port = instance._device_to_host_port_map[serial_with_port] |
| + del instance._device_to_host_port_map[serial_with_port] |
| + del instance._host_to_device_port_map[host_port] |
| @staticmethod |
| - def KillHost(build_type='Debug'): |
| - """Kills the forwarder process running on the host. |
| + def _GetPidForLock(): |
| + """Returns the PID used for host_forwarder initialization. |
| + |
| + In case multi-process sharding is used, the PID of the "sharder" is used. |
| + The "sharder" is the initial process that forks that is the parent process. |
| + By default, multi-processing is not used. In that case the PID of the |
| + current process is returned. |
| + """ |
| + use_multiprocessing = Forwarder._MULTIPROCESSING_ENV_VAR in os.environ |
| + return os.getppid() if use_multiprocessing else os.getpid() |
| + |
| + def _InitHostLocked(self): |
| + """Initializes the host forwarder daemon. |
| + |
| + Note that the global lock must be acquired before calling this method. This |
| + method kills any existing host_forwarder process that could be stale. |
| + """ |
| + # See if the host_forwarder daemon was already initialized by a concurrent |
| + # process or thread (in case multi-process sharding is not used). |
| + pid_for_lock = Forwarder._GetPidForLock() |
| + fd = os.open(Forwarder._LOCK_PATH, os.O_RDWR | os.O_CREAT) |
| + with os.fdopen(fd, 'r+') as pid_file: |
| + pid_with_start_time = pid_file.readline() |
| + if pid_with_start_time: |
| + (pid, process_start_time) = pid_with_start_time.split(':') |
| + if pid == str(pid_for_lock): |
| + if process_start_time == str(_GetProcessStartTime(pid_for_lock)): |
| + return |
| + self._KillHostLocked() |
| + pid_file.seek(0) |
| + pid_file.write( |
| + '%s:%s' % (pid_for_lock, str(_GetProcessStartTime(pid_for_lock)))) |
| + |
| + def _InitDeviceLocked(self, adb, tool): |
| + """Initializes the device_forwarder daemon for a specific device (once). |
| + |
| + Note that the global lock must be acquired before calling this method. This |
| + method kills any existing device_forwarder daemon on the device that could |
| + be stale, pushes the latest version of the daemon (to the device) and starts |
| + it. |
| Args: |
| - build_type: 'Release' or 'Debug' (default='Debug') |
| + adb: An AndroidCommands instance. |
| + tool: Tool class to use to get wrapper, if necessary, for executing the |
| + forwarder (see valgrind_tools.py). |
| + """ |
| + device_serial = adb.Adb().GetSerialNumber() |
| + if device_serial in self._initialized_devices: |
| + return |
| + Forwarder._KillDeviceLocked(adb, tool) |
| + adb.PushIfNeeded( |
| + self._device_forwarder_path_on_host, |
| + Forwarder._DEVICE_FORWARDER_FOLDER) |
| + (exit_code, output) = adb.GetShellCommandStatusAndOutput( |
| + '%s %s %s' % (Forwarder._LD_LIBRARY_PATH, tool.GetUtilWrapper(), |
| + Forwarder._DEVICE_FORWARDER_PATH)) |
| + if exit_code != 0: |
| + raise Exception( |
| + 'Failed to start device forwarder:\n%s' % '\n'.join(output)) |
| + self._initialized_devices.add(device_serial) |
| + |
| + def _KillHostLocked(self): |
| + """Kills the forwarder process running on the host. |
| + |
| + Note that the global lock must be acquired before calling this method. |
| """ |
| logging.info('Killing host_forwarder.') |
| - host_forwarder_path = _MakeBinaryPath(build_type, 'host_forwarder') |
| - if not os.path.exists(host_forwarder_path): |
| - host_forwarder_path = _MakeBinaryPath( |
| - 'Release' if build_type == 'Debug' else 'Debug', 'host_forwarder') |
| - assert os.path.exists(host_forwarder_path), 'Please build forwarder2' |
| (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( |
| - [host_forwarder_path, '--kill-server']) |
| + [self._host_forwarder_path, '--kill-server']) |
| if exit_code != 0: |
| (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( |
| - ['pkill', 'host_forwarder']) |
| + ['pkill', '-9', 'host_forwarder']) |
| if exit_code != 0: |
| raise Exception('%s exited with %d:\n%s' % ( |
| - host_forwarder_path, exit_code, '\n'.join(output))) |
| + self._host_forwarder_path, exit_code, '\n'.join(output))) |
| @staticmethod |
| - def KillDevice(adb, tool): |
| + def _KillDeviceLocked(adb, tool): |
| """Kills the forwarder process running on the device. |
| + Note that the global lock must be acquired before calling this method. |
| + |
| Args: |
| adb: Instance of AndroidCommands for talking to the device. |
| tool: Wrapper tool (e.g. valgrind) that can be used to execute the device |
| @@ -180,14 +319,3 @@ class Forwarder(object): |
| pids = adb.ExtractPid('device_forwarder') |
| if pids: |
| raise Exception('Timed out while killing device_forwarder') |
| - |
| - def DevicePortForHostPort(self, host_port): |
| - """Returns the device port that corresponds to a given host port.""" |
| - with self._lock: |
| - return self._host_to_device_port_map.get(host_port) |
| - |
| - def Close(self): |
| - """Releases the previously forwarded ports.""" |
| - with self._lock: |
| - for device_port in self._device_to_host_port_map.copy(): |
| - self._UnmapDevicePortInternalLocked(device_port) |