michael@0: #!/usr/bin/env python michael@0: # michael@0: # Copyright (c) 2012 The Chromium Authors. All rights reserved. michael@0: # Use of this source code is governed by a BSD-style license that can be michael@0: # found in the LICENSE file. michael@0: michael@0: """Provides a convenient wrapper for spawning a test lighttpd instance. michael@0: michael@0: Usage: michael@0: lighttpd_server PATH_TO_DOC_ROOT michael@0: """ michael@0: michael@0: import codecs michael@0: import contextlib michael@0: import httplib michael@0: import os michael@0: import random michael@0: import shutil michael@0: import socket michael@0: import subprocess michael@0: import sys michael@0: import tempfile michael@0: import time michael@0: michael@0: from pylib import constants michael@0: from pylib import pexpect michael@0: michael@0: class LighttpdServer(object): michael@0: """Wraps lighttpd server, providing robust startup. michael@0: michael@0: Args: michael@0: document_root: Path to root of this server's hosted files. michael@0: port: TCP port on the _host_ machine that the server will listen on. If michael@0: ommitted it will attempt to use 9000, or if unavailable it will find michael@0: a free port from 8001 - 8999. michael@0: lighttpd_path, lighttpd_module_path: Optional paths to lighttpd binaries. michael@0: base_config_path: If supplied this file will replace the built-in default michael@0: lighttpd config file. michael@0: extra_config_contents: If specified, this string will be appended to the michael@0: base config (default built-in, or from base_config_path). michael@0: config_path, error_log, access_log: Optional paths where the class should michael@0: place temprary files for this session. michael@0: """ michael@0: michael@0: def __init__(self, document_root, port=None, michael@0: lighttpd_path=None, lighttpd_module_path=None, michael@0: base_config_path=None, extra_config_contents=None, michael@0: config_path=None, error_log=None, access_log=None): michael@0: self.temp_dir = tempfile.mkdtemp(prefix='lighttpd_for_chrome_android') michael@0: self.document_root = os.path.abspath(document_root) michael@0: self.fixed_port = port michael@0: self.port = port or constants.LIGHTTPD_DEFAULT_PORT michael@0: self.server_tag = 'LightTPD ' + str(random.randint(111111, 999999)) michael@0: self.lighttpd_path = lighttpd_path or '/usr/sbin/lighttpd' michael@0: self.lighttpd_module_path = lighttpd_module_path or '/usr/lib/lighttpd' michael@0: self.base_config_path = base_config_path michael@0: self.extra_config_contents = extra_config_contents michael@0: self.config_path = config_path or self._Mktmp('config') michael@0: self.error_log = error_log or self._Mktmp('error_log') michael@0: self.access_log = access_log or self._Mktmp('access_log') michael@0: self.pid_file = self._Mktmp('pid_file') michael@0: self.process = None michael@0: michael@0: def _Mktmp(self, name): michael@0: return os.path.join(self.temp_dir, name) michael@0: michael@0: def _GetRandomPort(self): michael@0: # The ports of test server is arranged in constants.py. michael@0: return random.randint(constants.LIGHTTPD_RANDOM_PORT_FIRST, michael@0: constants.LIGHTTPD_RANDOM_PORT_LAST) michael@0: michael@0: def StartupHttpServer(self): michael@0: """Starts up a http server with specified document root and port.""" michael@0: # If we want a specific port, make sure no one else is listening on it. michael@0: if self.fixed_port: michael@0: self._KillProcessListeningOnPort(self.fixed_port) michael@0: while True: michael@0: if self.base_config_path: michael@0: # Read the config michael@0: with codecs.open(self.base_config_path, 'r', 'utf-8') as f: michael@0: config_contents = f.read() michael@0: else: michael@0: config_contents = self._GetDefaultBaseConfig() michael@0: if self.extra_config_contents: michael@0: config_contents += self.extra_config_contents michael@0: # Write out the config, filling in placeholders from the members of |self| michael@0: with codecs.open(self.config_path, 'w', 'utf-8') as f: michael@0: f.write(config_contents % self.__dict__) michael@0: if (not os.path.exists(self.lighttpd_path) or michael@0: not os.access(self.lighttpd_path, os.X_OK)): michael@0: raise EnvironmentError( michael@0: 'Could not find lighttpd at %s.\n' michael@0: 'It may need to be installed (e.g. sudo apt-get install lighttpd)' michael@0: % self.lighttpd_path) michael@0: self.process = pexpect.spawn(self.lighttpd_path, michael@0: ['-D', '-f', self.config_path, michael@0: '-m', self.lighttpd_module_path], michael@0: cwd=self.temp_dir) michael@0: client_error, server_error = self._TestServerConnection() michael@0: if not client_error: michael@0: assert int(open(self.pid_file, 'r').read()) == self.process.pid michael@0: break michael@0: self.process.close() michael@0: michael@0: if self.fixed_port or not 'in use' in server_error: michael@0: print 'Client error:', client_error michael@0: print 'Server error:', server_error michael@0: return False michael@0: self.port = self._GetRandomPort() michael@0: return True michael@0: michael@0: def ShutdownHttpServer(self): michael@0: """Shuts down our lighttpd processes.""" michael@0: if self.process: michael@0: self.process.terminate() michael@0: shutil.rmtree(self.temp_dir, ignore_errors=True) michael@0: michael@0: def _TestServerConnection(self): michael@0: # Wait for server to start michael@0: server_msg = '' michael@0: for timeout in xrange(1, 5): michael@0: client_error = None michael@0: try: michael@0: with contextlib.closing(httplib.HTTPConnection( michael@0: '127.0.0.1', self.port, timeout=timeout)) as http: michael@0: http.set_debuglevel(timeout > 3) michael@0: http.request('HEAD', '/') michael@0: r = http.getresponse() michael@0: r.read() michael@0: if (r.status == 200 and r.reason == 'OK' and michael@0: r.getheader('Server') == self.server_tag): michael@0: return (None, server_msg) michael@0: client_error = ('Bad response: %s %s version %s\n ' % michael@0: (r.status, r.reason, r.version) + michael@0: '\n '.join([': '.join(h) for h in r.getheaders()])) michael@0: except (httplib.HTTPException, socket.error) as client_error: michael@0: pass # Probably too quick connecting: try again michael@0: # Check for server startup error messages michael@0: ix = self.process.expect([pexpect.TIMEOUT, pexpect.EOF, '.+'], michael@0: timeout=timeout) michael@0: if ix == 2: # stdout spew from the server michael@0: server_msg += self.process.match.group(0) michael@0: elif ix == 1: # EOF -- server has quit so giveup. michael@0: client_error = client_error or 'Server exited' michael@0: break michael@0: return (client_error or 'Timeout', server_msg) michael@0: michael@0: def _KillProcessListeningOnPort(self, port): michael@0: """Checks if there is a process listening on port number |port| and michael@0: terminates it if found. michael@0: michael@0: Args: michael@0: port: Port number to check. michael@0: """ michael@0: if subprocess.call(['fuser', '-kv', '%d/tcp' % port]) == 0: michael@0: # Give the process some time to terminate and check that it is gone. michael@0: time.sleep(2) michael@0: assert subprocess.call(['fuser', '-v', '%d/tcp' % port]) != 0, \ michael@0: 'Unable to kill process listening on port %d.' % port michael@0: michael@0: def _GetDefaultBaseConfig(self): michael@0: return """server.tag = "%(server_tag)s" michael@0: server.modules = ( "mod_access", michael@0: "mod_accesslog", michael@0: "mod_alias", michael@0: "mod_cgi", michael@0: "mod_rewrite" ) michael@0: michael@0: # default document root required michael@0: #server.document-root = "." michael@0: michael@0: # files to check for if .../ is requested michael@0: index-file.names = ( "index.php", "index.pl", "index.cgi", michael@0: "index.html", "index.htm", "default.htm" ) michael@0: # mimetype mapping michael@0: mimetype.assign = ( michael@0: ".gif" => "image/gif", michael@0: ".jpg" => "image/jpeg", michael@0: ".jpeg" => "image/jpeg", michael@0: ".png" => "image/png", michael@0: ".svg" => "image/svg+xml", michael@0: ".css" => "text/css", michael@0: ".html" => "text/html", michael@0: ".htm" => "text/html", michael@0: ".xhtml" => "application/xhtml+xml", michael@0: ".xhtmlmp" => "application/vnd.wap.xhtml+xml", michael@0: ".js" => "application/x-javascript", michael@0: ".log" => "text/plain", michael@0: ".conf" => "text/plain", michael@0: ".text" => "text/plain", michael@0: ".txt" => "text/plain", michael@0: ".dtd" => "text/xml", michael@0: ".xml" => "text/xml", michael@0: ".manifest" => "text/cache-manifest", michael@0: ) michael@0: michael@0: # Use the "Content-Type" extended attribute to obtain mime type if possible michael@0: mimetype.use-xattr = "enable" michael@0: michael@0: ## michael@0: # which extensions should not be handle via static-file transfer michael@0: # michael@0: # .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi michael@0: static-file.exclude-extensions = ( ".php", ".pl", ".cgi" ) michael@0: michael@0: server.bind = "127.0.0.1" michael@0: server.port = %(port)s michael@0: michael@0: ## virtual directory listings michael@0: dir-listing.activate = "enable" michael@0: #dir-listing.encoding = "iso-8859-2" michael@0: #dir-listing.external-css = "style/oldstyle.css" michael@0: michael@0: ## enable debugging michael@0: #debug.log-request-header = "enable" michael@0: #debug.log-response-header = "enable" michael@0: #debug.log-request-handling = "enable" michael@0: #debug.log-file-not-found = "enable" michael@0: michael@0: #### SSL engine michael@0: #ssl.engine = "enable" michael@0: #ssl.pemfile = "server.pem" michael@0: michael@0: # Autogenerated test-specific config follows. michael@0: michael@0: cgi.assign = ( ".cgi" => "/usr/bin/env", michael@0: ".pl" => "/usr/bin/env", michael@0: ".asis" => "/bin/cat", michael@0: ".php" => "/usr/bin/php-cgi" ) michael@0: michael@0: server.errorlog = "%(error_log)s" michael@0: accesslog.filename = "%(access_log)s" michael@0: server.upload-dirs = ( "/tmp" ) michael@0: server.pid-file = "%(pid_file)s" michael@0: server.document-root = "%(document_root)s" michael@0: michael@0: """ michael@0: michael@0: michael@0: def main(argv): michael@0: server = LighttpdServer(*argv[1:]) michael@0: try: michael@0: if server.StartupHttpServer(): michael@0: raw_input('Server running at http://127.0.0.1:%s -' michael@0: ' press Enter to exit it.' % server.port) michael@0: else: michael@0: print 'Server exit code:', server.process.exitstatus michael@0: finally: michael@0: server.ShutdownHttpServer() michael@0: michael@0: michael@0: if __name__ == '__main__': michael@0: sys.exit(main(sys.argv))