|
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 |
|
9 Usage: |
|
10 lighttpd_server PATH_TO_DOC_ROOT |
|
11 """ |
|
12 |
|
13 import codecs |
|
14 import contextlib |
|
15 import httplib |
|
16 import os |
|
17 import random |
|
18 import shutil |
|
19 import socket |
|
20 import subprocess |
|
21 import sys |
|
22 import tempfile |
|
23 import time |
|
24 |
|
25 from pylib import constants |
|
26 from pylib import pexpect |
|
27 |
|
28 class 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" |
|
163 server.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 |
|
173 index-file.names = ( "index.php", "index.pl", "index.cgi", |
|
174 "index.html", "index.htm", "default.htm" ) |
|
175 # mimetype mapping |
|
176 mimetype.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 |
|
198 mimetype.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 |
|
204 static-file.exclude-extensions = ( ".php", ".pl", ".cgi" ) |
|
205 |
|
206 server.bind = "127.0.0.1" |
|
207 server.port = %(port)s |
|
208 |
|
209 ## virtual directory listings |
|
210 dir-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 |
|
226 cgi.assign = ( ".cgi" => "/usr/bin/env", |
|
227 ".pl" => "/usr/bin/env", |
|
228 ".asis" => "/bin/cat", |
|
229 ".php" => "/usr/bin/php-cgi" ) |
|
230 |
|
231 server.errorlog = "%(error_log)s" |
|
232 accesslog.filename = "%(access_log)s" |
|
233 server.upload-dirs = ( "/tmp" ) |
|
234 server.pid-file = "%(pid_file)s" |
|
235 server.document-root = "%(document_root)s" |
|
236 |
|
237 """ |
|
238 |
|
239 |
|
240 def 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 |
|
252 if __name__ == '__main__': |
|
253 sys.exit(main(sys.argv)) |