plain 0.75.0__py3-none-any.whl → 0.77.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.

Potentially problematic release.


This version of plain might be problematic. Click here for more details.

@@ -0,0 +1,302 @@
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 datetime import datetime
16
+ from random import randint
17
+ from ssl import SSLError
18
+ from typing import TYPE_CHECKING, Any
19
+
20
+ from plain.internal.reloader import Reloader
21
+
22
+ from .. import util
23
+ from ..http.errors import (
24
+ ConfigurationProblem,
25
+ InvalidHeader,
26
+ InvalidHeaderName,
27
+ InvalidHTTPVersion,
28
+ InvalidRequestLine,
29
+ InvalidRequestMethod,
30
+ InvalidSchemeHeaders,
31
+ LimitRequestHeaders,
32
+ LimitRequestLine,
33
+ ObsoleteFolding,
34
+ UnsupportedTransferCoding,
35
+ )
36
+ from ..http.wsgi import Response, default_environ
37
+ from .workertmp import WorkerTmp
38
+
39
+ if TYPE_CHECKING:
40
+ import socket
41
+
42
+ from ..app import ServerApplication
43
+ from ..config import Config
44
+ from ..glogging import Logger
45
+ from ..http.message import Request
46
+
47
+ # Maximum jitter to add to max_requests to stagger worker restarts
48
+ MAX_REQUESTS_JITTER = 50
49
+
50
+
51
+ class Worker:
52
+ SIGNALS = [
53
+ getattr(signal, f"SIG{x}")
54
+ for x in ("ABRT HUP QUIT INT TERM USR1 USR2 WINCH CHLD".split())
55
+ ]
56
+
57
+ PIPE = []
58
+
59
+ def __init__(
60
+ self,
61
+ age: int,
62
+ ppid: int,
63
+ sockets: list[socket.socket],
64
+ app: ServerApplication,
65
+ timeout: int,
66
+ cfg: Config,
67
+ log: Logger,
68
+ ):
69
+ """\
70
+ This is called pre-fork so it shouldn't do anything to the
71
+ current process. If there's a need to make process wide
72
+ changes you'll want to do that in ``self.init_process()``.
73
+ """
74
+ self.age = age
75
+ self.pid: str | int = "[booting]"
76
+ self.ppid = ppid
77
+ self.sockets = sockets
78
+ self.app = app
79
+ self.timeout = timeout
80
+ self.cfg = cfg
81
+ self.booted = False
82
+ self.aborted = False
83
+ self.reloader: Any = None
84
+
85
+ self.nr = 0
86
+
87
+ if cfg.max_requests > 0:
88
+ jitter = randint(0, MAX_REQUESTS_JITTER)
89
+ self.max_requests = cfg.max_requests + jitter
90
+ else:
91
+ self.max_requests = sys.maxsize
92
+
93
+ self.alive = True
94
+ self.log = log
95
+ self.tmp = WorkerTmp(cfg)
96
+
97
+ def __str__(self) -> str:
98
+ return f"<Worker {self.pid}>"
99
+
100
+ def notify(self) -> None:
101
+ """\
102
+ Your worker subclass must arrange to have this method called
103
+ once every ``self.timeout`` seconds. If you fail in accomplishing
104
+ this task, the master process will murder your workers.
105
+ """
106
+ self.tmp.notify()
107
+
108
+ def run(self) -> None:
109
+ """\
110
+ This is the mainloop of a worker process. You should override
111
+ this method in a subclass to provide the intended behaviour
112
+ for your particular evil schemes.
113
+ """
114
+ raise NotImplementedError()
115
+
116
+ def init_process(self) -> None:
117
+ """\
118
+ If you override this method in a subclass, the last statement
119
+ in the function should be to call this method with
120
+ super().init_process() so that the ``run()`` loop is initiated.
121
+ """
122
+
123
+ # Reseed the random number generator
124
+ util.seed()
125
+
126
+ # For waking ourselves up
127
+ self.PIPE = os.pipe()
128
+ for p in self.PIPE:
129
+ util.set_non_blocking(p)
130
+ util.close_on_exec(p)
131
+
132
+ # Prevent fd inheritance
133
+ for s in self.sockets:
134
+ util.close_on_exec(s)
135
+ util.close_on_exec(self.tmp.fileno())
136
+
137
+ self.wait_fds = self.sockets + [self.PIPE[0]]
138
+
139
+ self.log.close_on_exec()
140
+
141
+ self.init_signals()
142
+
143
+ # start the reloader
144
+ if self.cfg.reload:
145
+
146
+ def changed(fname: str) -> None:
147
+ self.log.debug("Server worker reloading: %s modified", fname)
148
+ self.alive = False
149
+ os.write(self.PIPE[1], b"1")
150
+ time.sleep(0.1)
151
+ sys.exit(0)
152
+
153
+ self.reloader = Reloader(callback=changed, watch_html=True)
154
+
155
+ self.load_wsgi()
156
+ if self.reloader:
157
+ self.reloader.start()
158
+
159
+ # Enter main run loop
160
+ self.booted = True
161
+ self.run()
162
+
163
+ def load_wsgi(self) -> None:
164
+ try:
165
+ self.wsgi = self.app.wsgi()
166
+ except SyntaxError as e:
167
+ if not self.cfg.reload:
168
+ raise
169
+
170
+ self.log.exception(e)
171
+
172
+ # fix from PR #1228
173
+ # storing the traceback into exc_tb will create a circular reference.
174
+ # per https://docs.python.org/2/library/sys.html#sys.exc_info warning,
175
+ # delete the traceback after use.
176
+ try:
177
+ _, exc_val, exc_tb = sys.exc_info()
178
+
179
+ tb_string = io.StringIO()
180
+ traceback.print_tb(exc_tb, file=tb_string)
181
+ self.wsgi = util.make_fail_app(tb_string.getvalue())
182
+ finally:
183
+ del exc_tb
184
+
185
+ def init_signals(self) -> None:
186
+ # reset signaling
187
+ for s in self.SIGNALS:
188
+ signal.signal(s, signal.SIG_DFL)
189
+ # init new signaling
190
+ signal.signal(signal.SIGQUIT, self.handle_quit)
191
+ signal.signal(signal.SIGTERM, self.handle_exit)
192
+ signal.signal(signal.SIGINT, self.handle_quit)
193
+ signal.signal(signal.SIGWINCH, self.handle_winch)
194
+ signal.signal(signal.SIGUSR1, self.handle_usr1)
195
+ signal.signal(signal.SIGABRT, self.handle_abort)
196
+
197
+ # Don't let SIGTERM and SIGUSR1 disturb active requests
198
+ # by interrupting system calls
199
+ signal.siginterrupt(signal.SIGTERM, False)
200
+ signal.siginterrupt(signal.SIGUSR1, False)
201
+
202
+ if hasattr(signal, "set_wakeup_fd"):
203
+ signal.set_wakeup_fd(self.PIPE[1])
204
+
205
+ def handle_usr1(self, sig: int, frame: Any) -> None:
206
+ self.log.reopen_files()
207
+
208
+ def handle_exit(self, sig: int, frame: Any) -> None:
209
+ self.alive = False
210
+
211
+ def handle_quit(self, sig: int, frame: Any) -> None:
212
+ self.alive = False
213
+ time.sleep(0.1)
214
+ sys.exit(0)
215
+
216
+ def handle_abort(self, sig: int, frame: Any) -> None:
217
+ self.alive = False
218
+ sys.exit(1)
219
+
220
+ def handle_error(
221
+ self, req: Request | None, client: socket.socket, addr: Any, exc: Exception
222
+ ) -> None:
223
+ request_start = datetime.now()
224
+ addr = addr or ("", -1) # unix socket case
225
+ if isinstance(
226
+ exc,
227
+ InvalidRequestLine
228
+ | InvalidRequestMethod
229
+ | InvalidHTTPVersion
230
+ | InvalidHeader
231
+ | InvalidHeaderName
232
+ | LimitRequestLine
233
+ | LimitRequestHeaders
234
+ | InvalidSchemeHeaders
235
+ | UnsupportedTransferCoding
236
+ | ConfigurationProblem
237
+ | ObsoleteFolding
238
+ | SSLError,
239
+ ):
240
+ status_int = 400
241
+ reason = "Bad Request"
242
+
243
+ if isinstance(exc, InvalidRequestLine):
244
+ mesg = f"Invalid Request Line '{str(exc)}'"
245
+ elif isinstance(exc, InvalidRequestMethod):
246
+ mesg = f"Invalid Method '{str(exc)}'"
247
+ elif isinstance(exc, InvalidHTTPVersion):
248
+ mesg = f"Invalid HTTP Version '{str(exc)}'"
249
+ elif isinstance(exc, UnsupportedTransferCoding):
250
+ mesg = f"{str(exc)}"
251
+ status_int = 501
252
+ elif isinstance(exc, ConfigurationProblem):
253
+ mesg = f"{str(exc)}"
254
+ status_int = 500
255
+ elif isinstance(exc, ObsoleteFolding):
256
+ mesg = f"{str(exc)}"
257
+ elif isinstance(exc, InvalidHeaderName | InvalidHeader):
258
+ mesg = f"{str(exc)}"
259
+ if not req and hasattr(exc, "req"):
260
+ req = exc.req # type: ignore[attr-defined] # for access log
261
+ elif isinstance(exc, LimitRequestLine):
262
+ mesg = f"{str(exc)}"
263
+ elif isinstance(exc, LimitRequestHeaders):
264
+ reason = "Request Header Fields Too Large"
265
+ mesg = f"Error parsing headers: '{str(exc)}'"
266
+ status_int = 431
267
+ elif isinstance(exc, InvalidSchemeHeaders):
268
+ mesg = f"{str(exc)}"
269
+ elif isinstance(exc, SSLError):
270
+ reason = "Forbidden"
271
+ mesg = f"'{str(exc)}'"
272
+ status_int = 403
273
+
274
+ msg = "Invalid request from ip={ip}: {error}"
275
+ self.log.warning(msg.format(ip=addr[0], error=str(exc)))
276
+ else:
277
+ if hasattr(req, "uri"):
278
+ self.log.exception("Error handling request %s", req.uri)
279
+ else:
280
+ self.log.exception("Error handling request (no URI read)")
281
+ status_int = 500
282
+ reason = "Internal Server Error"
283
+ mesg = ""
284
+
285
+ if req is not None:
286
+ request_time = datetime.now() - request_start
287
+ environ = default_environ(req, client, self.cfg)
288
+ environ["REMOTE_ADDR"] = addr[0]
289
+ environ["REMOTE_PORT"] = str(addr[1])
290
+ resp = Response(req, client, self.cfg)
291
+ resp.status = f"{status_int} {reason}"
292
+ resp.response_length = len(mesg)
293
+ self.log.access(resp, req, environ, request_time)
294
+
295
+ try:
296
+ util.write_error(client, status_int, reason, mesg)
297
+ except Exception:
298
+ self.log.debug("Failed to send error message.")
299
+
300
+ def handle_winch(self, sig: int, fname: Any) -> None:
301
+ # Ignore SIGWINCH in worker. Fixes a crash on OpenBSD.
302
+ self.log.debug("worker: SIGWINCH ignored.")
@@ -0,0 +1,210 @@
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: socket.socket) -> 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(self, listener: socket.socket, client: socket.socket, addr: Any) -> None:
132
+ req = None
133
+ try:
134
+ if self.cfg.is_ssl:
135
+ client = sock.ssl_wrap_socket(client, self.cfg)
136
+ parser = http.RequestParser(self.cfg, client, addr)
137
+ req = next(parser)
138
+ self.handle_request(listener, req, client, addr)
139
+ except http.errors.NoMoreData as e:
140
+ self.log.debug("Ignored premature client disconnection. %s", e)
141
+ except StopIteration as e:
142
+ self.log.debug("Closing connection. %s", e)
143
+ except ssl.SSLError as e:
144
+ if e.args[0] == ssl.SSL_ERROR_EOF:
145
+ self.log.debug("ssl connection closed")
146
+ client.close()
147
+ else:
148
+ self.log.debug("Error processing SSL request.")
149
+ self.handle_error(req, client, addr, e)
150
+ except OSError as e:
151
+ if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN):
152
+ self.log.exception("Socket error processing request.")
153
+ else:
154
+ if e.errno == errno.ECONNRESET:
155
+ self.log.debug("Ignoring connection reset")
156
+ elif e.errno == errno.ENOTCONN:
157
+ self.log.debug("Ignoring socket not connected")
158
+ else:
159
+ self.log.debug("Ignoring EPIPE")
160
+ except BaseException as e:
161
+ self.handle_error(req, client, addr, e)
162
+ finally:
163
+ util.close(client)
164
+
165
+ def handle_request(
166
+ self, listener: socket.socket, req: Any, client: socket.socket, addr: Any
167
+ ) -> None:
168
+ environ = {}
169
+ resp = None
170
+ try:
171
+ request_start = datetime.now()
172
+ resp, environ = wsgi.create(
173
+ req, client, addr, listener.getsockname(), self.cfg
174
+ )
175
+ # Force the connection closed until someone shows
176
+ # a buffering proxy that supports Keep-Alive to
177
+ # the backend.
178
+ resp.force_close()
179
+ self.nr += 1
180
+ if self.nr >= self.max_requests:
181
+ self.log.info("Autorestarting worker after current request.")
182
+ self.alive = False
183
+ respiter = self.wsgi(environ, resp.start_response)
184
+ try:
185
+ if isinstance(respiter, environ["wsgi.file_wrapper"]):
186
+ resp.write_file(respiter)
187
+ else:
188
+ for item in respiter:
189
+ resp.write(item)
190
+ resp.close()
191
+ finally:
192
+ request_time = datetime.now() - request_start
193
+ self.log.access(resp, req, environ, request_time)
194
+ if hasattr(respiter, "close"):
195
+ respiter.close()
196
+ except OSError:
197
+ # pass to next try-except level
198
+ util.reraise(*sys.exc_info())
199
+ except Exception:
200
+ if resp and resp.headers_sent:
201
+ # If the requests have already been sent, we should close the
202
+ # connection to indicate the error.
203
+ self.log.exception("Error handling request")
204
+ try:
205
+ client.shutdown(socket.SHUT_RDWR)
206
+ client.close()
207
+ except OSError:
208
+ pass
209
+ raise StopIteration()
210
+ raise