OLD | NEW |
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 logging | 5 import logging |
6 import os | 6 import os |
7 import re | 7 import re |
8 import sys | 8 import sys |
9 import time | 9 import time |
10 | 10 |
11 import android_commands | 11 import android_commands |
12 import cmd_helper | 12 import cmd_helper |
13 import constants | 13 import constants |
14 import ports | 14 import ports |
15 | 15 |
16 from pylib import pexpect | 16 from pylib import pexpect |
17 | 17 |
| 18 |
| 19 def _MakeBinaryPath(build_type, binary_name): |
| 20 return os.path.join(constants.CHROME_DIR, 'out', build_type, binary_name) |
| 21 |
| 22 |
18 class Forwarder(object): | 23 class Forwarder(object): |
19 """Class to manage port forwards from the device to the host.""" | 24 """Class to manage port forwards from the device to the host.""" |
20 | 25 |
21 _DEVICE_FORWARDER_PATH = constants.TEST_EXECUTABLE_DIR + '/device_forwarder' | 26 _DEVICE_FORWARDER_PATH = constants.TEST_EXECUTABLE_DIR + '/device_forwarder' |
22 | 27 |
23 # Unix Abstract socket path: | 28 # Unix Abstract socket path: |
24 _DEVICE_ADB_CONTROL_PORT = 'chrome_device_forwarder' | 29 _DEVICE_ADB_CONTROL_PORT = 'chrome_device_forwarder' |
25 _TIMEOUT_SECS = 30 | 30 _TIMEOUT_SECS = 30 |
26 | 31 |
27 def __init__(self, adb, port_pairs, tool, host_name, build_type): | 32 def __init__(self, adb, build_type): |
28 """Forwards TCP ports on the device back to the host. | 33 """Forwards TCP ports on the device back to the host. |
29 | 34 |
30 Works like adb forward, but in reverse. | 35 Works like adb forward, but in reverse. |
31 | 36 |
32 Args: | 37 Args: |
33 adb: Instance of AndroidCommands for talking to the device. | 38 adb: Instance of AndroidCommands for talking to the device. |
| 39 build_type: 'Release' or 'Debug'. |
| 40 """ |
| 41 assert build_type in ('Release', 'Debug') |
| 42 self._adb = adb |
| 43 self._host_to_device_port_map = dict() |
| 44 self._device_process = None |
| 45 self._host_forwarder_path = _MakeBinaryPath(build_type, 'host_forwarder') |
| 46 self._device_forwarder_path = _MakeBinaryPath( |
| 47 build_type, 'device_forwarder') |
| 48 |
| 49 def Run(self, port_pairs, tool, host_name): |
| 50 """Runs the forwarder. |
| 51 |
| 52 Args: |
34 port_pairs: A list of tuples (device_port, host_port) to forward. Note | 53 port_pairs: A list of tuples (device_port, host_port) to forward. Note |
35 that you can specify 0 as a device_port, in which case a | 54 that you can specify 0 as a device_port, in which case a |
36 port will by dynamically assigned on the device. You can | 55 port will by dynamically assigned on the device. You can |
37 get the number of the assigned port using the | 56 get the number of the assigned port using the |
38 DevicePortForHostPort method. | 57 DevicePortForHostPort method. |
39 tool: Tool class to use to get wrapper, if necessary, for executing the | 58 tool: Tool class to use to get wrapper, if necessary, for executing the |
40 forwarder (see valgrind_tools.py). | 59 forwarder (see valgrind_tools.py). |
41 host_name: Address to forward to, must be addressable from the | 60 host_name: Address to forward to, must be addressable from the |
42 host machine. Usually use loopback '127.0.0.1'. | 61 host machine. Usually use loopback '127.0.0.1'. |
43 build_type: 'Release' or 'Debug'. | |
44 | 62 |
45 Raises: | 63 Raises: |
46 Exception on failure to forward the port. | 64 Exception on failure to forward the port. |
47 """ | 65 """ |
48 self._adb = adb | 66 host_adb_control_port = ports.AllocateTestServerPort() |
49 self._host_to_device_port_map = dict() | 67 if not host_adb_control_port: |
50 self._host_process = None | 68 raise Exception('Failed to allocate a TCP port in the host machine.') |
51 self._device_process = None | 69 self._adb.PushIfNeeded(self._device_forwarder_path, |
52 self._adb_forward_process = None | 70 Forwarder._DEVICE_FORWARDER_PATH) |
| 71 redirection_commands = [ |
| 72 '%d:%d:%d:%s' % (host_adb_control_port, device, host, |
| 73 host_name) for device, host in port_pairs] |
| 74 logging.info('Command format: <ADB port>:<Device port>' + |
| 75 '[:<Forward to port>:<Forward to address>]') |
| 76 logging.info('Forwarding using commands: %s', redirection_commands) |
| 77 if cmd_helper.RunCmd( |
| 78 ['adb', '-s', self._adb._adb.GetSerialNumber(), 'forward', |
| 79 'tcp:%s' % host_adb_control_port, |
| 80 'localabstract:%s' % Forwarder._DEVICE_ADB_CONTROL_PORT]) != 0: |
| 81 raise Exception('Error while running adb forward.') |
53 | 82 |
54 self._host_adb_control_port = ports.AllocateTestServerPort() | 83 if not self._adb.ExtractPid('device_forwarder'): |
55 if not self._host_adb_control_port: | 84 # TODO(pliard): Get rid of pexpect here and make device_forwarder a daemon |
56 raise Exception('Failed to allocate a TCP port in the host machine.') | 85 # with a blocking CLI process that exits with a proper exit code and not |
57 adb.PushIfNeeded( | 86 # while the daemon is still setting up. This would be similar to how |
58 os.path.join(constants.CHROME_DIR, 'out', build_type, | 87 # host_forwarder works. |
59 'device_forwarder'), | 88 self._device_process = pexpect.spawn( |
60 Forwarder._DEVICE_FORWARDER_PATH) | 89 'adb', ['-s', |
61 self._host_forwarder_path = os.path.join(constants.CHROME_DIR, | 90 self._adb._adb.GetSerialNumber(), |
62 'out', | 91 'shell', |
63 build_type, | 92 '%s %s -D --adb_sock=%s' % ( |
64 'host_forwarder') | 93 tool.GetUtilWrapper(), |
65 forward_string = ['%d:%d:%s' % | 94 Forwarder._DEVICE_FORWARDER_PATH, |
66 (device, host, host_name) for device, host in port_pairs] | 95 Forwarder._DEVICE_ADB_CONTROL_PORT)]) |
67 logging.info('Forwarding ports: %s', forward_string) | 96 device_success_re = re.compile('Starting Device Forwarder.') |
| 97 device_failure_re = re.compile('.*:ERROR:(.*)') |
| 98 index = self._device_process.expect([device_success_re, |
| 99 device_failure_re, |
| 100 pexpect.EOF, |
| 101 pexpect.TIMEOUT], |
| 102 Forwarder._TIMEOUT_SECS) |
| 103 if index == 1: |
| 104 error_msg = str(self._device_process.match.group(1)) |
| 105 logging.error(self._device_process.before) |
| 106 self._device_process.close() |
| 107 raise Exception('Failed to start Device Forwarder with Error: %s' % |
| 108 error_msg) |
| 109 elif index == 2: |
| 110 logging.error(self._device_process.before) |
| 111 self._device_process.close() |
| 112 raise Exception( |
| 113 'Unexpected EOF while trying to start Device Forwarder.') |
| 114 elif index == 3: |
| 115 logging.error(self._device_process.before) |
| 116 self._device_process.close() |
| 117 raise Exception('Timeout while trying start Device Forwarder') |
| 118 |
| 119 for redirection_command in redirection_commands: |
| 120 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( |
| 121 [self._host_forwarder_path, redirection_command]) |
| 122 if exit_code != 0: |
| 123 raise Exception('%s exited with %d: %s' % (self._host_forwarder_path, |
| 124 exit_code, output)) |
| 125 tokens = output.split(':') |
| 126 if len(tokens) != 2: |
| 127 raise Exception('Unexpected host forwarder output "%s", ' + |
| 128 'expected "device_port:host_port"' % output) |
| 129 device_port = int(tokens[0]) |
| 130 host_port = int(tokens[1]) |
| 131 self._host_to_device_port_map[host_port] = device_port |
| 132 logging.info('Forwarding device port: %d to host port: %d.', device_port, |
| 133 host_port) |
| 134 |
| 135 @staticmethod |
| 136 def KillHost(build_type): |
| 137 host_forwarder_path = _MakeBinaryPath(build_type, 'host_forwarder') |
| 138 (exit_code, output) = cmd_helper.GetCmdStatusAndOutput( |
| 139 [host_forwarder_path, 'kill-server']) |
| 140 if exit_code != 0: |
| 141 raise Exception('%s exited with %d: %s' % (host_forwarder_path, |
| 142 exit_code, output)) |
| 143 |
| 144 @staticmethod |
| 145 def KillDevice(adb): |
| 146 logging.info('Killing device_forwarder.') |
68 timeout_sec = 5 | 147 timeout_sec = 5 |
69 host_pattern = 'host_forwarder.*' + ' '.join(forward_string) | 148 processes_killed = adb.KillAllBlocking('device_forwarder', timeout_sec) |
70 # TODO(felipeg): Rather than using a blocking kill() here, the device | |
71 # forwarder could try to bind the Unix Domain Socket until it succeeds or | |
72 # while it fails because the socket is already bound (with appropriate | |
73 # timeout handling obviously). | |
74 self._KillHostForwarderBlocking(host_pattern, timeout_sec) | |
75 self._KillDeviceForwarderBlocking(timeout_sec) | |
76 self._adb_forward_process = pexpect.spawn( | |
77 'adb', ['-s', | |
78 adb._adb.GetSerialNumber(), | |
79 'forward', | |
80 'tcp:%s' % self._host_adb_control_port, | |
81 'localabstract:%s' % Forwarder._DEVICE_ADB_CONTROL_PORT]) | |
82 self._device_process = pexpect.spawn( | |
83 'adb', ['-s', | |
84 adb._adb.GetSerialNumber(), | |
85 'shell', | |
86 '%s %s -D --adb_sock=%s' % ( | |
87 tool.GetUtilWrapper(), | |
88 Forwarder._DEVICE_FORWARDER_PATH, | |
89 Forwarder._DEVICE_ADB_CONTROL_PORT)]) | |
90 | |
91 device_success_re = re.compile('Starting Device Forwarder.') | |
92 device_failure_re = re.compile('.*:ERROR:(.*)') | |
93 index = self._device_process.expect([device_success_re, | |
94 device_failure_re, | |
95 pexpect.EOF, | |
96 pexpect.TIMEOUT], | |
97 Forwarder._TIMEOUT_SECS) | |
98 if index == 1: | |
99 # Failure | |
100 error_msg = str(self._device_process.match.group(1)) | |
101 logging.error(self._device_process.before) | |
102 self._CloseProcess() | |
103 raise Exception('Failed to start Device Forwarder with Error: %s' % | |
104 error_msg) | |
105 elif index == 2: | |
106 logging.error(self._device_process.before) | |
107 self._CloseProcess() | |
108 raise Exception('Unexpected EOF while trying to start Device Forwarder.') | |
109 elif index == 3: | |
110 logging.error(self._device_process.before) | |
111 self._CloseProcess() | |
112 raise Exception('Timeout while trying start Device Forwarder') | |
113 | |
114 self._host_process = pexpect.spawn(self._host_forwarder_path, | |
115 ['--adb_port=%s' % ( | |
116 self._host_adb_control_port)] + | |
117 forward_string) | |
118 | |
119 # Read the output of the command to determine which device ports where | |
120 # forwarded to which host ports (necessary if | |
121 host_success_re = re.compile('Forwarding device port (\d+) to host (\d+):') | |
122 host_failure_re = re.compile('Couldn\'t start forwarder server for port ' | |
123 'spec: (\d+):(\d+)') | |
124 for pair in port_pairs: | |
125 index = self._host_process.expect([host_success_re, | |
126 host_failure_re, | |
127 pexpect.EOF, | |
128 pexpect.TIMEOUT], | |
129 Forwarder._TIMEOUT_SECS) | |
130 if index == 0: | |
131 # Success | |
132 device_port = int(self._host_process.match.group(1)) | |
133 host_port = int(self._host_process.match.group(2)) | |
134 self._host_to_device_port_map[host_port] = device_port | |
135 logging.info("Forwarding device port: %d to host port: %d." % | |
136 (device_port, host_port)) | |
137 elif index == 1: | |
138 # Failure | |
139 device_port = int(self._host_process.match.group(1)) | |
140 host_port = int(self._host_process.match.group(2)) | |
141 self._CloseProcess() | |
142 raise Exception('Failed to forward port %d to %d' % (device_port, | |
143 host_port)) | |
144 elif index == 2: | |
145 logging.error(self._host_process.before) | |
146 self._CloseProcess() | |
147 raise Exception('Unexpected EOF while trying to forward ports %s' % | |
148 port_pairs) | |
149 elif index == 3: | |
150 logging.error(self._host_process.before) | |
151 self._CloseProcess() | |
152 raise Exception('Timeout while trying to forward ports %s' % port_pairs) | |
153 | |
154 def _KillHostForwarderBlocking(self, host_pattern, timeout_sec): | |
155 """Kills any existing host forwarders using the provided pattern. | |
156 | |
157 Note that this waits until the process terminates. | |
158 """ | |
159 cmd_helper.RunCmd(['pkill', '-f', host_pattern]) | |
160 elapsed = 0 | |
161 wait_period = 0.1 | |
162 while not cmd_helper.RunCmd(['pgrep', '-f', host_pattern]) and ( | |
163 elapsed < timeout_sec): | |
164 time.sleep(wait_period) | |
165 elapsed += wait_period | |
166 if elapsed >= timeout_sec: | |
167 raise Exception('Timed out while killing ' + host_pattern) | |
168 | |
169 def _KillDeviceForwarderBlocking(self, timeout_sec): | |
170 """Kills any existing device forwarders. | |
171 | |
172 Note that this waits until the process terminates. | |
173 """ | |
174 processes_killed = self._adb.KillAllBlocking( | |
175 'device_forwarder', timeout_sec) | |
176 if not processes_killed: | 149 if not processes_killed: |
177 pids = self._adb.ExtractPid('device_forwarder') | 150 pids = adb.ExtractPid('device_forwarder') |
178 if pids: | 151 if pids: |
179 raise Exception('Timed out while killing device_forwarder') | 152 raise Exception('Timed out while killing device_forwarder') |
180 | 153 |
181 def _CloseProcess(self): | |
182 if self._host_process: | |
183 self._host_process.close() | |
184 if self._device_process: | |
185 self._device_process.close() | |
186 if self._adb_forward_process: | |
187 self._adb_forward_process.close() | |
188 self._host_process = None | |
189 self._device_process = None | |
190 self._adb_forward_process = None | |
191 | |
192 def DevicePortForHostPort(self, host_port): | 154 def DevicePortForHostPort(self, host_port): |
193 """Get the device port that corresponds to a given host port.""" | 155 """Get the device port that corresponds to a given host port.""" |
194 return self._host_to_device_port_map.get(host_port) | 156 return self._host_to_device_port_map.get(host_port) |
195 | 157 |
196 def Close(self): | 158 def Close(self): |
197 """Terminate the forwarder process.""" | 159 """Terminate the forwarder process.""" |
198 self._CloseProcess() | 160 if self._device_process: |
| 161 self._device_process.close() |
| 162 self._device_process = None |
OLD | NEW |