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.
7 """Provides a convenient wrapper for spawning a test lighttpd instance.
10 lighttpd_server PATH_TO_DOC_ROOT
25 from pylib
import constants
26 from pylib
import pexpect
28 class LighttpdServer(object):
29 """Wraps lighttpd server, providing robust startup.
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
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.
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')
64 def _Mktmp(self
, name
):
65 return os
.path
.join(self
.temp_dir
, name
)
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
)
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.
76 self
._KillProcessListeningOnPort
(self
.fixed_port
)
78 if self
.base_config_path
:
80 with codecs
.open(self
.base_config_path
, 'r', 'utf-8') as f
:
81 config_contents
= f
.read()
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)'
95 self
.process
= pexpect
.spawn(self
.lighttpd_path
,
96 ['-D', '-f', self
.config_path
,
97 '-m', self
.lighttpd_module_path
],
99 client_error
, server_error
= self
._TestServerConnection
()
101 assert int(open(self
.pid_file
, 'r').read()) == self
.process
.pid
105 if self
.fixed_port
or not 'in use' in server_error
:
106 print 'Client error:', client_error
107 print 'Server error:', server_error
109 self
.port
= self
._GetRandomPort
()
112 def ShutdownHttpServer(self
):
113 """Shuts down our lighttpd processes."""
115 self
.process
.terminate()
116 shutil
.rmtree(self
.temp_dir
, ignore_errors
=True)
118 def _TestServerConnection(self
):
119 # Wait for server to start
121 for timeout
in xrange(1, 5):
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()
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
, '.+'],
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'
146 return (client_error
or 'Timeout', server_msg
)
148 def _KillProcessListeningOnPort(self
, port
):
149 """Checks if there is a process listening on port number |port| and
150 terminates it if found.
153 port: Port number to check.
155 if subprocess
.call(['fuser', '-kv', '%d/tcp' % port
]) == 0:
156 # Give the process some time to terminate and check that it is gone.
158 assert subprocess
.call(['fuser', '-v', '%d/tcp' % port
]) != 0, \
159 'Unable to kill process listening on port %d.' % port
161 def _GetDefaultBaseConfig(self
):
162 return """server.tag = "%(server_tag)s"
163 server.modules = ( "mod_access",
169 # default document root required
170 #server.document-root = "."
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" )
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",
197 # Use the "Content-Type" extended attribute to obtain mime type if possible
198 mimetype.use-xattr = "enable"
201 # which extensions should not be handle via static-file transfer
203 # .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi
204 static-file.exclude-extensions = ( ".php", ".pl", ".cgi" )
206 server.bind = "127.0.0.1"
207 server.port = %(port)s
209 ## virtual directory listings
210 dir-listing.activate = "enable"
211 #dir-listing.encoding = "iso-8859-2"
212 #dir-listing.external-css = "style/oldstyle.css"
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"
221 #ssl.engine = "enable"
222 #ssl.pemfile = "server.pem"
224 # Autogenerated test-specific config follows.
226 cgi.assign = ( ".cgi" => "/usr/bin/env",
227 ".pl" => "/usr/bin/env",
228 ".asis" => "/bin/cat",
229 ".php" => "/usr/bin/php-cgi" )
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"
241 server
= LighttpdServer(*argv
[1:])
243 if server
.StartupHttpServer():
244 raw_input('Server running at http://127.0.0.1:%s -'
245 ' press Enter to exit it.' % server
.port
)
247 print 'Server exit code:', server
.process
.exitstatus
249 server
.ShutdownHttpServer()
252 if __name__
== '__main__':
253 sys
.exit(main(sys
.argv
))