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