Revert of Remove OneClickSigninHelper since it is no longer used. (patchset #5 id...
[chromium-blink-merge.git] / build / android / pylib / forwarder.py
blob385966912f1a766a89857377305c80c610e44ea9
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 pylib import cmd_helper
13 from pylib import constants
14 from pylib import valgrind_tools
16 # TODO(jbudorick) Remove once telemetry gets switched over.
17 import pylib.android_commands
18 import pylib.device.device_utils
21 def _GetProcessStartTime(pid):
22 return psutil.Process(pid).create_time
25 class _FileLock(object):
26 """With statement-aware implementation of a file lock.
28 File locks are needed for cross-process synchronization when the
29 multiprocessing Python module is used.
30 """
31 def __init__(self, path):
32 self._fd = -1
33 self._path = path
35 def __enter__(self):
36 self._fd = os.open(self._path, os.O_RDONLY | os.O_CREAT)
37 if self._fd < 0:
38 raise Exception('Could not open file %s for reading' % self._path)
39 fcntl.flock(self._fd, fcntl.LOCK_EX)
41 def __exit__(self, _exception_type, _exception_value, traceback):
42 fcntl.flock(self._fd, fcntl.LOCK_UN)
43 os.close(self._fd)
46 class Forwarder(object):
47 """Thread-safe class to manage port forwards from the device to the host."""
49 _DEVICE_FORWARDER_FOLDER = (constants.TEST_EXECUTABLE_DIR +
50 '/forwarder/')
51 _DEVICE_FORWARDER_PATH = (constants.TEST_EXECUTABLE_DIR +
52 '/forwarder/device_forwarder')
53 _LOCK_PATH = '/tmp/chrome.forwarder.lock'
54 _MULTIPROCESSING_ENV_VAR = 'CHROME_FORWARDER_USE_MULTIPROCESSING'
55 # Defined in host_forwarder_main.cc
56 _HOST_FORWARDER_LOG = '/tmp/host_forwarder_log'
58 _instance = None
60 @staticmethod
61 def UseMultiprocessing():
62 """Tells the forwarder that multiprocessing is used."""
63 os.environ[Forwarder._MULTIPROCESSING_ENV_VAR] = '1'
65 @staticmethod
66 def Map(port_pairs, device, tool=None):
67 """Runs the forwarder.
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).
79 Raises:
80 Exception on failure to forward the port.
81 """
82 # TODO(jbudorick) Remove once telemetry gets switched over.
83 if isinstance(device, pylib.android_commands.AndroidCommands):
84 device = pylib.device.device_utils.DeviceUtils(device)
85 if not tool:
86 tool = valgrind_tools.CreateTool(None, device)
87 with _FileLock(Forwarder._LOCK_PATH):
88 instance = Forwarder._GetInstanceLocked(tool)
89 instance._InitDeviceLocked(device, tool)
91 device_serial = str(device)
92 redirection_commands = [
93 ['--adb=' + constants.GetAdbPath(),
94 '--serial-id=' + device_serial,
95 '--map', str(device_port), str(host_port)]
96 for device_port, host_port in port_pairs]
97 logging.info('Forwarding using commands: %s', redirection_commands)
99 for redirection_command in redirection_commands:
100 try:
101 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
102 [instance._host_forwarder_path] + redirection_command)
103 except OSError as e:
104 if e.errno == 2:
105 raise Exception('Unable to start host forwarder. Make sure you have'
106 ' built host_forwarder.')
107 else: raise
108 if exit_code != 0:
109 Forwarder._KillDeviceLocked(device, tool)
110 raise Exception('%s exited with %d:\n%s' % (
111 instance._host_forwarder_path, exit_code, '\n'.join(output)))
112 tokens = output.split(':')
113 if len(tokens) != 2:
114 raise Exception('Unexpected host forwarder output "%s", '
115 'expected "device_port:host_port"' % output)
116 device_port = int(tokens[0])
117 host_port = int(tokens[1])
118 serial_with_port = (device_serial, device_port)
119 instance._device_to_host_port_map[serial_with_port] = host_port
120 instance._host_to_device_port_map[host_port] = serial_with_port
121 logging.info('Forwarding device port: %d to host port: %d.',
122 device_port, host_port)
124 @staticmethod
125 def UnmapDevicePort(device_port, device):
126 """Unmaps a previously forwarded device port.
128 Args:
129 device: A DeviceUtils instance.
130 device_port: A previously forwarded port (through Map()).
132 # TODO(jbudorick) Remove once telemetry gets switched over.
133 if isinstance(device, pylib.android_commands.AndroidCommands):
134 device = pylib.device.device_utils.DeviceUtils(device)
135 with _FileLock(Forwarder._LOCK_PATH):
136 Forwarder._UnmapDevicePortLocked(device_port, device)
138 @staticmethod
139 def UnmapAllDevicePorts(device):
140 """Unmaps all the previously forwarded ports for the provided device.
142 Args:
143 device: A DeviceUtils instance.
144 port_pairs: A list of tuples (device_port, host_port) to unmap.
146 # TODO(jbudorick) Remove once telemetry gets switched over.
147 if isinstance(device, pylib.android_commands.AndroidCommands):
148 device = pylib.device.device_utils.DeviceUtils(device)
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 = valgrind_tools.CreateTool(None, device)
162 Forwarder._KillDeviceLocked(device, tool)
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_serial, device_port) = Forwarder._GetInstanceLocked(
169 None)._host_to_device_port_map.get(host_port)
170 return device_port
172 @staticmethod
173 def RemoveHostLog():
174 if os.path.exists(Forwarder._HOST_FORWARDER_LOG):
175 os.unlink(Forwarder._HOST_FORWARDER_LOG)
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()
184 @staticmethod
185 def _GetInstanceLocked(tool):
186 """Returns the singleton instance.
188 Note that the global lock must be acquired before calling this method.
190 Args:
191 tool: Tool class to use to get wrapper, if necessary, for executing the
192 forwarder (see valgrind_tools.py).
194 if not Forwarder._instance:
195 Forwarder._instance = Forwarder(tool)
196 return Forwarder._instance
198 def __init__(self, tool):
199 """Constructs a new instance of Forwarder.
201 Note that Forwarder is a singleton therefore this constructor should be
202 called only once.
204 Args:
205 tool: Tool class to use to get wrapper, if necessary, for executing the
206 forwarder (see valgrind_tools.py).
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 = os.path.join(
214 constants.GetOutDirectory(), 'host_forwarder')
215 assert os.path.exists(self._host_forwarder_path), 'Please build forwarder2'
216 self._device_forwarder_path_on_host = os.path.join(
217 constants.GetOutDirectory(), 'forwarder_dist')
218 self._InitHostLocked()
220 @staticmethod
221 def _UnmapDevicePortLocked(device_port, device):
222 """Internal method used by UnmapDevicePort().
224 Note that the global lock must be acquired before calling this method.
226 instance = Forwarder._GetInstanceLocked(None)
227 serial = str(device)
228 serial_with_port = (serial, device_port)
229 if not serial_with_port in instance._device_to_host_port_map:
230 logging.error('Trying to unmap non-forwarded port %d' % device_port)
231 return
232 redirection_command = ['--adb=' + constants.GetAdbPath(),
233 '--serial-id=' + serial,
234 '--unmap', str(device_port)]
235 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
236 [instance._host_forwarder_path] + redirection_command)
237 if exit_code != 0:
238 logging.error('%s exited with %d:\n%s' % (
239 instance._host_forwarder_path, exit_code, '\n'.join(output)))
240 host_port = instance._device_to_host_port_map[serial_with_port]
241 del instance._device_to_host_port_map[serial_with_port]
242 del instance._host_to_device_port_map[host_port]
244 @staticmethod
245 def _GetPidForLock():
246 """Returns the PID used for host_forwarder initialization.
248 In case multi-process sharding is used, the PID of the "sharder" is used.
249 The "sharder" is the initial process that forks that is the parent process.
250 By default, multi-processing is not used. In that case the PID of the
251 current process is returned.
253 use_multiprocessing = Forwarder._MULTIPROCESSING_ENV_VAR in os.environ
254 return os.getpgrp() if use_multiprocessing else os.getpid()
256 def _InitHostLocked(self):
257 """Initializes the host forwarder daemon.
259 Note that the global lock must be acquired before calling this method. This
260 method kills any existing host_forwarder process that could be stale.
262 # See if the host_forwarder daemon was already initialized by a concurrent
263 # process or thread (in case multi-process sharding is not used).
264 pid_for_lock = Forwarder._GetPidForLock()
265 fd = os.open(Forwarder._LOCK_PATH, os.O_RDWR | os.O_CREAT)
266 with os.fdopen(fd, 'r+') as pid_file:
267 pid_with_start_time = pid_file.readline()
268 if pid_with_start_time:
269 (pid, process_start_time) = pid_with_start_time.split(':')
270 if pid == str(pid_for_lock):
271 if process_start_time == str(_GetProcessStartTime(pid_for_lock)):
272 return
273 self._KillHostLocked()
274 pid_file.seek(0)
275 pid_file.write(
276 '%s:%s' % (pid_for_lock, str(_GetProcessStartTime(pid_for_lock))))
277 pid_file.truncate()
279 def _InitDeviceLocked(self, device, tool):
280 """Initializes the device_forwarder daemon for a specific device (once).
282 Note that the global lock must be acquired before calling this method. This
283 method kills any existing device_forwarder daemon on the device that could
284 be stale, pushes the latest version of the daemon (to the device) and starts
287 Args:
288 device: A DeviceUtils instance.
289 tool: Tool class to use to get wrapper, if necessary, for executing the
290 forwarder (see valgrind_tools.py).
292 device_serial = str(device)
293 if device_serial in self._initialized_devices:
294 return
295 Forwarder._KillDeviceLocked(device, tool)
296 device.PushChangedFiles([(
297 self._device_forwarder_path_on_host,
298 Forwarder._DEVICE_FORWARDER_FOLDER)])
299 cmd = '%s %s' % (tool.GetUtilWrapper(), Forwarder._DEVICE_FORWARDER_PATH)
300 (exit_code, output) = device.old_interface.GetAndroidToolStatusAndOutput(
301 cmd, lib_path=Forwarder._DEVICE_FORWARDER_FOLDER)
302 if exit_code != 0:
303 raise Exception(
304 'Failed to start device forwarder:\n%s' % '\n'.join(output))
305 self._initialized_devices.add(device_serial)
307 def _KillHostLocked(self):
308 """Kills the forwarder process running on the host.
310 Note that the global lock must be acquired before calling this method.
312 logging.info('Killing host_forwarder.')
313 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
314 [self._host_forwarder_path, '--kill-server'])
315 if exit_code != 0:
316 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
317 ['pkill', '-9', 'host_forwarder'])
318 if exit_code != 0:
319 raise Exception('%s exited with %d:\n%s' % (
320 self._host_forwarder_path, exit_code, '\n'.join(output)))
322 @staticmethod
323 def _KillDeviceLocked(device, tool):
324 """Kills the forwarder process running on the device.
326 Note that the global lock must be acquired before calling this method.
328 Args:
329 device: Instance of DeviceUtils for talking to the device.
330 tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
331 forwarder (see valgrind_tools.py).
333 logging.info('Killing device_forwarder.')
334 Forwarder._instance._initialized_devices.discard(str(device))
335 if not device.FileExists(Forwarder._DEVICE_FORWARDER_PATH):
336 return
338 cmd = '%s %s --kill-server' % (tool.GetUtilWrapper(),
339 Forwarder._DEVICE_FORWARDER_PATH)
340 device.old_interface.GetAndroidToolStatusAndOutput(
341 cmd, lib_path=Forwarder._DEVICE_FORWARDER_FOLDER)