From 9d4df905dc90abe0491a7c4bb685698683054cbd Mon Sep 17 00:00:00 2001 From: Hans Ulrich Niedermann Date: Wed, 28 May 2008 07:35:11 +0200 Subject: [PATCH] Multiple listening address support for runserver The default now is to listen to 127.0.0.1:8000 (unchanged), but if socket.has_ipv6, we start a second HTTP server which listens to [::1]:8000. If you give one or more command line parameters of the format 'port', 'ip4addr:port', or '[ip6addr]:port' to runserver, we will start one HTTP server to listen on the respective address/port, foregoing the default ones. Caution: This changes the API of django.core.servers.basehttp.run()! Caution: Our usage of select.poll() might be broken on some systems, or in special cases. Apart from that, this should be completely compatible. --- django/core/management/commands/runserver.py | 44 ++++++++++++++------------ django/core/servers/basehttp.py | 28 +++++++++++++--- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index 887d924..7c9259b 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -13,7 +13,7 @@ class Command(BaseCommand): help='Specifies the directory from which to serve admin media.'), ) help = "Starts a lightweight Web server for development." - args = '[optional port number, or ipaddr:port, or [ipv6addr]:port]' + args = '[optional port number, or ipaddr:port, or [ipv6addr]:port]...' # Validation is called explicitly each time the server is reloaded. requires_model_validation = False @@ -40,32 +40,37 @@ class Command(BaseCommand): address_family = socket.AF_INET6 if not port: raise CommandError('Could not parse [addr:]port value %r' % addrport) + if not port.isdigit(): + raise CommandError("%r is not a valid port number." % port) + # FIXME: Produce proper error on invalid addrs. + if not addr: + addr = '127.0.0.1' return (addr, port, address_family,) - def handle(self, addrport='', *args, **options): + def handle(self, *args, **options): import django from django.core.servers.basehttp import run, AdminMediaHandler, WSGIServerException from django.core.handlers.wsgi import WSGIHandler - # FIXME: Support listening to both ::1 and 127.0.0.1 as default - # FIXME: Error on invalid addrs. - if args: - raise CommandError('Usage is runserver %s' % self.args) - if addrport: + addrlist = [] + for addrport in args: addr, port, address_family = self.__parse_addrport(addrport) - else: - addr, port, address_family = '127.0.0.1', '8000', None + if addr or port: + addrlist.append((addr, int(port, 10), address_family, )) - # Default values - if not port: - port = '8000' - if not addr: - addr = '127.0.0.1' - # not address_family: That is OK with the called methods. + if not addrlist: + addrlist.append(('127.0.0.1', 8000, None, )) + if socket.has_ipv6: + addrlist.append(('::1', 8000, socket.AF_INET6, )) - if not port.isdigit(): - raise CommandError("%r is not a valid port number." % port) + listenurls = [] + for addr, port, address_family in addrlist: + if address_family == socket.AF_INET6: + url = "http://[%s]:%d/" % (addr, port) + else: + url = "http://%s:%d/" % (addr, port) + listenurls.append(url) use_reloader = options.get('use_reloader', True) admin_media_path = options.get('admin_media_path', '') @@ -78,13 +83,12 @@ class Command(BaseCommand): print "Validating models..." self.validate(display_num_errors=True) print "\nDjango version %s, using settings %r" % (django.get_version(), settings.SETTINGS_MODULE) - xaddr = (':' in addr) and ('['+addr+']') or addr - print "Development server is running at http://%s:%s/" % (xaddr, port) + print "Development server is running at %s" % (' '.join(listenurls)) print "Quit the server with %s." % quit_command try: path = admin_media_path or django.__path__[0] + '/contrib/admin/media' handler = AdminMediaHandler(WSGIHandler(), path) - run(addr, int(port), handler, address_family) + run(addrlist, handler) except WSGIServerException, e: # Use helpful error messages instead of ugly tracebacks. ERRORS = { diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py index cd85bea..e19eaf0 100644 --- a/django/core/servers/basehttp.py +++ b/django/core/servers/basehttp.py @@ -11,6 +11,7 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer import mimetypes import os import re +import select import sys import urllib @@ -661,8 +662,25 @@ class AdminMediaHandler(object): start_response(status, headers.items()) return output -def run(addr, port, wsgi_handler, address_family=None): - server_address = (addr, port) - httpd = WSGIServer(server_address, WSGIRequestHandler, address_family) - httpd.set_app(wsgi_handler) - httpd.serve_forever() +def run(addrlist, wsgi_handler): + # FIXME: This might be an erroneous buggy implementation. + # + # * What happens if the poll.poll() returns on just a fragment + # of the data for an HTTP request? + # handle_request() hopefully just waits for more data. + # * What happens if that data does not arrive? + # The whole server will be blocked for that time on all addrs. + # * Is that acceptable for a devel server? + # You decide. + poll = select.poll() + pollmap = {} + for addr, port, address_family in addrlist: + server_address = (addr, port) + httpd = WSGIServer(server_address, WSGIRequestHandler, address_family) + httpd.set_app(wsgi_handler) + pollmap[httpd.fileno()] = httpd + poll.register(httpd, select.POLLIN) + while True: + for fd, event in poll.poll(): + pollmap[fd].handle_request() + -- 1.5.5.1