|
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 |
|
3 # found in the LICENSE file. |
|
4 |
|
5 """A "Test Server Spawner" that handles killing/stopping per-test test servers. |
|
6 |
|
7 It's used to accept requests from the device to spawn and kill instances of the |
|
8 chrome test server on the host. |
|
9 """ |
|
10 |
|
11 import BaseHTTPServer |
|
12 import json |
|
13 import logging |
|
14 import os |
|
15 import select |
|
16 import struct |
|
17 import subprocess |
|
18 import threading |
|
19 import time |
|
20 import urlparse |
|
21 |
|
22 import constants |
|
23 from forwarder import Forwarder |
|
24 import ports |
|
25 |
|
26 |
|
27 # Path that are needed to import necessary modules when running testserver.py. |
|
28 os.environ['PYTHONPATH'] = os.environ.get('PYTHONPATH', '') + ':%s:%s:%s:%s' % ( |
|
29 os.path.join(constants.CHROME_DIR, 'third_party'), |
|
30 os.path.join(constants.CHROME_DIR, 'third_party', 'tlslite'), |
|
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=%s' % |
|
197 self.arguments['tls-intolerant']) |
|
198 if self.arguments.has_key('ssl-client-ca'): |
|
199 for ca in self.arguments['ssl-client-ca']: |
|
200 self.command_line.append('--ssl-client-ca=%s' % |
|
201 os.path.join(constants.CHROME_DIR, ca)) |
|
202 if self.arguments.has_key('ssl-bulk-cipher'): |
|
203 for bulk_cipher in self.arguments['ssl-bulk-cipher']: |
|
204 self.command_line.append('--ssl-bulk-cipher=%s' % bulk_cipher) |
|
205 |
|
206 def run(self): |
|
207 logging.info('Start running the thread!') |
|
208 self.wait_event.clear() |
|
209 self._GenerateCommandLineArguments() |
|
210 command = [os.path.join(constants.CHROME_DIR, 'net', 'tools', |
|
211 'testserver', 'testserver.py')] + self.command_line |
|
212 logging.info('Running: %s', command) |
|
213 self.process = subprocess.Popen(command) |
|
214 if self.process: |
|
215 if self.pipe_out: |
|
216 self.is_ready = self._WaitToStartAndGetPortFromTestServer() |
|
217 else: |
|
218 self.is_ready = _CheckPortStatus(self.host_port, True) |
|
219 if self.is_ready: |
|
220 self._test_server_forwarder = Forwarder( |
|
221 self.adb, [(0, self.host_port)], self.tool, '127.0.0.1', |
|
222 self.build_type) |
|
223 # Check whether the forwarder is ready on the device. |
|
224 self.is_ready = False |
|
225 device_port = self._test_server_forwarder.DevicePortForHostPort( |
|
226 self.host_port) |
|
227 if device_port: |
|
228 for timeout in range(1, 5): |
|
229 if ports.IsDevicePortUsed(self.adb, device_port, 'LISTEN'): |
|
230 self.is_ready = True |
|
231 self.forwarder_device_port = device_port |
|
232 break |
|
233 time.sleep(timeout) |
|
234 # Wake up the request handler thread. |
|
235 self.ready_event.set() |
|
236 # Keep thread running until Stop() gets called. |
|
237 while not self.stop_flag: |
|
238 time.sleep(1) |
|
239 if self.process.poll() is None: |
|
240 self.process.kill() |
|
241 if self._test_server_forwarder: |
|
242 self._test_server_forwarder.Close() |
|
243 self.process = None |
|
244 self.is_ready = False |
|
245 if self.pipe_out: |
|
246 os.close(self.pipe_in) |
|
247 os.close(self.pipe_out) |
|
248 self.pipe_in = None |
|
249 self.pipe_out = None |
|
250 logging.info('Test-server has died.') |
|
251 self.wait_event.set() |
|
252 |
|
253 def Stop(self): |
|
254 """Blocks until the loop has finished. |
|
255 |
|
256 Note that this must be called in another thread. |
|
257 """ |
|
258 if not self.process: |
|
259 return |
|
260 self.stop_flag = True |
|
261 self.wait_event.wait() |
|
262 |
|
263 |
|
264 class SpawningServerRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
|
265 """A handler used to process http GET/POST request.""" |
|
266 |
|
267 def _SendResponse(self, response_code, response_reason, additional_headers, |
|
268 contents): |
|
269 """Generates a response sent to the client from the provided parameters. |
|
270 |
|
271 Args: |
|
272 response_code: number of the response status. |
|
273 response_reason: string of reason description of the response. |
|
274 additional_headers: dict of additional headers. Each key is the name of |
|
275 the header, each value is the content of the header. |
|
276 contents: string of the contents we want to send to client. |
|
277 """ |
|
278 self.send_response(response_code, response_reason) |
|
279 self.send_header('Content-Type', 'text/html') |
|
280 # Specify the content-length as without it the http(s) response will not |
|
281 # be completed properly (and the browser keeps expecting data). |
|
282 self.send_header('Content-Length', len(contents)) |
|
283 for header_name in additional_headers: |
|
284 self.send_header(header_name, additional_headers[header_name]) |
|
285 self.end_headers() |
|
286 self.wfile.write(contents) |
|
287 self.wfile.flush() |
|
288 |
|
289 def _StartTestServer(self): |
|
290 """Starts the test server thread.""" |
|
291 logging.info('Handling request to spawn a test server.') |
|
292 content_type = self.headers.getheader('content-type') |
|
293 if content_type != 'application/json': |
|
294 raise Exception('Bad content-type for start request.') |
|
295 content_length = self.headers.getheader('content-length') |
|
296 if not content_length: |
|
297 content_length = 0 |
|
298 try: |
|
299 content_length = int(content_length) |
|
300 except: |
|
301 raise Exception('Bad content-length for start request.') |
|
302 logging.info(content_length) |
|
303 test_server_argument_json = self.rfile.read(content_length) |
|
304 logging.info(test_server_argument_json) |
|
305 assert not self.server.test_server_instance |
|
306 ready_event = threading.Event() |
|
307 self.server.test_server_instance = TestServerThread( |
|
308 ready_event, |
|
309 json.loads(test_server_argument_json), |
|
310 self.server.adb, |
|
311 self.server.tool, |
|
312 self.server.build_type) |
|
313 self.server.test_server_instance.setDaemon(True) |
|
314 self.server.test_server_instance.start() |
|
315 ready_event.wait() |
|
316 if self.server.test_server_instance.is_ready: |
|
317 self._SendResponse(200, 'OK', {}, json.dumps( |
|
318 {'port': self.server.test_server_instance.forwarder_device_port, |
|
319 'message': 'started'})) |
|
320 logging.info('Test server is running on port: %d.', |
|
321 self.server.test_server_instance.host_port) |
|
322 else: |
|
323 self.server.test_server_instance.Stop() |
|
324 self.server.test_server_instance = None |
|
325 self._SendResponse(500, 'Test Server Error.', {}, '') |
|
326 logging.info('Encounter problem during starting a test server.') |
|
327 |
|
328 def _KillTestServer(self): |
|
329 """Stops the test server instance.""" |
|
330 # There should only ever be one test server at a time. This may do the |
|
331 # wrong thing if we try and start multiple test servers. |
|
332 if not self.server.test_server_instance: |
|
333 return |
|
334 port = self.server.test_server_instance.host_port |
|
335 logging.info('Handling request to kill a test server on port: %d.', port) |
|
336 self.server.test_server_instance.Stop() |
|
337 # Make sure the status of test server is correct before sending response. |
|
338 if _CheckPortStatus(port, False): |
|
339 self._SendResponse(200, 'OK', {}, 'killed') |
|
340 logging.info('Test server on port %d is killed', port) |
|
341 else: |
|
342 self._SendResponse(500, 'Test Server Error.', {}, '') |
|
343 logging.info('Encounter problem during killing a test server.') |
|
344 self.server.test_server_instance = None |
|
345 |
|
346 def do_POST(self): |
|
347 parsed_path = urlparse.urlparse(self.path) |
|
348 action = parsed_path.path |
|
349 logging.info('Action for POST method is: %s.', action) |
|
350 if action == '/start': |
|
351 self._StartTestServer() |
|
352 else: |
|
353 self._SendResponse(400, 'Unknown request.', {}, '') |
|
354 logging.info('Encounter unknown request: %s.', action) |
|
355 |
|
356 def do_GET(self): |
|
357 parsed_path = urlparse.urlparse(self.path) |
|
358 action = parsed_path.path |
|
359 params = urlparse.parse_qs(parsed_path.query, keep_blank_values=1) |
|
360 logging.info('Action for GET method is: %s.', action) |
|
361 for param in params: |
|
362 logging.info('%s=%s', param, params[param][0]) |
|
363 if action == '/kill': |
|
364 self._KillTestServer() |
|
365 elif action == '/ping': |
|
366 # The ping handler is used to check whether the spawner server is ready |
|
367 # to serve the requests. We don't need to test the status of the test |
|
368 # server when handling ping request. |
|
369 self._SendResponse(200, 'OK', {}, 'ready') |
|
370 logging.info('Handled ping request and sent response.') |
|
371 else: |
|
372 self._SendResponse(400, 'Unknown request', {}, '') |
|
373 logging.info('Encounter unknown request: %s.', action) |
|
374 |
|
375 |
|
376 class SpawningServer(object): |
|
377 """The class used to start/stop a http server.""" |
|
378 |
|
379 def __init__(self, test_server_spawner_port, adb, tool, build_type): |
|
380 logging.info('Creating new spawner on port: %d.', test_server_spawner_port) |
|
381 self.server = BaseHTTPServer.HTTPServer(('', test_server_spawner_port), |
|
382 SpawningServerRequestHandler) |
|
383 self.port = test_server_spawner_port |
|
384 self.server.adb = adb |
|
385 self.server.tool = tool |
|
386 self.server.test_server_instance = None |
|
387 self.server.build_type = build_type |
|
388 |
|
389 def _Listen(self): |
|
390 logging.info('Starting test server spawner') |
|
391 self.server.serve_forever() |
|
392 |
|
393 def Start(self): |
|
394 listener_thread = threading.Thread(target=self._Listen) |
|
395 listener_thread.setDaemon(True) |
|
396 listener_thread.start() |
|
397 time.sleep(1) |
|
398 |
|
399 def Stop(self): |
|
400 if self.server.test_server_instance: |
|
401 self.server.test_server_instance.Stop() |
|
402 self.server.shutdown() |