runner.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. ##############################################################################
  2. #
  3. # Copyright (c) 2013 Zope Foundation and Contributors.
  4. # All Rights Reserved.
  5. #
  6. # This software is subject to the provisions of the Zope Public License,
  7. # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
  8. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
  9. # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  10. # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
  11. # FOR A PARTICULAR PURPOSE.
  12. #
  13. ##############################################################################
  14. """Command line runner.
  15. """
  16. import getopt
  17. import logging
  18. import os
  19. import os.path
  20. import re
  21. import sys
  22. from waitress import serve
  23. from waitress.adjustments import Adjustments
  24. from waitress.utilities import logger
  25. HELP = """\
  26. Usage:
  27. {0} [OPTS] MODULE:OBJECT
  28. Standard options:
  29. --help
  30. Show this information.
  31. --call
  32. Call the given object to get the WSGI application.
  33. --host=ADDR
  34. Hostname or IP address on which to listen, default is '0.0.0.0',
  35. which means "all IP addresses on this host".
  36. Note: May not be used together with --listen
  37. --port=PORT
  38. TCP port on which to listen, default is '8080'
  39. Note: May not be used together with --listen
  40. --listen=ip:port
  41. Tell waitress to listen on an ip port combination.
  42. Example:
  43. --listen=127.0.0.1:8080
  44. --listen=[::1]:8080
  45. --listen=*:8080
  46. This option may be used multiple times to listen on multiple sockets.
  47. A wildcard for the hostname is also supported and will bind to both
  48. IPv4/IPv6 depending on whether they are enabled or disabled.
  49. --[no-]ipv4
  50. Toggle on/off IPv4 support.
  51. Example:
  52. --no-ipv4
  53. This will disable IPv4 socket support. This affects wildcard matching
  54. when generating the list of sockets.
  55. --[no-]ipv6
  56. Toggle on/off IPv6 support.
  57. Example:
  58. --no-ipv6
  59. This will turn on IPv6 socket support. This affects wildcard matching
  60. when generating a list of sockets.
  61. --unix-socket=PATH
  62. Path of Unix socket. If a socket path is specified, a Unix domain
  63. socket is made instead of the usual inet domain socket.
  64. Not available on Windows.
  65. --unix-socket-perms=PERMS
  66. Octal permissions to use for the Unix domain socket, default is
  67. '600'.
  68. --url-scheme=STR
  69. Default wsgi.url_scheme value, default is 'http'.
  70. --url-prefix=STR
  71. The ``SCRIPT_NAME`` WSGI environment value. Setting this to anything
  72. except the empty string will cause the WSGI ``SCRIPT_NAME`` value to be
  73. the value passed minus any trailing slashes you add, and it will cause
  74. the ``PATH_INFO`` of any request which is prefixed with this value to
  75. be stripped of the prefix. Default is the empty string.
  76. --ident=STR
  77. Server identity used in the 'Server' header in responses. Default
  78. is 'waitress'.
  79. Tuning options:
  80. --threads=INT
  81. Number of threads used to process application logic, default is 4.
  82. --backlog=INT
  83. Connection backlog for the server. Default is 1024.
  84. --recv-bytes=INT
  85. Number of bytes to request when calling socket.recv(). Default is
  86. 8192.
  87. --send-bytes=INT
  88. Number of bytes to send to socket.send(). Default is 18000.
  89. Multiples of 9000 should avoid partly-filled TCP packets.
  90. --outbuf-overflow=INT
  91. A temporary file should be created if the pending output is larger
  92. than this. Default is 1048576 (1MB).
  93. --outbuf-high-watermark=INT
  94. The app_iter will pause when pending output is larger than this value
  95. and will resume once enough data is written to the socket to fall below
  96. this threshold. Default is 16777216 (16MB).
  97. --inbuf-overflow=INT
  98. A temporary file should be created if the pending input is larger
  99. than this. Default is 524288 (512KB).
  100. --connection-limit=INT
  101. Stop creating new channels if too many are already active.
  102. Default is 100.
  103. --cleanup-interval=INT
  104. Minimum seconds between cleaning up inactive channels. Default
  105. is 30. See '--channel-timeout'.
  106. --channel-timeout=INT
  107. Maximum number of seconds to leave inactive connections open.
  108. Default is 120. 'Inactive' is defined as 'has received no data
  109. from the client and has sent no data to the client'.
  110. --[no-]log-socket-errors
  111. Toggle whether premature client disconnect tracebacks ought to be
  112. logged. On by default.
  113. --max-request-header-size=INT
  114. Maximum size of all request headers combined. Default is 262144
  115. (256KB).
  116. --max-request-body-size=INT
  117. Maximum size of request body. Default is 1073741824 (1GB).
  118. --[no-]expose-tracebacks
  119. Toggle whether to expose tracebacks of unhandled exceptions to the
  120. client. Off by default.
  121. --asyncore-loop-timeout=INT
  122. The timeout value in seconds passed to asyncore.loop(). Default is 1.
  123. --asyncore-use-poll
  124. The use_poll argument passed to ``asyncore.loop()``. Helps overcome
  125. open file descriptors limit. Default is False.
  126. --channel-request-lookahead=INT
  127. Allows channels to stay readable and buffer more requests up to the
  128. given maximum even if a request is already being processed. This allows
  129. detecting if a client closed the connection while its request is being
  130. processed. Default is 0.
  131. """
  132. RUNNER_PATTERN = re.compile(
  133. r"""
  134. ^
  135. (?P<module>
  136. [a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
  137. )
  138. :
  139. (?P<object>
  140. [a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
  141. )
  142. $
  143. """,
  144. re.I | re.X,
  145. )
  146. def match(obj_name):
  147. matches = RUNNER_PATTERN.match(obj_name)
  148. if not matches:
  149. raise ValueError(f"Malformed application '{obj_name}'")
  150. return matches.group("module"), matches.group("object")
  151. def resolve(module_name, object_name):
  152. """Resolve a named object in a module."""
  153. # We cast each segments due to an issue that has been found to manifest
  154. # in Python 2.6.6, but not 2.6.8, and may affect other revisions of Python
  155. # 2.6 and 2.7, whereby ``__import__`` chokes if the list passed in the
  156. # ``fromlist`` argument are unicode strings rather than 8-bit strings.
  157. # The error triggered is "TypeError: Item in ``fromlist '' not a string".
  158. # My guess is that this was fixed by checking against ``basestring``
  159. # rather than ``str`` sometime between the release of 2.6.6 and 2.6.8,
  160. # but I've yet to go over the commits. I know, however, that the NEWS
  161. # file makes no mention of such a change to the behaviour of
  162. # ``__import__``.
  163. segments = [str(segment) for segment in object_name.split(".")]
  164. obj = __import__(module_name, fromlist=segments[:1])
  165. for segment in segments:
  166. obj = getattr(obj, segment)
  167. return obj
  168. def show_help(stream, name, error=None): # pragma: no cover
  169. if error is not None:
  170. print(f"Error: {error}\n", file=stream)
  171. print(HELP.format(name), file=stream)
  172. def show_exception(stream):
  173. exc_type, exc_value = sys.exc_info()[:2]
  174. args = getattr(exc_value, "args", None)
  175. print(
  176. ("There was an exception ({}) importing your module.\n").format(
  177. exc_type.__name__,
  178. ),
  179. file=stream,
  180. )
  181. if args:
  182. print("It had these arguments: ", file=stream)
  183. for idx, arg in enumerate(args, start=1):
  184. print(f"{idx}. {arg}\n", file=stream)
  185. else:
  186. print("It had no arguments.", file=stream)
  187. def run(argv=sys.argv, _serve=serve):
  188. """Command line runner."""
  189. name = os.path.basename(argv[0])
  190. try:
  191. kw, args = Adjustments.parse_args(argv[1:])
  192. except getopt.GetoptError as exc:
  193. show_help(sys.stderr, name, str(exc))
  194. return 1
  195. if kw["help"]:
  196. show_help(sys.stdout, name)
  197. return 0
  198. if len(args) != 1:
  199. show_help(sys.stderr, name, "Specify one application only")
  200. return 1
  201. # set a default level for the logger only if it hasn't been set explicitly
  202. # note that this level does not override any parent logger levels,
  203. # handlers, etc but without it no log messages are emitted by default
  204. if logger.level == logging.NOTSET:
  205. logger.setLevel(logging.INFO)
  206. try:
  207. module, obj_name = match(args[0])
  208. except ValueError as exc:
  209. show_help(sys.stderr, name, str(exc))
  210. show_exception(sys.stderr)
  211. return 1
  212. # Add the current directory onto sys.path
  213. sys.path.append(os.getcwd())
  214. # Get the WSGI function.
  215. try:
  216. app = resolve(module, obj_name)
  217. except ImportError:
  218. show_help(sys.stderr, name, f"Bad module '{module}'")
  219. show_exception(sys.stderr)
  220. return 1
  221. except AttributeError:
  222. show_help(sys.stderr, name, f"Bad object name '{obj_name}'")
  223. show_exception(sys.stderr)
  224. return 1
  225. if kw["call"]:
  226. app = app()
  227. # These arguments are specific to the runner, not waitress itself.
  228. del kw["call"], kw["help"]
  229. _serve(app, **kw)
  230. return 0