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 pylib
import cmd_helper
13 from pylib
import constants
14 from pylib
import valgrind_tools
16 # TODO(jbudorick) Remove once telemetry gets switched over.
17 import pylib
.android_commands
18 import pylib
.device
.device_utils
21 def _GetProcessStartTime(pid
):
22 return psutil
.Process(pid
).create_time
25 class _FileLock(object):
26 """With statement-aware implementation of a file lock.
28 File locks are needed for cross-process synchronization when the
29 multiprocessing Python module is used.
31 def __init__(self
, path
):
36 self
._fd
= os
.open(self
._path
, os
.O_RDONLY | os
.O_CREAT
)
38 raise Exception('Could not open file %s for reading' % self
._path
)
39 fcntl
.flock(self
._fd
, fcntl
.LOCK_EX
)
41 def __exit__(self
, _exception_type
, _exception_value
, traceback
):
42 fcntl
.flock(self
._fd
, fcntl
.LOCK_UN
)
46 class Forwarder(object):
47 """Thread-safe class to manage port forwards from the device to the host."""
49 _DEVICE_FORWARDER_FOLDER
= (constants
.TEST_EXECUTABLE_DIR
+
51 _DEVICE_FORWARDER_PATH
= (constants
.TEST_EXECUTABLE_DIR
+
52 '/forwarder/device_forwarder')
53 _LOCK_PATH
= '/tmp/chrome.forwarder.lock'
54 _MULTIPROCESSING_ENV_VAR
= 'CHROME_FORWARDER_USE_MULTIPROCESSING'
55 # Defined in host_forwarder_main.cc
56 _HOST_FORWARDER_LOG
= '/tmp/host_forwarder_log'
61 def UseMultiprocessing():
62 """Tells the forwarder that multiprocessing is used."""
63 os
.environ
[Forwarder
._MULTIPROCESSING
_ENV
_VAR
] = '1'
66 def Map(port_pairs
, device
, tool
=None):
67 """Runs the forwarder.
70 port_pairs: A list of tuples (device_port, host_port) to forward. Note
71 that you can specify 0 as a device_port, in which case a
72 port will by dynamically assigned on the device. You can
73 get the number of the assigned port using the
74 DevicePortForHostPort method.
75 device: A DeviceUtils instance.
76 tool: Tool class to use to get wrapper, if necessary, for executing the
77 forwarder (see valgrind_tools.py).
80 Exception on failure to forward the port.
82 # TODO(jbudorick) Remove once telemetry gets switched over.
83 if isinstance(device
, pylib
.android_commands
.AndroidCommands
):
84 device
= pylib
.device
.device_utils
.DeviceUtils(device
)
86 tool
= valgrind_tools
.CreateTool(None, device
)
87 with
_FileLock(Forwarder
._LOCK
_PATH
):
88 instance
= Forwarder
._GetInstanceLocked
(tool
)
89 instance
._InitDeviceLocked
(device
, tool
)
91 device_serial
= str(device
)
92 redirection_commands
= [
93 ['--serial-id=' + device_serial
, '--map', str(device_port
),
94 str(host_port
)] for device_port
, host_port
in port_pairs
]
95 logging
.info('Forwarding using commands: %s', redirection_commands
)
97 for redirection_command
in redirection_commands
:
99 (exit_code
, output
) = cmd_helper
.GetCmdStatusAndOutput(
100 [instance
._host
_forwarder
_path
] + redirection_command
)
103 raise Exception('Unable to start host forwarder. Make sure you have'
104 ' built host_forwarder.')
107 Forwarder
._KillDeviceLocked
(device
, tool
)
108 raise Exception('%s exited with %d:\n%s' % (
109 instance
._host
_forwarder
_path
, exit_code
, '\n'.join(output
)))
110 tokens
= output
.split(':')
112 raise Exception('Unexpected host forwarder output "%s", '
113 'expected "device_port:host_port"' % output
)
114 device_port
= int(tokens
[0])
115 host_port
= int(tokens
[1])
116 serial_with_port
= (device_serial
, device_port
)
117 instance
._device
_to
_host
_port
_map
[serial_with_port
] = host_port
118 instance
._host
_to
_device
_port
_map
[host_port
] = serial_with_port
119 logging
.info('Forwarding device port: %d to host port: %d.',
120 device_port
, host_port
)
123 def UnmapDevicePort(device_port
, device
):
124 """Unmaps a previously forwarded device port.
127 device: A DeviceUtils instance.
128 device_port: A previously forwarded port (through Map()).
130 # TODO(jbudorick) Remove once telemetry gets switched over.
131 if isinstance(device
, pylib
.android_commands
.AndroidCommands
):
132 device
= pylib
.device
.device_utils
.DeviceUtils(device
)
133 with
_FileLock(Forwarder
._LOCK
_PATH
):
134 Forwarder
._UnmapDevicePortLocked
(device_port
, device
)
137 def UnmapAllDevicePorts(device
):
138 """Unmaps all the previously forwarded ports for the provided device.
141 device: A DeviceUtils instance.
142 port_pairs: A list of tuples (device_port, host_port) to unmap.
144 # TODO(jbudorick) Remove once telemetry gets switched over.
145 if isinstance(device
, pylib
.android_commands
.AndroidCommands
):
146 device
= pylib
.device
.device_utils
.DeviceUtils(device
)
147 with
_FileLock(Forwarder
._LOCK
_PATH
):
148 if not Forwarder
._instance
:
150 adb_serial
= str(device
)
151 if adb_serial
not in Forwarder
._instance
._initialized
_devices
:
153 port_map
= Forwarder
._GetInstanceLocked
(
154 None)._device
_to
_host
_port
_map
155 for (device_serial
, device_port
) in port_map
.keys():
156 if adb_serial
== device_serial
:
157 Forwarder
._UnmapDevicePortLocked
(device_port
, device
)
158 # There are no more ports mapped, kill the device_forwarder.
159 tool
= valgrind_tools
.CreateTool(None, device
)
160 Forwarder
._KillDeviceLocked
(device
, tool
)
163 def DevicePortForHostPort(host_port
):
164 """Returns the device port that corresponds to a given host port."""
165 with
_FileLock(Forwarder
._LOCK
_PATH
):
166 (_device_serial
, device_port
) = Forwarder
._GetInstanceLocked
(
167 None)._host
_to
_device
_port
_map
.get(host_port
)
172 if os
.path
.exists(Forwarder
._HOST
_FORWARDER
_LOG
):
173 os
.unlink(Forwarder
._HOST
_FORWARDER
_LOG
)
177 if not os
.path
.exists(Forwarder
._HOST
_FORWARDER
_LOG
):
179 with
file(Forwarder
._HOST
_FORWARDER
_LOG
, 'r') as f
:
183 def _GetInstanceLocked(tool
):
184 """Returns the singleton instance.
186 Note that the global lock must be acquired before calling this method.
189 tool: Tool class to use to get wrapper, if necessary, for executing the
190 forwarder (see valgrind_tools.py).
192 if not Forwarder
._instance
:
193 Forwarder
._instance
= Forwarder(tool
)
194 return Forwarder
._instance
196 def __init__(self
, tool
):
197 """Constructs a new instance of Forwarder.
199 Note that Forwarder is a singleton therefore this constructor should be
203 tool: Tool class to use to get wrapper, if necessary, for executing the
204 forwarder (see valgrind_tools.py).
206 assert not Forwarder
._instance
208 self
._initialized
_devices
= set()
209 self
._device
_to
_host
_port
_map
= dict()
210 self
._host
_to
_device
_port
_map
= dict()
211 self
._host
_forwarder
_path
= os
.path
.join(
212 constants
.GetOutDirectory(), 'host_forwarder')
213 assert os
.path
.exists(self
._host
_forwarder
_path
), 'Please build forwarder2'
214 self
._device
_forwarder
_path
_on
_host
= os
.path
.join(
215 constants
.GetOutDirectory(), 'forwarder_dist')
216 self
._InitHostLocked
()
219 def _UnmapDevicePortLocked(device_port
, device
):
220 """Internal method used by UnmapDevicePort().
222 Note that the global lock must be acquired before calling this method.
224 instance
= Forwarder
._GetInstanceLocked
(None)
226 serial_with_port
= (serial
, device_port
)
227 if not serial_with_port
in instance
._device
_to
_host
_port
_map
:
228 logging
.error('Trying to unmap non-forwarded port %d' % device_port
)
230 redirection_command
= ['--serial-id=' + serial
, '--unmap', str(device_port
)]
231 (exit_code
, output
) = cmd_helper
.GetCmdStatusAndOutput(
232 [instance
._host
_forwarder
_path
] + redirection_command
)
234 logging
.error('%s exited with %d:\n%s' % (
235 instance
._host
_forwarder
_path
, exit_code
, '\n'.join(output
)))
236 host_port
= instance
._device
_to
_host
_port
_map
[serial_with_port
]
237 del instance
._device
_to
_host
_port
_map
[serial_with_port
]
238 del instance
._host
_to
_device
_port
_map
[host_port
]
241 def _GetPidForLock():
242 """Returns the PID used for host_forwarder initialization.
244 In case multi-process sharding is used, the PID of the "sharder" is used.
245 The "sharder" is the initial process that forks that is the parent process.
246 By default, multi-processing is not used. In that case the PID of the
247 current process is returned.
249 use_multiprocessing
= Forwarder
._MULTIPROCESSING
_ENV
_VAR
in os
.environ
250 return os
.getpgrp() if use_multiprocessing
else os
.getpid()
252 def _InitHostLocked(self
):
253 """Initializes the host forwarder daemon.
255 Note that the global lock must be acquired before calling this method. This
256 method kills any existing host_forwarder process that could be stale.
258 # See if the host_forwarder daemon was already initialized by a concurrent
259 # process or thread (in case multi-process sharding is not used).
260 pid_for_lock
= Forwarder
._GetPidForLock
()
261 fd
= os
.open(Forwarder
._LOCK
_PATH
, os
.O_RDWR | os
.O_CREAT
)
262 with os
.fdopen(fd
, 'r+') as pid_file
:
263 pid_with_start_time
= pid_file
.readline()
264 if pid_with_start_time
:
265 (pid
, process_start_time
) = pid_with_start_time
.split(':')
266 if pid
== str(pid_for_lock
):
267 if process_start_time
== str(_GetProcessStartTime(pid_for_lock
)):
269 self
._KillHostLocked
()
272 '%s:%s' % (pid_for_lock
, str(_GetProcessStartTime(pid_for_lock
))))
275 def _InitDeviceLocked(self
, device
, tool
):
276 """Initializes the device_forwarder daemon for a specific device (once).
278 Note that the global lock must be acquired before calling this method. This
279 method kills any existing device_forwarder daemon on the device that could
280 be stale, pushes the latest version of the daemon (to the device) and starts
284 device: A DeviceUtils instance.
285 tool: Tool class to use to get wrapper, if necessary, for executing the
286 forwarder (see valgrind_tools.py).
288 device_serial
= str(device
)
289 if device_serial
in self
._initialized
_devices
:
291 Forwarder
._KillDeviceLocked
(device
, tool
)
292 device
.PushChangedFiles([(
293 self
._device
_forwarder
_path
_on
_host
,
294 Forwarder
._DEVICE
_FORWARDER
_FOLDER
)])
295 cmd
= '%s %s' % (tool
.GetUtilWrapper(), Forwarder
._DEVICE
_FORWARDER
_PATH
)
296 (exit_code
, output
) = device
.old_interface
.GetAndroidToolStatusAndOutput(
297 cmd
, lib_path
=Forwarder
._DEVICE
_FORWARDER
_FOLDER
)
300 'Failed to start device forwarder:\n%s' % '\n'.join(output
))
301 self
._initialized
_devices
.add(device_serial
)
303 def _KillHostLocked(self
):
304 """Kills the forwarder process running on the host.
306 Note that the global lock must be acquired before calling this method.
308 logging
.info('Killing host_forwarder.')
309 (exit_code
, output
) = cmd_helper
.GetCmdStatusAndOutput(
310 [self
._host
_forwarder
_path
, '--kill-server'])
312 (exit_code
, output
) = cmd_helper
.GetCmdStatusAndOutput(
313 ['pkill', '-9', 'host_forwarder'])
315 raise Exception('%s exited with %d:\n%s' % (
316 self
._host
_forwarder
_path
, exit_code
, '\n'.join(output
)))
319 def _KillDeviceLocked(device
, tool
):
320 """Kills the forwarder process running on the device.
322 Note that the global lock must be acquired before calling this method.
325 device: Instance of DeviceUtils for talking to the device.
326 tool: Wrapper tool (e.g. valgrind) that can be used to execute the device
327 forwarder (see valgrind_tools.py).
329 logging
.info('Killing device_forwarder.')
330 Forwarder
._instance
._initialized
_devices
.discard(str(device
))
331 if not device
.FileExists(Forwarder
._DEVICE
_FORWARDER
_PATH
):
334 cmd
= '%s %s --kill-server' % (tool
.GetUtilWrapper(),
335 Forwarder
._DEVICE
_FORWARDER
_PATH
)
336 device
.old_interface
.GetAndroidToolStatusAndOutput(
337 cmd
, lib_path
=Forwarder
._DEVICE
_FORWARDER
_FOLDER
)