| Index: ppapi/native_client/tools/browser_tester/browsertester/server.py
|
| diff --git a/ppapi/native_client/tools/browser_tester/browsertester/server.py b/ppapi/native_client/tools/browser_tester/browsertester/server.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ff885808b057ccf9b127e304513825b3755b6402
|
| --- /dev/null
|
| +++ b/ppapi/native_client/tools/browser_tester/browsertester/server.py
|
| @@ -0,0 +1,246 @@
|
| +# Copyright (c) 2011 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +import BaseHTTPServer
|
| +import cgi
|
| +import mimetypes
|
| +import os
|
| +import os.path
|
| +import posixpath
|
| +import SimpleHTTPServer
|
| +import SocketServer
|
| +import threading
|
| +import time
|
| +import urllib
|
| +import urlparse
|
| +
|
| +class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
| +
|
| + def NormalizePath(self, path):
|
| + path = path.split('?', 1)[0]
|
| + path = path.split('#', 1)[0]
|
| + path = posixpath.normpath(urllib.unquote(path))
|
| + words = path.split('/')
|
| +
|
| + bad = set((os.curdir, os.pardir, ''))
|
| + words = [word for word in words if word not in bad]
|
| + # The path of the request should always use POSIX-style path separators, so
|
| + # that the filename input of --map_file can be a POSIX-style path and still
|
| + # match correctly in translate_path().
|
| + return '/'.join(words)
|
| +
|
| + def translate_path(self, path):
|
| + path = self.NormalizePath(path)
|
| + if path in self.server.file_mapping:
|
| + return self.server.file_mapping[path]
|
| + for extra_dir in self.server.serving_dirs:
|
| + # TODO(halyavin): set allowed paths in another parameter?
|
| + full_path = os.path.join(extra_dir, os.path.basename(path))
|
| + if os.path.isfile(full_path):
|
| + return full_path
|
| + if not path.endswith('favicon.ico') and not self.server.allow_404:
|
| + self.server.listener.ServerError('Cannot find file \'%s\'' % path)
|
| + return path
|
| +
|
| + def guess_type(self, path):
|
| + # We store the extension -> MIME type mapping in the server instead of the
|
| + # request handler so we that can add additional mapping entries via the
|
| + # command line.
|
| + base, ext = posixpath.splitext(path)
|
| + if ext in self.server.extensions_mapping:
|
| + return self.server.extensions_mapping[ext]
|
| + ext = ext.lower()
|
| + if ext in self.server.extensions_mapping:
|
| + return self.server.extensions_mapping[ext]
|
| + else:
|
| + return self.server.extensions_mapping['']
|
| +
|
| + def SendRPCResponse(self, response):
|
| + self.send_response(200)
|
| + self.send_header("Content-type", "text/plain")
|
| + self.send_header("Content-length", str(len(response)))
|
| + self.end_headers()
|
| + self.wfile.write(response)
|
| +
|
| + # shut down the connection
|
| + self.wfile.flush()
|
| + self.connection.shutdown(1)
|
| +
|
| + def HandleRPC(self, name, query):
|
| + kargs = {}
|
| + for k, v in query.iteritems():
|
| + assert len(v) == 1, k
|
| + kargs[k] = v[0]
|
| +
|
| + l = self.server.listener
|
| + try:
|
| + response = getattr(l, name)(**kargs)
|
| + except Exception, e:
|
| + self.SendRPCResponse('%r' % (e,))
|
| + raise
|
| + else:
|
| + self.SendRPCResponse(response)
|
| +
|
| + # For Last-Modified-based caching, the timestamp needs to be old enough
|
| + # for the browser cache to be used (at least 60 seconds).
|
| + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
|
| + # Often we clobber and regenerate files for testing, so this is needed
|
| + # to actually use the browser cache.
|
| + def send_header(self, keyword, value):
|
| + if keyword == 'Last-Modified':
|
| + last_mod_format = '%a, %d %b %Y %H:%M:%S GMT'
|
| + old_value_as_t = time.strptime(value, last_mod_format)
|
| + old_value_in_secs = time.mktime(old_value_as_t)
|
| + new_value_in_secs = old_value_in_secs - 360
|
| + value = time.strftime(last_mod_format,
|
| + time.localtime(new_value_in_secs))
|
| + SimpleHTTPServer.SimpleHTTPRequestHandler.send_header(self,
|
| + keyword,
|
| + value)
|
| +
|
| + def do_POST(self):
|
| + # Backwards compatible - treat result as tuple without named fields.
|
| + _, _, path, _, query, _ = urlparse.urlparse(self.path)
|
| +
|
| + self.server.listener.Log('POST %s (%s)' % (self.path, path))
|
| + if path == '/echo':
|
| + self.send_response(200)
|
| + self.end_headers()
|
| + data = self.rfile.read(int(self.headers.getheader('content-length')))
|
| + self.wfile.write(data)
|
| + else:
|
| + self.send_error(404, 'File not found')
|
| +
|
| + self.server.ResetTimeout()
|
| +
|
| + def do_GET(self):
|
| + # Backwards compatible - treat result as tuple without named fields.
|
| + _, _, path, _, query, _ = urlparse.urlparse(self.path)
|
| +
|
| + tester = '/TESTER/'
|
| + if path.startswith(tester):
|
| + # If the path starts with '/TESTER/', the GET is an RPC call.
|
| + name = path[len(tester):]
|
| + # Supporting Python 2.5 prevents us from using urlparse.parse_qs
|
| + query = cgi.parse_qs(query, True)
|
| +
|
| + self.server.rpc_lock.acquire()
|
| + try:
|
| + self.HandleRPC(name, query)
|
| + finally:
|
| + self.server.rpc_lock.release()
|
| +
|
| + # Don't reset the timeout. This is not "part of the test", rather it's
|
| + # used to tell us if the renderer process is still alive.
|
| + if name == 'JavaScriptIsAlive':
|
| + self.server.JavaScriptIsAlive()
|
| + return
|
| +
|
| + elif path in self.server.redirect_mapping:
|
| + dest = self.server.redirect_mapping[path]
|
| + self.send_response(301, 'Moved')
|
| + self.send_header('Location', dest)
|
| + self.end_headers()
|
| + self.wfile.write(self.error_message_format %
|
| + {'code': 301,
|
| + 'message': 'Moved',
|
| + 'explain': 'Object moved permanently'})
|
| + self.server.listener.Log('REDIRECT %s (%s -> %s)' %
|
| + (self.path, path, dest))
|
| + else:
|
| + self.server.listener.Log('GET %s (%s)' % (self.path, path))
|
| + # A normal GET request for transferring files, etc.
|
| + f = self.send_head()
|
| + if f:
|
| + self.copyfile(f, self.wfile)
|
| + f.close()
|
| +
|
| + self.server.ResetTimeout()
|
| +
|
| + def copyfile(self, source, outputfile):
|
| + # Bandwidth values <= 0.0 are considered infinite
|
| + if self.server.bandwidth <= 0.0:
|
| + return SimpleHTTPServer.SimpleHTTPRequestHandler.copyfile(
|
| + self, source, outputfile)
|
| +
|
| + self.server.listener.Log('Simulating %f mbps server BW' %
|
| + self.server.bandwidth)
|
| + chunk_size = 1500 # What size to use?
|
| + bits_per_sec = self.server.bandwidth * 1000000
|
| + start_time = time.time()
|
| + data_sent = 0
|
| + while True:
|
| + chunk = source.read(chunk_size)
|
| + if len(chunk) == 0:
|
| + break
|
| + cur_elapsed = time.time() - start_time
|
| + target_elapsed = (data_sent + len(chunk)) * 8 / bits_per_sec
|
| + if (cur_elapsed < target_elapsed):
|
| + time.sleep(target_elapsed - cur_elapsed)
|
| + outputfile.write(chunk)
|
| + data_sent += len(chunk)
|
| + self.server.listener.Log('Streamed %d bytes in %f s' %
|
| + (data_sent, time.time() - start_time))
|
| +
|
| + # Disable the built-in logging
|
| + def log_message(self, format, *args):
|
| + pass
|
| +
|
| +
|
| +# The ThreadingMixIn allows the server to handle multiple requests
|
| +# concurently (or at least as concurently as Python allows). This is desirable
|
| +# because server sockets only allow a limited "backlog" of pending connections
|
| +# and in the worst case the browser could make multiple connections and exceed
|
| +# this backlog - causing the server to drop requests. Using ThreadingMixIn
|
| +# helps reduce the chance this will happen.
|
| +# There were apparently some problems using this Mixin with Python 2.5, but we
|
| +# are no longer using anything older than 2.6.
|
| +class Server(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
| +
|
| + def Configure(
|
| + self, file_mapping, redirect_mapping, extensions_mapping, allow_404,
|
| + bandwidth, listener, serving_dirs=[]):
|
| + self.file_mapping = file_mapping
|
| + self.redirect_mapping = redirect_mapping
|
| + self.extensions_mapping.update(extensions_mapping)
|
| + self.allow_404 = allow_404
|
| + self.bandwidth = bandwidth
|
| + self.listener = listener
|
| + self.rpc_lock = threading.Lock()
|
| + self.serving_dirs = serving_dirs
|
| +
|
| + def TestingBegun(self, timeout):
|
| + self.test_in_progress = True
|
| + # self.timeout does not affect Python 2.5.
|
| + self.timeout = timeout
|
| + self.ResetTimeout()
|
| + self.JavaScriptIsAlive()
|
| + # Have we seen any requests from the browser?
|
| + self.received_request = False
|
| +
|
| + def ResetTimeout(self):
|
| + self.last_activity = time.time()
|
| + self.received_request = True
|
| +
|
| + def JavaScriptIsAlive(self):
|
| + self.last_js_activity = time.time()
|
| +
|
| + def TimeSinceJSHeartbeat(self):
|
| + return time.time() - self.last_js_activity
|
| +
|
| + def TestingEnded(self):
|
| + self.test_in_progress = False
|
| +
|
| + def TimedOut(self, total_time):
|
| + return (total_time >= 0.0 and
|
| + (time.time() - self.last_activity) >= total_time)
|
| +
|
| +
|
| +def Create(host, port):
|
| + server = Server((host, port), RequestHandler)
|
| + server.extensions_mapping = mimetypes.types_map.copy()
|
| + server.extensions_mapping.update({
|
| + '': 'application/octet-stream' # Default
|
| + })
|
| + return server
|
|
|