1#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6import json
7import os
8import socket
9import sys
10import traceback
11
12from tvcm import project as project_module
13
14import SocketServer
15import SimpleHTTPServer
16import BaseHTTPServer
17
18TEST_DATA_PREFIX = '/test_data'
19
20
21class DevServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
22
23  def __init__(self, *args, **kwargs):
24    SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, *args, **kwargs)
25
26  def send_response(self, code, message=None):
27    SimpleHTTPServer.SimpleHTTPRequestHandler.send_response(self, code, message)
28    if code == 200:
29      self.send_header('Cache-Control', 'no-cache')
30
31  def do_GET(self):
32    if self.do_path_handler('GET'):
33      return
34
35    return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
36
37  def do_POST(self):
38    if self.do_path_handler('POST'):
39      return
40    return SimpleHTTPServer.SimpleHTTPRequestHandler.do_POST(self)
41
42  def do_path_handler(self, method):
43    handler = self.server.GetPathHandler(self.path, method)
44    if handler:
45      try:
46        handler(self)
47      except Exception, ex:
48        send_500(self, 'While parsing %s' % self.path, ex, path=self.path)
49      return True
50    return False
51
52  def send_head(self):
53    return SimpleHTTPServer.SimpleHTTPRequestHandler.send_head(self)
54
55  def translate_path(self, path):
56    path = path.split('?', 1)[0]
57    path = path.split('#', 1)[0]
58
59    if path.startswith(TEST_DATA_PREFIX):
60      path = path[len(TEST_DATA_PREFIX):]
61
62    for mapped_path in self.server.project.source_paths:
63      rel = os.path.relpath(path, '/')
64      candidate = os.path.join(mapped_path, rel)
65      if os.path.exists(candidate):
66        return candidate
67    return ''
68
69  def log_error(self, log_format, *args):
70    if self.server._quiet:
71      return
72    if self.path == '/favicon.ico':
73      return
74    self.log_message('While processing %s: ', self.path)
75    SimpleHTTPServer.SimpleHTTPRequestHandler.log_error(self, log_format, *args)
76
77  def log_request(self, code='-', size='-'):
78    # Don't spam the console unless it is important.
79    pass
80
81  def finish(self):
82    try:
83      SimpleHTTPServer.SimpleHTTPRequestHandler.finish(self)
84    except socket.error:
85        # An final socket error may have occurred here, such as
86        # the local error ECONNABORTED.
87        pass
88
89
90def send_500(self, msg, ex, log_error=True, path=None):
91  if path is None:
92    is_html_output = False
93  else:
94    path = path.split('?', 1)[0]
95    path = path.split('#', 1)[0]
96    is_html_output = path.endswith('.html')
97
98  if is_html_output:
99    msg = """<!DOCTYPE html>
100    <html>
101    <body>
102    <h1>OMG something is wrong</h1>
103    <b><pre><code id="message"></code></pre></b></p>
104    <pre><code id="details"></code></pre>
105    <script>
106    document.addEventListener('DOMContentLoaded', function() {
107      document.querySelector('#details').textContent = %s;
108      document.querySelector('#message').textContent = %s;
109      });
110    </script>
111    </body>
112    </html>
113""" % (json.dumps(traceback.format_exc()), json.dumps(ex.message))
114    ctype = 'text/html'
115  else:
116    msg = json.dumps({'details': traceback.format_exc(),
117                      'message': ex.message})
118    ctype = 'application/json'
119
120  if log_error:
121    self.log_error('%s: %s', msg, ex.message)
122  self.send_response(500)
123  self.send_header('Content-Type', ctype)
124  self.send_header('Cache-Control', 'no-cache')
125  self.send_header('Content-Length', len(msg))
126  self.end_headers()
127  self.wfile.write(msg)
128  return
129
130
131class PathHandler(object):
132  def __init__(self, path, handler, supports_get, supports_post):
133    self.path = path
134    self.handler = handler
135    self.supports_get = supports_get
136    self.supports_post = supports_post
137
138  def CanHandle(self, path, method):
139    if path != self.path:
140      return False
141    if method == 'GET' and self.supports_get:
142      return True
143    if method == 'POST' and self.supports_post:
144      return True
145    return False
146
147
148def do_GET_root(request):
149  request.send_response(301)
150  request.send_header('Location', request.server.default_path)
151  request.end_headers()
152
153
154class DevServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
155
156  def __init__(self, port, quiet=False, project=None):
157    BaseHTTPServer.HTTPServer.__init__(
158        self, ('localhost', port), DevServerHandler)
159    self._shutdown_request = None
160    self._quiet = quiet
161    if port == 0:
162      port = self.server_address[1]
163    self._port = port
164    self._path_handlers = []
165    if project:
166      self._project = project
167    else:
168      self._project = project_module.Project([])
169
170    self.AddPathHandler('/', do_GET_root)
171    self.AddPathHandler('', do_GET_root)
172    self.default_path = '/base/tests.html'
173    # Redirect old tests.html places to the new location until folks have
174    # gotten used to its new location.
175    self.AddPathHandler('/tvcm/tests.html', do_GET_root)
176    self.AddPathHandler('/tests.html', do_GET_root)
177
178  def AddPathHandler(self, path, handler,
179                     supports_get=True, supports_post=False):
180    self._path_handlers.append(
181        PathHandler(path, handler, supports_get, supports_post))
182
183  def GetPathHandler(self, path, method):
184    for h in self._path_handlers:
185      if h.CanHandle(path, method):
186        return h.handler
187    return None
188
189  def AddSourcePathMapping(self, file_system_path):
190    self._project.AddSourcePath(file_system_path)
191
192  def RequestShutdown(self, exit_code):
193    self._shutdown_request = exit_code
194
195  @property
196  def project(self):
197    return self._project
198
199  @property
200  def loader(self):
201    return self._project.loader
202
203  @property
204  def port(self):
205    return self._port
206
207  @property
208  def data_dir(self):
209    return self._data_dir
210
211  def serve_forever(self):  # pylint: disable=arguments-differ
212    if not self._quiet:
213      sys.stderr.write('Now running on http://localhost:%i\n' % self._port)
214    try:
215      self.timeout = 0.5
216      while True:
217        BaseHTTPServer.HTTPServer.handle_request(self)
218        if self._shutdown_request is not None:
219          sys.exit(self._shutdown_request)
220    except KeyboardInterrupt:
221      sys.exit(0)
222