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/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