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

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

Issue 18086004: Move Python setup/tear down logic into Forwarder itself. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Update outdated comment Created 7 years, 5 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:
frankf 2013/07/16 18:31:02 Perhaps UnmapDevicePort should also do this check.
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:
101 return 208 return
102 self._adb.PushIfNeeded( 209 redirection_command = ['--serial-id=' + serial, '--unmap', str(device_port)]
210 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
211 [instance._host_forwarder_path] + redirection_command)
212 if exit_code != 0:
213 logging.error('%s exited with %d:\n%s' % (
214 instance._host_forwarder_path, exit_code, '\n'.join(output)))
215 host_port = instance._device_to_host_port_map[serial_with_port]
216 del instance._device_to_host_port_map[serial_with_port]
217 del instance._host_to_device_port_map[host_port]
218
219 @staticmethod
220 def _GetPidForLock():
221 """Returns the PID used for host_forwarder initialization.
222
223 In case multi-process sharding is used, the PID of the "sharder" is used.
224 The "sharder" is the initial process that forks that is the parent process.
225 By default, multi-processing is not used. In that case the PID of the
226 current process is returned.
227 """
228 use_multiprocessing = Forwarder._MULTIPROCESSING_ENV_VAR in os.environ
229 return os.getppid() if use_multiprocessing else os.getpid()
230
231 def _InitHostLocked(self):
232 """Initializes the host forwarder daemon.
233
234 Note that the global lock must be acquired before calling this method. This
235 method kills any existing host_forwarder process that could be stale.
236 """
237 # See if the host_forwarder daemon was already initialized by a concurrent
238 # process or thread (in case multi-process sharding is not used).
239 pid_for_lock = Forwarder._GetPidForLock()
240 fd = os.open(Forwarder._LOCK_PATH, os.O_RDWR | os.O_CREAT)
241 with os.fdopen(fd, 'r+') as pid_file:
242 pid_with_start_time = pid_file.readline()
243 if pid_with_start_time:
244 (pid, process_start_time) = pid_with_start_time.split(':')
245 if pid == str(pid_for_lock):
246 if process_start_time == str(_GetProcessStartTime(pid_for_lock)):
247 return
248 self._KillHostLocked()
249 pid_file.seek(0)
250 pid_file.write(
251 '%s:%s\n' % (pid_for_lock, str(_GetProcessStartTime(pid_for_lock))))
frankf 2013/07/16 18:31:02 Make sure you don't get newline as part of process
252
253 def _InitDeviceLocked(self, adb, tool):
254 """Initializes the device_forwarder daemon for a specific device (once).
255
256 Note that the global lock must be acquired before calling this method. This
257 method kills any existing device_forwarder daemon on the device that could
258 be stale, pushes the latest version of the daemon (to the device) and starts
259 it.
260
261 Args:
262 adb: An AndroidCommands instance.
263 tool: Tool class to use to get wrapper, if necessary, for executing the
264 forwarder (see valgrind_tools.py).
265 """
266 device_serial = adb.Adb().GetSerialNumber()
267 if device_serial in self._initialized_devices:
268 return
269 Forwarder._KillDeviceLocked(adb, tool)
270 adb.PushIfNeeded(
103 self._device_forwarder_path_on_host, 271 self._device_forwarder_path_on_host,
104 Forwarder._DEVICE_FORWARDER_FOLDER) 272 Forwarder._DEVICE_FORWARDER_FOLDER)
105 (exit_code, output) = self._adb.GetShellCommandStatusAndOutput( 273 (exit_code, output) = adb.GetShellCommandStatusAndOutput(
106 '%s %s %s' % (Forwarder._LD_LIBRARY_PATH, tool.GetUtilWrapper(), 274 '%s %s %s' % (Forwarder._LD_LIBRARY_PATH, tool.GetUtilWrapper(),
107 Forwarder._DEVICE_FORWARDER_PATH)) 275 Forwarder._DEVICE_FORWARDER_PATH))
108 if exit_code != 0: 276 if exit_code != 0:
109 raise Exception( 277 raise Exception(
110 'Failed to start device forwarder:\n%s' % '\n'.join(output)) 278 'Failed to start device forwarder:\n%s' % '\n'.join(output))
111 self._device_initialized = True 279 self._initialized_devices.add(device_serial)
112 280
113 def UnmapDevicePort(self, device_port): 281 def _KillHostLocked(self):
114 """Unmaps a previously forwarded device port. 282 """Kills the forwarder process running on the host.
115 283
116 Args: 284 Note that the global lock must be acquired before calling this method.
117 device_port: A previously forwarded port (through Run()). 285 """
118 """ 286 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( 287 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
129 [self._host_forwarder_path] + redirection_command) 288 [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: 289 if exit_code != 0:
153 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( 290 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
154 ['pkill', 'host_forwarder']) 291 ['pkill', '-9', 'host_forwarder'])
155 if exit_code != 0: 292 if exit_code != 0:
156 raise Exception('%s exited with %d:\n%s' % ( 293 raise Exception('%s exited with %d:\n%s' % (
157 host_forwarder_path, exit_code, '\n'.join(output))) 294 self._host_forwarder_path, exit_code, '\n'.join(output)))
158 295
159 @staticmethod 296 @staticmethod
160 def KillDevice(adb, tool): 297 def _KillDeviceLocked(adb, tool):
161 """Kills the forwarder process running on the device. 298 """Kills the forwarder process running on the device.
162 299
300 Note that the global lock must be acquired before calling this method.
301
163 Args: 302 Args:
164 adb: Instance of AndroidCommands for talking to the device. 303 adb: Instance of AndroidCommands for talking to the device.
165 tool: Wrapper tool (e.g. valgrind) that can be used to execute the device 304 tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
166 forwarder (see valgrind_tools.py). 305 forwarder (see valgrind_tools.py).
167 """ 306 """
168 logging.info('Killing device_forwarder.') 307 logging.info('Killing device_forwarder.')
169 if not adb.FileExistsOnDevice(Forwarder._DEVICE_FORWARDER_PATH): 308 if not adb.FileExistsOnDevice(Forwarder._DEVICE_FORWARDER_PATH):
170 return 309 return
171 (exit_code, output) = adb.GetShellCommandStatusAndOutput( 310 (exit_code, output) = adb.GetShellCommandStatusAndOutput(
172 '%s %s --kill-server' % (tool.GetUtilWrapper(), 311 '%s %s --kill-server' % (tool.GetUtilWrapper(),
173 Forwarder._DEVICE_FORWARDER_PATH)) 312 Forwarder._DEVICE_FORWARDER_PATH))
174 # TODO(pliard): Remove the following call to KillAllBlocking() when we are 313 # TODO(pliard): Remove the following call to KillAllBlocking() when we are
175 # sure that the old version of device_forwarder (not supporting 314 # sure that the old version of device_forwarder (not supporting
176 # 'kill-server') is not running on the bots anymore. 315 # 'kill-server') is not running on the bots anymore.
177 timeout_sec = 5 316 timeout_sec = 5
178 processes_killed = adb.KillAllBlocking('device_forwarder', timeout_sec) 317 processes_killed = adb.KillAllBlocking('device_forwarder', timeout_sec)
179 if not processes_killed: 318 if not processes_killed:
180 pids = adb.ExtractPid('device_forwarder') 319 pids = adb.ExtractPid('device_forwarder')
181 if pids: 320 if pids:
182 raise Exception('Timed out while killing device_forwarder') 321 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/instrumentation/test_runner.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698