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
12 from devil
.utils
import cmd_helper
13 from pylib
import constants
14 from pylib
import valgrind_tools
17 def _GetProcessStartTime(pid
):
18 return psutil
.Process(pid
).create_time
21 class _FileLock(object):
22 """With statement-aware implementation of a file lock.
24 File locks are needed for cross-process synchronization when the
25 multiprocessing Python module is used.
27 def __init__(self
, path
):
32 self
._fd
= os
.open(self
._path
, os
.O_RDONLY | os
.O_CREAT
)
34 raise Exception('Could not open file %s for reading' % self
._path
)
35 fcntl
.flock(self
._fd
, fcntl
.LOCK_EX
)
37 def __exit__(self
, _exception_type
, _exception_value
, traceback
):
38 fcntl
.flock(self
._fd
, fcntl
.LOCK_UN
)
42 class Forwarder(object):
43 """Thread-safe class to manage port forwards from the device to the host."""
45 _DEVICE_FORWARDER_FOLDER
= (constants
.TEST_EXECUTABLE_DIR
+
47 _DEVICE_FORWARDER_PATH
= (constants
.TEST_EXECUTABLE_DIR
+
48 '/forwarder/device_forwarder')
49 _LOCK_PATH
= '/tmp/chrome.forwarder.lock'
50 # Defined in host_forwarder_main.cc
51 _HOST_FORWARDER_LOG
= '/tmp/host_forwarder_log'
56 def Map(port_pairs
, device
, tool
=None):
57 """Runs the forwarder.
60 port_pairs: A list of tuples (device_port, host_port) to forward. Note
61 that you can specify 0 as a device_port, in which case a
62 port will by dynamically assigned on the device. You can
63 get the number of the assigned port using the
64 DevicePortForHostPort method.
65 device: A DeviceUtils instance.
66 tool: Tool class to use to get wrapper, if necessary, for executing the
67 forwarder (see valgrind_tools.py).
70 Exception on failure to forward the port.
73 tool
= valgrind_tools
.CreateTool(None, device
)
74 with
_FileLock(Forwarder
._LOCK
_PATH
):
75 instance
= Forwarder
._GetInstanceLocked
(tool
)
76 instance
._InitDeviceLocked
(device
, tool
)
78 device_serial
= str(device
)
79 redirection_commands
= [
80 ['--adb=' + constants
.GetAdbPath(),
81 '--serial-id=' + device_serial
,
82 '--map', str(device_port
), str(host_port
)]
83 for device_port
, host_port
in port_pairs
]
84 logging
.info('Forwarding using commands: %s', redirection_commands
)
86 for redirection_command
in redirection_commands
:
88 (exit_code
, output
) = cmd_helper
.GetCmdStatusAndOutput(
89 [instance
._host
_forwarder
_path
] + redirection_command
)
92 raise Exception('Unable to start host forwarder. Make sure you have'
93 ' built host_forwarder.')
96 Forwarder
._KillDeviceLocked
(device
, tool
)
97 # Log alive forwarders
98 ps_out
= device
.RunShellCommand(['ps'])
99 logging
.info('Currently running device_forwarders:')
101 if 'device_forwarder' in line
:
102 logging
.info(' %s', line
)
103 raise Exception('%s exited with %d:\n%s' % (
104 instance
._host
_forwarder
_path
, exit_code
, '\n'.join(output
)))
105 tokens
= output
.split(':')
107 raise Exception('Unexpected host forwarder output "%s", '
108 'expected "device_port:host_port"' % output
)
109 device_port
= int(tokens
[0])
110 host_port
= int(tokens
[1])
111 serial_with_port
= (device_serial
, device_port
)
112 instance
._device
_to
_host
_port
_map
[serial_with_port
] = host_port
113 instance
._host
_to
_device
_port
_map
[host_port
] = serial_with_port
114 logging
.info('Forwarding device port: %d to host port: %d.',
115 device_port
, host_port
)
118 def UnmapDevicePort(device_port
, device
):
119 """Unmaps a previously forwarded device port.
122 device: A DeviceUtils instance.
123 device_port: A previously forwarded port (through Map()).
125 with
_FileLock(Forwarder
._LOCK
_PATH
):
126 Forwarder
._UnmapDevicePortLocked
(device_port
, device
)
129 def UnmapAllDevicePorts(device
):
130 """Unmaps all the previously forwarded ports for the provided device.
133 device: A DeviceUtils instance.
134 port_pairs: A list of tuples (device_port, host_port) to unmap.
136 with
_FileLock(Forwarder
._LOCK
_PATH
):
137 if not Forwarder
._instance
:
139 adb_serial
= str(device
)
140 if adb_serial
not in Forwarder
._instance
._initialized
_devices
:
142 port_map
= Forwarder
._GetInstanceLocked
(
143 None)._device
_to
_host
_port
_map
144 for (device_serial
, device_port
) in port_map
.keys():
145 if adb_serial
== device_serial
:
146 Forwarder
._UnmapDevicePortLocked
(device_port
, device
)
147 # There are no more ports mapped, kill the device_forwarder.
148 tool
= valgrind_tools
.CreateTool(None, device
)
149 Forwarder
._KillDeviceLocked
(device
, tool
)
152 def DevicePortForHostPort(host_port
):
153 """Returns the device port that corresponds to a given host port."""
154 with
_FileLock(Forwarder
._LOCK
_PATH
):
155 _
, device_port
= Forwarder
._GetInstanceLocked
(
156 None)._host
_to
_device
_port
_map
.get(host_port
)
161 if os
.path
.exists(Forwarder
._HOST
_FORWARDER
_LOG
):
162 os
.unlink(Forwarder
._HOST
_FORWARDER
_LOG
)
166 if not os
.path
.exists(Forwarder
._HOST
_FORWARDER
_LOG
):
168 with
file(Forwarder
._HOST
_FORWARDER
_LOG
, 'r') as f
:
172 def _GetInstanceLocked(tool
):
173 """Returns the singleton instance.
175 Note that the global lock must be acquired before calling this method.
178 tool: Tool class to use to get wrapper, if necessary, for executing the
179 forwarder (see valgrind_tools.py).
181 if not Forwarder
._instance
:
182 Forwarder
._instance
= Forwarder(tool
)
183 return Forwarder
._instance
185 def __init__(self
, tool
):
186 """Constructs a new instance of Forwarder.
188 Note that Forwarder is a singleton therefore this constructor should be
192 tool: Tool class to use to get wrapper, if necessary, for executing the
193 forwarder (see valgrind_tools.py).
195 assert not Forwarder
._instance
197 self
._initialized
_devices
= set()
198 self
._device
_to
_host
_port
_map
= dict()
199 self
._host
_to
_device
_port
_map
= dict()
200 self
._host
_forwarder
_path
= os
.path
.join(
201 constants
.GetOutDirectory(), 'host_forwarder')
202 assert os
.path
.exists(self
._host
_forwarder
_path
), 'Please build forwarder2'
203 self
._device
_forwarder
_path
_on
_host
= os
.path
.join(
204 constants
.GetOutDirectory(), 'forwarder_dist')
205 self
._InitHostLocked
()
208 def _UnmapDevicePortLocked(device_port
, device
):
209 """Internal method used by UnmapDevicePort().
211 Note that the global lock must be acquired before calling this method.
213 instance
= Forwarder
._GetInstanceLocked
(None)
215 serial_with_port
= (serial
, device_port
)
216 if not serial_with_port
in instance
._device
_to
_host
_port
_map
:
217 logging
.error('Trying to unmap non-forwarded port %d', device_port
)
219 redirection_command
= ['--adb=' + constants
.GetAdbPath(),
220 '--serial-id=' + serial
,
221 '--unmap', str(device_port
)]
222 logging
.info('Undo forwarding using command: %s', redirection_command
)
223 (exit_code
, output
) = cmd_helper
.GetCmdStatusAndOutput(
224 [instance
._host
_forwarder
_path
] + redirection_command
)
227 '%s exited with %d:\n%s',
228 instance
._host
_forwarder
_path
, exit_code
, '\n'.join(output
))
229 host_port
= instance
._device
_to
_host
_port
_map
[serial_with_port
]
230 del instance
._device
_to
_host
_port
_map
[serial_with_port
]
231 del instance
._host
_to
_device
_port
_map
[host_port
]
234 def _GetPidForLock():
235 """Returns the PID used for host_forwarder initialization.
237 The PID of the "sharder" is used to handle multiprocessing. The "sharder"
238 is the initial process that forks that is the parent process.
242 def _InitHostLocked(self
):
243 """Initializes the host forwarder daemon.
245 Note that the global lock must be acquired before calling this method. This
246 method kills any existing host_forwarder process that could be stale.
248 # See if the host_forwarder daemon was already initialized by a concurrent
249 # process or thread (in case multi-process sharding is not used).
250 pid_for_lock
= Forwarder
._GetPidForLock
()
251 fd
= os
.open(Forwarder
._LOCK
_PATH
, os
.O_RDWR | os
.O_CREAT
)
252 with os
.fdopen(fd
, 'r+') as pid_file
:
253 pid_with_start_time
= pid_file
.readline()
254 if pid_with_start_time
:
255 (pid
, process_start_time
) = pid_with_start_time
.split(':')
256 if pid
== str(pid_for_lock
):
257 if process_start_time
== str(_GetProcessStartTime(pid_for_lock
)):
259 self
._KillHostLocked
()
262 '%s:%s' % (pid_for_lock
, str(_GetProcessStartTime(pid_for_lock
))))
265 def _InitDeviceLocked(self
, device
, tool
):
266 """Initializes the device_forwarder daemon for a specific device (once).
268 Note that the global lock must be acquired before calling this method. This
269 method kills any existing device_forwarder daemon on the device that could
270 be stale, pushes the latest version of the daemon (to the device) and starts
274 device: A DeviceUtils instance.
275 tool: Tool class to use to get wrapper, if necessary, for executing the
276 forwarder (see valgrind_tools.py).
278 device_serial
= str(device
)
279 if device_serial
in self
._initialized
_devices
:
281 Forwarder
._KillDeviceLocked
(device
, tool
)
282 device
.PushChangedFiles([(
283 self
._device
_forwarder
_path
_on
_host
,
284 Forwarder
._DEVICE
_FORWARDER
_FOLDER
)])
285 cmd
= '%s %s' % (tool
.GetUtilWrapper(), Forwarder
._DEVICE
_FORWARDER
_PATH
)
286 device
.RunShellCommand(
287 cmd
, env
={'LD_LIBRARY_PATH': Forwarder
._DEVICE
_FORWARDER
_FOLDER
},
289 self
._initialized
_devices
.add(device_serial
)
291 def _KillHostLocked(self
):
292 """Kills the forwarder process running on the host.
294 Note that the global lock must be acquired before calling this method.
296 logging
.info('Killing host_forwarder.')
297 (exit_code
, output
) = cmd_helper
.GetCmdStatusAndOutput(
298 [self
._host
_forwarder
_path
, '--kill-server'])
300 (exit_code
, output
) = cmd_helper
.GetCmdStatusAndOutput(
301 ['pkill', '-9', 'host_forwarder'])
303 raise Exception('%s exited with %d:\n%s' % (
304 self
._host
_forwarder
_path
, exit_code
, '\n'.join(output
)))
307 def _KillDeviceLocked(device
, tool
):
308 """Kills the forwarder process running on the device.
310 Note that the global lock must be acquired before calling this method.
313 device: Instance of DeviceUtils for talking to the device.
314 tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
315 forwarder (see valgrind_tools.py).
317 logging
.info('Killing device_forwarder.')
318 Forwarder
._instance
._initialized
_devices
.discard(str(device
))
319 if not device
.FileExists(Forwarder
._DEVICE
_FORWARDER
_PATH
):
322 cmd
= '%s %s --kill-server' % (tool
.GetUtilWrapper(),
323 Forwarder
._DEVICE
_FORWARDER
_PATH
)
324 device
.RunShellCommand(
325 cmd
, env
={'LD_LIBRARY_PATH': Forwarder
._DEVICE
_FORWARDER
_FOLDER
},