#!/usr/bin/env python
# crate_anon/crateweb/core/management/commands/runcpserver.py
"""
===============================================================================
Copyright (C) 2015-2018 Rudolf Cardinal (rudolf@pobox.com).
This file is part of CRATE.
CRATE is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CRATE is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CRATE. If not, see <http://www.gnu.org/licenses/>.
===============================================================================
Django management command framework for CherryPy based on:
https://lincolnloop.com/blog/2008/mar/25/serving-django-cherrypy/
Idea and code snippets borrowed from
http://www.xhtml.net/scripts/Django-CherryPy-server-DjangoCerise
Adapted to run as a management command.
Some bugs fixed by RNC.
Then rewritten by RNC.
Then modified to serve CRATE, with static files, etc.
Then daemonizing code removed:
https://code.djangoproject.com/ticket/4996
TEST COMMAND:
./manage.py runcpserver --port 8080 --ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem --ssl_private_key /etc/ssl/private/ssl-cert-snakeoil.key
""" # noqa
from argparse import ArgumentParser, Namespace
import logging
# import errno
# import os
# import signal
# import time
# try:
# import grp
# import pwd
# unix = True
# except ImportError:
# grp = None
# pwd = None
# unix = False
import cherrypy
from django.conf import settings
from django.core.management.base import BaseCommand
from django.utils import translation
from crate_anon.crateweb.config.wsgi import application as wsgi_application
# COULD ALSO USE:
# from django.core.handlers.wsgi import WSGIHandler
# wsgi_application = WSGIHandler()
log = logging.getLogger(__name__)
CRATE_STATIC_URL_PATH = settings.STATIC_URL.rstrip('/')
NEED_UNIX = "Need UNIX for group/user operations"
DEFAULT_ROOT = settings.FORCE_SCRIPT_NAME
[docs]class Command(BaseCommand):
help = ("Run this project in a CherryPy webserver. To do this, "
"CherryPy is required (pip install cherrypy).")
[docs] def add_arguments(self, parser: ArgumentParser) -> None:
parser.add_argument(
'--host', type=str, default="127.0.0.1",
help="hostname to listen on (default: 127.0.0.1)")
parser.add_argument(
'--port', type=int, default=8088,
help="port to listen on (default: 8088)")
parser.add_argument(
"--server_name", type=str, default="localhost",
help="CherryPy's SERVER_NAME environ entry (default: localhost)")
# parser.add_argument(
# "--daemonize", action="store_true",
# help="whether to detach from terminal (default: False)")
# parser.add_argument(
# "--pidfile", type=str,
# help="write the spawned process ID to this file")
# parser.add_argument(
# "--workdir", type=str,
# help="change to this directory when daemonizing")
parser.add_argument(
"--threads", type=int, default=10,
help="Number of threads for server to use (default: 10)")
parser.add_argument(
"--ssl_certificate", type=str,
help="SSL certificate file "
"(e.g. /etc/ssl/certs/ssl-cert-snakeoil.pem)")
parser.add_argument(
"--ssl_private_key", type=str,
help="SSL private key file "
"(e.g. /etc/ssl/private/ssl-cert-snakeoil.key)")
# parser.add_argument(
# "--server_user", type=str, default="www-data",
# help="user to run daemonized process (default: www-data)")
# parser.add_argument(
# "--server_group", type=str, default="www-data",
# help="group to run daemonized process (default: www-data)")
parser.add_argument(
"--log_screen", dest="log_screen", action="store_true",
help="log access requests etc. to terminal (default)")
parser.add_argument(
"--no_log_screen", dest="log_screen", action="store_false",
help="don't log access requests etc. to terminal")
parser.add_argument(
"--debug_static", action="store_true",
help="show debug info for static file requests")
parser.add_argument(
"--root_path", type=str, default=DEFAULT_ROOT,
help="Root path to serve CRATE at. Default: {}".format(
DEFAULT_ROOT))
parser.set_defaults(log_screen=True)
# parser.add_argument(
# "--stop", action="store_true",
# help="stop server")
[docs] def handle(self, *args, **options):
opts = Namespace(**options)
# Activate the current language, because it won't get activated later.
try:
translation.activate(settings.LANGUAGE_CODE)
except AttributeError:
pass
# noinspection PyTypeChecker
runcpserver(opts)
# 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 unix:
# raise OSError(NEED_UNIX)
# 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/
# """
# if not unix:
# raise OSError(NEED_UNIX)
# 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 still_alive(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 as e:
# if e[0] == errno.ESRCH:
# # process has died
# return False
# else:
# raise # TODO
# 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 still_alive(pid):
# # process didn't exit cleanly, make one last effort to kill it
# os.kill(pid, signal.SIGKILL)
# if still_alive(pid):
# raise OSError("Process {} did not stop.".format(pid))
# os.remove(pidfile)
[docs]class Missing(object):
"""Basic web interface to say "not here"."""
config = {
'/': {
# Anything so as to prevent complaints about an empty config.
'tools.sessions.on': False,
}
}
@cherrypy.expose
def index(self):
return "[CherryPy server says:] Nothing to see here. Wrong URL path."
# noinspection PyUnresolvedReferences
[docs]def start_server(opts: Namespace) -> None:
"""
Start CherryPy server
"""
# if opts.daemonize and opts.server_user and opts.server_group:
# # ensure the that the daemon runs as specified user
# change_uid_gid(opts.server_user, opts.server_group)
cherrypy.config.update({
'server.socket_host': opts.host,
'server.socket_port': opts.port,
'server.thread_pool': opts.threads,
'server.server_name': opts.server_name,
'server.log_screen': opts.log_screen,
})
if opts.ssl_certificate and opts.ssl_private_key:
cherrypy.config.update({
'server.ssl_module': 'builtin',
'server.ssl_certificate': opts.ssl_certificate,
'server.ssl_private_key': opts.ssl_private_key,
})
log.info("Starting on host: {}".format(opts.host))
log.info("Starting on port: {}".format(opts.port))
log.info("Static files will be served from filesystem path: {}".format(
settings.STATIC_ROOT))
log.info("Static files will be served at URL path: {}".format(
CRATE_STATIC_URL_PATH))
log.info("CRATE will be at: {}".format(opts.root_path))
log.info("Thread pool size: {}".format(opts.threads))
static_config = {
'/': {
'tools.staticdir.root': settings.STATIC_ROOT,
'tools.staticdir.debug': opts.debug_static,
},
CRATE_STATIC_URL_PATH: {
'tools.staticdir.on': True,
'tools.staticdir.dir': '',
},
}
cherrypy.tree.mount(Missing(), '', config=static_config)
cherrypy.tree.graft(wsgi_application, opts.root_path)
# noinspection PyBroadException,PyPep8
try:
cherrypy.engine.start()
cherrypy.engine.block()
except: # 2017-03-13: shouldn't restrict to KeyboardInterrupt!
cherrypy.engine.stop()
def runcpserver(opts: Namespace) -> None:
# if opts.stop:
# if not opts.pidfile:
# raise ValueError("Must specify --pidfile to use --stop")
# print('stopping server')
# stop_server(opts.pidfile)
# return True
# if opts.daemonize:
# if not opts.pidfile:
# opts.pidfile = '/var/run/cpserver_{}.pid'.format(opts.port)
# stop_server(opts.pidfile)
#
# if opts.workdir:
# become_daemon(our_home_dir=opts.workdir)
# else:
# become_daemon()
#
# fp = open(opts.pidfile, 'w')
# fp.write("{}\n".format(os.getpid()))
# fp.close()
# Start the webserver
log.info('starting server with options {}'.format(opts))
start_server(opts)
def main():
command = Command()
parser = ArgumentParser()
command.add_arguments(parser)
cmdargs = parser.parse_args()
runcpserver(cmdargs)
if __name__ == '__main__':
main()