forwarder.py revision a0e5c0de428e9dea6d07dd57c5594fb1f1c17c20
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
7import fcntl
8import logging
9import os
10import psutil
11
12from devil import base_error
13from devil import devil_env
14from devil.android import device_errors
15from devil.android.constants import file_system
16from devil.android.sdk import adb_wrapper
17from devil.android.valgrind_tools import base_tool
18from devil.utils import cmd_helper
19
20logger = logging.getLogger(__name__)
21
22# If passed as the device port, this will tell the forwarder to allocate
23# a dynamic port on the device. The actual port can then be retrieved with
24# Forwarder.DevicePortForHostPort.
25DYNAMIC_DEVICE_PORT = 0
26
27
28def _GetProcessStartTime(pid):
29  return psutil.Process(pid).create_time
30
31
32def _LogMapFailureDiagnostics(device):
33  # The host forwarder daemon logs to /tmp/host_forwarder_log, so print the end
34  # of that.
35  try:
36    with open('/tmp/host_forwarder_log') as host_forwarder_log:
37      logger.info('Last 50 lines of the host forwarder daemon log:')
38      for line in host_forwarder_log.read().splitlines()[-50:]:
39        logger.info('    %s', line)
40  except Exception: # pylint: disable=broad-except
41    # Grabbing the host forwarder log is best-effort. Ignore all errors.
42    logger.warning('Failed to get the contents of host_forwarder_log.')
43
44  # The device forwarder daemon logs to the logcat, so print the end of that.
45  try:
46    logger.info('Last 50 lines of logcat:')
47    for logcat_line in device.adb.Logcat(dump=True)[-50:]:
48      logger.info('    %s', logcat_line)
49  except device_errors.CommandFailedError:
50    # Grabbing the device forwarder log is also best-effort. Ignore all errors.
51    logger.warning('Failed to get the contents of the logcat.')
52
53  # Log alive device forwarders.
54  try:
55    ps_out = device.RunShellCommand(['ps'], check_return=True)
56    logger.info('Currently running device_forwarders:')
57    for line in ps_out:
58      if 'device_forwarder' in line:
59        logger.info('    %s', line)
60  except device_errors.CommandFailedError:
61    logger.warning('Failed to list currently running device_forwarder '
62                   'instances.')
63
64
65class _FileLock(object):
66  """With statement-aware implementation of a file lock.
67
68  File locks are needed for cross-process synchronization when the
69  multiprocessing Python module is used.
70  """
71
72  def __init__(self, path):
73    self._fd = -1
74    self._path = path
75
76  def __enter__(self):
77    self._fd = os.open(self._path, os.O_RDONLY | os.O_CREAT)
78    if self._fd < 0:
79      raise Exception('Could not open file %s for reading' % self._path)
80    fcntl.flock(self._fd, fcntl.LOCK_EX)
81
82  def __exit__(self, _exception_type, _exception_value, traceback):
83    fcntl.flock(self._fd, fcntl.LOCK_UN)
84    os.close(self._fd)
85
86
87class HostForwarderError(base_error.BaseError):
88  """Exception for failures involving host_forwarder."""
89
90  def __init__(self, message):
91    super(HostForwarderError, self).__init__(message)
92
93
94class Forwarder(object):
95  """Thread-safe class to manage port forwards from the device to the host."""
96
97  _DEVICE_FORWARDER_FOLDER = (file_system.TEST_EXECUTABLE_DIR +
98                              '/forwarder/')
99  _DEVICE_FORWARDER_PATH = (file_system.TEST_EXECUTABLE_DIR +
100                            '/forwarder/device_forwarder')
101  _LOCK_PATH = '/tmp/chrome.forwarder.lock'
102  # Defined in host_forwarder_main.cc
103  _HOST_FORWARDER_LOG = '/tmp/host_forwarder_log'
104
105  _TIMEOUT = 60  # seconds
106
107  _instance = None
108
109  @staticmethod
110  def Map(port_pairs, device, tool=None):
111    """Runs the forwarder.
112
113    Args:
114      port_pairs: A list of tuples (device_port, host_port) to forward. Note
115                 that you can specify 0 as a device_port, in which case a
116                 port will by dynamically assigned on the device. You can
117                 get the number of the assigned port using the
118                 DevicePortForHostPort method.
119      device: A DeviceUtils instance.
120      tool: Tool class to use to get wrapper, if necessary, for executing the
121            forwarder (see valgrind_tools.py).
122
123    Raises:
124      Exception on failure to forward the port.
125    """
126    if not tool:
127      tool = base_tool.BaseTool()
128    with _FileLock(Forwarder._LOCK_PATH):
129      instance = Forwarder._GetInstanceLocked(tool)
130      instance._InitDeviceLocked(device, tool)
131
132      device_serial = str(device)
133      map_arg_lists = [
134          ['--adb=' + adb_wrapper.AdbWrapper.GetAdbPath(),
135           '--serial-id=' + device_serial,
136           '--map', str(device_port), str(host_port)]
137          for device_port, host_port in port_pairs]
138      logger.info('Forwarding using commands: %s', map_arg_lists)
139
140      for map_arg_list in map_arg_lists:
141        try:
142          map_cmd = [instance._host_forwarder_path] + map_arg_list
143          (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
144              map_cmd, Forwarder._TIMEOUT)
145        except cmd_helper.TimeoutError as e:
146          raise HostForwarderError(
147              '`%s` timed out:\n%s' % (' '.join(map_cmd), e.output))
148        except OSError as e:
149          if e.errno == 2:
150            raise HostForwarderError(
151                'Unable to start host forwarder. '
152                'Make sure you have built host_forwarder.')
153          else: raise
154        if exit_code != 0:
155          try:
156            instance._KillDeviceLocked(device, tool)
157          except device_errors.CommandFailedError:
158            # We don't want the failure to kill the device forwarder to
159            # supersede the original failure to map.
160            logging.warning(
161                'Failed to kill the device forwarder after map failure: %s',
162                str(e))
163          _LogMapFailureDiagnostics(device)
164          formatted_output = ('\n'.join(output) if isinstance(output, list)
165                              else output)
166          raise HostForwarderError(
167              '`%s` exited with %d:\n%s' % (
168                  ' '.join(map_cmd),
169                  exit_code,
170                  formatted_output))
171        tokens = output.split(':')
172        if len(tokens) != 2:
173          raise HostForwarderError(
174              'Unexpected host forwarder output "%s", '
175              'expected "device_port:host_port"' % output)
176        device_port = int(tokens[0])
177        host_port = int(tokens[1])
178        serial_with_port = (device_serial, device_port)
179        instance._device_to_host_port_map[serial_with_port] = host_port
180        instance._host_to_device_port_map[host_port] = serial_with_port
181        logger.info('Forwarding device port: %d to host port: %d.',
182                    device_port, host_port)
183
184  @staticmethod
185  def UnmapDevicePort(device_port, device):
186    """Unmaps a previously forwarded device port.
187
188    Args:
189      device: A DeviceUtils instance.
190      device_port: A previously forwarded port (through Map()).
191    """
192    with _FileLock(Forwarder._LOCK_PATH):
193      Forwarder._UnmapDevicePortLocked(device_port, device)
194
195  @staticmethod
196  def UnmapAllDevicePorts(device):
197    """Unmaps all the previously forwarded ports for the provided device.
198
199    Args:
200      device: A DeviceUtils instance.
201      port_pairs: A list of tuples (device_port, host_port) to unmap.
202    """
203    with _FileLock(Forwarder._LOCK_PATH):
204      instance = Forwarder._GetInstanceLocked(None)
205      unmap_all_cmd = [
206          instance._host_forwarder_path,
207          '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
208          '--serial-id=%s' % device.serial,
209          '--unmap-all'
210      ]
211      try:
212        exit_code, output = cmd_helper.GetCmdStatusAndOutputWithTimeout(
213            unmap_all_cmd, Forwarder._TIMEOUT)
214      except cmd_helper.TimeoutError as e:
215        raise HostForwarderError(
216            '`%s` timed out:\n%s' % (' '.join(unmap_all_cmd), e.output))
217      if exit_code != 0:
218        error_msg = [
219            '`%s` exited with %d' % (' '.join(unmap_all_cmd), exit_code)]
220        if isinstance(output, list):
221          error_msg += output
222        else:
223          error_msg += [output]
224        raise HostForwarderError('\n'.join(error_msg))
225
226      # Clean out any entries from the device & host map.
227      device_map = instance._device_to_host_port_map
228      host_map = instance._host_to_device_port_map
229      for device_serial_and_port, host_port in device_map.items():
230        device_serial = device_serial_and_port[0]
231        if device_serial == device.serial:
232          del device_map[device_serial_and_port]
233          del host_map[host_port]
234
235      # Kill the device forwarder.
236      tool = base_tool.BaseTool()
237      instance._KillDeviceLocked(device, tool)
238
239  @staticmethod
240  def DevicePortForHostPort(host_port):
241    """Returns the device port that corresponds to a given host port."""
242    with _FileLock(Forwarder._LOCK_PATH):
243      serial_and_port = Forwarder._GetInstanceLocked(
244          None)._host_to_device_port_map.get(host_port)
245      return serial_and_port[1] if serial_and_port else None
246
247  @staticmethod
248  def RemoveHostLog():
249    if os.path.exists(Forwarder._HOST_FORWARDER_LOG):
250      os.unlink(Forwarder._HOST_FORWARDER_LOG)
251
252  @staticmethod
253  def GetHostLog():
254    if not os.path.exists(Forwarder._HOST_FORWARDER_LOG):
255      return ''
256    with file(Forwarder._HOST_FORWARDER_LOG, 'r') as f:
257      return f.read()
258
259  @staticmethod
260  def _GetInstanceLocked(tool):
261    """Returns the singleton instance.
262
263    Note that the global lock must be acquired before calling this method.
264
265    Args:
266      tool: Tool class to use to get wrapper, if necessary, for executing the
267            forwarder (see valgrind_tools.py).
268    """
269    if not Forwarder._instance:
270      Forwarder._instance = Forwarder(tool)
271    return Forwarder._instance
272
273  def __init__(self, tool):
274    """Constructs a new instance of Forwarder.
275
276    Note that Forwarder is a singleton therefore this constructor should be
277    called only once.
278
279    Args:
280      tool: Tool class to use to get wrapper, if necessary, for executing the
281            forwarder (see valgrind_tools.py).
282    """
283    assert not Forwarder._instance
284    self._tool = tool
285    self._initialized_devices = set()
286    self._device_to_host_port_map = dict()
287    self._host_to_device_port_map = dict()
288    self._host_forwarder_path = devil_env.config.FetchPath('forwarder_host')
289    assert os.path.exists(self._host_forwarder_path), 'Please build forwarder2'
290    self._InitHostLocked()
291
292  @staticmethod
293  def _UnmapDevicePortLocked(device_port, device):
294    """Internal method used by UnmapDevicePort().
295
296    Note that the global lock must be acquired before calling this method.
297    """
298    instance = Forwarder._GetInstanceLocked(None)
299    serial = str(device)
300    serial_with_port = (serial, device_port)
301    if not serial_with_port in instance._device_to_host_port_map:
302      logger.error('Trying to unmap non-forwarded port %d', device_port)
303      return
304
305    host_port = instance._device_to_host_port_map[serial_with_port]
306    del instance._device_to_host_port_map[serial_with_port]
307    del instance._host_to_device_port_map[host_port]
308
309    unmap_cmd = [
310        instance._host_forwarder_path,
311        '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
312        '--serial-id=%s' % serial,
313        '--unmap', str(device_port)
314    ]
315    try:
316      (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
317          unmap_cmd, Forwarder._TIMEOUT)
318    except cmd_helper.TimeoutError as e:
319      raise HostForwarderError(
320          '`%s` timed out:\n%s' % (' '.join(unmap_cmd), e.output))
321    if exit_code != 0:
322      logger.error(
323          '`%s` exited with %d:\n%s',
324          ' '.join(unmap_cmd),
325          exit_code,
326          '\n'.join(output) if isinstance(output, list) else output)
327
328  @staticmethod
329  def _GetPidForLock():
330    """Returns the PID used for host_forwarder initialization.
331
332    The PID of the "sharder" is used to handle multiprocessing. The "sharder"
333    is the initial process that forks that is the parent process.
334    """
335    return os.getpgrp()
336
337  def _InitHostLocked(self):
338    """Initializes the host forwarder daemon.
339
340    Note that the global lock must be acquired before calling this method. This
341    method kills any existing host_forwarder process that could be stale.
342    """
343    # See if the host_forwarder daemon was already initialized by a concurrent
344    # process or thread (in case multi-process sharding is not used).
345    pid_for_lock = Forwarder._GetPidForLock()
346    fd = os.open(Forwarder._LOCK_PATH, os.O_RDWR | os.O_CREAT)
347    with os.fdopen(fd, 'r+') as pid_file:
348      pid_with_start_time = pid_file.readline()
349      if pid_with_start_time:
350        (pid, process_start_time) = pid_with_start_time.split(':')
351        if pid == str(pid_for_lock):
352          if process_start_time == str(_GetProcessStartTime(pid_for_lock)):
353            return
354      self._KillHostLocked()
355      pid_file.seek(0)
356      pid_file.write(
357          '%s:%s' % (pid_for_lock, str(_GetProcessStartTime(pid_for_lock))))
358      pid_file.truncate()
359
360  def _InitDeviceLocked(self, device, tool):
361    """Initializes the device_forwarder daemon for a specific device (once).
362
363    Note that the global lock must be acquired before calling this method. This
364    method kills any existing device_forwarder daemon on the device that could
365    be stale, pushes the latest version of the daemon (to the device) and starts
366    it.
367
368    Args:
369      device: A DeviceUtils instance.
370      tool: Tool class to use to get wrapper, if necessary, for executing the
371            forwarder (see valgrind_tools.py).
372    """
373    device_serial = str(device)
374    if device_serial in self._initialized_devices:
375      return
376    try:
377      self._KillDeviceLocked(device, tool)
378    except device_errors.CommandFailedError:
379      logger.warning('Failed to kill device forwarder. Rebooting.')
380      device.Reboot()
381    forwarder_device_path_on_host = devil_env.config.FetchPath(
382        'forwarder_device', device=device)
383    forwarder_device_path_on_device = (
384        Forwarder._DEVICE_FORWARDER_FOLDER
385        if os.path.isdir(forwarder_device_path_on_host)
386        else Forwarder._DEVICE_FORWARDER_PATH)
387    device.PushChangedFiles([(
388        forwarder_device_path_on_host,
389        forwarder_device_path_on_device)])
390
391    cmd = [Forwarder._DEVICE_FORWARDER_PATH]
392    wrapper = tool.GetUtilWrapper()
393    if wrapper:
394      cmd.insert(0, wrapper)
395    device.RunShellCommand(
396        cmd, env={'LD_LIBRARY_PATH': Forwarder._DEVICE_FORWARDER_FOLDER},
397        check_return=True)
398    self._initialized_devices.add(device_serial)
399
400  @staticmethod
401  def KillHost():
402    """Kills the forwarder process running on the host."""
403    with _FileLock(Forwarder._LOCK_PATH):
404      Forwarder._GetInstanceLocked(None)._KillHostLocked()
405
406  def _KillHostLocked(self):
407    """Kills the forwarder process running on the host.
408
409    Note that the global lock must be acquired before calling this method.
410    """
411    logger.info('Killing host_forwarder.')
412    try:
413      kill_cmd = [self._host_forwarder_path, '--kill-server']
414      (exit_code, _o) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
415          kill_cmd, Forwarder._TIMEOUT)
416      if exit_code != 0:
417        kill_cmd = ['pkill', '-9', 'host_forwarder']
418        (exit_code, output) = cmd_helper.GetCmdStatusAndOutputWithTimeout(
419            kill_cmd, Forwarder._TIMEOUT)
420        if exit_code != 0:
421          raise HostForwarderError(
422              '%s exited with %d:\n%s' % (
423                  self._host_forwarder_path,
424                  exit_code,
425                  '\n'.join(output) if isinstance(output, list) else output))
426    except cmd_helper.TimeoutError as e:
427      raise HostForwarderError(
428          '`%s` timed out:\n%s' % (' '.join(kill_cmd), e.output))
429
430  @staticmethod
431  def KillDevice(device, tool=None):
432    """Kills the forwarder process running on the device.
433
434    Args:
435      device: Instance of DeviceUtils for talking to the device.
436      tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
437            forwarder (see valgrind_tools.py).
438    """
439    with _FileLock(Forwarder._LOCK_PATH):
440      Forwarder._GetInstanceLocked(None)._KillDeviceLocked(
441          device, tool or base_tool.BaseTool())
442
443  def _KillDeviceLocked(self, device, tool):
444    """Kills the forwarder process running on the device.
445
446    Note that the global lock must be acquired before calling this method.
447
448    Args:
449      device: Instance of DeviceUtils for talking to the device.
450      tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
451            forwarder (see valgrind_tools.py).
452    """
453    logger.info('Killing device_forwarder.')
454    self._initialized_devices.discard(device.serial)
455    if not device.FileExists(Forwarder._DEVICE_FORWARDER_PATH):
456      return
457
458    cmd = [Forwarder._DEVICE_FORWARDER_PATH, '--kill-server']
459    wrapper = tool.GetUtilWrapper()
460    if wrapper:
461      cmd.insert(0, wrapper)
462    device.RunShellCommand(
463        cmd, env={'LD_LIBRARY_PATH': Forwarder._DEVICE_FORWARDER_FOLDER},
464        check_return=True)
465