server.py revision cb55f11a8f8ad66cdfc7643298a8772837cc35df
1#!/usr/bin/python 2 3''' 4Copyright 2013 Google Inc. 5 6Use of this source code is governed by a BSD-style license that can be 7found in the LICENSE file. 8''' 9 10''' 11HTTP server for our HTML rebaseline viewer. 12''' 13 14# System-level imports 15import argparse 16import BaseHTTPServer 17import json 18import os 19import posixpath 20import re 21import shutil 22import sys 23 24# Imports from within Skia 25# 26# We need to add the 'tools' directory, so that we can import svn.py within 27# that directory. 28# Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* 29# so any dirs that are already in the PYTHONPATH will be preferred. 30PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) 31TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) 32TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') 33if TOOLS_DIRECTORY not in sys.path: 34 sys.path.append(TOOLS_DIRECTORY) 35import svn 36 37# Imports from local dir 38import results 39 40ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual' 41PATHSPLIT_RE = re.compile('/([^/]+)/(.+)') 42TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(os.path.dirname( 43 os.path.realpath(__file__)))) 44 45# A simple dictionary of file name extensions to MIME types. The empty string 46# entry is used as the default when no extension was given or if the extension 47# has no entry in this dictionary. 48MIME_TYPE_MAP = {'': 'application/octet-stream', 49 'html': 'text/html', 50 'css': 'text/css', 51 'png': 'image/png', 52 'js': 'application/javascript', 53 'json': 'application/json' 54 } 55 56DEFAULT_ACTUALS_DIR = '.gm-actuals' 57DEFAULT_EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm') 58DEFAULT_PORT = 8888 59 60_SERVER = None # This gets filled in by main() 61 62class Server(object): 63 """ HTTP server for our HTML rebaseline viewer. 64 65 params: 66 actuals_dir: directory under which we will check out the latest actual 67 GM results 68 expectations_dir: directory under which to find GM expectations (they 69 must already be in that directory) 70 port: which TCP port to listen on for HTTP requests 71 export: whether to allow HTTP clients on other hosts to access this server 72 """ 73 def __init__(self, 74 actuals_dir=DEFAULT_ACTUALS_DIR, 75 expectations_dir=DEFAULT_EXPECTATIONS_DIR, 76 port=DEFAULT_PORT, export=False): 77 self._actuals_dir = actuals_dir 78 self._expectations_dir = expectations_dir 79 self._port = port 80 self._export = export 81 82 def fetch_results(self): 83 """ Create self.results, based on the expectations in 84 self._expectations_dir and the latest actuals from skia-autogen. 85 86 TODO(epoger): Add a new --browseonly mode setting. In that mode, 87 the gm-actuals and expectations will automatically be updated every few 88 minutes. See discussion in https://codereview.chromium.org/24274003/ . 89 """ 90 print 'Checking out latest actual GM results from %s into %s ...' % ( 91 ACTUALS_SVN_REPO, self._actuals_dir) 92 actuals_repo = svn.Svn(self._actuals_dir) 93 if not os.path.isdir(self._actuals_dir): 94 os.makedirs(self._actuals_dir) 95 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') 96 else: 97 actuals_repo.Update('.') 98 print 'Parsing results from actuals in %s and expectations in %s ...' % ( 99 self._actuals_dir, self._expectations_dir) 100 self.results = results.Results( 101 actuals_root=self._actuals_dir, 102 expected_root=self._expectations_dir) 103 104 def run(self): 105 self.fetch_results() 106 if self._export: 107 server_address = ('', self._port) 108 print ('WARNING: Running in "export" mode. Users on other machines will ' 109 'be able to modify your GM expectations!') 110 else: 111 server_address = ('127.0.0.1', self._port) 112 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) 113 print 'Ready for requests on http://%s:%d' % ( 114 http_server.server_name, http_server.server_port) 115 http_server.serve_forever() 116 117 118class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): 119 """ HTTP request handlers for various types of queries this server knows 120 how to handle (static HTML and Javascript, expected/actual results, etc.) 121 """ 122 def do_GET(self): 123 """ Handles all GET requests, forwarding them to the appropriate 124 do_GET_* dispatcher. """ 125 if self.path == '' or self.path == '/' or self.path == '/index.html' : 126 self.redirect_to('/static/view.html') 127 return 128 if self.path == '/favicon.ico' : 129 self.redirect_to('/static/favicon.ico') 130 return 131 132 # All requests must be of this form: 133 # /dispatcher/remainder 134 # where "dispatcher" indicates which do_GET_* dispatcher to run 135 # and "remainder" is the remaining path sent to that dispatcher. 136 normpath = posixpath.normpath(self.path) 137 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() 138 dispatchers = { 139 'results': self.do_GET_results, 140 'static': self.do_GET_static, 141 } 142 dispatcher = dispatchers[dispatcher_name] 143 dispatcher(remainder) 144 145 def do_GET_results(self, result_type): 146 """ Handle a GET request for GM results. 147 For now, we ignore the remaining path info, because we only know how to 148 return all results. 149 150 TODO(epoger): Unless we start making use of result_type, remove that 151 parameter.""" 152 print 'do_GET_results: sending results of type "%s"' % result_type 153 response_dict = _SERVER.results.GetAll() 154 if response_dict: 155 self.send_json_dict(response_dict) 156 else: 157 self.send_error(404) 158 159 def do_GET_static(self, path): 160 """ Handle a GET request for a file under the 'static' directory. 161 Only allow serving of files within the 'static' directory that is a 162 filesystem sibling of this script. """ 163 print 'do_GET_static: sending file "%s"' % path 164 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) 165 full_path = os.path.realpath(os.path.join(static_dir, path)) 166 if full_path.startswith(static_dir): 167 self.send_file(full_path) 168 else: 169 print ('Attempted do_GET_static() of path [%s] outside of static dir [%s]' 170 % (full_path, static_dir)) 171 self.send_error(404) 172 173 def redirect_to(self, url): 174 """ Redirect the HTTP client to a different url. """ 175 self.send_response(301) 176 self.send_header('Location', url) 177 self.end_headers() 178 179 def send_file(self, path): 180 """ Send the contents of the file at this path, with a mimetype based 181 on the filename extension. """ 182 # Grab the extension if there is one 183 extension = os.path.splitext(path)[1] 184 if len(extension) >= 1: 185 extension = extension[1:] 186 187 # Determine the MIME type of the file from its extension 188 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP['']) 189 190 # Open the file and send it over HTTP 191 if os.path.isfile(path): 192 with open(path, 'rb') as sending_file: 193 self.send_response(200) 194 self.send_header('Content-type', mime_type) 195 self.end_headers() 196 self.wfile.write(sending_file.read()) 197 else: 198 self.send_error(404) 199 200 def send_json_dict(self, json_dict): 201 """ Send the contents of this dictionary in JSON format, with a JSON 202 mimetype. """ 203 self.send_response(200) 204 self.send_header('Content-type', 'application/json') 205 self.end_headers() 206 json.dump(json_dict, self.wfile) 207 208 209def main(): 210 parser = argparse.ArgumentParser() 211 parser.add_argument('--actuals-dir', 212 help=('Directory into which we will check out the latest ' 213 'actual GM results. If this directory does not ' 214 'exist, it will be created. Defaults to %(default)s'), 215 default=DEFAULT_ACTUALS_DIR) 216 parser.add_argument('--expectations-dir', 217 help=('Directory under which to find GM expectations; ' 218 'defaults to %(default)s'), 219 default=DEFAULT_EXPECTATIONS_DIR) 220 parser.add_argument('--export', action='store_true', 221 help=('Instead of only allowing access from HTTP clients ' 222 'on localhost, allow HTTP clients on other hosts ' 223 'to access this server. WARNING: doing so will ' 224 'allow users on other hosts to modify your ' 225 'GM expectations!')) 226 parser.add_argument('--port', type=int, 227 help=('Which TCP port to listen on for HTTP requests; ' 228 'defaults to %(default)s'), 229 default=DEFAULT_PORT) 230 args = parser.parse_args() 231 global _SERVER 232 _SERVER = Server(expectations_dir=args.expectations_dir, 233 port=args.port, export=args.export) 234 _SERVER.run() 235 236if __name__ == '__main__': 237 main() 238