mirror of
https://github.com/frappe/gunicorn.git
synced 2026-01-14 11:09:11 +08:00
The Arbiter is smart about getting the CWD; first it checks the CWD environment (which doesn't resolve symlinks), then it falls back to the python os.getcwd() (which does resolve symlinks). However, the Arbiter is the only place that does this, which will then do the right thing when we reexec. However, when reloading the Arbiter, it won't pick up changes if the symlink has changed. By changing the *app.py entry points to also use the same method for determining the CWD, we'll insert a symlink path into the first location in sys.path. Then our reloaded app will correctly pull in any new changes.
440 lines
12 KiB
Python
440 lines
12 KiB
Python
# -*- coding: utf-8 -
|
|
#
|
|
# This file is part of gunicorn released under the MIT license.
|
|
# See the NOTICE for more information.
|
|
|
|
|
|
import fcntl
|
|
import os
|
|
import pkg_resources
|
|
import random
|
|
import resource
|
|
import socket
|
|
import sys
|
|
import textwrap
|
|
import time
|
|
import traceback
|
|
import inspect
|
|
import errno
|
|
import warnings
|
|
|
|
from gunicorn.six import text_type, string_types
|
|
|
|
MAXFD = 1024
|
|
REDIRECT_TO = getattr(os, 'devnull', '/dev/null')
|
|
|
|
timeout_default = object()
|
|
|
|
CHUNK_SIZE = (16 * 1024)
|
|
|
|
MAX_BODY = 1024 * 132
|
|
|
|
weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
|
monthname = [None,
|
|
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
|
|
|
|
# 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 x in range(level, 1, -1):
|
|
try:
|
|
dot = package.rindex('.', 0, dot)
|
|
except ValueError:
|
|
raise ValueError("attempted relative import beyond top-level "
|
|
"package")
|
|
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="sync", 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()
|
|
raise RuntimeError("class uri %r invalid or not found: \n\n[%s]" % (uri,
|
|
exc))
|
|
else:
|
|
components = uri.split('.')
|
|
if len(components) == 1:
|
|
try:
|
|
if uri.startswith("#"):
|
|
uri = uri[1:]
|
|
|
|
return pkg_resources.load_entry_point("gunicorn",
|
|
section, uri)
|
|
except:
|
|
exc = traceback.format_exc()
|
|
raise RuntimeError("class uri %r invalid or not found: \n\n[%s]" % (uri,
|
|
exc))
|
|
|
|
klass = components.pop(-1)
|
|
try:
|
|
mod = __import__('.'.join(components))
|
|
except:
|
|
exc = traceback.format_exc()
|
|
raise RuntimeError("class uri %r invalid or not found: \n\n[%s]" % (uri,
|
|
exc))
|
|
|
|
for comp in components[1:]:
|
|
mod = getattr(mod, comp)
|
|
return getattr(mod, klass)
|
|
|
|
|
|
def set_owner_process(uid, gid):
|
|
""" set user and group of workers processes """
|
|
if gid:
|
|
# versions of python < 2.6.2 don't manage unsigned int for
|
|
# groups like on osx or fedora
|
|
gid = abs(gid) & 0x7FFFFFFF
|
|
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 existance 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
|
|
return True
|
|
|
|
|
|
def parse_address(netloc, default_port=8000):
|
|
if netloc.startswith("unix://"):
|
|
return netloc.split("unix://")[1]
|
|
|
|
if netloc.startswith("unix:"):
|
|
return netloc.split("unix:")[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 get_maxfd():
|
|
maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
|
|
if (maxfd == resource.RLIM_INFINITY):
|
|
maxfd = MAXFD
|
|
return maxfd
|
|
|
|
|
|
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 writelines(sock, lines, chunked=False):
|
|
for line in list(lines):
|
|
write(sock, line, chunked)
|
|
|
|
|
|
def write_error(sock, status_int, reason, mesg):
|
|
html = textwrap.dedent("""\
|
|
<html>
|
|
<head>
|
|
<title>%(reason)s</title>
|
|
</head>
|
|
<body>
|
|
<h1>%(reason)s</h1>
|
|
%(mesg)s
|
|
</body>
|
|
</html>
|
|
""") % {"reason": reason, "mesg": 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 normalize_name(name):
|
|
return "-".join([w.lower().capitalize() for w in name.split("-")])
|
|
|
|
|
|
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):
|
|
raise ImportError("Failed to find application, did "
|
|
"you mean '%s:%s'?" % (module.rsplit(".", 1)[0], obj))
|
|
else:
|
|
raise
|
|
|
|
mod = sys.modules[module]
|
|
app = eval(obj, mod.__dict__)
|
|
if app is None:
|
|
raise ImportError("Failed to find application object: %r" % obj)
|
|
if not callable(app):
|
|
raise TypeError("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()
|
|
year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp)
|
|
s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
|
|
weekdayname[wd],
|
|
day, monthname[month], year,
|
|
hh, mm, ss)
|
|
return s
|
|
|
|
|
|
def is_hoppish(header):
|
|
return header.lower().strip() in hop_headers
|
|
|
|
|
|
def daemonize():
|
|
"""\
|
|
Standard daemonization of a process.
|
|
http://www.svbug.com/documentation/comp.unix.programmer-FAQ/faq_2.html#SEC16
|
|
"""
|
|
if not 'GUNICORN_FD' in os.environ:
|
|
if os.fork():
|
|
os._exit(0)
|
|
os.setsid()
|
|
|
|
if os.fork():
|
|
os._exit(0)
|
|
|
|
os.umask(0)
|
|
maxfd = get_maxfd()
|
|
closerange(0, maxfd)
|
|
|
|
os.open(REDIRECT_TO, os.O_RDWR)
|
|
os.dup2(0, 1)
|
|
os.dup2(0, 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):
|
|
"""Converts a string argument to a byte string"""
|
|
if isinstance(value, bytes):
|
|
return value
|
|
assert isinstance(value, text_type)
|
|
return value.encode("utf-8")
|