blob: 11ae794d4ade6a9d7fdbbbb286b8e724377f26fb [file] [log] [blame]
[email protected]3f09d182011-11-23 19:13:441#!/usr/bin/env python
[email protected]f033c512012-07-10 13:21:432#
3# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]55ee7f4d2011-10-24 18:59:204# 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
[email protected]55ee7f4d2011-10-24 18:59:2017import random
18import shutil
19import socket
[email protected]f033c512012-07-10 13:21:4320import subprocess
[email protected]55ee7f4d2011-10-24 18:59:2021import sys
22import tempfile
[email protected]f033c512012-07-10 13:21:4323import time
24
25from pylib import constants
[email protected]f05d5822012-10-17 21:03:1226from pylib import pexpect
[email protected]55ee7f4d2011-10-24 18:59:2027
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
[email protected]f033c512012-07-10 13:21:4352 self.port = port or constants.LIGHTTPD_DEFAULT_PORT
[email protected]55ee7f4d2011-10-24 18:59:2053 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):
[email protected]f033c512012-07-10 13:21:4368 # 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)
[email protected]55ee7f4d2011-10-24 18:59:2071
72 def StartupHttpServer(self):
73 """Starts up a http server with specified document root and port."""
[email protected]f033c512012-07-10 13:21:4374 # 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)
[email protected]55ee7f4d2011-10-24 18:59:2077 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
[email protected]f033c512012-07-10 13:21:43148 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
[email protected]55ee7f4d2011-10-24 18:59:20161 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))