Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(906)

Side by Side Diff: build/android/pylib/forwarder.py

Issue 20824008: Reland r212020: Move Python setup/tear down logic into Forwarder ... (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Close unused FDs Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 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 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 import fcntl
5 import logging 6 import logging
6 import os 7 import os
8 import psutil
7 import re 9 import re
8 import sys 10 import sys
9 import threading
10 import time 11 import time
11 12
12 import android_commands 13 import android_commands
13 import cmd_helper 14 import cmd_helper
14 import constants 15 import constants
15 16
16 from pylib import pexpect 17 from pylib import valgrind_tools
17 18
18 19
19 def _MakeBinaryPath(build_type, binary_name): 20 def _MakeBinaryPath(build_type, binary_name):
20 return os.path.join(cmd_helper.OutDirectory.get(), build_type, binary_name) 21 return os.path.join(cmd_helper.OutDirectory.get(), build_type, binary_name)
21 22
22 23
24 def _GetProcessStartTime(pid):
25 return psutil.Process(pid).create_time
26
27
28 class _FileLock(object):
29 """With statement-aware implementation of a file lock.
30
31 File locks are needed for cross-process synchronization when the
32 multiprocessing Python module is used.
33 """
34 def __init__(self, path):
35 self._path = path
36
37 def __enter__(self):
38 self._fd = os.open(self._path, os.O_RDONLY | os.O_CREAT)
39 if self._fd < 0:
40 raise Exception('Could not open file %s for reading' % self._path)
41 fcntl.flock(self._fd, fcntl.LOCK_EX)
42
43 def __exit__(self, type, value, traceback):
44 fcntl.flock(self._fd, fcntl.LOCK_UN)
45 os.close(self._fd)
46
47
23 class Forwarder(object): 48 class Forwarder(object):
24 """Thread-safe class to manage port forwards from the device to the host.""" 49 """Thread-safe class to manage port forwards from the device to the host."""
25 50
26 _DEVICE_FORWARDER_FOLDER = (constants.TEST_EXECUTABLE_DIR + 51 _DEVICE_FORWARDER_FOLDER = (constants.TEST_EXECUTABLE_DIR +
27 '/forwarder/') 52 '/forwarder/')
28 _DEVICE_FORWARDER_PATH = (constants.TEST_EXECUTABLE_DIR + 53 _DEVICE_FORWARDER_PATH = (constants.TEST_EXECUTABLE_DIR +
29 '/forwarder/device_forwarder') 54 '/forwarder/device_forwarder')
30 _LD_LIBRARY_PATH = 'LD_LIBRARY_PATH=%s' % _DEVICE_FORWARDER_FOLDER 55 _LD_LIBRARY_PATH = 'LD_LIBRARY_PATH=%s' % _DEVICE_FORWARDER_FOLDER
56 _LOCK_PATH = '/tmp/chrome.forwarder.lock'
57 _MULTIPROCESSING_ENV_VAR = 'CHROME_FORWARDER_USE_MULTIPROCESSING'
31 58
32 def __init__(self, adb, build_type): 59 _instance = None
33 """Forwards TCP ports on the device back to the host.
34 60
35 Works like adb forward, but in reverse. 61 @staticmethod
62 def UseMultiprocessing():
63 """Tells the forwarder that multiprocessing is used."""
64 os.environ[Forwarder._MULTIPROCESSING_ENV_VAR] = '1'
36 65
37 Args: 66 @staticmethod
38 adb: Instance of AndroidCommands for talking to the device. 67 def Map(port_pairs, adb, build_type='Debug', tool=None):
39 build_type: 'Release' or 'Debug'.
40 """
41 assert build_type in ('Release', 'Debug')
42 self._adb = adb
43 self._device_to_host_port_map = dict()
44 self._host_to_device_port_map = dict()
45 self._device_initialized = False
46 self._host_adb_control_port = 0
47 self._lock = threading.Lock()
48 self._host_forwarder_path = _MakeBinaryPath(build_type, 'host_forwarder')
49 self._device_forwarder_path_on_host = os.path.join(
50 cmd_helper.OutDirectory.get(), build_type, 'forwarder_dist')
51
52 def Run(self, port_pairs, tool):
53 """Runs the forwarder. 68 """Runs the forwarder.
54 69
55 Args: 70 Args:
56 port_pairs: A list of tuples (device_port, host_port) to forward. Note 71 port_pairs: A list of tuples (device_port, host_port) to forward. Note
57 that you can specify 0 as a device_port, in which case a 72 that you can specify 0 as a device_port, in which case a
58 port will by dynamically assigned on the device. You can 73 port will by dynamically assigned on the device. You can
59 get the number of the assigned port using the 74 get the number of the assigned port using the
60 DevicePortForHostPort method. 75 DevicePortForHostPort method.
76 adb: An AndroidCommands instance.
61 tool: Tool class to use to get wrapper, if necessary, for executing the 77 tool: Tool class to use to get wrapper, if necessary, for executing the
62 forwarder (see valgrind_tools.py). 78 forwarder (see valgrind_tools.py).
63 79
64 Raises: 80 Raises:
65 Exception on failure to forward the port. 81 Exception on failure to forward the port.
66 """ 82 """
67 with self._lock: 83 if not tool:
68 self._InitDeviceLocked(tool) 84 tool = valgrind_tools.CreateTool(None, adb)
69 host_name = '127.0.0.1' 85 with _FileLock(Forwarder._LOCK_PATH):
86 instance = Forwarder._GetInstanceLocked(build_type, tool)
87 instance._InitDeviceLocked(adb, tool)
88
89 device_serial = adb.Adb().GetSerialNumber()
70 redirection_commands = [ 90 redirection_commands = [
71 ['--serial-id=' + self._adb.Adb().GetSerialNumber(), '--map', 91 ['--serial-id=' + device_serial, '--map', str(device),
72 str(device), str(host)] for device, host in port_pairs] 92 str(host)] for device, host in port_pairs]
73 logging.info('Forwarding using commands: %s', redirection_commands) 93 logging.info('Forwarding using commands: %s', redirection_commands)
74 94
75 for redirection_command in redirection_commands: 95 for redirection_command in redirection_commands:
76 try: 96 try:
77 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( 97 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
78 [self._host_forwarder_path] + redirection_command) 98 [instance._host_forwarder_path] + redirection_command)
79 except OSError as e: 99 except OSError as e:
80 if e.errno == 2: 100 if e.errno == 2:
81 raise Exception('Unable to start host forwarder. Make sure you have' 101 raise Exception('Unable to start host forwarder. Make sure you have'
82 ' built host_forwarder.') 102 ' built host_forwarder.')
83 else: raise 103 else: raise
84 if exit_code != 0: 104 if exit_code != 0:
85 raise Exception('%s exited with %d:\n%s' % ( 105 raise Exception('%s exited with %d:\n%s' % (
86 self._host_forwarder_path, exit_code, '\n'.join(output))) 106 instance._host_forwarder_path, exit_code, '\n'.join(output)))
87 tokens = output.split(':') 107 tokens = output.split(':')
88 if len(tokens) != 2: 108 if len(tokens) != 2:
89 raise Exception(('Unexpected host forwarder output "%s", ' + 109 raise Exception(('Unexpected host forwarder output "%s", ' +
90 'expected "device_port:host_port"') % output) 110 'expected "device_port:host_port"') % output)
91 device_port = int(tokens[0]) 111 device_port = int(tokens[0])
92 host_port = int(tokens[1]) 112 host_port = int(tokens[1])
93 self._device_to_host_port_map[device_port] = host_port 113 serial_with_port = (device_serial, device_port)
94 self._host_to_device_port_map[host_port] = device_port 114 instance._device_to_host_port_map[serial_with_port] = host_port
115 instance._host_to_device_port_map[host_port] = serial_with_port
95 logging.info('Forwarding device port: %d to host port: %d.', 116 logging.info('Forwarding device port: %d to host port: %d.',
96 device_port, host_port) 117 device_port, host_port)
97 118
98 def _InitDeviceLocked(self, tool): 119 @staticmethod
99 """Initializes the device forwarder process (only once).""" 120 def UnmapDevicePort(device_port, adb):
100 if self._device_initialized: 121 """Unmaps a previously forwarded device port.
122
123 Args:
124 adb: An AndroidCommands instance.
125 device_port: A previously forwarded port (through Map()).
126 """
127 with _FileLock(Forwarder._LOCK_PATH):
128 Forwarder._UnmapDevicePortLocked(device_port, adb)
129
130 @staticmethod
131 def UnmapAllDevicePorts(adb):
132 """Unmaps all the previously forwarded ports for the provided device.
133
134 Args:
135 adb: An AndroidCommands instance.
136 port_pairs: A list of tuples (device_port, host_port) to unmap.
137 """
138 with _FileLock(Forwarder._LOCK_PATH):
139 port_map = Forwarder._GetInstanceLocked(
140 None, None)._device_to_host_port_map
141 adb_serial = adb.Adb().GetSerialNumber()
142 for ((device_serial, device_port), _) in port_map:
143 if adb_serial == device_serial:
144 Forwarder._UnmapDevicePortLocked(device_port, adb)
145
146 @staticmethod
147 def DevicePortForHostPort(host_port):
148 """Returns the device port that corresponds to a given host port."""
149 with _FileLock(Forwarder._LOCK_PATH):
150 (device_serial, device_port) = Forwarder._GetInstanceLocked(
151 None, None)._host_to_device_port_map.get(host_port)
152 return device_port
153
154 @staticmethod
155 def _GetInstanceLocked(build_type, tool):
156 """Returns the singleton instance.
157
158 Note that the global lock must be acquired before calling this method.
159
160 Args:
161 build_type: 'Release' or 'Debug'
162 tool: Tool class to use to get wrapper, if necessary, for executing the
163 forwarder (see valgrind_tools.py).
164 """
165 if not Forwarder._instance:
166 Forwarder._instance = Forwarder(build_type, tool)
167 return Forwarder._instance
168
169 def __init__(self, build_type, tool):
170 """Constructs a new instance of Forwarder.
171
172 Note that Forwarder is a singleton therefore this constructor should be
173 called only once.
174
175 Args:
176 build_type: 'Release' or 'Debug'
177 tool: Tool class to use to get wrapper, if necessary, for executing the
178 forwarder (see valgrind_tools.py).
179 """
180 assert not Forwarder._instance
181 self._build_type = build_type
182 self._tool = tool
183 self._initialized_devices = set()
184 self._device_to_host_port_map = dict()
185 self._host_to_device_port_map = dict()
186 self._host_forwarder_path = _MakeBinaryPath(
187 self._build_type, 'host_forwarder')
188 if not os.path.exists(self._host_forwarder_path):
189 self._build_type = 'Release' if self._build_type == 'Debug' else 'Debug'
190 self._host_forwarder_path = _MakeBinaryPath(
191 self._build_type, 'host_forwarder')
192 assert os.path.exists(
193 self._host_forwarder_path), 'Please build forwarder2'
194 self._device_forwarder_path_on_host = os.path.join(
195 cmd_helper.OutDirectory.get(), self._build_type, 'forwarder_dist')
196 self._InitHostLocked()
197
198 @staticmethod
199 def _UnmapDevicePortLocked(device_port, adb):
200 """Internal method used by UnmapDevicePort().
201
202 Note that the global lock must be acquired before calling this method.
203 """
204 instance = Forwarder._GetInstanceLocked(None, None)
205 serial = adb.Adb().GetSerialNumber()
206 serial_with_port = (serial, device_port)
207 if not serial_with_port in instance._device_to_host_port_map:
208 logging.error('Trying to unmap non-forwarded port %d' % device_port)
101 return 209 return
102 self._adb.PushIfNeeded( 210 redirection_command = ['--serial-id=' + serial, '--unmap', str(device_port)]
211 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
212 [instance._host_forwarder_path] + redirection_command)
213 if exit_code != 0:
214 logging.error('%s exited with %d:\n%s' % (
215 instance._host_forwarder_path, exit_code, '\n'.join(output)))
216 host_port = instance._device_to_host_port_map[serial_with_port]
217 del instance._device_to_host_port_map[serial_with_port]
218 del instance._host_to_device_port_map[host_port]
219
220 @staticmethod
221 def _GetPidForLock():
222 """Returns the PID used for host_forwarder initialization.
223
224 In case multi-process sharding is used, the PID of the "sharder" is used.
225 The "sharder" is the initial process that forks that is the parent process.
226 By default, multi-processing is not used. In that case the PID of the
227 current process is returned.
228 """
229 use_multiprocessing = Forwarder._MULTIPROCESSING_ENV_VAR in os.environ
230 return os.getppid() if use_multiprocessing else os.getpid()
231
232 def _InitHostLocked(self):
233 """Initializes the host forwarder daemon.
234
235 Note that the global lock must be acquired before calling this method. This
236 method kills any existing host_forwarder process that could be stale.
237 """
238 # See if the host_forwarder daemon was already initialized by a concurrent
239 # process or thread (in case multi-process sharding is not used).
240 pid_for_lock = Forwarder._GetPidForLock()
241 fd = os.open(Forwarder._LOCK_PATH, os.O_RDWR | os.O_CREAT)
242 with os.fdopen(fd, 'r+') as pid_file:
243 pid_with_start_time = pid_file.readline()
244 if pid_with_start_time:
245 (pid, process_start_time) = pid_with_start_time.split(':')
246 if pid == str(pid_for_lock):
247 if process_start_time == str(_GetProcessStartTime(pid_for_lock)):
248 return
249 self._KillHostLocked()
250 pid_file.seek(0)
251 pid_file.write(
252 '%s:%s' % (pid_for_lock, str(_GetProcessStartTime(pid_for_lock))))
253
254 def _InitDeviceLocked(self, adb, tool):
255 """Initializes the device_forwarder daemon for a specific device (once).
256
257 Note that the global lock must be acquired before calling this method. This
258 method kills any existing device_forwarder daemon on the device that could
259 be stale, pushes the latest version of the daemon (to the device) and starts
260 it.
261
262 Args:
263 adb: An AndroidCommands instance.
264 tool: Tool class to use to get wrapper, if necessary, for executing the
265 forwarder (see valgrind_tools.py).
266 """
267 device_serial = adb.Adb().GetSerialNumber()
268 if device_serial in self._initialized_devices:
269 return
270 Forwarder._KillDeviceLocked(adb, tool)
271 adb.PushIfNeeded(
103 self._device_forwarder_path_on_host, 272 self._device_forwarder_path_on_host,
104 Forwarder._DEVICE_FORWARDER_FOLDER) 273 Forwarder._DEVICE_FORWARDER_FOLDER)
105 (exit_code, output) = self._adb.GetShellCommandStatusAndOutput( 274 (exit_code, output) = adb.GetShellCommandStatusAndOutput(
106 '%s %s %s' % (Forwarder._LD_LIBRARY_PATH, tool.GetUtilWrapper(), 275 '%s %s %s' % (Forwarder._LD_LIBRARY_PATH, tool.GetUtilWrapper(),
107 Forwarder._DEVICE_FORWARDER_PATH)) 276 Forwarder._DEVICE_FORWARDER_PATH))
108 if exit_code != 0: 277 if exit_code != 0:
109 raise Exception( 278 raise Exception(
110 'Failed to start device forwarder:\n%s' % '\n'.join(output)) 279 'Failed to start device forwarder:\n%s' % '\n'.join(output))
111 self._device_initialized = True 280 self._initialized_devices.add(device_serial)
112 281
113 def UnmapDevicePort(self, device_port): 282 def _KillHostLocked(self):
114 """Unmaps a previously forwarded device port. 283 """Kills the forwarder process running on the host.
115 284
116 Args: 285 Note that the global lock must be acquired before calling this method.
117 device_port: A previously forwarded port (through Run()). 286 """
118 """ 287 logging.info('Killing host_forwarder.')
119 with self._lock:
120 self._UnmapDevicePortInternalLocked(device_port)
121
122 def _UnmapDevicePortInternalLocked(self, device_port):
123 if not device_port in self._device_to_host_port_map:
124 return
125 redirection_command = [
126 '--serial-id=' + self._adb.Adb().GetSerialNumber(), '--unmap',
127 str(device_port)]
128 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( 288 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
129 [self._host_forwarder_path] + redirection_command) 289 [self._host_forwarder_path, '--kill-server'])
130 if exit_code != 0:
131 logging.error('%s exited with %d:\n%s' % (
132 self._host_forwarder_path, exit_code, '\n'.join(output)))
133 host_port = self._device_to_host_port_map[device_port]
134 del self._device_to_host_port_map[device_port]
135 del self._host_to_device_port_map[host_port]
136
137 @staticmethod
138 def KillHost(build_type='Debug'):
139 """Kills the forwarder process running on the host.
140
141 Args:
142 build_type: 'Release' or 'Debug' (default='Debug')
143 """
144 logging.info('Killing host_forwarder.')
145 host_forwarder_path = _MakeBinaryPath(build_type, 'host_forwarder')
146 if not os.path.exists(host_forwarder_path):
147 host_forwarder_path = _MakeBinaryPath(
148 'Release' if build_type == 'Debug' else 'Debug', 'host_forwarder')
149 assert os.path.exists(host_forwarder_path), 'Please build forwarder2'
150 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
151 [host_forwarder_path, '--kill-server'])
152 if exit_code != 0: 290 if exit_code != 0:
153 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( 291 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
154 ['pkill', 'host_forwarder']) 292 ['pkill', '-9', 'host_forwarder'])
155 if exit_code != 0: 293 if exit_code != 0:
156 raise Exception('%s exited with %d:\n%s' % ( 294 raise Exception('%s exited with %d:\n%s' % (
157 host_forwarder_path, exit_code, '\n'.join(output))) 295 self._host_forwarder_path, exit_code, '\n'.join(output)))
158 296
159 @staticmethod 297 @staticmethod
160 def KillDevice(adb, tool): 298 def _KillDeviceLocked(adb, tool):
161 """Kills the forwarder process running on the device. 299 """Kills the forwarder process running on the device.
162 300
301 Note that the global lock must be acquired before calling this method.
302
163 Args: 303 Args:
164 adb: Instance of AndroidCommands for talking to the device. 304 adb: Instance of AndroidCommands for talking to the device.
165 tool: Wrapper tool (e.g. valgrind) that can be used to execute the device 305 tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
166 forwarder (see valgrind_tools.py). 306 forwarder (see valgrind_tools.py).
167 """ 307 """
168 logging.info('Killing device_forwarder.') 308 logging.info('Killing device_forwarder.')
169 if not adb.FileExistsOnDevice(Forwarder._DEVICE_FORWARDER_PATH): 309 if not adb.FileExistsOnDevice(Forwarder._DEVICE_FORWARDER_PATH):
170 return 310 return
171 (exit_code, output) = adb.GetShellCommandStatusAndOutput( 311 (exit_code, output) = adb.GetShellCommandStatusAndOutput(
172 '%s %s --kill-server' % (tool.GetUtilWrapper(), 312 '%s %s --kill-server' % (tool.GetUtilWrapper(),
173 Forwarder._DEVICE_FORWARDER_PATH)) 313 Forwarder._DEVICE_FORWARDER_PATH))
174 # TODO(pliard): Remove the following call to KillAllBlocking() when we are 314 # TODO(pliard): Remove the following call to KillAllBlocking() when we are
175 # sure that the old version of device_forwarder (not supporting 315 # sure that the old version of device_forwarder (not supporting
176 # 'kill-server') is not running on the bots anymore. 316 # 'kill-server') is not running on the bots anymore.
177 timeout_sec = 5 317 timeout_sec = 5
178 processes_killed = adb.KillAllBlocking('device_forwarder', timeout_sec) 318 processes_killed = adb.KillAllBlocking('device_forwarder', timeout_sec)
179 if not processes_killed: 319 if not processes_killed:
180 pids = adb.ExtractPid('device_forwarder') 320 pids = adb.ExtractPid('device_forwarder')
181 if pids: 321 if pids:
182 raise Exception('Timed out while killing device_forwarder') 322 raise Exception('Timed out while killing device_forwarder')
183
184 def DevicePortForHostPort(self, host_port):
185 """Returns the device port that corresponds to a given host port."""
186 with self._lock:
187 return self._host_to_device_port_map.get(host_port)
188
189 def Close(self):
190 """Releases the previously forwarded ports."""
191 with self._lock:
192 for device_port in self._device_to_host_port_map.copy():
193 self._UnmapDevicePortInternalLocked(device_port)
OLDNEW
« no previous file with comments | « build/android/pylib/chrome_test_server_spawner.py ('k') | build/android/pylib/host_driven/python_test_sharder.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698