Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/gunicorn/util.py: 30%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#
2# This file is part of gunicorn released under the MIT license.
3# See the NOTICE for more information.
4import ast
5import email.utils
6import errno
7import fcntl
8import html
9import importlib
10import inspect
11import io
12import logging
13import os
14import pwd
15import random
16import re
17import socket
18import sys
19import textwrap
20import time
21import traceback
22import warnings
24try:
25 import importlib.metadata as importlib_metadata
26except (ModuleNotFoundError, ImportError):
27 import importlib_metadata
29from gunicorn.errors import AppImportError
30from gunicorn.workers import SUPPORTED_WORKERS
31import urllib.parse
33REDIRECT_TO = getattr(os, 'devnull', '/dev/null')
35# Characters that make a path a glob pattern rather than a literal path.
36# glob.has_magic() does the same thing but is absent from glob.__all__.
37GLOB_MAGIC_RE = re.compile(r'[*?[]')
39# Server and Date aren't technically hop-by-hop
40# headers, but they are in the purview of the
41# origin server which the WSGI spec says we should
42# act like. So we drop them and add our own.
43#
44# In the future, concatenation server header values
45# might be better, but nothing else does it and
46# dropping them is easier.
47hop_headers = set("""
48 connection keep-alive proxy-authenticate proxy-authorization
49 te trailers transfer-encoding upgrade
50 server date
51 """.split())
53# setproctitle causes segfaults on macOS due to fork() safety issues
54# https://github.com/benoitc/gunicorn/issues/3021
55if sys.platform == "darwin":
56 def _setproctitle(title):
57 pass
58else:
59 try:
60 from setproctitle import setproctitle, getproctitle
62 # Force early initialization before any os.environ modifications
63 # (e.g. removing LISTEN_FDS in systemd socket activation)
64 # https://github.com/benoitc/gunicorn/issues/3430
65 getproctitle()
67 def _setproctitle(title):
68 setproctitle("gunicorn: %s" % title)
69 except ImportError:
70 def _setproctitle(title):
71 pass
74def is_glob_pattern(value):
75 """Return True if the string should be treated as a glob pattern."""
76 return GLOB_MAGIC_RE.search(value) is not None
79def load_entry_point(distribution, group, name):
80 dist_obj = importlib_metadata.distribution(distribution)
81 eps = [ep for ep in dist_obj.entry_points
82 if ep.group == group and ep.name == name]
83 if not eps:
84 raise ImportError("Entry point %r not found" % ((group, name),))
85 return eps[0].load()
88def load_class(uri, default="gunicorn.workers.sync.SyncWorker",
89 section="gunicorn.workers"):
90 if inspect.isclass(uri):
91 return uri
92 if uri.startswith("egg:"):
93 # uses entry points
94 entry_str = uri.split("egg:")[1]
95 try:
96 dist, name = entry_str.rsplit("#", 1)
97 except ValueError:
98 dist = entry_str
99 name = default
101 try:
102 return load_entry_point(dist, section, name)
103 except Exception:
104 exc = traceback.format_exc()
105 msg = "class uri %r invalid or not found: \n\n[%s]"
106 raise RuntimeError(msg % (uri, exc))
107 else:
108 components = uri.split('.')
109 if len(components) == 1:
110 while True:
111 if uri.startswith("#"):
112 uri = uri[1:]
114 if uri in SUPPORTED_WORKERS:
115 components = SUPPORTED_WORKERS[uri].split(".")
116 break
118 try:
119 return load_entry_point(
120 "gunicorn", section, uri
121 )
122 except Exception:
123 exc = traceback.format_exc()
124 msg = "class uri %r invalid or not found: \n\n[%s]"
125 raise RuntimeError(msg % (uri, exc))
127 klass = components.pop(-1)
129 try:
130 mod = importlib.import_module('.'.join(components))
131 except Exception:
132 exc = traceback.format_exc()
133 msg = "class uri %r invalid or not found: \n\n[%s]"
134 raise RuntimeError(msg % (uri, exc))
135 return getattr(mod, klass)
138positionals = (
139 inspect.Parameter.POSITIONAL_ONLY,
140 inspect.Parameter.POSITIONAL_OR_KEYWORD,
141)
144def get_arity(f):
145 sig = inspect.signature(f)
146 arity = 0
148 for param in sig.parameters.values():
149 if param.kind in positionals:
150 arity += 1
152 return arity
155def get_username(uid):
156 """ get the username for a user id"""
157 return pwd.getpwuid(uid).pw_name
160def set_owner_process(uid, gid, initgroups=False):
161 """ set user and group of workers processes """
163 if gid:
164 if uid:
165 try:
166 username = get_username(uid)
167 except KeyError:
168 initgroups = False
170 if initgroups:
171 os.initgroups(username, gid)
172 elif gid != os.getgid():
173 os.setgid(gid)
175 if uid and uid != os.getuid():
176 os.setuid(uid)
179def chown(path, uid, gid):
180 os.chown(path, uid, gid)
183if sys.platform.startswith("win"):
184 def _waitfor(func, pathname, waitall=False):
185 # Perform the operation
186 func(pathname)
187 # Now setup the wait loop
188 if waitall:
189 dirname = pathname
190 else:
191 dirname, name = os.path.split(pathname)
192 dirname = dirname or '.'
193 # Check for `pathname` to be removed from the filesystem.
194 # The exponential backoff of the timeout amounts to a total
195 # of ~1 second after which the deletion is probably an error
196 # anyway.
197 # Testing on a i7@4.3GHz shows that usually only 1 iteration is
198 # required when contention occurs.
199 timeout = 0.001
200 while timeout < 1.0:
201 # Note we are only testing for the existence of the file(s) in
202 # the contents of the directory regardless of any security or
203 # access rights. If we have made it this far, we have sufficient
204 # permissions to do that much using Python's equivalent of the
205 # Windows API FindFirstFile.
206 # Other Windows APIs can fail or give incorrect results when
207 # dealing with files that are pending deletion.
208 L = os.listdir(dirname)
209 if not L if waitall else name in L:
210 return
211 # Increase the timeout and try again
212 time.sleep(timeout)
213 timeout *= 2
214 warnings.warn('tests may fail, delete still pending for ' + pathname,
215 RuntimeWarning, stacklevel=4)
217 def _unlink(filename):
218 _waitfor(os.unlink, filename)
219else:
220 _unlink = os.unlink
223def unlink(filename):
224 try:
225 _unlink(filename)
226 except OSError as error:
227 # The filename need not exist.
228 if error.errno not in (errno.ENOENT, errno.ENOTDIR):
229 raise
232def is_ipv6(addr):
233 try:
234 socket.inet_pton(socket.AF_INET6, addr)
235 except OSError: # not a valid address
236 return False
237 except ValueError: # ipv6 not supported on this platform
238 return False
239 return True
242def parse_address(netloc, default_port='8000'):
243 if re.match(r'unix:(//)?', netloc):
244 return re.split(r'unix:(//)?', netloc)[-1]
246 if netloc.startswith("fd://"):
247 fd = netloc[5:]
248 try:
249 return int(fd)
250 except ValueError:
251 raise RuntimeError("%r is not a valid file descriptor." % fd) from None
253 if netloc.startswith("tcp://"):
254 netloc = netloc.split("tcp://")[1]
255 host, port = netloc, default_port
257 if '[' in netloc and ']' in netloc:
258 host = netloc.split(']')[0][1:]
259 port = (netloc.split(']:') + [default_port])[1]
260 elif ':' in netloc:
261 host, port = (netloc.split(':') + [default_port])[:2]
262 elif netloc == "":
263 host, port = "0.0.0.0", default_port
265 try:
266 port = int(port)
267 except ValueError:
268 raise RuntimeError("%r is not a valid port number." % port)
270 return host.lower(), port
273def close_on_exec(fd):
274 flags = fcntl.fcntl(fd, fcntl.F_GETFD)
275 flags |= fcntl.FD_CLOEXEC
276 fcntl.fcntl(fd, fcntl.F_SETFD, flags)
279def set_non_blocking(fd):
280 flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK
281 fcntl.fcntl(fd, fcntl.F_SETFL, flags)
284def close(sock):
285 try:
286 sock.close()
287 except OSError:
288 pass
291def close_graceful(sock, timeout=2.0, max_drain=65536):
292 """Close a TCP socket following RFC 9112 section 9.6.
294 Half-closes the write side to send FIN, then lingers on the read side
295 to drain the kernel recv buffer until the peer closes or a cap is hit,
296 then fully closes. This avoids the kernel sending RST (truncating the
297 last response segment) when unread request data remains in the buffer.
298 """
299 try:
300 try:
301 sock.shutdown(socket.SHUT_WR)
302 except OSError:
303 return
304 deadline = time.monotonic() + timeout
305 drained = 0
306 while drained < max_drain:
307 remaining = deadline - time.monotonic()
308 if remaining <= 0:
309 break
310 try:
311 sock.settimeout(remaining)
312 data = sock.recv(4096)
313 except (socket.timeout, OSError):
314 break
315 if not data:
316 break
317 drained += len(data)
318 finally:
319 try:
320 sock.close()
321 except OSError:
322 pass
325try:
326 from os import closerange
327except ImportError:
328 def closerange(fd_low, fd_high):
329 # Iterate through and close all file descriptors.
330 for fd in range(fd_low, fd_high):
331 try:
332 os.close(fd)
333 except OSError: # ERROR, fd wasn't open to begin with (ignored)
334 pass
337def write_chunk(sock, data):
338 if isinstance(data, str):
339 data = data.encode('utf-8')
340 chunk_size = "%X\r\n" % len(data)
341 chunk = b"".join([chunk_size.encode('utf-8'), data, b"\r\n"])
342 sock.sendall(chunk)
345def write(sock, data, chunked=False):
346 if chunked:
347 return write_chunk(sock, data)
348 sock.sendall(data)
351def write_nonblock(sock, data, chunked=False):
352 timeout = sock.gettimeout()
353 if timeout != 0.0:
354 try:
355 sock.setblocking(0)
356 return write(sock, data, chunked)
357 finally:
358 sock.setblocking(1)
359 else:
360 return write(sock, data, chunked)
363def write_error(sock, status_int, reason, mesg):
364 html_error = textwrap.dedent("""\
365 <html>
366 <head>
367 <title>%(reason)s</title>
368 </head>
369 <body>
370 <h1><p>%(reason)s</p></h1>
371 %(mesg)s
372 </body>
373 </html>
374 """) % {"reason": reason, "mesg": html.escape(mesg)}
376 http = textwrap.dedent("""\
377 HTTP/1.1 %s %s\r
378 Connection: close\r
379 Content-Type: text/html\r
380 Content-Length: %d\r
381 \r
382 %s""") % (str(status_int), reason, len(html_error), html_error)
383 write_nonblock(sock, http.encode('latin1'))
386def _called_with_wrong_args(f):
387 """Check whether calling a function raised a ``TypeError`` because
388 the call failed or because something in the function raised the
389 error.
391 :param f: The function that was called.
392 :return: ``True`` if the call failed.
393 """
394 tb = sys.exc_info()[2]
396 try:
397 while tb is not None:
398 if tb.tb_frame.f_code is f.__code__:
399 # In the function, it was called successfully.
400 return False
402 tb = tb.tb_next
404 # Didn't reach the function.
405 return True
406 finally:
407 # Delete tb to break a circular reference in Python 2.
408 # https://docs.python.org/2/library/sys.html#sys.exc_info
409 del tb
412def import_app(module):
413 parts = module.split(":", 1)
414 if len(parts) == 1:
415 obj = "application"
416 else:
417 module, obj = parts[0], parts[1]
419 try:
420 mod = importlib.import_module(module)
421 except ImportError:
422 if module.endswith(".py") and os.path.exists(module):
423 msg = "Failed to find application, did you mean '%s:%s'?"
424 raise ImportError(msg % (module.rsplit(".", 1)[0], obj))
425 raise
427 # Parse obj as a single expression to determine if it's a valid
428 # attribute name or function call.
429 try:
430 expression = ast.parse(obj, mode="eval").body
431 except SyntaxError:
432 raise AppImportError(
433 "Failed to parse %r as an attribute name or function call." % obj
434 )
436 if isinstance(expression, ast.Name):
437 name = expression.id
438 args = kwargs = None
439 elif isinstance(expression, ast.Call):
440 # Ensure the function name is an attribute name only.
441 if not isinstance(expression.func, ast.Name):
442 raise AppImportError("Function reference must be a simple name: %r" % obj)
444 name = expression.func.id
446 # Parse the positional and keyword arguments as literals.
447 try:
448 args = [ast.literal_eval(arg) for arg in expression.args]
449 kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expression.keywords}
450 except ValueError:
451 # literal_eval gives cryptic error messages, show a generic
452 # message with the full expression instead.
453 raise AppImportError(
454 "Failed to parse arguments as literal values: %r" % obj
455 )
456 else:
457 raise AppImportError(
458 "Failed to parse %r as an attribute name or function call." % obj
459 )
461 is_debug = logging.root.level == logging.DEBUG
462 try:
463 app = getattr(mod, name)
464 except AttributeError:
465 if is_debug:
466 traceback.print_exception(*sys.exc_info())
467 raise AppImportError("Failed to find attribute %r in %r." % (name, module))
469 # If the expression was a function call, call the retrieved object
470 # to get the real application.
471 if args is not None:
472 try:
473 app = app(*args, **kwargs)
474 except TypeError as e:
475 # If the TypeError was due to bad arguments to the factory
476 # function, show Python's nice error message without a
477 # traceback.
478 if _called_with_wrong_args(app):
479 raise AppImportError(
480 "".join(traceback.format_exception_only(TypeError, e)).strip()
481 )
483 # Otherwise it was raised from within the function, show the
484 # full traceback.
485 raise
487 if app is None:
488 raise AppImportError("Failed to find application object: %r" % obj)
490 if not callable(app):
491 raise AppImportError("Application object must be callable.")
492 return app
495def getcwd():
496 # get current path, try to use PWD env first
497 try:
498 a = os.stat(os.environ['PWD'])
499 b = os.stat(os.getcwd())
500 if a.st_ino == b.st_ino and a.st_dev == b.st_dev:
501 cwd = os.environ['PWD']
502 else:
503 cwd = os.getcwd()
504 except Exception:
505 cwd = os.getcwd()
506 return cwd
509def http_date(timestamp=None):
510 """Return the current date and time formatted for a message header."""
511 if timestamp is None:
512 timestamp = time.time()
513 s = email.utils.formatdate(timestamp, localtime=False, usegmt=True)
514 return s
517def is_hoppish(header):
518 return header.lower().strip() in hop_headers
521def daemonize(enable_stdio_inheritance=False):
522 """\
523 Standard daemonization of a process.
524 http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7
525 """
526 if 'GUNICORN_FD' not in os.environ:
527 if os.fork():
528 os._exit(0)
529 os.setsid()
531 if os.fork():
532 os._exit(0)
534 os.umask(0o22)
536 # In both the following any file descriptors above stdin
537 # stdout and stderr are left untouched. The inheritance
538 # option simply allows one to have output go to a file
539 # specified by way of shell redirection when not wanting
540 # to use --error-log option.
542 if not enable_stdio_inheritance:
543 # Remap all of stdin, stdout and stderr on to
544 # /dev/null. The expectation is that users have
545 # specified the --error-log option.
547 closerange(0, 3)
549 fd_null = os.open(REDIRECT_TO, os.O_RDWR)
550 # PEP 446, make fd for /dev/null inheritable
551 os.set_inheritable(fd_null, True)
553 # expect fd_null to be always 0 here, but in-case not ...
554 if fd_null != 0:
555 os.dup2(fd_null, 0)
557 os.dup2(fd_null, 1)
558 os.dup2(fd_null, 2)
560 else:
561 fd_null = os.open(REDIRECT_TO, os.O_RDWR)
563 # Always redirect stdin to /dev/null as we would
564 # never expect to need to read interactive input.
566 if fd_null != 0:
567 os.close(0)
568 os.dup2(fd_null, 0)
570 # If stdout and stderr are still connected to
571 # their original file descriptors we check to see
572 # if they are associated with terminal devices.
573 # When they are we map them to /dev/null so that
574 # are still detached from any controlling terminal
575 # properly. If not we preserve them as they are.
576 #
577 # If stdin and stdout were not hooked up to the
578 # original file descriptors, then all bets are
579 # off and all we can really do is leave them as
580 # they were.
581 #
582 # This will allow 'gunicorn ... > output.log 2>&1'
583 # to work with stdout/stderr going to the file
584 # as expected.
585 #
586 # Note that if using --error-log option, the log
587 # file specified through shell redirection will
588 # only be used up until the log file specified
589 # by the option takes over. As it replaces stdout
590 # and stderr at the file descriptor level, then
591 # anything using stdout or stderr, including having
592 # cached a reference to them, will still work.
594 def redirect(stream, fd_expect):
595 try:
596 fd = stream.fileno()
597 if fd == fd_expect and stream.isatty():
598 os.close(fd)
599 os.dup2(fd_null, fd)
600 except AttributeError:
601 pass
603 redirect(sys.stdout, 1)
604 redirect(sys.stderr, 2)
607def seed():
608 try:
609 random.seed(os.urandom(64))
610 except NotImplementedError:
611 random.seed('%s.%s' % (time.time(), os.getpid()))
614def check_is_writable(path):
615 try:
616 with open(path, 'a') as f:
617 f.close()
618 except OSError as e:
619 raise RuntimeError("Error: '%s' isn't writable [%r]" % (path, e))
622def to_bytestring(value, encoding="utf8"):
623 """Converts a string argument to a byte string"""
624 if isinstance(value, bytes):
625 return value
626 if not isinstance(value, str):
627 raise TypeError('%r is not a string' % value)
629 return value.encode(encoding)
632def has_fileno(obj):
633 if not hasattr(obj, "fileno"):
634 return False
636 # check BytesIO case and maybe others
637 try:
638 obj.fileno()
639 except (AttributeError, OSError, io.UnsupportedOperation):
640 return False
642 return True
645def warn(msg):
646 print("!!!", file=sys.stderr)
648 lines = msg.splitlines()
649 for i, line in enumerate(lines):
650 if i == 0:
651 line = "WARNING: %s" % line
652 print("!!! %s" % line, file=sys.stderr)
654 print("!!!\n", file=sys.stderr)
655 sys.stderr.flush()
658def make_fail_app(msg):
659 msg = to_bytestring(msg)
661 def app(environ, start_response):
662 start_response("500 Internal Server Error", [
663 ("Content-Type", "text/plain"),
664 ("Content-Length", str(len(msg)))
665 ])
666 return [msg]
668 return app
671def split_request_uri(uri):
672 if uri.startswith("//"):
673 # When the path starts with //, urlsplit considers it as a
674 # relative uri while the RFC says we should consider it as abs_path
675 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
676 # We use temporary dot prefix to workaround this behaviour
677 parts = urllib.parse.urlsplit("." + uri)
678 return parts._replace(path=parts.path[1:])
680 return urllib.parse.urlsplit(uri)
683# From six.reraise
684def reraise(tp, value, tb=None):
685 try:
686 if value is None:
687 value = tp()
688 if value.__traceback__ is not tb:
689 raise value.with_traceback(tb)
690 raise value
691 finally:
692 value = None
693 tb = None
696def bytes_to_str(b):
697 if isinstance(b, str):
698 return b
699 return str(b, 'latin1')
702def unquote_to_wsgi_str(string):
703 return urllib.parse.unquote_to_bytes(string).decode('latin-1')