1# Copyright (c) 2015 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 5import argparse 6import json 7import os 8import sys 9import urlparse 10 11from hooks import install 12 13from paste import fileapp 14from paste import httpserver 15 16import webapp2 17from webapp2 import Route, RedirectHandler 18 19from dashboard_build import dashboard_dev_server_config 20from perf_insights_build import perf_insights_dev_server_config 21from tracing_build import tracing_dev_server_config 22 23_MAIN_HTML = """<html><body> 24<h1>Run Unit Tests</h1> 25<ul> 26%s 27</ul> 28<h1>Quick links</h1> 29<ul> 30%s 31</ul> 32</body></html> 33""" 34 35_QUICK_LINKS = [ 36 ('Trace File Viewer', 37 '/tracing_examples/trace_viewer.html'), 38 ('Perf Insights Viewer', 39 '/perf_insights_examples/perf_insights_viewer.html') 40] 41 42_LINK_ITEM = '<li><a href="%s">%s</a></li>' 43 44def _GetFilesIn(basedir): 45 data_files = [] 46 for dirpath, dirnames, filenames in os.walk(basedir, followlinks=True): 47 new_dirnames = [d for d in dirnames if not d.startswith('.')] 48 del dirnames[:] 49 dirnames += new_dirnames 50 51 for f in filenames: 52 if f.startswith('.'): 53 continue 54 if f == 'README.md': 55 continue 56 full_f = os.path.join(dirpath, f) 57 rel_f = os.path.relpath(full_f, basedir) 58 data_files.append(rel_f) 59 60 data_files.sort() 61 return data_files 62 63 64def _RelPathToUnixPath(p): 65 return p.replace(os.sep, '/') 66 67 68class TestResultHandler(webapp2.RequestHandler): 69 def post(self, *args, **kwargs): # pylint: disable=unused-argument 70 msg = self.request.body 71 ostream = sys.stdout if 'PASSED' in msg else sys.stderr 72 ostream.write(msg + '\n') 73 return self.response.write('') 74 75 76class TestsCompletedHandler(webapp2.RequestHandler): 77 def post(self, *args, **kwargs): # pylint: disable=unused-argument 78 msg = self.request.body 79 sys.stdout.write(msg + '\n') 80 exit_code = 0 if 'ALL_PASSED' in msg else 1 81 if hasattr(self.app.server, 'please_exit'): 82 self.app.server.please_exit(exit_code) 83 return self.response.write('') 84 85 86class DirectoryListingHandler(webapp2.RequestHandler): 87 def get(self, *args, **kwargs): # pylint: disable=unused-argument 88 source_path = kwargs.pop('_source_path', None) 89 mapped_path = kwargs.pop('_mapped_path', None) 90 assert mapped_path.endswith('/') 91 92 data_files_relative_to_top = _GetFilesIn(source_path) 93 data_files = [mapped_path + x 94 for x in data_files_relative_to_top] 95 96 files_as_json = json.dumps(data_files) 97 self.response.content_type = 'application/json' 98 return self.response.write(files_as_json) 99 100 101class FileAppWithGZipHandling(fileapp.FileApp): 102 def guess_type(self): 103 content_type, content_encoding = \ 104 super(FileAppWithGZipHandling, self).guess_type() 105 if not self.filename.endswith('.gz'): 106 return content_type, content_encoding 107 # By default, FileApp serves gzip files as their underlying type with 108 # Content-Encoding of gzip. That causes them to show up on the client 109 # decompressed. That ends up being surprising to our xhr.html system. 110 return None, None 111 112class SourcePathsHandler(webapp2.RequestHandler): 113 def get(self, *args, **kwargs): # pylint: disable=unused-argument 114 source_paths = kwargs.pop('_source_paths', []) 115 116 path = self.request.path 117 # This is how we do it. Its... strange, but its what we've done since 118 # the dawn of time. Aka 4 years ago, lol. 119 for mapped_path in source_paths: 120 rel = os.path.relpath(path, '/') 121 candidate = os.path.join(mapped_path, rel) 122 if os.path.exists(candidate): 123 app = FileAppWithGZipHandling(candidate) 124 app.cache_control(no_cache=True) 125 return app 126 self.abort(404) 127 128 @staticmethod 129 def GetServingPathForAbsFilename(source_paths, filename): 130 if not os.path.isabs(filename): 131 raise Exception('filename must be an absolute path') 132 133 for mapped_path in source_paths: 134 if not filename.startswith(mapped_path): 135 continue 136 rel = os.path.relpath(filename, mapped_path) 137 unix_rel = _RelPathToUnixPath(rel) 138 return unix_rel 139 return None 140 141 142class SimpleDirectoryHandler(webapp2.RequestHandler): 143 def get(self, *args, **kwargs): # pylint: disable=unused-argument 144 top_path = os.path.abspath(kwargs.pop('_top_path', None)) 145 if not top_path.endswith(os.path.sep): 146 top_path += os.path.sep 147 148 joined_path = os.path.abspath( 149 os.path.join(top_path, kwargs.pop('rest_of_path'))) 150 if not joined_path.startswith(top_path): 151 self.response.set_status(403) 152 return 153 app = FileAppWithGZipHandling(joined_path) 154 app.cache_control(no_cache=True) 155 return app 156 157 158class TestOverviewHandler(webapp2.RequestHandler): 159 def get(self, *args, **kwargs): # pylint: disable=unused-argument 160 test_links = [] 161 for name, path in kwargs.pop('pds').iteritems(): 162 test_links.append(_LINK_ITEM % (path, name)) 163 quick_links = [] 164 for name, path in _QUICK_LINKS: 165 quick_links.append(_LINK_ITEM % (path, name)) 166 self.response.out.write(_MAIN_HTML % ('\n'.join(test_links), 167 '\n'.join(quick_links))) 168 169class DevServerApp(webapp2.WSGIApplication): 170 def __init__(self, pds, args): 171 super(DevServerApp, self).__init__(debug=True) 172 self.pds = pds 173 self._server = None 174 self._all_source_paths = [] 175 self._all_mapped_test_data_paths = [] 176 self._InitFromArgs(args) 177 178 @property 179 def server(self): 180 return self._server 181 182 @server.setter 183 def server(self, server): 184 self._server = server 185 186 def _InitFromArgs(self, args): 187 default_tests = dict((pd.GetName(), pd.GetRunUnitTestsUrl()) 188 for pd in self.pds) 189 routes = [ 190 Route('/tests.html', TestOverviewHandler, 191 defaults={'pds': default_tests}), 192 Route('', RedirectHandler, defaults={'_uri': '/tests.html'}), 193 Route('/', RedirectHandler, defaults={'_uri': '/tests.html'}), 194 ] 195 for pd in self.pds: 196 routes += pd.GetRoutes(args) 197 routes += [ 198 Route('/%s/notify_test_result' % pd.GetName(), 199 TestResultHandler), 200 Route('/%s/notify_tests_completed' % pd.GetName(), 201 TestsCompletedHandler) 202 ] 203 204 for pd in self.pds: 205 # Test data system. 206 for mapped_path, source_path in pd.GetTestDataPaths(args): 207 self._all_mapped_test_data_paths.append((mapped_path, source_path)) 208 routes.append(Route('%s__file_list__' % mapped_path, 209 DirectoryListingHandler, 210 defaults={ 211 '_source_path': source_path, 212 '_mapped_path': mapped_path 213 })) 214 routes.append(Route('%s<rest_of_path:.+>' % mapped_path, 215 SimpleDirectoryHandler, 216 defaults={'_top_path': source_path})) 217 218 # This must go last, because its catch-all. 219 # 220 # Its funky that we have to add in the root path. The long term fix is to 221 # stop with the crazy multi-source-pathing thing. 222 for pd in self.pds: 223 self._all_source_paths += pd.GetSourcePaths(args) 224 routes.append( 225 Route('/<:.+>', SourcePathsHandler, 226 defaults={'_source_paths': self._all_source_paths})) 227 228 for route in routes: 229 self.router.add(route) 230 231 def GetAbsFilenameForHref(self, href): 232 for source_path in self._all_source_paths: 233 full_source_path = os.path.abspath(source_path) 234 expanded_href_path = os.path.abspath(os.path.join(full_source_path, 235 href.lstrip('/'))) 236 if (os.path.exists(expanded_href_path) and 237 os.path.commonprefix([full_source_path, 238 expanded_href_path]) == full_source_path): 239 return expanded_href_path 240 return None 241 242 def GetURLForAbsFilename(self, filename): 243 assert self.server is not None 244 for mapped_path, source_path in self._all_mapped_test_data_paths: 245 if not filename.startswith(source_path): 246 continue 247 rel = os.path.relpath(filename, source_path) 248 unix_rel = _RelPathToUnixPath(rel) 249 url = urlparse.urljoin(mapped_path, unix_rel) 250 return url 251 252 path = SourcePathsHandler.GetServingPathForAbsFilename( 253 self._all_source_paths, filename) 254 if path is None: 255 return None 256 return urlparse.urljoin('/', path) 257 258 259def _AddPleaseExitMixinToServer(server): 260 # Shutting down httpserver gracefully and yielding a return code requires 261 # a bit of mixin code. 262 263 exit_code_attempt = [] 264 def PleaseExit(exit_code): 265 if len(exit_code_attempt) > 0: 266 return 267 exit_code_attempt.append(exit_code) 268 server.running = False 269 270 real_serve_forever = server.serve_forever 271 272 def ServeForever(): 273 try: 274 real_serve_forever() 275 except KeyboardInterrupt: 276 # allow CTRL+C to shutdown 277 return 255 278 279 if len(exit_code_attempt) == 1: 280 return exit_code_attempt[0] 281 # The serve_forever returned for some reason separate from 282 # exit_please. 283 return 0 284 285 server.please_exit = PleaseExit 286 server.serve_forever = ServeForever 287 288 289def _AddCommandLineArguments(pds, argv): 290 parser = argparse.ArgumentParser(description='Run development server') 291 parser.add_argument( 292 '--no-install-hooks', dest='install_hooks', action='store_false') 293 parser.add_argument('-p', '--port', default=8003, type=int) 294 for pd in pds: 295 g = parser.add_argument_group(pd.GetName()) 296 pd.AddOptionstToArgParseGroup(g) 297 args = parser.parse_args(args=argv[1:]) 298 return args 299 300 301def Main(argv): 302 pds = [ 303 dashboard_dev_server_config.DashboardDevServerConfig(), 304 perf_insights_dev_server_config.PerfInsightsDevServerConfig(), 305 tracing_dev_server_config.TracingDevServerConfig(), 306 ] 307 308 args = _AddCommandLineArguments(pds, argv) 309 310 if args.install_hooks: 311 install.InstallHooks() 312 313 app = DevServerApp(pds, args=args) 314 315 server = httpserver.serve(app, host='127.0.0.1', port=args.port, 316 start_loop=False) 317 _AddPleaseExitMixinToServer(server) 318 # pylint: disable=no-member 319 server.urlbase = 'http://127.0.0.1:%i' % server.server_port 320 app.server = server 321 322 sys.stderr.write('Now running on %s\n' % server.urlbase) 323 324 return server.serve_forever() 325