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