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/Prethread.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.Prethread - Pre-threading non-blocking I/O HTTP and WebSocket server
|
|
3
|
+
================================================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.Prethread import Prethread
|
|
7
|
+
|
|
8
|
+
prethread = Prethread(listen=['http://*:8080'])
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@prethread.unsubscribe('request').on('request')
|
|
12
|
+
def request(prethread, 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
|
+
prethread.run()
|
|
27
|
+
|
|
28
|
+
# Or import the module, as in Google Python Style Guide
|
|
29
|
+
|
|
30
|
+
import Pyjo.Server.Prethread
|
|
31
|
+
|
|
32
|
+
prethread = Pyjo.Server.Prethread.new(listen=['http://*:8080'])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@prethread.unsubscribe('request').on('request')
|
|
36
|
+
def request(prethread, 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
|
+
prethread.run()
|
|
51
|
+
|
|
52
|
+
:mod:`Pyjo.Server.Prethread` is a full featured pre-threading non-blocking
|
|
53
|
+
I/O HTTP and WebSocket server, which works as :mod:`Pyjo.Server.Prefork`
|
|
54
|
+
does, with threads of one process for the worker processes. Each worker is a
|
|
55
|
+
copy of the server in a thread of its own, with the :data:`Pyjo.IOLoop.singleton`
|
|
56
|
+
of the thread and the listen sockets of the manager. Note that the server uses
|
|
57
|
+
signals for process management, so you should avoid modifying signal handlers
|
|
58
|
+
in your applications.
|
|
59
|
+
|
|
60
|
+
The workers share the memory of the process and the application that the
|
|
61
|
+
manager has loaded, so the application has to be safe to use in several
|
|
62
|
+
threads at the same time. Python runs one thread at a time where it has the
|
|
63
|
+
global interpreter lock, so that the workers run at the same time only while
|
|
64
|
+
they wait, for the network or a blocking call, and on all processor cores on
|
|
65
|
+
a free-threaded Python. A worker that does not stop when it is told to, as
|
|
66
|
+
one in a blocking call, cannot be killed, as a process can, so the manager
|
|
67
|
+
leaves it, and it ends with the process.
|
|
68
|
+
|
|
69
|
+
The workers need no :func:`os.fork`, so the server runs on Windows too, where
|
|
70
|
+
:mod:`Pyjo.Server.Prefork` does not.
|
|
71
|
+
|
|
72
|
+
Manager signals
|
|
73
|
+
---------------
|
|
74
|
+
|
|
75
|
+
The :mod:`Pyjo.Server.Prethread` manager process can be controlled at runtime
|
|
76
|
+
with the following signals.
|
|
77
|
+
|
|
78
|
+
INT, TERM
|
|
79
|
+
~~~~~~~~~
|
|
80
|
+
|
|
81
|
+
Shut down server gracefully. The manager and the workers close the listen
|
|
82
|
+
sockets, so that a new connection is refused, and the workers finish the
|
|
83
|
+
accepted ones, after :attr:`~Pyjo.Server.Daemon.Pyjo_Server_Daemon.shutdown_delay`,
|
|
84
|
+
while the workers serve as before; on Windows, where the workers share the
|
|
85
|
+
sockets of the manager, a new connection waits until the server ends. A
|
|
86
|
+
second ``INT``, as of Ctrl-C pressed twice, shuts it down immediately, also
|
|
87
|
+
during the delay, a second ``TERM`` does nothing.
|
|
88
|
+
|
|
89
|
+
QUIT
|
|
90
|
+
~~~~
|
|
91
|
+
|
|
92
|
+
Shut down server immediately. On Windows, which has no ``QUIT``, ``BREAK``
|
|
93
|
+
does it, as of Ctrl-Break.
|
|
94
|
+
|
|
95
|
+
HUP
|
|
96
|
+
~~~
|
|
97
|
+
|
|
98
|
+
Restart the workers gracefully, or emit the event ``reload`` when it has
|
|
99
|
+
subscribers.
|
|
100
|
+
|
|
101
|
+
TTIN
|
|
102
|
+
~~~~
|
|
103
|
+
|
|
104
|
+
Increase worker pool by one.
|
|
105
|
+
|
|
106
|
+
TTOU
|
|
107
|
+
~~~~
|
|
108
|
+
|
|
109
|
+
Decrease worker pool by one.
|
|
110
|
+
|
|
111
|
+
The manager does not handle ``CHLD``, so that the application gets the exit
|
|
112
|
+
status of its child processes.
|
|
113
|
+
|
|
114
|
+
Windows has no ``HUP``, ``TTIN`` and ``TTOU``, and a ``TERM`` from another
|
|
115
|
+
process terminates the process there without its handler, so Ctrl-C and
|
|
116
|
+
Ctrl-Break of the console are what shut the server down.
|
|
117
|
+
|
|
118
|
+
Events
|
|
119
|
+
------
|
|
120
|
+
|
|
121
|
+
:mod:`Pyjo.Server.Prethread` inherits all events from
|
|
122
|
+
:mod:`Pyjo.Server.Prefork`, which pass the id of the thread of a worker, as
|
|
123
|
+
:func:`threading.get_native_id` gives it, where :mod:`Pyjo.Server.Prefork`
|
|
124
|
+
passes its process id. ::
|
|
125
|
+
|
|
126
|
+
prethread.on('spawn', lambda prethread, id: print(f'Worker {id} started'))
|
|
127
|
+
|
|
128
|
+
See also
|
|
129
|
+
--------
|
|
130
|
+
|
|
131
|
+
:mod:`Pyjoyment.Command.prethread`, :mod:`Pyjo.Server.Prefork`,
|
|
132
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
133
|
+
|
|
134
|
+
It has no counterpart in Mojolicious; it is inspired by
|
|
135
|
+
:mod:`Pyjo.Server.Prefork`, the counterpart of `Mojo::Server::Prefork
|
|
136
|
+
<https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/Prefork.pm>`_,
|
|
137
|
+
as `Thrall <https://metacpan.org/pod/Thrall>`_ is by Starlight.
|
|
138
|
+
|
|
139
|
+
Classes
|
|
140
|
+
-------
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
import Pyjo.IOLoop
|
|
144
|
+
import Pyjo.Server.Prefork
|
|
145
|
+
import Pyjo.URL
|
|
146
|
+
|
|
147
|
+
import copy
|
|
148
|
+
import os
|
|
149
|
+
import signal
|
|
150
|
+
import socket
|
|
151
|
+
import sys
|
|
152
|
+
import tempfile
|
|
153
|
+
import threading
|
|
154
|
+
import time
|
|
155
|
+
import traceback
|
|
156
|
+
import weakref
|
|
157
|
+
|
|
158
|
+
from typing import Any, NotRequired, TypedDict
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class _Thread(TypedDict):
|
|
162
|
+
control: socket.socket
|
|
163
|
+
ended: threading.Event
|
|
164
|
+
killed: NotRequired[float]
|
|
165
|
+
thread: threading.Thread
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class Pyjo_Server_Prethread(Pyjo.Server.Prefork.object):
|
|
169
|
+
"""
|
|
170
|
+
:mod:`Pyjo.Server.Prethread` inherits all attributes and methods from
|
|
171
|
+
:mod:`Pyjo.Server.Prefork`, with ``prethread.pid`` in a temporary
|
|
172
|
+
directory for the default of :attr:`~Pyjo.Server.Prefork.Pyjo_Server_Prefork.pid_file`.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
# The signals of the manager, without CHLD, which is the one of the children of the application; Windows has INT,
|
|
176
|
+
# TERM and BREAK of Ctrl-Break, which takes the place of QUIT
|
|
177
|
+
if sys.platform == 'win32':
|
|
178
|
+
_MANAGED = (signal.SIGINT, signal.SIGTERM, signal.SIGBREAK)
|
|
179
|
+
else:
|
|
180
|
+
_MANAGED = (signal.SIGHUP, signal.SIGINT, signal.SIGTERM, signal.SIGQUIT, signal.SIGTTIN, signal.SIGTTOU)
|
|
181
|
+
|
|
182
|
+
# The seconds that a worker has to stop once it is told to stop immediately, before the manager leaves it
|
|
183
|
+
_LEAVE = 1
|
|
184
|
+
|
|
185
|
+
# The workers are threads, which need no fork
|
|
186
|
+
_FORKS = False
|
|
187
|
+
|
|
188
|
+
def __init__(self, *, pid_file: str | os.PathLike[str] | None = None, **kwargs: Any) -> None:
|
|
189
|
+
"""::
|
|
190
|
+
|
|
191
|
+
prethread = Pyjo.Server.Prethread.new()
|
|
192
|
+
prethread = Pyjo.Server.Prethread.new(listen=['http://*:8080'], workers=10)
|
|
193
|
+
|
|
194
|
+
Construct a new :mod:`Pyjo.Server.Prethread` object.
|
|
195
|
+
"""
|
|
196
|
+
super().__init__(
|
|
197
|
+
pid_file=pid_file if pid_file is not None else os.path.join(tempfile.gettempdir(), 'prethread.pid'),
|
|
198
|
+
**kwargs,
|
|
199
|
+
)
|
|
200
|
+
self._threads: dict[int, _Thread] = {}
|
|
201
|
+
self._pair: tuple[socket.socket, socket.socket] | None = None
|
|
202
|
+
|
|
203
|
+
# The heartbeats go through a socket pair in the place of a pipe, as select of Windows watches sockets only
|
|
204
|
+
def _pipe(self) -> tuple[int, int]:
|
|
205
|
+
reader, writer = self._pair = socket.socketpair()
|
|
206
|
+
return reader.fileno(), writer.fileno()
|
|
207
|
+
|
|
208
|
+
def _read_pipe(self, reader: int) -> bytes:
|
|
209
|
+
assert self._pair is not None
|
|
210
|
+
return self._pair[0].recv(4194304)
|
|
211
|
+
|
|
212
|
+
def _close_pipe(self, reader: int, writer: int) -> None:
|
|
213
|
+
if (pair := self._pair) is not None:
|
|
214
|
+
self._pair = None
|
|
215
|
+
pair[0].close()
|
|
216
|
+
pair[1].close()
|
|
217
|
+
|
|
218
|
+
def _send(self, data: bytes) -> None:
|
|
219
|
+
if (pair := self._pair) is None:
|
|
220
|
+
raise OSError('No pipe')
|
|
221
|
+
pair[1].sendall(data)
|
|
222
|
+
|
|
223
|
+
def _heartbeat(self, finished: bool) -> None:
|
|
224
|
+
try:
|
|
225
|
+
self._send(f'{threading.get_native_id()}:{int(finished)}\n'.encode())
|
|
226
|
+
except Exception:
|
|
227
|
+
Pyjo.IOLoop.singleton.stop()
|
|
228
|
+
|
|
229
|
+
def _kill_worker(self, pid: int, graceful: bool) -> bool:
|
|
230
|
+
if (t := self._threads.get(pid)) is None:
|
|
231
|
+
return False
|
|
232
|
+
if not graceful:
|
|
233
|
+
now = time.monotonic()
|
|
234
|
+
if (killed := t.get('killed')) is not None:
|
|
235
|
+
if killed + self._LEAVE > now:
|
|
236
|
+
return True
|
|
237
|
+
self.app.log.error(f'Worker {pid} does not stop, leaving it')
|
|
238
|
+
self._threads.pop(pid)['control'].close()
|
|
239
|
+
return False
|
|
240
|
+
t['killed'] = now
|
|
241
|
+
try:
|
|
242
|
+
t['control'].send(b'T' if graceful else b'Q')
|
|
243
|
+
except OSError:
|
|
244
|
+
pass
|
|
245
|
+
return True
|
|
246
|
+
|
|
247
|
+
def _delay_worker(self, pid: int) -> None:
|
|
248
|
+
if (t := self._threads.get(pid)) is None:
|
|
249
|
+
return
|
|
250
|
+
try:
|
|
251
|
+
t['control'].send(b'D')
|
|
252
|
+
except OSError:
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
# The listen sockets are left open on Windows, where the workers share the sockets of the manager, which their
|
|
256
|
+
# reactors watch until they stop, and a new connection waits for the end of the server, as in Mojolicious
|
|
257
|
+
def _close_listen(self) -> None:
|
|
258
|
+
if sys.platform != 'win32':
|
|
259
|
+
super()._close_listen()
|
|
260
|
+
|
|
261
|
+
def _spawn(self) -> None:
|
|
262
|
+
# The listen sockets of the manager, which the worker duplicates, or shares on Windows
|
|
263
|
+
loop = self.ioloop
|
|
264
|
+
listen = [
|
|
265
|
+
(location, loop.acceptor(id).handle) for location, id in zip(self.listen, self.acceptors, strict=True)
|
|
266
|
+
]
|
|
267
|
+
control, wakee = socket.socketpair()
|
|
268
|
+
ended = threading.Event()
|
|
269
|
+
thread = threading.Thread(
|
|
270
|
+
target=self._serve, args=(listen, wakee, ended), name='Pyjo.Server.Prethread', daemon=True
|
|
271
|
+
)
|
|
272
|
+
thread.start()
|
|
273
|
+
assert (id := thread.native_id) is not None
|
|
274
|
+
self._threads[id] = {'control': control, 'ended': ended, 'thread': thread}
|
|
275
|
+
self.emit('spawn', id)._pool[id] = {'time': time.monotonic()}
|
|
276
|
+
|
|
277
|
+
def _wait(self) -> None:
|
|
278
|
+
super()._wait()
|
|
279
|
+
|
|
280
|
+
# The workers that have stopped, which the last line of a worker tells, with the moment that its thread takes to
|
|
281
|
+
# end after it
|
|
282
|
+
for id, t in list(self._threads.items()):
|
|
283
|
+
if t['ended'].is_set():
|
|
284
|
+
t['thread'].join(1)
|
|
285
|
+
if t['thread'].is_alive():
|
|
286
|
+
continue
|
|
287
|
+
t['control'].close()
|
|
288
|
+
del self._threads[id]
|
|
289
|
+
self.emit('reap', id)._stopped(id)
|
|
290
|
+
|
|
291
|
+
# The worker, a copy of the server in a thread, which runs the event loop of the thread until it stops
|
|
292
|
+
def _serve(
|
|
293
|
+
self, listen: list[tuple[Any, socket.socket | None]], control: socket.socket, ended: threading.Event
|
|
294
|
+
) -> None:
|
|
295
|
+
worker = copy.copy(self)
|
|
296
|
+
loop = worker.ioloop = Pyjo.IOLoop.singleton
|
|
297
|
+
worker.acceptors, worker._connections, worker._servers = [], {}, {}
|
|
298
|
+
worker.cleanup, worker.silent = False, True
|
|
299
|
+
try:
|
|
300
|
+
# The listen sockets of the manager, duplicated, as the worker closes them. On Windows, the workers share the
|
|
301
|
+
# sockets of the manager and leave them open, as accept of a duplicate, of WSADuplicateSocket or of
|
|
302
|
+
# DuplicateHandle, can block, also on a non-blocking socket, when the duplicate of another worker accepts the
|
|
303
|
+
# connection that select reported to both
|
|
304
|
+
loop.max_accepts = self.accepts
|
|
305
|
+
if max := worker.max_clients:
|
|
306
|
+
loop.max_connections = max
|
|
307
|
+
for location, handle in listen:
|
|
308
|
+
assert handle is not None
|
|
309
|
+
worker._listen(
|
|
310
|
+
_location(location, handle.fileno() if sys.platform == 'win32' else handle.dup().detach())
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Heartbeat messages
|
|
314
|
+
finished = False
|
|
315
|
+
ref = weakref.ref(worker)
|
|
316
|
+
|
|
317
|
+
def finish(loop: Any) -> None:
|
|
318
|
+
nonlocal finished
|
|
319
|
+
finished = True
|
|
320
|
+
if (worker := ref()) is not None:
|
|
321
|
+
worker._close_listen()
|
|
322
|
+
worker.max_requests = 1
|
|
323
|
+
|
|
324
|
+
loop.on('finish', finish)
|
|
325
|
+
|
|
326
|
+
def heartbeat(loop: Any) -> None:
|
|
327
|
+
if (worker := ref()) is not None:
|
|
328
|
+
worker._heartbeat(finished)
|
|
329
|
+
|
|
330
|
+
loop.next_tick(heartbeat)
|
|
331
|
+
loop.recurring(self.heartbeat_interval, heartbeat)
|
|
332
|
+
|
|
333
|
+
# The manager stops the worker with a byte, gracefully, after the delay of the graceful shutdown, or
|
|
334
|
+
# immediately, also when it is gone
|
|
335
|
+
immediate = False
|
|
336
|
+
|
|
337
|
+
def command(reactor: Any, writable: bool) -> None:
|
|
338
|
+
nonlocal immediate
|
|
339
|
+
try:
|
|
340
|
+
chunk = control.recv(4096)
|
|
341
|
+
except OSError:
|
|
342
|
+
return
|
|
343
|
+
if not chunk or b'Q' in chunk:
|
|
344
|
+
immediate = True
|
|
345
|
+
loop.stop()
|
|
346
|
+
elif b'T' in chunk:
|
|
347
|
+
loop.stop_gracefully()
|
|
348
|
+
elif (worker := ref()) is not None:
|
|
349
|
+
loop.stop_gracefully(worker.shutdown_delay)
|
|
350
|
+
|
|
351
|
+
loop.reactor.io(control, command).watch(control, True, False)
|
|
352
|
+
|
|
353
|
+
self.app.log.info(f'Worker {threading.get_native_id()} started')
|
|
354
|
+
loop.start()
|
|
355
|
+
|
|
356
|
+
# Not in Mojolicious: the end of the server in the thread of the worker, unless it has stopped immediately
|
|
357
|
+
if not immediate:
|
|
358
|
+
worker._server_stopped()
|
|
359
|
+
except BaseException:
|
|
360
|
+
traceback.print_exc()
|
|
361
|
+
finally:
|
|
362
|
+
loop.reactor.remove(control)
|
|
363
|
+
control.close()
|
|
364
|
+
if sys.platform == 'win32':
|
|
365
|
+
for acceptor in [*(loop.acceptor(id) for id in worker.acceptors), *worker._servers.values()]:
|
|
366
|
+
if acceptor is not None and (handle := acceptor.handle) is not None:
|
|
367
|
+
acceptor.stop()
|
|
368
|
+
handle.detach()
|
|
369
|
+
worker._cleanup()
|
|
370
|
+
loop.reset()
|
|
371
|
+
|
|
372
|
+
# The manager is woken up, to reap the worker at once
|
|
373
|
+
ended.set()
|
|
374
|
+
try:
|
|
375
|
+
self._send(b'\n')
|
|
376
|
+
except OSError:
|
|
377
|
+
pass
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# A listen location for the duplicate of a listen socket, or the socket on Windows, without the port and the path that
|
|
381
|
+
# would find the socket of the manager in PYJO_REUSE
|
|
382
|
+
def _location(location: Any, fd: int) -> str:
|
|
383
|
+
url = Pyjo.URL.new(str(location))
|
|
384
|
+
if url.protocol.endswith('+unix'):
|
|
385
|
+
url.host = None
|
|
386
|
+
else:
|
|
387
|
+
url.port = None
|
|
388
|
+
url.query.append(fd=fd)
|
|
389
|
+
return str(url)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
new = Pyjo_Server_Prethread.new
|
|
393
|
+
object = Pyjo_Server_Prethread
|
|
394
|
+
Prethread = Pyjo_Server_Prethread
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.Reload.Backend.Poll - Auto-restarting development server backend using stat
|
|
3
|
+
=======================================================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.Reload.Backend.Poll import Poll
|
|
7
|
+
|
|
8
|
+
backend = Poll()
|
|
9
|
+
if files := backend.modified_files():
|
|
10
|
+
print('Files have been changed')
|
|
11
|
+
|
|
12
|
+
# Or import the module, as in Google Python Style Guide
|
|
13
|
+
|
|
14
|
+
import Pyjo.Server.Reload.Backend.Poll
|
|
15
|
+
|
|
16
|
+
backend = Pyjo.Server.Reload.Backend.Poll.new()
|
|
17
|
+
if files := backend.modified_files():
|
|
18
|
+
print('Files have been changed')
|
|
19
|
+
|
|
20
|
+
:mod:`Pyjo.Server.Reload.Backend.Poll` is a backend for
|
|
21
|
+
:mod:`Pyjo.Server.Reload` that checks the size and the modification time
|
|
22
|
+
of the files with :func:`os.stat`, and waits for
|
|
23
|
+
:attr:`~Pyjo.Server.Reload.Backend.Pyjo_Server_Reload_Backend.watch_timeout`
|
|
24
|
+
when none has changed.
|
|
25
|
+
|
|
26
|
+
See also
|
|
27
|
+
--------
|
|
28
|
+
|
|
29
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
30
|
+
|
|
31
|
+
Ported from `Mojo::Server::Morbo::Backend::Poll <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/Morbo/Backend/Poll.pm>`_
|
|
32
|
+
of Mojolicious 9.49.
|
|
33
|
+
|
|
34
|
+
Classes
|
|
35
|
+
-------
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import Pyjo.File
|
|
39
|
+
import Pyjo.Server.Reload.Backend
|
|
40
|
+
|
|
41
|
+
import os
|
|
42
|
+
import time
|
|
43
|
+
|
|
44
|
+
from typing import Any, cast
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# The time the program started, as Perl's $^T
|
|
48
|
+
_START = time.time()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Renamed from Mojolicious: Mojo::Server::Morbo::Backend::Poll, as the names of Futurama are not ours
|
|
52
|
+
class Pyjo_Server_Reload_Backend_Poll(Pyjo.Server.Reload.Backend.object):
|
|
53
|
+
"""
|
|
54
|
+
:mod:`Pyjo.Server.Reload.Backend.Poll` inherits all attributes and methods
|
|
55
|
+
from :mod:`Pyjo.Server.Reload.Backend` and implements the following new
|
|
56
|
+
ones.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
60
|
+
"""::
|
|
61
|
+
|
|
62
|
+
backend = Pyjo.Server.Reload.Backend.Poll.new()
|
|
63
|
+
|
|
64
|
+
Construct a new :mod:`Pyjo.Server.Reload.Backend.Poll` object.
|
|
65
|
+
"""
|
|
66
|
+
super().__init__(**kwargs)
|
|
67
|
+
self._cache: dict[str, list[float]] = {}
|
|
68
|
+
|
|
69
|
+
def modified_files(self) -> list[str]:
|
|
70
|
+
"""::
|
|
71
|
+
|
|
72
|
+
files = backend.modified_files()
|
|
73
|
+
|
|
74
|
+
Check file size and mtime to determine which files have changed, this
|
|
75
|
+
is not particularly efficient, but very robust.
|
|
76
|
+
"""
|
|
77
|
+
cache = self._cache
|
|
78
|
+
known = set(cache)
|
|
79
|
+
files: list[str] = []
|
|
80
|
+
for file in (f for watch in self.watch for f in _files(os.fspath(cast(str, watch)))):
|
|
81
|
+
known.discard(file)
|
|
82
|
+
try:
|
|
83
|
+
stat = os.stat(file)
|
|
84
|
+
except OSError:
|
|
85
|
+
continue
|
|
86
|
+
stats = cache.setdefault(file, [_START, stat.st_size])
|
|
87
|
+
if stat.st_mtime <= stats[0] and stat.st_size == stats[1]:
|
|
88
|
+
continue
|
|
89
|
+
stats[:] = [stat.st_mtime, stat.st_size]
|
|
90
|
+
files.append(file)
|
|
91
|
+
|
|
92
|
+
# Any leftover file means they were deleted
|
|
93
|
+
for file in sorted(known):
|
|
94
|
+
files.append(file)
|
|
95
|
+
del cache[file]
|
|
96
|
+
|
|
97
|
+
if not len(files):
|
|
98
|
+
time.sleep(self.watch_timeout)
|
|
99
|
+
|
|
100
|
+
return files
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# A readable file, or the files of a directory
|
|
104
|
+
def _files(path: str) -> list[str]:
|
|
105
|
+
if os.path.isfile(path) and os.access(path, os.R_OK):
|
|
106
|
+
return [path]
|
|
107
|
+
# Not in Mojolicious: the bytecode that Python writes to __pycache__ when the server imports a module that it watches,
|
|
108
|
+
# which is not a change of a source, and would restart the server once after each start
|
|
109
|
+
return [file.to_str() for file in Pyjo.File.new(path).list_tree() if '__pycache__' not in file.to_list()]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
new = Pyjo_Server_Reload_Backend_Poll.new
|
|
113
|
+
object = Pyjo_Server_Reload_Backend_Poll
|
|
114
|
+
Poll = Pyjo_Server_Reload_Backend_Poll
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.Reload.Backend - Auto-restarting development server backend
|
|
3
|
+
=======================================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.Reload.Backend import Backend
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Pyjo_Server_Reload_Backend_Inotify(Backend):
|
|
10
|
+
def modified_files(self):
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
new = Pyjo_Server_Reload_Backend_Inotify.new
|
|
15
|
+
object = Pyjo_Server_Reload_Backend_Inotify
|
|
16
|
+
|
|
17
|
+
# Or import the module, as in Google Python Style Guide
|
|
18
|
+
|
|
19
|
+
import Pyjo.Server.Reload.Backend
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Pyjo_Server_Reload_Backend_Inotify(Pyjo.Server.Reload.Backend.object):
|
|
23
|
+
def modified_files(self):
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
new = Pyjo_Server_Reload_Backend_Inotify.new
|
|
28
|
+
object = Pyjo_Server_Reload_Backend_Inotify
|
|
29
|
+
|
|
30
|
+
:mod:`Pyjo.Server.Reload.Backend` is an abstract base class for
|
|
31
|
+
auto-restarting development server backends used by
|
|
32
|
+
:mod:`Pyjo.Server.Reload`.
|
|
33
|
+
|
|
34
|
+
A backend is a module named ``Pyjo.Server.Reload.Backend.<Name>``, with a
|
|
35
|
+
function ``new`` that constructs it and the class as ``object``, which
|
|
36
|
+
another distribution can add, as :func:`pkgutil.extend_path` keeps the
|
|
37
|
+
namespace open, and the ``PYJO_RELOAD_BACKEND`` environment variable selects
|
|
38
|
+
it.
|
|
39
|
+
|
|
40
|
+
See also
|
|
41
|
+
--------
|
|
42
|
+
|
|
43
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
44
|
+
|
|
45
|
+
Ported from `Mojo::Server::Morbo::Backend <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/Morbo/Backend.pm>`_
|
|
46
|
+
of Mojolicious 9.49.
|
|
47
|
+
|
|
48
|
+
Classes
|
|
49
|
+
-------
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
import Pyjo.Base
|
|
53
|
+
|
|
54
|
+
import os
|
|
55
|
+
|
|
56
|
+
from pkgutil import extend_path
|
|
57
|
+
from typing import Any
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Not in Mojolicious: a namespace that other distributions can add backends to
|
|
61
|
+
__path__ = extend_path(__path__, __name__)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Renamed from Mojolicious: Mojo::Server::Morbo::Backend, as the names of Futurama are not ours
|
|
65
|
+
class Pyjo_Server_Reload_Backend(Pyjo.Base.object):
|
|
66
|
+
"""
|
|
67
|
+
:mod:`Pyjo.Server.Reload.Backend` inherits all methods from
|
|
68
|
+
:mod:`Pyjo.Base` and implements the following new ones.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
def __init__(self, *, watch: list[Any] | None = None, watch_timeout: float | None = None) -> None:
|
|
72
|
+
"""::
|
|
73
|
+
|
|
74
|
+
backend = Pyjo.Server.Reload.Backend.new()
|
|
75
|
+
backend = Pyjo.Server.Reload.Backend.new(watch=['lib'], watch_timeout=2)
|
|
76
|
+
|
|
77
|
+
Construct a new :mod:`Pyjo.Server.Reload.Backend` object.
|
|
78
|
+
"""
|
|
79
|
+
super().__init__()
|
|
80
|
+
|
|
81
|
+
self.watch: list[Any] = watch if watch is not None else ['lib', 'templates']
|
|
82
|
+
"""::
|
|
83
|
+
|
|
84
|
+
watch = backend.watch
|
|
85
|
+
backend.watch = ['/home/sri/my_app']
|
|
86
|
+
|
|
87
|
+
Files and directories to watch for changes, defaults to the application
|
|
88
|
+
script as well as the ``lib`` and ``templates`` directories in the
|
|
89
|
+
current working directory.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
self._watch_timeout: float | None = watch_timeout
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def watch_timeout(self) -> float:
|
|
96
|
+
"""::
|
|
97
|
+
|
|
98
|
+
timeout = backend.watch_timeout
|
|
99
|
+
backend.watch_timeout = 10
|
|
100
|
+
|
|
101
|
+
Maximum amount of time in seconds a backend may block when waiting for
|
|
102
|
+
files to change, defaults to the value of the ``PYJO_RELOAD_TIMEOUT``
|
|
103
|
+
environment variable or ``1``.
|
|
104
|
+
"""
|
|
105
|
+
if self._watch_timeout is None:
|
|
106
|
+
self._watch_timeout = float(os.environ.get('PYJO_RELOAD_TIMEOUT') or 1)
|
|
107
|
+
return self._watch_timeout
|
|
108
|
+
|
|
109
|
+
@watch_timeout.setter
|
|
110
|
+
def watch_timeout(self, value: float) -> None:
|
|
111
|
+
self._watch_timeout = value
|
|
112
|
+
|
|
113
|
+
def modified_files(self) -> list[str]:
|
|
114
|
+
"""::
|
|
115
|
+
|
|
116
|
+
files = backend.modified_files()
|
|
117
|
+
|
|
118
|
+
Check if files from :attr:`watch` have been modified since the last
|
|
119
|
+
check and return their names. Meant to be overloaded in a subclass.
|
|
120
|
+
"""
|
|
121
|
+
raise NotImplementedError('Method "modified_files" not implemented by subclass')
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
new = Pyjo_Server_Reload_Backend.new
|
|
125
|
+
object = Pyjo_Server_Reload_Backend
|
|
126
|
+
Backend = Pyjo_Server_Reload_Backend
|