runserver_plus.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. # -*- coding: utf-8 -*-
  2. from __future__ import print_function
  3. import logging
  4. import os
  5. import re
  6. import socket
  7. import sys
  8. import django
  9. from django.conf import settings
  10. from django.core.exceptions import ImproperlyConfigured
  11. from django.core.management.base import BaseCommand, CommandError
  12. from django.core.servers.basehttp import get_internal_wsgi_application
  13. from django.utils.autoreload import get_reloader
  14. try:
  15. if 'whitenoise.runserver_nostatic' in settings.INSTALLED_APPS:
  16. USE_STATICFILES = False
  17. else:
  18. from django.contrib.staticfiles.handlers import StaticFilesHandler
  19. USE_STATICFILES = True
  20. except ImportError:
  21. USE_STATICFILES = False
  22. from django_extensions.management.technical_response import null_technical_500_response
  23. from django_extensions.management.utils import RedirectHandler, has_ipdb, setup_logger, signalcommand
  24. from django_extensions.management.debug_cursor import monkey_patch_cursordebugwrapper
  25. def gen_filenames():
  26. return get_reloader().watched_files()
  27. naiveip_re = re.compile(r"""^(?:
  28. (?P<addr>
  29. (?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) | # IPv4 address
  30. (?P<ipv6>\[[a-fA-F0-9:]+\]) | # IPv6 address
  31. (?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN
  32. ):)?(?P<port>\d+)$""", re.X)
  33. DEFAULT_PORT = "8000"
  34. DEFAULT_POLLER_RELOADER_INTERVAL = getattr(settings, 'RUNSERVERPLUS_POLLER_RELOADER_INTERVAL', 1)
  35. DEFAULT_POLLER_RELOADER_TYPE = getattr(settings, 'RUNSERVERPLUS_POLLER_RELOADER_TYPE', 'auto')
  36. logger = logging.getLogger(__name__)
  37. class Command(BaseCommand):
  38. help = "Starts a lightweight Web server for development."
  39. # Validation is called explicitly each time the server is reloaded.
  40. requires_system_checks = False
  41. DEFAULT_CRT_EXTENSION = ".crt"
  42. DEFAULT_KEY_EXTENSION = ".key"
  43. def add_arguments(self, parser):
  44. super().add_arguments(parser)
  45. parser.add_argument('addrport', nargs='?',
  46. help='Optional port number, or ipaddr:port')
  47. parser.add_argument('--ipv6', '-6', action='store_true', dest='use_ipv6', default=False,
  48. help='Tells Django to use a IPv6 address.')
  49. parser.add_argument('--noreload', action='store_false', dest='use_reloader', default=True,
  50. help='Tells Django to NOT use the auto-reloader.')
  51. parser.add_argument('--browser', action='store_true', dest='open_browser',
  52. help='Tells Django to open a browser.')
  53. parser.add_argument('--nothreading', action='store_false', dest='threaded',
  54. help='Do not run in multithreaded mode.')
  55. parser.add_argument('--threaded', action='store_true', dest='threaded',
  56. help='Run in multithreaded mode.')
  57. parser.add_argument('--output', dest='output_file', default=None,
  58. help='Specifies an output file to send a copy of all messages (not flushed immediately).')
  59. parser.add_argument('--print-sql', action='store_true', default=False,
  60. help="Print SQL queries as they're executed")
  61. parser.add_argument('--print-sql-location', action='store_true', default=False,
  62. help="Show location in code where SQL query generated from")
  63. cert_group = parser.add_mutually_exclusive_group()
  64. cert_group.add_argument('--cert', dest='cert_path', action="store", type=str,
  65. help='Deprecated alias for --cert-file option.')
  66. cert_group.add_argument('--cert-file', dest='cert_path', action="store", type=str,
  67. help='SSL .crt file path. If not provided path from --key-file will be selected. '
  68. 'Either --cert-file or --key-file must be provided to use SSL.')
  69. parser.add_argument('--key-file', dest='key_file_path', action="store", type=str,
  70. help='SSL .key file path. If not provided path from --cert-file will be selected. '
  71. 'Either --cert-file or --key-file must be provided to use SSL.')
  72. parser.add_argument('--extra-file', dest='extra_files', action="append", type=str, default=[],
  73. help='auto-reload whenever the given file changes too (can be specified multiple times)')
  74. parser.add_argument('--reloader-interval', dest='reloader_interval', action="store", type=int, default=DEFAULT_POLLER_RELOADER_INTERVAL,
  75. help='After how many seconds auto-reload should scan for updates in poller-mode [default=%s]' % DEFAULT_POLLER_RELOADER_INTERVAL)
  76. parser.add_argument('--reloader-type', dest='reloader_type', action="store", type=str, default=DEFAULT_POLLER_RELOADER_TYPE,
  77. help='Werkzeug reloader type [options are auto, watchdog, or stat, default=%s]' % DEFAULT_POLLER_RELOADER_TYPE)
  78. parser.add_argument('--pdb', action='store_true', dest='pdb', default=False,
  79. help='Drop into pdb shell at the start of any view.')
  80. parser.add_argument('--ipdb', action='store_true', dest='ipdb', default=False,
  81. help='Drop into ipdb shell at the start of any view.')
  82. parser.add_argument('--pm', action='store_true', dest='pm', default=False,
  83. help='Drop into (i)pdb shell if an exception is raised in a view.')
  84. parser.add_argument('--startup-messages', dest='startup_messages', action="store", default='reload',
  85. help='When to show startup messages: reload [default], once, always, never.')
  86. parser.add_argument('--keep-meta-shutdown', dest='keep_meta_shutdown_func', action='store_true', default=False,
  87. help="Keep request.META['werkzeug.server.shutdown'] function which is automatically removed "
  88. "because Django debug pages tries to call the function and unintentionally shuts down "
  89. "the Werkzeug server.")
  90. parser.add_argument("--nopin", dest="nopin", action="store_true", default=False,
  91. help="Disable the PIN in werkzeug. USE IT WISELY!"),
  92. if USE_STATICFILES:
  93. parser.add_argument('--nostatic', action="store_false", dest='use_static_handler', default=True,
  94. help='Tells Django to NOT automatically serve static files at STATIC_URL.')
  95. parser.add_argument('--insecure', action="store_true", dest='insecure_serving', default=False,
  96. help='Allows serving static files even if DEBUG is False.')
  97. @signalcommand
  98. def handle(self, *args, **options):
  99. addrport = options['addrport']
  100. startup_messages = options['startup_messages']
  101. if startup_messages == "reload":
  102. self.show_startup_messages = os.environ.get('RUNSERVER_PLUS_SHOW_MESSAGES')
  103. elif startup_messages == "once":
  104. self.show_startup_messages = not os.environ.get('RUNSERVER_PLUS_SHOW_MESSAGES')
  105. elif startup_messages == "never":
  106. self.show_startup_messages = False
  107. else:
  108. self.show_startup_messages = True
  109. os.environ['RUNSERVER_PLUS_SHOW_MESSAGES'] = '1'
  110. # Do not use default ending='\n', because StreamHandler() takes care of it
  111. if hasattr(self.stderr, 'ending'):
  112. self.stderr.ending = None
  113. setup_logger(logger, self.stderr, filename=options['output_file']) # , fmt="[%(name)s] %(message)s")
  114. logredirect = RedirectHandler(__name__)
  115. # Redirect werkzeug log items
  116. werklogger = logging.getLogger('werkzeug')
  117. werklogger.setLevel(logging.INFO)
  118. werklogger.addHandler(logredirect)
  119. werklogger.propagate = False
  120. pdb_option = options['pdb']
  121. ipdb_option = options['ipdb']
  122. pm = options['pm']
  123. try:
  124. from django_pdb.middleware import PdbMiddleware
  125. except ImportError:
  126. if pdb_option or ipdb_option or pm:
  127. raise CommandError("django-pdb is required for --pdb, --ipdb and --pm options. Please visit https://pypi.python.org/pypi/django-pdb or install via pip. (pip install django-pdb)")
  128. pm = False
  129. else:
  130. # Add pdb middleware if --pdb is specified or if in DEBUG mode
  131. if (pdb_option or ipdb_option or settings.DEBUG):
  132. middleware = 'django_pdb.middleware.PdbMiddleware'
  133. settings_middleware = getattr(settings, 'MIDDLEWARE', None) or settings.MIDDLEWARE_CLASSES
  134. if middleware not in settings_middleware:
  135. if isinstance(settings_middleware, tuple):
  136. settings_middleware += (middleware,)
  137. else:
  138. settings_middleware += [middleware]
  139. # If --pdb is specified then always break at the start of views.
  140. # Otherwise break only if a 'pdb' query parameter is set in the url
  141. if pdb_option:
  142. PdbMiddleware.always_break = 'pdb'
  143. elif ipdb_option:
  144. PdbMiddleware.always_break = 'ipdb'
  145. def postmortem(request, exc_type, exc_value, tb):
  146. if has_ipdb():
  147. import ipdb
  148. p = ipdb
  149. else:
  150. import pdb
  151. p = pdb
  152. print("Exception occured: %s, %s" % (exc_type, exc_value), file=sys.stderr)
  153. p.post_mortem(tb)
  154. # usurp django's handler
  155. from django.views import debug
  156. debug.technical_500_response = postmortem if pm else null_technical_500_response
  157. self.use_ipv6 = options['use_ipv6']
  158. if self.use_ipv6 and not socket.has_ipv6:
  159. raise CommandError('Your Python does not support IPv6.')
  160. self._raw_ipv6 = False
  161. if not addrport:
  162. try:
  163. addrport = settings.RUNSERVERPLUS_SERVER_ADDRESS_PORT
  164. except AttributeError:
  165. pass
  166. if not addrport:
  167. self.addr = ''
  168. self.port = DEFAULT_PORT
  169. else:
  170. m = re.match(naiveip_re, addrport)
  171. if m is None:
  172. raise CommandError('"%s" is not a valid port number '
  173. 'or address:port pair.' % addrport)
  174. self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
  175. if not self.port.isdigit():
  176. raise CommandError("%r is not a valid port number." %
  177. self.port)
  178. if self.addr:
  179. if _ipv6:
  180. self.addr = self.addr[1:-1]
  181. self.use_ipv6 = True
  182. self._raw_ipv6 = True
  183. elif self.use_ipv6 and not _fqdn:
  184. raise CommandError('"%s" is not a valid IPv6 address.'
  185. % self.addr)
  186. if not self.addr:
  187. self.addr = '::1' if self.use_ipv6 else '127.0.0.1'
  188. self._raw_ipv6 = True
  189. with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"], print_sql_location=options["print_sql_location"], logger=logger.info, confprefix="RUNSERVER_PLUS"):
  190. self.inner_run(options)
  191. def inner_run(self, options):
  192. try:
  193. from werkzeug import run_simple
  194. from werkzeug.debug import DebuggedApplication
  195. from werkzeug.serving import WSGIRequestHandler as _WSGIRequestHandler
  196. # Set colored output
  197. if settings.DEBUG:
  198. try:
  199. set_werkzeug_log_color()
  200. except Exception: # We are dealing with some internals, anything could go wrong
  201. if self.show_startup_messages:
  202. print("Wrapping internal werkzeug logger for color highlighting has failed!")
  203. pass
  204. except ImportError:
  205. raise CommandError("Werkzeug is required to use runserver_plus. Please visit http://werkzeug.pocoo.org/ or install via pip. (pip install Werkzeug)")
  206. class WSGIRequestHandler(_WSGIRequestHandler):
  207. def make_environ(self):
  208. environ = super().make_environ()
  209. if not options['keep_meta_shutdown_func']:
  210. del environ['werkzeug.server.shutdown']
  211. return environ
  212. threaded = options['threaded']
  213. use_reloader = options['use_reloader']
  214. open_browser = options['open_browser']
  215. quit_command = (sys.platform == 'win32') and 'CTRL-BREAK' or 'CONTROL-C'
  216. extra_files = options['extra_files']
  217. reloader_interval = options['reloader_interval']
  218. reloader_type = options['reloader_type']
  219. self.nopin = options['nopin']
  220. if self.show_startup_messages:
  221. print("Performing system checks...\n")
  222. if hasattr(self, 'check'):
  223. self.check(display_num_errors=self.show_startup_messages)
  224. else:
  225. self.validate(display_num_errors=self.show_startup_messages)
  226. try:
  227. self.check_migrations()
  228. except ImproperlyConfigured:
  229. pass
  230. handler = get_internal_wsgi_application()
  231. if USE_STATICFILES:
  232. use_static_handler = options['use_static_handler']
  233. insecure_serving = options['insecure_serving']
  234. if use_static_handler and (settings.DEBUG or insecure_serving):
  235. handler = StaticFilesHandler(handler)
  236. if options["cert_path"] or options["key_file_path"]:
  237. """
  238. OpenSSL is needed for SSL support.
  239. This will make flakes8 throw warning since OpenSSL is not used
  240. directly, alas, this is the only way to show meaningful error
  241. messages. See:
  242. http://lucumr.pocoo.org/2011/9/21/python-import-blackbox/
  243. for more information on python imports.
  244. """
  245. try:
  246. import OpenSSL # NOQA
  247. except ImportError:
  248. raise CommandError("Python OpenSSL Library is "
  249. "required to use runserver_plus with ssl support. "
  250. "Install via pip (pip install pyOpenSSL).")
  251. certfile, keyfile = self.determine_ssl_files_paths(options)
  252. dir_path, root = os.path.split(certfile)
  253. root, _ = os.path.splitext(root)
  254. try:
  255. from werkzeug.serving import make_ssl_devcert
  256. if os.path.exists(certfile) and os.path.exists(keyfile):
  257. ssl_context = (certfile, keyfile)
  258. else: # Create cert, key files ourselves.
  259. ssl_context = make_ssl_devcert(os.path.join(dir_path, root), host='localhost')
  260. except ImportError:
  261. if self.show_startup_messages:
  262. print("Werkzeug version is less than 0.9, trying adhoc certificate.")
  263. ssl_context = "adhoc"
  264. else:
  265. ssl_context = None
  266. bind_url = "%s://%s:%s/" % (
  267. "https" if ssl_context else "http", self.addr if not self._raw_ipv6 else '[%s]' % self.addr, self.port)
  268. if self.show_startup_messages:
  269. print("\nDjango version %s, using settings %r" % (django.get_version(), settings.SETTINGS_MODULE))
  270. print("Development server is running at %s" % (bind_url,))
  271. print("Using the Werkzeug debugger (http://werkzeug.pocoo.org/)")
  272. print("Quit the server with %s." % quit_command)
  273. if open_browser:
  274. import webbrowser
  275. webbrowser.open(bind_url)
  276. if use_reloader and settings.USE_I18N:
  277. extra_files.extend(filter(lambda filename: str(filename).endswith('.mo'), gen_filenames()))
  278. # Werkzeug needs to be clued in its the main instance if running
  279. # without reloader or else it won't show key.
  280. # https://git.io/vVIgo
  281. if not use_reloader:
  282. os.environ['WERKZEUG_RUN_MAIN'] = 'true'
  283. # Don't run a second instance of the debugger / reloader
  284. # See also: https://github.com/django-extensions/django-extensions/issues/832
  285. if os.environ.get('WERKZEUG_RUN_MAIN') != 'true':
  286. if self.nopin:
  287. os.environ['WERKZEUG_DEBUG_PIN'] = 'off'
  288. handler = DebuggedApplication(handler, True)
  289. run_simple(
  290. self.addr,
  291. int(self.port),
  292. handler,
  293. use_reloader=use_reloader,
  294. use_debugger=True,
  295. extra_files=extra_files,
  296. reloader_interval=reloader_interval,
  297. reloader_type=reloader_type,
  298. threaded=threaded,
  299. request_handler=WSGIRequestHandler,
  300. ssl_context=ssl_context,
  301. )
  302. @classmethod
  303. def determine_ssl_files_paths(cls, options):
  304. key_file_path = options.get('key_file_path') or ""
  305. cert_path = options.get('cert_path') or ""
  306. cert_file = cls._determine_path_for_file(cert_path, key_file_path, cls.DEFAULT_CRT_EXTENSION)
  307. key_file = cls._determine_path_for_file(key_file_path, cert_path, cls.DEFAULT_KEY_EXTENSION)
  308. return cert_file, key_file
  309. @classmethod
  310. def _determine_path_for_file(cls, current_file_path, other_file_path, expected_extension):
  311. directory = cls._get_directory_basing_on_file_paths(current_file_path, other_file_path)
  312. file_name = cls._get_file_name(current_file_path) or cls._get_file_name(other_file_path)
  313. extension = cls._get_extension(current_file_path) or expected_extension
  314. return os.path.join(directory, file_name + extension)
  315. @classmethod
  316. def _get_directory_basing_on_file_paths(cls, current_file_path, other_file_path):
  317. return cls._get_directory(current_file_path) or cls._get_directory(other_file_path) or os.getcwd()
  318. @classmethod
  319. def _get_directory(cls, file_path):
  320. return os.path.split(file_path)[0]
  321. @classmethod
  322. def _get_file_name(cls, file_path):
  323. return os.path.splitext(os.path.split(file_path)[1])[0]
  324. @classmethod
  325. def _get_extension(cls, file_path):
  326. return os.path.splitext(file_path)[1]
  327. def set_werkzeug_log_color():
  328. """Try to set color to the werkzeug log."""
  329. from django.core.management.color import color_style
  330. from werkzeug.serving import WSGIRequestHandler
  331. from werkzeug._internal import _log
  332. _style = color_style()
  333. _orig_log = WSGIRequestHandler.log
  334. def werk_log(self, type, message, *args):
  335. try:
  336. msg = '%s - - [%s] %s' % (
  337. self.address_string(),
  338. self.log_date_time_string(),
  339. message % args,
  340. )
  341. http_code = str(args[1])
  342. except Exception:
  343. return _orig_log(type, message, *args)
  344. # Utilize terminal colors, if available
  345. if http_code[0] == '2':
  346. # Put 2XX first, since it should be the common case
  347. msg = _style.HTTP_SUCCESS(msg)
  348. elif http_code[0] == '1':
  349. msg = _style.HTTP_INFO(msg)
  350. elif http_code == '304':
  351. msg = _style.HTTP_NOT_MODIFIED(msg)
  352. elif http_code[0] == '3':
  353. msg = _style.HTTP_REDIRECT(msg)
  354. elif http_code == '404':
  355. msg = _style.HTTP_NOT_FOUND(msg)
  356. elif http_code[0] == '4':
  357. msg = _style.HTTP_BAD_REQUEST(msg)
  358. else:
  359. # Any 5XX, or any other response
  360. msg = _style.HTTP_SERVER_ERROR(msg)
  361. _log(type, msg)
  362. WSGIRequestHandler.log = werk_log