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/App/FastCGI.py
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.App.FastCGI - FastCGI application
|
|
3
|
+
======================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.App.FastCGI import FastCGI
|
|
7
|
+
|
|
8
|
+
app = FastCGI(url='fcgi://127.0.0.1:9000')
|
|
9
|
+
|
|
10
|
+
# Or import the module, as in Google Python Style Guide
|
|
11
|
+
|
|
12
|
+
import Pyjo.App.FastCGI
|
|
13
|
+
|
|
14
|
+
app = Pyjo.App.FastCGI.new(url='fcgi://127.0.0.1:9000')
|
|
15
|
+
|
|
16
|
+
# PHP on php-fpm, on a UNIX domain socket, with its script
|
|
17
|
+
app = Pyjo.App.FastCGI.new(
|
|
18
|
+
url='fcgi+unix://%2Frun%2Fphp%2Ffpm.sock',
|
|
19
|
+
env={'SCRIPT_FILENAME': '/var/www/index.php', 'DOCUMENT_ROOT': '/var/www'},
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Mount it in an application of Pyjoyment
|
|
23
|
+
import Pyjoyment
|
|
24
|
+
|
|
25
|
+
pyjoyment = Pyjoyment.new()
|
|
26
|
+
pyjoyment.plugin('Mount', {'/php': app})
|
|
27
|
+
pyjoyment.plugin('Mount', {'/fcgi': 'fcgi://127.0.0.1:9000'})
|
|
28
|
+
|
|
29
|
+
:mod:`Pyjo.App.FastCGI` is an application of :mod:`Pyjo` that runs a
|
|
30
|
+
``FastCGI`` application, as php-fpm runs PHP, through a connection to it, as
|
|
31
|
+
a client of `FastCGI 1.0
|
|
32
|
+
<https://fastcgi-archives.github.io/FastCGI_Specification.html>`_ in the role
|
|
33
|
+
of a responder, so that a server of :mod:`Pyjo` serves it, a route of
|
|
34
|
+
:mod:`Pyjoyment` embeds it, as :mod:`Pyjoyment.Plugin.Mount` does, and
|
|
35
|
+
:mod:`Pyjo.Test.Pyjo` tests it.
|
|
36
|
+
|
|
37
|
+
The parameters of the request are its environment of ``CGI``: the part of the
|
|
38
|
+
path that a route embeds the application under is ``SCRIPT_NAME``, after the
|
|
39
|
+
base path of the request, which a part of the path that does not end with a
|
|
40
|
+
slash, as ``/php`` of ``/phpx``, cannot be, so that the application is not
|
|
41
|
+
found there, the rest of the path is ``PATH_INFO``, ``/`` for
|
|
42
|
+
the path of the mount itself, as an empty one is the whole path in
|
|
43
|
+
``SCRIPT_NAME`` for a server of ``FastCGI``, ``REQUEST_URI`` is the path with
|
|
44
|
+
the query, without the header ``Proxy``, whose ``HTTP_PROXY`` an application
|
|
45
|
+
may take for the proxy of its own requests, as with httpoxy, and :attr:`env`
|
|
46
|
+
adds to them, as ``SCRIPT_FILENAME`` for php-fpm, which :attr:`document_root`
|
|
47
|
+
gives for each request otherwise. The body of the request goes in records
|
|
48
|
+
``STDIN``, each once the one before has been written.
|
|
49
|
+
|
|
50
|
+
The connection is one for each request, through the event loop, which the
|
|
51
|
+
application closes after the response, or with :attr:`max_connections`, which
|
|
52
|
+
it keeps open, kept for the next request of the same event loop. A response of
|
|
53
|
+
``CGI`` that comes whole, with the end of the request, is written with its
|
|
54
|
+
``Content-Length``. Another one is written as it comes, each chunk once the
|
|
55
|
+
one before has been written to the client, with the chunked transfer encoding
|
|
56
|
+
when it does not give its ``Content-Length``. What the application writes to
|
|
57
|
+
its standard error is logged.
|
|
58
|
+
|
|
59
|
+
A connection that fails or closes before the response is a response ``502``,
|
|
60
|
+
and one that closes after the start of the response closes the connection of
|
|
61
|
+
the client, as one that is quiet for longer than :attr:`inactivity_timeout`
|
|
62
|
+
does, which, until the response starts, the connection of the client waits
|
|
63
|
+
for without the inactivity timeout of the server.
|
|
64
|
+
|
|
65
|
+
Attributes
|
|
66
|
+
----------
|
|
67
|
+
|
|
68
|
+
:mod:`Pyjo.App.FastCGI` inherits all attributes from :mod:`Pyjo.App` and
|
|
69
|
+
implements the following new ones.
|
|
70
|
+
|
|
71
|
+
See also
|
|
72
|
+
--------
|
|
73
|
+
|
|
74
|
+
:mod:`Pyjoyment`, :mod:`Pyjoyment.Plugin.Mount`, :mod:`Pyjo.App.WSGI`,
|
|
75
|
+
:mod:`Pyjo.Server.FastCGI`, https://mojolicious.org.
|
|
76
|
+
|
|
77
|
+
It has no counterpart in Mojolicious; it is the reverse of
|
|
78
|
+
:mod:`Pyjo.Server.FastCGI`.
|
|
79
|
+
|
|
80
|
+
Classes
|
|
81
|
+
-------
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
import Pyjo.App
|
|
85
|
+
import Pyjo.App.CGI
|
|
86
|
+
import Pyjo.Exception
|
|
87
|
+
import Pyjo.IOLoop
|
|
88
|
+
import Pyjo.URL
|
|
89
|
+
|
|
90
|
+
from Pyjo.Server.FastCGI import (
|
|
91
|
+
BEGIN_REQUEST,
|
|
92
|
+
END_REQUEST,
|
|
93
|
+
HEADER,
|
|
94
|
+
KEEP_CONN,
|
|
95
|
+
MAX_CONTENT,
|
|
96
|
+
PARAMS,
|
|
97
|
+
RESPONDER,
|
|
98
|
+
STDIN,
|
|
99
|
+
STDOUT,
|
|
100
|
+
_encode_pairs, # pyright: ignore[reportPrivateUsage]
|
|
101
|
+
_record, # pyright: ignore[reportPrivateUsage]
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
import os
|
|
105
|
+
import struct
|
|
106
|
+
import weakref
|
|
107
|
+
|
|
108
|
+
from typing import Any
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# The type of a record of the standard error of the application, which a responder may send
|
|
112
|
+
STDERR = 7
|
|
113
|
+
|
|
114
|
+
# The id of the request, the only one of its connection
|
|
115
|
+
ID = 1
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Pyjo_App_FastCGI(Pyjo.App.object):
|
|
119
|
+
"""
|
|
120
|
+
:mod:`Pyjo.App.FastCGI` inherits all methods from :mod:`Pyjo.App` and
|
|
121
|
+
implements the following new ones.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, attrs: dict[str, Any] | None = None, /, **kwargs: Any) -> None:
|
|
125
|
+
"""::
|
|
126
|
+
|
|
127
|
+
app = Pyjo.App.FastCGI.new(url='fcgi://127.0.0.1:9000')
|
|
128
|
+
|
|
129
|
+
Construct a new :mod:`Pyjo.App.FastCGI` object.
|
|
130
|
+
"""
|
|
131
|
+
self.connect_timeout: float | None = None
|
|
132
|
+
"""::
|
|
133
|
+
|
|
134
|
+
timeout = app.connect_timeout
|
|
135
|
+
app.connect_timeout = 5
|
|
136
|
+
|
|
137
|
+
Maximum amount of time in seconds establishing a connection to the
|
|
138
|
+
application may take before getting canceled, the parameter
|
|
139
|
+
``connect_timeout`` of the query of :attr:`url` or ``10``.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
self.document_root: str | os.PathLike[str] | None = None
|
|
143
|
+
"""::
|
|
144
|
+
|
|
145
|
+
root = app.document_root
|
|
146
|
+
app.document_root = '/var/www'
|
|
147
|
+
|
|
148
|
+
Directory of the scripts of the application, as of PHP, as the
|
|
149
|
+
application sees it, which takes the script of each request from its
|
|
150
|
+
path, as a web server does: the path after the mount, up to its first
|
|
151
|
+
part that ends with one of :attr:`script_extensions`, names the script,
|
|
152
|
+
whose file in the directory is ``SCRIPT_FILENAME`` and whose path after
|
|
153
|
+
the mount is ``SCRIPT_NAME``, the rest of the path is ``PATH_INFO``, and
|
|
154
|
+
a path that ends with a slash names ``index`` with the first of them in
|
|
155
|
+
it, as ``index.php``. A path with ``..`` is not found. With :attr:`script`, a path that names no script of the
|
|
156
|
+
directory is the one of :attr:`script`, as the one entry of an
|
|
157
|
+
application that takes every path, as with ``try_files`` of nginx,
|
|
158
|
+
which the path decides, not the files, which the application may see
|
|
159
|
+
elsewhere. ``SCRIPT_FILENAME`` of :attr:`env` takes its place.
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
self.env: dict[str, str] = {}
|
|
163
|
+
"""::
|
|
164
|
+
|
|
165
|
+
env = app.env
|
|
166
|
+
app.env = {'SCRIPT_FILENAME': '/var/www/index.php'}
|
|
167
|
+
|
|
168
|
+
Parameters that are added to the environment of ``CGI`` of each
|
|
169
|
+
request, and replace the ones of the same name.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
self.inactivity_timeout: float | None = None
|
|
173
|
+
"""::
|
|
174
|
+
|
|
175
|
+
timeout = app.inactivity_timeout
|
|
176
|
+
app.inactivity_timeout = 300
|
|
177
|
+
|
|
178
|
+
Maximum amount of time in seconds a connection to the application can
|
|
179
|
+
be inactive before it gets closed, the parameter
|
|
180
|
+
``inactivity_timeout`` of the query of :attr:`url` or ``60``. Until the
|
|
181
|
+
response starts, the connection of the client waits for it without
|
|
182
|
+
the inactivity timeout of the server, so that this one gives the
|
|
183
|
+
response ``502`` to an application that takes longer.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
self.max_connections: int | None = None
|
|
187
|
+
"""::
|
|
188
|
+
|
|
189
|
+
max = app.max_connections
|
|
190
|
+
app.max_connections = 4
|
|
191
|
+
|
|
192
|
+
Maximum number of idle connections to the application that are kept
|
|
193
|
+
and used again by the requests of the same event loop, the parameter
|
|
194
|
+
``max_connections`` of the query of :attr:`url` or ``0``, which closes
|
|
195
|
+
each connection after its request. The requests ask the application
|
|
196
|
+
to keep the connection, as ``fastcgi_keep_conn`` of nginx does, and an
|
|
197
|
+
idle connection that the application closes, or that is inactive for
|
|
198
|
+
longer than :attr:`inactivity_timeout`, is closed and left out. A
|
|
199
|
+
connection that is used again and closes before the response gets the
|
|
200
|
+
request again on a new one.
|
|
201
|
+
|
|
202
|
+
php-fpm binds a worker to a connection for as long as the connection
|
|
203
|
+
is open, also while it is idle, so that each connection that is kept
|
|
204
|
+
takes one of the ``pm.max_children`` of the pool of php-fpm, for each
|
|
205
|
+
event loop, as of each worker of :mod:`Pyjo.Server.Prefork`: too many
|
|
206
|
+
of them leave no worker to the other clients of the pool.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
self.script: str | None = None
|
|
210
|
+
"""::
|
|
211
|
+
|
|
212
|
+
script = app.script
|
|
213
|
+
app.script = '/var/www/index.php'
|
|
214
|
+
app.script = 'index.php'
|
|
215
|
+
|
|
216
|
+
Script of the application for every path, as the one entry of an
|
|
217
|
+
application of PHP that takes every path, with the path after the
|
|
218
|
+
mount as ``PATH_INFO``, or for every path that names no script of
|
|
219
|
+
:attr:`document_root`, in which it is then.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
self.script_extensions: list[str] | None = None
|
|
223
|
+
"""::
|
|
224
|
+
|
|
225
|
+
extensions = app.script_extensions
|
|
226
|
+
app.script_extensions = ['.php', '.phtml']
|
|
227
|
+
|
|
228
|
+
Extensions of the names of the scripts of :attr:`document_root`, the
|
|
229
|
+
parameters ``script_extensions`` of the query of :attr:`url`, or
|
|
230
|
+
``['.php']``.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
self.sendfile: Pyjo.App.CGI.Sendfile | None = None
|
|
234
|
+
"""::
|
|
235
|
+
|
|
236
|
+
places = app.sendfile
|
|
237
|
+
app.sendfile = '/var/www/private'
|
|
238
|
+
app.sendfile = ['/var/www/private', '/var/www/media']
|
|
239
|
+
app.sendfile = re.compile(r'/var/www/[^/]+/private/.*')
|
|
240
|
+
|
|
241
|
+
Directories of the files that the application may reply with by the
|
|
242
|
+
header ``X-Sendfile``, as Apache and lighttpd take it, with the path of
|
|
243
|
+
the file, absolute or in one of them, or regular expressions of the
|
|
244
|
+
whole path of such a file, once its symbolic links are resolved, as the
|
|
245
|
+
private files that are not in :attr:`document_root`, the parameters
|
|
246
|
+
``sendfile`` of the query of :attr:`url`, which are directories, or
|
|
247
|
+
none, which passes the header on as it is. A route that embeds the
|
|
248
|
+
application replies then with the file, as the helper ``reply.file`` of
|
|
249
|
+
:mod:`Pyjoyment` does, with the status and the other headers of the
|
|
250
|
+
application, and a file that they do not allow is not found.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
self.static: bool | None = None
|
|
254
|
+
"""::
|
|
255
|
+
|
|
256
|
+
boolean = app.static
|
|
257
|
+
app.static = True
|
|
258
|
+
|
|
259
|
+
Reply with a file of :attr:`document_root` for a path that names it and
|
|
260
|
+
no script, as ``try_files`` of nginx does, as the helper ``reply.file``
|
|
261
|
+
of :mod:`Pyjoyment` does, with ranges and conditional requests, the
|
|
262
|
+
parameter ``static`` of the query of :attr:`url`, false by default.
|
|
263
|
+
The files are those that :mod:`Pyjoyment` sees, as for an application
|
|
264
|
+
on the same file system, and a route that embeds the application
|
|
265
|
+
replies with them, as :mod:`Pyjoyment.Plugin.Mount` makes.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
self.url: str | Pyjo.URL.object | None = None
|
|
269
|
+
"""::
|
|
270
|
+
|
|
271
|
+
url = app.url
|
|
272
|
+
app.url = 'fcgi://127.0.0.1:9000'
|
|
273
|
+
app.url = 'fcgi+unix://%2Frun%2Fphp%2Ffpm.sock'
|
|
274
|
+
|
|
275
|
+
Location of the application, a TCP/IP address and port of ``fcgi://``
|
|
276
|
+
or the path of a UNIX domain socket of ``fcgi+unix://``, escaped as the
|
|
277
|
+
listen locations of :mod:`Pyjo.Server.Daemon` are. Its query can give
|
|
278
|
+
:attr:`connect_timeout`, :attr:`document_root`,
|
|
279
|
+
:attr:`inactivity_timeout`, :attr:`max_connections`, :attr:`script`,
|
|
280
|
+
:attr:`script_extensions`, :attr:`sendfile` and :attr:`static`, as the
|
|
281
|
+
listen locations give their options, which the attributes take the
|
|
282
|
+
place of. ::
|
|
283
|
+
|
|
284
|
+
app.url = 'fcgi://127.0.0.1:9000?document_root=%2Fvar%2Fwww&script=index.php'
|
|
285
|
+
app.url = 'fcgi://127.0.0.1:9000?inactivity_timeout=300&max_connections=4'
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
# The idle connections to the application of each event loop, of the process that made them
|
|
289
|
+
self._pid = os.getpid()
|
|
290
|
+
self._queues: weakref.WeakKeyDictionary[Any, _Queue] = weakref.WeakKeyDictionary()
|
|
291
|
+
|
|
292
|
+
super().__init__(attrs, **kwargs)
|
|
293
|
+
|
|
294
|
+
def close(self) -> None:
|
|
295
|
+
"""::
|
|
296
|
+
|
|
297
|
+
app.close()
|
|
298
|
+
|
|
299
|
+
Close the idle connections to the application that are kept, and the
|
|
300
|
+
connections and the servers of :attr:`ua`, as the end of a ``with``
|
|
301
|
+
block does.
|
|
302
|
+
"""
|
|
303
|
+
if self._pid == os.getpid():
|
|
304
|
+
for queue in list(self._queues.values()):
|
|
305
|
+
for idle in list(queue):
|
|
306
|
+
idle.stop()
|
|
307
|
+
idle.close()
|
|
308
|
+
queue.clear()
|
|
309
|
+
super().close()
|
|
310
|
+
|
|
311
|
+
def handler(self, tx: Any) -> None:
|
|
312
|
+
"""::
|
|
313
|
+
|
|
314
|
+
app.handler(Pyjo.Transaction.HTTP.new())
|
|
315
|
+
app.handler(Pyjoyment.Controller.new())
|
|
316
|
+
|
|
317
|
+
Run the application for the request of the transaction, or of the
|
|
318
|
+
controller of a route that embeds the application, and write its
|
|
319
|
+
response.
|
|
320
|
+
"""
|
|
321
|
+
if (url := self.url) is None:
|
|
322
|
+
raise Pyjo.Exception.new('No FastCGI application')
|
|
323
|
+
options, query = _location(url)
|
|
324
|
+
options['timeout'] = _seconds(self.connect_timeout, query.get('connect_timeout'), 10)
|
|
325
|
+
config = {
|
|
326
|
+
'inactivity': _seconds(self.inactivity_timeout, query.get('inactivity_timeout'), 60),
|
|
327
|
+
'max': _number(self.max_connections, query.get('max_connections')),
|
|
328
|
+
'root': os.fspath(self.document_root) if self.document_root is not None else query.get('document_root'),
|
|
329
|
+
'script': self.script if self.script is not None else query.get('script'),
|
|
330
|
+
'extensions': _extensions(self.script_extensions, query.get('script_extensions')),
|
|
331
|
+
'sendfile': Pyjo.App.CGI._places(self.sendfile if self.sendfile is not None else query.get('sendfile')), # pyright: ignore[reportPrivateUsage]
|
|
332
|
+
'static': self.static if self.static is not None else _flag(query.get('static')),
|
|
333
|
+
}
|
|
334
|
+
_Call.run(self, config, tx, options)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
# A request to the application, in records of FastCGI on a connection to it
|
|
338
|
+
class _Call(Pyjo.App.CGI._Response): # pyright: ignore[reportPrivateUsage]
|
|
339
|
+
name = 'FastCGI'
|
|
340
|
+
|
|
341
|
+
# The path of the mount itself, as a server of FastCGI takes an empty PATH_INFO for the whole path in SCRIPT_NAME,
|
|
342
|
+
# as nginx and Caddy send it
|
|
343
|
+
mount_path = '/'
|
|
344
|
+
|
|
345
|
+
def __init__(self, app: Pyjo_App_FastCGI, config: dict[str, Any], tx: Any, c: Any, options: dict[str, Any]) -> None:
|
|
346
|
+
super().__init__(app, config, tx, c)
|
|
347
|
+
self.options = options
|
|
348
|
+
self.max: int = config['max']
|
|
349
|
+
self.buffer = bytearray()
|
|
350
|
+
self.connection: str | None = None
|
|
351
|
+
self.draining = False
|
|
352
|
+
self.ended = False
|
|
353
|
+
self.id: str | None = None
|
|
354
|
+
self.location = ':'.join(str(options[name]) for name in ('path', 'address', 'port') if name in options)
|
|
355
|
+
self.params = b''
|
|
356
|
+
self.received = False
|
|
357
|
+
self.reused = False
|
|
358
|
+
|
|
359
|
+
# The connection to the application has been closed, before the end of the request or after it
|
|
360
|
+
def _closed(self, stream: Any) -> None:
|
|
361
|
+
self.stream = None
|
|
362
|
+
if self.done or self.closed:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# A connection that was kept, which the application closed before it got the request, as one of its workers
|
|
366
|
+
# ended, gets the request again on a new one, as the request did nothing yet
|
|
367
|
+
if self.reused and not self.received:
|
|
368
|
+
return self._connect()
|
|
369
|
+
self._failed('FastCGI connection closed before the end of the response')
|
|
370
|
+
|
|
371
|
+
def _connect(self) -> None:
|
|
372
|
+
self.buffer.clear()
|
|
373
|
+
self.offset = 0
|
|
374
|
+
self.reused = False
|
|
375
|
+
self.id = Pyjo.IOLoop.client(self._connected, **self.options)
|
|
376
|
+
|
|
377
|
+
def _connected(self, loop: Any, err: Any, stream: Any) -> None:
|
|
378
|
+
self.connection, self.id = self.id, None
|
|
379
|
+
if self.closed:
|
|
380
|
+
if stream is not None:
|
|
381
|
+
stream.close()
|
|
382
|
+
return
|
|
383
|
+
if err:
|
|
384
|
+
return self._failed(f'FastCGI connection failed: {err}')
|
|
385
|
+
|
|
386
|
+
self.stream = stream
|
|
387
|
+
stream.timeout = self.inactivity
|
|
388
|
+
stream.on('read', self._read)
|
|
389
|
+
stream.on('close', self._closed)
|
|
390
|
+
stream.on('error', self._error)
|
|
391
|
+
stream.on('timeout', self._timeout)
|
|
392
|
+
|
|
393
|
+
# The request, one for the connection, which the application closes after it, unless it is asked to keep it
|
|
394
|
+
begin = _record(BEGIN_REQUEST, ID, struct.pack('>HB5x', RESPONDER, KEEP_CONN if self.max else 0))
|
|
395
|
+
params = b''.join(
|
|
396
|
+
_record(PARAMS, ID, self.params[i : i + MAX_CONTENT]) for i in range(0, len(self.params), MAX_CONTENT)
|
|
397
|
+
)
|
|
398
|
+
stream.write(begin + params + _record(PARAMS, ID))
|
|
399
|
+
self._stdin()
|
|
400
|
+
|
|
401
|
+
def _error(self, stream: Any, err: Any) -> None:
|
|
402
|
+
self._failed(f'FastCGI connection failed: {err}')
|
|
403
|
+
|
|
404
|
+
# The client has the whole response, of its length, before the end of the request, which a connection that is kept
|
|
405
|
+
# waits for
|
|
406
|
+
def _finish(self, *args: Any) -> None:
|
|
407
|
+
length = self.tx.res.headers.content_length
|
|
408
|
+
if (
|
|
409
|
+
self.max
|
|
410
|
+
and self.started
|
|
411
|
+
and not self.done
|
|
412
|
+
and not self.chunked
|
|
413
|
+
and not len(self.pending)
|
|
414
|
+
and self.stream is not None
|
|
415
|
+
and length is not None
|
|
416
|
+
and length.isdigit()
|
|
417
|
+
and self.sent >= int(length)
|
|
418
|
+
):
|
|
419
|
+
self.closed = self.done = self.draining = True
|
|
420
|
+
return
|
|
421
|
+
super()._finish(*args)
|
|
422
|
+
|
|
423
|
+
def _open(self, params: dict[str, str]) -> None:
|
|
424
|
+
self.params = _encode_pairs(params)
|
|
425
|
+
|
|
426
|
+
# A connection that was kept, or a new one
|
|
427
|
+
if self.max and (idle := _dequeue(self.app, self.location)) is not None:
|
|
428
|
+
self.id, self.reused = idle.id, True
|
|
429
|
+
idle.stream.start()
|
|
430
|
+
return self._connected(None, None, idle.stream)
|
|
431
|
+
self._connect()
|
|
432
|
+
|
|
433
|
+
def _read(self, stream: Any, chunk: bytes) -> None:
|
|
434
|
+
if self.done and not self.draining:
|
|
435
|
+
return
|
|
436
|
+
self.received = True
|
|
437
|
+
buffer = self.buffer
|
|
438
|
+
buffer += chunk
|
|
439
|
+
ended = False
|
|
440
|
+
while len(buffer) >= HEADER.size:
|
|
441
|
+
version, type, id, length, padding = HEADER.unpack_from(buffer)
|
|
442
|
+
if version != 1:
|
|
443
|
+
return self._failed(f'FastCGI version {version} is not supported')
|
|
444
|
+
end = HEADER.size + length + padding
|
|
445
|
+
if len(buffer) < end:
|
|
446
|
+
break
|
|
447
|
+
content = bytes(buffer[HEADER.size : HEADER.size + length])
|
|
448
|
+
del buffer[:end]
|
|
449
|
+
if id != ID:
|
|
450
|
+
continue
|
|
451
|
+
if type == STDOUT:
|
|
452
|
+
if self.draining:
|
|
453
|
+
if len(content):
|
|
454
|
+
return self._drained(False)
|
|
455
|
+
continue
|
|
456
|
+
self._stdout(content)
|
|
457
|
+
elif type == STDERR and len(content):
|
|
458
|
+
self.app.log.warn(f'FastCGI application: {content.decode("utf-8", "replace").rstrip()}')
|
|
459
|
+
elif type == END_REQUEST:
|
|
460
|
+
ended = self.ended = True
|
|
461
|
+
break
|
|
462
|
+
if self.draining:
|
|
463
|
+
if ended:
|
|
464
|
+
self._drained(True)
|
|
465
|
+
return
|
|
466
|
+
self._flush(ended)
|
|
467
|
+
|
|
468
|
+
# The end of the request after the whole response, or more of the response than its length, which closes it
|
|
469
|
+
def _drained(self, ended: bool) -> None:
|
|
470
|
+
self.draining = False
|
|
471
|
+
if not ended:
|
|
472
|
+
self.aborted = True
|
|
473
|
+
self._stop()
|
|
474
|
+
|
|
475
|
+
# The body of the request, a record once the one before has been written, and an empty record at its end
|
|
476
|
+
def _stdin(self, *args: Any) -> None:
|
|
477
|
+
if (stream := self.stream) is None or self.done:
|
|
478
|
+
return
|
|
479
|
+
chunk = self.tx.req.content.get_body_chunk(self.offset) or b''
|
|
480
|
+
if not len(chunk):
|
|
481
|
+
return stream.write(_record(STDIN, ID))
|
|
482
|
+
self.offset += len(chunk)
|
|
483
|
+
records = b''.join(_record(STDIN, ID, chunk[i : i + MAX_CONTENT]) for i in range(0, len(chunk), MAX_CONTENT))
|
|
484
|
+
stream.write(records, self._stdin)
|
|
485
|
+
|
|
486
|
+
def _timeout(self, stream: Any) -> None:
|
|
487
|
+
self._failed('FastCGI connection inactive')
|
|
488
|
+
|
|
489
|
+
# The end of the connection to the application, and of its connecting, or the connection kept for the next request,
|
|
490
|
+
# after a whole response, with nothing after it
|
|
491
|
+
def _stop(self) -> None:
|
|
492
|
+
if (id := self.id) is not None:
|
|
493
|
+
self.id = None
|
|
494
|
+
Pyjo.IOLoop.remove(id)
|
|
495
|
+
if (stream := self.stream) is not None:
|
|
496
|
+
self.stream = None
|
|
497
|
+
stream.unsubscribe('read', self._read).unsubscribe('close', self._closed)
|
|
498
|
+
stream.unsubscribe('error', self._error).unsubscribe('timeout', self._timeout)
|
|
499
|
+
if self.max and self.ended and not self.aborted and not len(self.buffer) and self.connection is not None:
|
|
500
|
+
return _enqueue(self.app, self.max, _Idle(self.location, self.connection, stream))
|
|
501
|
+
stream.close()
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# An idle connection to the application, which is closed when the application closes it, its inactivity timeout ends,
|
|
505
|
+
# or the application sends what no request asked for
|
|
506
|
+
class _Idle:
|
|
507
|
+
def __init__(self, location: str, id: str, stream: Any) -> None:
|
|
508
|
+
self.location = location
|
|
509
|
+
self.id = id
|
|
510
|
+
self.queue: weakref.ref[_Queue] | None = None
|
|
511
|
+
self.stream = stream
|
|
512
|
+
|
|
513
|
+
def close(self, *args: Any) -> None:
|
|
514
|
+
self.stream.close()
|
|
515
|
+
|
|
516
|
+
def closed(self, stream: Any) -> None:
|
|
517
|
+
if self.queue is not None and (queue := self.queue()) is not None and self in queue:
|
|
518
|
+
queue.remove(self)
|
|
519
|
+
|
|
520
|
+
def start(self, queue: '_Queue') -> None:
|
|
521
|
+
self.queue = weakref.ref(queue)
|
|
522
|
+
stream = self.stream
|
|
523
|
+
stream.on('close', self.closed)
|
|
524
|
+
stream.on('error', self.close)
|
|
525
|
+
stream.on('read', self.close)
|
|
526
|
+
stream.start()
|
|
527
|
+
|
|
528
|
+
def stop(self) -> None:
|
|
529
|
+
self.queue = None
|
|
530
|
+
self.stream.unsubscribe('close', self.closed).unsubscribe('error', self.close).unsubscribe('read', self.close)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
# The idle connections of an event loop, which an idle one refers to weakly, as the stream of the event loop refers to it
|
|
534
|
+
class _Queue(list[_Idle]):
|
|
535
|
+
pass
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
# An idle connection to the location, which the application has not closed, out of the queue of the event loop
|
|
539
|
+
def _dequeue(app: Pyjo_App_FastCGI, location: str) -> _Idle | None:
|
|
540
|
+
queue = _queue(app)
|
|
541
|
+
for idle in list(queue):
|
|
542
|
+
if idle.location != location:
|
|
543
|
+
continue
|
|
544
|
+
queue.remove(idle)
|
|
545
|
+
idle.stop()
|
|
546
|
+
|
|
547
|
+
# An end of the connection that is there to read
|
|
548
|
+
if idle.stream.is_readable:
|
|
549
|
+
idle.close()
|
|
550
|
+
continue
|
|
551
|
+
return idle
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
# An idle connection into the queue of the event loop, after the oldest ones that are too many
|
|
556
|
+
def _enqueue(app: Pyjo_App_FastCGI, max: int, idle: _Idle) -> None:
|
|
557
|
+
queue = _queue(app)
|
|
558
|
+
while len(queue) and len(queue) >= max:
|
|
559
|
+
old = queue.pop(0)
|
|
560
|
+
old.stop()
|
|
561
|
+
old.close()
|
|
562
|
+
queue.append(idle)
|
|
563
|
+
idle.start(queue)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# The extensions of the scripts of the attribute, of the query of the location, or the one of PHP
|
|
567
|
+
def _extensions(value: list[str] | None, query: list[str] | None) -> list[str]:
|
|
568
|
+
if value is not None:
|
|
569
|
+
return value
|
|
570
|
+
return query if query else ['.php']
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# The options of the connection to the location of the application, and the parameters of its query
|
|
574
|
+
def _location(url: Any) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
575
|
+
url = Pyjo.URL.new(str(url))
|
|
576
|
+
names = ('connect_timeout', 'document_root', 'inactivity_timeout', 'max_connections', 'script', 'static')
|
|
577
|
+
query: dict[str, Any] = {name: value for name in names if (value := url.query.param(name)) is not None}
|
|
578
|
+
for name in ('script_extensions', 'sendfile'):
|
|
579
|
+
if values := url.query.every_param(name):
|
|
580
|
+
query[name] = values
|
|
581
|
+
if (proto := url.protocol) == 'fcgi+unix':
|
|
582
|
+
return {'path': url.host}, query
|
|
583
|
+
if proto == 'fcgi' and url.host and url.port:
|
|
584
|
+
return {'address': url.host, 'port': url.port}, query
|
|
585
|
+
raise Pyjo.Exception.new(f'Invalid FastCGI location "{url}"')
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# A number of the attribute, of the query of the location, or none
|
|
589
|
+
def _number(value: int | None, query: str | None) -> int:
|
|
590
|
+
if value is not None:
|
|
591
|
+
return value
|
|
592
|
+
if query is None:
|
|
593
|
+
return 0
|
|
594
|
+
try:
|
|
595
|
+
return int(query)
|
|
596
|
+
except ValueError:
|
|
597
|
+
raise Pyjo.Exception.new(f'Invalid FastCGI number "{query}"') from None
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
# The idle connections of the event loop, where the child of a fork closes the ones of its parent, which the parent
|
|
601
|
+
# still uses
|
|
602
|
+
def _queue(app: Pyjo_App_FastCGI) -> _Queue:
|
|
603
|
+
loop = Pyjo.IOLoop.singleton
|
|
604
|
+
if app._pid != (pid := os.getpid()): # pyright: ignore[reportPrivateUsage]
|
|
605
|
+
for idle in app._queues.get(loop, []): # pyright: ignore[reportPrivateUsage]
|
|
606
|
+
idle.stop()
|
|
607
|
+
idle.close()
|
|
608
|
+
app._pid, app._queues = pid, weakref.WeakKeyDictionary() # pyright: ignore[reportPrivateUsage]
|
|
609
|
+
return app._queues.setdefault(loop, _Queue()) # pyright: ignore[reportPrivateUsage]
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# A timeout of the attribute, of the query of the location, or its default
|
|
613
|
+
def _seconds(value: float | None, query: str | None, default: float) -> float:
|
|
614
|
+
if value is not None:
|
|
615
|
+
return value
|
|
616
|
+
if query is None:
|
|
617
|
+
return default
|
|
618
|
+
try:
|
|
619
|
+
return float(query)
|
|
620
|
+
except ValueError:
|
|
621
|
+
raise Pyjo.Exception.new(f'Invalid FastCGI timeout "{query}"') from None
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
# A flag of the query of the location, as the listen locations of Pyjo.Server.Daemon have them
|
|
625
|
+
def _flag(value: str | None) -> bool:
|
|
626
|
+
return value is not None and value not in ('', '0')
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
new = Pyjo_App_FastCGI.new
|
|
630
|
+
object = Pyjo_App_FastCGI
|
|
631
|
+
FastCGI = Pyjo_App_FastCGI
|