1#!/usr/bin/env python 2# 3# Copyright (c) 2012 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Provides a convenient wrapper for spawning a test lighttpd instance. 8 9Usage: 10 lighttpd_server PATH_TO_DOC_ROOT 11""" 12 13import codecs 14import contextlib 15import httplib 16import os 17import random 18import shutil 19import socket 20import subprocess 21import sys 22import tempfile 23import time 24 25from pylib import constants 26from pylib import pexpect 27 28class LighttpdServer(object): 29 """Wraps lighttpd server, providing robust startup. 30 31 Args: 32 document_root: Path to root of this server's hosted files. 33 port: TCP port on the _host_ machine that the server will listen on. If 34 ommitted it will attempt to use 9000, or if unavailable it will find 35 a free port from 8001 - 8999. 36 lighttpd_path, lighttpd_module_path: Optional paths to lighttpd binaries. 37 base_config_path: If supplied this file will replace the built-in default 38 lighttpd config file. 39 extra_config_contents: If specified, this string will be appended to the 40 base config (default built-in, or from base_config_path). 41 config_path, error_log, access_log: Optional paths where the class should 42 place temprary files for this session. 43 """ 44 45 def __init__(self, document_root, port=None, 46 lighttpd_path=None, lighttpd_module_path=None, 47 base_config_path=None, extra_config_contents=None, 48 config_path=None, error_log=None, access_log=None): 49 self.temp_dir = tempfile.mkdtemp(prefix='lighttpd_for_chrome_android') 50 self.document_root = os.path.abspath(document_root) 51 self.fixed_port = port 52 self.port = port or constants.LIGHTTPD_DEFAULT_PORT 53 self.server_tag = 'LightTPD ' + str(random.randint(111111, 999999)) 54 self.lighttpd_path = lighttpd_path or '/usr/sbin/lighttpd' 55 self.lighttpd_module_path = lighttpd_module_path or '/usr/lib/lighttpd' 56 self.base_config_path = base_config_path 57 self.extra_config_contents = extra_config_contents 58 self.config_path = config_path or self._Mktmp('config') 59 self.error_log = error_log or self._Mktmp('error_log') 60 self.access_log = access_log or self._Mktmp('access_log') 61 self.pid_file = self._Mktmp('pid_file') 62 self.process = None 63 64 def _Mktmp(self, name): 65 return os.path.join(self.temp_dir, name) 66 67 def _GetRandomPort(self): 68 # The ports of test server is arranged in constants.py. 69 return random.randint(constants.LIGHTTPD_RANDOM_PORT_FIRST, 70 constants.LIGHTTPD_RANDOM_PORT_LAST) 71 72 def StartupHttpServer(self): 73 """Starts up a http server with specified document root and port.""" 74 # If we want a specific port, make sure no one else is listening on it. 75 if self.fixed_port: 76 self._KillProcessListeningOnPort(self.fixed_port) 77 while True: 78 if self.base_config_path: 79 # Read the config 80 with codecs.open(self.base_config_path, 'r', 'utf-8') as f: 81 config_contents = f.read() 82 else: 83 config_contents = self._GetDefaultBaseConfig() 84 if self.extra_config_contents: 85 config_contents += self.extra_config_contents 86 # Write out the config, filling in placeholders from the members of |self| 87 with codecs.open(self.config_path, 'w', 'utf-8') as f: 88 f.write(config_contents % self.__dict__) 89 if (not os.path.exists(self.lighttpd_path) or 90 not os.access(self.lighttpd_path, os.X_OK)): 91 raise EnvironmentError( 92 'Could not find lighttpd at %s.\n' 93 'It may need to be installed (e.g. sudo apt-get install lighttpd)' 94 % self.lighttpd_path) 95 self.process = pexpect.spawn(self.lighttpd_path, 96 ['-D', '-f', self.config_path, 97 '-m', self.lighttpd_module_path], 98 cwd=self.temp_dir) 99 client_error, server_error = self._TestServerConnection() 100 if not client_error: 101 assert int(open(self.pid_file, 'r').read()) == self.process.pid 102 break 103 self.process.close() 104 105 if self.fixed_port or not 'in use' in server_error: 106 print 'Client error:', client_error 107 print 'Server error:', server_error 108 return False 109 self.port = self._GetRandomPort() 110 return True 111 112 def ShutdownHttpServer(self): 113 """Shuts down our lighttpd processes.""" 114 if self.process: 115 self.process.terminate() 116 shutil.rmtree(self.temp_dir, ignore_errors=True) 117 118 def _TestServerConnection(self): 119 # Wait for server to start 120 server_msg = '' 121 for timeout in xrange(1, 5): 122 client_error = None 123 try: 124 with contextlib.closing(httplib.HTTPConnection( 125 '127.0.0.1', self.port, timeout=timeout)) as http: 126 http.set_debuglevel(timeout > 3) 127 http.request('HEAD', '/') 128 r = http.getresponse() 129 r.read() 130 if (r.status == 200 and r.reason == 'OK' and 131 r.getheader('Server') == self.server_tag): 132 return (None, server_msg) 133 client_error = ('Bad response: %s %s version %s\n ' % 134 (r.status, r.reason, r.version) + 135 '\n '.join([': '.join(h) for h in r.getheaders()])) 136 except (httplib.HTTPException, socket.error) as client_error: 137 pass # Probably too quick connecting: try again 138 # Check for server startup error messages 139 ix = self.process.expect([pexpect.TIMEOUT, pexpect.EOF, '.+'], 140 timeout=timeout) 141 if ix == 2: # stdout spew from the server 142 server_msg += self.process.match.group(0) 143 elif ix == 1: # EOF -- server has quit so giveup. 144 client_error = client_error or 'Server exited' 145 break 146 return (client_error or 'Timeout', server_msg) 147 148 def _KillProcessListeningOnPort(self, port): 149 """Checks if there is a process listening on port number |port| and 150 terminates it if found. 151 152 Args: 153 port: Port number to check. 154 """ 155 if subprocess.call(['fuser', '-kv', '%d/tcp' % port]) == 0: 156 # Give the process some time to terminate and check that it is gone. 157 time.sleep(2) 158 assert subprocess.call(['fuser', '-v', '%d/tcp' % port]) != 0, \ 159 'Unable to kill process listening on port %d.' % port 160 161 def _GetDefaultBaseConfig(self): 162 return """server.tag = "%(server_tag)s" 163server.modules = ( "mod_access", 164 "mod_accesslog", 165 "mod_alias", 166 "mod_cgi", 167 "mod_rewrite" ) 168 169# default document root required 170#server.document-root = "." 171 172# files to check for if .../ is requested 173index-file.names = ( "index.php", "index.pl", "index.cgi", 174 "index.html", "index.htm", "default.htm" ) 175# mimetype mapping 176mimetype.assign = ( 177 ".gif" => "image/gif", 178 ".jpg" => "image/jpeg", 179 ".jpeg" => "image/jpeg", 180 ".png" => "image/png", 181 ".svg" => "image/svg+xml", 182 ".css" => "text/css", 183 ".html" => "text/html", 184 ".htm" => "text/html", 185 ".xhtml" => "application/xhtml+xml", 186 ".xhtmlmp" => "application/vnd.wap.xhtml+xml", 187 ".js" => "application/x-javascript", 188 ".log" => "text/plain", 189 ".conf" => "text/plain", 190 ".text" => "text/plain", 191 ".txt" => "text/plain", 192 ".dtd" => "text/xml", 193 ".xml" => "text/xml", 194 ".manifest" => "text/cache-manifest", 195 ) 196 197# Use the "Content-Type" extended attribute to obtain mime type if possible 198mimetype.use-xattr = "enable" 199 200## 201# which extensions should not be handle via static-file transfer 202# 203# .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi 204static-file.exclude-extensions = ( ".php", ".pl", ".cgi" ) 205 206server.bind = "127.0.0.1" 207server.port = %(port)s 208 209## virtual directory listings 210dir-listing.activate = "enable" 211#dir-listing.encoding = "iso-8859-2" 212#dir-listing.external-css = "style/oldstyle.css" 213 214## enable debugging 215#debug.log-request-header = "enable" 216#debug.log-response-header = "enable" 217#debug.log-request-handling = "enable" 218#debug.log-file-not-found = "enable" 219 220#### SSL engine 221#ssl.engine = "enable" 222#ssl.pemfile = "server.pem" 223 224# Autogenerated test-specific config follows. 225 226cgi.assign = ( ".cgi" => "/usr/bin/env", 227 ".pl" => "/usr/bin/env", 228 ".asis" => "/bin/cat", 229 ".php" => "/usr/bin/php-cgi" ) 230 231server.errorlog = "%(error_log)s" 232accesslog.filename = "%(access_log)s" 233server.upload-dirs = ( "/tmp" ) 234server.pid-file = "%(pid_file)s" 235server.document-root = "%(document_root)s" 236 237""" 238 239 240def main(argv): 241 server = LighttpdServer(*argv[1:]) 242 try: 243 if server.StartupHttpServer(): 244 raw_input('Server running at http://127.0.0.1:%s -' 245 ' press Enter to exit it.' % server.port) 246 else: 247 print 'Server exit code:', server.process.exitstatus 248 finally: 249 server.ShutdownHttpServer() 250 251 252if __name__ == '__main__': 253 sys.exit(main(sys.argv)) 254