diff --git a/AUTHORS b/AUTHORS index 924edf09c3..a7ce5c488a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -65,6 +65,7 @@ answer newbie questions, and generally made Django that much better: Kieran Holland Robert Rock Howard Jason Huggins + jcrasta@gmail.com Michael Josephson jpellerin@gmail.com junzhang.jn@gmail.com diff --git a/django/core/management.py b/django/core/management.py index 61213d0965..06c7f3ab1b 100644 --- a/django/core/management.py +++ b/django/core/management.py @@ -1081,6 +1081,12 @@ def dbshell(): runshell() dbshell.args = "" +def runfcgi(args): + """Run this project as a FastCGI application. requires flup.""" + from django.core.servers.fastcgi import runfastcgi + runfastcgi(args) +runfcgi.args = '[various KEY=val options, use `runfcgi help` for help]' + # Utilities for command-line script DEFAULT_ACTION_MAPPING = { @@ -1091,6 +1097,7 @@ DEFAULT_ACTION_MAPPING = { 'inspectdb': inspectdb, 'install': install, 'reset': reset, + 'runfcgi': runfcgi, 'runserver': runserver, 'shell': run_shell, 'sql': get_sql_create, @@ -1210,6 +1217,8 @@ def execute_from_command_line(action_mapping=DEFAULT_ACTION_MAPPING, argv=None): except ValueError: addr, port = '', args[1] action_mapping[action](addr, port) + elif action == 'runfcgi': + action_mapping[action](args[1:]) else: from django.db import models try: diff --git a/django/core/servers/fastcgi.py b/django/core/servers/fastcgi.py new file mode 100644 index 0000000000..dedc1f8ba1 --- /dev/null +++ b/django/core/servers/fastcgi.py @@ -0,0 +1,147 @@ +""" +FastCGI server that implements the WSGI protocol. + +Uses the flup python package: http://www.saddi.com/software/flup/ + +This is a adaptation of the flup package to add FastCGI server support +to run Django apps from Web servers that support the FastCGI protocol. +This module can be run standalone or from the django-admin / manage.py +scripts using the "runfcgi" directive. + +Run with the extra option "help" for a list of additional options you can +pass to this server. +""" + +import sys, os + +__version__ = "0.1" +__all__ = ["runfastcgi"] + +FASTCGI_HELP = r"""runfcgi: + Run this project as a fastcgi application. To do this, the + flup package from http://www.saddi.com/software/flup/ is + required. + +Usage: + django-admin.py runfcgi --settings=yourproject.settings [fcgi settings] + manage.py runfcgi [fcgi settings] + +Optional Fcgi settings: (setting=value) + host=HOSTNAME hostname to listen on.. + port=PORTNUM port to listen on. + socket=FILE UNIX socket to listen on. + method=IMPL prefork or threaded (default prefork) + maxspare=NUMBER max number of spare processes to keep running. + minspare=NUMBER min number of spare processes to prefork. + maxchildren=NUMBER hard limit number of processes in prefork mode. + daemonize=BOOL whether to detach from terminal. + pidfile=FILE write the spawned process-id to this file. + workdir=DIRECTORY change to this directory when daemonizing + +Examples: + Run a "standard" fastcgi process on a file-descriptor + (for webservers which spawn your processes for you) + $ manage.py runfcgi method=threaded + + Run a fastcgi server on a TCP host/port + $ manage.py runfcgi method=prefork host=127.0.0.1 port=8025 + + Run a fastcgi server on a UNIX domain socket (posix platforms only) + $ manage.py runfcgi method=prefork socket=/tmp/fcgi.sock + + Run a fastCGI as a daemon and write the spawned PID in a file + $ manage.py runfcgi socket=/tmp/fcgi.sock method=prefork \ + daemonize=true pidfile=/var/run/django-fcgi.pid + +""" + +FASTCGI_OPTIONS = { + 'host': None, + 'port': None, + 'socket': None, + 'method': 'fork', + 'daemonize': None, + 'workdir': '/', + 'pidfile': None, + 'maxspare': 5, + 'minspare': 2, + 'maxchildren': 50, +} + +def fastcgi_help(message=None): + print FASTCGI_HELP + if message: + print message + return False + +def runfastcgi(argset): + options = FASTCGI_OPTIONS.copy() + for x in argset: + if "=" in x: + k, v = x.split('=', 1) + else: + k, v = x, True + options[k.lower()] = v + + if "help" in options: + return fastcgi_help() + + try: + import flup + except ImportError, e: + print >> sys.stderr, "ERROR: %s" % e + print >> sys.stderr, " Unable to load the flup package. In order to run django" + print >> sys.stderr, " as a FastCGI application, you will need to get flup from" + print >> sys.stderr, " http://www.saddi.com/software/flup/ If you've already" + print >> sys.stderr, " installed flup, then make sure you have it in your PYTHONPATH." + return False + + if options['method'] in ('prefork', 'fork'): + from flup.server.fcgi_fork import WSGIServer + wsgi_opts = { + 'maxSpare': int(options["maxspare"]), + 'minSpare': int(options["minspare"]), + 'maxChildren': int(options["maxchildren"]), + } + elif options['method'] in ('thread', 'threaded'): + from flup.server.fcgi import WSGIServer + wsgi_opts = {} + else: + return fastcgi_help("ERROR: Implementation must be one of prefork or thread.") + + # Prep up and go + from django.core.handlers.wsgi import WSGIHandler + + if options["host"] and options["port"] and not options["socket"]: + wsgi_opts['bindAddress'] = (options["host"], int(options["port"])) + elif options["socket"] and not options["host"] and not options["port"]: + wsgi_opts['bindAddress'] = options["socket"] + elif not options["socket"] and not options["host"] and not options["port"]: + wsgi_opts['bindAddress'] = None + else: + return fastcgi_help("Invalid combination of host, port, socket.") + + if options["daemonize"] is None: + # Default to daemonizing if we're running on a socket/named pipe. + daemonize = (wsgi_opts['bindAddress'] is not None) + else: + if options["daemonize"].lower() in ('true', 'yes', 't'): + daemonize = True + elif options["daemonize"].lower() in ('false', 'no', 'f'): + daemonize = False + else: + return fastcgi_help("ERROR: Invalid option for daemonize parameter.") + + if daemonize: + from django.utils.daemonize import become_daemon + become_daemon(our_home_dir=options["workdir"]) + + if options["pidfile"]: + fp = open(options["pidfile"], "w") + fp.write("%d\n" % os.getpid()) + fp.close() + + WSGIServer(WSGIHandler(), **wsgi_opts).run() + +if __name__ == '__main__': + runfastcgi(sys.argv[1:]) diff --git a/django/utils/daemonize.py b/django/utils/daemonize.py new file mode 100644 index 0000000000..9671b8d91f --- /dev/null +++ b/django/utils/daemonize.py @@ -0,0 +1,55 @@ +import os +import sys + +if os.name == 'posix': + def become_daemon(our_home_dir='.', out_log='/dev/null', err_log='/dev/null'): + "Robustly turn into a UNIX daemon, running in our_home_dir." + # First fork + try: + if os.fork() > 0: + sys.exit(0) # kill off parent + except OSError, e: + sys.stderr.write("fork #1 failed: (%d) %s\n" % (e.errno, e.strerror)) + sys.exit(1) + os.setsid() + os.chdir(our_home_dir) + os.umask(0) + + # Second fork + try: + if os.fork() > 0: + sys.exit(0) + except OSError, e: + sys.stderr.write("fork #2 failed: (%d) %s\n" % (e.errno, e.strerror)) + sys.exit(1) + + si = open('/dev/null', 'r') + so = open(out_log, 'a+', 0) + se = open(err_log, 'a+', 0) + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) +else: + def become_daemon(our_home_dir='.', out_log=None, err_log=None): + """ + If we're not running under a POSIX system, just simulate the daemon + mode by doing redirections and directory changing. + """ + os.chdir(our_home_dir) + os.umask(0) + sys.stdin.close() + sys.stdout.close() + sys.stderr.close() + if err_log: + sys.stderr = open(err_log, 'a', 0) + else: + sys.stderr = NullDevice() + if out_log: + sys.stdout = open(out_log, 'a', 0) + else: + sys.stdout = NullDevice() + + class NullDevice: + "A writeable object that writes to nowhere -- like /dev/null." + def write(self, s): + pass