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