558 lines
16 KiB
Python
558 lines
16 KiB
Python
|
# -*- coding: utf-8 -
|
||
|
#
|
||
|
# This file is part of gunicorn released under the MIT license.
|
||
|
# See the NOTICE for more information.
|
||
|
|
||
|
from __future__ import print_function
|
||
|
|
||
|
import email.utils
|
||
|
import fcntl
|
||
|
import io
|
||
|
import os
|
||
|
import pkg_resources
|
||
|
import pwd
|
||
|
import random
|
||
|
import socket
|
||
|
import sys
|
||
|
import textwrap
|
||
|
import time
|
||
|
import traceback
|
||
|
import inspect
|
||
|
import errno
|
||
|
import warnings
|
||
|
import logging
|
||
|
import re
|
||
|
|
||
|
from gunicorn import _compat
|
||
|
from gunicorn.errors import AppImportError
|
||
|
from gunicorn.six import text_type
|
||
|
from gunicorn.workers import SUPPORTED_WORKERS
|
||
|
|
||
|
REDIRECT_TO = getattr(os, 'devnull', '/dev/null')
|
||
|
|
||
|
# Server and Date aren't technically hop-by-hop
|
||
|
# headers, but they are in the purview of the
|
||
|
# origin server which the WSGI spec says we should
|
||
|
# act like. So we drop them and add our own.
|
||
|
#
|
||
|
# In the future, concatenation server header values
|
||
|
# might be better, but nothing else does it and
|
||
|
# dropping them is easier.
|
||
|
hop_headers = set("""
|
||
|
connection keep-alive proxy-authenticate proxy-authorization
|
||
|
te trailers transfer-encoding upgrade
|
||
|
server date
|
||
|
""".split())
|
||
|
|
||
|
try:
|
||
|
from setproctitle import setproctitle
|
||
|
|
||
|
def _setproctitle(title):
|
||
|
setproctitle("gunicorn: %s" % title)
|
||
|
except ImportError:
|
||
|
def _setproctitle(title):
|
||
|
return
|
||
|
|
||
|
|
||
|
try:
|
||
|
from importlib import import_module
|
||
|
except ImportError:
|
||
|
def _resolve_name(name, package, level):
|
||
|
"""Return the absolute name of the module to be imported."""
|
||
|
if not hasattr(package, 'rindex'):
|
||
|
raise ValueError("'package' not set to a string")
|
||
|
dot = len(package)
|
||
|
for _ in range(level, 1, -1):
|
||
|
try:
|
||
|
dot = package.rindex('.', 0, dot)
|
||
|
except ValueError:
|
||
|
msg = "attempted relative import beyond top-level package"
|
||
|
raise ValueError(msg)
|
||
|
return "%s.%s" % (package[:dot], name)
|
||
|
|
||
|
def import_module(name, package=None):
|
||
|
"""Import a module.
|
||
|
|
||
|
The 'package' argument is required when performing a relative import. It
|
||
|
specifies the package to use as the anchor point from which to resolve the
|
||
|
relative import to an absolute import.
|
||
|
|
||
|
"""
|
||
|
if name.startswith('.'):
|
||
|
if not package:
|
||
|
raise TypeError("relative imports require the 'package' argument")
|
||
|
level = 0
|
||
|
for character in name:
|
||
|
if character != '.':
|
||
|
break
|
||
|
level += 1
|
||
|
name = _resolve_name(name[level:], package, level)
|
||
|
__import__(name)
|
||
|
return sys.modules[name]
|
||
|
|
||
|
|
||
|
def load_class(uri, default="gunicorn.workers.sync.SyncWorker",
|
||
|
section="gunicorn.workers"):
|
||
|
if inspect.isclass(uri):
|
||
|
return uri
|
||
|
if uri.startswith("egg:"):
|
||
|
# uses entry points
|
||
|
entry_str = uri.split("egg:")[1]
|
||
|
try:
|
||
|
dist, name = entry_str.rsplit("#", 1)
|
||
|
except ValueError:
|
||
|
dist = entry_str
|
||
|
name = default
|
||
|
|
||
|
try:
|
||
|
return pkg_resources.load_entry_point(dist, section, name)
|
||
|
except:
|
||
|
exc = traceback.format_exc()
|
||
|
msg = "class uri %r invalid or not found: \n\n[%s]"
|
||
|
raise RuntimeError(msg % (uri, exc))
|
||
|
else:
|
||
|
components = uri.split('.')
|
||
|
if len(components) == 1:
|
||
|
while True:
|
||
|
if uri.startswith("#"):
|
||
|
uri = uri[1:]
|
||
|
|
||
|
if uri in SUPPORTED_WORKERS:
|
||
|
components = SUPPORTED_WORKERS[uri].split(".")
|
||
|
break
|
||
|
|
||
|
try:
|
||
|
return pkg_resources.load_entry_point("gunicorn",
|
||
|
section, uri)
|
||
|
except:
|
||
|
exc = traceback.format_exc()
|
||
|
msg = "class uri %r invalid or not found: \n\n[%s]"
|
||
|
raise RuntimeError(msg % (uri, exc))
|
||
|
|
||
|
klass = components.pop(-1)
|
||
|
|
||
|
try:
|
||
|
mod = import_module('.'.join(components))
|
||
|
except:
|
||
|
exc = traceback.format_exc()
|
||
|
msg = "class uri %r invalid or not found: \n\n[%s]"
|
||
|
raise RuntimeError(msg % (uri, exc))
|
||
|
return getattr(mod, klass)
|
||
|
|
||
|
|
||
|
def get_username(uid):
|
||
|
""" get the username for a user id"""
|
||
|
return pwd.getpwuid(uid).pw_name
|
||
|
|
||
|
|
||
|
def set_owner_process(uid, gid, initgroups=False):
|
||
|
""" set user and group of workers processes """
|
||
|
|
||
|
if gid:
|
||
|
if uid:
|
||
|
try:
|
||
|
username = get_username(uid)
|
||
|
except KeyError:
|
||
|
initgroups = False
|
||
|
|
||
|
# versions of python < 2.6.2 don't manage unsigned int for
|
||
|
# groups like on osx or fedora
|
||
|
gid = abs(gid) & 0x7FFFFFFF
|
||
|
|
||
|
if initgroups:
|
||
|
os.initgroups(username, gid)
|
||
|
elif gid != os.getgid():
|
||
|
os.setgid(gid)
|
||
|
|
||
|
if uid:
|
||
|
os.setuid(uid)
|
||
|
|
||
|
|
||
|
def chown(path, uid, gid):
|
||
|
gid = abs(gid) & 0x7FFFFFFF # see note above.
|
||
|
os.chown(path, uid, gid)
|
||
|
|
||
|
|
||
|
if sys.platform.startswith("win"):
|
||
|
def _waitfor(func, pathname, waitall=False):
|
||
|
# Peform the operation
|
||
|
func(pathname)
|
||
|
# Now setup the wait loop
|
||
|
if waitall:
|
||
|
dirname = pathname
|
||
|
else:
|
||
|
dirname, name = os.path.split(pathname)
|
||
|
dirname = dirname or '.'
|
||
|
# Check for `pathname` to be removed from the filesystem.
|
||
|
# The exponential backoff of the timeout amounts to a total
|
||
|
# of ~1 second after which the deletion is probably an error
|
||
|
# anyway.
|
||
|
# Testing on a i7@4.3GHz shows that usually only 1 iteration is
|
||
|
# required when contention occurs.
|
||
|
timeout = 0.001
|
||
|
while timeout < 1.0:
|
||
|
# Note we are only testing for the existence of the file(s) in
|
||
|
# the contents of the directory regardless of any security or
|
||
|
# access rights. If we have made it this far, we have sufficient
|
||
|
# permissions to do that much using Python's equivalent of the
|
||
|
# Windows API FindFirstFile.
|
||
|
# Other Windows APIs can fail or give incorrect results when
|
||
|
# dealing with files that are pending deletion.
|
||
|
L = os.listdir(dirname)
|
||
|
if not L if waitall else name in L:
|
||
|
return
|
||
|
# Increase the timeout and try again
|
||
|
time.sleep(timeout)
|
||
|
timeout *= 2
|
||
|
warnings.warn('tests may fail, delete still pending for ' + pathname,
|
||
|
RuntimeWarning, stacklevel=4)
|
||
|
|
||
|
def _unlink(filename):
|
||
|
_waitfor(os.unlink, filename)
|
||
|
else:
|
||
|
_unlink = os.unlink
|
||
|
|
||
|
|
||
|
def unlink(filename):
|
||
|
try:
|
||
|
_unlink(filename)
|
||
|
except OSError as error:
|
||
|
# The filename need not exist.
|
||
|
if error.errno not in (errno.ENOENT, errno.ENOTDIR):
|
||
|
raise
|
||
|
|
||
|
|
||
|
def is_ipv6(addr):
|
||
|
try:
|
||
|
socket.inet_pton(socket.AF_INET6, addr)
|
||
|
except socket.error: # not a valid address
|
||
|
return False
|
||
|
except ValueError: # ipv6 not supported on this platform
|
||
|
return False
|
||
|
return True
|
||
|
|
||
|
|
||
|
def parse_address(netloc, default_port=8000):
|
||
|
if re.match(r'unix:(//)?', netloc):
|
||
|
return re.split(r'unix:(//)?', netloc)[-1]
|
||
|
|
||
|
if netloc.startswith("tcp://"):
|
||
|
netloc = netloc.split("tcp://")[1]
|
||
|
|
||
|
# get host
|
||
|
if '[' in netloc and ']' in netloc:
|
||
|
host = netloc.split(']')[0][1:].lower()
|
||
|
elif ':' in netloc:
|
||
|
host = netloc.split(':')[0].lower()
|
||
|
elif netloc == "":
|
||
|
host = "0.0.0.0"
|
||
|
else:
|
||
|
host = netloc.lower()
|
||
|
|
||
|
#get port
|
||
|
netloc = netloc.split(']')[-1]
|
||
|
if ":" in netloc:
|
||
|
port = netloc.split(':', 1)[1]
|
||
|
if not port.isdigit():
|
||
|
raise RuntimeError("%r is not a valid port number." % port)
|
||
|
port = int(port)
|
||
|
else:
|
||
|
port = default_port
|
||
|
return (host, port)
|
||
|
|
||
|
|
||
|
def close_on_exec(fd):
|
||
|
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||
|
flags |= fcntl.FD_CLOEXEC
|
||
|
fcntl.fcntl(fd, fcntl.F_SETFD, flags)
|
||
|
|
||
|
|
||
|
def set_non_blocking(fd):
|
||
|
flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK
|
||
|
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
|
||
|
|
||
|
|
||
|
def close(sock):
|
||
|
try:
|
||
|
sock.close()
|
||
|
except socket.error:
|
||
|
pass
|
||
|
|
||
|
try:
|
||
|
from os import closerange
|
||
|
except ImportError:
|
||
|
def closerange(fd_low, fd_high):
|
||
|
# Iterate through and close all file descriptors.
|
||
|
for fd in range(fd_low, fd_high):
|
||
|
try:
|
||
|
os.close(fd)
|
||
|
except OSError: # ERROR, fd wasn't open to begin with (ignored)
|
||
|
pass
|
||
|
|
||
|
|
||
|
def write_chunk(sock, data):
|
||
|
if isinstance(data, text_type):
|
||
|
data = data.encode('utf-8')
|
||
|
chunk_size = "%X\r\n" % len(data)
|
||
|
chunk = b"".join([chunk_size.encode('utf-8'), data, b"\r\n"])
|
||
|
sock.sendall(chunk)
|
||
|
|
||
|
|
||
|
def write(sock, data, chunked=False):
|
||
|
if chunked:
|
||
|
return write_chunk(sock, data)
|
||
|
sock.sendall(data)
|
||
|
|
||
|
|
||
|
def write_nonblock(sock, data, chunked=False):
|
||
|
timeout = sock.gettimeout()
|
||
|
if timeout != 0.0:
|
||
|
try:
|
||
|
sock.setblocking(0)
|
||
|
return write(sock, data, chunked)
|
||
|
finally:
|
||
|
sock.setblocking(1)
|
||
|
else:
|
||
|
return write(sock, data, chunked)
|
||
|
|
||
|
|
||
|
def write_error(sock, status_int, reason, mesg):
|
||
|
html = textwrap.dedent("""\
|
||
|
<html>
|
||
|
<head>
|
||
|
<title>%(reason)s</title>
|
||
|
</head>
|
||
|
<body>
|
||
|
<h1><p>%(reason)s</p></h1>
|
||
|
%(mesg)s
|
||
|
</body>
|
||
|
</html>
|
||
|
""") % {"reason": reason, "mesg": _compat.html_escape(mesg)}
|
||
|
|
||
|
http = textwrap.dedent("""\
|
||
|
HTTP/1.1 %s %s\r
|
||
|
Connection: close\r
|
||
|
Content-Type: text/html\r
|
||
|
Content-Length: %d\r
|
||
|
\r
|
||
|
%s""") % (str(status_int), reason, len(html), html)
|
||
|
write_nonblock(sock, http.encode('latin1'))
|
||
|
|
||
|
|
||
|
def import_app(module):
|
||
|
parts = module.split(":", 1)
|
||
|
if len(parts) == 1:
|
||
|
module, obj = module, "application"
|
||
|
else:
|
||
|
module, obj = parts[0], parts[1]
|
||
|
|
||
|
try:
|
||
|
__import__(module)
|
||
|
except ImportError:
|
||
|
if module.endswith(".py") and os.path.exists(module):
|
||
|
msg = "Failed to find application, did you mean '%s:%s'?"
|
||
|
raise ImportError(msg % (module.rsplit(".", 1)[0], obj))
|
||
|
else:
|
||
|
raise
|
||
|
|
||
|
mod = sys.modules[module]
|
||
|
|
||
|
is_debug = logging.root.level == logging.DEBUG
|
||
|
try:
|
||
|
app = eval(obj, vars(mod))
|
||
|
except NameError:
|
||
|
if is_debug:
|
||
|
traceback.print_exception(*sys.exc_info())
|
||
|
raise AppImportError("Failed to find application object %r in %r" % (obj, module))
|
||
|
|
||
|
if app is None:
|
||
|
raise AppImportError("Failed to find application object: %r" % obj)
|
||
|
|
||
|
if not callable(app):
|
||
|
raise AppImportError("Application object must be callable.")
|
||
|
return app
|
||
|
|
||
|
|
||
|
def getcwd():
|
||
|
# get current path, try to use PWD env first
|
||
|
try:
|
||
|
a = os.stat(os.environ['PWD'])
|
||
|
b = os.stat(os.getcwd())
|
||
|
if a.st_ino == b.st_ino and a.st_dev == b.st_dev:
|
||
|
cwd = os.environ['PWD']
|
||
|
else:
|
||
|
cwd = os.getcwd()
|
||
|
except:
|
||
|
cwd = os.getcwd()
|
||
|
return cwd
|
||
|
|
||
|
|
||
|
def http_date(timestamp=None):
|
||
|
"""Return the current date and time formatted for a message header."""
|
||
|
if timestamp is None:
|
||
|
timestamp = time.time()
|
||
|
s = email.utils.formatdate(timestamp, localtime=False, usegmt=True)
|
||
|
return s
|
||
|
|
||
|
|
||
|
def is_hoppish(header):
|
||
|
return header.lower().strip() in hop_headers
|
||
|
|
||
|
|
||
|
def daemonize(enable_stdio_inheritance=False):
|
||
|
"""\
|
||
|
Standard daemonization of a process.
|
||
|
http://www.svbug.com/documentation/comp.unix.programmer-FAQ/faq_2.html#SEC16
|
||
|
"""
|
||
|
if 'GUNICORN_FD' not in os.environ:
|
||
|
if os.fork():
|
||
|
os._exit(0)
|
||
|
os.setsid()
|
||
|
|
||
|
if os.fork():
|
||
|
os._exit(0)
|
||
|
|
||
|
os.umask(0o22)
|
||
|
|
||
|
# In both the following any file descriptors above stdin
|
||
|
# stdout and stderr are left untouched. The inheritance
|
||
|
# option simply allows one to have output go to a file
|
||
|
# specified by way of shell redirection when not wanting
|
||
|
# to use --error-log option.
|
||
|
|
||
|
if not enable_stdio_inheritance:
|
||
|
# Remap all of stdin, stdout and stderr on to
|
||
|
# /dev/null. The expectation is that users have
|
||
|
# specified the --error-log option.
|
||
|
|
||
|
closerange(0, 3)
|
||
|
|
||
|
fd_null = os.open(REDIRECT_TO, os.O_RDWR)
|
||
|
|
||
|
if fd_null != 0:
|
||
|
os.dup2(fd_null, 0)
|
||
|
|
||
|
os.dup2(fd_null, 1)
|
||
|
os.dup2(fd_null, 2)
|
||
|
|
||
|
else:
|
||
|
fd_null = os.open(REDIRECT_TO, os.O_RDWR)
|
||
|
|
||
|
# Always redirect stdin to /dev/null as we would
|
||
|
# never expect to need to read interactive input.
|
||
|
|
||
|
if fd_null != 0:
|
||
|
os.close(0)
|
||
|
os.dup2(fd_null, 0)
|
||
|
|
||
|
# If stdout and stderr are still connected to
|
||
|
# their original file descriptors we check to see
|
||
|
# if they are associated with terminal devices.
|
||
|
# When they are we map them to /dev/null so that
|
||
|
# are still detached from any controlling terminal
|
||
|
# properly. If not we preserve them as they are.
|
||
|
#
|
||
|
# If stdin and stdout were not hooked up to the
|
||
|
# original file descriptors, then all bets are
|
||
|
# off and all we can really do is leave them as
|
||
|
# they were.
|
||
|
#
|
||
|
# This will allow 'gunicorn ... > output.log 2>&1'
|
||
|
# to work with stdout/stderr going to the file
|
||
|
# as expected.
|
||
|
#
|
||
|
# Note that if using --error-log option, the log
|
||
|
# file specified through shell redirection will
|
||
|
# only be used up until the log file specified
|
||
|
# by the option takes over. As it replaces stdout
|
||
|
# and stderr at the file descriptor level, then
|
||
|
# anything using stdout or stderr, including having
|
||
|
# cached a reference to them, will still work.
|
||
|
|
||
|
def redirect(stream, fd_expect):
|
||
|
try:
|
||
|
fd = stream.fileno()
|
||
|
if fd == fd_expect and stream.isatty():
|
||
|
os.close(fd)
|
||
|
os.dup2(fd_null, fd)
|
||
|
except AttributeError:
|
||
|
pass
|
||
|
|
||
|
redirect(sys.stdout, 1)
|
||
|
redirect(sys.stderr, 2)
|
||
|
|
||
|
|
||
|
def seed():
|
||
|
try:
|
||
|
random.seed(os.urandom(64))
|
||
|
except NotImplementedError:
|
||
|
random.seed('%s.%s' % (time.time(), os.getpid()))
|
||
|
|
||
|
|
||
|
def check_is_writeable(path):
|
||
|
try:
|
||
|
f = open(path, 'a')
|
||
|
except IOError as e:
|
||
|
raise RuntimeError("Error: '%s' isn't writable [%r]" % (path, e))
|
||
|
f.close()
|
||
|
|
||
|
|
||
|
def to_bytestring(value, encoding="utf8"):
|
||
|
"""Converts a string argument to a byte string"""
|
||
|
if isinstance(value, bytes):
|
||
|
return value
|
||
|
if not isinstance(value, text_type):
|
||
|
raise TypeError('%r is not a string' % value)
|
||
|
|
||
|
return value.encode(encoding)
|
||
|
|
||
|
def has_fileno(obj):
|
||
|
if not hasattr(obj, "fileno"):
|
||
|
return False
|
||
|
|
||
|
# check BytesIO case and maybe others
|
||
|
try:
|
||
|
obj.fileno()
|
||
|
except (AttributeError, IOError, io.UnsupportedOperation):
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|
||
|
|
||
|
def warn(msg):
|
||
|
print("!!!", file=sys.stderr)
|
||
|
|
||
|
lines = msg.splitlines()
|
||
|
for i, line in enumerate(lines):
|
||
|
if i == 0:
|
||
|
line = "WARNING: %s" % line
|
||
|
print("!!! %s" % line, file=sys.stderr)
|
||
|
|
||
|
print("!!!\n", file=sys.stderr)
|
||
|
sys.stderr.flush()
|
||
|
|
||
|
|
||
|
def make_fail_app(msg):
|
||
|
msg = to_bytestring(msg)
|
||
|
|
||
|
def app(environ, start_response):
|
||
|
start_response("500 Internal Server Error", [
|
||
|
("Content-Type", "text/plain"),
|
||
|
("Content-Length", str(len(msg)))
|
||
|
])
|
||
|
return [msg]
|
||
|
|
||
|
return app
|
||
|
|
||
|
|
||
|
def split_request_uri(uri):
|
||
|
if uri.startswith("//"):
|
||
|
# When the path starts with //, urlsplit considers it as a
|
||
|
# relative uri while the RFC says we should consider it as abs_path
|
||
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
|
||
|
# We use temporary dot prefix to workaround this behaviour
|
||
|
parts = _compat.urlsplit("." + uri)
|
||
|
return parts._replace(path=parts.path[1:])
|
||
|
|
||
|
return _compat.urlsplit(uri)
|