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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/Server/Prefork.py ADDED
@@ -0,0 +1,917 @@
1
+ """
2
+ Pyjo.Server.Prefork - Pre-forking non-blocking I/O HTTP and WebSocket server
3
+ ============================================================================
4
+ ::
5
+
6
+ from Pyjo.Server.Prefork import Prefork
7
+
8
+ prefork = Prefork(listen=['http://*:8080'])
9
+
10
+
11
+ @prefork.unsubscribe('request').on('request')
12
+ def request(prefork, 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
+ prefork.run()
27
+
28
+ # Or import the module, as in Google Python Style Guide
29
+
30
+ import Pyjo.Server.Prefork
31
+
32
+ prefork = Pyjo.Server.Prefork.new(listen=['http://*:8080'])
33
+
34
+
35
+ @prefork.unsubscribe('request').on('request')
36
+ def request(prefork, 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
+ prefork.run()
51
+
52
+ :mod:`Pyjo.Server.Prefork` is a full featured, UNIX optimized, pre-forking
53
+ non-blocking I/O HTTP and WebSocket server, built around the very well tested
54
+ and reliable :mod:`Pyjo.Server.Daemon`, with IPv6, TLS, SNI, UNIX domain
55
+ socket, Comet (long polling), keep-alive and multiple event loop support.
56
+ Note that the server uses signals for process management, so you should avoid
57
+ modifying signal handlers in your applications.
58
+
59
+ The workers are forks of the manager, made with :func:`os.fork`, so the
60
+ server runs where :func:`os.fork` exists, not on Windows, where
61
+ :mod:`Pyjo.Server.Prethread` has the workers in threads. Name resolution in
62
+ a thread, SOCKS5 and TLS support can be disabled with the ``PYJO_NO_NNR``,
63
+ ``PYJO_NO_SOCKS`` and ``PYJO_NO_TLS`` environment variables.
64
+
65
+ Manager signals
66
+ ---------------
67
+
68
+ The :mod:`Pyjo.Server.Prefork` manager process can be controlled at runtime
69
+ with the following signals.
70
+
71
+ INT, TERM
72
+ ~~~~~~~~~
73
+
74
+ Shut down server gracefully. The manager and the workers close the listen
75
+ sockets, so that a new connection is refused, and the workers finish the
76
+ accepted ones, after :attr:`~Pyjo.Server.Daemon.Pyjo_Server_Daemon.shutdown_delay`,
77
+ while the workers serve as before, which the manager tells them with ``USR1``.
78
+ A second ``INT``, as of Ctrl-C pressed twice, shuts it down immediately, also
79
+ during the delay, a second ``TERM`` does nothing.
80
+
81
+ QUIT
82
+ ~~~~
83
+
84
+ Shut down server immediately.
85
+
86
+ HUP
87
+ ~~~
88
+
89
+ Restart the workers gracefully, or emit the event ``reload`` when it has
90
+ subscribers.
91
+
92
+ TTIN
93
+ ~~~~
94
+
95
+ Increase worker pool by one.
96
+
97
+ TTOU
98
+ ~~~~
99
+
100
+ Decrease worker pool by one.
101
+
102
+ Worker signals
103
+ --------------
104
+
105
+ :mod:`Pyjo.Server.Prefork` worker processes can be controlled at runtime
106
+ with the following signals.
107
+
108
+ TERM
109
+ ~~~~
110
+
111
+ Stop worker gracefully, with its copies of the listen sockets closed.
112
+
113
+ USR1
114
+ ~~~~
115
+
116
+ Stop worker gracefully after
117
+ :attr:`~Pyjo.Server.Daemon.Pyjo_Server_Daemon.shutdown_delay`, while it
118
+ serves as before.
119
+
120
+ QUIT
121
+ ~~~~
122
+
123
+ Stop worker immediately.
124
+
125
+ The workers ignore ``INT``, which Ctrl-C sends to all the processes of the
126
+ server, and ``HUP``, as the manager stops them.
127
+
128
+ Events
129
+ ------
130
+
131
+ :mod:`Pyjo.Server.Prefork` inherits all events from :mod:`Pyjo.Server.Daemon`
132
+ and can emit the following new ones.
133
+
134
+ finish
135
+ ~~~~~~
136
+ ::
137
+
138
+ prefork.on('finish', lambda prefork, graceful: ...)
139
+
140
+ Emitted when the server shuts down. ::
141
+
142
+ prefork.on('finish', lambda prefork, graceful: print('Graceful server shutdown' if graceful else 'Server shutdown'))
143
+
144
+ heartbeat
145
+ ~~~~~~~~~
146
+ ::
147
+
148
+ prefork.on('heartbeat', lambda prefork, pid: ...)
149
+
150
+ Emitted when a heartbeat message has been received from a worker. ::
151
+
152
+ prefork.on('heartbeat', lambda prefork, pid: print(f'Worker {pid} has a heartbeat'))
153
+
154
+ reap
155
+ ~~~~
156
+ ::
157
+
158
+ prefork.on('reap', lambda prefork, pid: ...)
159
+
160
+ Emitted when a child process exited. ::
161
+
162
+ prefork.on('reap', lambda prefork, pid: print(f'Worker {pid} stopped'))
163
+
164
+ reload
165
+ ~~~~~~
166
+ ::
167
+
168
+ prefork.on('reload', lambda prefork: ...)
169
+
170
+ Emitted when the manager gets the signal ``HUP``, in the place of the restart
171
+ of the workers. ::
172
+
173
+ prefork.on('reload', lambda prefork: print('Reloading the configuration'))
174
+
175
+ spawn
176
+ ~~~~~
177
+ ::
178
+
179
+ prefork.on('spawn', lambda prefork, pid: ...)
180
+
181
+ Emitted when a worker process is spawned. ::
182
+
183
+ prefork.on('spawn', lambda prefork, pid: print(f'Worker {pid} started'))
184
+
185
+ wait
186
+ ~~~~
187
+ ::
188
+
189
+ prefork.on('wait', lambda prefork: ...)
190
+
191
+ Emitted when the manager starts waiting for new heartbeat messages. ::
192
+
193
+ prefork.on('wait', lambda prefork: print(f'Waiting for heartbeat messages from {prefork.workers} workers'))
194
+
195
+ See also
196
+ --------
197
+
198
+ :mod:`Pyjoyment`, https://mojolicious.org.
199
+
200
+ Ported from `Mojo::Server::Prefork <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/Prefork.pm>`_
201
+ of Mojolicious 9.49.
202
+
203
+ Classes
204
+ -------
205
+ """
206
+
207
+ import Pyjo.Exception
208
+ import Pyjo.File
209
+ import Pyjo.Server.Daemon
210
+
211
+ import os
212
+ import re
213
+ import select
214
+ import signal
215
+ import socket
216
+ import sys
217
+ import tempfile
218
+ import time
219
+ import traceback
220
+ import weakref
221
+
222
+ from typing import Any, NoReturn, TypedDict
223
+
224
+
225
+ class _Worker(TypedDict, total=False):
226
+ force: bool
227
+ graceful: float | None
228
+ delayed: bool
229
+ healthy: bool
230
+ quit: int
231
+ time: float
232
+
233
+
234
+ class Pyjo_Server_Prefork(Pyjo.Server.Daemon.object):
235
+ """
236
+ :mod:`Pyjo.Server.Prefork` inherits all methods from
237
+ :mod:`Pyjo.Server.Daemon` and implements the following new ones.
238
+ """
239
+
240
+ # Not in Mojolicious: the workers are forks of the manager, which Pyjo.Server.Prethread changes
241
+ _FORKS = True
242
+
243
+ # Not in Mojolicious: the signals of the manager, as a class attribute, which Pyjo.Server.Prethread changes; Windows
244
+ # has INT, TERM and BREAK of Ctrl-Break, which takes the place of QUIT
245
+ if sys.platform == 'win32':
246
+ _MANAGED: tuple[signal.Signals, ...] = (signal.SIGINT, signal.SIGTERM, signal.SIGBREAK)
247
+ else:
248
+ _MANAGED = (
249
+ signal.SIGCHLD,
250
+ signal.SIGHUP,
251
+ signal.SIGINT,
252
+ signal.SIGTERM,
253
+ signal.SIGQUIT,
254
+ signal.SIGTTIN,
255
+ signal.SIGTTOU,
256
+ )
257
+
258
+ def __init__(
259
+ self,
260
+ *,
261
+ accepts: int = 10000,
262
+ cleanup: bool = True,
263
+ graceful_timeout: float = 25,
264
+ heartbeat_interval: float = 5,
265
+ heartbeat_timeout: float = 50,
266
+ pid_file: str | os.PathLike[str] | None = None,
267
+ spare: int = 2,
268
+ workers: int = 4,
269
+ **kwargs: Any,
270
+ ) -> None:
271
+ """::
272
+
273
+ prefork = Pyjo.Server.Prefork.new()
274
+ prefork = Pyjo.Server.Prefork.new(listen=['http://*:8080'], workers=10)
275
+
276
+ Construct a new :mod:`Pyjo.Server.Prefork` object.
277
+ """
278
+ super().__init__(**kwargs)
279
+
280
+ self.accepts = accepts
281
+ """::
282
+
283
+ accepts = prefork.accepts
284
+ prefork.accepts = 100
285
+
286
+ Maximum number of connections a worker is allowed to accept, before
287
+ stopping gracefully and then getting replaced with a newly started
288
+ worker, passed along to :attr:`Pyjo.IOLoop.Pyjo_IOLoop.max_accepts`,
289
+ defaults to ``10000``. Setting the value to ``0`` will allow workers to
290
+ accept new connections indefinitely. Note that up to half of this value
291
+ can be subtracted randomly to improve load balancing, and to make sure
292
+ that not all workers restart at the same time.
293
+ """
294
+
295
+ self.cleanup = cleanup
296
+ """::
297
+
298
+ boolean = prefork.cleanup
299
+ prefork.cleanup = False
300
+
301
+ Delete :attr:`pid_file` automatically once it is not needed anymore,
302
+ defaults to a true value.
303
+ """
304
+
305
+ # Unlike Mojolicious: 25 seconds, where Mojo has 120, below the 30 seconds that Kubernetes gives a container to
306
+ # stop, so that the server forces its workers and stops by itself before it is killed
307
+ self.graceful_timeout = graceful_timeout
308
+ """::
309
+
310
+ timeout = prefork.graceful_timeout
311
+ prefork.graceful_timeout = 15
312
+
313
+ Maximum amount of time in seconds stopping a worker gracefully may take
314
+ before being forced, defaults to ``25``, less than the 30 seconds that
315
+ Kubernetes gives a container to stop. Note that this value should
316
+ usually be a little larger than the maximum amount of time you expect
317
+ any one request to take.
318
+ """
319
+
320
+ self.heartbeat_interval = heartbeat_interval
321
+ """::
322
+
323
+ interval = prefork.heartbeat_interval
324
+ prefork.heartbeat_interval = 3
325
+
326
+ Heartbeat interval in seconds, defaults to ``5``.
327
+ """
328
+
329
+ self.heartbeat_timeout = heartbeat_timeout
330
+ """::
331
+
332
+ timeout = prefork.heartbeat_timeout
333
+ prefork.heartbeat_timeout = 2
334
+
335
+ Maximum amount of time in seconds before a worker without a heartbeat
336
+ will be stopped gracefully, defaults to ``50``. Note that this value
337
+ should usually be a little larger than the maximum amount of time you
338
+ expect any one operation to block the event loop.
339
+ """
340
+
341
+ self.pid_file: str | os.PathLike[str] = (
342
+ pid_file if pid_file is not None else os.path.join(tempfile.gettempdir(), 'prefork.pid')
343
+ )
344
+ """::
345
+
346
+ file = prefork.pid_file
347
+ prefork.pid_file = '/tmp/prefork.pid'
348
+
349
+ Full path of process id file, defaults to ``prefork.pid`` in a
350
+ temporary directory.
351
+ """
352
+
353
+ self.spare = spare
354
+ """::
355
+
356
+ spare = prefork.spare
357
+ prefork.spare = 4
358
+
359
+ Temporarily spawn up to this number of additional workers if there is a
360
+ need, defaults to ``2``. This allows for new workers to be started while
361
+ old ones are still shutting down gracefully, drastically reducing the
362
+ performance cost of worker restarts.
363
+ """
364
+
365
+ self.workers = workers
366
+ """::
367
+
368
+ workers = prefork.workers
369
+ prefork.workers = 10
370
+
371
+ Number of worker processes, defaults to ``4``. A good rule of thumb is
372
+ two worker processes per CPU core for applications that perform mostly
373
+ non-blocking operations, blocking operations often require more and
374
+ benefit from decreasing concurrency with
375
+ :attr:`Pyjo.Server.Daemon.Pyjo_Server_Daemon.max_clients` (often as
376
+ low as ``1``).
377
+ """
378
+
379
+ self._delayed: float | None = None
380
+ self._finished = False
381
+ self._graceful = False
382
+ self._pool: dict[int, _Worker] = {}
383
+ self._reader: int | None = None
384
+ self._running = False
385
+ self._signals: list[int] = []
386
+ self._wakee: socket.socket | None = None
387
+ self._writer: int | None = None
388
+
389
+ def check_pid(self) -> int | None:
390
+ """::
391
+
392
+ pid = prefork.check_pid()
393
+
394
+ Get process id for running server from :attr:`pid_file` or delete it if
395
+ server is not running. ::
396
+
397
+ if not prefork.check_pid():
398
+ print('Server is not running')
399
+ """
400
+ file = Pyjo.File.new(self.pid_file)
401
+ if not os.access(file, os.R_OK):
402
+ return None
403
+ try:
404
+ pid = int((file.slurp() or b'').strip())
405
+ except ValueError:
406
+ pid = 0
407
+
408
+ # Running
409
+ if pid and _alive(pid):
410
+ return pid
411
+
412
+ # Not running
413
+ file.remove()
414
+ return None
415
+
416
+ def ensure_pid_file(self, pid: int) -> None:
417
+ """::
418
+
419
+ prefork.ensure_pid_file(pid)
420
+
421
+ Ensure :attr:`pid_file` exists.
422
+ """
423
+ # Check if PID file already exists
424
+ file = Pyjo.File.new(self.pid_file)
425
+ if os.path.exists(file):
426
+ return
427
+
428
+ # Create PID file
429
+ try:
430
+ file.spew(f'{pid}\n'.encode()).chmod(0o644)
431
+ except Exception as e:
432
+ err = f'Can\'t create process id file "{file}": {e}'
433
+ self.app.log.error(err)
434
+ raise Pyjo.Exception.new(err) from e
435
+ self.app.log.info(f'Creating process id file "{file}"')
436
+
437
+ @property
438
+ def healthy(self) -> int:
439
+ """::
440
+
441
+ healthy = prefork.healthy
442
+
443
+ Number of currently active worker processes with a heartbeat.
444
+ """
445
+ return sum(1 for w in self._pool.values() if w.get('healthy'))
446
+
447
+ def run(self) -> None:
448
+ """::
449
+
450
+ prefork.run()
451
+
452
+ Run server and wait for the manager signals.
453
+ """
454
+ # Unlike Mojolicious: no fork at all, where Mojo has no fork emulation of Perl on Windows
455
+ if self._FORKS and not hasattr(os, 'fork'):
456
+ print('Pre-forking needs os.fork.')
457
+ sys.exit(0)
458
+
459
+ # Pipe for worker communication
460
+ try:
461
+ reader, writer = self._reader, self._writer = self._pipe()
462
+ except OSError as e:
463
+ raise OSError(e.errno, f"Can't create pipe: {e.strerror}") from e
464
+
465
+ # Clean manager environment
466
+ # Not in Mojolicious: a handler only notes the signal, which the manager handles where it waits for
467
+ # heartbeats, as Perl defers the handlers of signals to a safe point, and a socket pair that Python writes to
468
+ # on a signal wakes the manager up, as Python repeats a system call that a signal interrupts until its timeout
469
+ def note(signum: int, frame: Any) -> None:
470
+ self._signals.append(signum)
471
+
472
+ # Not in Mojolicious: local %SIG, the handlers of the signals are set back at the end
473
+ handlers: dict[signal.Signals, Any] = {}
474
+ for name in self._MANAGED:
475
+ handlers[name] = signal.signal(name, note)
476
+ waker, self._wakee = socket.socketpair()
477
+ waker.setblocking(False)
478
+ self._wakee.setblocking(False)
479
+ wakeup = signal.set_wakeup_fd(waker.fileno())
480
+
481
+ try:
482
+ # Preload application before starting workers
483
+ self.start().app.log.info(f'Manager {os.getpid()} started')
484
+ self.ioloop.max_accepts = self.accepts
485
+ self._running = True
486
+ while self._running:
487
+ self._manage()
488
+ self.app.log.info(f'Manager {os.getpid()} stopped')
489
+
490
+ # Not in Mojolicious: the end of the server in the manager, which has started it, once the workers have
491
+ # stopped, unless it has stopped immediately
492
+ if self._graceful:
493
+ self._server_stopped()
494
+ finally:
495
+ signal.set_wakeup_fd(wakeup)
496
+
497
+ # Not in Mojolicious: the signals are blocked while the handlers are set back, so that a signal that comes
498
+ # in between is not left to a handler that is gone, which Python reports as a race condition; Windows
499
+ # blocks no signal
500
+ if sys.platform == 'win32':
501
+ for name, handler in handlers.items():
502
+ signal.signal(name, handler)
503
+ else:
504
+ signal.pthread_sigmask(signal.SIG_BLOCK, handlers)
505
+ try:
506
+ for name, handler in handlers.items():
507
+ signal.signal(name, handler)
508
+ finally:
509
+ signal.pthread_sigmask(signal.SIG_UNBLOCK, handlers)
510
+
511
+ # Not in Mojolicious: the pipe is closed at the end, as Perl closes it with the object
512
+ self._close_pipe(reader, writer)
513
+ waker.close()
514
+ self._wakee.close()
515
+ self._reader = self._writer = self._wakee = None
516
+
517
+ # Not in Mojolicious: the pipe of the heartbeats in methods of their own, which Pyjo.Server.Prethread changes
518
+ def _pipe(self) -> tuple[int, int]:
519
+ return os.pipe()
520
+
521
+ def _read_pipe(self, reader: int) -> bytes:
522
+ return os.read(reader, 4194304)
523
+
524
+ def _close_pipe(self, reader: int, writer: int) -> None:
525
+ os.close(reader)
526
+ os.close(writer)
527
+
528
+ # DESTROY: the process id file is deleted with the object, after the listen sockets of the daemon are closed
529
+ def _cleanup(self) -> None:
530
+ super()._cleanup()
531
+ if self.cleanup:
532
+ Pyjo.File.new(self.pid_file).remove()
533
+
534
+ def _heartbeat(self, finished: bool) -> None:
535
+ try:
536
+ assert self._writer is not None
537
+ os.write(self._writer, f'{os.getpid()}:{int(finished)}\n'.encode())
538
+ except Exception:
539
+ _exit(0)
540
+
541
+ def _manage(self) -> None:
542
+ # Not in Mojolicious: the delay of a graceful shutdown, which the workers are told, also the ones that start in it
543
+ if (delayed := self._delayed) is not None and not self._finished:
544
+ if delayed <= time.monotonic():
545
+ self._term(True)
546
+ else:
547
+ for pid, w in self._pool.items():
548
+ if not w.get('delayed'):
549
+ w['delayed'] = True
550
+ self._delay_worker(pid)
551
+
552
+ # Spawn more workers if necessary and check PID file
553
+ if not self._finished:
554
+ graceful = sum(1 for w in self._pool.values() if w.get('graceful'))
555
+ spare = self.spare
556
+ spare = (spare if graceful > spare else graceful) if graceful else 0
557
+ need = (self.workers - len(self._pool)) + spare
558
+ while need > 0:
559
+ self._spawn()
560
+ need -= 1
561
+ self.ensure_pid_file(os.getpid())
562
+
563
+ # Shutdown
564
+ elif not len(self._pool):
565
+ self._running = False
566
+ return
567
+
568
+ # Wait for heartbeats
569
+ self._wait()
570
+
571
+ interval = self.heartbeat_interval
572
+ ht = self.heartbeat_timeout
573
+ gt = self.graceful_timeout
574
+ log = self.app.log
575
+ now = time.monotonic()
576
+
577
+ for pid in list(self._pool):
578
+ if (w := self._pool.get(pid)) is None:
579
+ continue
580
+
581
+ # No heartbeat (graceful stop)
582
+ if not w.get('graceful') and w.get('time', now) + interval + ht <= now:
583
+ log.error(f'Worker {pid} has no heartbeat ({_number(ht)} seconds), restarting (see FAQ for more)')
584
+ w['graceful'] = now
585
+
586
+ # Graceful stop with timeout
587
+ graceful = w['graceful'] = w.get('graceful') or (now if self._graceful else None)
588
+ if graceful:
589
+ quit = w.get('quit', 0)
590
+ w['quit'] = quit + 1
591
+ if not quit:
592
+ log.info(f'Stopping worker {pid} gracefully ({_number(gt)} seconds)')
593
+ if not self._kill_worker(pid, True):
594
+ self._stopped(pid)
595
+ if graceful and graceful + gt <= now:
596
+ w['force'] = True
597
+
598
+ # Normal stop
599
+ if w.get('force') or (self._finished and not graceful):
600
+ log.warn(f'Stopping worker {pid} immediately')
601
+ if not self._kill_worker(pid, False):
602
+ self._stopped(pid)
603
+
604
+ # Not in Mojolicious: a worker is stopped in a method of its own, which Pyjo.Server.Prethread does without signals;
605
+ # true if the worker has been told to stop
606
+ def _kill_worker(self, pid: int, graceful: bool) -> bool:
607
+ # Unlike Mojolicious: TERM stops a worker gracefully, where Mojo sends QUIT
608
+ return _kill(signal.SIGTERM if graceful else signal.SIGKILL, pid)
609
+
610
+ # Not in Mojolicious: a worker is told the delay of a graceful shutdown, in a method of its own, which
611
+ # Pyjo.Server.Prethread does without signals
612
+ def _delay_worker(self, pid: int) -> None:
613
+ if sys.platform != 'win32':
614
+ _kill(signal.SIGUSR1, pid)
615
+
616
+ # The signals that the handlers noted, as the handlers of Mojo do them
617
+ def _signal(self) -> None:
618
+ while len(self._signals):
619
+ signum = self._signals.pop(0)
620
+ if sys.platform != 'win32' and signum == signal.SIGCHLD:
621
+ while True:
622
+ try:
623
+ pid, _ = os.waitpid(-1, os.WNOHANG)
624
+ except ChildProcessError:
625
+ break
626
+ if pid <= 0:
627
+ break
628
+ self.emit('reap', pid)._stopped(pid)
629
+ # Unlike Mojolicious: INT and TERM shut the server down gracefully, as Docker and Kubernetes stop it with TERM,
630
+ # and QUIT immediately, where Mojo does it the other way round, as nginx and unicorn do; a second INT, as of
631
+ # Ctrl-C pressed twice, shuts it down immediately, also during the delay
632
+ elif signum in (signal.SIGINT, signal.SIGTERM):
633
+ if not self._finished and self._delayed is None:
634
+ self._shutdown()
635
+ elif signum == signal.SIGINT:
636
+ self._term()
637
+ elif signum == _QUIT:
638
+ self._term()
639
+ # Not in Mojolicious: HUP restarts the workers gracefully, as gunicorn does, unless something else reloads;
640
+ # Windows has no HUP, TTIN and TTOU
641
+ elif sys.platform == 'win32':
642
+ continue
643
+ elif signum == signal.SIGHUP:
644
+ self._reload()
645
+ elif signum == signal.SIGTTIN:
646
+ self.workers += 1
647
+ elif signum == signal.SIGTTOU and self.workers > 0:
648
+ self.workers -= 1
649
+ for w in self._pool.values():
650
+ if not w.get('graceful'):
651
+ w['graceful'] = time.monotonic()
652
+ break
653
+
654
+ def _reload(self) -> None:
655
+ if self.has_subscribers('reload'):
656
+ self.emit('reload')
657
+ return
658
+ self.app.log.info('Restarting workers gracefully')
659
+ now = time.monotonic()
660
+ for w in self._pool.values():
661
+ w['graceful'] = w.get('graceful') or now
662
+
663
+ # Not in Mojolicious: the graceful shutdown after its delay, while the workers serve as before
664
+ def _shutdown(self) -> None:
665
+ if (delay := self.shutdown_delay) <= 0:
666
+ return self._term(True)
667
+ self.app.log.info(f'Stopping gracefully in {_number(delay)} seconds')
668
+ self._delayed = time.monotonic() + delay
669
+
670
+ def _spawn(self) -> None:
671
+ # Manager
672
+ # Not in Mojolicious: the buffers of the standard streams are written before, so that the worker does not write
673
+ # them again
674
+ _flush()
675
+ # Unlike Mojolicious: TERM and QUIT are blocked while the manager forks, until the worker has its handlers, where
676
+ # the worker of Mojo takes them with the handlers of the manager and serves on, and Python clears a signal that
677
+ # comes during the fork
678
+ signal.pthread_sigmask(signal.SIG_BLOCK, _WORKER)
679
+ try:
680
+ pid = os.fork()
681
+ except OSError as e:
682
+ signal.pthread_sigmask(signal.SIG_UNBLOCK, _WORKER)
683
+ raise OSError(e.errno, f"Can't fork: {e.strerror}") from e
684
+ if pid:
685
+ signal.pthread_sigmask(signal.SIG_UNBLOCK, _WORKER)
686
+ self.emit('spawn', pid)._pool[pid] = {'time': time.monotonic()}
687
+ return
688
+
689
+ self._worker()
690
+
691
+ def _stopped(self, pid: int) -> None:
692
+ if (w := self._pool.pop(pid, None)) is None:
693
+ return
694
+
695
+ log = self.app.log
696
+ log.info(f'Worker {pid} stopped')
697
+ if not w.get('healthy'):
698
+ log.error(f'Worker {pid} stopped too early, shutting down')
699
+ self._term()
700
+
701
+ def _term(self, graceful: bool = False) -> None:
702
+ self.emit('finish', graceful)
703
+ self._finished, self._graceful = True, graceful
704
+ # Not in Mojolicious: the delay of a graceful shutdown is over, so that the manager waits as before
705
+ self._delayed = None
706
+
707
+ # Unlike Mojolicious: the copies of the listen sockets of the manager are closed, as it starts no more workers, so
708
+ # that a new connection is refused once the workers have closed theirs, where the manager of Mojo keeps them until
709
+ # it ends; the new manager of a hot deployment has copies of its own
710
+ self._close_listen()
711
+
712
+ # Not in Mojolicious: the workers that are stopping gracefully stop immediately too, after a graceful shutdown
713
+ if not graceful:
714
+ for w in self._pool.values():
715
+ w['force'] = True
716
+
717
+ def _wait(self) -> None:
718
+ # Poll for heartbeats
719
+ reader, wakee = self.emit('wait')._reader, self._wakee
720
+ assert reader is not None and wakee is not None
721
+ # Not in Mojolicious: the end of the delay of a graceful shutdown is not waited for longer
722
+ timeout = 1 if (delayed := self._delayed) is None else min(1, max(0, delayed - time.monotonic()))
723
+ readable = select.select([reader, wakee], [], [], timeout)[0]
724
+ if wakee in readable:
725
+ try:
726
+ wakee.recv(4096)
727
+ except OSError:
728
+ pass
729
+ self._signal()
730
+ if reader not in readable:
731
+ return
732
+ try:
733
+ chunk = self._read_pipe(reader)
734
+ except OSError:
735
+ return
736
+ if not chunk:
737
+ return
738
+
739
+ # Update heartbeats (and stop gracefully if necessary)
740
+ now = time.monotonic()
741
+ for m in re.finditer(rb'(\d+):(\d)\n', chunk):
742
+ if (w := self._pool.get(int(m[1]))) is None:
743
+ continue
744
+ w['healthy'], w['time'] = True, now
745
+ self.emit('heartbeat', int(m[1]))
746
+ if m[2] != b'0':
747
+ w['graceful'] = w.get('graceful') or now
748
+ w['quit'] = w.get('quit', 0) + 1
749
+
750
+ # The worker, a fork of the manager, which runs the event loop until it stops, and exits
751
+ def _worker(self) -> NoReturn:
752
+ code = 0
753
+ try:
754
+ # Heartbeat messages
755
+ self.cleanup = False
756
+ loop = self.ioloop
757
+ finished = False
758
+
759
+ def finish(loop: Any) -> None:
760
+ nonlocal finished
761
+ finished = True
762
+
763
+ loop.on('finish', finish)
764
+ ref = weakref.ref(self)
765
+
766
+ def heartbeat(loop: Any) -> None:
767
+ if (prefork := ref()) is not None:
768
+ prefork._heartbeat(finished)
769
+
770
+ loop.next_tick(heartbeat)
771
+ loop.recurring(self.heartbeat_interval, heartbeat)
772
+
773
+ # Clean worker environment
774
+ for name in (signal.SIGCHLD, signal.SIGTTIN, signal.SIGTTOU):
775
+ signal.signal(name, signal.SIG_DFL)
776
+ # Unlike Mojolicious: the workers ignore INT, which Ctrl-C sends to all the processes of the server, and HUP,
777
+ # as the manager stops them
778
+ for name in (signal.SIGHUP, signal.SIGINT):
779
+ signal.signal(name, signal.SIG_IGN)
780
+ # Not in Mojolicious: the handlers of TERM and QUIT only note the signal, which the event loop handles, as
781
+ # Perl defers the handlers of signals to a safe point, and a socket pair that Python writes to on a signal
782
+ # wakes the event loop up, as Python repeats a system call that a signal interrupts until its timeout
783
+ noted: list[int] = []
784
+ immediate = False
785
+
786
+ def note(signum: int, frame: Any) -> None:
787
+ noted.append(signum)
788
+
789
+ for name in _WORKER:
790
+ signal.signal(name, note)
791
+ waker, wakee = socket.socketpair()
792
+ waker.setblocking(False)
793
+ wakee.setblocking(False)
794
+ signal.set_wakeup_fd(waker.fileno())
795
+
796
+ def wake(reactor: Any, writable: bool) -> None:
797
+ try:
798
+ wakee.recv(4096)
799
+ except OSError:
800
+ pass
801
+ # Unlike Mojolicious: TERM stops the worker gracefully, and QUIT immediately, where Mojo has QUIT for a
802
+ # graceful stop
803
+ nonlocal immediate
804
+ while noted:
805
+ if (signum := noted.pop(0)) == signal.SIGQUIT:
806
+ immediate = True
807
+ loop.stop()
808
+ # Not in Mojolicious: USR1 stops the worker gracefully after the delay of the graceful shutdown
809
+ elif signum == signal.SIGUSR1:
810
+ if (prefork := ref()) is not None:
811
+ loop.stop_gracefully(prefork.shutdown_delay)
812
+ else:
813
+ loop.stop_gracefully()
814
+
815
+ loop.reactor.io(wakee, wake).watch(wakee, True, False)
816
+
817
+ # Not in Mojolicious: TERM and QUIT, which the manager blocks while it forks, come now that the worker has its
818
+ # handlers
819
+ signal.pthread_sigmask(signal.SIG_UNBLOCK, _WORKER)
820
+
821
+ # Unlike Mojolicious: the worker closes its copies of the listen sockets, as the daemon does
822
+ def max_requests(loop: Any) -> None:
823
+ if (prefork := ref()) is not None:
824
+ prefork._close_listen()
825
+ prefork.max_requests = 1
826
+
827
+ loop.on('finish', max_requests)
828
+ if (reader := self._reader) is not None:
829
+ self._reader = None
830
+ os.close(reader)
831
+ # Left out from Mojolicious: srand, as random seeds itself again in the child of a fork
832
+
833
+ self.app.log.info(f'Worker {os.getpid()} started')
834
+ loop.start()
835
+
836
+ # Not in Mojolicious: the end of the server in the worker, unless it has stopped immediately
837
+ if not immediate:
838
+ self._server_stopped()
839
+
840
+ # Not in Mojolicious: sys.exit() is the exit of the worker, as exit is in Perl
841
+ except SystemExit as e:
842
+ code = e.code if isinstance(e.code, int) else 0 if e.code is None else 1
843
+ except BaseException:
844
+ traceback.print_exc()
845
+ code = 255
846
+ _exit(code)
847
+
848
+
849
+ # Not in Mojolicious: Windows has no QUIT, of Ctrl-\ in a terminal, and BREAK of Ctrl-Break takes its place in a console
850
+ _QUIT = signal.SIGBREAK if sys.platform == 'win32' else signal.SIGQUIT
851
+
852
+ # Not in Mojolicious: the signals that the workers handle, which the manager blocks while it forks
853
+ _WORKER = {signal.SIGTERM, _QUIT} if sys.platform == 'win32' else {signal.SIGTERM, _QUIT, signal.SIGUSR1}
854
+
855
+
856
+ # Not in Mojolicious: os._exit, as the worker leaves what the manager does at its end, with the buffers of the standard
857
+ # streams written before, which os._exit leaves
858
+ def _exit(code: int) -> NoReturn:
859
+ _flush()
860
+ os._exit(code)
861
+
862
+
863
+ def _flush() -> None:
864
+ for stream in (sys.stdout, sys.stderr):
865
+ try:
866
+ stream.flush()
867
+ except Exception:
868
+ pass
869
+
870
+
871
+ # Perl's kill 0, true if the process runs. On Windows, os.kill with 0 is not a check: 0 is CTRL_C_EVENT there, which
872
+ # goes to every process of the console, and another signal is TerminateProcess, so the process is opened and asked for
873
+ # its exit code instead
874
+ if sys.platform == 'win32':
875
+ import ctypes
876
+
877
+ from ctypes import wintypes
878
+
879
+ _kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
880
+ _kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
881
+ _kernel32.OpenProcess.restype = wintypes.HANDLE
882
+ _kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD))
883
+ _kernel32.CloseHandle.argtypes = (wintypes.HANDLE,)
884
+
885
+ # PROCESS_QUERY_LIMITED_INFORMATION, ERROR_ACCESS_DENIED of a process that runs as someone else, and STILL_ACTIVE
886
+ _QUERY, _DENIED, _ACTIVE = 0x1000, 5, 259
887
+
888
+
889
+ def _alive(pid: int) -> bool:
890
+ if sys.platform != 'win32':
891
+ return _kill(0, pid)
892
+ if not (handle := _kernel32.OpenProcess(_QUERY, False, pid)):
893
+ return ctypes.get_last_error() == _DENIED
894
+ try:
895
+ code = wintypes.DWORD()
896
+ return bool(_kernel32.GetExitCodeProcess(handle, ctypes.byref(code))) and code.value == _ACTIVE
897
+ finally:
898
+ _kernel32.CloseHandle(handle)
899
+
900
+
901
+ # Perl's kill, true if the signal was sent
902
+ def _kill(sig: int, pid: int) -> bool:
903
+ try:
904
+ os.kill(pid, sig)
905
+ return True
906
+ except OSError:
907
+ return False
908
+
909
+
910
+ # A number of seconds as Perl prints it, without ".0"
911
+ def _number(value: float) -> str:
912
+ return str(int(value)) if value == int(value) else str(value)
913
+
914
+
915
+ new = Pyjo_Server_Prefork.new
916
+ object = Pyjo_Server_Prefork
917
+ Prefork = Pyjo_Server_Prefork