diff --git a/MANIFEST.in b/MANIFEST.in index efacf46d..fdb2c1c9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,7 @@ include LICENSE include NOTICE include README.rst include THANKS +include gunicorn/options.ini recursive-include tests * recursive-include examples * recursive-include doc * diff --git a/gunicorn/app.py b/gunicorn/app.py deleted file mode 100644 index cc2c988c..00000000 --- a/gunicorn/app.py +++ /dev/null @@ -1,94 +0,0 @@ -# -*- coding: utf-8 - -# -# This file is part of gunicorn released under the MIT license. -# See the NOTICE for more information. - -import os -import sys - -from gunicorn import util - -class Application(object): - """\ - An application interface for configuring and loading - the various necessities for any given web framework. - """ - - def get_config(self): - return {} - - def load(self): - raise NotImplementedError - -class WSGIApplication(Application): - - def __init__(self, app_uri): - self.app_uri = app_uri - - def load(self): - return util.import_app(self.app_uri) - -class DjangoApplication(Application): - - def __init__(self, settings_modname, project_path): - self.project_path = project_path - self.settings_modname = settings_modname - - # update sys.path - sys.path.insert(0, project_path) - sys.path.append(os.path.join(project_path, os.pardir)) - - def load(self): - import django.core.handlers.wsgi - os.environ['DJANGO_SETTINGS_MODULE'] = self.settings_modname - return django.core.handlers.wsgi.WSGIHandler() - -class PasterApplication(Application): - - def __init__(self, cfgurl, relpath, global_opts): - self.cfgurl = cfgurl - self.relpath = relpath - self.global_opts = global_opts - - def local_conf(self): - from paste.deploy import loadwsgi - ctx = loadwsgi.loadcontext(loadwsgi.SERVER, self.cfgurl, - relative_to=self.relpath) - - def mk_bind(): - host = ctx.local_conf.get('host') - port = ctx.local_conf.get('port') - if host and port: - return '%s:%s' % (host, port) - elif host: - return host - - ret = {} - vars = { - 'bind': mk_bind, - 'workers': lambda: ctx.local_conf.get('workers', 1), - 'umask': lambda: int(ctx.local_conf.get('umask', UMASK)), - 'group': lambda: ctx.local_conf.get('group'), - 'user': lambda: ctx.local_conf.get('user') - } - for vn in vars: - if self.global_ops.get(vn): - val = vars[vn]() - if val: - ret[vn] = val - - keys = ctx.local_conf.items() - keys = filter(self.global_opts.get, keys) - keys = filter(ret.has_key, keys) - ret.update((k, ctx.local_conf[k]) for k in keys) - - if not self.global_opts.get("debug"): - ret['debug'] = (ctx.global_conf.get('debug') == "true") - - ret['default_proc_name'] = ctx.global_conf.get('__file__') - - return ret - - def load(self): - from paste.deploy import loadapp - return loadapp(self.cfgurl, relative_to=self.relpath) diff --git a/gunicorn/app/__init__.py b/gunicorn/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gunicorn/app/base.py b/gunicorn/app/base.py new file mode 100644 index 00000000..c5754073 --- /dev/null +++ b/gunicorn/app/base.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 - +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import logging +import optparse +import os + +from gunicorn import __version__ +from gunicorn import util +from gunicorn.arbiter import Arbiter +from gunicorn.config import Config +from gunicorn.debug import spew + +class Application(object): + """\ + An application interface for configuring and loading + the various necessities for any given web framework. + """ + def __init__(self, usage): + self.log = logging.getLogger(__name__) + self.cfg = Config(usage) + + parser = self.cfg.parser() + opts, args = parser.parse_args() + cfg = self.init(parser, opts, args) + + # Load up the any app specific configuration + if cfg: + for k, v in cfg: + self.cfg.set(k.lower(), v) + + # Load up the config file if its found. + if opts.config and os.path.exists(opts.config): + cfg = globals().copy() + try: + execfile(opts.config, cfg, cfg) + except Exception, e: + print "Failed to read config file: %s" % opts.config + traceback.print_exc() + sys.exit(1) + + for k, v in cfg.iteritems(): + self.cfg.set(k.lower(), v) + + # Lastly, update the configuration with any command line + # settings. + for k, v in opts.__dict__.iteritems(): + if v is None or self.cfg.modified(k.lower()): + continue + self.cfg.set(k.lower(), v) + + self.configure_logging() + + def init(self, parser, opts, args): + raise NotImplementedError + + def load(self): + raise NotImplementedError + + def run(self): + if self.cfg.spew: + debug.spew() + if self.cfg.daemon: + util.daemonize() + else: + os.setpgrp() + Arbiter(self).run() + + def configure_logging(self): + """\ + Set the log level and choose the destination for log output. + """ + logger = logging.getLogger('gunicorn') + + handlers = [] + if self.cfg.logfile != "-": + handlers.append(logging.FileHandler(self.cfg.logfile)) + else: + handlers.append(logging.StreamHandler()) + + levels = { + "critical": logging.CRITICAL, + "error": logging.ERROR, + "warning": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG + } + + loglevel = levels.get(self.cfg.loglevel.lower(), logging.INFO) + logger.setLevel(loglevel) + + format = r"%(asctime)s [%(process)d] [%(levelname)s] %(message)s" + datefmt = r"%Y-%m-%d %H:%M:%S" + for h in handlers: + h.setFormatter(logging.Formatter(format, datefmt)) + logger.addHandler(h) + + diff --git a/gunicorn/app/djangoapp.py b/gunicorn/app/djangoapp.py new file mode 100644 index 00000000..b9074ece --- /dev/null +++ b/gunicorn/app/djangoapp.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 - +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import os +import sys + +import django.core.handlers.wsgi + +from gunicorn import util +from gunicorn.app.base import Application + +class DjangoApplication(Application): + + def init(parser, opts, args): + self.project_path = os.getcwd() + + if args: + settings_path = os.path.abspath(os.path.normpath(args[0])) + if not os.path.exists(settings_path): + self.no_settings(settings_path) + else: + self.project_path = os.path.dirname(settings_path) + else: + settings_path = os.path.join(project_path, "settings.py") + if not os.path.exists(settings_path): + self.no_settings(settings_path) + + project_name = os.path.split(project_path)[-1] + settings_name, ext = os.path.splitext(os.path.basename(settings_path)) + settings_modname = "%s.%s" % (project_name, settings_name) + self.cfg.default_proc_name = settings_modname + + sys.path.insert(0, self.project_path) + sys.path.append(os.path.join(project_path, os.pardir)) + + def no_settings(self, path): + error = "Settings file '%s' not found in current folder.\n" % path + sys.stderr.write(error) + sys.stderr.flush() + sys.exit(1) + + def load(self): + os.environ['DJANGO_SETTINGS_MODULE'] = self.settings_modname + return django.core.handlers.wsgi.WSGIHandler() diff --git a/gunicorn/app/pasterapp.py b/gunicorn/app/pasterapp.py new file mode 100644 index 00000000..dd62504d --- /dev/null +++ b/gunicorn/app/pasterapp.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 - +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import os +import pkg_resources +import sys + +from paste.deploy import loadapp, loadwsgi +SERVER = loadwsgi.SERVER + +class PasterApplication(Application): + + def init(self, parser, opts, args): + if len(args) != 1: + parser.error("No application name specified.") + + cfgfname = os.path.normpath(os.path.join(os.getcwd(), args[0])) + cfgfname = os.path.abspath(cfgfname) + if not os.path.exists(cfgfname): + parser.error("Config file not found.") + + self.cfgurl = 'config:%s' % cfgfname + self.relpath = os.path.dirname(cfgfname) + + sys.path.insert(0, self.relpath) + pkg_resources.working_set.add_entry(self.relpath) + + return self.app_config() + + def app_config(self): + cx = loadwsgi.loadcontext(SERVER, self.cfgurl, relative_to=self.relpath) + gc, lc = cx.global_conf, cx.local_conf + + cfg = {} + + host, port = lc.get('host'), lc.get('port') + if host and port: + cfg['bind'] = '%s:%s' % (host, port) + elif host: + cfg['bind'] = host + + cfg['workers'] = int(lc.get('workers', 1)) + cfg['umask'] = int(lc.get('umask', 0)) + cfg['default_proc_name'] = gc.get('__file__') + cfg.update(dict((k,v) for (k,v) in lc.items() if k not in cfg)) + return cfg + + def load(self): + return loadapp(self.cfgurl, relative_to=self.relpath) + +class PasterServerApplication(Application): + + def __init__(self, app, *args, **kwargs): + self.log = logging.getLogger(__name__) + self.cfg = Config() + self.app = app + + cfg = {} + host, port = kwargs.get('host'), kwargs.get('port') + if host and port: + cfg['bind'] = '%s:%s' % (host, port) + elif host: + cfg['bind'] = host + + if gcfg: + for k, v in list(gcfg.items()): + if k.lower() in self.cfg.settings: + self.cfg.set(k.lower(), v) + self.cfg.default_proc_name = kwargs.__file__ + + self.configure_logging(cfg) + + def load(self): + return self.app + + diff --git a/gunicorn/app/wsgiapp.py b/gunicorn/app/wsgiapp.py new file mode 100644 index 00000000..500290ba --- /dev/null +++ b/gunicorn/app/wsgiapp.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 - +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import os +import sys +import traceback + +from gunicorn import util +from gunicorn.app.base import Application + +class WSGIApplication(Application): + + def init(self, parser, opts, args): + if len(args) != 1: + parser.error("No application module specified.") + + self.cfg.set("default_proc_name", args[0]) + self.app_uri = args[0] + + sys.path.insert(0, os.getcwd()) + try: + self.load() + except: + print "Failed to import application: %s" % self.app_uri + traceback.print_exc() + sys.exit(1) + + def load(self): + return util.import_app(self.app_uri) \ No newline at end of file diff --git a/gunicorn/arbiter.py b/gunicorn/arbiter.py index cb330d48..405c9fb0 100644 --- a/gunicorn/arbiter.py +++ b/gunicorn/arbiter.py @@ -44,24 +44,19 @@ class Arbiter(object): if name[:3] == "SIG" and name[3] != "_" ) - def __init__(self, cfg, app): - self.cfg = cfg - self.app = app + def __init__(self, app): + self.cfg = app.cfg + self.app = app.load() self.log = logging.getLogger(__name__) - self.address = cfg.address - self.num_workers = cfg.workers - self.debug = cfg.debug - self.timeout = cfg.timeout - self.proc_name = cfg.proc_name - - try: - self.worker_class = cfg.worker_class - except ImportError, e: - self.log.error("%s" % e) - sys.exit(1) - + self.address = self.cfg.address + self.num_workers = self.cfg.workers + self.debug = self.cfg.debug + self.timeout = self.cfg.timeout + self.proc_name = self.cfg.proc_name + self.worker_class = self.cfg.worker_class + self.pidfile = None self.worker_age = 0 self.reexec_pid = 0 @@ -301,7 +296,7 @@ class Arbiter(object): os.environ['GUNICORN_FD'] = str(self.LISTENER.fileno()) os.chdir(self.START_CTX['cwd']) - self.cfg.before_exec(self) + self.cfg.pre_exec(self) os.execvpe(self.START_CTX[0], self.START_CTX['args'], os.environ) def murder_workers(self): @@ -366,7 +361,7 @@ class Arbiter(object): self.worker_age += 1 worker = self.worker_class(self.worker_age, self.pid, self.LISTENER, self.app, self.timeout/2.0, self.cfg) - self.cfg.before_fork(self, worker) + self.cfg.pre_fork(self, worker) pid = os.fork() if pid != 0: self.WORKERS[pid] = worker @@ -378,7 +373,7 @@ class Arbiter(object): util._setproctitle("worker [%s]" % self.proc_name) self.log.debug("Booting worker: %s (age: %s)" % ( worker_pid, self.worker_age)) - self.cfg.after_fork(self, worker) + self.cfg.post_fork(self, worker) worker.init_process() sys.exit(0) except SystemExit: diff --git a/gunicorn/config.py b/gunicorn/config.py index 5ebbe848..92d37266 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -3,99 +3,153 @@ # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. +import ConfigParser import grp +import inspect +import optparse import os import pwd import sys +import types +from gunicorn import __version__ from gunicorn import util +class Setting(object): + def __init__(self, name, opts): + self.name = name + self.section = opts["section"] + self.order = int(opts.get("order", 0)) + self.cli = opts["cli"].split() + self.type = opts["type"].strip() + self.arity = opts.get('arity', None) + if self.arity: + self.arity = int(self.arity) + self.meta = opts.get('meta', "").strip() or None + self.action = opts.get('action', "store").strip() + self.default = opts.get('default').strip() or None + self.desc = opts["desc"] + self.short = self.desc.splitlines()[0].strip() + + # Special case the callable types. + self.value = None + if self.default and self.type != 'callable': + self.set(self.default, modified=False) + + # A flag that tells us if this setting + # has been altered + self.modified = False + + def add_option(self, parser): + if not len(self.cli): + return + args = tuple(self.cli) + opttypes = { + "pos_int": "int", + "bool": None + } + kwargs = { + "dest": self.name, + "metavar": self.meta or None, + "action": self.action, + "type": opttypes.get(self.type, "string"), + "default": None, + "help": "%s [%s]" % (self.short, self.default) + } + parser.add_option(*args, **kwargs) + + def get(self): + return self.value + + def set(self, val, modified=True): + validator = getattr(self, "set_%s" % self.type) + self.value = validator(val) + self.modified = modified + + def set_bool(self, val): + if isinstance(val, types.BooleanType): + return val + if val.lower().strip() == "true": + return True + elif val.lower().strip() == "false": + return False + else: + raise ValueError("Invalid boolean: %s" % val) + + def set_pos_int(self, val): + if not isinstance(val, (types.IntType, types.LongType)): + val = int(val, 0) + if val < 0: + raise ValueError("Value must be positive: %s" % val) + return val + + def set_string(self, val): + return val.strip() + + def set_callable(self, val): + if not callable(val): + raise TypeError("Value is not callable: %s" % val) + arity = len(inspect.getargspec(val)[0]) + if arity != self.arity: + raise TypeError("Value must have an arity of: %s" % self.arity) + return val + class Config(object): - - DEFAULT_CONFIG_FILE = 'gunicorn.conf.py' - - DEFAULTS = dict( - backlog=2048, - bind='127.0.0.1:8000', - daemon=False, - debug=False, - default_proc_name = os.getcwd(), - group=None, - keepalive=2, - logfile='-', - loglevel='info', - pidfile=None, - proc_name = None, - spew=False, - timeout=30, - tmp_upload_dir=None, - umask="0", - user=None, - workers=1, - worker_connections=1000, - worker_class="egg:gunicorn#sync", - after_fork=lambda server, worker: server.log.info( - "Worker spawned (pid: %s)" % worker.pid), + def __init__(self, usage): + self.settings = {} + self.usage = usage + + path = os.path.join(os.path.dirname(__file__), "options.ini") + opts = ConfigParser.SafeConfigParser() + if not len(opts.read(path)): + raise RuntimeError("Options configuration file is missing!") - before_fork=lambda server, worker: True, + for sect in opts.sections(): + self.settings[sect] = Setting(sect, dict(opts.items(sect))) - before_exec=lambda server: server.log.info("Forked child, reexecuting") - ) - - def __init__(self, opts, path=None): - self.cfg = {} - self.opts = opts - if path is None: - path = os.path.join(os.getcwd(), self.DEFAULT_CONFIG_FILE) - self.path = path - self.load() - - def update(self, opts): - opts = dict((k, v) for (k, v) in opts.iteritems() if v is not None) - self.opts.update(opts) - self.cfg.update(opts) - - def load(self): - self.cfg = self.DEFAULTS.copy() - - if os.path.exists(self.path): - try: - execfile(self.path, globals(), self.cfg) - except Exception, e: - sys.exit("Could not read config file: %r\n %s" % (self.path, e)) - self.cfg.pop("__builtins__", None) - - opts = dict((k, v) for (k, v) in opts.iteritems() if v is not None) - self.cfg.update(opts) - - def __getitem__(self, key): - try: - return getattr(self, key) - except AttributeError: - pass - return self.cfg[key] + # Special case hook functions + self.settings['pre_fork'].set(def_pre_fork, modified=False) + self.settings['post_fork'].set(def_post_fork, modified=False) + self.settings['pre_exec'].set(def_pre_exec, modified=False) - def __getattr__(self, key): - try: - super(Config, self).__getattribute__(key) - except AttributeError: - if key in self.cfg: - return self.cfg[key] - raise - - def __contains__(self, key): - return (key in self.cfg) - - def __iter__(self): - return self.cfg.iteritems() + def __getattr__(self, name): + if name not in self.settings: + raise AttributeError("No configuration setting for: %s" % name) + return self.settings[name].get() + + def __setattr__(self, name, value): + if name != "settings" and name in self.settings: + raise AttributeError("Invalid access!") + super(Config, self).__setattr__(name, value) + + def set(self, name, value): + if name not in self.settings: + raise AttributeError("No configuration setting for: %s" % name) + self.settings[name].set(value) - def get(self, key, default=None): - return self.cfg.get(key, default) + def parser(self): + kwargs = { + "usage": self.usage, + "version": __version__, + "formatter": HelpFormatter() + } + parser = optparse.OptionParser(**kwargs) + + keys = self.settings.keys() + def sorter(k): + return (self.settings[k].section, self.settings[k].order) + keys.sort(key=sorter) + for k in keys: + self.settings[k].add_option(parser) + return parser + + def was_modified(self, name): + return self.settings[name].modified @property def worker_class(self): - uri = self.cfg.get('worker_class', None) or 'egg:gunicorn#sync' + uri = self.settings['worker_class'].get() worker_class = util.load_worker_class(uri) if hasattr(worker_class, "setup"): worker_class.setup() @@ -103,72 +157,69 @@ class Config(object): @property def workers(self): - if not self.cfg.get('workers'): - raise RuntimeError("invalid workers number") - workers = int(self.cfg["workers"]) - if not workers: - raise RuntimeError("number of workers < 1") - if self.cfg['debug'] == True: - workers = 1 - return workers + if self.settings['debug'].get(): + return 1 + return self.settings['workers'].get() @property def address(self): - if not self.cfg['bind']: - raise RuntimeError("Listener address is not set") - return util.parse_address(util.to_bytestring(self.cfg['bind'])) - - @property - def umask(self): - if not self.cfg.get('umask'): - return 0 - umask = self.cfg['umask'] - if isinstance(umask, basestring): - return int(umask, 0) - return umask + bind = self.settings['bind'].get() + return util.parse_address(util.to_bytestring(bind)) @property def uid(self): - if not self.cfg.get('user'): - return os.geteuid() + user = self.settings['user'].get() - user = self.cfg.get('user') - if user.isdigit() or isinstance(user, int): - uid = int(user) + if not user: + return os.geteuid() + elif user.isdigit() or isinstance(user, int): + return int(user) else: - uid = pwd.getpwnam(user).pw_uid - return uid + return pwd.getpwnam(user).pw_uid @property def gid(self): - if not self.cfg.get('group'): + group = self.settings['group'].get() + + if not group: return os.getegid() - group = self.cfg.get('group') - if group.isdigit() or isinstance(group, int): - gid = int(group) + elif group.isdigit() or isinstance(user, int): + return int(group) else: - gid = grp.getgrnam(group).gr_gid - return gid + return grp.getgrnam(group).gr_gid @property def proc_name(self): - if not self.cfg.get('proc_name'): - return self.cfg.get('default_proc_name') - return self.cfg.get('proc_name') - - def after_fork(self, *args): - return self._hook("after_fork", *args) - - def before_fork(self, *args): - return self._hook("before_fork", *args) - - def before_exec(self, *args): - return self._hook("before_exec", *args) + pn = self.settings['proc_name'].get() + if pn: + return pn + else: + return self.settings['default_proc_name'] + + @property + def pre_fork(self): + return self.settings['pre_fork'].get() + + @property + def post_fork(self): + return self.settings['post_fork'].get() + + @property + def pre_exec(self): + return self.settings['pre_exec'].get() + + +def def_pre_fork(server, worker): + pass + +def def_post_fork(server, worker): + server.log.info("Worker spawned (pid: %s)" % worker.pid) + +def def_pre_exec(server): + server.log.info("Forked child, reexecuting.") + + +class HelpFormatter(optparse.IndentedHelpFormatter): + pass + - def _hook(self, hookname, *args): - hook = self.cfg.get(hookname) - if not hook: - return - if not callable(hook): - raise RuntimeError("%r hook isn't a callable" % hookname) - return hook(*args) diff --git a/gunicorn/http/request.py b/gunicorn/http/request.py index 5072edf9..b8144a29 100644 --- a/gunicorn/http/request.py +++ b/gunicorn/http/request.py @@ -45,9 +45,8 @@ class Request(object): "SERVER_SOFTWARE": "gunicorn/%s" % __version__ } - def __init__(self, socket, client_address, server_address, conf): - self.debug = conf['debug'] - self.conf = conf + def __init__(self, cfg, socket, client_address, server_address): + self.cfg = cfg self.socket = socket self.client_address = client_address @@ -85,18 +84,17 @@ class Request(object): self.socket.send("HTTP/1.1 100 Continue\r\n\r\n") if not self.parser.content_len and not self.parser.is_chunked: - wsgi_input = TeeInput(self.socket, self.parser, StringIO(), - self.conf) + wsgi_input = TeeInput(self.cfg, self.socket, self.parser, StringIO()) content_length = "0" else: - wsgi_input = TeeInput(self.socket, self.parser, buf2, self.conf) + wsgi_input = TeeInput(self.cfg, self.socket, self.parser, buf2) content_length = str(wsgi_input.len) # This value should evaluate true if an equivalent application # object may be simultaneously invoked by another process, and # should evaluate false otherwise. In debug mode we fall to one # worker so we comply to pylons and other paster app. - wsgi_multiprocess = (self.debug == False) + wsgi_multiprocess = self.cfg.workers > 1 # authors should be aware that REMOTE_HOST and REMOTE_ADDR # may not qualify the remote addr: diff --git a/gunicorn/http/tee.py b/gunicorn/http/tee.py index d067ad25..385051d1 100644 --- a/gunicorn/http/tee.py +++ b/gunicorn/http/tee.py @@ -26,8 +26,8 @@ class TeeInput(object): CHUNK_SIZE = util.CHUNK_SIZE - def __init__(self, socket, parser, buf, conf): - self.conf = conf + def __init__(self, cfg, socket, parser, buf): + self.cfg = cfg self.buf = StringIO() self.parser = parser self._sock = socket @@ -39,7 +39,7 @@ class TeeInput(object): elif self._len and self._len < util.MAX_BODY: self.tmp = StringIO() else: - self.tmp = tempfile.TemporaryFile(dir=self.conf['tmp_upload_dir']) + self.tmp = tempfile.TemporaryFile(dir=self.cfg['tmp_upload_dir']) if len(buf.getvalue()) > 0: chunk, self.buf = parser.filter_body(buf) diff --git a/gunicorn/main.py b/gunicorn/main.py index 2b2324f2..ba9c66b2 100644 --- a/gunicorn/main.py +++ b/gunicorn/main.py @@ -2,176 +2,32 @@ # # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. - -import logging -import optparse as op -import os -import pkg_resources -import sys - -from gunicorn.arbiter import Arbiter -from gunicorn.config import Config -from gunicorn.debug import spew -from gunicorn import util, __version__ - -LOG_LEVELS = { - "critical": logging.CRITICAL, - "error": logging.ERROR, - "warning": logging.WARNING, - "info": logging.INFO, - "debug": logging.DEBUG -} - -UMASK = "0" - -def options(): - """ build command lines options passed to OptParse object """ - return [ - op.make_option('-c', '--config', dest='config', type='string', - help='Config file. [%default]'), - op.make_option('-b', '--bind', dest='bind', - help='Adress to listen on. Ex. 127.0.0.1:8000 or unix:/tmp/gunicorn.sock'), - op.make_option('-w', '--workers', dest='workers', - help='Number of workers to spawn. [1]'), - op.make_option('-k', '--worker-class', dest='worker_class', - help="The type of request processing to use "+ - "[egg:gunicorn#sync]"), - op.make_option('-p','--pid', dest='pidfile', - help='set the background PID FILE'), - op.make_option('-D', '--daemon', dest='daemon', action="store_true", - help='Run daemonized in the background.'), - op.make_option('-m', '--umask', dest="umask", type='string', - help="Define umask of daemon process"), - op.make_option('-u', '--user', dest="user", - help="Change worker user"), - op.make_option('-g', '--group', dest="group", - help="Change worker group"), - op.make_option('-n', '--name', dest='proc_name', - help="Process name"), - op.make_option('--log-level', dest='loglevel', - help='Log level below which to silence messages. [info]'), - op.make_option('--log-file', dest='logfile', - help='Log to a file. - equals stdout. [-]'), - op.make_option('-d', '--debug', dest='debug', action="store_true", - default=False, help='Debug mode. only 1 worker.'), - op.make_option('--spew', dest='spew', action="store_true", - default=False, help="Install a trace hook") - ] - -def main(usage, get_app): - """\ - Used by the various runners to setup options and - launch the arbiter. - """ - vrs = "%prog " + __version__ - parser = op.OptionParser(usage=usage, option_list=options(), version=vrs) - opts, args = parser.parse_args() - - cfg = Config(opts.__dict__, opts.config) - app = get_app(parser, opts, args) - cfg.update(app.get_config()) - if cfg.spew: - spew() - if cfg.daemon: - daemonize() - else: - os.setpgrp() - configure_logging(cfg) - - Arbiter(cfg, app.load()).run() def run(): """\ The ``gunicorn`` command line runner for launcing Gunicorn with generic WSGI applications. - """ - sys.path.insert(0, os.getcwd()) - - def get_app(parser, opts, args): - if len(args) != 1: - parser.error("No application module specified.") - opts.default_proc_name = args[0] - - application = app.WSGIApplication(args[0]) - try: - application.load() - except Exception, e: - parser.error("Failed to import application module:\n %s" % e) - return application - - main("%prog [OPTIONS] APP_MODULE", get_app) + """ + from gunicorn.app.wsgiapp import WSGIApplication + WSGIApplication("%prog [OPTIONS] APP_MODULE").run() def run_django(): """\ The ``gunicorn_django`` command line runner for launching Django applications. """ - - def settings_notfound(path): - error = "Settings file '%s' not found in current folder.\n" % path - sys.stderr.write(error) - sys.stderr.flush() - sys.exit(1) - - def get_app(parser, opts, args): - import django.core.handlers.wsgi - - project_path = os.getcwd() - - if args: - settings_path = os.path.abspath(os.path.normpath(args[0])) - if not os.path.exists(settings_path): - settings_notfound(settings_path) - else: - project_path = os.path.dirname(settings_path) - else: - settings_path = os.path.join(project_path, "settings.py") - if not os.path.exists(settings_path): - settings_notfound(settings_path) - - # set environ - project_name = os.path.split(project_path)[-1] - settings_name, ext = os.path.splitext(os.path.basename(settings_path)) - settings_modname = "%s.%s" % (project_name, settings_name) - opts.default_proc_name = settings_modname - - # django wsgi app - return app.DjangoApplication(settings_modname, project_path) - - - - main("%prog [OPTIONS] [SETTINGS_PATH]", get_app) + from gunicorn.app.djangoapp import DjangoApplication + DjangoApplication("%prog [OPTIONS] [SETTINGS_PATH]").run() def run_paster(): """\ The ``gunicorn_paster`` command for launcing Paster compatible apllications like Pylons or Turbogears2 """ - from paste.deploy import loadwsgi + from gunicorn.app.pasterapp import PasterApplication + PasterApplication("%prog [OPTIONS] pasteconfig.ini").run() - def get_app(parser, opts, args): - if len(args) != 1: - parser.error("No application name specified.") - - cfgfname = os.path.normpath(os.path.join(os.getcwd(), args[0])) - cfgfname = os.path.abspath(cfgfname) - if not os.path.exists(cfgfname): - parser.error("Config file not found.") - - cfgurl = 'config:%s' % cfgfname - relpath = os.path.dirname(cfgfname) - - # load module in sys path - sys.path.insert(0, relpath) - - # add to eggs - pkg_resources.working_set.add_entry(relpath) - - return app.PasterApplication(cfgurl, relpath, opts.__dict__) - - main("%prog [OPTIONS] pasteconfig.ini", get_app) - -def paste_server(app, gcfg=None, host="127.0.0.1", port=None, *args, **kwargs): +def paste_server(*args, **kwargs): """\ A paster server. @@ -183,77 +39,9 @@ def paste_server(app, gcfg=None, host="127.0.0.1", port=None, *args, **kwargs): port = 5000 """ - opts = kwargs.copy() - if port and not host.startswith("unix:"): - bind = "%s:%s" % (host, port) - else: - bind = host - opts['bind'] = bind + from gunicorn.app.pasterapp import PasterServerApplication + PasterServerApplication(app, *args, **kwargs).run() - if gcfg: - for key, value in list(gcfg.items()): - if value and value is not None: - if key == "debug": - value = (value == "true") - opts[key] = value - opts['default_proc_name'] = opts['__file__'] - - cfg = Config(opts) - - if cfg.spew: - spew() - if cfg.daemon: - daemonize() - else: - os.setpgrp() - configure_logging(cfg) - Arbiter(cfg, app).run() + -def daemonize(): - """\ - Standard daemonization of a process. Code is basd on the - ActiveState recipe at: - http://code.activestate.com/recipes/278731/ - """ - if not 'GUNICORN_FD' in os.environ: - if os.fork() == 0: - os.setsid() - if os.fork() != 0: - os.umask(0) - else: - os._exit(0) - else: - os._exit(0) - - maxfd = util.get_maxfd() - # Iterate through and close all file descriptors. - for fd in range(0, maxfd): - try: - os.close(fd) - except OSError: # ERROR, fd wasn't open to begin with (ignored) - pass - - os.open(util.REDIRECT_TO, os.O_RDWR) - os.dup2(0, 1) - os.dup2(0, 2) - -def configure_logging(opts): - """\ - Set the log level and choose the destination for log output. - """ - handlers = [] - if opts['logfile'] != "-": - handlers.append(logging.FileHandler(opts['logfile'])) - else: - handlers.append(logging.StreamHandler()) - - loglevel = LOG_LEVELS.get(opts['loglevel'].lower(), logging.INFO) - - logger = logging.getLogger('gunicorn') - logger.setLevel(loglevel) - format = r"%(asctime)s [%(process)d] [%(levelname)s] %(message)s" - datefmt = r"%Y-%m-%d %H:%M:%S" - for h in handlers: - h.setFormatter(logging.Formatter(format, datefmt)) - logger.addHandler(h) diff --git a/gunicorn/options.ini b/gunicorn/options.ini new file mode 100644 index 00000000..e5957c24 --- /dev/null +++ b/gunicorn/options.ini @@ -0,0 +1,334 @@ +# This file is read by config.py to set up the default +# information for all server settings. It is here so +# that we can use the data to generate all the various +# docs related to configuration information. + +# +# Config file +# + +[config] +section = Config +cli = -c --config +meta = FILE +type = string +default = gunicorn.conf.py +desc = The path to a Gunicorn config file. + + By default Gunicorn will try to read a file named 'gunicorn.conf.py' in the + current directory. + + Only has an effect when specified on the command line or as part of an + application specific configuration. + +# +# Server Socket +# + +[bind] +section = Server Socket +order = 0 +cli = -b --bind +meta = ADDRESS +type = string +default = 127.0.0.1:8000 +desc = The socket to bind. + +[backlog] +section = Server Socket +order = 1 +cli = --backlog +meta = INT +type = pos_int +default = 2048 +desc = The maximum number of pending connections. + + This refers to the number of clients that can be waiting to be served. + Exceeding this number results in the client getting an error when attempting + to connect. It should only affect servers under significant load. + + Must be a positive integer. Generally set in the 64-2048 range. + +# +# Worker processes +# + +[workers] +section = Worker Processes +order = 1 +cli = -w --workers +meta = INT +type = pos_int +default = 1 +desc = The number of worker process for handling requests. + + A positive integer generally in the 2-4 x $(NUM_CORES) range. You'll want to + vary this a bit to find the best for your particular application's work + load. + +[worker_class] +section = Worker Processes +order = 2 +cli = -k --worker-class +meta = STRING +type = string +default = egg:gunicorn#sync +desc = The type of workers to use. + + The default async class should handle most 'normal' types of work loads. + You'll want to read http://gunicorn/deployment.hml for information on when + you might want to choose one of the other worker classes. + + An string referring to a 'gunicorn.workers' entry point or a MODULE:CLASS + pair where CLASS is a subclass of gunicorn.workers.base.Worker. The default + provided values are: + + egg:gunicorn#sync + egg:gunicorn#eventlet - Requires eventlet >= 0.9.7 + egg:gunicorn#gevent - Requires gevent >= 0.12.2 (?) + egg:gunicorn#tornado - Requires tornado >= 0.2 + +[worker_connections] +section = Worker Processes +order = 3 +cli = --worker-connections +meta = INT +type = pos_int +default = 1000 +desc = The maximum number of simultaneous clients. + + This setting only affects the Eventlet and Gevent worker types. + + A positive integer generally set to around 1000. + +[timeout] +section = Worker Processes +order = 4 +cli = -t --timeout +meta = INT +type = pos_int +default = 30 +desc = Workers silent for more than this many seconds are killed and restarted. + + Generally set to thirty seconds. Only set this noticeably higher if you're + sure of the repercussions for sync workers. For the non sync workers it just + means that the worker process is still communicating and is not tied to the + length of time required to handle a single request. + +[keepalive] +section = Worker Processes +order = 5 +cli = --keep-alive +meta = INT +type = pos_int +default = 2 +desc = The number of seconds to wait for requests on a Keep-Alive connection. + + A positive integer. Generally set in the 1-5 seconds range. + +# +# Debugging +# + +[debug] +section = Debugging +order = 1 +cli = -d --debug +type = bool +action = store_true +default = False +desc = Turn on debugging in the server. + + This limits the number of worker processes to 1 and changes some error + handling that's sent to clients. + +[spew] +section = Debugging +order = 2 +cli = --spew +type = bool +action = store_true +default = False +desc = Install a trace function that spews every line executed by the server. + + This is the nuclear option. + +# +# Server mechanics +# + +[daemon] +section = Server Mechanics +order = 1 +cli = -D --daemon +action = store_true +type = bool +default = False +desc = Daemonize the Gunicorn process. + + Detaches the server from the controlling terminal and enters the background. + +[pidfile] +section = Server Mechanics +order = 2 +cli = -p --pid +meta = FILE +type = string +default = +desc = A filename to use for the PID file. + + If not set, no PID file will be written. + +[user] +section = Server Mechanics +order = 3 +cli = -u --user +type = string +default = +desc = Switch worker processes to run as this user. + + A valid user id (as an integer) or the name of a user that can be retrieved + with a call to pwd.getpwnam(value) or None to not change the worker process + user. + +[group] +section = Server Mechanics +order = 4 +cli = -g --group +type = string +default = +desc = Switch worker process to run as this group. + + A valid group id (as an integer) or the name of a user that can be retrieved + with a call to pwd.getgrnam(value) or None to change the worker processes + group. + +[umask] +section = Server Mechanics +order = 5 +cli = -m --umask +meta = INT +type = pos_int +default = 0 +desc = A bit mask for the file mode on files written by Gunicorn. + + Note that this affects unix socket permissions. + + A valid value for the os.umask(mode) call or a string compatible with + int(value, 0) (0 means Python guesses the base, so values like "0", "0xFF", + "0022" are valid for decimal, hex, and octal representations) + +[tmp_upload_dir] +section = Server Mechanics +order = 6 +cli = +meta = DIR +type = string +default = +desc = Directory to store temporary request data as they are read. + + This may disappear in the near future. + + This path should be writable by the process permissions set for Gunicorn + workers. If not specified, Gunicorn will choose a system generated temporary + directory. + +# +# Logging +# + +[logfile] +section = Logging +order = 1 +cli = --log-file +meta = FILE +type = string +default = - +desc = The log file to write to. + + "-" means log to stdout. + +[loglevel] +section = Logging +order = 2 +cli = --log-level +meta = LEVEL +type = string +default = info +desc = The granularity of log output + + Valid level names are: + + debug + info + warning + error + critical + +# +# Process naming +# + +[proc_name] +section = Process Naming +order = 1 +cli = -n --name +meta = STRING +type = string +default = gunicorn +desc = A base to use with setproctitle for process naming. + + This affects things like 'ps' and 'top'. If you're going to be running more + than one instance of Gunicorn you'll probably want to set a name to tell + them apart. This requires that you install the setproctitle module. + + It defaults to 'gunicorn'. + +[default_proc_name] +section = Process Naming +order = 2 +cli = +type = string +default = gunicorn +desc = Internal setting that is adjusted for each type of application. + +# +# Server hooks +# + +[pre_fork] +section = Server Hooks +order = 1 +cli = +type = callable +arity = 2 +default = +desc = Called just before a worker is forked. + + The callable needs to accept two instance variables for the Arbiter and new + Worker. + +[post_fork] +section = Server Hooks +order = 2 +cli = +type = callable +arity = 2 +default = +desc = Called just after a worker has been forked. + + The callable needs to accept two instance variables for the Arbiter and new + Worker. + +[pre_exec] +section = Server Hooks +order = 3 +cli = +type = callable +arity = 1 +default = +desc = Called just before a new master process is forked. + + The callable needs to accept a single instance variable for the Arbiter. + + Cannot be specified from the command line. diff --git a/gunicorn/sock.py b/gunicorn/sock.py index 18ed08b4..3b913588 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -36,7 +36,7 @@ class BaseSocket(object): if not bound: self.bind(sock) sock.setblocking(0) - sock.listen(self.conf['backlog']) + sock.listen(self.conf.backlog) return sock diff --git a/gunicorn/util.py b/gunicorn/util.py index e46610bf..9c3fd94b 100644 --- a/gunicorn/util.py +++ b/gunicorn/util.py @@ -208,4 +208,33 @@ def to_bytestring(s): return s def is_hoppish(header): - return header.lower().strip() in hop_headers \ No newline at end of file + return header.lower().strip() in hop_headers + +def daemonize(): + """\ + Standard daemonization of a process. Code is basd on the + ActiveState recipe at: + http://code.activestate.com/recipes/278731/ + """ + if not 'GUNICORN_FD' in os.environ: + if os.fork() == 0: + os.setsid() + if os.fork() != 0: + os.umask(0) + else: + os._exit(0) + else: + os._exit(0) + + maxfd = util.get_maxfd() + + # Iterate through and close all file descriptors. + for fd in range(0, maxfd): + try: + os.close(fd) + except OSError: # ERROR, fd wasn't open to begin with (ignored) + pass + + os.open(util.REDIRECT_TO, os.O_RDWR) + os.dup2(0, 1) + os.dup2(0, 2) \ No newline at end of file diff --git a/gunicorn/workers/async.py b/gunicorn/workers/async.py index 999d9622..3fff7dd9 100644 --- a/gunicorn/workers/async.py +++ b/gunicorn/workers/async.py @@ -21,8 +21,7 @@ class AsyncWorker(Worker): self.worker_connections = self.cfg.worker_connections def keepalive_request(self, client, addr): - return http.KeepAliveRequest(client, addr, self.address, - self.cfg) + return http.KeepAliveRequest(self.cfg, client, addr, self.address) def handle(self, client, addr): try: diff --git a/gunicorn/workers/sync.py b/gunicorn/workers/sync.py index c4c35784..7597234d 100644 --- a/gunicorn/workers/sync.py +++ b/gunicorn/workers/sync.py @@ -89,7 +89,7 @@ class SyncWorker(Worker): util.close(client) def handle_request(self, client, addr): - req = http.Request(client, addr, self.address, self.cfg) + req = http.Request(self.cfg, client, addr, self.address) try: environ = req.read() if not environ or not req.parser.status_line: