Pyjoyment 0.1.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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/Server/Daemon.py
ADDED
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.Daemon - Non-blocking I/O HTTP and WebSocket server
|
|
3
|
+
===============================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.Daemon import Daemon
|
|
7
|
+
|
|
8
|
+
daemon = Daemon(listen=['http://*:8080'])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@daemon.unsubscribe('request').on('request')
|
|
12
|
+
def request(daemon, tx):
|
|
13
|
+
# Request
|
|
14
|
+
method = tx.req.method
|
|
15
|
+
path = tx.req.url.path
|
|
16
|
+
|
|
17
|
+
# Response
|
|
18
|
+
tx.res.code = 200
|
|
19
|
+
tx.res.headers.content_type = 'text/plain'
|
|
20
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
21
|
+
|
|
22
|
+
# Resume transaction
|
|
23
|
+
tx.resume()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
daemon.run()
|
|
27
|
+
|
|
28
|
+
# Or import the module, as in Google Python Style Guide
|
|
29
|
+
|
|
30
|
+
import Pyjo.Server.Daemon
|
|
31
|
+
|
|
32
|
+
daemon = Pyjo.Server.Daemon.new(listen=['http://*:8080'])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@daemon.unsubscribe('request').on('request')
|
|
36
|
+
def request(daemon, tx):
|
|
37
|
+
# Request
|
|
38
|
+
method = tx.req.method
|
|
39
|
+
path = tx.req.url.path
|
|
40
|
+
|
|
41
|
+
# Response
|
|
42
|
+
tx.res.code = 200
|
|
43
|
+
tx.res.headers.content_type = 'text/plain'
|
|
44
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
45
|
+
|
|
46
|
+
# Resume transaction
|
|
47
|
+
tx.resume()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
daemon.run()
|
|
51
|
+
|
|
52
|
+
:mod:`Pyjo.Server.Daemon` is a full featured, highly portable non-blocking
|
|
53
|
+
I/O HTTP and WebSocket server, with IPv6, TLS, SNI, Comet (long polling),
|
|
54
|
+
keep-alive and multiple event loop support. It also speaks FastCGI to a web
|
|
55
|
+
server in front of it, on the listen locations ``fcgi://`` and
|
|
56
|
+
``fcgi+unix://``, with :mod:`Pyjo.Server.FastCGI`.
|
|
57
|
+
|
|
58
|
+
TLS support comes from :mod:`ssl` of the standard library, and can be
|
|
59
|
+
disabled with the ``PYJO_NO_TLS`` environment variable, as name resolution
|
|
60
|
+
and SOCKS5 support of :mod:`Pyjo.IOLoop` with ``PYJO_NO_NNR`` and
|
|
61
|
+
``PYJO_NO_SOCKS``.
|
|
62
|
+
|
|
63
|
+
A daemon is also a context manager, which stops listening and closes its
|
|
64
|
+
connections at its end.
|
|
65
|
+
|
|
66
|
+
The end of a ``with`` block stops the daemon and closes its sockets.
|
|
67
|
+
|
|
68
|
+
Signals
|
|
69
|
+
-------
|
|
70
|
+
|
|
71
|
+
The :mod:`Pyjo.Server.Daemon` process can be controlled at runtime with the
|
|
72
|
+
following signals.
|
|
73
|
+
|
|
74
|
+
INT, TERM
|
|
75
|
+
~~~~~~~~~
|
|
76
|
+
|
|
77
|
+
Shut down server gracefully: close the listen sockets, so that a new
|
|
78
|
+
connection is refused, and wait for the accepted ones to be closed, after
|
|
79
|
+
:attr:`~Pyjo_Server_Daemon.shutdown_delay`, while it serves as before. A
|
|
80
|
+
second ``INT``, as of Ctrl-C pressed twice, shuts it down immediately, also
|
|
81
|
+
during the delay, a second ``TERM`` does nothing.
|
|
82
|
+
|
|
83
|
+
QUIT
|
|
84
|
+
~~~~
|
|
85
|
+
|
|
86
|
+
Shut down server immediately. On Windows, which has no ``QUIT``, ``BREAK``
|
|
87
|
+
does it, as of Ctrl-Break.
|
|
88
|
+
|
|
89
|
+
HUP
|
|
90
|
+
~~~
|
|
91
|
+
|
|
92
|
+
Ignored, as there is nothing to reload. Windows has no ``HUP``.
|
|
93
|
+
|
|
94
|
+
Events
|
|
95
|
+
------
|
|
96
|
+
|
|
97
|
+
:mod:`Pyjo.Server.Daemon` inherits all events from :mod:`Pyjo.Server`.
|
|
98
|
+
|
|
99
|
+
Debugging
|
|
100
|
+
---------
|
|
101
|
+
|
|
102
|
+
You can set the ``PYJO_SERVER_DEBUG`` environment variable to get some
|
|
103
|
+
advanced diagnostics information printed to :data:`sys.stderr`. ::
|
|
104
|
+
|
|
105
|
+
PYJO_SERVER_DEBUG=1
|
|
106
|
+
|
|
107
|
+
See also
|
|
108
|
+
--------
|
|
109
|
+
|
|
110
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
111
|
+
|
|
112
|
+
Ported from `Mojo::Server::Daemon <https://github.com/mojolicious/mojo/blob/v9.52/lib/Mojo/Server/Daemon.pm>`_
|
|
113
|
+
of Mojolicious 9.52.
|
|
114
|
+
|
|
115
|
+
Classes
|
|
116
|
+
-------
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
import Pyjo.Exception
|
|
120
|
+
import Pyjo.IOLoop
|
|
121
|
+
import Pyjo.IOLoop.Stream
|
|
122
|
+
import Pyjo.IOLoop.TLS
|
|
123
|
+
import Pyjo.Reactor
|
|
124
|
+
import Pyjo.Server
|
|
125
|
+
import Pyjo.Server.FastCGI
|
|
126
|
+
import Pyjo.Transaction
|
|
127
|
+
import Pyjo.Transaction.HTTP
|
|
128
|
+
import Pyjo.Transaction.WebSocket
|
|
129
|
+
import Pyjo.URL
|
|
130
|
+
|
|
131
|
+
from Pyjo.String.Unicode import split
|
|
132
|
+
from Pyjo.Util import term_escape, warn
|
|
133
|
+
from Pyjo.WebSocket import server_handshake
|
|
134
|
+
|
|
135
|
+
import atexit
|
|
136
|
+
import contextlib
|
|
137
|
+
import os
|
|
138
|
+
import re
|
|
139
|
+
import signal
|
|
140
|
+
import socket
|
|
141
|
+
import ssl
|
|
142
|
+
import sys
|
|
143
|
+
import threading
|
|
144
|
+
import weakref
|
|
145
|
+
|
|
146
|
+
from collections.abc import Sequence
|
|
147
|
+
from types import TracebackType
|
|
148
|
+
from typing import Any, Self
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# A location to listen on, which Pyjo.URL parses
|
|
152
|
+
type Location = str | Pyjo.URL.object
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
DEBUG = bool(os.environ.get('PYJO_SERVER_DEBUG'))
|
|
156
|
+
|
|
157
|
+
# The protocol versions of IO::Socket::SSL's SSL_version
|
|
158
|
+
VERSIONS = {
|
|
159
|
+
'TLSv1': ssl.TLSVersion.TLSv1,
|
|
160
|
+
'TLSv1_1': ssl.TLSVersion.TLSv1_1,
|
|
161
|
+
'TLSv1_2': ssl.TLSVersion.TLSv1_2,
|
|
162
|
+
'TLSv1_3': ssl.TLSVersion.TLSv1_3,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class Pyjo_Server_Daemon(Pyjo.Server.object):
|
|
167
|
+
"""
|
|
168
|
+
:mod:`Pyjo.Server.Daemon` inherits all methods from :mod:`Pyjo.Server`
|
|
169
|
+
and implements the following new ones.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
def __init__(
|
|
173
|
+
self,
|
|
174
|
+
*,
|
|
175
|
+
acceptors: list[str] | None = None,
|
|
176
|
+
backlog: int | None = None,
|
|
177
|
+
inactivity_timeout: float | None = None,
|
|
178
|
+
ioloop: Pyjo.IOLoop.object | None = None,
|
|
179
|
+
keep_alive_timeout: float | None = None,
|
|
180
|
+
lingering_timeout: float = 5,
|
|
181
|
+
listen: Sequence[Location] | None = None,
|
|
182
|
+
max_clients: int | None = None,
|
|
183
|
+
max_requests: int = 100,
|
|
184
|
+
shutdown_delay: float = 0,
|
|
185
|
+
silent: bool | None = None,
|
|
186
|
+
**kwargs: Any,
|
|
187
|
+
) -> None:
|
|
188
|
+
"""::
|
|
189
|
+
|
|
190
|
+
daemon = Pyjo.Server.Daemon.new()
|
|
191
|
+
daemon = Pyjo.Server.Daemon.new(listen=['http://*:8080'], silent=True)
|
|
192
|
+
|
|
193
|
+
Construct a new :mod:`Pyjo.Server.Daemon` object.
|
|
194
|
+
"""
|
|
195
|
+
super().__init__(**kwargs)
|
|
196
|
+
|
|
197
|
+
self.acceptors: list[str] = acceptors if acceptors is not None else []
|
|
198
|
+
"""::
|
|
199
|
+
|
|
200
|
+
acceptors = daemon.acceptors
|
|
201
|
+
daemon.acceptors = ['6be0c140ef00a389c5d039536b56d139']
|
|
202
|
+
|
|
203
|
+
Active acceptor ids. ::
|
|
204
|
+
|
|
205
|
+
# Check port
|
|
206
|
+
port = daemon.ioloop.acceptor(daemon.acceptors[0]).port
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
self.backlog = backlog
|
|
210
|
+
"""::
|
|
211
|
+
|
|
212
|
+
backlog = daemon.backlog
|
|
213
|
+
daemon.backlog = 128
|
|
214
|
+
|
|
215
|
+
Listen backlog size, defaults to :data:`socket.SOMAXCONN`.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
self._inactivity_timeout: float | None = inactivity_timeout
|
|
219
|
+
|
|
220
|
+
self.ioloop: Pyjo.IOLoop.object = ioloop if ioloop is not None else Pyjo.IOLoop.object.singleton()
|
|
221
|
+
"""::
|
|
222
|
+
|
|
223
|
+
loop = daemon.ioloop
|
|
224
|
+
daemon.ioloop = Pyjo.IOLoop.new()
|
|
225
|
+
|
|
226
|
+
Event loop object to use for I/O operations, defaults to the global
|
|
227
|
+
:mod:`Pyjo.IOLoop` singleton.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
self._keep_alive_timeout: float | None = keep_alive_timeout
|
|
231
|
+
self._locations: list[Location] | None = list(listen) if listen is not None else None
|
|
232
|
+
|
|
233
|
+
# Not in Mojolicious: the rest of a request that has its response already, as one over a limit, is read before the
|
|
234
|
+
# connection is closed, as a close with unread data resets it, which loses the response on Windows
|
|
235
|
+
self.lingering_timeout = lingering_timeout
|
|
236
|
+
"""::
|
|
237
|
+
|
|
238
|
+
timeout = daemon.lingering_timeout
|
|
239
|
+
daemon.lingering_timeout = 10
|
|
240
|
+
|
|
241
|
+
Maximum amount of time in seconds a connection whose request has not
|
|
242
|
+
been read to its end, as one that exceeds a limit, is kept open after its
|
|
243
|
+
response, to read and discard the rest of the request, so that the
|
|
244
|
+
client receives the response before the connection is closed, defaults
|
|
245
|
+
to ``5``.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
self.max_clients = max_clients
|
|
249
|
+
"""::
|
|
250
|
+
|
|
251
|
+
max = daemon.max_clients
|
|
252
|
+
daemon.max_clients = 100
|
|
253
|
+
|
|
254
|
+
Maximum number of accepted connections this server is allowed to
|
|
255
|
+
handle concurrently, before stopping to accept new incoming
|
|
256
|
+
connections, passed along to :attr:`Pyjo.IOLoop.Pyjo_IOLoop.max_connections`.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
self.max_requests = max_requests
|
|
260
|
+
"""::
|
|
261
|
+
|
|
262
|
+
max = daemon.max_requests
|
|
263
|
+
daemon.max_requests = 250
|
|
264
|
+
|
|
265
|
+
Maximum number of keep-alive requests per connection, defaults to
|
|
266
|
+
``100``.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
# Not in Mojolicious: the time that a load balancer, which checks the readiness of the server, needs to send no
|
|
270
|
+
# more connections to it
|
|
271
|
+
self.shutdown_delay = shutdown_delay
|
|
272
|
+
"""::
|
|
273
|
+
|
|
274
|
+
delay = daemon.shutdown_delay
|
|
275
|
+
daemon.shutdown_delay = 5
|
|
276
|
+
|
|
277
|
+
Amount of time in seconds the server keeps accepting and serving
|
|
278
|
+
connections after ``INT`` or ``TERM``, before it closes the listen
|
|
279
|
+
sockets and shuts down gracefully, while
|
|
280
|
+
:attr:`Pyjo.IOLoop.Pyjo_IOLoop.is_stopping` is true already, so that
|
|
281
|
+
the readiness of :mod:`Pyjoyment.Plugin.Health` fails and a load
|
|
282
|
+
balancer sends the connections to another instance, defaults to
|
|
283
|
+
``0``.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
self.silent = silent
|
|
287
|
+
"""::
|
|
288
|
+
|
|
289
|
+
boolean = daemon.silent
|
|
290
|
+
daemon.silent = boolean
|
|
291
|
+
|
|
292
|
+
Disable console messages.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
self._connections: dict[str, dict[str, Any]] = {}
|
|
296
|
+
self._servers: dict[str, Any] = {}
|
|
297
|
+
_DAEMONS.add(self)
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def inactivity_timeout(self) -> float:
|
|
301
|
+
"""::
|
|
302
|
+
|
|
303
|
+
timeout = daemon.inactivity_timeout
|
|
304
|
+
daemon.inactivity_timeout = 5
|
|
305
|
+
|
|
306
|
+
Maximum amount of time in seconds a connection with an active request
|
|
307
|
+
can be inactive before getting closed, defaults to the value of the
|
|
308
|
+
``PYJO_INACTIVITY_TIMEOUT`` environment variable or ``30``. Setting
|
|
309
|
+
the value to ``0`` will allow connections to be inactive
|
|
310
|
+
indefinitely.
|
|
311
|
+
"""
|
|
312
|
+
if self._inactivity_timeout is None:
|
|
313
|
+
self._inactivity_timeout = _number(os.environ.get('PYJO_INACTIVITY_TIMEOUT'), 30)
|
|
314
|
+
return self._inactivity_timeout
|
|
315
|
+
|
|
316
|
+
@inactivity_timeout.setter
|
|
317
|
+
def inactivity_timeout(self, value: float) -> None:
|
|
318
|
+
self._inactivity_timeout = value
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def keep_alive_timeout(self) -> float:
|
|
322
|
+
"""::
|
|
323
|
+
|
|
324
|
+
timeout = daemon.keep_alive_timeout
|
|
325
|
+
daemon.keep_alive_timeout = 10
|
|
326
|
+
|
|
327
|
+
Maximum amount of time in seconds a connection without an active
|
|
328
|
+
request can be inactive before getting closed, defaults to the value
|
|
329
|
+
of the ``PYJO_KEEP_ALIVE_TIMEOUT`` environment variable or ``5``.
|
|
330
|
+
Setting the value to ``0`` will allow connections to be inactive
|
|
331
|
+
indefinitely.
|
|
332
|
+
"""
|
|
333
|
+
if self._keep_alive_timeout is None:
|
|
334
|
+
self._keep_alive_timeout = _number(os.environ.get('PYJO_KEEP_ALIVE_TIMEOUT'), 5)
|
|
335
|
+
return self._keep_alive_timeout
|
|
336
|
+
|
|
337
|
+
@keep_alive_timeout.setter
|
|
338
|
+
def keep_alive_timeout(self, value: float) -> None:
|
|
339
|
+
self._keep_alive_timeout = value
|
|
340
|
+
|
|
341
|
+
@property
|
|
342
|
+
def listen(self) -> list[Location]:
|
|
343
|
+
"""::
|
|
344
|
+
|
|
345
|
+
listen = daemon.listen
|
|
346
|
+
daemon.listen = ['https://127.0.0.1:8080']
|
|
347
|
+
|
|
348
|
+
List of one or more locations to listen on, as strings or
|
|
349
|
+
:mod:`Pyjo.URL` objects, defaults to the value of the ``PYJO_LISTEN``
|
|
350
|
+
environment variable, to ``http://*:`` with the port of the ``PORT``
|
|
351
|
+
environment variable, which platforms as Knative, Cloud Run and Heroku
|
|
352
|
+
set, or to ``http://*:3000`` (shortcut for ``http://0.0.0.0:3000``). ::
|
|
353
|
+
|
|
354
|
+
# Listen on all IPv4 interfaces
|
|
355
|
+
daemon.listen = ['http://*:3000']
|
|
356
|
+
|
|
357
|
+
# Listen on all IPv4 and IPv6 interfaces
|
|
358
|
+
daemon.listen = ['http://[::]:3000']
|
|
359
|
+
|
|
360
|
+
# Listen on IPv6 interface
|
|
361
|
+
daemon.listen = ['http://[::1]:4000']
|
|
362
|
+
|
|
363
|
+
# Listen on IPv4 and IPv6 interfaces
|
|
364
|
+
daemon.listen = ['http://127.0.0.1:3000', 'http://[::1]:3000']
|
|
365
|
+
|
|
366
|
+
# Listen on UNIX domain socket "/tmp/myapp.sock" (percent encoded slash)
|
|
367
|
+
daemon.listen = ['http+unix://%2Ftmp%2Fmyapp.sock']
|
|
368
|
+
|
|
369
|
+
# FastCGI for a web server in front of the application
|
|
370
|
+
daemon.listen = ['fcgi://127.0.0.1:9000']
|
|
371
|
+
|
|
372
|
+
# FastCGI on UNIX domain socket "/tmp/myapp.sock"
|
|
373
|
+
daemon.listen = ['fcgi+unix://%2Ftmp%2Fmyapp.sock']
|
|
374
|
+
|
|
375
|
+
# File descriptor, as used by systemd
|
|
376
|
+
daemon.listen = ['http://127.0.0.1?fd=3']
|
|
377
|
+
|
|
378
|
+
# Allow multiple servers to use the same port (SO_REUSEPORT)
|
|
379
|
+
daemon.listen = ['http://*:8080?reuse=1']
|
|
380
|
+
|
|
381
|
+
# Listen on two ports with HTTP and HTTPS at the same time
|
|
382
|
+
daemon.listen = ['http://*:3000', 'https://*:4000']
|
|
383
|
+
|
|
384
|
+
# Location of a URL object
|
|
385
|
+
daemon.listen = [Pyjo.URL.new().set(scheme='http', host='127.0.0.1', port=3000)]
|
|
386
|
+
|
|
387
|
+
# Use a custom certificate and key
|
|
388
|
+
daemon.listen = ['https://*:3000?cert=/x/server.crt&key=/y/server.key']
|
|
389
|
+
|
|
390
|
+
# Domain specific certificates and keys (SNI)
|
|
391
|
+
daemon.listen = ['https://*:3000?example.com_cert=/x/my.crt&example.com_key=/y/my.key']
|
|
392
|
+
|
|
393
|
+
# Or even a custom certificate authority
|
|
394
|
+
daemon.listen = ['https://*:3000?cert=/x/server.crt&key=/y/server.key&ca=/z/ca.crt']
|
|
395
|
+
|
|
396
|
+
These parameters are currently available:
|
|
397
|
+
|
|
398
|
+
ca
|
|
399
|
+
::
|
|
400
|
+
|
|
401
|
+
ca=/etc/tls/ca.crt
|
|
402
|
+
|
|
403
|
+
Path to TLS certificate authority file used to verify the peer
|
|
404
|
+
certificate.
|
|
405
|
+
|
|
406
|
+
cert
|
|
407
|
+
::
|
|
408
|
+
|
|
409
|
+
cert=/etc/tls/server.crt
|
|
410
|
+
mojolicious.org_cert=/etc/tls/mojo.crt
|
|
411
|
+
|
|
412
|
+
Path to the TLS cert file, defaults to a built-in test
|
|
413
|
+
certificate.
|
|
414
|
+
|
|
415
|
+
ciphers
|
|
416
|
+
::
|
|
417
|
+
|
|
418
|
+
ciphers=AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH
|
|
419
|
+
|
|
420
|
+
TLS cipher specification string. For more information about the
|
|
421
|
+
format see
|
|
422
|
+
https://www.openssl.org/docs/manmaster/man1/ciphers.html#CIPHER-STRINGS.
|
|
423
|
+
|
|
424
|
+
fd
|
|
425
|
+
::
|
|
426
|
+
|
|
427
|
+
fd=3
|
|
428
|
+
|
|
429
|
+
File descriptor with an already prepared listen socket.
|
|
430
|
+
|
|
431
|
+
key
|
|
432
|
+
::
|
|
433
|
+
|
|
434
|
+
key=/etc/tls/server.key
|
|
435
|
+
mojolicious.org_key=/etc/tls/mojo.key
|
|
436
|
+
|
|
437
|
+
Path to the TLS key file, defaults to a built-in test key.
|
|
438
|
+
|
|
439
|
+
reuse
|
|
440
|
+
::
|
|
441
|
+
|
|
442
|
+
reuse=1
|
|
443
|
+
|
|
444
|
+
Allow multiple servers to use the same port with the
|
|
445
|
+
``SO_REUSEPORT`` socket option.
|
|
446
|
+
|
|
447
|
+
single_accept
|
|
448
|
+
::
|
|
449
|
+
|
|
450
|
+
single_accept=1
|
|
451
|
+
|
|
452
|
+
Only accept one connection at a time.
|
|
453
|
+
|
|
454
|
+
verify
|
|
455
|
+
::
|
|
456
|
+
|
|
457
|
+
verify=0x00
|
|
458
|
+
|
|
459
|
+
TLS verification mode: ``0x00`` verifies no peer, ``0x01``
|
|
460
|
+
verifies a peer certificate if there is one, and ``0x03`` requires
|
|
461
|
+
it.
|
|
462
|
+
|
|
463
|
+
version
|
|
464
|
+
::
|
|
465
|
+
|
|
466
|
+
version=TLSv1_2
|
|
467
|
+
|
|
468
|
+
TLS protocol version.
|
|
469
|
+
"""
|
|
470
|
+
if self._locations is None:
|
|
471
|
+
# Not in Mojolicious: the port of PORT, which the platform that runs the server tells it to listen on
|
|
472
|
+
port = os.environ.get('PORT') or ''
|
|
473
|
+
default = f'http://*:{port}' if re.fullmatch(r'\d+', port) else 'http://*:3000'
|
|
474
|
+
self._locations = [location or '' for location in split(',', os.environ.get('PYJO_LISTEN') or default)]
|
|
475
|
+
return self._locations
|
|
476
|
+
|
|
477
|
+
@listen.setter
|
|
478
|
+
def listen(self, value: Sequence[Location]) -> None:
|
|
479
|
+
self._locations = list(value)
|
|
480
|
+
|
|
481
|
+
def __del__(self) -> None:
|
|
482
|
+
try:
|
|
483
|
+
if sys.is_finalizing():
|
|
484
|
+
return
|
|
485
|
+
# Not in Mojolicious: the reactor that the daemon has, without making one
|
|
486
|
+
reactor = getattr(getattr(self, 'ioloop', None), '_reactor', None)
|
|
487
|
+
Pyjo.Reactor._finalize(self._cleanup, reactor) # pyright: ignore[reportPrivateUsage]
|
|
488
|
+
except Exception:
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
# Not in Mojolicious: a context manager, which does what DESTROY does, as PyPy frees the daemon only when its
|
|
492
|
+
# garbage collector runs
|
|
493
|
+
def __enter__(self) -> Self:
|
|
494
|
+
return self
|
|
495
|
+
|
|
496
|
+
def __exit__(
|
|
497
|
+
self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
|
|
498
|
+
) -> None:
|
|
499
|
+
self.close()
|
|
500
|
+
|
|
501
|
+
# Not in Mojolicious: the release of the listen sockets and the connections, which Perl does in DESTROY, also at the
|
|
502
|
+
# end of a with block
|
|
503
|
+
def close(self) -> None:
|
|
504
|
+
"""::
|
|
505
|
+
|
|
506
|
+
daemon.close()
|
|
507
|
+
|
|
508
|
+
Close the listen sockets and the connections of the server, as the end
|
|
509
|
+
of a ``with`` block does.
|
|
510
|
+
"""
|
|
511
|
+
self._cleanup()
|
|
512
|
+
|
|
513
|
+
def ports(self) -> list[int]:
|
|
514
|
+
"""::
|
|
515
|
+
|
|
516
|
+
ports = daemon.ports()
|
|
517
|
+
|
|
518
|
+
Get all ports this server is currently listening on. ::
|
|
519
|
+
|
|
520
|
+
# All ports
|
|
521
|
+
for port in daemon.ports():
|
|
522
|
+
print(port)
|
|
523
|
+
"""
|
|
524
|
+
return [self.ioloop.acceptor(id).port for id in self.acceptors]
|
|
525
|
+
|
|
526
|
+
def run(self) -> None:
|
|
527
|
+
"""::
|
|
528
|
+
|
|
529
|
+
daemon.run()
|
|
530
|
+
|
|
531
|
+
Run server and wait for the signals ``INT``, ``TERM`` and ``QUIT``.
|
|
532
|
+
"""
|
|
533
|
+
# Make sure the event loop can be stopped in regular intervals
|
|
534
|
+
loop = self.ioloop
|
|
535
|
+
interval = loop.recurring(1, lambda loop: None)
|
|
536
|
+
graceful = immediate = False
|
|
537
|
+
|
|
538
|
+
# Unlike Mojolicious: INT and TERM stop the event loop gracefully, as Docker and Kubernetes stop the server with
|
|
539
|
+
# TERM, and QUIT immediately, where Mojo stops it immediately on INT and TERM; a second INT, as of Ctrl-C pressed
|
|
540
|
+
# twice, stops it immediately, and the graceful stop is left to the next tick, as a handler of Python runs in the
|
|
541
|
+
# middle of what the event loop does
|
|
542
|
+
def stop(signum: int, frame: Any) -> None:
|
|
543
|
+
nonlocal graceful, immediate
|
|
544
|
+
if signum == _QUIT or (signum == signal.SIGINT and graceful):
|
|
545
|
+
immediate = True
|
|
546
|
+
loop.stop()
|
|
547
|
+
elif not graceful:
|
|
548
|
+
graceful = True
|
|
549
|
+
loop.next_tick(lambda loop: loop.stop_gracefully(self.shutdown_delay))
|
|
550
|
+
|
|
551
|
+
# Not in Mojolicious: the graceful stop, after its delay, closes the listen sockets, and a connection that is
|
|
552
|
+
# kept alive after its next response, as a worker of Mojo::Server::Prefork does, so that a client that keeps
|
|
553
|
+
# requesting does not keep the server from stopping; local $self->{max_requests}, the value is set back at the end
|
|
554
|
+
max_requests = self.max_requests
|
|
555
|
+
|
|
556
|
+
def finish(loop: Any) -> None:
|
|
557
|
+
self._close_listen()
|
|
558
|
+
self.max_requests = 1
|
|
559
|
+
|
|
560
|
+
finished = loop.once('finish', finish)
|
|
561
|
+
|
|
562
|
+
# Not in Mojolicious: a thread other than the main one cannot handle signals, and HUP is ignored, as there is
|
|
563
|
+
# nothing to reload
|
|
564
|
+
handlers: dict[signal.Signals, Any] = {}
|
|
565
|
+
waker = wakee = None
|
|
566
|
+
wakeup = -1
|
|
567
|
+
if threading.current_thread() is threading.main_thread():
|
|
568
|
+
for name in (signal.SIGINT, signal.SIGTERM, _QUIT):
|
|
569
|
+
handlers[name] = signal.signal(name, stop)
|
|
570
|
+
if sys.platform != 'win32':
|
|
571
|
+
handlers[signal.SIGHUP] = signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
|
572
|
+
|
|
573
|
+
# Not in Mojolicious: a socket pair that Python writes to on a signal wakes the event loop up, as Python
|
|
574
|
+
# repeats a system call that a signal interrupts until its timeout, so that a stop does not wait for the
|
|
575
|
+
# interval, as the workers of Pyjo.Server.Prefork are woken up
|
|
576
|
+
waker, wakee = socket.socketpair()
|
|
577
|
+
waker.setblocking(False)
|
|
578
|
+
wakee.setblocking(False)
|
|
579
|
+
wakeup = signal.set_wakeup_fd(waker.fileno())
|
|
580
|
+
|
|
581
|
+
def wake(reactor: Any, writable: bool) -> None:
|
|
582
|
+
try:
|
|
583
|
+
wakee.recv(4096)
|
|
584
|
+
except OSError:
|
|
585
|
+
pass
|
|
586
|
+
|
|
587
|
+
loop.reactor.io(wakee, wake).watch(wakee, True, False)
|
|
588
|
+
try:
|
|
589
|
+
self.start().ioloop.start()
|
|
590
|
+
|
|
591
|
+
# Not in Mojolicious: the end of the server, unless it has stopped immediately, whose cleanup a signal that
|
|
592
|
+
# stops it immediately interrupts
|
|
593
|
+
if not immediate:
|
|
594
|
+
self._server_stopped()
|
|
595
|
+
finally:
|
|
596
|
+
if waker is not None and wakee is not None:
|
|
597
|
+
signal.set_wakeup_fd(wakeup)
|
|
598
|
+
loop.reactor.remove(wakee)
|
|
599
|
+
waker.close()
|
|
600
|
+
wakee.close()
|
|
601
|
+
for name, handler in handlers.items():
|
|
602
|
+
signal.signal(name, handler)
|
|
603
|
+
loop.remove(interval)
|
|
604
|
+
loop.unsubscribe('finish', finished)
|
|
605
|
+
self.max_requests = max_requests
|
|
606
|
+
|
|
607
|
+
# Not in Mojolicious: the cleanup of the application runs the event loop of its promise, which a signal that stops
|
|
608
|
+
# the server immediately stops, as it is the one of the server
|
|
609
|
+
def _server_stopped(self) -> None:
|
|
610
|
+
import Pyjo.Promise
|
|
611
|
+
|
|
612
|
+
if not isinstance(promise := self.app.server_stopped(self), Pyjo.Promise.object):
|
|
613
|
+
return
|
|
614
|
+
loop = promise.ioloop
|
|
615
|
+
done: list[bool] = []
|
|
616
|
+
|
|
617
|
+
def finish(*args: Any) -> None:
|
|
618
|
+
done.append(True)
|
|
619
|
+
loop.stop()
|
|
620
|
+
|
|
621
|
+
promise.then(finish, finish)
|
|
622
|
+
if not done:
|
|
623
|
+
loop.start()
|
|
624
|
+
|
|
625
|
+
def start(self) -> Self:
|
|
626
|
+
"""::
|
|
627
|
+
|
|
628
|
+
daemon = daemon.start()
|
|
629
|
+
|
|
630
|
+
Start or resume accepting connections through :attr:`ioloop`. ::
|
|
631
|
+
|
|
632
|
+
# Listen on random port
|
|
633
|
+
daemon.listen = ['http://127.0.0.1']
|
|
634
|
+
port = daemon.start().ports()[0]
|
|
635
|
+
|
|
636
|
+
# Run multiple web servers concurrently
|
|
637
|
+
daemon1 = Pyjo.Server.Daemon.new(listen=['http://*:3000']).start()
|
|
638
|
+
daemon2 = Pyjo.Server.Daemon.new(listen=['http://*:4000']).start()
|
|
639
|
+
if not Pyjo.IOLoop.is_running():
|
|
640
|
+
Pyjo.IOLoop.start()
|
|
641
|
+
"""
|
|
642
|
+
loop = self.ioloop
|
|
643
|
+
if max := self.max_clients:
|
|
644
|
+
loop.max_connections = max
|
|
645
|
+
|
|
646
|
+
# Resume accepting connections
|
|
647
|
+
if servers := self._servers:
|
|
648
|
+
for id in list(servers):
|
|
649
|
+
self.acceptors.append(loop.acceptor(servers.pop(id)))
|
|
650
|
+
|
|
651
|
+
# Start listening
|
|
652
|
+
elif not len(self.acceptors):
|
|
653
|
+
self.app.server(self)
|
|
654
|
+
for listen in self.listen:
|
|
655
|
+
self._listen(listen)
|
|
656
|
+
|
|
657
|
+
return self
|
|
658
|
+
|
|
659
|
+
def stop(self) -> Self:
|
|
660
|
+
"""::
|
|
661
|
+
|
|
662
|
+
daemon = daemon.stop()
|
|
663
|
+
|
|
664
|
+
Stop accepting connections through :attr:`ioloop`.
|
|
665
|
+
"""
|
|
666
|
+
# Suspend accepting connections but keep listen sockets open
|
|
667
|
+
loop = self.ioloop
|
|
668
|
+
while len(self.acceptors):
|
|
669
|
+
id = self.acceptors.pop(0)
|
|
670
|
+
self._servers[id] = loop.acceptor(id)
|
|
671
|
+
loop.remove(id, close=False)
|
|
672
|
+
|
|
673
|
+
return self
|
|
674
|
+
|
|
675
|
+
def _build_tx(self, id: str, c: dict[str, Any]) -> Pyjo.Transaction.HTTP.object:
|
|
676
|
+
tx = self.build_tx()
|
|
677
|
+
tx.connection = id
|
|
678
|
+
# Unlike Mojolicious: Pyjoyment names itself
|
|
679
|
+
tx.res.headers.server = 'Pyjoyment (Python)'
|
|
680
|
+
stream = self.ioloop.stream(id)
|
|
681
|
+
stream.timeout = self.inactivity_timeout
|
|
682
|
+
handle = stream.handle
|
|
683
|
+
if handle.family != getattr(socket, 'AF_UNIX', None):
|
|
684
|
+
if (addr := c.get('addr')) is None:
|
|
685
|
+
addr = c['addr'] = (*handle.getsockname()[:2], *handle.getpeername()[:2])
|
|
686
|
+
tx.local_address, tx.local_port, tx.remote_address, tx.remote_port = addr
|
|
687
|
+
if c.get('tls'):
|
|
688
|
+
tx.req.url.base.scheme = 'https'
|
|
689
|
+
|
|
690
|
+
ref = weakref.ref(self)
|
|
691
|
+
|
|
692
|
+
@tx.on('request')
|
|
693
|
+
def request(tx: Pyjo.Transaction.HTTP.object) -> None:
|
|
694
|
+
if (this := ref()) is None:
|
|
695
|
+
return
|
|
696
|
+
req = tx.req
|
|
697
|
+
if error := req.error:
|
|
698
|
+
this._trace(id, error['message'])
|
|
699
|
+
|
|
700
|
+
# WebSocket
|
|
701
|
+
if req.is_handshake:
|
|
702
|
+
ws = this._connections[id]['next'] = Pyjo.Transaction.WebSocket.new(handshake=tx)
|
|
703
|
+
this.emit('request', server_handshake(ws))
|
|
704
|
+
|
|
705
|
+
# HTTP
|
|
706
|
+
else:
|
|
707
|
+
this.emit('request', tx)
|
|
708
|
+
|
|
709
|
+
# Last keep-alive request or corrupted connection
|
|
710
|
+
c = this._connections.get(id, {})
|
|
711
|
+
if (c.get('requests') or 1) >= this.max_requests or req.error:
|
|
712
|
+
tx.res.headers.connection = 'close'
|
|
713
|
+
|
|
714
|
+
def resume(tx: Pyjo.Transaction.HTTP.object) -> None:
|
|
715
|
+
_call(ref, '_write', id)
|
|
716
|
+
|
|
717
|
+
tx.on('resume', resume)
|
|
718
|
+
this._write(id)
|
|
719
|
+
|
|
720
|
+
# Kept alive if we have more than one request on the connection
|
|
721
|
+
c['requests'] = c.get('requests', 0) + 1
|
|
722
|
+
if c['requests'] > 1:
|
|
723
|
+
tx.kept_alive = True
|
|
724
|
+
return tx
|
|
725
|
+
|
|
726
|
+
def _cleanup(self) -> None:
|
|
727
|
+
loop = self.ioloop
|
|
728
|
+
for id in [*self._connections, *self.acceptors]:
|
|
729
|
+
loop.remove(id)
|
|
730
|
+
self.acceptors = []
|
|
731
|
+
# Not in Mojolicious: the listen sockets of a stopped daemon, which Perl closes as it frees them
|
|
732
|
+
for server in self._servers.values():
|
|
733
|
+
server.close()
|
|
734
|
+
self._servers = {}
|
|
735
|
+
|
|
736
|
+
# Unlike Mojolicious: the listen sockets are closed when the server starts to stop gracefully, after its delay, so
|
|
737
|
+
# that a new connection is refused at once, where Mojo only stops accepting connections and keeps the sockets until it
|
|
738
|
+
# ends, so that a new connection waits in the backlog of the socket, without a response, until the server ends and
|
|
739
|
+
# resets it
|
|
740
|
+
def _close_listen(self) -> None:
|
|
741
|
+
loop = self.ioloop
|
|
742
|
+
for id in self.acceptors:
|
|
743
|
+
if (acceptor := loop.acceptor(id)) is not None:
|
|
744
|
+
acceptor.close()
|
|
745
|
+
for server in self._servers.values():
|
|
746
|
+
server.close()
|
|
747
|
+
|
|
748
|
+
def _close(self, id: str) -> None:
|
|
749
|
+
if (fcgi := self._connections.get(id, {}).get('fcgi')) is not None:
|
|
750
|
+
fcgi.closed()
|
|
751
|
+
if (tx := self._connections.get(id, {}).get('tx')) is not None:
|
|
752
|
+
tx.closed()
|
|
753
|
+
# Not in Mojolicious: the WebSocket of a handshake whose response has not been written, which Mojo drops without
|
|
754
|
+
# its event finish, so that what the response waits for, as the websocket.accept of an ASGI application, ends
|
|
755
|
+
if (ws := self._connections.get(id, {}).get('next')) is not None:
|
|
756
|
+
ws.closed()
|
|
757
|
+
self._connections.pop(id, None)
|
|
758
|
+
|
|
759
|
+
def _finish(self, id: str) -> None:
|
|
760
|
+
# Always remove connection for WebSockets
|
|
761
|
+
c = self._connections.get(id)
|
|
762
|
+
if c is None or (tx := c.get('tx')) is None:
|
|
763
|
+
return
|
|
764
|
+
if tx.is_websocket:
|
|
765
|
+
return self._remove(id)
|
|
766
|
+
|
|
767
|
+
# Finish transaction
|
|
768
|
+
c.pop('tx').closed()
|
|
769
|
+
|
|
770
|
+
# Upgrade connection to WebSocket
|
|
771
|
+
if (ws := c.pop('next', None)) is not None:
|
|
772
|
+
# Successful upgrade
|
|
773
|
+
if ws.handshake.res.code == 101:
|
|
774
|
+
ws.established = True
|
|
775
|
+
c['tx'] = ws
|
|
776
|
+
ref = weakref.ref(self)
|
|
777
|
+
|
|
778
|
+
def resume(ws: Pyjo.Transaction.WebSocket.object) -> None:
|
|
779
|
+
_call(ref, '_write', id)
|
|
780
|
+
|
|
781
|
+
ws.on('resume', resume)
|
|
782
|
+
self._write(id)
|
|
783
|
+
return ws.server_read(tx.req.content.leftovers())
|
|
784
|
+
|
|
785
|
+
# Failed upgrade
|
|
786
|
+
else:
|
|
787
|
+
ws.closed()
|
|
788
|
+
|
|
789
|
+
# Close connection if necessary
|
|
790
|
+
if tx.error or not tx.keep_alive:
|
|
791
|
+
if (req := tx.req).error or not req.is_finished:
|
|
792
|
+
return self._linger(id)
|
|
793
|
+
return self._remove(id)
|
|
794
|
+
|
|
795
|
+
# Build new transaction for leftovers
|
|
796
|
+
if len(leftovers := tx.req.content.leftovers()):
|
|
797
|
+
tx = c['tx'] = self._build_tx(id, c)
|
|
798
|
+
tx.server_read(leftovers)
|
|
799
|
+
|
|
800
|
+
# Keep-alive connection
|
|
801
|
+
if c.get('tx') is None:
|
|
802
|
+
self.ioloop.stream(id).timeout = self.keep_alive_timeout
|
|
803
|
+
|
|
804
|
+
# Not in Mojolicious: the end of the response, with the rest of the request read and discarded until the client closes
|
|
805
|
+
# the connection or the lingering timeout, as a close with unread data resets the connection, which loses the response
|
|
806
|
+
# on Windows, as it does on other systems where the reset comes before the client has read it
|
|
807
|
+
def _linger(self, id: str) -> None:
|
|
808
|
+
stream = self.ioloop.stream(id)
|
|
809
|
+
self._close(id)
|
|
810
|
+
if stream is None:
|
|
811
|
+
return
|
|
812
|
+
stream.unsubscribe('read')
|
|
813
|
+
|
|
814
|
+
# The end of the response, which a TLS connection cannot send without its own close
|
|
815
|
+
if not isinstance(handle := stream.handle, ssl.SSLSocket):
|
|
816
|
+
with contextlib.suppress(OSError):
|
|
817
|
+
handle.shutdown(socket.SHUT_WR)
|
|
818
|
+
self.ioloop.timer(self.lingering_timeout, lambda loop: loop.remove(id))
|
|
819
|
+
|
|
820
|
+
def _listen(self, listen: Location) -> None:
|
|
821
|
+
# A Pyjo.URL stringifies, as a Mojo::URL does for Mojo::URL->new
|
|
822
|
+
url = Pyjo.URL.new(str(listen))
|
|
823
|
+
proto = url.protocol
|
|
824
|
+
# Not in Mojolicious: FastCGI, with Pyjo.Server.FastCGI
|
|
825
|
+
if proto not in ('http', 'https', 'http+unix', 'fcgi', 'fcgi+unix'):
|
|
826
|
+
raise Pyjo.Exception.new(f'Invalid listen location "{listen}"')
|
|
827
|
+
fcgi = proto.startswith('fcgi')
|
|
828
|
+
|
|
829
|
+
query = url.query
|
|
830
|
+
options: dict[str, Any] = {'backlog': self.backlog}
|
|
831
|
+
if (fd := query.param('fd')) is not None:
|
|
832
|
+
options['fd'] = int(fd)
|
|
833
|
+
for name in ('single_accept', 'reuse'):
|
|
834
|
+
options[name] = (value := query.param(name)) is not None and value not in ('', '0')
|
|
835
|
+
if proto in ('http+unix', 'fcgi+unix'):
|
|
836
|
+
options['path'] = url.host
|
|
837
|
+
else:
|
|
838
|
+
if (host := url.host) != '*':
|
|
839
|
+
options['address'] = host
|
|
840
|
+
if port := url.port:
|
|
841
|
+
options['port'] = port
|
|
842
|
+
|
|
843
|
+
options['tls_ca'] = query.param('ca')
|
|
844
|
+
for name in query.names():
|
|
845
|
+
if m := re.fullmatch(r'(.*)_(cert|key)', name):
|
|
846
|
+
options.setdefault(f'tls_{m[2]}', {})[m[1]] = query.param(name)
|
|
847
|
+
if cert := query.param('cert'):
|
|
848
|
+
options.setdefault('tls_cert', {})[''] = cert
|
|
849
|
+
if key := query.param('key'):
|
|
850
|
+
options.setdefault('tls_key', {})[''] = key
|
|
851
|
+
tls = options['tls'] = proto == 'https'
|
|
852
|
+
|
|
853
|
+
# Unlike Mojolicious: the options of IO::Socket::SSL change the context that the other options make
|
|
854
|
+
ciphers, verify, version = query.param('ciphers'), query.param('verify'), query.param('version')
|
|
855
|
+
if tls and (ciphers is not None or verify is not None or version is not None):
|
|
856
|
+
context = Pyjo.IOLoop.TLS.context(server=True, **options)
|
|
857
|
+
if ciphers is not None:
|
|
858
|
+
context.set_ciphers(ciphers)
|
|
859
|
+
if verify is not None:
|
|
860
|
+
mode = int(verify, 16)
|
|
861
|
+
context.verify_mode = (
|
|
862
|
+
ssl.CERT_NONE if not mode & 1 else ssl.CERT_REQUIRED if mode & 2 else ssl.CERT_OPTIONAL
|
|
863
|
+
)
|
|
864
|
+
if version is not None and (tls_version := VERSIONS.get(version)) is not None:
|
|
865
|
+
context.minimum_version = context.maximum_version = tls_version
|
|
866
|
+
options['tls_context'] = context
|
|
867
|
+
|
|
868
|
+
ref = weakref.ref(self)
|
|
869
|
+
|
|
870
|
+
def server(loop: Pyjo.IOLoop.object, stream: Pyjo.IOLoop.Stream.object, id: str) -> None:
|
|
871
|
+
if (this := ref()) is None:
|
|
872
|
+
return
|
|
873
|
+
this._connections[id] = {'tls': tls}
|
|
874
|
+
# Not in Mojolicious: FastCGI, whose records Pyjo.Server.FastCGI reads and writes
|
|
875
|
+
if fcgi:
|
|
876
|
+
this._connections[id]['fcgi'] = Pyjo.Server.FastCGI.new(this, id)
|
|
877
|
+
if DEBUG:
|
|
878
|
+
warn(f'-- Accept {id} ({_peer(stream.handle)})\n')
|
|
879
|
+
stream.timeout = this.inactivity_timeout
|
|
880
|
+
|
|
881
|
+
def close(stream: Pyjo.IOLoop.Stream.object) -> None:
|
|
882
|
+
_call(ref, '_close', id)
|
|
883
|
+
|
|
884
|
+
def error(stream: Pyjo.IOLoop.Stream.object, err: Any) -> None:
|
|
885
|
+
_call(ref, '_error', id, err)
|
|
886
|
+
|
|
887
|
+
def read(stream: Pyjo.IOLoop.Stream.object, chunk: bytes) -> None:
|
|
888
|
+
_call(ref, '_read', id, chunk)
|
|
889
|
+
|
|
890
|
+
def timeout(stream: Pyjo.IOLoop.Stream.object) -> None:
|
|
891
|
+
_call(ref, '_trace', id, 'Inactivity timeout (see FAQ for more)')
|
|
892
|
+
|
|
893
|
+
stream.on('close', close)
|
|
894
|
+
stream.on('error', error)
|
|
895
|
+
stream.on('read', read)
|
|
896
|
+
stream.on('timeout', timeout)
|
|
897
|
+
|
|
898
|
+
self.acceptors.append(self.ioloop.server(server, **options))
|
|
899
|
+
|
|
900
|
+
if self.silent:
|
|
901
|
+
return
|
|
902
|
+
self.app.log.info(f'Listening at "{url}"')
|
|
903
|
+
query.pairs = []
|
|
904
|
+
if url.host == '*':
|
|
905
|
+
url.host = '127.0.0.1'
|
|
906
|
+
if not options.get('path') and not url.port:
|
|
907
|
+
url.port = self.ports()[-1]
|
|
908
|
+
# Not in Mojolicious: the message is flushed, as a pipe, as of a log or of a container, would keep it until the
|
|
909
|
+
# server ends, and lose it when the server is killed
|
|
910
|
+
print(f'Web application available at {options.get("path") or url}', flush=True)
|
|
911
|
+
|
|
912
|
+
def _error(self, id: str, err: Any) -> None:
|
|
913
|
+
self.app.log.error(str(err))
|
|
914
|
+
self._close(id)
|
|
915
|
+
|
|
916
|
+
def _read(self, id: str, chunk: bytes) -> None:
|
|
917
|
+
# Make sure we have a transaction
|
|
918
|
+
c = self._connections[id]
|
|
919
|
+
if (fcgi := c.get('fcgi')) is not None:
|
|
920
|
+
return fcgi.read(chunk)
|
|
921
|
+
if (tx := c.get('tx')) is None:
|
|
922
|
+
tx = c['tx'] = self._build_tx(id, c)
|
|
923
|
+
if DEBUG:
|
|
924
|
+
warn(term_escape(f'-- Server <<< Client ({_url(tx)})\n{chunk.decode("latin-1")}\n'))
|
|
925
|
+
tx.server_read(chunk)
|
|
926
|
+
|
|
927
|
+
def _remove(self, id: str) -> None:
|
|
928
|
+
self.ioloop.remove(id)
|
|
929
|
+
self._close(id)
|
|
930
|
+
|
|
931
|
+
def _trace(self, id: str, message: str) -> None:
|
|
932
|
+
if self._connections.get(id, {}).get('tx') is not None:
|
|
933
|
+
self.app.log.trace(message)
|
|
934
|
+
|
|
935
|
+
def _write(self, id: str) -> None:
|
|
936
|
+
# Protect from resume event recursion
|
|
937
|
+
c = self._connections.get(id)
|
|
938
|
+
if c is None or (tx := c.get('tx')) is None or c.get('writing'):
|
|
939
|
+
return
|
|
940
|
+
writing = c.get('writing')
|
|
941
|
+
c['writing'] = True
|
|
942
|
+
try:
|
|
943
|
+
chunk = tx.server_write()
|
|
944
|
+
finally:
|
|
945
|
+
c['writing'] = writing
|
|
946
|
+
if DEBUG:
|
|
947
|
+
warn(term_escape(f'-- Server >>> Client ({_url(tx)})\n{chunk.decode("latin-1")}\n'))
|
|
948
|
+
next = '_finish' if tx.is_finished else '_write' if len(chunk) else None
|
|
949
|
+
# Unlike Mojolicious: the stream that a callback of the event drain of the content, which server_write emits,
|
|
950
|
+
# has closed, as an application does once the last chunk of its response has been written, ends the write,
|
|
951
|
+
# where Mojo calls write on undef
|
|
952
|
+
if (stream := self.ioloop.stream(id)) is None:
|
|
953
|
+
return
|
|
954
|
+
if next is None:
|
|
955
|
+
stream.write(chunk)
|
|
956
|
+
return
|
|
957
|
+
ref = weakref.ref(self)
|
|
958
|
+
|
|
959
|
+
def written(stream: Pyjo.IOLoop.Stream.object) -> None:
|
|
960
|
+
_call(ref, next, id)
|
|
961
|
+
|
|
962
|
+
stream.write(chunk, written)
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
# A method of the daemon, as long as it exists, as Mojo weakens the daemon in its callbacks
|
|
966
|
+
def _call(ref: 'weakref.ref[Any]', method: str, *args: Any) -> None:
|
|
967
|
+
if (daemon := ref()) is not None:
|
|
968
|
+
getattr(daemon, method)(*args)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _number(value: str | None, default: float) -> float:
|
|
972
|
+
if value is None:
|
|
973
|
+
return default
|
|
974
|
+
number = float(value)
|
|
975
|
+
return int(number) if number.is_integer() else number
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def _peer(handle: Any) -> str:
|
|
979
|
+
if handle.family == getattr(socket, 'AF_UNIX', None):
|
|
980
|
+
return str(handle.getpeername())
|
|
981
|
+
return str(handle.getpeername()[0])
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def _url(tx: Pyjo.Transaction.object) -> str:
|
|
985
|
+
return str(tx.req.url.to_abs())
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
# Not in Mojolicious: the daemons that exist, whose listen sockets are closed at the end of the interpreter, as Perl
|
|
989
|
+
# closes them then, before Pyjo.IOLoop closes the connections of its event loops
|
|
990
|
+
_DAEMONS: 'weakref.WeakSet[Pyjo_Server_Daemon]' = weakref.WeakSet()
|
|
991
|
+
|
|
992
|
+
# Not in Mojolicious: Windows has no QUIT, of Ctrl-\ in a terminal, and BREAK of Ctrl-Break takes its place in a console
|
|
993
|
+
_QUIT = signal.SIGBREAK if sys.platform == 'win32' else signal.SIGQUIT
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
@atexit.register
|
|
997
|
+
def _cleanup() -> None:
|
|
998
|
+
for daemon in list(_DAEMONS):
|
|
999
|
+
try:
|
|
1000
|
+
daemon._cleanup() # pyright: ignore[reportPrivateUsage]
|
|
1001
|
+
except Exception:
|
|
1002
|
+
pass
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
new = Pyjo_Server_Daemon.new
|
|
1006
|
+
object = Pyjo_Server_Daemon
|
|
1007
|
+
Daemon = Pyjo_Server_Daemon
|