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