Upstream version 9.38.198.0
[platform/framework/web/crosswalk.git] / src / tools / telemetry / telemetry / core / memory_cache_http_server.py
1 # Copyright 2012 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
5 import BaseHTTPServer
6 import gzip
7 import mimetypes
8 import os
9 import SimpleHTTPServer
10 import SocketServer
11 import StringIO
12 import sys
13 import urlparse
14 from collections import namedtuple
15
16 from telemetry.core import local_server
17
18 ByteRange = namedtuple('ByteRange', ['from_byte', 'to_byte'])
19 ResourceAndRange = namedtuple('ResourceAndRange', ['resource', 'byte_range'])
20
21
22 class MemoryCacheHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
23
24   protocol_version = 'HTTP/1.1'  # override BaseHTTPServer setting
25   wbufsize = -1  # override StreamRequestHandler (a base class) setting
26
27   def do_GET(self):
28     """Serve a GET request."""
29     resource_range = self.SendHead()
30
31     if not resource_range or not resource_range.resource:
32       return
33     response = resource_range.resource['response']
34
35     if not resource_range.byte_range:
36       self.wfile.write(response)
37       return
38
39     start_index = resource_range.byte_range.from_byte
40     end_index = resource_range.byte_range.to_byte
41     self.wfile.write(response[start_index:end_index + 1])
42
43   def do_HEAD(self):
44     """Serve a HEAD request."""
45     self.SendHead()
46
47   def log_error(self, fmt, *args):
48     pass
49
50   def log_request(self, code='-', size='-'):
51     # Dont spam the console unless it is important.
52     pass
53
54   def SendHead(self):
55     path = os.path.realpath(self.translate_path(self.path))
56     if path not in self.server.resource_map:
57       self.send_error(404, 'File not found')
58       return None
59
60     resource = self.server.resource_map[path]
61     total_num_of_bytes = resource['content-length']
62     byte_range = self.GetByteRange(total_num_of_bytes)
63     if byte_range:
64       # request specified a range, so set response code to 206.
65       self.send_response(206)
66       self.send_header('Content-Range',
67                        'bytes %d-%d/%d' % (byte_range.from_byte,
68                                            byte_range.to_byte,
69                                            total_num_of_bytes))
70       total_num_of_bytes = byte_range.to_byte - byte_range.from_byte + 1
71     else:
72       self.send_response(200)
73
74     self.send_header('Content-Length', str(total_num_of_bytes))
75     self.send_header('Content-Type', resource['content-type'])
76     self.send_header('Last-Modified',
77                      self.date_time_string(resource['last-modified']))
78     if resource['zipped']:
79       self.send_header('Content-Encoding', 'gzip')
80     self.end_headers()
81     return ResourceAndRange(resource, byte_range)
82
83   def GetByteRange(self, total_num_of_bytes):
84     """Parse the header and get the range values specified.
85
86     Args:
87       total_num_of_bytes: Total # of bytes in requested resource,
88       used to calculate upper range limit.
89     Returns:
90       A ByteRange namedtuple object with the requested byte-range values.
91       If no Range is explicitly requested or there is a failure parsing,
92       return None.
93       If range specified is in the format "N-", return N-END. Refer to
94       http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for details.
95       If upper range limit is greater than total # of bytes, return upper index.
96     """
97
98     range_header = self.headers.getheader('Range')
99     if range_header is None:
100       return None
101     if not range_header.startswith('bytes='):
102       return None
103
104     # The range header is expected to be a string in this format:
105     # bytes=0-1
106     # Get the upper and lower limits of the specified byte-range.
107     # We've already confirmed that range_header starts with 'bytes='.
108     byte_range_values = range_header[len('bytes='):].split('-')
109     from_byte = 0
110     to_byte = 0
111
112     if len(byte_range_values) == 2:
113       # If to_range is not defined return all bytes starting from from_byte.
114       to_byte = (int(byte_range_values[1]) if  byte_range_values[1]
115           else total_num_of_bytes - 1)
116       # If from_range is not defined return last 'to_byte' bytes.
117       from_byte = (int(byte_range_values[0]) if byte_range_values[0]
118           else total_num_of_bytes - to_byte)
119     else:
120       return None
121
122     # Do some validation.
123     if from_byte < 0:
124       return None
125
126     # Make to_byte the end byte by default in edge cases.
127     if to_byte < from_byte or to_byte >= total_num_of_bytes:
128       to_byte = total_num_of_bytes - 1
129
130     return ByteRange(from_byte, to_byte)
131
132
133 class _MemoryCacheHTTPServerImpl(SocketServer.ThreadingMixIn,
134                                  BaseHTTPServer.HTTPServer):
135   # Increase the request queue size. The default value, 5, is set in
136   # SocketServer.TCPServer (the parent of BaseHTTPServer.HTTPServer).
137   # Since we're intercepting many domains through this single server,
138   # it is quite possible to get more than 5 concurrent requests.
139   request_queue_size = 128
140
141   # Don't prevent python from exiting when there is thread activity.
142   daemon_threads = True
143
144   def __init__(self, host_port, handler, paths):
145     BaseHTTPServer.HTTPServer.__init__(self, host_port, handler)
146     self.resource_map = {}
147     for path in paths:
148       if os.path.isdir(path):
149         self.AddDirectoryToResourceMap(path)
150       else:
151         self.AddFileToResourceMap(path)
152
153   def AddDirectoryToResourceMap(self, directory_path):
154     """Loads all files in directory_path into the in-memory resource map."""
155     for root, dirs, files in os.walk(directory_path):
156       # Skip hidden files and folders (like .svn and .git).
157       files = [f for f in files if f[0] != '.']
158       dirs[:] = [d for d in dirs if d[0] != '.']
159
160       for f in files:
161         file_path = os.path.join(root, f)
162         if not os.path.exists(file_path):  # Allow for '.#' files
163           continue
164         self.AddFileToResourceMap(file_path)
165
166   def AddFileToResourceMap(self, file_path):
167     """Loads file_path into the in-memory resource map."""
168     file_path = os.path.realpath(file_path)
169     if file_path in self.resource_map:
170       return
171
172     with open(file_path, 'rb') as fd:
173       response = fd.read()
174       fs = os.fstat(fd.fileno())
175     content_type = mimetypes.guess_type(file_path)[0]
176     zipped = False
177     if content_type in ['text/html', 'text/css', 'application/javascript']:
178       zipped = True
179       sio = StringIO.StringIO()
180       gzf = gzip.GzipFile(fileobj=sio, compresslevel=9, mode='wb')
181       gzf.write(response)
182       gzf.close()
183       response = sio.getvalue()
184       sio.close()
185     self.resource_map[file_path] = {
186         'content-type': content_type,
187         'content-length': len(response),
188         'last-modified': fs.st_mtime,
189         'response': response,
190         'zipped': zipped
191         }
192
193     index = 'index.html'
194     if os.path.basename(file_path) == index:
195       dir_path = os.path.dirname(file_path)
196       self.resource_map[dir_path] = self.resource_map[file_path]
197
198
199 class MemoryCacheHTTPServerBackend(local_server.LocalServerBackend):
200   def __init__(self):
201     super(MemoryCacheHTTPServerBackend, self).__init__()
202     self._httpd = None
203
204   def StartAndGetNamedPorts(self, args):
205     base_dir = args['base_dir']
206     os.chdir(base_dir)
207
208     paths = args['paths']
209     for path in paths:
210       if not os.path.realpath(path).startswith(os.path.realpath(os.getcwd())):
211         print >> sys.stderr, '"%s" is not under the cwd.' % path
212         sys.exit(1)
213
214     server_address = (args['host'], args['port'])
215     MemoryCacheHTTPRequestHandler.protocol_version = 'HTTP/1.1'
216     self._httpd = _MemoryCacheHTTPServerImpl(
217         server_address, MemoryCacheHTTPRequestHandler, paths)
218     return [local_server.NamedPort('http', self._httpd.server_address[1])]
219
220   def ServeForever(self):
221     return self._httpd.serve_forever()
222
223
224 class MemoryCacheHTTPServer(local_server.LocalServer):
225   def __init__(self, paths):
226     super(MemoryCacheHTTPServer, self).__init__(
227         MemoryCacheHTTPServerBackend)
228     self._base_dir = None
229
230     for path in paths:
231       assert os.path.exists(path), '%s does not exist.' % path
232
233     paths = list(paths)
234     self._paths = paths
235
236     self._paths_as_set = set(map(os.path.realpath, paths))
237
238     common_prefix = os.path.commonprefix(paths)
239     if os.path.isdir(common_prefix):
240       self._base_dir = common_prefix
241     else:
242       self._base_dir = os.path.dirname(common_prefix)
243
244   def GetBackendStartupArgs(self):
245     return {'base_dir': self._base_dir,
246             'paths': self._paths,
247             'host': self.host_ip,
248             'port': 0}
249
250   @property
251   def paths(self):
252     return self._paths_as_set
253
254   @property
255   def url(self):
256     return self.forwarder.url
257
258   def UrlOf(self, path):
259     relative_path = os.path.relpath(path, self._base_dir)
260     # Preserve trailing slash or backslash.
261     # It doesn't matter in a file path, but it does matter in a URL.
262     if path.endswith(os.sep) or (os.altsep and path.endswith(os.altsep)):
263       relative_path += '/'
264     return urlparse.urljoin(self.url, relative_path.replace(os.sep, '/'))