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
)
69 # The ports of test server is arranged in constants.py.
70 return random
.randint(constants
.LIGHTTPD_RANDOM_PORT_FIRST
,
71 constants
.LIGHTTPD_RANDOM_PORT_LAST
)
73 def StartupHttpServer(self
):
74 """Starts up a http server with specified document root and port."""
75 # If we want a specific port, make sure no one else is listening on it.
77 self
._KillProcessListeningOnPort
(self
.fixed_port
)
79 if self
.base_config_path
:
81 with codecs
.open(self
.base_config_path
, 'r', 'utf-8') as f
:
82 config_contents
= f
.read()
84 config_contents
= self
._GetDefaultBaseConfig
()
85 if self
.extra_config_contents
:
86 config_contents
+= self
.extra_config_contents
87 # Write out the config, filling in placeholders from the members of |self|
88 with codecs
.open(self
.config_path
, 'w', 'utf-8') as f
:
89 f
.write(config_contents
% self
.__dict
__)
90 if (not os
.path
.exists(self
.lighttpd_path
) or
91 not os
.access(self
.lighttpd_path
, os
.X_OK
)):
92 raise EnvironmentError(
93 'Could not find lighttpd at %s.\n'
94 'It may need to be installed (e.g. sudo apt-get install lighttpd)'
96 self
.process
= pexpect
.spawn(self
.lighttpd_path
,
97 ['-D', '-f', self
.config_path
,
98 '-m', self
.lighttpd_module_path
],
100 client_error
, server_error
= self
._TestServerConnection
()
102 assert int(open(self
.pid_file
, 'r').read()) == self
.process
.pid
106 if self
.fixed_port
or not 'in use' in server_error
:
107 print 'Client error:', client_error
108 print 'Server error:', server_error
110 self
.port
= self
._GetRandomPort
()
113 def ShutdownHttpServer(self
):
114 """Shuts down our lighttpd processes."""
116 self
.process
.terminate()
117 shutil
.rmtree(self
.temp_dir
, ignore_errors
=True)
119 def _TestServerConnection(self
):
120 # Wait for server to start
122 for timeout
in xrange(1, 5):
125 with contextlib
.closing(httplib
.HTTPConnection(
126 '127.0.0.1', self
.port
, timeout
=timeout
)) as http
:
127 http
.set_debuglevel(timeout
> 3)
128 http
.request('HEAD', '/')
129 r
= http
.getresponse()
131 if (r
.status
== 200 and r
.reason
== 'OK' and
132 r
.getheader('Server') == self
.server_tag
):
133 return (None, server_msg
)
134 client_error
= ('Bad response: %s %s version %s\n ' %
135 (r
.status
, r
.reason
, r
.version
) +
136 '\n '.join([': '.join(h
) for h
in r
.getheaders()]))
137 except (httplib
.HTTPException
, socket
.error
) as client_error
:
138 pass # Probably too quick connecting: try again
139 # Check for server startup error messages
140 ix
= self
.process
.expect([pexpect
.TIMEOUT
, pexpect
.EOF
, '.+'],
142 if ix
== 2: # stdout spew from the server
143 server_msg
+= self
.process
.match
.group(0) # pylint: disable=no-member
144 elif ix
== 1: # EOF -- server has quit so giveup.
145 client_error
= client_error
or 'Server exited'
147 return (client_error
or 'Timeout', server_msg
)
150 def _KillProcessListeningOnPort(port
):
151 """Checks if there is a process listening on port number |port| and
152 terminates it if found.
155 port: Port number to check.
157 if subprocess
.call(['fuser', '-kv', '%d/tcp' % port
]) == 0:
158 # Give the process some time to terminate and check that it is gone.
160 assert subprocess
.call(['fuser', '-v', '%d/tcp' % port
]) != 0, \
161 'Unable to kill process listening on port %d.' % port
164 def _GetDefaultBaseConfig():
165 return """server.tag = "%(server_tag)s"
166 server.modules = ( "mod_access",
172 # default document root required
173 #server.document-root = "."
175 # files to check for if .../ is requested
176 index-file.names = ( "index.php", "index.pl", "index.cgi",
177 "index.html", "index.htm", "default.htm" )
180 ".gif" => "image/gif",
181 ".jpg" => "image/jpeg",
182 ".jpeg" => "image/jpeg",
183 ".png" => "image/png",
184 ".svg" => "image/svg+xml",
185 ".css" => "text/css",
186 ".html" => "text/html",
187 ".htm" => "text/html",
188 ".xhtml" => "application/xhtml+xml",
189 ".xhtmlmp" => "application/vnd.wap.xhtml+xml",
190 ".js" => "application/x-javascript",
191 ".log" => "text/plain",
192 ".conf" => "text/plain",
193 ".text" => "text/plain",
194 ".txt" => "text/plain",
195 ".dtd" => "text/xml",
196 ".xml" => "text/xml",
197 ".manifest" => "text/cache-manifest",
200 # Use the "Content-Type" extended attribute to obtain mime type if possible
201 mimetype.use-xattr = "enable"
204 # which extensions should not be handle via static-file transfer
206 # .php, .pl, .fcgi are most often handled by mod_fastcgi or mod_cgi
207 static-file.exclude-extensions = ( ".php", ".pl", ".cgi" )
209 server.bind = "127.0.0.1"
210 server.port = %(port)s
212 ## virtual directory listings
213 dir-listing.activate = "enable"
214 #dir-listing.encoding = "iso-8859-2"
215 #dir-listing.external-css = "style/oldstyle.css"
218 #debug.log-request-header = "enable"
219 #debug.log-response-header = "enable"
220 #debug.log-request-handling = "enable"
221 #debug.log-file-not-found = "enable"
224 #ssl.engine = "enable"
225 #ssl.pemfile = "server.pem"
227 # Autogenerated test-specific config follows.
229 cgi.assign = ( ".cgi" => "/usr/bin/env",
230 ".pl" => "/usr/bin/env",
231 ".asis" => "/bin/cat",
232 ".php" => "/usr/bin/php-cgi" )
234 server.errorlog = "%(error_log)s"
235 accesslog.filename = "%(access_log)s"
236 server.upload-dirs = ( "/tmp" )
237 server.pid-file = "%(pid_file)s"
238 server.document-root = "%(document_root)s"
244 server
= LighttpdServer(*argv
[1:])
246 if server
.StartupHttpServer():
247 raw_input('Server running at http://127.0.0.1:%s -'
248 ' press Enter to exit it.' % server
.port
)
250 print 'Server exit code:', server
.process
.exitstatus
252 server
.ShutdownHttpServer()
255 if __name__
== '__main__':
256 sys
.exit(main(sys
.argv
))