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
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.IOLoop.ChildProcess - Non-blocking child process
|
|
3
|
+
======================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.IOLoop.ChildProcess import ChildProcess
|
|
7
|
+
|
|
8
|
+
child = ChildProcess()
|
|
9
|
+
child.spawn(['ls', '-l'])
|
|
10
|
+
|
|
11
|
+
# Or import the module, as in Google Python Style Guide
|
|
12
|
+
|
|
13
|
+
import Pyjo.IOLoop.ChildProcess
|
|
14
|
+
|
|
15
|
+
# Run a program, with its output and its exit status
|
|
16
|
+
child = Pyjo.IOLoop.ChildProcess.new()
|
|
17
|
+
child.on('spawn', lambda child: child.stdout.on('read', lambda stream, chunk: print(chunk)))
|
|
18
|
+
child.on('close', lambda child, code, signal: print(f'Exited with {code}'))
|
|
19
|
+
child.on('error', lambda child, err: print(f"Can't run: {err}"))
|
|
20
|
+
child.spawn(['ls', '-l'])
|
|
21
|
+
|
|
22
|
+
# Write to its standard input, and end it
|
|
23
|
+
child = Pyjo.IOLoop.ChildProcess.new().spawn(['sort'])
|
|
24
|
+
child.stdout.on('read', lambda stream, chunk: print(chunk))
|
|
25
|
+
child.stdin.write(b'b\\na\\n')
|
|
26
|
+
child.stdin.close_gracefully()
|
|
27
|
+
|
|
28
|
+
# Its output as a promise, also in a coroutine
|
|
29
|
+
child = Pyjo.IOLoop.ChildProcess.new()
|
|
30
|
+
child.run_p(['ls', '-l']).then(lambda code, out, err: print(out))
|
|
31
|
+
|
|
32
|
+
# Start reactor if necessary
|
|
33
|
+
if not child.reactor.is_running:
|
|
34
|
+
child.reactor.start()
|
|
35
|
+
|
|
36
|
+
:mod:`Pyjo.IOLoop.ChildProcess` runs a program in a process of its own, as
|
|
37
|
+
``CGI`` programs and command line tools are, with its standard input, output
|
|
38
|
+
and error as pipes that the reactor watches, as
|
|
39
|
+
:mod:`Pyjo.IOLoop.Stream` objects, without blocking the event loop. It runs
|
|
40
|
+
the program, not a function of Python, as :mod:`Pyjo.IOLoop.Subprocess` does
|
|
41
|
+
in a fork of the process.
|
|
42
|
+
|
|
43
|
+
The process is reaped by the reactor, which checks for its end with a timer,
|
|
44
|
+
and at once when its output and its error have ended, as most programs end
|
|
45
|
+
them when they exit. The end of the interpreter kills and reaps the processes
|
|
46
|
+
that are left, and closes their pipes.
|
|
47
|
+
|
|
48
|
+
The child of a fork leaves the processes of its parent to it: it closes its
|
|
49
|
+
copies of their pipes, and neither reaps them nor emits their events, which
|
|
50
|
+
the parent does.
|
|
51
|
+
|
|
52
|
+
The reactor watches pipes on POSIX systems, not on Windows, where
|
|
53
|
+
:meth:`~Pyjo_IOLoop_ChildProcess.spawn` emits the event ``error``.
|
|
54
|
+
|
|
55
|
+
Events
|
|
56
|
+
------
|
|
57
|
+
|
|
58
|
+
:mod:`Pyjo.IOLoop.ChildProcess` inherits all events from
|
|
59
|
+
:mod:`Pyjo.EventEmitter` and can emit the following new ones.
|
|
60
|
+
|
|
61
|
+
close
|
|
62
|
+
~~~~~
|
|
63
|
+
::
|
|
64
|
+
|
|
65
|
+
child.on('close', lambda child, code, signal: ...)
|
|
66
|
+
|
|
67
|
+
Emitted once the process has exited and its output and its error have ended,
|
|
68
|
+
with its exit code, or the signal that terminated it, after the event
|
|
69
|
+
``exit``. ::
|
|
70
|
+
|
|
71
|
+
child.on('close', lambda child, code, signal: print(f'Process {child.pid} closed with {code}'))
|
|
72
|
+
|
|
73
|
+
error
|
|
74
|
+
~~~~~
|
|
75
|
+
::
|
|
76
|
+
|
|
77
|
+
child.on('error', lambda child, err: ...)
|
|
78
|
+
|
|
79
|
+
Emitted if the program could not be run, as one that does not exist. ::
|
|
80
|
+
|
|
81
|
+
child.on('error', lambda child, err: print(f'Error: {err}'))
|
|
82
|
+
|
|
83
|
+
exit
|
|
84
|
+
~~~~
|
|
85
|
+
::
|
|
86
|
+
|
|
87
|
+
child.on('exit', lambda child, code, signal: ...)
|
|
88
|
+
|
|
89
|
+
Emitted once the process has exited, with its exit code, or ``None`` and the
|
|
90
|
+
signal that terminated it. Its output and its error may not have ended yet.
|
|
91
|
+
::
|
|
92
|
+
|
|
93
|
+
child.on('exit', lambda child, code, signal: print(f'Process {child.pid} exited with {code}'))
|
|
94
|
+
|
|
95
|
+
spawn
|
|
96
|
+
~~~~~
|
|
97
|
+
::
|
|
98
|
+
|
|
99
|
+
child.on('spawn', lambda child: ...)
|
|
100
|
+
|
|
101
|
+
Emitted once the process has been spawned, before its output and its error
|
|
102
|
+
are read, so that the subscribers of this event get all of them. ::
|
|
103
|
+
|
|
104
|
+
child.on('spawn', lambda child: print(f'Running process {child.pid}'))
|
|
105
|
+
|
|
106
|
+
See also
|
|
107
|
+
--------
|
|
108
|
+
|
|
109
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
110
|
+
|
|
111
|
+
Inspired by `child_process <https://nodejs.org/api/child_process.html>`_ of
|
|
112
|
+
Node.js, as Mojolicious runs no programs in its event loop.
|
|
113
|
+
|
|
114
|
+
Classes
|
|
115
|
+
-------
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
import Pyjo.EventEmitter
|
|
119
|
+
import Pyjo.Exception
|
|
120
|
+
import Pyjo.IOLoop
|
|
121
|
+
import Pyjo.IOLoop.Stream
|
|
122
|
+
import Pyjo.Reactor
|
|
123
|
+
|
|
124
|
+
import atexit
|
|
125
|
+
import os
|
|
126
|
+
import signal as signals
|
|
127
|
+
import subprocess
|
|
128
|
+
import weakref
|
|
129
|
+
|
|
130
|
+
from collections.abc import Mapping, Sequence
|
|
131
|
+
from typing import Any, Self
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# The child processes that have not closed yet, with the process that started them, which the end of the interpreter
|
|
135
|
+
# kills and reaps, with their pipes, as Python warns of a process that still runs when it frees its object. A process
|
|
136
|
+
# stays until it closes, also after it has been reaped, as its output and its error, which a process that it has
|
|
137
|
+
# started can keep open, are read until their end, and nothing else may refer to it, as an application that is done
|
|
138
|
+
# with its output
|
|
139
|
+
_running: dict['Pyjo_IOLoop_ChildProcess', int] = {}
|
|
140
|
+
|
|
141
|
+
# The delay of the next check for the end of a process, which grows to the longest one
|
|
142
|
+
_DELAYS = (0, 0.01, 0.02, 0.05, 0.1)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Pyjo_IOLoop_ChildProcess(Pyjo.EventEmitter.object):
|
|
146
|
+
"""
|
|
147
|
+
:mod:`Pyjo.IOLoop.ChildProcess` inherits all methods from
|
|
148
|
+
:mod:`Pyjo.EventEmitter` and implements the following new ones.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(self, *, reactor: Pyjo.Reactor.object | None = None) -> None:
|
|
152
|
+
"""::
|
|
153
|
+
|
|
154
|
+
child = Pyjo.IOLoop.ChildProcess.new()
|
|
155
|
+
child = Pyjo.IOLoop.ChildProcess.new(reactor=Pyjo.Reactor.Poll.new())
|
|
156
|
+
|
|
157
|
+
Construct a new :mod:`Pyjo.IOLoop.ChildProcess` object.
|
|
158
|
+
"""
|
|
159
|
+
super().__init__()
|
|
160
|
+
|
|
161
|
+
self._check = 0
|
|
162
|
+
self._detached = False
|
|
163
|
+
self._ended = 0
|
|
164
|
+
self._exit_code: int | None = None
|
|
165
|
+
self._process: subprocess.Popen[bytes] | None = None
|
|
166
|
+
self._reactor: weakref.ref[Pyjo.Reactor.object] | None = None
|
|
167
|
+
self._signal_code: int | None = None
|
|
168
|
+
self._stderr: Pyjo.IOLoop.Stream.object | None = None
|
|
169
|
+
self._stdin: Pyjo.IOLoop.Stream.object | None = None
|
|
170
|
+
self._stdout: Pyjo.IOLoop.Stream.object | None = None
|
|
171
|
+
self._timer: str | None = None
|
|
172
|
+
if reactor is not None:
|
|
173
|
+
self.reactor = reactor
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def exit_code(self) -> int | None:
|
|
177
|
+
"""::
|
|
178
|
+
|
|
179
|
+
code = child.exit_code
|
|
180
|
+
|
|
181
|
+
Exit code of the process, or ``None`` if it is still running or a
|
|
182
|
+
signal terminated it.
|
|
183
|
+
"""
|
|
184
|
+
return self._exit_code
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def pid(self) -> int | None:
|
|
188
|
+
"""::
|
|
189
|
+
|
|
190
|
+
pid = child.pid
|
|
191
|
+
|
|
192
|
+
Process id of the spawned process if available.
|
|
193
|
+
"""
|
|
194
|
+
return self._process.pid if self._process is not None else None
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def reactor(self) -> Pyjo.Reactor.object:
|
|
198
|
+
"""::
|
|
199
|
+
|
|
200
|
+
reactor = child.reactor
|
|
201
|
+
child.reactor = Pyjo.Reactor.Poll.new()
|
|
202
|
+
|
|
203
|
+
Low-level event reactor, defaults to the ``reactor`` attribute value of
|
|
204
|
+
the global :mod:`Pyjo.IOLoop` singleton. Note that this attribute is
|
|
205
|
+
weakened.
|
|
206
|
+
"""
|
|
207
|
+
if self._reactor is None:
|
|
208
|
+
self._reactor = weakref.ref(Pyjo.IOLoop.singleton.reactor)
|
|
209
|
+
if (reactor := self._reactor()) is None:
|
|
210
|
+
raise ReferenceError('weakly-referenced object no longer exists')
|
|
211
|
+
return reactor
|
|
212
|
+
|
|
213
|
+
@reactor.setter
|
|
214
|
+
def reactor(self, value: Pyjo.Reactor.object) -> None:
|
|
215
|
+
self._reactor = weakref.ref(value)
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def signal_code(self) -> int | None:
|
|
219
|
+
"""::
|
|
220
|
+
|
|
221
|
+
signal = child.signal_code
|
|
222
|
+
|
|
223
|
+
Signal that terminated the process, or ``None``.
|
|
224
|
+
"""
|
|
225
|
+
return self._signal_code
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def stderr(self) -> Pyjo.IOLoop.Stream.object | None:
|
|
229
|
+
"""::
|
|
230
|
+
|
|
231
|
+
stream = child.stderr
|
|
232
|
+
|
|
233
|
+
:mod:`Pyjo.IOLoop.Stream` object of the standard error of the process,
|
|
234
|
+
which the reactor reads without an inactivity timeout.
|
|
235
|
+
"""
|
|
236
|
+
return self._stderr
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def stdin(self) -> Pyjo.IOLoop.Stream.object | None:
|
|
240
|
+
"""::
|
|
241
|
+
|
|
242
|
+
stream = child.stdin
|
|
243
|
+
|
|
244
|
+
:mod:`Pyjo.IOLoop.Stream` object of the standard input of the process,
|
|
245
|
+
which the reactor writes without an inactivity timeout, and which ends
|
|
246
|
+
with :meth:`~Pyjo.IOLoop.Stream.Pyjo_IOLoop_Stream.close_gracefully`.
|
|
247
|
+
It closes once the process closes its end, so that a write that fails
|
|
248
|
+
is no error.
|
|
249
|
+
"""
|
|
250
|
+
return self._stdin
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def stdout(self) -> Pyjo.IOLoop.Stream.object | None:
|
|
254
|
+
"""::
|
|
255
|
+
|
|
256
|
+
stream = child.stdout
|
|
257
|
+
|
|
258
|
+
:mod:`Pyjo.IOLoop.Stream` object of the standard output of the
|
|
259
|
+
process, which the reactor reads without an inactivity timeout. ::
|
|
260
|
+
|
|
261
|
+
# Kill a process that has been quiet for 60 seconds
|
|
262
|
+
child.stdout.timeout = 60
|
|
263
|
+
child.stdout.on('timeout', lambda stream: child.kill())
|
|
264
|
+
"""
|
|
265
|
+
return self._stdout
|
|
266
|
+
|
|
267
|
+
def kill(self, signal: int = signals.SIGTERM) -> bool:
|
|
268
|
+
"""::
|
|
269
|
+
|
|
270
|
+
boolean = child.kill()
|
|
271
|
+
boolean = child.kill(signal.SIGKILL)
|
|
272
|
+
|
|
273
|
+
Send a signal to the process, ``SIGTERM`` by default, if it is still
|
|
274
|
+
running, or to the processes of its group, which it may have started,
|
|
275
|
+
when it has been spawned ``detached``, while any of them runs.
|
|
276
|
+
"""
|
|
277
|
+
if (process := self._process) is None:
|
|
278
|
+
return False
|
|
279
|
+
if self._detached:
|
|
280
|
+
try:
|
|
281
|
+
os.killpg(process.pid, signal)
|
|
282
|
+
|
|
283
|
+
# No process of the group, or only the process that has exited and has not been reaped yet, for which macOS
|
|
284
|
+
# gives EPERM
|
|
285
|
+
except (ProcessLookupError, PermissionError):
|
|
286
|
+
return False
|
|
287
|
+
return True
|
|
288
|
+
if process.poll() is not None:
|
|
289
|
+
return False
|
|
290
|
+
process.send_signal(signal)
|
|
291
|
+
return True
|
|
292
|
+
|
|
293
|
+
def run_p(self, args: Sequence[str | bytes | os.PathLike[str]], *, input: bytes = b'', **options: Any) -> Any:
|
|
294
|
+
"""::
|
|
295
|
+
|
|
296
|
+
promise = child.run_p(['ls', '-l'])
|
|
297
|
+
promise = child.run_p(['sort'], input=b'b\\na\\n', cwd='/tmp')
|
|
298
|
+
|
|
299
|
+
Same as :meth:`spawn`, with the options of :meth:`spawn`, but writes
|
|
300
|
+
``input`` to the standard input of the process and ends it, and
|
|
301
|
+
returns a :mod:`Pyjo.Promise` object, which is fulfilled with the exit
|
|
302
|
+
code, ``None`` for a signal, and the whole output and error of the
|
|
303
|
+
process as :mod:`Pyjo.String.Bytes` objects once it closes, or
|
|
304
|
+
rejected if the program could not be run. ::
|
|
305
|
+
|
|
306
|
+
# The output of a program in a coroutine
|
|
307
|
+
code, out, err = await Pyjo.IOLoop.ChildProcess.new().run_p(['git', 'log', '-1'])
|
|
308
|
+
"""
|
|
309
|
+
# Imported here, as Pyjo.Promise uses Pyjo.IOLoop
|
|
310
|
+
import Pyjo.Promise
|
|
311
|
+
import Pyjo.String.Bytes
|
|
312
|
+
|
|
313
|
+
p = Pyjo.Promise.new()
|
|
314
|
+
out: list[bytes] = []
|
|
315
|
+
err: list[bytes] = []
|
|
316
|
+
|
|
317
|
+
def close(child: Pyjo_IOLoop_ChildProcess, code: int | None, signal: int | None) -> None:
|
|
318
|
+
p.resolve(code, Pyjo.String.Bytes.new(b''.join(out)), Pyjo.String.Bytes.new(b''.join(err)))
|
|
319
|
+
|
|
320
|
+
def error(child: Pyjo_IOLoop_ChildProcess, e: BaseException) -> None:
|
|
321
|
+
p.reject(e)
|
|
322
|
+
|
|
323
|
+
def output(stream: Any, chunk: bytes) -> None:
|
|
324
|
+
out.append(chunk)
|
|
325
|
+
|
|
326
|
+
def errors(stream: Any, chunk: bytes) -> None:
|
|
327
|
+
err.append(chunk)
|
|
328
|
+
|
|
329
|
+
self.once('close', close)
|
|
330
|
+
self.once('error', error)
|
|
331
|
+
self.spawn(args, **options)
|
|
332
|
+
if (stdout := self._stdout) is not None and (stderr := self._stderr) is not None:
|
|
333
|
+
stdout.on('read', output)
|
|
334
|
+
stderr.on('read', errors)
|
|
335
|
+
if (stdin := self._stdin) is not None:
|
|
336
|
+
stdin.write(input).close_gracefully()
|
|
337
|
+
return p
|
|
338
|
+
|
|
339
|
+
def spawn(
|
|
340
|
+
self,
|
|
341
|
+
args: Sequence[str | bytes | os.PathLike[str]],
|
|
342
|
+
*,
|
|
343
|
+
cwd: str | bytes | os.PathLike[str] | None = None,
|
|
344
|
+
detached: bool = False,
|
|
345
|
+
env: Mapping[str, str] | Mapping[bytes, bytes] | None = None,
|
|
346
|
+
) -> Self:
|
|
347
|
+
"""::
|
|
348
|
+
|
|
349
|
+
child = child.spawn(['ls', '-l'])
|
|
350
|
+
child = child.spawn(['php-cgi', 'index.php'], cwd='/var/www', env={'REDIRECT_STATUS': '200'})
|
|
351
|
+
child = child.spawn(['make', 'all'], detached=True)
|
|
352
|
+
|
|
353
|
+
Run a program, the first of ``args``, with the others as its
|
|
354
|
+
arguments, in the directory ``cwd`` and with the environment ``env``,
|
|
355
|
+
the ones of this process by default. With ``detached``, the process
|
|
356
|
+
runs in a session of its own, as the leader of a process group, as
|
|
357
|
+
``detached`` of Node.js, so that :meth:`kill` signals the processes
|
|
358
|
+
that it starts too, and a signal of the terminal of this process, as
|
|
359
|
+
of Ctrl-C, does not reach it. The events ``spawn`` or ``error`` are
|
|
360
|
+
emitted in the next tick of the reactor, so that the ones that are
|
|
361
|
+
subscribed after the call get them too, and the output and the error
|
|
362
|
+
are read from the event ``spawn`` on.
|
|
363
|
+
"""
|
|
364
|
+
reactor = self.reactor
|
|
365
|
+
if os.name == 'nt':
|
|
366
|
+
return self._later('error', Pyjo.Exception.new('Child processes need pipes that the reactor watches'))
|
|
367
|
+
try:
|
|
368
|
+
self._process = process = subprocess.Popen(
|
|
369
|
+
args,
|
|
370
|
+
bufsize=0,
|
|
371
|
+
cwd=cwd,
|
|
372
|
+
env=env,
|
|
373
|
+
start_new_session=detached,
|
|
374
|
+
stdin=subprocess.PIPE,
|
|
375
|
+
stdout=subprocess.PIPE,
|
|
376
|
+
stderr=subprocess.PIPE,
|
|
377
|
+
)
|
|
378
|
+
except OSError as e:
|
|
379
|
+
return self._later('error', e)
|
|
380
|
+
self._detached = detached
|
|
381
|
+
_running[self] = os.getpid()
|
|
382
|
+
_register()
|
|
383
|
+
assert process.stdin is not None and process.stdout is not None and process.stderr is not None
|
|
384
|
+
|
|
385
|
+
# The output and the error, until their end, which the end of the process usually is, read from the event
|
|
386
|
+
# spawn on
|
|
387
|
+
for name, handle in (('_stdout', process.stdout), ('_stderr', process.stderr)):
|
|
388
|
+
os.set_blocking(handle.fileno(), False)
|
|
389
|
+
stream = _Output(handle)
|
|
390
|
+
stream.reactor = reactor
|
|
391
|
+
stream.timeout = 0
|
|
392
|
+
stream.on('close', self._closed)
|
|
393
|
+
setattr(self, name, stream)
|
|
394
|
+
|
|
395
|
+
# The input, which the reactor writes without reading
|
|
396
|
+
os.set_blocking(process.stdin.fileno(), False)
|
|
397
|
+
self._stdin = stdin = _Input(process.stdin)
|
|
398
|
+
stdin.reactor = reactor
|
|
399
|
+
stdin.timeout = 0
|
|
400
|
+
stdin.start()
|
|
401
|
+
|
|
402
|
+
self._wait(_DELAYS[-1])
|
|
403
|
+
ref = weakref.ref(self)
|
|
404
|
+
|
|
405
|
+
# The output and the error are read once the subscribers of the event spawn can read them, as a reactor may
|
|
406
|
+
# read before the next tick
|
|
407
|
+
def spawned(reactor: Any) -> None:
|
|
408
|
+
if (child := ref()) is None or child._process is None:
|
|
409
|
+
return
|
|
410
|
+
for stream in (child._stdout, child._stderr):
|
|
411
|
+
if isinstance(stream, _Output):
|
|
412
|
+
stream.begin()
|
|
413
|
+
child.emit('spawn')
|
|
414
|
+
|
|
415
|
+
reactor.next_tick(spawned)
|
|
416
|
+
return self
|
|
417
|
+
|
|
418
|
+
# The output or the error of the process has ended, so that it has likely exited
|
|
419
|
+
def _closed(self, stream: Any) -> None:
|
|
420
|
+
if (process := self._process) is None:
|
|
421
|
+
return
|
|
422
|
+
self._ended += 1
|
|
423
|
+
if self._ended < 2:
|
|
424
|
+
return
|
|
425
|
+
if process.returncode is not None:
|
|
426
|
+
return self._emit_close()
|
|
427
|
+
self._check = 0
|
|
428
|
+
self._wait(_DELAYS[0])
|
|
429
|
+
|
|
430
|
+
# The process of the parent of a fork, whose pipes the child closes, without the events of its end, as Popen.poll
|
|
431
|
+
# gives the exit code 0 for a process that is not a child of the process that calls it
|
|
432
|
+
def _leave(self) -> None:
|
|
433
|
+
# The poll of a process that is not a child gives it the exit code 0, so that Popen does not warn that it still
|
|
434
|
+
# runs when it is freed
|
|
435
|
+
if (process := self._process) is not None:
|
|
436
|
+
self._process = None
|
|
437
|
+
process.poll()
|
|
438
|
+
if (timer := self._timer) is not None:
|
|
439
|
+
self._timer = None
|
|
440
|
+
# The reactor of an event loop that is gone has no timer to remove, and the pipes are closed all the same
|
|
441
|
+
if self._reactor is not None and (reactor := self._reactor()) is not None:
|
|
442
|
+
reactor.remove(timer)
|
|
443
|
+
for stream in (self._stdin, self._stdout, self._stderr):
|
|
444
|
+
if stream is not None:
|
|
445
|
+
stream.close()
|
|
446
|
+
|
|
447
|
+
def _emit_close(self) -> None:
|
|
448
|
+
_running.pop(self, None)
|
|
449
|
+
self.emit('close', self._exit_code, self._signal_code)
|
|
450
|
+
|
|
451
|
+
def _later(self, name: str, *args: Any) -> Self:
|
|
452
|
+
ref = weakref.ref(self)
|
|
453
|
+
|
|
454
|
+
def emit(reactor: Any) -> None:
|
|
455
|
+
if (child := ref()) is not None:
|
|
456
|
+
child.emit(name, *args)
|
|
457
|
+
|
|
458
|
+
self.reactor.next_tick(emit)
|
|
459
|
+
return self
|
|
460
|
+
|
|
461
|
+
# The end of the process
|
|
462
|
+
def _poll(self, reactor: Any) -> None:
|
|
463
|
+
self._timer = None
|
|
464
|
+
if (process := self._process) is None:
|
|
465
|
+
return
|
|
466
|
+
if (code := process.poll()) is None:
|
|
467
|
+
self._check = min(self._check + 1, len(_DELAYS) - 1)
|
|
468
|
+
return self._wait(_DELAYS[self._check])
|
|
469
|
+
|
|
470
|
+
# The input that the process can read no more
|
|
471
|
+
if (stdin := self._stdin) is not None:
|
|
472
|
+
stdin.close()
|
|
473
|
+
if code < 0:
|
|
474
|
+
self._signal_code = -code
|
|
475
|
+
else:
|
|
476
|
+
self._exit_code = code
|
|
477
|
+
self.emit('exit', self._exit_code, self._signal_code)
|
|
478
|
+
if self._ended >= 2:
|
|
479
|
+
self._emit_close()
|
|
480
|
+
|
|
481
|
+
def _wait(self, after: float) -> None:
|
|
482
|
+
reactor = self.reactor
|
|
483
|
+
if self._timer is not None:
|
|
484
|
+
reactor.remove(self._timer)
|
|
485
|
+
self._timer = reactor.timer(after, self._poll)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# The output or the error of a process, which the reactor reads from the event spawn on, and which keeps whether it is
|
|
489
|
+
# paused until then
|
|
490
|
+
class _Output(Pyjo.IOLoop.Stream.object):
|
|
491
|
+
_begun = False
|
|
492
|
+
|
|
493
|
+
def begin(self) -> None:
|
|
494
|
+
self._begun = True
|
|
495
|
+
paused, self._paused = self._paused, False
|
|
496
|
+
super().start()
|
|
497
|
+
if paused:
|
|
498
|
+
super().stop()
|
|
499
|
+
|
|
500
|
+
def start(self) -> None:
|
|
501
|
+
if self._begun:
|
|
502
|
+
return super().start()
|
|
503
|
+
self._paused = False
|
|
504
|
+
|
|
505
|
+
def stop(self) -> None:
|
|
506
|
+
if self._begun:
|
|
507
|
+
return super().stop()
|
|
508
|
+
self._paused = True
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
# The standard input of a process, which the reactor writes and does not read, so that it closes when a write fails,
|
|
512
|
+
# where a stream finds an error when it reads
|
|
513
|
+
class _Input(Pyjo.IOLoop.Stream.object):
|
|
514
|
+
def start(self) -> None:
|
|
515
|
+
super().start()
|
|
516
|
+
self.stop()
|
|
517
|
+
|
|
518
|
+
# A hang-up or an error of the pipe, which a reactor of poll reports also when it watches for nothing, as the
|
|
519
|
+
# process has closed its end
|
|
520
|
+
def _read_handle(self) -> None:
|
|
521
|
+
self.close()
|
|
522
|
+
|
|
523
|
+
def _write(self) -> None:
|
|
524
|
+
handle = self._handle
|
|
525
|
+
if len(self._buffer):
|
|
526
|
+
try:
|
|
527
|
+
written = os.write(handle.fileno(), self._buffer)
|
|
528
|
+
except (BlockingIOError, InterruptedError):
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
# The process has closed its end
|
|
532
|
+
except OSError:
|
|
533
|
+
return self.close()
|
|
534
|
+
self._written += written
|
|
535
|
+
chunk = bytes(self._buffer[:written])
|
|
536
|
+
del self._buffer[:written]
|
|
537
|
+
self.emit('write', chunk)._again()
|
|
538
|
+
|
|
539
|
+
if not len(self._buffer):
|
|
540
|
+
self.emit('drain')
|
|
541
|
+
if self.is_writing:
|
|
542
|
+
return
|
|
543
|
+
if self._graceful:
|
|
544
|
+
return self.close()
|
|
545
|
+
if self._handle is not None:
|
|
546
|
+
self.reactor.watch(self._handle, False, False)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# The child of a fork leaves the processes of its parent, before its event loop reads their pipes. The handler is
|
|
550
|
+
# registered when a process is spawned, after the one of the reactor, which os.register_at_fork calls first, so that the
|
|
551
|
+
# reactor has moved to a loop of its own, as a loop of epoll shares its state with the parent
|
|
552
|
+
_registered = False
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _register() -> None:
|
|
556
|
+
global _registered
|
|
557
|
+
if not _registered and hasattr(os, 'register_at_fork'):
|
|
558
|
+
_registered = True
|
|
559
|
+
os.register_at_fork(after_in_child=_after_fork_in_child)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _after_fork_in_child() -> None:
|
|
563
|
+
pid = os.getpid()
|
|
564
|
+
for child, owner in list(_running.items()):
|
|
565
|
+
if owner != pid:
|
|
566
|
+
del _running[child]
|
|
567
|
+
try:
|
|
568
|
+
child._leave() # pyright: ignore[reportPrivateUsage]
|
|
569
|
+
except Exception:
|
|
570
|
+
pass
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
# The processes that are left, killed and reaped, of this process, not of the one that forked it
|
|
574
|
+
@atexit.register
|
|
575
|
+
def _kill() -> None:
|
|
576
|
+
pid = os.getpid()
|
|
577
|
+
for child, owner in list(_running.items()):
|
|
578
|
+
if owner != pid or (process := child._process) is None: # pyright: ignore[reportPrivateUsage]
|
|
579
|
+
continue
|
|
580
|
+
if process.poll() is None:
|
|
581
|
+
child.kill(signals.SIGKILL)
|
|
582
|
+
process.wait()
|
|
583
|
+
for stream in (child.stdin, child.stdout, child.stderr):
|
|
584
|
+
if stream is not None:
|
|
585
|
+
stream.close()
|
|
586
|
+
_running.clear()
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
new = Pyjo_IOLoop_ChildProcess.new
|
|
590
|
+
object = Pyjo_IOLoop_ChildProcess
|
|
591
|
+
ChildProcess = Pyjo_IOLoop_ChildProcess
|