lighttpd_server.py 9.4 KB

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