OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 | 2 |
3 """ | 3 """ |
4 Copyright 2013 Google Inc. | 4 Copyright 2013 Google Inc. |
5 | 5 |
6 Use of this source code is governed by a BSD-style license that can be | 6 Use of this source code is governed by a BSD-style license that can be |
7 found in the LICENSE file. | 7 found in the LICENSE file. |
8 | 8 |
9 HTTP server for our HTML rebaseline viewer. | 9 HTTP server for our HTML rebaseline viewer. |
10 """ | 10 """ |
11 | 11 |
12 # System-level imports | 12 # System-level imports |
13 import argparse | 13 import argparse |
14 import BaseHTTPServer | 14 import BaseHTTPServer |
15 import json | 15 import json |
| 16 import logging |
16 import os | 17 import os |
17 import posixpath | 18 import posixpath |
18 import re | 19 import re |
19 import shutil | 20 import shutil |
20 import sys | 21 import sys |
| 22 import urlparse |
21 | 23 |
22 # Imports from within Skia | 24 # Imports from within Skia |
23 # | 25 # |
24 # We need to add the 'tools' directory, so that we can import svn.py within | 26 # We need to add the 'tools' directory, so that we can import svn.py within |
25 # that directory. | 27 # that directory. |
26 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* | 28 # Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end* |
27 # so any dirs that are already in the PYTHONPATH will be preferred. | 29 # so any dirs that are already in the PYTHONPATH will be preferred. |
28 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) | 30 PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) |
29 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) | 31 TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY)) |
30 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') | 32 TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools') |
(...skipping 53 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
84 return self._export | 86 return self._export |
85 | 87 |
86 def fetch_results(self): | 88 def fetch_results(self): |
87 """ Create self.results, based on the expectations in | 89 """ Create self.results, based on the expectations in |
88 self._expectations_dir and the latest actuals from skia-autogen. | 90 self._expectations_dir and the latest actuals from skia-autogen. |
89 | 91 |
90 TODO(epoger): Add a new --browseonly mode setting. In that mode, | 92 TODO(epoger): Add a new --browseonly mode setting. In that mode, |
91 the gm-actuals and expectations will automatically be updated every few | 93 the gm-actuals and expectations will automatically be updated every few |
92 minutes. See discussion in https://codereview.chromium.org/24274003/ . | 94 minutes. See discussion in https://codereview.chromium.org/24274003/ . |
93 """ | 95 """ |
94 print 'Checking out latest actual GM results from %s into %s ...' % ( | 96 logging.info('Checking out latest actual GM results from %s into %s ...' % ( |
95 ACTUALS_SVN_REPO, self._actuals_dir) | 97 ACTUALS_SVN_REPO, self._actuals_dir)) |
96 actuals_repo = svn.Svn(self._actuals_dir) | 98 actuals_repo = svn.Svn(self._actuals_dir) |
97 if not os.path.isdir(self._actuals_dir): | 99 if not os.path.isdir(self._actuals_dir): |
98 os.makedirs(self._actuals_dir) | 100 os.makedirs(self._actuals_dir) |
99 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') | 101 actuals_repo.Checkout(ACTUALS_SVN_REPO, '.') |
100 else: | 102 else: |
101 actuals_repo.Update('.') | 103 actuals_repo.Update('.') |
102 print 'Parsing results from actuals in %s and expectations in %s ...' % ( | 104 logging.info( |
103 self._actuals_dir, self._expectations_dir) | 105 'Parsing results from actuals in %s and expectations in %s ...' % ( |
| 106 self._actuals_dir, self._expectations_dir)) |
104 self.results = results.Results( | 107 self.results = results.Results( |
105 actuals_root=self._actuals_dir, | 108 actuals_root=self._actuals_dir, |
106 expected_root=self._expectations_dir) | 109 expected_root=self._expectations_dir) |
107 | 110 |
108 def run(self): | 111 def run(self): |
109 self.fetch_results() | 112 self.fetch_results() |
110 if self._export: | 113 if self._export: |
111 server_address = ('', self._port) | 114 server_address = ('', self._port) |
112 print ('WARNING: Running in "export" mode. Users on other machines will ' | 115 logging.warning('Running in "export" mode. Users on other machines will ' |
113 'be able to modify your GM expectations!') | 116 'be able to modify your GM expectations!') |
114 else: | 117 else: |
115 server_address = ('127.0.0.1', self._port) | 118 server_address = ('127.0.0.1', self._port) |
116 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) | 119 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler) |
117 print 'Ready for requests on http://%s:%d' % ( | 120 logging.info('Ready for requests on http://%s:%d' % ( |
118 http_server.server_name, http_server.server_port) | 121 http_server.server_name, http_server.server_port)) |
119 http_server.serve_forever() | 122 http_server.serve_forever() |
120 | 123 |
121 | 124 |
122 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): | 125 class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): |
123 """ HTTP request handlers for various types of queries this server knows | 126 """ HTTP request handlers for various types of queries this server knows |
124 how to handle (static HTML and Javascript, expected/actual results, etc.) | 127 how to handle (static HTML and Javascript, expected/actual results, etc.) |
125 """ | 128 """ |
126 def do_GET(self): | 129 def do_GET(self): |
127 """ Handles all GET requests, forwarding them to the appropriate | 130 """ Handles all GET requests, forwarding them to the appropriate |
128 do_GET_* dispatcher. """ | 131 do_GET_* dispatcher. """ |
129 if self.path == '' or self.path == '/' or self.path == '/index.html' : | 132 if self.path == '' or self.path == '/' or self.path == '/index.html' : |
130 self.redirect_to('/static/view.html') | 133 self.redirect_to('/static/view.html?resultsToLoad=all') |
131 return | 134 return |
132 if self.path == '/favicon.ico' : | 135 if self.path == '/favicon.ico' : |
133 self.redirect_to('/static/favicon.ico') | 136 self.redirect_to('/static/favicon.ico') |
134 return | 137 return |
135 | 138 |
136 # All requests must be of this form: | 139 # All requests must be of this form: |
137 # /dispatcher/remainder | 140 # /dispatcher/remainder |
138 # where 'dispatcher' indicates which do_GET_* dispatcher to run | 141 # where 'dispatcher' indicates which do_GET_* dispatcher to run |
139 # and 'remainder' is the remaining path sent to that dispatcher. | 142 # and 'remainder' is the remaining path sent to that dispatcher. |
140 normpath = posixpath.normpath(self.path) | 143 normpath = posixpath.normpath(self.path) |
141 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() | 144 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups() |
142 dispatchers = { | 145 dispatchers = { |
143 'results': self.do_GET_results, | 146 'results': self.do_GET_results, |
144 'static': self.do_GET_static, | 147 'static': self.do_GET_static, |
145 } | 148 } |
146 dispatcher = dispatchers[dispatcher_name] | 149 dispatcher = dispatchers[dispatcher_name] |
147 dispatcher(remainder) | 150 dispatcher(remainder) |
148 | 151 |
149 def do_GET_results(self, result_type): | 152 def do_GET_results(self, type): |
150 """ Handle a GET request for GM results. | 153 """ Handle a GET request for GM results. |
151 For now, we ignore the remaining path info, because we only know how to | |
152 return all results. | |
153 | 154 |
154 Args: | 155 Args: |
155 result_type: currently unused | 156 type: string indicating which set of results to return; |
156 | 157 must be one of the results.RESULTS_* constants |
157 TODO(epoger): Unless we start making use of result_type, remove that | 158 """ |
158 parameter.""" | 159 logging.debug('do_GET_results: sending results of type "%s"' % type) |
159 print 'do_GET_results: sending results of type "%s"' % result_type | 160 try: |
160 # TODO(epoger): Cache response_dict rather than the results object, to save | 161 # TODO(epoger): Rather than using a global variable for the handler |
161 # time on subsequent fetches (no need to regenerate the header, etc.) | 162 # to refer to the Server object, make Server a subclass of |
162 response_dict = _SERVER.results.GetAll() | 163 # HTTPServer, and then it could be available to the handler via |
163 if response_dict: | 164 # the handler's .server instance variable. |
| 165 response_dict = _SERVER.results.get_results_of_type(type) |
164 response_dict['header'] = { | 166 response_dict['header'] = { |
165 # Hash of testData, which the client must return with any edits-- | 167 # Hash of testData, which the client must return with any edits-- |
166 # this ensures that the edits were made to a particular dataset. | 168 # this ensures that the edits were made to a particular dataset. |
167 'data-hash': str(hash(repr(response_dict['testData']))), | 169 'data-hash': str(hash(repr(response_dict['testData']))), |
168 | 170 |
169 # Whether the server will accept edits back. | 171 # Whether the server will accept edits back. |
170 # TODO(epoger): Not yet implemented, so hardcoding to False; | 172 # TODO(epoger): Not yet implemented, so hardcoding to False; |
171 # once we implement the 'browseonly' mode discussed in | 173 # once we implement the 'browseonly' mode discussed in |
172 # https://codereview.chromium.org/24274003/#msg6 , this value will vary. | 174 # https://codereview.chromium.org/24274003/#msg6 , this value will vary. |
173 'isEditable': False, | 175 'isEditable': False, |
174 | 176 |
175 # Whether the service is accessible from other hosts. | 177 # Whether the service is accessible from other hosts. |
176 'isExported': _SERVER.is_exported(), | 178 'isExported': _SERVER.is_exported(), |
177 } | 179 } |
178 self.send_json_dict(response_dict) | 180 self.send_json_dict(response_dict) |
179 else: | 181 except: |
180 self.send_error(404) | 182 self.send_error(404) |
181 | 183 |
182 def do_GET_static(self, path): | 184 def do_GET_static(self, path): |
183 """ Handle a GET request for a file under the 'static' directory. | 185 """ Handle a GET request for a file under the 'static' directory. |
184 Only allow serving of files within the 'static' directory that is a | 186 Only allow serving of files within the 'static' directory that is a |
185 filesystem sibling of this script. | 187 filesystem sibling of this script. |
186 | 188 |
187 Args: | 189 Args: |
188 path: path to file (under static directory) to retrieve | 190 path: path to file (under static directory) to retrieve |
189 """ | 191 """ |
190 print 'do_GET_static: sending file "%s"' % path | 192 # Strip arguments ('?resultsToLoad=all') from the path |
| 193 path = urlparse.urlparse(path).path |
| 194 |
| 195 logging.debug('do_GET_static: sending file "%s"' % path) |
191 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) | 196 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static')) |
192 full_path = os.path.realpath(os.path.join(static_dir, path)) | 197 full_path = os.path.realpath(os.path.join(static_dir, path)) |
193 if full_path.startswith(static_dir): | 198 if full_path.startswith(static_dir): |
194 self.send_file(full_path) | 199 self.send_file(full_path) |
195 else: | 200 else: |
196 print ('Attempted do_GET_static() of path [%s] outside of static dir [%s]' | 201 logging.error( |
197 % (full_path, static_dir)) | 202 'Attempted do_GET_static() of path [%s] outside of static dir [%s]' |
| 203 % (full_path, static_dir)) |
198 self.send_error(404) | 204 self.send_error(404) |
199 | 205 |
200 def redirect_to(self, url): | 206 def redirect_to(self, url): |
201 """ Redirect the HTTP client to a different url. | 207 """ Redirect the HTTP client to a different url. |
202 | 208 |
203 Args: | 209 Args: |
204 url: URL to redirect the HTTP client to | 210 url: URL to redirect the HTTP client to |
205 """ | 211 """ |
206 self.send_response(301) | 212 self.send_response(301) |
207 self.send_header('Location', url) | 213 self.send_header('Location', url) |
(...skipping 31 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
239 Args: | 245 Args: |
240 json_dict: dictionary to send | 246 json_dict: dictionary to send |
241 """ | 247 """ |
242 self.send_response(200) | 248 self.send_response(200) |
243 self.send_header('Content-type', 'application/json') | 249 self.send_header('Content-type', 'application/json') |
244 self.end_headers() | 250 self.end_headers() |
245 json.dump(json_dict, self.wfile) | 251 json.dump(json_dict, self.wfile) |
246 | 252 |
247 | 253 |
248 def main(): | 254 def main(): |
| 255 logging.basicConfig(level=logging.INFO) |
249 parser = argparse.ArgumentParser() | 256 parser = argparse.ArgumentParser() |
250 parser.add_argument('--actuals-dir', | 257 parser.add_argument('--actuals-dir', |
251 help=('Directory into which we will check out the latest ' | 258 help=('Directory into which we will check out the latest ' |
252 'actual GM results. If this directory does not ' | 259 'actual GM results. If this directory does not ' |
253 'exist, it will be created. Defaults to %(default)s'), | 260 'exist, it will be created. Defaults to %(default)s'), |
254 default=DEFAULT_ACTUALS_DIR) | 261 default=DEFAULT_ACTUALS_DIR) |
255 parser.add_argument('--expectations-dir', | 262 parser.add_argument('--expectations-dir', |
256 help=('Directory under which to find GM expectations; ' | 263 help=('Directory under which to find GM expectations; ' |
257 'defaults to %(default)s'), | 264 'defaults to %(default)s'), |
258 default=DEFAULT_EXPECTATIONS_DIR) | 265 default=DEFAULT_EXPECTATIONS_DIR) |
259 parser.add_argument('--export', action='store_true', | 266 parser.add_argument('--export', action='store_true', |
260 help=('Instead of only allowing access from HTTP clients ' | 267 help=('Instead of only allowing access from HTTP clients ' |
261 'on localhost, allow HTTP clients on other hosts ' | 268 'on localhost, allow HTTP clients on other hosts ' |
262 'to access this server. WARNING: doing so will ' | 269 'to access this server. WARNING: doing so will ' |
263 'allow users on other hosts to modify your ' | 270 'allow users on other hosts to modify your ' |
264 'GM expectations!')) | 271 'GM expectations!')) |
265 parser.add_argument('--port', type=int, | 272 parser.add_argument('--port', type=int, |
266 help=('Which TCP port to listen on for HTTP requests; ' | 273 help=('Which TCP port to listen on for HTTP requests; ' |
267 'defaults to %(default)s'), | 274 'defaults to %(default)s'), |
268 default=DEFAULT_PORT) | 275 default=DEFAULT_PORT) |
269 args = parser.parse_args() | 276 args = parser.parse_args() |
270 global _SERVER | 277 global _SERVER |
271 _SERVER = Server(expectations_dir=args.expectations_dir, | 278 _SERVER = Server(expectations_dir=args.expectations_dir, |
272 port=args.port, export=args.export) | 279 port=args.port, export=args.export) |
273 _SERVER.run() | 280 _SERVER.run() |
274 | 281 |
275 if __name__ == '__main__': | 282 if __name__ == '__main__': |
276 main() | 283 main() |
OLD | NEW |