1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/media/webrtc/trunk/build/android/pylib/chrome_test_server_spawner.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,402 @@ 1.4 +# Copyright (c) 2012 The Chromium Authors. All rights reserved. 1.5 +# Use of this source code is governed by a BSD-style license that can be 1.6 +# found in the LICENSE file. 1.7 + 1.8 +"""A "Test Server Spawner" that handles killing/stopping per-test test servers. 1.9 + 1.10 +It's used to accept requests from the device to spawn and kill instances of the 1.11 +chrome test server on the host. 1.12 +""" 1.13 + 1.14 +import BaseHTTPServer 1.15 +import json 1.16 +import logging 1.17 +import os 1.18 +import select 1.19 +import struct 1.20 +import subprocess 1.21 +import threading 1.22 +import time 1.23 +import urlparse 1.24 + 1.25 +import constants 1.26 +from forwarder import Forwarder 1.27 +import ports 1.28 + 1.29 + 1.30 +# Path that are needed to import necessary modules when running testserver.py. 1.31 +os.environ['PYTHONPATH'] = os.environ.get('PYTHONPATH', '') + ':%s:%s:%s:%s' % ( 1.32 + os.path.join(constants.CHROME_DIR, 'third_party'), 1.33 + os.path.join(constants.CHROME_DIR, 'third_party', 'tlslite'), 1.34 + os.path.join(constants.CHROME_DIR, 'third_party', 'pyftpdlib', 'src'), 1.35 + os.path.join(constants.CHROME_DIR, 'net', 'tools', 'testserver')) 1.36 + 1.37 + 1.38 +SERVER_TYPES = { 1.39 + 'http': '', 1.40 + 'ftp': '-f', 1.41 + 'sync': '--sync', 1.42 + 'tcpecho': '--tcp-echo', 1.43 + 'udpecho': '--udp-echo', 1.44 +} 1.45 + 1.46 + 1.47 +# The timeout (in seconds) of starting up the Python test server. 1.48 +TEST_SERVER_STARTUP_TIMEOUT = 10 1.49 + 1.50 + 1.51 +def _CheckPortStatus(port, expected_status): 1.52 + """Returns True if port has expected_status. 1.53 + 1.54 + Args: 1.55 + port: the port number. 1.56 + expected_status: boolean of expected status. 1.57 + 1.58 + Returns: 1.59 + Returns True if the status is expected. Otherwise returns False. 1.60 + """ 1.61 + for timeout in range(1, 5): 1.62 + if ports.IsHostPortUsed(port) == expected_status: 1.63 + return True 1.64 + time.sleep(timeout) 1.65 + return False 1.66 + 1.67 + 1.68 +def _GetServerTypeCommandLine(server_type): 1.69 + """Returns the command-line by the given server type. 1.70 + 1.71 + Args: 1.72 + server_type: the server type to be used (e.g. 'http'). 1.73 + 1.74 + Returns: 1.75 + A string containing the command-line argument. 1.76 + """ 1.77 + if server_type not in SERVER_TYPES: 1.78 + raise NotImplementedError('Unknown server type: %s' % server_type) 1.79 + if server_type == 'udpecho': 1.80 + raise Exception('Please do not run UDP echo tests because we do not have ' 1.81 + 'a UDP forwarder tool.') 1.82 + return SERVER_TYPES[server_type] 1.83 + 1.84 + 1.85 +class TestServerThread(threading.Thread): 1.86 + """A thread to run the test server in a separate process.""" 1.87 + 1.88 + def __init__(self, ready_event, arguments, adb, tool, build_type): 1.89 + """Initialize TestServerThread with the following argument. 1.90 + 1.91 + Args: 1.92 + ready_event: event which will be set when the test server is ready. 1.93 + arguments: dictionary of arguments to run the test server. 1.94 + adb: instance of AndroidCommands. 1.95 + tool: instance of runtime error detection tool. 1.96 + build_type: 'Release' or 'Debug'. 1.97 + """ 1.98 + threading.Thread.__init__(self) 1.99 + self.wait_event = threading.Event() 1.100 + self.stop_flag = False 1.101 + self.ready_event = ready_event 1.102 + self.ready_event.clear() 1.103 + self.arguments = arguments 1.104 + self.adb = adb 1.105 + self.tool = tool 1.106 + self.test_server_process = None 1.107 + self.is_ready = False 1.108 + self.host_port = self.arguments['port'] 1.109 + assert isinstance(self.host_port, int) 1.110 + self._test_server_forwarder = None 1.111 + # The forwarder device port now is dynamically allocated. 1.112 + self.forwarder_device_port = 0 1.113 + # Anonymous pipe in order to get port info from test server. 1.114 + self.pipe_in = None 1.115 + self.pipe_out = None 1.116 + self.command_line = [] 1.117 + self.build_type = build_type 1.118 + 1.119 + def _WaitToStartAndGetPortFromTestServer(self): 1.120 + """Waits for the Python test server to start and gets the port it is using. 1.121 + 1.122 + The port information is passed by the Python test server with a pipe given 1.123 + by self.pipe_out. It is written as a result to |self.host_port|. 1.124 + 1.125 + Returns: 1.126 + Whether the port used by the test server was successfully fetched. 1.127 + """ 1.128 + assert self.host_port == 0 and self.pipe_out and self.pipe_in 1.129 + (in_fds, _, _) = select.select([self.pipe_in, ], [], [], 1.130 + TEST_SERVER_STARTUP_TIMEOUT) 1.131 + if len(in_fds) == 0: 1.132 + logging.error('Failed to wait to the Python test server to be started.') 1.133 + return False 1.134 + # First read the data length as an unsigned 4-byte value. This 1.135 + # is _not_ using network byte ordering since the Python test server packs 1.136 + # size as native byte order and all Chromium platforms so far are 1.137 + # configured to use little-endian. 1.138 + # TODO(jnd): Change the Python test server and local_test_server_*.cc to 1.139 + # use a unified byte order (either big-endian or little-endian). 1.140 + data_length = os.read(self.pipe_in, struct.calcsize('=L')) 1.141 + if data_length: 1.142 + (data_length,) = struct.unpack('=L', data_length) 1.143 + assert data_length 1.144 + if not data_length: 1.145 + logging.error('Failed to get length of server data.') 1.146 + return False 1.147 + port_json = os.read(self.pipe_in, data_length) 1.148 + if not port_json: 1.149 + logging.error('Failed to get server data.') 1.150 + return False 1.151 + logging.info('Got port json data: %s', port_json) 1.152 + port_json = json.loads(port_json) 1.153 + if port_json.has_key('port') and isinstance(port_json['port'], int): 1.154 + self.host_port = port_json['port'] 1.155 + return _CheckPortStatus(self.host_port, True) 1.156 + logging.error('Failed to get port information from the server data.') 1.157 + return False 1.158 + 1.159 + def _GenerateCommandLineArguments(self): 1.160 + """Generates the command line to run the test server. 1.161 + 1.162 + Note that all options are processed by following the definitions in 1.163 + testserver.py. 1.164 + """ 1.165 + if self.command_line: 1.166 + return 1.167 + # The following arguments must exist. 1.168 + type_cmd = _GetServerTypeCommandLine(self.arguments['server-type']) 1.169 + if type_cmd: 1.170 + self.command_line.append(type_cmd) 1.171 + self.command_line.append('--port=%d' % self.host_port) 1.172 + # Use a pipe to get the port given by the instance of Python test server 1.173 + # if the test does not specify the port. 1.174 + if self.host_port == 0: 1.175 + (self.pipe_in, self.pipe_out) = os.pipe() 1.176 + self.command_line.append('--startup-pipe=%d' % self.pipe_out) 1.177 + self.command_line.append('--host=%s' % self.arguments['host']) 1.178 + data_dir = self.arguments['data-dir'] or 'chrome/test/data' 1.179 + if not os.path.isabs(data_dir): 1.180 + data_dir = os.path.join(constants.CHROME_DIR, data_dir) 1.181 + self.command_line.append('--data-dir=%s' % data_dir) 1.182 + # The following arguments are optional depending on the individual test. 1.183 + if self.arguments.has_key('log-to-console'): 1.184 + self.command_line.append('--log-to-console') 1.185 + if self.arguments.has_key('auth-token'): 1.186 + self.command_line.append('--auth-token=%s' % self.arguments['auth-token']) 1.187 + if self.arguments.has_key('https'): 1.188 + self.command_line.append('--https') 1.189 + if self.arguments.has_key('cert-and-key-file'): 1.190 + self.command_line.append('--cert-and-key-file=%s' % os.path.join( 1.191 + constants.CHROME_DIR, self.arguments['cert-and-key-file'])) 1.192 + if self.arguments.has_key('ocsp'): 1.193 + self.command_line.append('--ocsp=%s' % self.arguments['ocsp']) 1.194 + if self.arguments.has_key('https-record-resume'): 1.195 + self.command_line.append('--https-record-resume') 1.196 + if self.arguments.has_key('ssl-client-auth'): 1.197 + self.command_line.append('--ssl-client-auth') 1.198 + if self.arguments.has_key('tls-intolerant'): 1.199 + self.command_line.append('--tls-intolerant=%s' % 1.200 + self.arguments['tls-intolerant']) 1.201 + if self.arguments.has_key('ssl-client-ca'): 1.202 + for ca in self.arguments['ssl-client-ca']: 1.203 + self.command_line.append('--ssl-client-ca=%s' % 1.204 + os.path.join(constants.CHROME_DIR, ca)) 1.205 + if self.arguments.has_key('ssl-bulk-cipher'): 1.206 + for bulk_cipher in self.arguments['ssl-bulk-cipher']: 1.207 + self.command_line.append('--ssl-bulk-cipher=%s' % bulk_cipher) 1.208 + 1.209 + def run(self): 1.210 + logging.info('Start running the thread!') 1.211 + self.wait_event.clear() 1.212 + self._GenerateCommandLineArguments() 1.213 + command = [os.path.join(constants.CHROME_DIR, 'net', 'tools', 1.214 + 'testserver', 'testserver.py')] + self.command_line 1.215 + logging.info('Running: %s', command) 1.216 + self.process = subprocess.Popen(command) 1.217 + if self.process: 1.218 + if self.pipe_out: 1.219 + self.is_ready = self._WaitToStartAndGetPortFromTestServer() 1.220 + else: 1.221 + self.is_ready = _CheckPortStatus(self.host_port, True) 1.222 + if self.is_ready: 1.223 + self._test_server_forwarder = Forwarder( 1.224 + self.adb, [(0, self.host_port)], self.tool, '127.0.0.1', 1.225 + self.build_type) 1.226 + # Check whether the forwarder is ready on the device. 1.227 + self.is_ready = False 1.228 + device_port = self._test_server_forwarder.DevicePortForHostPort( 1.229 + self.host_port) 1.230 + if device_port: 1.231 + for timeout in range(1, 5): 1.232 + if ports.IsDevicePortUsed(self.adb, device_port, 'LISTEN'): 1.233 + self.is_ready = True 1.234 + self.forwarder_device_port = device_port 1.235 + break 1.236 + time.sleep(timeout) 1.237 + # Wake up the request handler thread. 1.238 + self.ready_event.set() 1.239 + # Keep thread running until Stop() gets called. 1.240 + while not self.stop_flag: 1.241 + time.sleep(1) 1.242 + if self.process.poll() is None: 1.243 + self.process.kill() 1.244 + if self._test_server_forwarder: 1.245 + self._test_server_forwarder.Close() 1.246 + self.process = None 1.247 + self.is_ready = False 1.248 + if self.pipe_out: 1.249 + os.close(self.pipe_in) 1.250 + os.close(self.pipe_out) 1.251 + self.pipe_in = None 1.252 + self.pipe_out = None 1.253 + logging.info('Test-server has died.') 1.254 + self.wait_event.set() 1.255 + 1.256 + def Stop(self): 1.257 + """Blocks until the loop has finished. 1.258 + 1.259 + Note that this must be called in another thread. 1.260 + """ 1.261 + if not self.process: 1.262 + return 1.263 + self.stop_flag = True 1.264 + self.wait_event.wait() 1.265 + 1.266 + 1.267 +class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 1.268 + """A handler used to process http GET/POST request.""" 1.269 + 1.270 + def _SendResponse(self, response_code, response_reason, additional_headers, 1.271 + contents): 1.272 + """Generates a response sent to the client from the provided parameters. 1.273 + 1.274 + Args: 1.275 + response_code: number of the response status. 1.276 + response_reason: string of reason description of the response. 1.277 + additional_headers: dict of additional headers. Each key is the name of 1.278 + the header, each value is the content of the header. 1.279 + contents: string of the contents we want to send to client. 1.280 + """ 1.281 + self.send_response(response_code, response_reason) 1.282 + self.send_header('Content-Type', 'text/html') 1.283 + # Specify the content-length as without it the http(s) response will not 1.284 + # be completed properly (and the browser keeps expecting data). 1.285 + self.send_header('Content-Length', len(contents)) 1.286 + for header_name in additional_headers: 1.287 + self.send_header(header_name, additional_headers[header_name]) 1.288 + self.end_headers() 1.289 + self.wfile.write(contents) 1.290 + self.wfile.flush() 1.291 + 1.292 + def _StartTestServer(self): 1.293 + """Starts the test server thread.""" 1.294 + logging.info('Handling request to spawn a test server.') 1.295 + content_type = self.headers.getheader('content-type') 1.296 + if content_type != 'application/json': 1.297 + raise Exception('Bad content-type for start request.') 1.298 + content_length = self.headers.getheader('content-length') 1.299 + if not content_length: 1.300 + content_length = 0 1.301 + try: 1.302 + content_length = int(content_length) 1.303 + except: 1.304 + raise Exception('Bad content-length for start request.') 1.305 + logging.info(content_length) 1.306 + test_server_argument_json = self.rfile.read(content_length) 1.307 + logging.info(test_server_argument_json) 1.308 + assert not self.server.test_server_instance 1.309 + ready_event = threading.Event() 1.310 + self.server.test_server_instance = TestServerThread( 1.311 + ready_event, 1.312 + json.loads(test_server_argument_json), 1.313 + self.server.adb, 1.314 + self.server.tool, 1.315 + self.server.build_type) 1.316 + self.server.test_server_instance.setDaemon(True) 1.317 + self.server.test_server_instance.start() 1.318 + ready_event.wait() 1.319 + if self.server.test_server_instance.is_ready: 1.320 + self._SendResponse(200, 'OK', {}, json.dumps( 1.321 + {'port': self.server.test_server_instance.forwarder_device_port, 1.322 + 'message': 'started'})) 1.323 + logging.info('Test server is running on port: %d.', 1.324 + self.server.test_server_instance.host_port) 1.325 + else: 1.326 + self.server.test_server_instance.Stop() 1.327 + self.server.test_server_instance = None 1.328 + self._SendResponse(500, 'Test Server Error.', {}, '') 1.329 + logging.info('Encounter problem during starting a test server.') 1.330 + 1.331 + def _KillTestServer(self): 1.332 + """Stops the test server instance.""" 1.333 + # There should only ever be one test server at a time. This may do the 1.334 + # wrong thing if we try and start multiple test servers. 1.335 + if not self.server.test_server_instance: 1.336 + return 1.337 + port = self.server.test_server_instance.host_port 1.338 + logging.info('Handling request to kill a test server on port: %d.', port) 1.339 + self.server.test_server_instance.Stop() 1.340 + # Make sure the status of test server is correct before sending response. 1.341 + if _CheckPortStatus(port, False): 1.342 + self._SendResponse(200, 'OK', {}, 'killed') 1.343 + logging.info('Test server on port %d is killed', port) 1.344 + else: 1.345 + self._SendResponse(500, 'Test Server Error.', {}, '') 1.346 + logging.info('Encounter problem during killing a test server.') 1.347 + self.server.test_server_instance = None 1.348 + 1.349 + def do_POST(self): 1.350 + parsed_path = urlparse.urlparse(self.path) 1.351 + action = parsed_path.path 1.352 + logging.info('Action for POST method is: %s.', action) 1.353 + if action == '/start': 1.354 + self._StartTestServer() 1.355 + else: 1.356 + self._SendResponse(400, 'Unknown request.', {}, '') 1.357 + logging.info('Encounter unknown request: %s.', action) 1.358 + 1.359 + def do_GET(self): 1.360 + parsed_path = urlparse.urlparse(self.path) 1.361 + action = parsed_path.path 1.362 + params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1) 1.363 + logging.info('Action for GET method is: %s.', action) 1.364 + for param in params: 1.365 + logging.info('%s=%s', param, params[param][0]) 1.366 + if action == '/kill': 1.367 + self._KillTestServer() 1.368 + elif action == '/ping': 1.369 + # The ping handler is used to check whether the spawner server is ready 1.370 + # to serve the requests. We don't need to test the status of the test 1.371 + # server when handling ping request. 1.372 + self._SendResponse(200, 'OK', {}, 'ready') 1.373 + logging.info('Handled ping request and sent response.') 1.374 + else: 1.375 + self._SendResponse(400, 'Unknown request', {}, '') 1.376 + logging.info('Encounter unknown request: %s.', action) 1.377 + 1.378 + 1.379 +class SpawningServer(object): 1.380 + """The class used to start/stop a http server.""" 1.381 + 1.382 + def __init__(self, test_server_spawner_port, adb, tool, build_type): 1.383 + logging.info('Creating new spawner on port: %d.', test_server_spawner_port) 1.384 + self.server = BaseHTTPServer.HTTPServer(('', test_server_spawner_port), 1.385 + SpawningServerRequestHandler) 1.386 + self.port = test_server_spawner_port 1.387 + self.server.adb = adb 1.388 + self.server.tool = tool 1.389 + self.server.test_server_instance = None 1.390 + self.server.build_type = build_type 1.391 + 1.392 + def _Listen(self): 1.393 + logging.info('Starting test server spawner') 1.394 + self.server.serve_forever() 1.395 + 1.396 + def Start(self): 1.397 + listener_thread = threading.Thread(target=self._Listen) 1.398 + listener_thread.setDaemon(True) 1.399 + listener_thread.start() 1.400 + time.sleep(1) 1.401 + 1.402 + def Stop(self): 1.403 + if self.server.test_server_instance: 1.404 + self.server.test_server_instance.Stop() 1.405 + self.server.shutdown()