plain 0.66.0__py3-none-any.whl → 0.101.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. plain/CHANGELOG.md +684 -0
  2. plain/README.md +1 -1
  3. plain/assets/compile.py +25 -12
  4. plain/assets/finders.py +24 -17
  5. plain/assets/fingerprints.py +10 -7
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +47 -33
  8. plain/chores/README.md +25 -23
  9. plain/chores/__init__.py +2 -1
  10. plain/chores/core.py +27 -0
  11. plain/chores/registry.py +23 -53
  12. plain/cli/README.md +185 -16
  13. plain/cli/__init__.py +2 -1
  14. plain/cli/agent.py +236 -0
  15. plain/cli/build.py +7 -8
  16. plain/cli/changelog.py +11 -5
  17. plain/cli/chores.py +32 -34
  18. plain/cli/core.py +112 -28
  19. plain/cli/docs.py +52 -11
  20. plain/cli/formatting.py +40 -17
  21. plain/cli/install.py +10 -54
  22. plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
  23. plain/cli/output.py +6 -2
  24. plain/cli/preflight.py +175 -102
  25. plain/cli/print.py +4 -4
  26. plain/cli/registry.py +95 -26
  27. plain/cli/request.py +206 -0
  28. plain/cli/runtime.py +45 -0
  29. plain/cli/scaffold.py +2 -7
  30. plain/cli/server.py +153 -0
  31. plain/cli/settings.py +53 -49
  32. plain/cli/shell.py +15 -12
  33. plain/cli/startup.py +9 -8
  34. plain/cli/upgrade.py +17 -104
  35. plain/cli/urls.py +12 -7
  36. plain/cli/utils.py +3 -3
  37. plain/csrf/README.md +65 -40
  38. plain/csrf/middleware.py +53 -43
  39. plain/debug.py +5 -2
  40. plain/exceptions.py +22 -114
  41. plain/forms/README.md +453 -24
  42. plain/forms/__init__.py +55 -4
  43. plain/forms/boundfield.py +15 -8
  44. plain/forms/exceptions.py +1 -1
  45. plain/forms/fields.py +346 -143
  46. plain/forms/forms.py +75 -45
  47. plain/http/README.md +356 -9
  48. plain/http/__init__.py +41 -26
  49. plain/http/cookie.py +15 -7
  50. plain/http/exceptions.py +65 -0
  51. plain/http/middleware.py +32 -0
  52. plain/http/multipartparser.py +99 -88
  53. plain/http/request.py +362 -250
  54. plain/http/response.py +99 -197
  55. plain/internal/__init__.py +8 -1
  56. plain/internal/files/base.py +35 -19
  57. plain/internal/files/locks.py +19 -11
  58. plain/internal/files/move.py +8 -3
  59. plain/internal/files/temp.py +25 -6
  60. plain/internal/files/uploadedfile.py +47 -28
  61. plain/internal/files/uploadhandler.py +64 -58
  62. plain/internal/files/utils.py +24 -10
  63. plain/internal/handlers/base.py +34 -23
  64. plain/internal/handlers/exception.py +68 -65
  65. plain/internal/handlers/wsgi.py +65 -54
  66. plain/internal/middleware/headers.py +37 -11
  67. plain/internal/middleware/hosts.py +11 -13
  68. plain/internal/middleware/https.py +17 -7
  69. plain/internal/middleware/slash.py +14 -9
  70. plain/internal/reloader.py +77 -0
  71. plain/json.py +2 -1
  72. plain/logs/README.md +161 -62
  73. plain/logs/__init__.py +1 -1
  74. plain/logs/{loggers.py → app.py} +71 -67
  75. plain/logs/configure.py +63 -14
  76. plain/logs/debug.py +17 -6
  77. plain/logs/filters.py +15 -0
  78. plain/logs/formatters.py +7 -4
  79. plain/packages/README.md +105 -23
  80. plain/packages/config.py +15 -7
  81. plain/packages/registry.py +40 -15
  82. plain/paginator.py +31 -21
  83. plain/preflight/README.md +208 -23
  84. plain/preflight/__init__.py +5 -24
  85. plain/preflight/checks.py +12 -0
  86. plain/preflight/files.py +19 -13
  87. plain/preflight/registry.py +80 -58
  88. plain/preflight/results.py +37 -0
  89. plain/preflight/security.py +65 -71
  90. plain/preflight/settings.py +54 -0
  91. plain/preflight/urls.py +10 -48
  92. plain/runtime/README.md +115 -47
  93. plain/runtime/__init__.py +10 -6
  94. plain/runtime/global_settings.py +43 -33
  95. plain/runtime/secret.py +20 -0
  96. plain/runtime/user_settings.py +110 -38
  97. plain/runtime/utils.py +1 -1
  98. plain/server/LICENSE +35 -0
  99. plain/server/README.md +155 -0
  100. plain/server/__init__.py +9 -0
  101. plain/server/app.py +52 -0
  102. plain/server/arbiter.py +555 -0
  103. plain/server/config.py +118 -0
  104. plain/server/errors.py +31 -0
  105. plain/server/glogging.py +292 -0
  106. plain/server/http/__init__.py +12 -0
  107. plain/server/http/body.py +283 -0
  108. plain/server/http/errors.py +155 -0
  109. plain/server/http/message.py +400 -0
  110. plain/server/http/parser.py +70 -0
  111. plain/server/http/unreader.py +88 -0
  112. plain/server/http/wsgi.py +421 -0
  113. plain/server/pidfile.py +92 -0
  114. plain/server/sock.py +240 -0
  115. plain/server/util.py +317 -0
  116. plain/server/workers/__init__.py +6 -0
  117. plain/server/workers/base.py +304 -0
  118. plain/server/workers/sync.py +212 -0
  119. plain/server/workers/thread.py +399 -0
  120. plain/server/workers/workertmp.py +50 -0
  121. plain/signals/README.md +170 -1
  122. plain/signals/__init__.py +0 -1
  123. plain/signals/dispatch/dispatcher.py +49 -27
  124. plain/signing.py +131 -35
  125. plain/skills/README.md +36 -0
  126. plain/skills/plain-docs/SKILL.md +25 -0
  127. plain/skills/plain-install/SKILL.md +26 -0
  128. plain/skills/plain-request/SKILL.md +39 -0
  129. plain/skills/plain-shell/SKILL.md +24 -0
  130. plain/skills/plain-upgrade/SKILL.md +35 -0
  131. plain/templates/README.md +211 -20
  132. plain/templates/jinja/__init__.py +14 -27
  133. plain/templates/jinja/environments.py +5 -4
  134. plain/templates/jinja/extensions.py +12 -5
  135. plain/templates/jinja/filters.py +7 -2
  136. plain/templates/jinja/globals.py +2 -2
  137. plain/test/README.md +184 -22
  138. plain/test/client.py +340 -222
  139. plain/test/encoding.py +9 -6
  140. plain/test/exceptions.py +7 -2
  141. plain/urls/README.md +157 -73
  142. plain/urls/converters.py +18 -15
  143. plain/urls/exceptions.py +2 -2
  144. plain/urls/patterns.py +56 -40
  145. plain/urls/resolvers.py +38 -28
  146. plain/urls/utils.py +5 -1
  147. plain/utils/README.md +250 -3
  148. plain/utils/cache.py +17 -11
  149. plain/utils/crypto.py +21 -5
  150. plain/utils/datastructures.py +89 -56
  151. plain/utils/dateparse.py +9 -6
  152. plain/utils/deconstruct.py +15 -7
  153. plain/utils/decorators.py +5 -1
  154. plain/utils/dotenv.py +373 -0
  155. plain/utils/duration.py +8 -4
  156. plain/utils/encoding.py +14 -7
  157. plain/utils/functional.py +66 -49
  158. plain/utils/hashable.py +5 -1
  159. plain/utils/html.py +36 -22
  160. plain/utils/http.py +16 -9
  161. plain/utils/inspect.py +14 -6
  162. plain/utils/ipv6.py +7 -3
  163. plain/utils/itercompat.py +6 -1
  164. plain/utils/module_loading.py +7 -3
  165. plain/utils/regex_helper.py +37 -23
  166. plain/utils/safestring.py +14 -6
  167. plain/utils/text.py +41 -23
  168. plain/utils/timezone.py +33 -22
  169. plain/utils/tree.py +35 -19
  170. plain/validators.py +94 -52
  171. plain/views/README.md +156 -79
  172. plain/views/__init__.py +0 -1
  173. plain/views/base.py +25 -18
  174. plain/views/errors.py +13 -5
  175. plain/views/exceptions.py +4 -1
  176. plain/views/forms.py +6 -6
  177. plain/views/objects.py +52 -49
  178. plain/views/redirect.py +18 -15
  179. plain/views/templates.py +5 -3
  180. plain/wsgi.py +3 -1
  181. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
  182. plain-0.101.2.dist-info/RECORD +201 -0
  183. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
  184. plain-0.101.2.dist-info/entry_points.txt +2 -0
  185. plain/AGENTS.md +0 -18
  186. plain/cli/agent/__init__.py +0 -20
  187. plain/cli/agent/docs.py +0 -80
  188. plain/cli/agent/md.py +0 -87
  189. plain/cli/agent/prompt.py +0 -45
  190. plain/cli/agent/request.py +0 -181
  191. plain/csrf/views.py +0 -31
  192. plain/logs/utils.py +0 -46
  193. plain/preflight/messages.py +0 -81
  194. plain/templates/AGENTS.md +0 -3
  195. plain-0.66.0.dist-info/RECORD +0 -168
  196. plain-0.66.0.dist-info/entry_points.txt +0 -4
  197. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,304 @@
1
+ from __future__ import annotations
2
+
3
+ #
4
+ #
5
+ # This file is part of gunicorn released under the MIT license.
6
+ # See the LICENSE for more information.
7
+ #
8
+ # Vendored and modified for Plain.
9
+ import io
10
+ import os
11
+ import signal
12
+ import sys
13
+ import time
14
+ import traceback
15
+ from abc import ABC, abstractmethod
16
+ from datetime import datetime
17
+ from random import randint
18
+ from ssl import SSLError
19
+ from typing import TYPE_CHECKING, Any
20
+
21
+ from plain.internal.reloader import Reloader
22
+
23
+ from .. import sock, util
24
+ from ..http.errors import (
25
+ ConfigurationProblem,
26
+ InvalidHeader,
27
+ InvalidHeaderName,
28
+ InvalidHTTPVersion,
29
+ InvalidRequestLine,
30
+ InvalidRequestMethod,
31
+ InvalidSchemeHeaders,
32
+ LimitRequestHeaders,
33
+ LimitRequestLine,
34
+ ObsoleteFolding,
35
+ UnsupportedTransferCoding,
36
+ )
37
+ from ..http.wsgi import Response, default_environ
38
+ from .workertmp import WorkerTmp
39
+
40
+ if TYPE_CHECKING:
41
+ import socket
42
+
43
+ from ..app import ServerApplication
44
+ from ..config import Config
45
+ from ..glogging import Logger
46
+ from ..http.message import Request
47
+
48
+ # Maximum jitter to add to max_requests to stagger worker restarts
49
+ MAX_REQUESTS_JITTER = 50
50
+
51
+
52
+ class Worker(ABC):
53
+ SIGNALS = [
54
+ getattr(signal, f"SIG{x}")
55
+ for x in ("ABRT HUP QUIT INT TERM USR1 USR2 WINCH CHLD".split())
56
+ ]
57
+
58
+ PIPE = []
59
+
60
+ def __init__(
61
+ self,
62
+ age: int,
63
+ ppid: int,
64
+ sockets: list[sock.BaseSocket],
65
+ app: ServerApplication,
66
+ timeout: int | float,
67
+ cfg: Config,
68
+ log: Logger,
69
+ ):
70
+ """\
71
+ This is called pre-fork so it shouldn't do anything to the
72
+ current process. If there's a need to make process wide
73
+ changes you'll want to do that in ``self.init_process()``.
74
+ """
75
+ self.age = age
76
+ self.pid: str | int = "[booting]"
77
+ self.ppid = ppid
78
+ self.sockets = sockets
79
+ self.app = app
80
+ self.timeout = timeout
81
+ self.cfg = cfg
82
+ self.booted = False
83
+ self.aborted = False
84
+ self.reloader: Any = None
85
+
86
+ self.nr = 0
87
+
88
+ if cfg.max_requests > 0:
89
+ jitter = randint(0, MAX_REQUESTS_JITTER)
90
+ self.max_requests = cfg.max_requests + jitter
91
+ else:
92
+ self.max_requests = sys.maxsize
93
+
94
+ self.alive = True
95
+ self.log = log
96
+ self.tmp = WorkerTmp(cfg)
97
+
98
+ def __str__(self) -> str:
99
+ return f"<Worker {self.pid}>"
100
+
101
+ def notify(self) -> None:
102
+ """\
103
+ Your worker subclass must arrange to have this method called
104
+ once every ``self.timeout`` seconds. If you fail in accomplishing
105
+ this task, the master process will murder your workers.
106
+ """
107
+ self.tmp.notify()
108
+
109
+ @abstractmethod
110
+ def run(self) -> None:
111
+ """\
112
+ This is the mainloop of a worker process. You should override
113
+ this method in a subclass to provide the intended behaviour
114
+ for your particular evil schemes.
115
+ """
116
+ ...
117
+
118
+ def init_process(self) -> None:
119
+ """\
120
+ If you override this method in a subclass, the last statement
121
+ in the function should be to call this method with
122
+ super().init_process() so that the ``run()`` loop is initiated.
123
+ """
124
+
125
+ # Reseed the random number generator
126
+ util.seed()
127
+
128
+ # For waking ourselves up
129
+ self.PIPE = os.pipe()
130
+ for p in self.PIPE:
131
+ util.set_non_blocking(p)
132
+ util.close_on_exec(p)
133
+
134
+ # Prevent fd inheritance
135
+ for s in self.sockets:
136
+ util.close_on_exec(s.fileno())
137
+ util.close_on_exec(self.tmp.fileno())
138
+
139
+ self.wait_fds: list[sock.BaseSocket | int] = self.sockets + [self.PIPE[0]]
140
+
141
+ self.log.close_on_exec()
142
+
143
+ self.init_signals()
144
+
145
+ # start the reloader
146
+ if self.cfg.reload:
147
+
148
+ def changed(fname: str) -> None:
149
+ self.log.debug("Server worker reloading: %s modified", fname)
150
+ self.alive = False
151
+ os.write(self.PIPE[1], b"1")
152
+ time.sleep(0.1)
153
+ sys.exit(0)
154
+
155
+ self.reloader = Reloader(callback=changed, watch_html=True)
156
+
157
+ self.load_wsgi()
158
+ if self.reloader:
159
+ self.reloader.start()
160
+
161
+ # Enter main run loop
162
+ self.booted = True
163
+ self.run()
164
+
165
+ def load_wsgi(self) -> None:
166
+ try:
167
+ self.wsgi = self.app.wsgi()
168
+ except SyntaxError:
169
+ if not self.cfg.reload:
170
+ raise
171
+
172
+ self.log.exception("Error loading WSGI application")
173
+
174
+ # fix from PR #1228
175
+ # storing the traceback into exc_tb will create a circular reference.
176
+ # per https://docs.python.org/2/library/sys.html#sys.exc_info warning,
177
+ # delete the traceback after use.
178
+ try:
179
+ _, exc_val, exc_tb = sys.exc_info()
180
+
181
+ tb_string = io.StringIO()
182
+ traceback.print_tb(exc_tb, file=tb_string)
183
+ self.wsgi = util.make_fail_app(tb_string.getvalue())
184
+ finally:
185
+ del exc_tb
186
+
187
+ def init_signals(self) -> None:
188
+ # reset signaling
189
+ for s in self.SIGNALS:
190
+ signal.signal(s, signal.SIG_DFL)
191
+ # init new signaling
192
+ signal.signal(signal.SIGQUIT, self.handle_quit)
193
+ signal.signal(signal.SIGTERM, self.handle_exit)
194
+ signal.signal(signal.SIGINT, self.handle_quit)
195
+ signal.signal(signal.SIGWINCH, self.handle_winch)
196
+ signal.signal(signal.SIGUSR1, self.handle_usr1)
197
+ signal.signal(signal.SIGABRT, self.handle_abort)
198
+
199
+ # Don't let SIGTERM and SIGUSR1 disturb active requests
200
+ # by interrupting system calls
201
+ signal.siginterrupt(signal.SIGTERM, False)
202
+ signal.siginterrupt(signal.SIGUSR1, False)
203
+
204
+ if hasattr(signal, "set_wakeup_fd"):
205
+ signal.set_wakeup_fd(self.PIPE[1])
206
+
207
+ def handle_usr1(self, sig: int, frame: Any) -> None:
208
+ self.log.reopen_files()
209
+
210
+ def handle_exit(self, sig: int, frame: Any) -> None:
211
+ self.alive = False
212
+
213
+ def handle_quit(self, sig: int, frame: Any) -> None:
214
+ self.alive = False
215
+ time.sleep(0.1)
216
+ sys.exit(0)
217
+
218
+ def handle_abort(self, sig: int, frame: Any) -> None:
219
+ self.alive = False
220
+ sys.exit(1)
221
+
222
+ def handle_error(
223
+ self, req: Request | None, client: socket.socket, addr: Any, exc: BaseException
224
+ ) -> None:
225
+ request_start = datetime.now()
226
+ addr = addr or ("", -1) # unix socket case
227
+ if isinstance(
228
+ exc,
229
+ InvalidRequestLine
230
+ | InvalidRequestMethod
231
+ | InvalidHTTPVersion
232
+ | InvalidHeader
233
+ | InvalidHeaderName
234
+ | LimitRequestLine
235
+ | LimitRequestHeaders
236
+ | InvalidSchemeHeaders
237
+ | UnsupportedTransferCoding
238
+ | ConfigurationProblem
239
+ | ObsoleteFolding
240
+ | SSLError,
241
+ ):
242
+ status_int = 400
243
+ reason = "Bad Request"
244
+
245
+ if isinstance(exc, InvalidRequestLine):
246
+ mesg = f"Invalid Request Line '{str(exc)}'"
247
+ elif isinstance(exc, InvalidRequestMethod):
248
+ mesg = f"Invalid Method '{str(exc)}'"
249
+ elif isinstance(exc, InvalidHTTPVersion):
250
+ mesg = f"Invalid HTTP Version '{str(exc)}'"
251
+ elif isinstance(exc, UnsupportedTransferCoding):
252
+ mesg = f"{str(exc)}"
253
+ status_int = 501
254
+ elif isinstance(exc, ConfigurationProblem):
255
+ mesg = f"{str(exc)}"
256
+ status_int = 500
257
+ elif isinstance(exc, ObsoleteFolding):
258
+ mesg = f"{str(exc)}"
259
+ elif isinstance(exc, InvalidHeaderName | InvalidHeader):
260
+ mesg = f"{str(exc)}"
261
+ if not req and hasattr(exc, "req"):
262
+ req = exc.req # type: ignore[assignment] # for access log
263
+ elif isinstance(exc, LimitRequestLine):
264
+ mesg = f"{str(exc)}"
265
+ elif isinstance(exc, LimitRequestHeaders):
266
+ reason = "Request Header Fields Too Large"
267
+ mesg = f"Error parsing headers: '{str(exc)}'"
268
+ status_int = 431
269
+ elif isinstance(exc, InvalidSchemeHeaders):
270
+ mesg = f"{str(exc)}"
271
+ elif isinstance(exc, SSLError):
272
+ reason = "Forbidden"
273
+ mesg = f"'{str(exc)}'"
274
+ status_int = 403
275
+
276
+ msg = "Invalid request from ip={ip}: {error}"
277
+ self.log.warning(msg.format(ip=addr[0], error=str(exc)))
278
+ else:
279
+ if hasattr(req, "uri"):
280
+ self.log.exception("Error handling request %s", req.uri)
281
+ else:
282
+ self.log.exception("Error handling request (no URI read)")
283
+ status_int = 500
284
+ reason = "Internal Server Error"
285
+ mesg = ""
286
+
287
+ if req is not None:
288
+ request_time = datetime.now() - request_start
289
+ environ = default_environ(req, client, self.cfg)
290
+ environ["REMOTE_ADDR"] = addr[0]
291
+ environ["REMOTE_PORT"] = str(addr[1])
292
+ resp = Response(req, client, self.cfg)
293
+ resp.status = f"{status_int} {reason}"
294
+ resp.response_length = len(mesg)
295
+ self.log.access(resp, req, environ, request_time)
296
+
297
+ try:
298
+ util.write_error(client, status_int, reason, mesg)
299
+ except Exception:
300
+ self.log.debug("Failed to send error message.")
301
+
302
+ def handle_winch(self, sig: int, fname: Any) -> None:
303
+ # Ignore SIGWINCH in worker. Fixes a crash on OpenBSD.
304
+ self.log.debug("worker: SIGWINCH ignored.")
@@ -0,0 +1,212 @@
1
+ from __future__ import annotations
2
+
3
+ #
4
+ #
5
+ # This file is part of gunicorn released under the MIT license.
6
+ # See the LICENSE for more information.
7
+ #
8
+ # Vendored and modified for Plain.
9
+ import errno
10
+ import os
11
+ import select
12
+ import socket
13
+ import ssl
14
+ import sys
15
+ from datetime import datetime
16
+ from typing import Any
17
+
18
+ from .. import http, sock, util
19
+ from ..http import wsgi
20
+ from . import base
21
+
22
+
23
+ class StopWaiting(Exception):
24
+ """exception raised to stop waiting for a connection"""
25
+
26
+
27
+ class SyncWorker(base.Worker):
28
+ def accept(self, listener: sock.BaseSocket) -> None:
29
+ client, addr = listener.accept()
30
+ client.setblocking(True)
31
+ util.close_on_exec(client.fileno())
32
+ self.handle(listener, client, addr)
33
+
34
+ def wait(self, timeout: float) -> list[Any] | None:
35
+ try:
36
+ self.notify()
37
+ ret = select.select(self.wait_fds, [], [], timeout)
38
+ if ret[0]:
39
+ if self.PIPE[0] in ret[0]:
40
+ os.read(self.PIPE[0], 1)
41
+ return ret[0]
42
+ return None
43
+
44
+ except OSError as e:
45
+ if e.args[0] == errno.EINTR:
46
+ return self.sockets
47
+ if e.args[0] == errno.EBADF:
48
+ if self.nr < 0:
49
+ return self.sockets
50
+ else:
51
+ raise StopWaiting
52
+ raise
53
+
54
+ def is_parent_alive(self) -> bool:
55
+ # If our parent changed then we shut down.
56
+ if self.ppid != os.getppid():
57
+ self.log.info("Parent changed, shutting down: %s", self)
58
+ return False
59
+ return True
60
+
61
+ def run_for_one(self, timeout: float) -> None:
62
+ listener = self.sockets[0]
63
+ while self.alive:
64
+ self.notify()
65
+
66
+ # Accept a connection. If we get an error telling us
67
+ # that no connection is waiting we fall down to the
68
+ # select which is where we'll wait for a bit for new
69
+ # workers to come give us some love.
70
+ try:
71
+ self.accept(listener)
72
+ # Keep processing clients until no one is waiting. This
73
+ # prevents the need to select() for every client that we
74
+ # process.
75
+ continue
76
+
77
+ except OSError as e:
78
+ if e.errno not in (errno.EAGAIN, errno.ECONNABORTED, errno.EWOULDBLOCK):
79
+ raise
80
+
81
+ if not self.is_parent_alive():
82
+ return None
83
+
84
+ try:
85
+ self.wait(timeout)
86
+ except StopWaiting:
87
+ return None
88
+
89
+ def run_for_multiple(self, timeout: float) -> None:
90
+ while self.alive:
91
+ self.notify()
92
+
93
+ try:
94
+ ready = self.wait(timeout)
95
+ except StopWaiting:
96
+ return None
97
+
98
+ if ready is not None:
99
+ for listener in ready:
100
+ if listener == self.PIPE[0]:
101
+ continue
102
+
103
+ try:
104
+ self.accept(listener)
105
+ except OSError as e:
106
+ if e.errno not in (
107
+ errno.EAGAIN,
108
+ errno.ECONNABORTED,
109
+ errno.EWOULDBLOCK,
110
+ ):
111
+ raise
112
+
113
+ if not self.is_parent_alive():
114
+ return None
115
+
116
+ def run(self) -> None:
117
+ # if no timeout is given the worker will never wait and will
118
+ # use the CPU for nothing. This minimal timeout prevent it.
119
+ timeout = self.timeout or 0.5
120
+
121
+ # self.socket appears to lose its blocking status after
122
+ # we fork in the arbiter. Reset it here.
123
+ for s in self.sockets:
124
+ s.setblocking(False)
125
+
126
+ if len(self.sockets) > 1:
127
+ self.run_for_multiple(timeout)
128
+ else:
129
+ self.run_for_one(timeout)
130
+
131
+ def handle(
132
+ self, listener: sock.BaseSocket, client: socket.socket, addr: Any
133
+ ) -> None:
134
+ req = None
135
+ try:
136
+ if self.cfg.is_ssl:
137
+ client = sock.ssl_wrap_socket(client, self.cfg)
138
+ parser = http.RequestParser(self.cfg, client, addr)
139
+ req = next(parser)
140
+ self.handle_request(listener, req, client, addr)
141
+ except http.errors.NoMoreData as e:
142
+ self.log.debug("Ignored premature client disconnection. %s", e)
143
+ except StopIteration as e:
144
+ self.log.debug("Closing connection. %s", e)
145
+ except ssl.SSLError as e:
146
+ if e.args[0] == ssl.SSL_ERROR_EOF:
147
+ self.log.debug("ssl connection closed")
148
+ client.close()
149
+ else:
150
+ self.log.debug("Error processing SSL request.")
151
+ self.handle_error(req, client, addr, e)
152
+ except OSError as e:
153
+ if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):
154
+ self.log.exception("Socket error processing request.")
155
+ else:
156
+ if e.errno == errno.ECONNRESET:
157
+ self.log.debug("Ignoring connection reset")
158
+ elif e.errno == errno.ENOTCONN:
159
+ self.log.debug("Ignoring socket not connected")
160
+ else:
161
+ self.log.debug("Ignoring EPIPE")
162
+ except BaseException as e:
163
+ self.handle_error(req, client, addr, e)
164
+ finally:
165
+ util.close(client)
166
+
167
+ def handle_request(
168
+ self, listener: sock.BaseSocket, req: Any, client: socket.socket, addr: Any
169
+ ) -> None:
170
+ environ = {}
171
+ resp = None
172
+ try:
173
+ request_start = datetime.now()
174
+ resp, environ = wsgi.create(
175
+ req, client, addr, listener.getsockname(), self.cfg
176
+ )
177
+ # Force the connection closed until someone shows
178
+ # a buffering proxy that supports Keep-Alive to
179
+ # the backend.
180
+ resp.force_close()
181
+ self.nr += 1
182
+ if self.nr >= self.max_requests:
183
+ self.log.info("Autorestarting worker after current request.")
184
+ self.alive = False
185
+ respiter = self.wsgi(environ, resp.start_response)
186
+ try:
187
+ if isinstance(respiter, environ["wsgi.file_wrapper"]):
188
+ resp.write_file(respiter)
189
+ else:
190
+ for item in respiter:
191
+ resp.write(item)
192
+ resp.close()
193
+ finally:
194
+ request_time = datetime.now() - request_start
195
+ self.log.access(resp, req, environ, request_time)
196
+ if hasattr(respiter, "close"):
197
+ respiter.close()
198
+ except OSError:
199
+ # pass to next try-except level
200
+ util.reraise(*sys.exc_info())
201
+ except Exception:
202
+ if resp and resp.headers_sent:
203
+ # If the requests have already been sent, we should close the
204
+ # connection to indicate the error.
205
+ self.log.exception("Error handling request")
206
+ try:
207
+ client.shutdown(socket.SHUT_RDWR)
208
+ client.close()
209
+ except OSError:
210
+ pass
211
+ raise StopIteration()
212
+ raise