Source

django-wsgiserver / django_wsgiserver / management / commands / runwsgiserver.py

Full commit
#!/usr/bin/env python
"""
Idea and code snippets borrowed from http://www.xhtml.net/scripts/Django-CherryPy-server-DjangoCerise
Adapted to run as a management command
switched to standalone
"""


import logging, sys, os, signal, time, errno
from socket import gethostname
from django.core.management.base import BaseCommand
from django_wsgiserver import mediahandler
import django.contrib.admin # used to find where admin media is in filesystem

CPWSGI_HELP = r"""
  Run this project in CherryPy's production quality http webserver.
  Note that it's called wsgiserver but it is actually a complete http server.

    runwsgiserver [options] [cpwsgi settings] [stop]

Optional CherryPy server settings: (setting=value)
  host=HOSTNAME         hostname to listen on
                        Defaults to 127.0.0.1,
                        (set to 0.0.0.0 to bind all ip4 interfaces or :: for
                        all ip6 interfaces)
  port=PORTNUM          port to listen on
                        Defaults to 8000
  server_name=STRING    CherryPy's SERVER_NAME environ entry
                        Defaults to localhost
  daemonize=BOOL        whether to detach from terminal
                        Defaults to False
  pidfile=FILE          write the spawned process-id to this file
  workdir=DIRECTORY     change to this directory when daemonizing
  threads=NUMBER        Number of threads for server to use
  ssl_certificate=FILE  SSL certificate file
  ssl_private_key=FILE  SSL private key file
  server_user=STRING    user to run daemonized process
                        Defaults to www-data
  server_group=STRING   group to daemonized process
                        Defaults to www-data
  adminserve=True|False  Serve the django admin media automatically. Useful
                         in development. Defaults to True so turn to False
                         if  using in production.

Examples:
  Run a "standard" CherryPy wsgi server
    $ manage.py runwsgiserver

  Run a CherryPy server on port 80
    $ manage.py runwsgiserver port=80

  Run a CherryPy server as a daemon and write the spawned PID in a file
    $ manage.py runwsgiserver daemonize=true pidfile=/var/run/django-cpwsgi.pid
  
  Run a CherryPy server using ssl with test certificates located in /tmp
    $ manage.py runwsgiserver ssl_certificate=/tmp/testserver.crt ssl_private_key=/tmp/testserver.key

"""

CPWSGI_OPTIONS = {
'host': '127.0.0.1', # changed from localhost to avoid ip6 problem -clm
'port': 8000,   # changed from 8088 to 8000 to follow django devserver default
'server_name': 'localhost',
'threads': 10, 
'daemonize': False,
'workdir': None,
'pidfile': None,
'server_user': 'www-data',
'server_group': 'www-data',
'ssl_certificate': None,
'ssl_private_key': None,
# options useful for development - should these all be true by default or off by default?
'autoreload' : False,
'adminserve' : True,  # please serve the admin media too by default
'staticserve' : True, # serve static
'servestaticdirs': True, # will use STATICFILES_DIRS and actuall serve each directory so
                          # you don't need to collect them all in development
}


class Command(BaseCommand):
    help = "CherryPy Server for project. Requires CherryPy."
    args = "[various KEY=val options, use `runwsgiserver help` for help]"

    def handle(self, *args, **options):
        from django.conf import settings
        from django.utils import translation
        # Activate the current language, because it won't get activated later.
        try:
            translation.activate(settings.LANGUAGE_CODE)
        except AttributeError:
            pass
        runwsgiserver(args)
        
    def usage(self, subcommand):
        return CPWSGI_HELP

def change_uid_gid(uid, gid=None):
    """Try to change UID and GID to the provided values.
    UID and GID are given as names like 'nobody' not integer.

    Src: http://mail.mems-exchange.org/durusmail/quixote-users/4940/1/
    """
    if not os.geteuid() == 0:
        # Do not try to change the gid/uid if not root.
        return
    (uid, gid) = get_uid_gid(uid, gid)
    os.setgid(gid)
    os.setuid(uid)

def get_uid_gid(uid, gid=None):
    """Try to change UID and GID to the provided values.
    UID and GID are given as names like 'nobody' not integer.

    Src: http://mail.mems-exchange.org/durusmail/quixote-users/4940/1/
    """
    import pwd, grp
    uid, default_grp = pwd.getpwnam(uid)[2:4]
    if gid is None:
        gid = default_grp
    else:
        try:
            gid = grp.getgrnam(gid)[2]            
        except KeyError:
            gid = default_grp
    return (uid, gid)
    
def poll_process(pid):
    """
    Poll for process with given pid up to 10 times waiting .25 seconds in between each poll. 
    Returns False if the process no longer exists otherwise, True.
    """
    for n in range(10):
        time.sleep(0.25)
        try:
            # poll the process state
            os.kill(pid, 0)
        except OSError, e:
            if e[0] == errno.ESRCH:
                # process has died
                return False
            else:
                raise Exception
    return True

def stop_server(pidfile):
    """
    Stop process whose pid was written to supplied pidfile. 
    First try SIGTERM and if it fails, SIGKILL. If process is still running, an exception is raised.
    """
    if os.path.exists(pidfile):
        pid = int(open(pidfile).read())
        try:
            os.kill(pid, signal.SIGTERM)
        except OSError: #process does not exist
            os.remove(pidfile)
            return
        if poll_process(pid):
            #process didn't exit cleanly, make one last effort to kill it
            os.kill(pid, signal.SIGKILL)
            #if still_alive(pid):
            if poll_process(pid):
                raise OSError, "Process %s did not stop."
        os.remove(pidfile)

def start_server(options):
    """
    Start CherryPy server

    Want SSL support?
    a. The new (3.1 or 3.2) way: Just set server.ssl_adapter to an SSLAdapter instance.
    b. The old way (deprecated way) is to set these attributes:

       server.ssl_certificate = <filename>
       server.ssl_private_key = <filename>

       But this is the only way from the management command line
       in the future I may need to adapt this to use a server.ssl_adapter

    """
    
    if options['daemonize'] and options['server_user'] and options['server_group']:
        #ensure the that the daemon runs as specified user
        change_uid_gid(options['server_user'], options['server_group'])
    
    from django_wsgiserver.wsgiserver import CherryPyWSGIServer 
    #from cherrypy.wsgiserver import CherryPyWSGIServer
    from django.core.handlers.wsgi import WSGIHandler
    app = WSGIHandler()
    if options['adminserve']: # serve the admin media too
        # AdminMediaHandler is middleware for local use
        import django.core.servers.basehttp
        app = django.core.servers.basehttp.AdminMediaHandler(app)
        
    server = CherryPyWSGIServer(
        (options['host'], int(options['port'])),
        app,
        int(options['threads']), 
        options['server_name']
    )
    if options['ssl_certificate'] and options['ssl_private_key']:
        server.ssl_certificate = options['ssl_certificate']
        server.ssl_private_key = options['ssl_private_key']  
    try:
        server.start()
    except KeyboardInterrupt:
        server.stop()

def process_staticfiles_dirs(staticfiles_dirs, default_prefix='/static/'): # settings.STATIC_URL
    """
    normalizes all elements of STATICFILES_DIRS to be of ('prefix','/root/path/to/files') form
    the prefix gets added after STATIC_URL
    """
    staticlocations=[]
    for staticdir in staticfiles_dirs:
        # elements of staticfiles_dirs are ether simple path strings like "/var/www/polls/static"
        # or are tuples/
        if isinstance(staticdir, (list, tuple)):
            prefix, root = staticdir
            root = os.path.abspath(root)
        else:
            # default url prefix used for single path elements
            root = os.path.abspath(staticdir)
            prefix = os.path.join(default_prefix, os.path.basename(root))+r'/' # end with /

        staticlocations.append((prefix, root))
    return staticlocations


def start_server_servestatic(options):
    """
    Start CherryPy server AND serve default static files

    Want SSL support?
    a. The new (3.1 or 3.2) way: Just set server.ssl_adapter to an SSLAdapter instance.
    b. The old way (deprecated way) is to set these attributes:

       server.ssl_certificate = <filename>
       server.ssl_private_key = <filename>

       But this is the only way from the management command line
       in the future I may need to adapt this to use a server.ssl_adapter

    """
    # debug
    # print "options:"
    from pprint import pprint; pprint(options)
    
    if options['daemonize'] and options['server_user'] and options['server_group']:
        #ensure the that the daemon runs as specified user
        change_uid_gid(options['server_user'], options['server_group'])
    
    from django_wsgiserver.wsgiserver import CherryPyWSGIServer, WSGIPathInfoDispatcher
    #from cherrypy.wsgiserver import CherryPyWSGIServer, WSGIPathInfoDispatcher
    from django.core.handlers.wsgi import WSGIHandler
    from django.conf import settings
    app = WSGIHandler()
    path = { '/': app}
    if options['adminserve']: # serve the admin media too
        # AdminMediaHandler is middleware for local use
        #import django.core.servers.basehttp
        #adminapp = django.core.servers.basehttp.AdminMediaHandler(app)
        # another way to serve the admin media three application
        import django.contrib.admin
        path[settings.ADMIN_MEDIA_PREFIX] = mediahandler.MediaHandler(        
            os.path.join( django.contrib.admin.__path__[0], 'media'))

    if options['staticserve']:
        try:
            if settings.STATIC_URL:
                path[settings.STATIC_URL] = mediahandler.MediaHandler(settings.STATIC_ROOT)
            else:
                print "****"
                print "STATIC_URL and STATIC_ROOT  must be set in settings file if wsgiserver option staticserve is set to True"
                print "****"

        except AttributeError, msg:
            print msg
            print "****"
            print "STATIC_URL and STATIC_ROOT  must be set in settings file"
            print "****"
            raise

        if settings.STATICFILES_FINDERS:
            # debug
            print "see settings.STATICFILES_FINDERS"
            pprint (settings.STATICFILES_FINDERS)
            from django.contrib.staticfiles.finders import AppDirectoriesFinder
            app_static_finder=AppDirectoriesFinder(settings.INSTALLED_APPS)
            print "app_static_finder.storages:"
            pprint(app_static_finder.storages)
            for key,val in app_static_finder.storages.items():
                print key, " static location:", val.location
            # need to decide what to do with this in terms of the fusion of the app static directories
                
                 
        if options['servestaticdirs'] and hasattr(settings, 'STATICFILES_DIRS'):
            staticlocations = process_staticfiles_dirs(settings.STATICFILES_DIRS)
            # debug !!!
            from pprint import pprint; print "staticlocations:"; pprint(staticlocations)
            for urlprefix, root in staticlocations:
                path[os.path.join(settings.STATIC_URL, urlprefix)] =  mediahandler.MediaHandler(root)

    # debug
    from pprint import pprint; print "apps:"; pprint(path)
    dispatcher =  WSGIPathInfoDispatcher( path )
        
    server = CherryPyWSGIServer(
        (options['host'], int(options['port'])),
        dispatcher,
        int(options['threads']), 
        options['server_name']
    )
    if options['ssl_certificate'] and options['ssl_private_key']:
        server.ssl_certificate = options['ssl_certificate']
        server.ssl_private_key = options['ssl_private_key']  
    try:
        server.start()
    except KeyboardInterrupt:
        server.stop()

def runwsgiserver(argset=[], **kwargs):
    # Get the options
    options = CPWSGI_OPTIONS.copy()
    options.update(kwargs)
    for x in argset:
        if "=" in x:
            k, v = x.split('=', 1)
        else:
            k, v = x, True
        if v=='False' or v=='false':
            v = False
            # print "found false", v
        options[k.lower()] = v
        
    if "help" in options:
        print CPWSGI_HELP
        return
        
    if "stop" in options:
        stop_server(options['pidfile'])
        return True
    
    if options['daemonize']:
        if not options['pidfile']:
            options['pidfile'] = '/var/run/cpwsgi_%s.pid' % options['port']
        stop_server(options['pidfile'])     
       
        from django.utils.daemonize import become_daemon
        if options['workdir']:
            become_daemon(our_home_dir=options['workdir'])
        else:
            become_daemon()

        fp = open(options['pidfile'], 'w')
        fp.write("%d\n" % os.getpid())
        fp.close()
    if options['autoreload']:
        import autoreload
        autoreload.run()
    
    # Start the webserver
    print 'starting server with options %s' % options
    start_server_servestatic(options)


if __name__ == '__main__':
    runwsgiserver(sys.argv[1:])