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