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 """A "Test Server Spawner" that handles killing/stopping per-test test servers. | 5 """A "Test Server Spawner" that handles killing/stopping per-test test servers. |
6 | 6 |
7 It's used to accept requests from the device to spawn and kill instances of the | 7 It's used to accept requests from the device to spawn and kill instances of the |
8 chrome test server on the host. | 8 chrome test server on the host. |
9 """ | 9 """ |
10 | 10 |
11 import BaseHTTPServer | 11 import BaseHTTPServer |
| 12 import json |
12 import logging | 13 import logging |
13 import os | 14 import os |
14 import sys | 15 import select |
| 16 import struct |
| 17 import subprocess |
15 import threading | 18 import threading |
16 import time | 19 import time |
17 import urlparse | 20 import urlparse |
18 | 21 |
19 # Path that are needed to import testserver | 22 import constants |
20 cr_src = os.path.join(os.path.abspath(os.path.dirname(__file__)), | 23 from forwarder import Forwarder |
21 '..', '..', '..') | 24 import ports |
22 sys.path.append(os.path.join(cr_src, 'third_party')) | 25 |
23 sys.path.append(os.path.join(cr_src, 'third_party', 'tlslite')) | 26 |
24 sys.path.append(os.path.join(cr_src, 'third_party', 'pyftpdlib', 'src')) | 27 # Path that are needed to import necessary modules when running testserver.py. |
25 sys.path.append(os.path.join(cr_src, 'net', 'tools', 'testserver')) | 28 os.environ['PYTHONPATH'] += ':%s:%s:%s:%s' % ( |
26 import testserver | 29 os.path.join(constants.CHROME_DIR, 'third_party'), |
27 | 30 os.path.join(constants.CHROME_DIR, 'third_party', 'tlslite'), |
28 _test_servers = [] | 31 os.path.join(constants.CHROME_DIR, 'third_party', 'pyftpdlib', 'src'), |
| 32 os.path.join(constants.CHROME_DIR, 'net', 'tools', 'testserver')) |
| 33 |
| 34 |
| 35 SERVER_TYPES = { |
| 36 'http': '', |
| 37 'ftp': '-f', |
| 38 'sync': '--sync', |
| 39 'tcpecho': '--tcp-echo', |
| 40 'udpecho': '--udp-echo', |
| 41 } |
| 42 |
| 43 |
| 44 # The timeout (in seconds) of starting up the Python test server. |
| 45 TEST_SERVER_STARTUP_TIMEOUT = 10 |
| 46 |
| 47 |
| 48 def _CheckPortStatus(port, expected_status): |
| 49 """Returns True if port has expected_status. |
| 50 |
| 51 Args: |
| 52 port: the port number. |
| 53 expected_status: boolean of expected status. |
| 54 |
| 55 Returns: |
| 56 Returns True if the status is expected. Otherwise returns False. |
| 57 """ |
| 58 for timeout in range(1, 5): |
| 59 if ports.IsHostPortUsed(port) == expected_status: |
| 60 return True |
| 61 time.sleep(timeout) |
| 62 return False |
| 63 |
| 64 |
| 65 def _GetServerTypeCommandLine(server_type): |
| 66 """Returns the command-line by the given server type. |
| 67 |
| 68 Args: |
| 69 server_type: the server type to be used (e.g. 'http'). |
| 70 |
| 71 Returns: |
| 72 A string containing the command-line argument. |
| 73 """ |
| 74 if server_type not in SERVER_TYPES: |
| 75 raise NotImplementedError('Unknown server type: %s' % server_type) |
| 76 if server_type == 'udpecho': |
| 77 raise Exception('Please do not run UDP echo tests because we do not have ' |
| 78 'a UDP forwarder tool.') |
| 79 return SERVER_TYPES[server_type] |
| 80 |
| 81 |
| 82 class TestServerThread(threading.Thread): |
| 83 """A thread to run the test server in a separate process.""" |
| 84 |
| 85 def __init__(self, ready_event, arguments, adb, tool, build_type): |
| 86 """Initialize TestServerThread with the following argument. |
| 87 |
| 88 Args: |
| 89 ready_event: event which will be set when the test server is ready. |
| 90 arguments: dictionary of arguments to run the test server. |
| 91 adb: instance of AndroidCommands. |
| 92 tool: instance of runtime error detection tool. |
| 93 build_type: 'Release' or 'Debug'. |
| 94 """ |
| 95 threading.Thread.__init__(self) |
| 96 self.wait_event = threading.Event() |
| 97 self.stop_flag = False |
| 98 self.ready_event = ready_event |
| 99 self.ready_event.clear() |
| 100 self.arguments = arguments |
| 101 self.adb = adb |
| 102 self.tool = tool |
| 103 self.test_server_process = None |
| 104 self.is_ready = False |
| 105 self.host_port = self.arguments['port'] |
| 106 assert isinstance(self.host_port, int) |
| 107 self._test_server_forwarder = None |
| 108 # The forwarder device port now is dynamically allocated. |
| 109 self.forwarder_device_port = 0 |
| 110 # Anonymous pipe in order to get port info from test server. |
| 111 self.pipe_in = None |
| 112 self.pipe_out = None |
| 113 self.command_line = [] |
| 114 self.build_type = build_type |
| 115 |
| 116 def _WaitToStartAndGetPortFromTestServer(self): |
| 117 """Waits for the Python test server to start and gets the port it is using. |
| 118 |
| 119 The port information is passed by the Python test server with a pipe given |
| 120 by self.pipe_out. It is written as a result to |self.host_port|. |
| 121 |
| 122 Returns: |
| 123 Whether the port used by the test server was successfully fetched. |
| 124 """ |
| 125 assert self.host_port == 0 and self.pipe_out and self.pipe_in |
| 126 (in_fds, _, _) = select.select([self.pipe_in, ], [], [], |
| 127 TEST_SERVER_STARTUP_TIMEOUT) |
| 128 if len(in_fds) == 0: |
| 129 logging.error('Failed to wait to the Python test server to be started.') |
| 130 return False |
| 131 # First read the data length as an unsigned 4-byte value. This |
| 132 # is _not_ using network byte ordering since the Python test server packs |
| 133 # size as native byte order and all Chromium platforms so far are |
| 134 # configured to use little-endian. |
| 135 # TODO(jnd): Change the Python test server and local_test_server_*.cc to |
| 136 # use a unified byte order (either big-endian or little-endian). |
| 137 data_length = os.read(self.pipe_in, struct.calcsize('=L')) |
| 138 if data_length: |
| 139 (data_length,) = struct.unpack('=L', data_length) |
| 140 assert data_length |
| 141 if not data_length: |
| 142 logging.error('Failed to get length of server data.') |
| 143 return False |
| 144 port_json = os.read(self.pipe_in, data_length) |
| 145 if not port_json: |
| 146 logging.error('Failed to get server data.') |
| 147 return False |
| 148 logging.info('Got port json data: %s', port_json) |
| 149 port_json = json.loads(port_json) |
| 150 if port_json.has_key('port') and isinstance(port_json['port'], int): |
| 151 self.host_port = port_json['port'] |
| 152 return _CheckPortStatus(self.host_port, True) |
| 153 logging.error('Failed to get port information from the server data.') |
| 154 return False |
| 155 |
| 156 def _GenerateCommandLineArguments(self): |
| 157 """Generates the command line to run the test server. |
| 158 |
| 159 Note that all options are processed by following the definitions in |
| 160 testserver.py. |
| 161 """ |
| 162 if self.command_line: |
| 163 return |
| 164 # The following arguments must exist. |
| 165 type_cmd = _GetServerTypeCommandLine(self.arguments['server-type']) |
| 166 if type_cmd: |
| 167 self.command_line.append(type_cmd) |
| 168 self.command_line.append('--port=%d' % self.host_port) |
| 169 # Use a pipe to get the port given by the instance of Python test server |
| 170 # if the test does not specify the port. |
| 171 if self.host_port == 0: |
| 172 (self.pipe_in, self.pipe_out) = os.pipe() |
| 173 self.command_line.append('--startup-pipe=%d' % self.pipe_out) |
| 174 self.command_line.append('--host=%s' % self.arguments['host']) |
| 175 data_dir = self.arguments['data-dir'] or 'chrome/test/data' |
| 176 if not os.path.isabs(data_dir): |
| 177 data_dir = os.path.join(constants.CHROME_DIR, data_dir) |
| 178 self.command_line.append('--data-dir=%s' % data_dir) |
| 179 # The following arguments are optional depending on the individual test. |
| 180 if self.arguments.has_key('log-to-console'): |
| 181 self.command_line.append('--log-to-console') |
| 182 if self.arguments.has_key('auth-token'): |
| 183 self.command_line.append('--auth-token=%s' % self.arguments['auth-token']) |
| 184 if self.arguments.has_key('https'): |
| 185 self.command_line.append('--https') |
| 186 if self.arguments.has_key('cert-and-key-file'): |
| 187 self.command_line.append('--cert-and-key-file=%s' % os.path.join( |
| 188 constants.CHROME_DIR, self.arguments['cert-and-key-file'])) |
| 189 if self.arguments.has_key('ocsp'): |
| 190 self.command_line.append('--ocsp=%s' % self.arguments['ocsp']) |
| 191 if self.arguments.has_key('https-record-resume'): |
| 192 self.command_line.append('--https-record-resume') |
| 193 if self.arguments.has_key('ssl-client-auth'): |
| 194 self.command_line.append('--ssl-client-auth') |
| 195 if self.arguments.has_key('tls-intolerant'): |
| 196 self.command_line.append('--tls-intolerant') |
| 197 if self.arguments.has_key('ssl-client-ca'): |
| 198 for ca in self.arguments['ssl-client-ca']: |
| 199 self.command_line.append('--ssl-client-ca=%s' % |
| 200 os.path.join(constants.CHROME_DIR, ca)) |
| 201 if self.arguments.has_key('ssl-bulk-cipher'): |
| 202 for bulk_cipher in self.arguments['ssl-bulk-cipher']: |
| 203 self.command_line.append('--ssl-bulk-cipher=%s' % bulk_cipher) |
| 204 |
| 205 def run(self): |
| 206 logging.info('Start running the thread!') |
| 207 self.wait_event.clear() |
| 208 self._GenerateCommandLineArguments() |
| 209 command = '%s %s' % ( |
| 210 os.path.join(constants.CHROME_DIR, 'net', 'tools', 'testserver', |
| 211 'testserver.py'), |
| 212 ' '.join(self.command_line)) |
| 213 logging.info(command) |
| 214 self.process = subprocess.Popen(command, shell=True) |
| 215 if self.process: |
| 216 if self.pipe_out: |
| 217 self.is_ready = self._WaitToStartAndGetPortFromTestServer() |
| 218 else: |
| 219 self.is_ready = _CheckPortStatus(self.host_port, True) |
| 220 if self.is_ready: |
| 221 self._test_server_forwarder = Forwarder( |
| 222 self.adb, [(0, self.host_port)], self.tool, '127.0.0.1', |
| 223 self.build_type) |
| 224 # Check whether the forwarder is ready on the device. |
| 225 self.is_ready = False |
| 226 device_port = self._test_server_forwarder.DevicePortForHostPort( |
| 227 self.host_port) |
| 228 if device_port: |
| 229 for timeout in range(1, 5): |
| 230 if ports.IsDevicePortUsed(self.adb, device_port, 'LISTEN'): |
| 231 self.is_ready = True |
| 232 self.forwarder_device_port = device_port |
| 233 break |
| 234 time.sleep(timeout) |
| 235 # Wake up the request handler thread. |
| 236 self.ready_event.set() |
| 237 # Keep thread running until Stop() gets called. |
| 238 while not self.stop_flag: |
| 239 time.sleep(1) |
| 240 if self.process.poll() is None: |
| 241 self.process.kill() |
| 242 if self._test_server_forwarder: |
| 243 self._test_server_forwarder.Close() |
| 244 self.process = None |
| 245 self.is_ready = False |
| 246 if self.pipe_out: |
| 247 os.close(self.pipe_in) |
| 248 os.close(self.pipe_out) |
| 249 self.pipe_in = None |
| 250 self.pipe_out = None |
| 251 logging.info('Test-server has died.') |
| 252 self.wait_event.set() |
| 253 |
| 254 def Stop(self): |
| 255 """Blocks until the loop has finished. |
| 256 |
| 257 Note that this must be called in another thread. |
| 258 """ |
| 259 if not self.process: |
| 260 return |
| 261 self.stop_flag = True |
| 262 self.wait_event.wait() |
| 263 |
29 | 264 |
30 class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): | 265 class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
31 """A handler used to process http GET request. | 266 """A handler used to process http GET/POST request.""" |
32 """ | 267 |
33 | 268 def _SendResponse(self, response_code, response_reason, additional_headers, |
34 def GetServerType(self, server_type): | 269 contents): |
35 """Returns the server type to use when starting the test server. | 270 """Generates a response sent to the client from the provided parameters. |
36 | 271 |
37 This function translate the command-line argument into the appropriate | 272 Args: |
38 numerical constant. | 273 response_code: number of the response status. |
39 # TODO(yfriedman): Do that translation! | 274 response_reason: string of reason description of the response. |
40 """ | 275 additional_headers: dict of additional headers. Each key is the name of |
41 if server_type: | 276 the header, each value is the content of the header. |
42 pass | 277 contents: string of the contents we want to send to client. |
43 return 0 | 278 """ |
| 279 self.send_response(response_code, response_reason) |
| 280 self.send_header('Content-Type', 'text/html') |
| 281 # Specify the content-length as without it the http(s) response will not |
| 282 # be completed properly (and the browser keeps expecting data). |
| 283 self.send_header('Content-Length', len(contents)) |
| 284 for header_name in additional_headers: |
| 285 self.send_header(header_name, additional_headers[header_name]) |
| 286 self.end_headers() |
| 287 self.wfile.write(contents) |
| 288 self.wfile.flush() |
| 289 |
| 290 def _StartTestServer(self): |
| 291 """Starts the test server thread.""" |
| 292 logging.info('Handling request to spawn a test server.') |
| 293 content_type = self.headers.getheader('content-type') |
| 294 if content_type != 'application/json': |
| 295 raise Exception('Bad content-type for start request.') |
| 296 content_length = self.headers.getheader('content-length') |
| 297 if not content_length: |
| 298 content_length = 0 |
| 299 try: |
| 300 content_length = int(content_length) |
| 301 except: |
| 302 raise Exception('Bad content-length for start request.') |
| 303 logging.info(content_length) |
| 304 test_server_argument_json = self.rfile.read(content_length) |
| 305 logging.info(test_server_argument_json) |
| 306 assert not self.server.test_server_instance |
| 307 ready_event = threading.Event() |
| 308 self.server.test_server_instance = TestServerThread( |
| 309 ready_event, |
| 310 json.loads(test_server_argument_json), |
| 311 self.server.adb, |
| 312 self.server.tool, |
| 313 self.server.build_type) |
| 314 self.server.test_server_instance.setDaemon(True) |
| 315 self.server.test_server_instance.start() |
| 316 ready_event.wait() |
| 317 if self.server.test_server_instance.is_ready: |
| 318 self._SendResponse(200, 'OK', {}, json.dumps( |
| 319 {'port': self.server.test_server_instance.forwarder_device_port, |
| 320 'message': 'started'})) |
| 321 logging.info('Test server is running on port: %d.', |
| 322 self.server.test_server_instance.host_port) |
| 323 else: |
| 324 self.server.test_server_instance.Stop() |
| 325 self.server.test_server_instance = None |
| 326 self._SendResponse(500, 'Test Server Error.', {}, '') |
| 327 logging.info('Encounter problem during starting a test server.') |
| 328 |
| 329 def _KillTestServer(self): |
| 330 """Stops the test server instance.""" |
| 331 # There should only ever be one test server at a time. This may do the |
| 332 # wrong thing if we try and start multiple test servers. |
| 333 if not self.server.test_server_instance: |
| 334 return |
| 335 port = self.server.test_server_instance.host_port |
| 336 logging.info('Handling request to kill a test server on port: %d.', port) |
| 337 self.server.test_server_instance.Stop() |
| 338 # Make sure the status of test server is correct before sending response. |
| 339 if _CheckPortStatus(port, False): |
| 340 self._SendResponse(200, 'OK', {}, 'killed') |
| 341 logging.info('Test server on port %d is killed', port) |
| 342 else: |
| 343 self._SendResponse(500, 'Test Server Error.', {}, '') |
| 344 logging.info('Encounter problem during killing a test server.') |
| 345 self.server.test_server_instance = None |
| 346 |
| 347 def do_POST(self): |
| 348 parsed_path = urlparse.urlparse(self.path) |
| 349 action = parsed_path.path |
| 350 logging.info('Action for POST method is: %s.', action) |
| 351 if action == '/start': |
| 352 self._StartTestServer() |
| 353 else: |
| 354 self._SendResponse(400, 'Unknown request.', {}, '') |
| 355 logging.info('Encounter unknown request: %s.', action) |
44 | 356 |
45 def do_GET(self): | 357 def do_GET(self): |
46 parsed_path = urlparse.urlparse(self.path) | 358 parsed_path = urlparse.urlparse(self.path) |
47 action = parsed_path.path | 359 action = parsed_path.path |
48 params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1) | 360 params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1) |
49 logging.info('Action is: %s' % action) | 361 logging.info('Action for GET method is: %s.', action) |
50 if action == '/killserver': | 362 for param in params: |
51 # There should only ever be one test server at a time. This may do the | 363 logging.info('%s=%s', param, params[param][0]) |
52 # wrong thing if we try and start multiple test servers. | 364 if action == '/kill': |
53 _test_servers.pop().Stop() | 365 self._KillTestServer() |
54 elif action == '/start': | 366 elif action == '/ping': |
55 logging.info('Handling request to spawn a test webserver') | 367 # The ping handler is used to check whether the spawner server is ready |
56 for param in params: | 368 # to serve the requests. We don't need to test the status of the test |
57 logging.info('%s=%s' % (param, params[param][0])) | 369 # server when handling ping request. |
58 s_type = 0 | 370 self._SendResponse(200, 'OK', {}, 'ready') |
59 doc_root = None | 371 logging.info('Handled ping request and sent response.') |
60 if 'server_type' in params: | 372 else: |
61 s_type = self.GetServerType(params['server_type'][0]) | 373 self._SendResponse(400, 'Unknown request', {}, '') |
62 if 'doc_root' in params: | 374 logging.info('Encounter unknown request: %s.', action) |
63 doc_root = params['doc_root'][0] | |
64 self.webserver_thread = threading.Thread( | |
65 target=self.SpawnTestWebServer, args=(s_type, doc_root)) | |
66 self.webserver_thread.setDaemon(True) | |
67 self.webserver_thread.start() | |
68 self.send_response(200, 'OK') | |
69 self.send_header('Content-type', 'text/html') | |
70 self.end_headers() | |
71 self.wfile.write('<html><head><title>started</title></head></html>') | |
72 logging.info('Returned OK!!!') | |
73 | |
74 def SpawnTestWebServer(self, s_type, doc_root): | |
75 class Options(object): | |
76 log_to_console = True | |
77 server_type = s_type | |
78 port = self.server.test_server_port | |
79 data_dir = doc_root or 'chrome/test/data' | |
80 file_root_url = '/files/' | |
81 cert = False | |
82 policy_keys = None | |
83 policy_user = None | |
84 startup_pipe = None | |
85 options = Options() | |
86 logging.info('Listening on %d, type %d, data_dir %s' % (options.port, | |
87 options.server_type, options.data_dir)) | |
88 testserver.main(options, None, server_list=_test_servers) | |
89 logging.info('Test-server has died.') | |
90 | 375 |
91 | 376 |
92 class SpawningServer(object): | 377 class SpawningServer(object): |
93 """The class used to start/stop a http server. | 378 """The class used to start/stop a http server.""" |
94 """ | 379 |
95 | 380 def __init__(self, test_server_spawner_port, adb, tool, build_type): |
96 def __init__(self, test_server_spawner_port, test_server_port): | 381 logging.info('Creating new spawner on port: %d.', test_server_spawner_port) |
97 logging.info('Creating new spawner %d', test_server_spawner_port) | 382 self.server = BaseHTTPServer.HTTPServer(('', test_server_spawner_port), |
98 self.server = testserver.StoppableHTTPServer(('', test_server_spawner_port), | 383 SpawningServerRequestHandler) |
99 SpawningServerRequestHandler) | |
100 self.port = test_server_spawner_port | 384 self.port = test_server_spawner_port |
101 self.server.test_server_port = test_server_port | 385 self.server.adb = adb |
102 | 386 self.server.tool = tool |
103 def Listen(self): | 387 self.server.test_server_instance = None |
| 388 self.server.build_type = build_type |
| 389 |
| 390 def _Listen(self): |
104 logging.info('Starting test server spawner') | 391 logging.info('Starting test server spawner') |
105 self.server.serve_forever() | 392 self.server.serve_forever() |
106 | 393 |
107 def Start(self): | 394 def Start(self): |
108 listener_thread = threading.Thread(target=self.Listen) | 395 listener_thread = threading.Thread(target=self._Listen) |
109 listener_thread.setDaemon(True) | 396 listener_thread.setDaemon(True) |
110 listener_thread.start() | 397 listener_thread.start() |
111 time.sleep(1) | 398 time.sleep(1) |
112 | 399 |
113 def Stop(self): | 400 def Stop(self): |
114 self.server.Stop() | 401 if self.server.test_server_instance: |
| 402 self.server.test_server_instance.Stop() |
| 403 self.server.shutdown() |
OLD | NEW |