#36211: Subclassing the "runserver" handler doesn't serve static files
-------------------------------------+-------------------------------------
     Reporter:  Ivan Voras           |                     Type:  Bug
       Status:  new                  |                Component:  Core
                                     |  (Management commands)
      Version:  5.1                  |                 Severity:  Normal
     Keywords:  runserver,           |             Triage Stage:
  autoreload                         |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
 Just for convenience of development, not for production, I'm trying to
 write a "runserver" lookalike command that also starts Celery, and also
 uses the autoloader to do it. So I've subclassed Django's runserver
 `Command` class and redefined the `handler()`. It took me ages to
 understand RUN_MAIN and process management, but here's the result:

 {{{
 import atexit
 import errno
 import logging
 import os
 import re
 import socket
 import subprocess
 from time import sleep

 from django.conf import settings
 from django.core.management.base import CommandError
 from django.core.servers.basehttp import run
 from django.db import connections
 from django.utils import autoreload
 from django.utils.regex_helper import _lazy_re_compile
 from django.core.management.commands.runserver import Command as
 RunserverCommand

 log = logging.getLogger("glassior")

 naiveip_re = _lazy_re_compile(
     r"""^(?:
 (?P<addr>
     (?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) |         # IPv4 address
     (?P<ipv6>\[[a-fA-F0-9:]+\]) |               # IPv6 address
     (?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN
 ):)?(?P<port>\d+)$""",
     re.X,
 )

 celery_process = None

 class Command(RunserverCommand):
     help = "Starts a lightweight web server for development and a Celery
 worker with autoreload."

     def handle(self, *args, **options):
         print('runservercelery: Starting both Django and a Celery worker
 with autoreload...', os.environ.get("RUN_MAIN", False))

         if not settings.DEBUG and not settings.ALLOWED_HOSTS:
             raise CommandError("You must set settings.ALLOWED_HOSTS if
 DEBUG is False.")

         self.use_ipv6 = options["use_ipv6"]
         if self.use_ipv6 and not socket.has_ipv6:
             raise CommandError("Your Python does not support IPv6.")
         self._raw_ipv6 = False
         if not options["addrport"]:
             self.addr = ""
             self.port = self.default_port
         else:
             m = re.match(naiveip_re, options["addrport"])
             if m is None:
                 raise CommandError(
                     '"%s" is not a valid port number '
                     "or address:port pair." % options["addrport"]
                 )
             self.addr, _ipv4, _ipv6, _fqdn, self.port = m.groups()
             if not self.port.isdigit():
                 raise CommandError("%r is not a valid port number." %
 self.port)
             if self.addr:
                 if _ipv6:
                     self.addr = self.addr[1:-1]
                     self.use_ipv6 = True
                     self._raw_ipv6 = True
                 elif self.use_ipv6 and not _fqdn:
                     raise CommandError('"%s" is not a valid IPv6 address.'
 % self.addr)
         if not self.addr:
             self.addr = self.default_addr_ipv6 if self.use_ipv6 else
 self.default_addr
             self._raw_ipv6 = self.use_ipv6

         if options["use_reloader"]:
             autoreload.run_with_reloader(self.main_loop, *args, **options)
         else:
             self.main_loop(*args, **options)

     def start_celery(self):
         global celery_process
         if os.environ.get("RUN_MAIN", None) != 'true':
             return

         celery_process = subprocess.Popen(
             'celery -A glassiorapp worker -l info --without-gossip
 --without-mingle --without-heartbeat -c 1',
             shell=True,
             process_group=0,
         )
         log.info(f"Started celery worker (PID: {celery_process.pid})")


     def our_inner_run(self, *args, **options) -> int | None:
         """
         Taken from
 django.core.management.commands.runserver.Command.inner_run.
         Returns exit code (None = no error) instead of calling sys.exit().
         """
         # If an exception was silenced in ManagementUtility.execute in
 order
         # to be raised in the child process, raise it now.
         autoreload.raise_last_exception()

         threading = False # options["use_threading"]
         # 'shutdown_message' is a stealth option.
         shutdown_message = options.get("shutdown_message", "")

         if not options["skip_checks"]:
             self.stdout.write("Performing system checks...\n\n")
             self.check(display_num_errors=True)
         # Need to check migrations here, so can't use the
         # requires_migrations_check attribute.
         self.check_migrations()
         # Close all connections opened during migration checking.
         for conn in connections.all(initialized_only=True):
             conn.close()

         try:
             handler = self.get_handler(*args, **options)
             run(
                 self.addr,
                 int(self.port),
                 handler,
                 ipv6=self.use_ipv6,
                 threading=threading,
                 on_bind=self.on_bind,
                 server_cls=self.server_cls,
             )
         except OSError as e:
             # Use helpful error messages instead of ugly tracebacks.
             ERRORS = {
                 errno.EACCES: "You don't have permission to access that
 port.",
                 errno.EADDRINUSE: "That port is already in use.",
                 errno.EADDRNOTAVAIL: "That IP address can't be assigned
 to.",
             }
             try:
                 error_text = ERRORS[e.errno]
             except KeyError:
                 error_text = e
             self.stderr.write("Error: %s" % error_text)
             # Need to use an OS exit because sys.exit doesn't work in a
 thread
             return 1
         except KeyboardInterrupt:
             print("**** KeyboardInterrupt") # This is never reached.
             if shutdown_message:
                 self.stdout.write(shutdown_message)
             return 0

     def main_loop(self, *args, **options):
         self.start_celery()
         exit_code = self.our_inner_run(*args, **options) # Django's code
         # So, apparently our_inner_run doesn't return.
         print(f"***** our_inner_run exit_code={exit_code}")


 @atexit.register
 def stop_celery():
     global celery_process
     if celery_process:
         log.info(f"Stopping celery worker (PID: {celery_process.pid})")
         # It's a mess.
         os.system(f"kill -TERM -{celery_process.pid}")
         celery_process = None
         sleep(1)
 }}}

 This works, BUT it doesn't start the static file server. I don't see
 anything special in the original handler, or in the new one, that would
 cause this, so I'm just stumped. Switching between running the original
 runserver and this one, the original serves static files perfectly fine,
 and this one returns the "no route found" error:

 {{{
 Using the URLconf defined in glassiorapp.urls, Django tried these URL
 patterns, in this order:

 admin/
 g/
 The current path, static/web/color_modes.js, didn’t match any of these.

 You’re seeing this error because you have DEBUG = True in your Django
 settings file. Change that to False, and Django will display a standard
 404 page.
 }}}

 The actual app is being run and responds to the URL endpoints.

 Is there something magical about the default runserver?
-- 
Ticket URL: <https://code.djangoproject.com/ticket/36211>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/0107019534ff488f-16fbeb4e-4f5e-4ec3-a668-4f26167c339b-000000%40eu-central-1.amazonses.com.

Reply via email to