plain 0.68.0__py3-none-any.whl → 0.103.0__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.
- plain/CHANGELOG.md +684 -1
- plain/README.md +1 -1
- plain/agents/.claude/rules/plain.md +88 -0
- plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
- plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -36
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +234 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +110 -26
- plain/cli/docs.py +98 -21
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +27 -75
- plain/cli/print.py +4 -4
- plain/cli/registry.py +96 -10
- plain/cli/{agent/request.py → request.py} +67 -33
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -8
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +27 -16
- plain/paginator.py +31 -21
- plain/preflight/README.md +209 -24
- plain/preflight/__init__.py +1 -0
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +26 -11
- plain/preflight/results.py +15 -7
- plain/preflight/security.py +15 -13
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +4 -1
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +34 -25
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +38 -22
- plain/urls/resolvers.py +35 -25
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/METADATA +4 -2
- plain-0.103.0.dist-info/RECORD +198 -0
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
- plain-0.103.0.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/templates/AGENTS.md +0 -3
- plain-0.68.0.dist-info/RECORD +0 -169
- plain-0.68.0.dist-info/entry_points.txt +0 -5
- {plain-0.68.0.dist-info → plain-0.103.0.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
|