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

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: Implement Marcus' idea 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
7 import re 8 import re
8 import sys 9 import sys
9 import threading
10 import time 10 import time
11 11
12 import android_commands 12 import android_commands
13 import cmd_helper 13 import cmd_helper
14 import constants 14 import constants
15 15
16 from pylib import pexpect 16 from pylib import valgrind_tools
17 17
18 18
19 def _MakeBinaryPath(build_type, binary_name): 19 def _MakeBinaryPath(build_type, binary_name):
20 return os.path.join(cmd_helper.OutDirectory.get(), build_type, binary_name) 20 return os.path.join(cmd_helper.OutDirectory.get(), build_type, binary_name)
21 21
22 22
23 def _GetProcessStartTime(pid):
24 return open('/proc/%s/stat' % pid, 'r').readline().split(' ')[21]
frankf 2013/07/15 22:23:22 Can you use psutil: https://code.google.com/p/psut
Philippe 2013/07/16 12:31:21 Yes, thanks. I thought that this was a third-party
25
26
27 class _FileLock(object):
28 """With statement-aware implementation of a file lock.
29
30 File locks are needed for cross-process synchronization when the
31 multiprocessing Python module is used.
32 """
33 def __init__(self, path):
34 self._path = path
35
36 def __enter__(self):
37 self._file = open(self._path, 'a+')
38 fcntl.flock(self._file, fcntl.LOCK_EX)
39
40 def __exit__(self, type, value, traceback):
41 fcntl.flock(self._file, fcntl.LOCK_UN)
42 self._file.close()
43
44
23 class Forwarder(object): 45 class Forwarder(object):
24 """Thread-safe class to manage port forwards from the device to the host.""" 46 """Thread-safe class to manage port forwards from the device to the host."""
25 47
26 _DEVICE_FORWARDER_FOLDER = (constants.TEST_EXECUTABLE_DIR + 48 _DEVICE_FORWARDER_FOLDER = (constants.TEST_EXECUTABLE_DIR +
27 '/forwarder/') 49 '/forwarder/')
28 _DEVICE_FORWARDER_PATH = (constants.TEST_EXECUTABLE_DIR + 50 _DEVICE_FORWARDER_PATH = (constants.TEST_EXECUTABLE_DIR +
29 '/forwarder/device_forwarder') 51 '/forwarder/device_forwarder')
30 _LD_LIBRARY_PATH = 'LD_LIBRARY_PATH=%s' % _DEVICE_FORWARDER_FOLDER 52 _LD_LIBRARY_PATH = 'LD_LIBRARY_PATH=%s' % _DEVICE_FORWARDER_FOLDER
53 _LOCK_PATH = '/tmp/chrome.forwarder.lock'
54 _MULTIPROCESSING_ENV_VAR = 'CHROME_FORWARDER_USE_MULTIPROCESSING'
31 55
32 def __init__(self, adb, build_type): 56 _lock = _FileLock(_LOCK_PATH)
33 """Forwards TCP ports on the device back to the host. 57 _instance = None
34 58
35 Works like adb forward, but in reverse. 59 @staticmethod
60 def use_multiprocessing():
61 """Tells the forwarder that multiprocessing is used."""
62 os.environ[Forwarder._MULTIPROCESSING_ENV_VAR] = '1'
36 63
37 Args: 64 @staticmethod
38 adb: Instance of AndroidCommands for talking to the device. 65 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. 66 """Runs the forwarder.
54 67
55 Args: 68 Args:
56 port_pairs: A list of tuples (device_port, host_port) to forward. Note 69 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 70 that you can specify 0 as a device_port, in which case a
58 port will by dynamically assigned on the device. You can 71 port will by dynamically assigned on the device. You can
59 get the number of the assigned port using the 72 get the number of the assigned port using the
60 DevicePortForHostPort method. 73 DevicePortForHostPort method.
74 adb: An AndroidCommands instance.
61 tool: Tool class to use to get wrapper, if necessary, for executing the 75 tool: Tool class to use to get wrapper, if necessary, for executing the
62 forwarder (see valgrind_tools.py). 76 forwarder (see valgrind_tools.py).
63 77
64 Raises: 78 Raises:
65 Exception on failure to forward the port. 79 Exception on failure to forward the port.
66 """ 80 """
67 with self._lock: 81 if not tool:
68 self._InitDeviceLocked(tool) 82 tool = valgrind_tools.CreateTool(None, adb)
69 host_name = '127.0.0.1' 83 with Forwarder._lock:
84 instance = Forwarder._GetInstanceLocked(build_type, tool)
85 instance._InitDeviceOnceLocked(adb, tool)
86
70 redirection_commands = [ 87 redirection_commands = [
71 ['--serial-id=' + self._adb.Adb().GetSerialNumber(), '--map', 88 ['--serial-id=' + adb.Adb().GetSerialNumber(), '--map',
72 str(device), str(host)] for device, host in port_pairs] 89 str(device), str(host)] for device, host in port_pairs]
73 logging.info('Forwarding using commands: %s', redirection_commands) 90 logging.info('Forwarding using commands: %s', redirection_commands)
74 91
75 for redirection_command in redirection_commands: 92 for redirection_command in redirection_commands:
76 try: 93 try:
77 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( 94 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
78 [self._host_forwarder_path] + redirection_command) 95 [instance._host_forwarder_path] + redirection_command)
79 except OSError as e: 96 except OSError as e:
80 if e.errno == 2: 97 if e.errno == 2:
81 raise Exception('Unable to start host forwarder. Make sure you have' 98 raise Exception('Unable to start host forwarder. Make sure you have'
82 ' built host_forwarder.') 99 ' built host_forwarder.')
83 else: raise 100 else: raise
84 if exit_code != 0: 101 if exit_code != 0:
85 raise Exception('%s exited with %d:\n%s' % ( 102 raise Exception('%s exited with %d:\n%s' % (
86 self._host_forwarder_path, exit_code, '\n'.join(output))) 103 instance._host_forwarder_path, exit_code, '\n'.join(output)))
87 tokens = output.split(':') 104 tokens = output.split(':')
88 if len(tokens) != 2: 105 if len(tokens) != 2:
89 raise Exception(('Unexpected host forwarder output "%s", ' + 106 raise Exception(('Unexpected host forwarder output "%s", ' +
90 'expected "device_port:host_port"') % output) 107 'expected "device_port:host_port"') % output)
91 device_port = int(tokens[0]) 108 device_port = int(tokens[0])
92 host_port = int(tokens[1]) 109 host_port = int(tokens[1])
93 self._device_to_host_port_map[device_port] = host_port 110 instance._device_to_host_port_map[device_port] = host_port
94 self._host_to_device_port_map[host_port] = device_port 111 instance._host_to_device_port_map[host_port] = device_port
95 logging.info('Forwarding device port: %d to host port: %d.', 112 logging.info('Forwarding device port: %d to host port: %d.',
96 device_port, host_port) 113 device_port, host_port)
97 114
98 def _InitDeviceLocked(self, tool): 115 @staticmethod
99 """Initializes the device forwarder process (only once).""" 116 def UnmapDevicePort(device_port, adb):
100 if self._device_initialized: 117 """Unmaps a previously forwarded device port.
118
119 Args:
120 adb: An AndroidCommands instance.
121 device_port: A previously forwarded port (through Map()).
122 """
123 with Forwarder._lock:
124 instance = Forwarder._GetInstanceLocked(None, None)
125 if not device_port in instance._device_to_host_port_map:
126 return
127 redirection_command = [
128 '--serial-id=' + adb.Adb().GetSerialNumber(), '--unmap',
129 str(device_port)]
130 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
131 [instance._host_forwarder_path] + redirection_command)
132 if exit_code != 0:
133 logging.error('%s exited with %d:\n%s' % (
134 instance._host_forwarder_path, exit_code, '\n'.join(output)))
135 host_port = instance._device_to_host_port_map[device_port]
136 del instance._device_to_host_port_map[device_port]
137 del instance._host_to_device_port_map[host_port]
138
139 @staticmethod
140 def UnmapPorts(port_pairs, adb):
frankf 2013/07/15 22:23:22 To clarify my earlier comment: In many cases, you
Philippe 2013/07/16 12:31:21 Done.
141 """Unmaps previously forwarded ports.
142
143 Args:
144 adb: An AndroidCommands instance.
145 port_pairs: A list of tuples (device_port, host_port) to unmap.
146 """
147 for (device_port, _) in port_pairs:
148 Forwarder.UnmapDevicePort(device_port, adb)
149
150 @staticmethod
151 def DevicePortForHostPort(host_port):
152 """Returns the device port that corresponds to a given host port."""
153 with Forwarder._lock:
154 return Forwarder._GetInstanceLocked(
155 None, None)._host_to_device_port_map.get(host_port)
156
157 @staticmethod
158 def _GetInstanceLocked(build_type, tool):
159 """Returns the singleton instance.
160
161 Note that the global lock must be acquired before calling this method.
162
163 Args:
164 build_type: 'Release' or 'Debug'
165 tool: Tool class to use to get wrapper, if necessary, for executing the
166 forwarder (see valgrind_tools.py).
167 """
168 if not Forwarder._instance:
169 Forwarder._instance = Forwarder(build_type, tool)
170 return Forwarder._instance
171
172 def __init__(self, build_type, tool):
173 """Constructs a new instance of Forwarder.
174
175 Note that Forwarder is a singleton therefore this constructor should be
176 called only once.
177
178 Args:
179 build_type: 'Release' or 'Debug'
180 tool: Tool class to use to get wrapper, if necessary, for executing the
181 forwarder (see valgrind_tools.py).
182 """
183 assert not Forwarder._instance
184 self._build_type = build_type
185 self._tool = tool
186 self._initialized_devices = set()
187 self._device_to_host_port_map = dict()
188 self._host_to_device_port_map = dict()
189 self._InitHostOnceLocked()
bulach 2013/07/15 17:51:43 nit: maybe move 189 below, i.e., do all member ini
Philippe 2013/07/16 12:31:21 Good idea.
190 self._device_forwarder_path_on_host = os.path.join(
191 cmd_helper.OutDirectory.get(), self._build_type, 'forwarder_dist')
192
193 @staticmethod
194 def _GetPid():
bulach 2013/07/15 17:51:43 nit: maybe GetPidForLock() ?
Philippe 2013/07/16 12:31:21 Done.
195 """Returns the PID used for host_forwarder initialization.
196
197 In case multi-process sharding is used, the PID of the "sharder" is used.
198 The "sharder" is the initial process that forks that is the parent process.
199 By default, multi-processing is not used. In that case the PID of the
200 current process is returned.
201 """
202 use_multiprocessing = Forwarder._MULTIPROCESSING_ENV_VAR in os.environ
203 return os.getppid() if use_multiprocessing else os.getpid()
204
205 def _InitHostOnceLocked(self):
frankf 2013/07/15 22:23:22 To be consistent remove the 'Once' from method nam
Philippe 2013/07/16 12:31:21 Done. Note that I meant 'only one time' by 'once'
206 """Initializes the host forwarder daemon.
207
208 Note that the global lock must be acquired before calling this method. This
209 method determines the host_forwarder binary location and kills any existing
210 host_forwarder process that could be stale.
211 """
212 # See if the host_forwarder daemon was already initialized by a concurrent
213 # process or thread (in case multi-process sharding is not used).
214 current_pid = str(Forwarder._GetPid())
bulach 2013/07/15 17:51:43 nit: maybe pid_for_lock? (current_pid is a big con
Philippe 2013/07/16 12:31:21 Done.
215 with open(Forwarder._LOCK_PATH, 'r') as pid_file:
bulach 2013/07/15 17:51:43 w+ ?
Philippe 2013/07/16 12:31:21 I had a fun debugging session with these file mode
216 pid_with_start_time = pid_file.readline()
217 if pid_with_start_time:
218 (pid, process_start_time) = pid_with_start_time.split(':')
frankf 2013/07/15 22:23:22 where's process_start_time used?
Philippe 2013/07/16 12:31:21 Great catch! This was a bug.
219 if pid == current_pid:
220 if _GetProcessStartTime(pid) == _GetProcessStartTime(current_pid):
221 return
222 self._host_forwarder_path = _MakeBinaryPath(
223 self._build_type, 'host_forwarder')
224 if not os.path.exists(self._host_forwarder_path):
225 self._build_type = 'Release' if self._build_type == 'Debug' else 'Debug'
226 self._host_forwarder_path = _MakeBinaryPath(
227 self._build_type, 'host_forwarder')
228 assert os.path.exists(
229 self._host_forwarder_path), 'Please build forwarder2'
bulach 2013/07/15 17:51:43 nit: I suppose 222-229 could be moved to the ctor
Philippe 2013/07/16 12:31:21 Yes, good idea.
230 self._KillHostLocked()
231 pid_file.close()
bulach 2013/07/15 17:51:43 pid_file.seek(0), and avoid re-opening it below.
Philippe 2013/07/16 12:31:21 Done.
232 pid_file = open(Forwarder._LOCK_PATH, 'w+')
233 pid_file.write(
234 '%s:%s\n' % (current_pid, _GetProcessStartTime(current_pid)))
235 pid_file.flush()
bulach 2013/07/15 17:51:43 doesn't "with" take care of flushing?
Philippe 2013/07/16 12:31:21 Yeah, indeed. I used not to have this 'with' state
236
237 def _InitDeviceOnceLocked(self, adb, tool):
238 """Initializes the device_forwarder daemon for a specific device (once).
239
240 Note that the global lock must be acquired before calling this method. This
241 method kills any existing device_forwarder daemon on the device that could
242 be stale, pushes the latest version of the daemon (to the device) and starts
243 it.
244
245 Args:
246 adb: An AndroidCommands instance.
247 tool: Tool class to use to get wrapper, if necessary, for executing the
248 forwarder (see valgrind_tools.py).
249 """
250 device_serial = adb.Adb().GetSerialNumber()
251 if device_serial in self._initialized_devices:
101 return 252 return
102 self._adb.PushIfNeeded( 253 Forwarder._KillDeviceLocked(adb, tool)
254 adb.PushIfNeeded(
103 self._device_forwarder_path_on_host, 255 self._device_forwarder_path_on_host,
104 Forwarder._DEVICE_FORWARDER_FOLDER) 256 Forwarder._DEVICE_FORWARDER_FOLDER)
105 (exit_code, output) = self._adb.GetShellCommandStatusAndOutput( 257 (exit_code, output) = adb.GetShellCommandStatusAndOutput(
106 '%s %s %s' % (Forwarder._LD_LIBRARY_PATH, tool.GetUtilWrapper(), 258 '%s %s %s' % (Forwarder._LD_LIBRARY_PATH, tool.GetUtilWrapper(),
107 Forwarder._DEVICE_FORWARDER_PATH)) 259 Forwarder._DEVICE_FORWARDER_PATH))
108 if exit_code != 0: 260 if exit_code != 0:
109 raise Exception( 261 raise Exception(
110 'Failed to start device forwarder:\n%s' % '\n'.join(output)) 262 'Failed to start device forwarder:\n%s' % '\n'.join(output))
111 self._device_initialized = True 263 self._initialized_devices.add(device_serial)
112 264
113 def UnmapDevicePort(self, device_port): 265 def _KillHostLocked(self):
114 """Unmaps a previously forwarded device port. 266 """Kills the forwarder process running on the host.
115 267
116 Args: 268 Note that the global lock must be acquired before calling this method.
117 device_port: A previously forwarded port (through Run()).
118 """ 269 """
119 with self._lock: 270 logging.info('Killing host_forwarder.')
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( 271 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
129 [self._host_forwarder_path] + redirection_command) 272 [self._host_forwarder_path, '--kill-server'])
130 if exit_code != 0: 273 if exit_code != 0:
131 logging.error('%s exited with %d:\n%s' % ( 274 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
132 self._host_forwarder_path, exit_code, '\n'.join(output))) 275 ['pkill', '-9', 'host_forwarder'])
133 host_port = self._device_to_host_port_map[device_port] 276 if exit_code != 0:
134 del self._device_to_host_port_map[device_port] 277 raise Exception('%s exited with %d:\n%s' % (
135 del self._host_to_device_port_map[host_port] 278 self._host_forwarder_path, exit_code, '\n'.join(output)))
136 279
137 @staticmethod 280 @staticmethod
138 def KillHost(build_type='Debug'): 281 def _KillDeviceLocked(adb, tool):
139 """Kills the forwarder process running on the host. 282 """Kills the forwarder process running on the device.
140 283
141 Args: 284 Note that the global lock must be acquired before calling this method.
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:
153 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput(
154 ['pkill', 'host_forwarder'])
155 if exit_code != 0:
156 raise Exception('%s exited with %d:\n%s' % (
157 host_forwarder_path, exit_code, '\n'.join(output)))
158
159 @staticmethod
160 def KillDevice(adb, tool):
161 """Kills the forwarder process running on the device.
162 285
163 Args: 286 Args:
164 adb: Instance of AndroidCommands for talking to the device. 287 adb: Instance of AndroidCommands for talking to the device.
165 tool: Wrapper tool (e.g. valgrind) that can be used to execute the device 288 tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
166 forwarder (see valgrind_tools.py). 289 forwarder (see valgrind_tools.py).
167 """ 290 """
168 logging.info('Killing device_forwarder.') 291 logging.info('Killing device_forwarder.')
169 if not adb.FileExistsOnDevice(Forwarder._DEVICE_FORWARDER_PATH): 292 if not adb.FileExistsOnDevice(Forwarder._DEVICE_FORWARDER_PATH):
170 return 293 return
171 (exit_code, output) = adb.GetShellCommandStatusAndOutput( 294 (exit_code, output) = adb.GetShellCommandStatusAndOutput(
172 '%s %s --kill-server' % (tool.GetUtilWrapper(), 295 '%s %s --kill-server' % (tool.GetUtilWrapper(),
173 Forwarder._DEVICE_FORWARDER_PATH)) 296 Forwarder._DEVICE_FORWARDER_PATH))
174 # TODO(pliard): Remove the following call to KillAllBlocking() when we are 297 # TODO(pliard): Remove the following call to KillAllBlocking() when we are
175 # sure that the old version of device_forwarder (not supporting 298 # sure that the old version of device_forwarder (not supporting
176 # 'kill-server') is not running on the bots anymore. 299 # 'kill-server') is not running on the bots anymore.
177 timeout_sec = 5 300 timeout_sec = 5
178 processes_killed = adb.KillAllBlocking('device_forwarder', timeout_sec) 301 processes_killed = adb.KillAllBlocking('device_forwarder', timeout_sec)
179 if not processes_killed: 302 if not processes_killed:
180 pids = adb.ExtractPid('device_forwarder') 303 pids = adb.ExtractPid('device_forwarder')
181 if pids: 304 if pids:
182 raise Exception('Timed out while killing device_forwarder') 305 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

Powered by Google App Engine
This is Rietveld 408576698