blob: 670c94de28b3dc715e1acc2c153a6388559fa88a [file] [log] [blame]
epoger@google.comf9d134d2013-09-27 15:02:44 +00001#!/usr/bin/python
2
epoger@google.com9fb6c8a2013-10-09 18:05:58 +00003"""
epoger@google.comf9d134d2013-09-27 15:02:44 +00004Copyright 2013 Google Inc.
5
6Use of this source code is governed by a BSD-style license that can be
7found in the LICENSE file.
epoger@google.comf9d134d2013-09-27 15:02:44 +00008
epoger@google.comf9d134d2013-09-27 15:02:44 +00009HTTP server for our HTML rebaseline viewer.
epoger@google.com9fb6c8a2013-10-09 18:05:58 +000010"""
epoger@google.comf9d134d2013-09-27 15:02:44 +000011
12# System-level imports
13import argparse
14import BaseHTTPServer
15import json
epoger@google.comdcb4e652013-10-11 18:45:33 +000016import logging
epoger@google.comf9d134d2013-09-27 15:02:44 +000017import os
18import posixpath
19import re
20import shutil
epoger@google.comb08c7072013-10-30 14:09:04 +000021import socket
rmistry@google.comd6bab022013-12-02 13:50:38 +000022import subprocess
epoger@google.comf9d134d2013-09-27 15:02:44 +000023import sys
epoger@google.com542b65f2013-10-15 20:10:33 +000024import thread
rmistry@google.comd6bab022013-12-02 13:50:38 +000025import threading
epoger@google.com542b65f2013-10-15 20:10:33 +000026import time
epoger@google.comdcb4e652013-10-11 18:45:33 +000027import urlparse
epoger@google.comf9d134d2013-09-27 15:02:44 +000028
29# Imports from within Skia
30#
31# We need to add the 'tools' directory, so that we can import svn.py within
32# that directory.
33# Make sure that the 'tools' dir is in the PYTHONPATH, but add it at the *end*
34# so any dirs that are already in the PYTHONPATH will be preferred.
epoger@google.comcb55f112013-10-02 19:27:35 +000035PARENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
36TRUNK_DIRECTORY = os.path.dirname(os.path.dirname(PARENT_DIRECTORY))
epoger@google.comf9d134d2013-09-27 15:02:44 +000037TOOLS_DIRECTORY = os.path.join(TRUNK_DIRECTORY, 'tools')
38if TOOLS_DIRECTORY not in sys.path:
39 sys.path.append(TOOLS_DIRECTORY)
40import svn
41
42# Imports from local dir
43import results
44
45ACTUALS_SVN_REPO = 'http://skia-autogen.googlecode.com/svn/gm-actual'
46PATHSPLIT_RE = re.compile('/([^/]+)/(.+)')
epoger@google.comb063e132013-11-25 18:06:29 +000047EXPECTATIONS_DIR = os.path.join(TRUNK_DIRECTORY, 'expectations', 'gm')
epoger@google.com9dddf6f2013-11-08 16:25:25 +000048GENERATED_IMAGES_ROOT = os.path.join(PARENT_DIRECTORY, 'static',
49 'generated-images')
epoger@google.comf9d134d2013-09-27 15:02:44 +000050
51# A simple dictionary of file name extensions to MIME types. The empty string
52# entry is used as the default when no extension was given or if the extension
53# has no entry in this dictionary.
54MIME_TYPE_MAP = {'': 'application/octet-stream',
55 'html': 'text/html',
56 'css': 'text/css',
57 'png': 'image/png',
58 'js': 'application/javascript',
59 'json': 'application/json'
60 }
61
62DEFAULT_ACTUALS_DIR = '.gm-actuals'
epoger@google.comf9d134d2013-09-27 15:02:44 +000063DEFAULT_PORT = 8888
64
epoger@google.comeb832592013-10-23 15:07:26 +000065_HTTP_HEADER_CONTENT_LENGTH = 'Content-Length'
66_HTTP_HEADER_CONTENT_TYPE = 'Content-Type'
67
epoger@google.comf9d134d2013-09-27 15:02:44 +000068_SERVER = None # This gets filled in by main()
69
rmistry@google.comd6bab022013-12-02 13:50:38 +000070
71def _run_command(args, directory):
72 """Runs a command and returns stdout as a single string.
73
74 Args:
75 args: the command to run, as a list of arguments
76 directory: directory within which to run the command
77
78 Returns: stdout, as a string
79
80 Raises an Exception if the command failed (exited with nonzero return code).
81 """
82 logging.debug('_run_command: %s in directory %s' % (args, directory))
83 proc = subprocess.Popen(args, cwd=directory,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 (stdout, stderr) = proc.communicate()
87 if proc.returncode is not 0:
88 raise Exception('command "%s" failed in dir "%s": %s' %
89 (args, directory, stderr))
90 return stdout
91
92
epoger@google.com591469b2013-11-20 19:58:06 +000093def _get_routable_ip_address():
epoger@google.comb08c7072013-10-30 14:09:04 +000094 """Returns routable IP address of this host (the IP address of its network
95 interface that would be used for most traffic, not its localhost
96 interface). See http://stackoverflow.com/a/166589 """
97 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
98 sock.connect(('8.8.8.8', 80))
99 host = sock.getsockname()[0]
100 sock.close()
101 return host
102
rmistry@google.comd6bab022013-12-02 13:50:38 +0000103
epoger@google.com591469b2013-11-20 19:58:06 +0000104def _create_svn_checkout(dir_path, repo_url):
105 """Creates local checkout of an SVN repository at the specified directory
106 path, returning an svn.Svn object referring to the local checkout.
107
108 Args:
109 dir_path: path to the local checkout; if this directory does not yet exist,
110 it will be created and the repo will be checked out into it
111 repo_url: URL of SVN repo to check out into dir_path (unless the local
112 checkout already exists)
113 Returns: an svn.Svn object referring to the local checkout.
114 """
115 local_checkout = svn.Svn(dir_path)
116 if not os.path.isdir(dir_path):
117 os.makedirs(dir_path)
118 local_checkout.Checkout(repo_url, '.')
119 return local_checkout
120
epoger@google.comb08c7072013-10-30 14:09:04 +0000121
epoger@google.comf9d134d2013-09-27 15:02:44 +0000122class Server(object):
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000123 """ HTTP server for our HTML rebaseline viewer. """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000124
epoger@google.comf9d134d2013-09-27 15:02:44 +0000125 def __init__(self,
126 actuals_dir=DEFAULT_ACTUALS_DIR,
epoger@google.com542b65f2013-10-15 20:10:33 +0000127 port=DEFAULT_PORT, export=False, editable=True,
128 reload_seconds=0):
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000129 """
130 Args:
131 actuals_dir: directory under which we will check out the latest actual
132 GM results
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000133 port: which TCP port to listen on for HTTP requests
134 export: whether to allow HTTP clients on other hosts to access this server
epoger@google.com542b65f2013-10-15 20:10:33 +0000135 editable: whether HTTP clients are allowed to submit new baselines
136 reload_seconds: polling interval with which to check for new results;
137 if 0, don't check for new results at all
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000138 """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000139 self._actuals_dir = actuals_dir
epoger@google.comf9d134d2013-09-27 15:02:44 +0000140 self._port = port
141 self._export = export
epoger@google.com542b65f2013-10-15 20:10:33 +0000142 self._editable = editable
143 self._reload_seconds = reload_seconds
epoger@google.com591469b2013-11-20 19:58:06 +0000144 self._actuals_repo = _create_svn_checkout(
145 dir_path=actuals_dir, repo_url=ACTUALS_SVN_REPO)
146
rmistry@google.comd6bab022013-12-02 13:50:38 +0000147 # Reentrant lock that must be held whenever updating EITHER of:
148 # 1. self._results
149 # 2. the expected or actual results on local disk
150 self.results_rlock = threading.RLock()
151 # self._results will be filled in by calls to update_results()
152 self._results = None
epoger@google.comf9d134d2013-09-27 15:02:44 +0000153
rmistry@google.comd6bab022013-12-02 13:50:38 +0000154 @property
155 def results(self):
156 """ Returns the most recently generated results, or None if update_results()
157 has not been called yet. """
158 return self._results
159
160 @property
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000161 def is_exported(self):
162 """ Returns true iff HTTP clients on other hosts are allowed to access
163 this server. """
164 return self._export
165
rmistry@google.comd6bab022013-12-02 13:50:38 +0000166 @property
epoger@google.com542b65f2013-10-15 20:10:33 +0000167 def is_editable(self):
168 """ Returns true iff HTTP clients are allowed to submit new baselines. """
169 return self._editable
epoger@google.comf9d134d2013-09-27 15:02:44 +0000170
rmistry@google.comd6bab022013-12-02 13:50:38 +0000171 @property
epoger@google.com542b65f2013-10-15 20:10:33 +0000172 def reload_seconds(self):
173 """ Returns the result reload period in seconds, or 0 if we don't reload
174 results. """
175 return self._reload_seconds
176
epoger@google.comeb832592013-10-23 15:07:26 +0000177 def update_results(self):
rmistry@google.comd6bab022013-12-02 13:50:38 +0000178 """ Create or update self._results, based on the expectations in
epoger@google.comb063e132013-11-25 18:06:29 +0000179 EXPECTATIONS_DIR and the latest actuals from skia-autogen.
rmistry@google.comd6bab022013-12-02 13:50:38 +0000180
181 We hold self.results_rlock while we do this, to guarantee that no other
182 thread attempts to update either self._results or the underlying files at
183 the same time.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000184 """
rmistry@google.comd6bab022013-12-02 13:50:38 +0000185 with self.results_rlock:
186 logging.info('Updating actual GM results in %s from SVN repo %s ...' % (
187 self._actuals_dir, ACTUALS_SVN_REPO))
188 self._actuals_repo.Update('.')
epoger@google.com591469b2013-11-20 19:58:06 +0000189
rmistry@google.comd6bab022013-12-02 13:50:38 +0000190 # We only update the expectations dir if the server was run with a
191 # nonzero --reload argument; otherwise, we expect the user to maintain
192 # her own expectations as she sees fit.
193 #
194 # Because the Skia repo is moving from SVN to git, and git does not
195 # support updating a single directory tree, we have to update the entire
196 # repo checkout.
197 #
198 # Because Skia uses depot_tools, we have to update using "gclient sync"
199 # instead of raw git (or SVN) update. Happily, this will work whether
200 # the checkout was created using git or SVN.
201 if self._reload_seconds:
202 logging.info(
203 'Updating expected GM results in %s by syncing Skia repo ...' %
204 EXPECTATIONS_DIR)
205 _run_command(['gclient', 'sync'], TRUNK_DIRECTORY)
206
epoger@google.com591469b2013-11-20 19:58:06 +0000207 logging.info(
epoger@google.com9dddf6f2013-11-08 16:25:25 +0000208 ('Parsing results from actuals in %s and expectations in %s, '
rmistry@google.comd6bab022013-12-02 13:50:38 +0000209 + 'and generating pixel diffs (may take a while) ...') % (
210 self._actuals_dir, EXPECTATIONS_DIR))
211 self._results = results.Results(
212 actuals_root=self._actuals_dir,
213 expected_root=EXPECTATIONS_DIR,
214 generated_images_root=GENERATED_IMAGES_ROOT)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000215
epoger@google.com542b65f2013-10-15 20:10:33 +0000216 def _result_reloader(self):
rmistry@google.comd6bab022013-12-02 13:50:38 +0000217 """ Reload results at the appropriate interval. This never exits, so it
218 should be run in its own thread.
epoger@google.com542b65f2013-10-15 20:10:33 +0000219 """
rmistry@google.comd6bab022013-12-02 13:50:38 +0000220 while True:
epoger@google.com542b65f2013-10-15 20:10:33 +0000221 time.sleep(self._reload_seconds)
epoger@google.comeb832592013-10-23 15:07:26 +0000222 self.update_results()
epoger@google.com542b65f2013-10-15 20:10:33 +0000223
epoger@google.comf9d134d2013-09-27 15:02:44 +0000224 def run(self):
epoger@google.comeb832592013-10-23 15:07:26 +0000225 self.update_results()
rmistry@google.comd6bab022013-12-02 13:50:38 +0000226 if self._reload_seconds:
227 thread.start_new_thread(self._result_reloader, ())
epoger@google.com542b65f2013-10-15 20:10:33 +0000228
epoger@google.comf9d134d2013-09-27 15:02:44 +0000229 if self._export:
230 server_address = ('', self._port)
epoger@google.com591469b2013-11-20 19:58:06 +0000231 host = _get_routable_ip_address()
epoger@google.com542b65f2013-10-15 20:10:33 +0000232 if self._editable:
233 logging.warning('Running with combination of "export" and "editable" '
234 'flags. Users on other machines will '
235 'be able to modify your GM expectations!')
epoger@google.comf9d134d2013-09-27 15:02:44 +0000236 else:
epoger@google.comb08c7072013-10-30 14:09:04 +0000237 host = '127.0.0.1'
238 server_address = (host, self._port)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000239 http_server = BaseHTTPServer.HTTPServer(server_address, HTTPRequestHandler)
epoger@google.com61952902013-11-08 17:23:54 +0000240 logging.info('Ready for requests on http://%s:%d' % (
241 host, http_server.server_port))
epoger@google.comf9d134d2013-09-27 15:02:44 +0000242 http_server.serve_forever()
243
244
245class HTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
246 """ HTTP request handlers for various types of queries this server knows
247 how to handle (static HTML and Javascript, expected/actual results, etc.)
248 """
249 def do_GET(self):
250 """ Handles all GET requests, forwarding them to the appropriate
251 do_GET_* dispatcher. """
252 if self.path == '' or self.path == '/' or self.path == '/index.html' :
epoger@google.com045c3d32013-11-01 16:46:41 +0000253 self.redirect_to('/static/index.html')
epoger@google.comf9d134d2013-09-27 15:02:44 +0000254 return
255 if self.path == '/favicon.ico' :
256 self.redirect_to('/static/favicon.ico')
257 return
258
259 # All requests must be of this form:
260 # /dispatcher/remainder
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000261 # where 'dispatcher' indicates which do_GET_* dispatcher to run
262 # and 'remainder' is the remaining path sent to that dispatcher.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000263 normpath = posixpath.normpath(self.path)
264 (dispatcher_name, remainder) = PATHSPLIT_RE.match(normpath).groups()
265 dispatchers = {
266 'results': self.do_GET_results,
267 'static': self.do_GET_static,
268 }
269 dispatcher = dispatchers[dispatcher_name]
270 dispatcher(remainder)
271
epoger@google.comdcb4e652013-10-11 18:45:33 +0000272 def do_GET_results(self, type):
epoger@google.comf9d134d2013-09-27 15:02:44 +0000273 """ Handle a GET request for GM results.
epoger@google.comf9d134d2013-09-27 15:02:44 +0000274
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000275 Args:
epoger@google.comdcb4e652013-10-11 18:45:33 +0000276 type: string indicating which set of results to return;
277 must be one of the results.RESULTS_* constants
278 """
279 logging.debug('do_GET_results: sending results of type "%s"' % type)
280 try:
epoger@google.com591469b2013-11-20 19:58:06 +0000281 # Since we must make multiple calls to the Results object, grab a
282 # reference to it in case it is updated to point at a new Results
283 # object within another thread.
284 #
epoger@google.comdcb4e652013-10-11 18:45:33 +0000285 # TODO(epoger): Rather than using a global variable for the handler
286 # to refer to the Server object, make Server a subclass of
287 # HTTPServer, and then it could be available to the handler via
288 # the handler's .server instance variable.
epoger@google.com591469b2013-11-20 19:58:06 +0000289 results_obj = _SERVER.results
290 response_dict = results_obj.get_results_of_type(type)
291 time_updated = results_obj.get_timestamp()
epoger@google.com542b65f2013-10-15 20:10:33 +0000292
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000293 response_dict['header'] = {
epoger@google.com542b65f2013-10-15 20:10:33 +0000294 # Timestamps:
295 # 1. when this data was last updated
296 # 2. when the caller should check back for new data (if ever)
297 #
298 # We only return these timestamps if the --reload argument was passed;
299 # otherwise, we have no idea when the expectations were last updated
300 # (we allow the user to maintain her own expectations as she sees fit).
rmistry@google.comd6bab022013-12-02 13:50:38 +0000301 'timeUpdated': time_updated if _SERVER.reload_seconds else None,
epoger@google.com542b65f2013-10-15 20:10:33 +0000302 'timeNextUpdateAvailable': (
rmistry@google.comd6bab022013-12-02 13:50:38 +0000303 (time_updated+_SERVER.reload_seconds) if _SERVER.reload_seconds
epoger@google.com542b65f2013-10-15 20:10:33 +0000304 else None),
305
epoger@google.comeb832592013-10-23 15:07:26 +0000306 # The type we passed to get_results_of_type()
307 'type': type,
308
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000309 # Hash of testData, which the client must return with any edits--
310 # this ensures that the edits were made to a particular dataset.
epoger@google.com542b65f2013-10-15 20:10:33 +0000311 'dataHash': str(hash(repr(response_dict['testData']))),
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000312
313 # Whether the server will accept edits back.
rmistry@google.comd6bab022013-12-02 13:50:38 +0000314 'isEditable': _SERVER.is_editable,
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000315
316 # Whether the service is accessible from other hosts.
rmistry@google.comd6bab022013-12-02 13:50:38 +0000317 'isExported': _SERVER.is_exported,
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000318 }
epoger@google.comf9d134d2013-09-27 15:02:44 +0000319 self.send_json_dict(response_dict)
epoger@google.comdcb4e652013-10-11 18:45:33 +0000320 except:
epoger@google.comf9d134d2013-09-27 15:02:44 +0000321 self.send_error(404)
epoger@google.com542b65f2013-10-15 20:10:33 +0000322 raise
epoger@google.comf9d134d2013-09-27 15:02:44 +0000323
324 def do_GET_static(self, path):
epoger@google.comcb55f112013-10-02 19:27:35 +0000325 """ Handle a GET request for a file under the 'static' directory.
326 Only allow serving of files within the 'static' directory that is a
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000327 filesystem sibling of this script.
328
329 Args:
330 path: path to file (under static directory) to retrieve
331 """
epoger@google.comdcb4e652013-10-11 18:45:33 +0000332 # Strip arguments ('?resultsToLoad=all') from the path
333 path = urlparse.urlparse(path).path
334
335 logging.debug('do_GET_static: sending file "%s"' % path)
epoger@google.comcb55f112013-10-02 19:27:35 +0000336 static_dir = os.path.realpath(os.path.join(PARENT_DIRECTORY, 'static'))
337 full_path = os.path.realpath(os.path.join(static_dir, path))
338 if full_path.startswith(static_dir):
339 self.send_file(full_path)
340 else:
epoger@google.comdcb4e652013-10-11 18:45:33 +0000341 logging.error(
342 'Attempted do_GET_static() of path [%s] outside of static dir [%s]'
343 % (full_path, static_dir))
epoger@google.comcb55f112013-10-02 19:27:35 +0000344 self.send_error(404)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000345
epoger@google.comeb832592013-10-23 15:07:26 +0000346 def do_POST(self):
347 """ Handles all POST requests, forwarding them to the appropriate
348 do_POST_* dispatcher. """
349 # All requests must be of this form:
350 # /dispatcher
351 # where 'dispatcher' indicates which do_POST_* dispatcher to run.
352 normpath = posixpath.normpath(self.path)
353 dispatchers = {
354 '/edits': self.do_POST_edits,
355 }
356 try:
357 dispatcher = dispatchers[normpath]
358 dispatcher()
359 self.send_response(200)
360 except:
361 self.send_error(404)
362 raise
363
364 def do_POST_edits(self):
365 """ Handle a POST request with modifications to GM expectations, in this
366 format:
367
368 {
369 'oldResultsType': 'all', # type of results that the client loaded
370 # and then made modifications to
371 'oldResultsHash': 39850913, # hash of results when the client loaded them
372 # (ensures that the client and server apply
373 # modifications to the same base)
374 'modifications': [
375 {
376 'builder': 'Test-Android-Nexus10-MaliT604-Arm7-Debug',
377 'test': 'strokerect',
378 'config': 'gpu',
379 'expectedHashType': 'bitmap-64bitMD5',
380 'expectedHashDigest': '1707359671708613629',
381 },
382 ...
383 ],
384 }
385
386 Raises an Exception if there were any problems.
387 """
rmistry@google.comd6bab022013-12-02 13:50:38 +0000388 if not _SERVER.is_editable:
epoger@google.comeb832592013-10-23 15:07:26 +0000389 raise Exception('this server is not running in --editable mode')
390
391 content_type = self.headers[_HTTP_HEADER_CONTENT_TYPE]
392 if content_type != 'application/json;charset=UTF-8':
393 raise Exception('unsupported %s [%s]' % (
394 _HTTP_HEADER_CONTENT_TYPE, content_type))
395
396 content_length = int(self.headers[_HTTP_HEADER_CONTENT_LENGTH])
397 json_data = self.rfile.read(content_length)
398 data = json.loads(json_data)
399 logging.debug('do_POST_edits: received new GM expectations data [%s]' %
400 data)
401
rmistry@google.comd6bab022013-12-02 13:50:38 +0000402 # Update the results on disk with the information we received from the
403 # client.
404 # We must hold _SERVER.results_rlock while we do this, to guarantee that
405 # no other thread updates expectations (from the Skia repo) while we are
406 # updating them (using the info we received from the client).
407 with _SERVER.results_rlock:
408 oldResultsType = data['oldResultsType']
409 oldResults = _SERVER.results.get_results_of_type(oldResultsType)
410 oldResultsHash = str(hash(repr(oldResults['testData'])))
411 if oldResultsHash != data['oldResultsHash']:
412 raise Exception('results of type "%s" changed while the client was '
413 'making modifications. The client should reload the '
414 'results and submit the modifications again.' %
415 oldResultsType)
416 _SERVER.results.edit_expectations(data['modifications'])
417 # Read the updated results back from disk.
418 _SERVER.update_results()
epoger@google.comeb832592013-10-23 15:07:26 +0000419
epoger@google.comf9d134d2013-09-27 15:02:44 +0000420 def redirect_to(self, url):
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000421 """ Redirect the HTTP client to a different url.
422
423 Args:
424 url: URL to redirect the HTTP client to
425 """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000426 self.send_response(301)
427 self.send_header('Location', url)
428 self.end_headers()
429
430 def send_file(self, path):
431 """ Send the contents of the file at this path, with a mimetype based
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000432 on the filename extension.
433
434 Args:
435 path: path of file whose contents to send to the HTTP client
436 """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000437 # Grab the extension if there is one
438 extension = os.path.splitext(path)[1]
439 if len(extension) >= 1:
440 extension = extension[1:]
441
442 # Determine the MIME type of the file from its extension
443 mime_type = MIME_TYPE_MAP.get(extension, MIME_TYPE_MAP[''])
444
445 # Open the file and send it over HTTP
446 if os.path.isfile(path):
447 with open(path, 'rb') as sending_file:
448 self.send_response(200)
449 self.send_header('Content-type', mime_type)
450 self.end_headers()
451 self.wfile.write(sending_file.read())
452 else:
453 self.send_error(404)
454
455 def send_json_dict(self, json_dict):
456 """ Send the contents of this dictionary in JSON format, with a JSON
epoger@google.com9fb6c8a2013-10-09 18:05:58 +0000457 mimetype.
458
459 Args:
460 json_dict: dictionary to send
461 """
epoger@google.comf9d134d2013-09-27 15:02:44 +0000462 self.send_response(200)
463 self.send_header('Content-type', 'application/json')
464 self.end_headers()
465 json.dump(json_dict, self.wfile)
466
467
468def main():
epoger@google.comdcb4e652013-10-11 18:45:33 +0000469 logging.basicConfig(level=logging.INFO)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000470 parser = argparse.ArgumentParser()
471 parser.add_argument('--actuals-dir',
472 help=('Directory into which we will check out the latest '
473 'actual GM results. If this directory does not '
474 'exist, it will be created. Defaults to %(default)s'),
475 default=DEFAULT_ACTUALS_DIR)
epoger@google.com542b65f2013-10-15 20:10:33 +0000476 parser.add_argument('--editable', action='store_true',
epoger@google.comeb832592013-10-23 15:07:26 +0000477 help=('Allow HTTP clients to submit new baselines.'))
epoger@google.comf9d134d2013-09-27 15:02:44 +0000478 parser.add_argument('--export', action='store_true',
479 help=('Instead of only allowing access from HTTP clients '
480 'on localhost, allow HTTP clients on other hosts '
481 'to access this server. WARNING: doing so will '
482 'allow users on other hosts to modify your '
epoger@google.com542b65f2013-10-15 20:10:33 +0000483 'GM expectations, if combined with --editable.'))
epoger@google.comafaad3d2013-09-30 15:06:25 +0000484 parser.add_argument('--port', type=int,
485 help=('Which TCP port to listen on for HTTP requests; '
486 'defaults to %(default)s'),
487 default=DEFAULT_PORT)
epoger@google.com542b65f2013-10-15 20:10:33 +0000488 parser.add_argument('--reload', type=int,
489 help=('How often (a period in seconds) to update the '
epoger@google.comb063e132013-11-25 18:06:29 +0000490 'results. If specified, both expected and actual '
rmistry@google.comd6bab022013-12-02 13:50:38 +0000491 'results will be updated by running "gclient sync" '
492 'on your Skia checkout as a whole. '
epoger@google.com542b65f2013-10-15 20:10:33 +0000493 'By default, we do not reload at all, and you '
494 'must restart the server to pick up new data.'),
495 default=0)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000496 args = parser.parse_args()
497 global _SERVER
epoger@google.com542b65f2013-10-15 20:10:33 +0000498 _SERVER = Server(actuals_dir=args.actuals_dir,
epoger@google.com542b65f2013-10-15 20:10:33 +0000499 port=args.port, export=args.export, editable=args.editable,
500 reload_seconds=args.reload)
epoger@google.comf9d134d2013-09-27 15:02:44 +0000501 _SERVER.run()
502
rmistry@google.comd6bab022013-12-02 13:50:38 +0000503
epoger@google.comf9d134d2013-09-27 15:02:44 +0000504if __name__ == '__main__':
505 main()