1 # Copyright 2013 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 """A "Test Server Spawner" that handles killing/stopping per-test test servers.
7 It's used to accept requests from the device to spawn and kill instances of the
8 chrome test server on the host.
26 from pylib.forwarder import Forwarder
28 # Path that are needed to import necessary modules when launching a testserver.
29 os.environ['PYTHONPATH'] = os.environ.get('PYTHONPATH', '') + (':%s:%s:%s:%s:%s'
30 % (os.path.join(constants.DIR_SOURCE_ROOT, 'third_party'),
31 os.path.join(constants.DIR_SOURCE_ROOT, 'third_party', 'tlslite'),
32 os.path.join(constants.DIR_SOURCE_ROOT, 'third_party', 'pyftpdlib',
34 os.path.join(constants.DIR_SOURCE_ROOT, 'net', 'tools', 'testserver'),
35 os.path.join(constants.DIR_SOURCE_ROOT, 'sync', 'tools', 'testserver')))
41 'sync': '', # Sync uses its own script, and doesn't take a server type arg.
42 'tcpecho': '--tcp-echo',
43 'udpecho': '--udp-echo',
47 # The timeout (in seconds) of starting up the Python test server.
48 TEST_SERVER_STARTUP_TIMEOUT = 10
50 def _WaitUntil(predicate, max_attempts=5):
51 """Blocks until the provided predicate (function) is true.
54 Whether the provided predicate was satisfied once (before the timeout).
56 sleep_time_sec = 0.025
57 for attempt in xrange(1, max_attempts):
60 time.sleep(sleep_time_sec)
61 sleep_time_sec = min(1, sleep_time_sec * 2) # Don't wait more than 1 sec.
65 def _CheckPortStatus(port, expected_status):
66 """Returns True if port has expected_status.
69 port: the port number.
70 expected_status: boolean of expected status.
73 Returns True if the status is expected. Otherwise returns False.
75 return _WaitUntil(lambda: ports.IsHostPortUsed(port) == expected_status)
78 def _CheckDevicePortStatus(adb, port):
79 """Returns whether the provided port is used."""
80 return _WaitUntil(lambda: ports.IsDevicePortUsed(adb, port))
83 def _GetServerTypeCommandLine(server_type):
84 """Returns the command-line by the given server type.
87 server_type: the server type to be used (e.g. 'http').
90 A string containing the command-line argument.
92 if server_type not in SERVER_TYPES:
93 raise NotImplementedError('Unknown server type: %s' % server_type)
94 if server_type == 'udpecho':
95 raise Exception('Please do not run UDP echo tests because we do not have '
96 'a UDP forwarder tool.')
97 return SERVER_TYPES[server_type]
100 class TestServerThread(threading.Thread):
101 """A thread to run the test server in a separate process."""
103 def __init__(self, ready_event, arguments, adb, tool):
104 """Initialize TestServerThread with the following argument.
107 ready_event: event which will be set when the test server is ready.
108 arguments: dictionary of arguments to run the test server.
109 adb: instance of AndroidCommands.
110 tool: instance of runtime error detection tool.
112 threading.Thread.__init__(self)
113 self.wait_event = threading.Event()
114 self.stop_flag = False
115 self.ready_event = ready_event
116 self.ready_event.clear()
117 self.arguments = arguments
120 self.test_server_process = None
121 self.is_ready = False
122 self.host_port = self.arguments['port']
123 assert isinstance(self.host_port, int)
124 # The forwarder device port now is dynamically allocated.
125 self.forwarder_device_port = 0
126 # Anonymous pipe in order to get port info from test server.
129 self.command_line = []
131 def _WaitToStartAndGetPortFromTestServer(self):
132 """Waits for the Python test server to start and gets the port it is using.
134 The port information is passed by the Python test server with a pipe given
135 by self.pipe_out. It is written as a result to |self.host_port|.
138 Whether the port used by the test server was successfully fetched.
140 assert self.host_port == 0 and self.pipe_out and self.pipe_in
141 (in_fds, _, _) = select.select([self.pipe_in, ], [], [],
142 TEST_SERVER_STARTUP_TIMEOUT)
144 logging.error('Failed to wait to the Python test server to be started.')
146 # First read the data length as an unsigned 4-byte value. This
147 # is _not_ using network byte ordering since the Python test server packs
148 # size as native byte order and all Chromium platforms so far are
149 # configured to use little-endian.
150 # TODO(jnd): Change the Python test server and local_test_server_*.cc to
151 # use a unified byte order (either big-endian or little-endian).
152 data_length = os.read(self.pipe_in, struct.calcsize('=L'))
154 (data_length,) = struct.unpack('=L', data_length)
157 logging.error('Failed to get length of server data.')
159 port_json = os.read(self.pipe_in, data_length)
161 logging.error('Failed to get server data.')
163 logging.info('Got port json data: %s', port_json)
164 port_json = json.loads(port_json)
165 if port_json.has_key('port') and isinstance(port_json['port'], int):
166 self.host_port = port_json['port']
167 return _CheckPortStatus(self.host_port, True)
168 logging.error('Failed to get port information from the server data.')
171 def _GenerateCommandLineArguments(self):
172 """Generates the command line to run the test server.
174 Note that all options are processed by following the definitions in
177 if self.command_line:
179 # The following arguments must exist.
180 type_cmd = _GetServerTypeCommandLine(self.arguments['server-type'])
182 self.command_line.append(type_cmd)
183 self.command_line.append('--port=%d' % self.host_port)
184 # Use a pipe to get the port given by the instance of Python test server
185 # if the test does not specify the port.
186 if self.host_port == 0:
187 (self.pipe_in, self.pipe_out) = os.pipe()
188 self.command_line.append('--startup-pipe=%d' % self.pipe_out)
189 self.command_line.append('--host=%s' % self.arguments['host'])
190 data_dir = self.arguments['data-dir'] or 'chrome/test/data'
191 if not os.path.isabs(data_dir):
192 data_dir = os.path.join(constants.DIR_SOURCE_ROOT, data_dir)
193 self.command_line.append('--data-dir=%s' % data_dir)
194 # The following arguments are optional depending on the individual test.
195 if self.arguments.has_key('log-to-console'):
196 self.command_line.append('--log-to-console')
197 if self.arguments.has_key('auth-token'):
198 self.command_line.append('--auth-token=%s' % self.arguments['auth-token'])
199 if self.arguments.has_key('https'):
200 self.command_line.append('--https')
201 if self.arguments.has_key('cert-and-key-file'):
202 self.command_line.append('--cert-and-key-file=%s' % os.path.join(
203 constants.DIR_SOURCE_ROOT, self.arguments['cert-and-key-file']))
204 if self.arguments.has_key('ocsp'):
205 self.command_line.append('--ocsp=%s' % self.arguments['ocsp'])
206 if self.arguments.has_key('https-record-resume'):
207 self.command_line.append('--https-record-resume')
208 if self.arguments.has_key('ssl-client-auth'):
209 self.command_line.append('--ssl-client-auth')
210 if self.arguments.has_key('tls-intolerant'):
211 self.command_line.append('--tls-intolerant=%s' %
212 self.arguments['tls-intolerant'])
213 if self.arguments.has_key('ssl-client-ca'):
214 for ca in self.arguments['ssl-client-ca']:
215 self.command_line.append('--ssl-client-ca=%s' %
216 os.path.join(constants.DIR_SOURCE_ROOT, ca))
217 if self.arguments.has_key('ssl-bulk-cipher'):
218 for bulk_cipher in self.arguments['ssl-bulk-cipher']:
219 self.command_line.append('--ssl-bulk-cipher=%s' % bulk_cipher)
221 def _CloseUnnecessaryFDsForTestServerProcess(self):
222 # This is required to avoid subtle deadlocks that could be caused by the
223 # test server child process inheriting undesirable file descriptors such as
224 # file lock file descriptors.
225 for fd in xrange(0, 1024):
226 if fd != self.pipe_out:
233 logging.info('Start running the thread!')
234 self.wait_event.clear()
235 self._GenerateCommandLineArguments()
236 command = constants.DIR_SOURCE_ROOT
237 if self.arguments['server-type'] == 'sync':
238 command = [os.path.join(command, 'sync', 'tools', 'testserver',
239 'sync_testserver.py')] + self.command_line
241 command = [os.path.join(command, 'net', 'tools', 'testserver',
242 'testserver.py')] + self.command_line
243 logging.info('Running: %s', command)
244 self.process = subprocess.Popen(
245 command, preexec_fn=self._CloseUnnecessaryFDsForTestServerProcess)
248 self.is_ready = self._WaitToStartAndGetPortFromTestServer()
250 self.is_ready = _CheckPortStatus(self.host_port, True)
252 Forwarder.Map([(0, self.host_port)], self.adb, self.tool)
253 # Check whether the forwarder is ready on the device.
254 self.is_ready = False
255 device_port = Forwarder.DevicePortForHostPort(self.host_port)
256 if device_port and _CheckDevicePortStatus(self.adb, device_port):
258 self.forwarder_device_port = device_port
259 # Wake up the request handler thread.
260 self.ready_event.set()
261 # Keep thread running until Stop() gets called.
262 _WaitUntil(lambda: self.stop_flag, max_attempts=sys.maxint)
263 if self.process.poll() is None:
265 Forwarder.UnmapDevicePort(self.forwarder_device_port, self.adb)
267 self.is_ready = False
269 os.close(self.pipe_in)
270 os.close(self.pipe_out)
273 logging.info('Test-server has died.')
274 self.wait_event.set()
277 """Blocks until the loop has finished.
279 Note that this must be called in another thread.
283 self.stop_flag = True
284 self.wait_event.wait()
287 class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
288 """A handler used to process http GET/POST request."""
290 def _SendResponse(self, response_code, response_reason, additional_headers,
292 """Generates a response sent to the client from the provided parameters.
295 response_code: number of the response status.
296 response_reason: string of reason description of the response.
297 additional_headers: dict of additional headers. Each key is the name of
298 the header, each value is the content of the header.
299 contents: string of the contents we want to send to client.
301 self.send_response(response_code, response_reason)
302 self.send_header('Content-Type', 'text/html')
303 # Specify the content-length as without it the http(s) response will not
304 # be completed properly (and the browser keeps expecting data).
305 self.send_header('Content-Length', len(contents))
306 for header_name in additional_headers:
307 self.send_header(header_name, additional_headers[header_name])
309 self.wfile.write(contents)
312 def _StartTestServer(self):
313 """Starts the test server thread."""
314 logging.info('Handling request to spawn a test server.')
315 content_type = self.headers.getheader('content-type')
316 if content_type != 'application/json':
317 raise Exception('Bad content-type for start request.')
318 content_length = self.headers.getheader('content-length')
319 if not content_length:
322 content_length = int(content_length)
324 raise Exception('Bad content-length for start request.')
325 logging.info(content_length)
326 test_server_argument_json = self.rfile.read(content_length)
327 logging.info(test_server_argument_json)
328 assert not self.server.test_server_instance
329 ready_event = threading.Event()
330 self.server.test_server_instance = TestServerThread(
332 json.loads(test_server_argument_json),
335 self.server.test_server_instance.setDaemon(True)
336 self.server.test_server_instance.start()
338 if self.server.test_server_instance.is_ready:
339 self._SendResponse(200, 'OK', {}, json.dumps(
340 {'port': self.server.test_server_instance.forwarder_device_port,
341 'message': 'started'}))
342 logging.info('Test server is running on port: %d.',
343 self.server.test_server_instance.host_port)
345 self.server.test_server_instance.Stop()
346 self.server.test_server_instance = None
347 self._SendResponse(500, 'Test Server Error.', {}, '')
348 logging.info('Encounter problem during starting a test server.')
350 def _KillTestServer(self):
351 """Stops the test server instance."""
352 # There should only ever be one test server at a time. This may do the
353 # wrong thing if we try and start multiple test servers.
354 if not self.server.test_server_instance:
356 port = self.server.test_server_instance.host_port
357 logging.info('Handling request to kill a test server on port: %d.', port)
358 self.server.test_server_instance.Stop()
359 # Make sure the status of test server is correct before sending response.
360 if _CheckPortStatus(port, False):
361 self._SendResponse(200, 'OK', {}, 'killed')
362 logging.info('Test server on port %d is killed', port)
364 self._SendResponse(500, 'Test Server Error.', {}, '')
365 logging.info('Encounter problem during killing a test server.')
366 self.server.test_server_instance = None
369 parsed_path = urlparse.urlparse(self.path)
370 action = parsed_path.path
371 logging.info('Action for POST method is: %s.', action)
372 if action == '/start':
373 self._StartTestServer()
375 self._SendResponse(400, 'Unknown request.', {}, '')
376 logging.info('Encounter unknown request: %s.', action)
379 parsed_path = urlparse.urlparse(self.path)
380 action = parsed_path.path
381 params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1)
382 logging.info('Action for GET method is: %s.', action)
384 logging.info('%s=%s', param, params[param][0])
385 if action == '/kill':
386 self._KillTestServer()
387 elif action == '/ping':
388 # The ping handler is used to check whether the spawner server is ready
389 # to serve the requests. We don't need to test the status of the test
390 # server when handling ping request.
391 self._SendResponse(200, 'OK', {}, 'ready')
392 logging.info('Handled ping request and sent response.')
394 self._SendResponse(400, 'Unknown request', {}, '')
395 logging.info('Encounter unknown request: %s.', action)
398 class SpawningServer(object):
399 """The class used to start/stop a http server."""
401 def __init__(self, test_server_spawner_port, adb, tool):
402 logging.info('Creating new spawner on port: %d.', test_server_spawner_port)
403 self.server = BaseHTTPServer.HTTPServer(('', test_server_spawner_port),
404 SpawningServerRequestHandler)
405 self.server.adb = adb
406 self.server.tool = tool
407 self.server.test_server_instance = None
408 self.server.build_type = constants.GetBuildType()
411 logging.info('Starting test server spawner')
412 self.server.serve_forever()
415 """Starts the test server spawner."""
416 listener_thread = threading.Thread(target=self._Listen)
417 listener_thread.setDaemon(True)
418 listener_thread.start()
421 """Stops the test server spawner.
423 Also cleans the server state.
426 self.server.shutdown()
428 def CleanupState(self):
429 """Cleans up the spawning server state.
431 This should be called if the test server spawner is reused,
432 to avoid sharing the test server instance.
434 if self.server.test_server_instance:
435 self.server.test_server_instance.Stop()
436 self.server.test_server_instance = None