kopia lustrzana https://github.com/cirospaciari/socketify.py
Porównaj commity
156 Commity
Autor | SHA1 | Data |
---|---|---|
![]() |
2ed7ec1403 | |
![]() |
b370876074 | |
![]() |
464804edef | |
![]() |
d2f26c682c | |
![]() |
328e249c2c | |
![]() |
d73ed582d5 | |
![]() |
226ae66f6d | |
![]() |
63b1546bbb | |
![]() |
90b08b55d3 | |
![]() |
2f9c267e01 | |
![]() |
30cec004e0 | |
![]() |
488f7bd6ac | |
![]() |
0a3b3fa32f | |
![]() |
70199e8d7d | |
![]() |
833f07b711 | |
![]() |
7ef429ef9f | |
![]() |
c692e29ba2 | |
![]() |
8ca74b05d8 | |
![]() |
df212efc27 | |
![]() |
2a398a3543 | |
![]() |
293b2a97c2 | |
![]() |
2aba41ff7b | |
![]() |
2c51d97097 | |
![]() |
26709f4086 | |
![]() |
e6710b6a9e | |
![]() |
e8b4c21515 | |
![]() |
1edf1f7e78 | |
![]() |
fb1561dd4e | |
![]() |
3b28e0edfe | |
![]() |
263ff096c6 | |
![]() |
dd9bb7512f | |
![]() |
037a29f3e2 | |
![]() |
4a6629c7fe | |
![]() |
87db175640 | |
![]() |
299e36500a | |
![]() |
dc41c89a39 | |
![]() |
fcea3cf783 | |
![]() |
a259258cd7 | |
![]() |
ce1ee6fa94 | |
![]() |
bd753ae0e2 | |
![]() |
9060913d49 | |
![]() |
f2c8ef04b2 | |
![]() |
005b150d4a | |
![]() |
68beb7b40f | |
![]() |
ce55fd194b | |
![]() |
6439307307 | |
![]() |
21c1f0f5fb | |
![]() |
c96c243fd2 | |
![]() |
b3e7b494ab | |
![]() |
34f3d04036 | |
![]() |
375b5e0d14 | |
![]() |
a63b75e30a | |
![]() |
99e716e657 | |
![]() |
2cda5f7243 | |
![]() |
1d57eff163 | |
![]() |
40404519e5 | |
![]() |
98dad2117a | |
![]() |
41af724854 | |
![]() |
d954bad875 | |
![]() |
664b2d9ff1 | |
![]() |
0de0e99b02 | |
![]() |
6bf18b7a79 | |
![]() |
3afb7ec2f2 | |
![]() |
e0e28263f5 | |
![]() |
18892b729d | |
![]() |
27cfb3482b | |
![]() |
3f7c3d4670 | |
![]() |
819b1f62d6 | |
![]() |
043762d141 | |
![]() |
4744855d64 | |
![]() |
8badbc3eb5 | |
![]() |
037673c68d | |
![]() |
23f6e26d26 | |
![]() |
620b853607 | |
![]() |
7adfb90ccf | |
![]() |
f006601588 | |
![]() |
a340dda35b | |
![]() |
cc10a3e4ba | |
![]() |
ed8b0eec3a | |
![]() |
288c1d7e61 | |
![]() |
81f5b1e4a1 | |
![]() |
2ae9059e05 | |
![]() |
f562556844 | |
![]() |
a447904ec7 | |
![]() |
f9a76df2f1 | |
![]() |
ea202f8e4d | |
![]() |
741cff380e | |
![]() |
9fdf8daaf5 | |
![]() |
d2ecb6ccbf | |
![]() |
ba6d8861b4 | |
![]() |
e904e8aed6 | |
![]() |
4794a1a5d6 | |
![]() |
9b8660db01 | |
![]() |
c72704a420 | |
![]() |
a246161a54 | |
![]() |
ebd10964ba | |
![]() |
6c94ad5c8c | |
![]() |
0a78f58783 | |
![]() |
2cb295c161 | |
![]() |
d32f948e22 | |
![]() |
bdddaedda8 | |
![]() |
a84dba6eab | |
![]() |
f04a5fd642 | |
![]() |
2f05a44196 | |
![]() |
f1375cf589 | |
![]() |
6abb96824d | |
![]() |
65e54834e4 | |
![]() |
cf2e4b04a1 | |
![]() |
230bb841ff | |
![]() |
45ff9caf50 | |
![]() |
543e83b561 | |
![]() |
3fd00f3c4a | |
![]() |
c8dca2508d | |
![]() |
66014be04a | |
![]() |
b18d2960a3 | |
![]() |
df53e84351 | |
![]() |
10385ab1bb | |
![]() |
92632b2a3b | |
![]() |
ced5fd120b | |
![]() |
e361580a4c | |
![]() |
0330adc846 | |
![]() |
c119755e27 | |
![]() |
80ecca83af | |
![]() |
2695744790 | |
![]() |
304febc624 | |
![]() |
242fe25118 | |
![]() |
71532dab2d | |
![]() |
606313c724 | |
![]() |
bc61ca9955 | |
![]() |
291af489f1 | |
![]() |
59619dd697 | |
![]() |
3625f3841b | |
![]() |
183f4651c1 | |
![]() |
d122026b65 | |
![]() |
eb65e30d00 | |
![]() |
ca1bb26c80 | |
![]() |
ce75cbb871 | |
![]() |
e73e52bb5b | |
![]() |
5e8fbafc77 | |
![]() |
cb6c8334a1 | |
![]() |
99f54d900d | |
![]() |
981f65c8d7 | |
![]() |
4f585bd84b | |
![]() |
14ab02f5f3 | |
![]() |
e5bf0b201f | |
![]() |
e901433da0 | |
![]() |
6ae34c3b77 | |
![]() |
2f8a0ca6d8 | |
![]() |
e2cfd96e7d | |
![]() |
846414056e | |
![]() |
e65eab6d61 | |
![]() |
e8b1199a64 | |
![]() |
bf0d70aca8 | |
![]() |
2b1f4e40c4 | |
![]() |
d522d6c367 | |
![]() |
83c8cc7258 |
|
@ -25,7 +25,7 @@ jobs:
|
|||
cmake -DCMAKE_BUILD_TYPE=Release -GNinja .. && ninja crypto ssl
|
||||
|
||||
cd ..\..\..\..\
|
||||
cl /MD /W3 /D /EHsc /Zc:__cplusplus /Ox /DLL /D_WINDLL /LD /D "NOMINMAX" /D "WIN32_LEAN_AND_MEAN" /D "UWS_NO_ZLIB" /D "UWS_WITH_PROXY" /D "LIBUS_USE_LIBUV" /I native/src/ /I uWebSockets/src /I uWebSockets/capi /I uWebSockets/uSockets/boringssl/include /D "LIBUS_USE_OPENSSL" /std:c++20 /I C:\vcpkg\packages\libuv_x64-windows-static-md\include /I uWebSockets/uSockets/src /Felibsocketify_windows_amd64.dll ./native/src/libsocketify.cpp uWebSockets/uSockets/src/*.c uWebSockets/uSockets/src/crypto/*.cpp uWebSockets/uSockets/src/eventing/*.c uWebSockets/uSockets/src/crypto/*.c advapi32.lib uWebSockets/uSockets/boringssl/amd64/ssl/ssl.lib uWebSockets/uSockets/boringssl/amd64/crypto/crypto.lib C:\vcpkg\installed\x64-windows-static-md\lib\uv_a.lib iphlpapi.lib userenv.lib psapi.lib user32.lib
|
||||
cl /MD /W3 /D /EHsc /Zc:__cplusplus /Ox /DLL /D_WINDLL /LD /D "NOMINMAX" /D "WIN32_LEAN_AND_MEAN" /D "UWS_NO_ZLIB" /D "UWS_WITH_PROXY" /D "LIBUS_USE_LIBUV" /I native/src/ /I uWebSockets/src /I uWebSockets/capi /I uWebSockets/uSockets/boringssl/include /D "LIBUS_USE_OPENSSL" /std:c++20 /I C:\vcpkg\packages\libuv_x64-windows-static-md\include /I uWebSockets/uSockets/src /Felibsocketify_windows_amd64.dll ./native/src/libsocketify.cpp uWebSockets/uSockets/src/*.c uWebSockets/uSockets/src/crypto/*.cpp uWebSockets/uSockets/src/eventing/*.c uWebSockets/uSockets/src/crypto/*.c advapi32.lib uWebSockets/uSockets/boringssl/amd64/ssl/ssl.lib uWebSockets/uSockets/boringssl/amd64/crypto/crypto.lib C:\vcpkg\installed\x64-windows-static-md\lib\libuv.lib iphlpapi.lib userenv.lib psapi.lib user32.lib shell32.lib dbghelp.lib ole32.lib uuid.lib ws2_32.lib
|
||||
|
||||
git add libsocketify_windows_amd64.dll
|
||||
git config --global user.email "ciro.spaciari@gmail.com"
|
||||
|
|
|
@ -4,4 +4,6 @@ __pycache__
|
|||
/dist
|
||||
*.o
|
||||
node_modules/
|
||||
yarn.lock
|
||||
yarn.lock
|
||||
.vscode
|
||||
/venv
|
81
README.md
81
README.md
|
@ -1,22 +1,25 @@
|
|||
# socketify.py
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/cirospaciari/socketify.py"><img src="https://raw.githubusercontent.com/cirospaciari/socketify.py/main/misc/logo.png" alt="Logo" height=170></a>
|
||||
<br />
|
||||
<br />
|
||||
<a href="https://github.com/cirospaciari/socketify.py/actions/workflows/linux.yml" target="_blank"><img src="https://github.com/cirospaciari/socketify.py/actions/workflows/linux.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/cirospaciari/socketify.py/actions/workflows/windows.yml" target="_blank"><img src="https://github.com/cirospaciari/socketify.py/actions/workflows/windows.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/cirospaciari/socketify.py/actions/workflows/macos.yml" target="_blank"><img src="https://github.com/cirospaciari/socketify.py/actions/workflows/macos.yml/badge.svg" /></a>
|
||||
<a href="https://github.com/cirospaciari/socketify.py/actions/workflows/macos_arm64.yml" target="_blank"><img src="https://github.com/cirospaciari/socketify.py/actions/workflows/macos_arm64.yml/badge.svg" /></a>
|
||||
<br/>
|
||||
<a href='https://github.com/cirospaciari/socketify.py'><img alt='GitHub Clones' src='https://img.shields.io/badge/dynamic/json?color=success&label=Clones&query=count&url=https://gist.githubusercontent.com/cirospaciari/2243d59951f4abe4fd2000f1e20bc561/raw/clone.json&logo=github'></a>
|
||||
<a href='https://pypi.org/project/socketify/' target="_blank"><img alt='PyPI Downloads' src='https://static.pepy.tech/personalized-badge/socketify?period=total&units=international_system&left_color=grey&right_color=brightgreen&left_text=Downloads'></a>
|
||||
<a href="https://github.com/sponsors/cirospaciari/" target="_blank"><img src="https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/cirospaciari"/></a>
|
||||
|
||||
|
||||
<a href='https://discord.socketify.dev/' target="_blank"><img alt='Discord' src='https://img.shields.io/discord/1042529276219641906?label=Discord'></a>
|
||||
</p>
|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://docs.socketify.dev">Documentation</a>
|
||||
<span> • </span>
|
||||
<a href="https://discord.socketify.dev/">Discord</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/cirospaciari/socketify.py/issues">Issues</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/cirospaciari/socketify.py/tree/main/examples">Examples</a>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
## 💡 Features
|
||||
|
||||
|
@ -34,12 +37,14 @@
|
|||
- Max Backpressure, Max Timeout, Max Payload and Idle Timeout Support
|
||||
- Automatic Ping / Pong Support
|
||||
- Per Socket Data
|
||||
- Middlewares
|
||||
- Templates Support (examples with [`Mako`](https://github.com/cirospaciari/socketify.py/tree/main/examples/template_mako.py) and [`Jinja2`](https://github.com/cirospaciari/socketify.py/tree/main/examples/template_jinja2.py))
|
||||
- ASGI Server with pub/sub extension for Falcon
|
||||
- WSGI Server
|
||||
- [`Middlewares`](https://docs.socketify.dev/middlewares.html)
|
||||
- [`Templates`](https://docs.socketify.dev/templates.html) Support (examples with [`Mako`](https://github.com/cirospaciari/socketify.py/tree/main/examples/template_mako.py) and [`Jinja2`](https://github.com/cirospaciari/socketify.py/tree/main/examples/template_jinja2.py))
|
||||
- [`ASGI Server`](https://docs.socketify.dev/cli.html)
|
||||
- [`WSGI Server`](https://docs.socketify.dev/cli.html)
|
||||
- [`Plugins/Extensions`](https://docs.socketify.dev/extensions.html)
|
||||
|
||||
## :mag_right: Upcoming Features
|
||||
|
||||
- In-Memory Cache Tools
|
||||
- Fetch like API powered by libuv
|
||||
- Async file IO powered by libuv
|
||||
|
@ -55,6 +60,7 @@ We created and adapted the full C API from [uNetworking/uWebSockets](https://git
|
|||
Join Github [`Discussions`](https://github.com/cirospaciari/socketify.py/discussions) or [`Discord`](https://discord.socketify.dev/) for help and have a look at the development progress.
|
||||
|
||||
## :zap: Benchmarks
|
||||
|
||||
Socketify WebFramework HTTP requests per second (Linux x64)
|
||||
|
||||

|
||||
|
@ -71,14 +77,14 @@ WebSocket messages per second (Linux x64)
|
|||
|
||||

|
||||
|
||||
|
||||
Http tested with TFB tool plaintext benchmark<br/>
|
||||
WebSocket tested with [Bun.sh](https://bun.sh) bench chat-client <br/>
|
||||
Source code in [bench](https://github.com/cirospaciari/socketify.py/tree/main/bench) and in [TechEmPower](https://github.com/TechEmpower/FrameworkBenchmarks)<br/>
|
||||
Source code in [TechEmPower](https://github.com/TechEmpower/FrameworkBenchmarks) and for websockets in [bench](https://github.com/cirospaciari/socketify.py/tree/main/bench)<br/>
|
||||
|
||||
Machine OS: Debian GNU/Linux bookworm/sid x86_64 Kernel: 6.0.0-2-amd64 CPU: Intel i7-7700HQ (8) @ 3.800GHz Memory: 32066MiB
|
||||
Machine OS: Debian GNU/Linux bookworm/sid x86_64 Kernel: 6.0.0-2-amd64 CPU: Intel i7-7700HQ (8) @ 3.800GHz Memory: 32066MiB
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
For macOS x64 & Silicon, Linux x64, Windows
|
||||
|
||||
```bash
|
||||
|
@ -90,9 +96,11 @@ pypy3 -m pip install -e socketify
|
|||
```
|
||||
|
||||
Using install via requirements.txt
|
||||
|
||||
```text
|
||||
socketify
|
||||
```
|
||||
|
||||
```bash
|
||||
pip install -r ./requirements.txt
|
||||
#or specify PyPy3
|
||||
|
@ -102,19 +110,28 @@ pypy3 -m pip install -r ./requirements.txt
|
|||
If you are using linux or macOS, you may need to install libuv and zlib in your system
|
||||
|
||||
macOS
|
||||
|
||||
```bash
|
||||
brew install libuv
|
||||
brew install zlib
|
||||
```
|
||||
|
||||
Linux
|
||||
Linux (Ubuntu/Debian)
|
||||
|
||||
```bash
|
||||
apt install libuv1 zlib1g
|
||||
```
|
||||
|
||||
Linux (RHEL/OEL)
|
||||
|
||||
```bash
|
||||
yum install cmake zlib-devel libuv-devel
|
||||
```
|
||||
|
||||
## 🤔 Usage
|
||||
|
||||
Hello world app
|
||||
|
||||
```python
|
||||
from socketify import App
|
||||
|
||||
|
@ -125,6 +142,7 @@ app.run()
|
|||
```
|
||||
|
||||
SSL version sample
|
||||
|
||||
``` python
|
||||
from socketify import App, AppOptions
|
||||
|
||||
|
@ -135,8 +153,9 @@ app.run()
|
|||
```
|
||||
|
||||
WebSockets
|
||||
|
||||
```python
|
||||
from socketify import App, AppOptions, OpCode, CompressOptions
|
||||
from socketify import App, OpCode, CompressOptions
|
||||
|
||||
def ws_open(ws):
|
||||
print('A WebSocket got connected!')
|
||||
|
@ -153,8 +172,9 @@ app.ws("/*", {
|
|||
'idle_timeout': 12,
|
||||
'open': ws_open,
|
||||
'message': ws_message,
|
||||
'drain': lambda ws: print('WebSocket backpressure: %i' % ws.get_buffered_amount()),
|
||||
'close': lambda ws, code, message: print('WebSocket closed')
|
||||
'drain': lambda ws: print(f'WebSocket backpressure: {ws.get_buffered_amount()}'),
|
||||
'close': lambda ws, code, message: print('WebSocket closed'),
|
||||
'subscription': lambda ws, topic, subscriptions, subscriptions_before: print(f'subscribe/unsubscribe on topic {topic} {subscriptions} {subscriptions_before}'),
|
||||
})
|
||||
app.any("/", lambda res,req: res.end("Nothing to see here!'"))
|
||||
app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % (config.port)))
|
||||
|
@ -164,6 +184,7 @@ app.run()
|
|||
We have more than 20 examples [click here](https://github.com/cirospaciari/socketify.py/tree/main/examples) for more
|
||||
|
||||
## :hammer: Building from source
|
||||
|
||||
```bash
|
||||
#clone and update submodules
|
||||
git clone https://github.com/cirospaciari/socketify.py.git
|
||||
|
@ -180,6 +201,7 @@ pypy3 -m pip uninstall socketify
|
|||
```
|
||||
|
||||
## :briefcase: Commercially supported
|
||||
|
||||
I'm a Brazilian consulting & contracting company dealing with anything related with [socketify.py](https://github.com/cirospaciari/socketify.py) and [socketify.rb](https://github.com/cirospaciari/socketify.rb)
|
||||
|
||||
Don't hesitate sending a mail if you are in need of advice, support, or having other business inquiries in mind. We'll figure out what's best for both parties.
|
||||
|
@ -187,28 +209,25 @@ Don't hesitate sending a mail if you are in need of advice, support, or having o
|
|||
Special thank's to [uNetworking AB](https://github.com/uNetworking) to develop [uWebSockets](https://github.com/uNetworking/uWebSockets), [uSockets](https://github.com/uNetworking/uSockets) and allow us to bring this features and performance to Python and PyPy
|
||||
|
||||
## :heart: Sponsors
|
||||
If you like to see this project thrive, you can sponsor us on GitHub too. We need all the help we can get
|
||||
|
||||
If you like to see this project thrive, you can sponsor us on GitHub too. We need all the help we can get.
|
||||
|
||||
Thank you [`Otavio Augusto`](https://github.com/middlebaws) to be the first sponsor of this project!
|
||||
|
||||
<a href="https://github.com/sponsors/cirospaciari/" target="_blank"><img src="https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/cirospaciari"/></a>
|
||||
|
||||
## :star: Stargazers
|
||||
|
||||
[](https://github.com/cirospaciari/socketify.py/stargazers)
|
||||
|
||||
## :wrench: Forkers
|
||||
|
||||
[](https://github.com/cirospaciari/socketify.py/network/members)
|
||||
|
||||
|
||||
## :question: socketify.py vs japronto
|
||||
|
||||
People really want to compare with japronto, but this projects are not really comparable. Socketify is an active project and will be maintained over time with security updates and new features, japronto don't get any github updates since 2020 and don't get any src update since 2018, japronto don't support SSL, WebSockets, [`PyPy3`](https://www.pypy.org/), Windows or macOS Silicon, socketify will support Http3 and a lot more features.
|
||||
|
||||
And yes, we can be faster than japronto when all our features and goals are achieved, and we are probably faster than any current maintained solution out there.
|
||||
|
||||
## :grey_question: uvloop
|
||||
|
||||
We don't use uvloop, because uvloop don't support Windows and PyPy3 at this moment, this can change in the future, but right now we want to implement our own libuv + asyncio solution, and a lot more.
|
||||
|
||||
## :dizzy: CFFI vs Cython vs HPy
|
||||
Cython performs really well on Python3 but really bad on PyPy3, CFFI are chosen for better support PyPy3 until we got our hands on an stable [`HPy`](https://hpyproject.org/) integration.
|
||||
|
||||
## :bookmark_tabs: Documentation
|
||||
See the full docs in [docs.socketify.dev](https://docs.socketify.dev) or in [/docs/README.md](docs/README.md)
|
||||
Cython performs really well on Python3 but really bad on PyPy3, CFFI are chosen for better support PyPy3 until we got our hands on a stable [`HPy`](https://hpyproject.org/) integration.
|
||||
|
|
2
SSGI.md
2
SSGI.md
|
@ -15,7 +15,7 @@ class SSGIHttpResponse:
|
|||
# send chunk of data, can be used to perform with less backpressure than using send
|
||||
# total_size is the sum of all lengths in bytes of all chunks to be sended
|
||||
# connection will end when total_size is met
|
||||
# returns tuple(bool, bool) first bool represents if the chunk is succefully sended, the second if the connection has ended
|
||||
# returns tuple(bool, bool) first bool represents if the chunk is successfully sended, the second if the connection has ended
|
||||
def send_chunk(self, chunk: Union[str, bytes, bytearray, memoryview], total_size: int = False) -> Awaitable:
|
||||
pass
|
||||
|
||||
|
|
|
@ -8,12 +8,11 @@ class Home:
|
|||
resp.content_type = falcon.MEDIA_TEXT # Default is JSON, so override
|
||||
resp.text = "Hello, World!"
|
||||
async def on_post(self, req, resp):
|
||||
# curl -d '{"key1":"value1", "key2":"value2"}' -H "Content-Type: application/json" -X POST http://localhost:8000/
|
||||
raw_data = await req.stream.read()
|
||||
print("data", raw_data)
|
||||
# curl -d '{"name":"test"}' -H "Content-Type: application/json" -X POST http://localhost:8000/
|
||||
json = await req.media
|
||||
resp.status = falcon.HTTP_200 # This is the default status
|
||||
resp.content_type = falcon.MEDIA_TEXT # Default is JSON, so override
|
||||
resp.text = raw_data
|
||||
resp.text = json.get("name", "")
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -8,11 +8,10 @@ class Home:
|
|||
resp.content_type = falcon.MEDIA_TEXT # Default is JSON, so override
|
||||
resp.text = "Hello, World!"
|
||||
def on_post(self, req, resp):
|
||||
raw_data = req.stream.getvalue()
|
||||
print("data", raw_data)
|
||||
raw_data = req.stream.read()
|
||||
resp.status = falcon.HTTP_200 # This is the default status
|
||||
resp.content_type = falcon.MEDIA_TEXT # Default is JSON, so override
|
||||
resp.text = raw_data
|
||||
resp.text = 'Ok'
|
||||
|
||||
|
||||
|
||||
|
@ -23,4 +22,4 @@ home = Home()
|
|||
app.add_route("/", home)
|
||||
|
||||
if __name__ == "__main__":
|
||||
WSGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(workers=8)
|
||||
WSGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(workers=1)
|
|
@ -1,6 +1,5 @@
|
|||
from socketify import ASGI
|
||||
|
||||
|
||||
async def app(scope, receive, send):
|
||||
assert scope['type'] == 'http'
|
||||
|
||||
|
@ -20,4 +19,4 @@ async def app(scope, receive, send):
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ASGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(8)
|
||||
ASGI(app, lifespan=False).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(1)
|
||||
|
|
|
@ -1,8 +1,70 @@
|
|||
from socketify import WSGI
|
||||
payload = None
|
||||
with open("xml.zip", "rb") as file:
|
||||
payload = file.read()
|
||||
|
||||
chunk_size = 64 * 1024
|
||||
content_length = len(payload)
|
||||
|
||||
def app_chunked(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'application/zip'), ('Transfer-Encoding', 'chunked')])
|
||||
|
||||
sended = 0
|
||||
while content_length > sended:
|
||||
end = sended + chunk_size
|
||||
yield payload[sended:end]
|
||||
sended = end
|
||||
|
||||
|
||||
def app(environ, start_response):
|
||||
start_response('200 OK', [('Content-Type', 'application/zip'), ('Content-Length', str(content_length))])
|
||||
|
||||
sended = 0
|
||||
while content_length > sended:
|
||||
end = sended + chunk_size
|
||||
yield payload[sended:end]
|
||||
sended = end
|
||||
|
||||
# import gc
|
||||
|
||||
# gc.collect()
|
||||
# gc.set_threshold(50, 3, 3)
|
||||
|
||||
# import tracemalloc
|
||||
|
||||
# tracemalloc.start()
|
||||
|
||||
def app_hello(environ, start_response):
|
||||
# start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', '13')])
|
||||
start_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
yield b'Hello, World!\n'
|
||||
|
||||
return [ b'Hello, World!']
|
||||
|
||||
if __name__ == "__main__":
|
||||
WSGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(8)
|
||||
# import fastwsgi
|
||||
# fastwsgi.run(wsgi_app=app_hello, host='127.0.0.1', port=8000, loglevel=0)
|
||||
# from meinheld import server
|
||||
# server.listen(("0.0.0.0", 8000))
|
||||
# server.run(app_hello)
|
||||
from socketify import WSGI
|
||||
WSGI(app_hello).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(1)
|
||||
# def run_app():
|
||||
# import fastwsgi
|
||||
# fastwsgi.run(wsgi_app=app_hello, host='127.0.0.1', port=8000)
|
||||
|
||||
# import os
|
||||
# pid_list = []
|
||||
# # fork limiting the cpu count - 1
|
||||
# for _ in range(1, 8):
|
||||
# pid = os.fork()
|
||||
# # n greater than 0 means parent process
|
||||
# if not pid > 0:
|
||||
# run_app()
|
||||
# break
|
||||
# pid_list.append(pid)
|
||||
|
||||
# run_app() # run app on the main process too :)
|
||||
|
||||
# # sigint everything to graceful shutdown
|
||||
# import signal
|
||||
# for pid in pid_list:
|
||||
# os.kill(pid, signal.SIGINT)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
while true
|
||||
do
|
||||
wrk -t1 -c200 -d1 -H 'Connection: keep-alive' http://127.0.0.1:8000 > /dev/null
|
||||
done
|
|
@ -0,0 +1,83 @@
|
|||
import sys
|
||||
import io
|
||||
import time
|
||||
import datetime
|
||||
import socket
|
||||
import optparse
|
||||
|
||||
parser = optparse.OptionParser("usage: %prog [options]", add_help_option=False)
|
||||
parser.add_option("-h", "--host", dest="host", default='127.0.0.1', type="string")
|
||||
parser.add_option("-p", "--port", dest="port", default=3000, type="int")
|
||||
(opt, args) = parser.parse_args()
|
||||
|
||||
def get_request(path = r'/', host = '127.0.0.1', port = 3000):
|
||||
req = f'GET {path}' + r' HTTP/1.1' + '\r\n'
|
||||
req += f'Host: {host}:{port}\r\n'
|
||||
req += r'User-Agent: curl/7.66.0' + '\r\n'
|
||||
req += r'Accept: */*' + '\r\n'
|
||||
req += '\r\n'
|
||||
return req
|
||||
|
||||
payload_tiny = get_request(host = opt.host, port = opt.port)
|
||||
payload_tiny = payload_tiny.encode('utf-8')
|
||||
|
||||
def create_sock(timeout = 0.001):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
sock.connect((opt.host, opt.port))
|
||||
return sock
|
||||
|
||||
sock = create_sock()
|
||||
sock.sendall(payload_tiny)
|
||||
time.sleep(0.020)
|
||||
resp = sock.recv(4096)
|
||||
print('====== response ========')
|
||||
print(resp.decode('utf-8'))
|
||||
print('========================')
|
||||
sock.close()
|
||||
|
||||
start_time = datetime.datetime.now()
|
||||
test1_limit = start_time + datetime.timedelta(seconds = 1)
|
||||
test2_limit = test1_limit + datetime.timedelta(seconds = 10)
|
||||
|
||||
sock = create_sock()
|
||||
while True:
|
||||
if datetime.datetime.now() >= test1_limit:
|
||||
break
|
||||
sock.sendall(payload_tiny)
|
||||
try:
|
||||
resp = sock.recv(4096)
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
print(f'Test 1 completed!')
|
||||
sock.close()
|
||||
|
||||
req_num = 1000*1000
|
||||
payload_huge = payload_tiny * req_num
|
||||
#print(len(payload_huge))
|
||||
print(f'Run test 2 ...')
|
||||
totalsent = 0
|
||||
totalresp = b''
|
||||
sock = create_sock()
|
||||
while True:
|
||||
if datetime.datetime.now() >= test2_limit:
|
||||
print(f'Test 2: Timeout exceeded!')
|
||||
break
|
||||
try:
|
||||
rc = sock.send(payload_huge[totalsent:])
|
||||
if rc == 0:
|
||||
#raise RuntimeError("socket connection broken")
|
||||
pass
|
||||
totalsent += rc
|
||||
resp = sock.recv(65*1024)
|
||||
totalresp += resp
|
||||
except socket.timeout:
|
||||
pass
|
||||
except ConnectionResetError:
|
||||
print(f'totalsent = {totalsent}, totalrecv = {len(totalresp)}')
|
||||
print(f'LastResp: {totalresp[-256:]}')
|
||||
raise
|
||||
|
||||
sock.close()
|
||||
print("==== Test Finish =====")
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
from quart import Quart
|
||||
|
||||
|
||||
app = Quart(__name__)
|
||||
|
||||
@app.get("/")
|
||||
async def plaintext():
|
||||
return "Hello, World!", {"Content-Type": "text/plain"}
|
||||
|
||||
# Quart perform really baddly for sure needs more optimizations, but socketify ASGI + PyPy performs better than uvicorn+httptools+gunicorn
|
|
@ -2,12 +2,15 @@ from socketify import App
|
|||
import os
|
||||
import multiprocessing
|
||||
import asyncio
|
||||
|
||||
def run_app():
|
||||
app = App(request_response_factory_max_items=200_000)
|
||||
router = app.router()
|
||||
|
||||
@router.get("/")
|
||||
async def home(res, req):
|
||||
res.end("Hello, World!")
|
||||
|
||||
app.get("/", home)
|
||||
res.send(b"Hello, World!")
|
||||
|
||||
app.listen(
|
||||
8000,
|
||||
lambda config: print(
|
||||
|
@ -26,7 +29,7 @@ def create_fork():
|
|||
|
||||
|
||||
# fork limiting the cpu count - 1
|
||||
for i in range(1, multiprocessing.cpu_count()):
|
||||
create_fork()
|
||||
# for i in range(1, multiprocessing.cpu_count()):
|
||||
# create_fork()
|
||||
|
||||
run_app() # run app on the main process too :)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from socketify import App, AppOptions, OpCode, CompressOptions
|
||||
|
||||
remaining_clients = 16
|
||||
app = App(websocket_factory_max_itens=1_500_000)
|
||||
app = App(websocket_factory_max_items=1_500_000)
|
||||
|
||||
|
||||
def ws_open(ws):
|
||||
|
|
|
@ -11,14 +11,24 @@
|
|||
<a href="https://github.com/cirospaciari/socketify.py/actions/workflows/macos_arm64.yml" target="_blank"><img src="https://github.com/cirospaciari/socketify.py/actions/workflows/macos_arm64.yml/badge.svg" /></a>
|
||||
<br/>
|
||||
<a href='https://github.com/cirospaciari/socketify.py'><img alt='GitHub Clones' src='https://img.shields.io/badge/dynamic/json?color=success&label=Clones&query=count&url=https://gist.githubusercontent.com/cirospaciari/2243d59951f4abe4fd2000f1e20bc561/raw/clone.json&logo=github'></a>
|
||||
<a href='https://pypi.org/project/socketify/' target="_blank"><img alt='PyPI Downloads' src='https://static.pepy.tech/personalized-badge/socketify?period=total&units=international_system&left_color=grey&right_color=brightgreen&left_text=Downloads'></a>
|
||||
<a href="https://github.com/sponsors/cirospaciari/" target="_blank"><img src="https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/cirospaciari"/></a>
|
||||
|
||||
|
||||
<a href='https://discord.socketify.dev/' target="_blank"><img alt='Discord' src='https://img.shields.io/discord/1042529276219641906?label=Discord'></a>
|
||||
</p>
|
||||
<br/>
|
||||
<div align="center">
|
||||
<a href="https://github.com/cirospaciari/socketify.py">Github</a>
|
||||
<span> • </span>
|
||||
<a href="https://discord.socketify.dev/">Discord</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/cirospaciari/socketify.py/issues">Issues</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/cirospaciari/socketify.py/tree/main/examples">Examples</a>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
Socketify.py is a reliable, high-performance Python web framework for building large-scale app backends and microservices.
|
||||
With no precedents websocket performance and an really fast HTTP server that can delivery encrypted TLS 1.3 quicker than most alternative servers can do even unencrypted, cleartext messaging.
|
||||
With no precedents websocket performance and a really fast HTTP server that can delivery encrypted TLS 1.3 quicker than most alternative servers can do even unencrypted, cleartext messaging.
|
||||
|
||||
## Summary
|
||||
|
||||
|
@ -34,6 +44,8 @@ With no precedents websocket performance and an really fast HTTP server that can
|
|||
- [Templates](templates.md)
|
||||
- [GraphiQL](graphiql.md)
|
||||
- [WebSockets and Backpressure](websockets-backpressure.md)
|
||||
- [Plugins / Extensions](extensions.md)
|
||||
- [SSL](ssl.md)
|
||||
- [CLI Reference](cli.md)
|
||||
- [CLI, ASGI and WSGI](cli.md)
|
||||
- [API Reference](api.md)
|
||||
- [Examples](examples.md)
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
- [Templates](templates.md)
|
||||
- [GraphiQL](graphiql.md)
|
||||
- [WebSockets and Backpressure](websockets-backpressure.md)
|
||||
- [Plugins / Extensions](extensions.md)
|
||||
- [SSL](ssl.md)
|
||||
- [CLI Reference](cli.md)
|
||||
- [CLI, ASGI and WSGI](cli.md)
|
||||
- [API Reference](api.md)
|
||||
- [Examples](examples.md)
|
||||
|
|
32
docs/api.md
32
docs/api.md
|
@ -2,8 +2,15 @@
|
|||
```python
|
||||
|
||||
class App:
|
||||
def __init__(self, options=None):
|
||||
def __init__(self, options=None, request_response_factory_max_items=0, websocket_factory_max_items=0, task_factory_max_items=100_000, lifespan=True):
|
||||
|
||||
def on_start(self, method: callable):
|
||||
def on_shutdown(self, method: callable):
|
||||
def on_error(self, method: callable):
|
||||
def router(self, prefix: str="", *middlewares):
|
||||
def register(self, extension):
|
||||
def template(self, template_engine):
|
||||
def json_serializer(self, json_serializer):
|
||||
def static(self, route, directory):
|
||||
def get(self, path, handler):
|
||||
def post(self, path, handler):
|
||||
|
@ -31,11 +38,12 @@ class App:
|
|||
|
||||
```
|
||||
|
||||
## AppResponse
|
||||
## Response
|
||||
```python
|
||||
class AppResponse:
|
||||
def __init__(self, response, loop, ssl, render=None):
|
||||
class Response:
|
||||
def __init__(self, response, app):
|
||||
def cork(self, callback):
|
||||
def close(self):
|
||||
def set_cookie(self, name, value, options={}):
|
||||
def run_async(self, task):
|
||||
async def get_form_urlencoded(self, encoding="utf-8"):
|
||||
|
@ -53,6 +61,8 @@ class AppResponse:
|
|||
def get_remote_address(self):
|
||||
def get_proxied_remote_address_bytes(self):
|
||||
def get_proxied_remote_address(self):
|
||||
def cork_send(self, message: any, content_type: str = b'text/plain', status : str | bytes | int = b'200 OK', headers=None, end_connection=False):
|
||||
def send(self, message: any = b"", content_type: str = b'text/plain', status : str | bytes | int = b'200 OK', headers=None, end_connection=False):
|
||||
def end(self, message, end_connection=False):
|
||||
def pause(self):
|
||||
def resume(self):
|
||||
|
@ -78,10 +88,10 @@ class AppResponse:
|
|||
def __del__(self):
|
||||
```
|
||||
|
||||
## AppRequest
|
||||
## Request
|
||||
```python
|
||||
class AppRequest:
|
||||
def __init__(self, request):
|
||||
class Request:
|
||||
def __init__(self, request, app):
|
||||
def get_cookie(self, name):
|
||||
def get_url(self):
|
||||
def get_full_url(self):
|
||||
|
@ -123,11 +133,13 @@ class AppOptions:
|
|||
```python
|
||||
|
||||
class WebSocket:
|
||||
def __init__(self, websocket, ssl, loop):
|
||||
def __init__(self, websocket, app):
|
||||
|
||||
# uuid for socket data, used to free data after socket closes
|
||||
def get_user_data_uuid(self):
|
||||
def get_user_data(self):
|
||||
# clone the current instance to preserve it (you need to watch for closed connections when using it)
|
||||
def clone(self):
|
||||
def get_buffered_amount(self):
|
||||
def subscribe(self, topic):
|
||||
def unsubscribe(self, topic):
|
||||
|
@ -217,4 +229,6 @@ class MiddlewareRouter:
|
|||
def connect(self, path, handler):
|
||||
def trace(self, path, handler):
|
||||
def any(self, path, handler):
|
||||
```
|
||||
```
|
||||
|
||||
### Next [Examples](examples.md)
|
||||
|
|
105
docs/basics.md
105
docs/basics.md
|
@ -7,8 +7,15 @@ This section is to show the basics of `AppResponse` and `AppRequest`
|
|||
`res.write(message)` were message can be String, bytes or an Object that can be converted to json, send the message to the response without ending.
|
||||
|
||||
`res.cork_end(message, end_connection=False)` or `res.end(message, end_connection=False)` were message can be String, bytes or an Object that can be converted to json, send the message to the response and end the response.
|
||||
|
||||
The above `res.end()` or `res.cork_end()` call will actually call three separate send functions; res.write_status, res.write_header and whatever it does itself. By wrapping the call in `res.cork` or `res.cork_end` you make sure these three send functions are efficient and only result in one single send syscall and one single SSL block if using SSL.
|
||||
|
||||
|
||||
`res.send(message, content_type=b'text/plain, status=b'200 OK', headers=None, end_connection=False)` and `res.cork_send(message, content_type=b'text/plain', status=b'200 OK', headers=None, end_connection=False)`
|
||||
combines `res.write_status()`, `res.write_headers()`, and `res.end()` in a way that is easier to use, if you want to send all in one call just using named parameters. Headers can receive any iterator of iterators/tuple like `iter(tuple(str, str))` where the first value is the header name and the following the value, using `res.cork_send` will make sure to send all the
|
||||
data in a corked state.
|
||||
|
||||
|
||||
Using `res.write_continue()` writes HTTP/1.1 100 Continue as response
|
||||
|
||||
|
||||
|
@ -51,9 +58,25 @@ def not_found(res, req):
|
|||
res.write_status(404).end("Not Found")
|
||||
|
||||
def ok(res, req):
|
||||
res.write_status("200 OK").end("Not Found")
|
||||
res.write_status("200 OK").end("OK")
|
||||
```
|
||||
|
||||
### Using send
|
||||
```python
|
||||
def not_found(res, req):
|
||||
res.send("Not Found", status=404)
|
||||
|
||||
def ok(res, req):
|
||||
res.send("OK", status="200 OK")
|
||||
|
||||
def json(res, req):
|
||||
res.send({"Hello", "World!"})
|
||||
|
||||
def with_headers(res, req):
|
||||
res.send({"Hello": "World!"}, headers=(("X-Rate-Limit-Remaining", "10"), (b'Another-Headers', b'Value')))
|
||||
```
|
||||
|
||||
|
||||
### Check the URL or Method
|
||||
`req.get_full_url()` will return the path with query string
|
||||
`req.get_url()` will return the path without query string
|
||||
|
@ -182,6 +205,22 @@ def route_handler(res, req):
|
|||
res.run_async(sendfile(res, req, "my_text"))
|
||||
```
|
||||
|
||||
|
||||
## Using ujson, orjson or any custom JSON serializer
|
||||
socketify by default uses built-in `json` module with has great performance on PyPy, but if you wanna use another module instead of the default you can just register using `app.json_serializer(module)`
|
||||
|
||||
```python
|
||||
from socketify import App
|
||||
import ujson
|
||||
app = App()
|
||||
|
||||
# set json serializer to ujson
|
||||
# json serializer must have dumps and loads functions
|
||||
app.json_serializer(ujson)
|
||||
|
||||
app.get("/", lambda res, req: res.end({"Hello":"World!"}))
|
||||
```
|
||||
|
||||
## Raw socket pointer
|
||||
|
||||
If for some reason you need the raw socket pointer you can use `res.get_native_handle()` and will get an CFFI handler.
|
||||
|
@ -194,4 +233,68 @@ If you need to access the raw pointer of `libuv` you can use `app.get_native_han
|
|||
HttpRequest object being stack-allocated and only valid in one single callback invocation so only valid in the first "segment" before the first await.
|
||||
If you just want to preserve headers, url, method, cookies and query string you can use `req.preserve()` to copy all data and keep it in the request object, but will be some performance penalty.
|
||||
|
||||
|
||||
# Lifespan / Lifecycle events
|
||||
You can use socketify start and shutdown events to create/clean thread pools, connections pools, etc when the application starts or shutdown itself.
|
||||
|
||||
If any exception occurs in the start event the application will continue and start normally,
|
||||
if you want to fail a start you need to catch the exception and use `sys.exit(1)` to shut down prematurely.
|
||||
|
||||
Both `app.on_start` and `app.on_shutdown` can be sync or async.
|
||||
|
||||
|
||||
```python
|
||||
from socketify import App
|
||||
|
||||
def run(app: App):
|
||||
|
||||
@app.on_start
|
||||
async def on_start():
|
||||
print("wait...")
|
||||
await asyncio.sleep(1)
|
||||
print("start!")
|
||||
|
||||
@app.on_shutdown
|
||||
async def on_shutdown():
|
||||
print("wait...")
|
||||
await asyncio.sleep(1)
|
||||
print("shutdown!")
|
||||
|
||||
router = app.router()
|
||||
|
||||
@router.get("/")
|
||||
def home(res, req):
|
||||
res.send("Hello, World!")
|
||||
|
||||
```
|
||||
|
||||
# Error handler events
|
||||
You can set a error handler to give the user an custom 500 page and/or for logging properly
|
||||
|
||||
Using `app.set_error_handler(on_error)` or `app.on_error` decorator.
|
||||
|
||||
|
||||
```python
|
||||
from socketify import App
|
||||
|
||||
def run(app: App):
|
||||
|
||||
@app.on_error
|
||||
def on_error(error, res, req):
|
||||
# here you can log properly the error and do a pretty response to your clients
|
||||
print("Somethind goes %s" % str(error))
|
||||
# response and request can be None if the error is in an async function
|
||||
if res != None:
|
||||
# if response exists try to send something
|
||||
res.write_status(500)
|
||||
res.end("Sorry we did something wrong")
|
||||
|
||||
router = app.router()
|
||||
|
||||
@router.get("/")
|
||||
def home(res, req):
|
||||
raise RuntimeError("Oops!")
|
||||
|
||||
|
||||
```
|
||||
### Next [Upload and Post](upload-post.md)
|
|
@ -30,7 +30,7 @@ Options:
|
|||
--ws-close-on-backpressure-limit BOOLEAN Close connections that hits maximum backpressure [default: False]
|
||||
--lifespan [auto|on|off] Lifespan implementation. [default: auto]
|
||||
--interface [auto|asgi|asgi3|wsgi|ssgi|socketify] Select ASGI (same as ASGI3), ASGI3, WSGI or SSGI as the application interface. [default: auto]
|
||||
--disable-listen-log BOOLEAN Disable log when start listenning [default: False]
|
||||
--disable-listen-log BOOLEAN Disable log when start listening [default: False]
|
||||
--version or -v Display the socketify.py version and exit.
|
||||
--ssl-keyfile TEXT SSL key file
|
||||
--ssl-certfile TEXT SSL certificate file
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
## 📚 Examples
|
||||
|
||||
All examples are located in the [`examples`](https://github.com/cirospaciari/socketify.py/tree/main/examples) directory.
|
||||
|
||||
### 🚀 Getting Started
|
||||
|
||||
- [`hello_world.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/hello_world.py) - Basic HTTP server setup
|
||||
- [`hello_world_cli.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/hello_world_cli.py) - Command-line interface example
|
||||
- [`hello_world_cli_ws.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/hello_world_cli_ws.py) - CLI with WebSocket support
|
||||
- [`hello_world_unix_domain.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/hello_world_unix_domain.py) - Unix domain socket example
|
||||
|
||||
### 🔒 Security & HTTPS
|
||||
|
||||
- [`https.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/https.py) - HTTPS server with SSL/TLS configuration
|
||||
|
||||
### 🌐 WebSocket Examples
|
||||
|
||||
- [`websockets.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/websockets.py) - Basic WebSocket implementation
|
||||
- [`ws_close_connection.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/ws_close_connection.py) - WebSocket connection management
|
||||
- [`chat/`](https://github.com/cirospaciari/socketify.py/tree/main/examples/chat) - Real-time chat application
|
||||
- [`broadcast.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/broadcast.py) - Broadcasting messages to multiple clients
|
||||
- [`backpressure.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/backpressure.py) - Handling WebSocket backpressure
|
||||
|
||||
### ⚙️ Middleware & Routing
|
||||
|
||||
- [`middleware.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/middleware.py) - Basic middleware implementation
|
||||
- [`middleware_async.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/middleware_async.py) - Asynchronous middleware
|
||||
- [`middleware_sync.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/middleware_sync.py) - Synchronous middleware
|
||||
- [`middleware_router.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/middleware_router.py) - Router-based middleware
|
||||
- [`router_and_basics.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/router_and_basics.py) - Routing fundamentals
|
||||
|
||||
### 🔄 Async/Sync Programming
|
||||
|
||||
- [`async.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/async.py) - Asynchronous request handling
|
||||
- [`upgrade.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/upgrade.py) - Protocol upgrade examples
|
||||
- [`upgrade_async.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/upgrade_async.py) - Asynchronous protocol upgrades
|
||||
|
||||
### 📁 File Handling & Static Content
|
||||
|
||||
- [`static_files.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/static_files.py) - Serving static files
|
||||
- [`file_stream.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/file_stream.py) - File streaming capabilities
|
||||
- [`upload_or_post.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/upload_or_post.py) - File uploads and POST data handling
|
||||
|
||||
### 🎨 Template Engines
|
||||
|
||||
- [`template_jinja2.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/template_jinja2.py) - Jinja2 template integration
|
||||
- [`template_mako.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/template_mako.py) - Mako template integration
|
||||
- [`templates/`](https://github.com/cirospaciari/socketify.py/tree/main/examples/templates) - Template examples and resources
|
||||
|
||||
### 🛠️ Advanced Features
|
||||
|
||||
- [`custom_json_serializer.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/custom_json_serializer.py) - Custom JSON serialization
|
||||
- [`http_request_cache.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/http_request_cache.py) - HTTP request caching
|
||||
- [`proxy.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/proxy.py) - Proxy server implementation
|
||||
- [`automatic_port_selection.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/automatic_port_selection.py) - Dynamic port selection
|
||||
|
||||
### 🔧 Server Configuration
|
||||
|
||||
- [`listen_options.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/listen_options.py) - Server listening options
|
||||
- [`graceful_shutdown.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/graceful_shutdown.py) - Graceful server shutdown
|
||||
- [`forks.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/forks.py) - Multi-process server setup
|
||||
|
||||
### 📊 GraphQL Integration
|
||||
|
||||
- [`graphiql.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/graphiql.py) - GraphiQL interface setup
|
||||
- [`graphiql_raw.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/graphiql_raw.py) - Raw GraphQL implementation
|
||||
|
||||
### 🐳 Development & Deployment
|
||||
|
||||
- [`docker/`](https://github.com/cirospaciari/socketify.py/tree/main/examples/docker) - Docker containerization examples
|
||||
- [`requirements.txt`](https://github.com/cirospaciari/socketify.py/tree/main/examples/requirements.txt) - Example dependencies
|
||||
|
||||
### 🛡️ Error Handling & Logging
|
||||
|
||||
- [`error_handler.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/error_handler.py) - Error handling strategies
|
||||
- [`better_logging.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/better_logging.py) - Advanced logging setup
|
||||
- [`not_found.py`](https://github.com/cirospaciari/socketify.py/tree/main/examples/not_found.py) - Custom 404 error pages
|
||||
|
||||
### 🔨 Utilities & Helpers
|
||||
|
||||
- [`helpers/`](https://github.com/cirospaciari/socketify.py/tree/main/examples/helpers) - Utility functions and helper modules
|
|
@ -0,0 +1,55 @@
|
|||
|
||||
# Plugins / Extensions
|
||||
|
||||
You can add more functionality to request, response, and websocket objects, for this you can use `app.register(extension)` to register an extension.
|
||||
Be aware that using extensions can have a performance impact and using it with `request_response_factory_max_items`, `websocket_factory_max_items`
|
||||
or the equivalent on CLI `--req-res-factory-maxitems`, `--ws-factory-maxitems` will reduce this performance impact.
|
||||
|
||||
Extensions must follow the signature `def extension(request, response, ws)`, request, response, and ws objects contain `method` decorator that binds a method to an instance,
|
||||
and also a `property(name: str, default_value: any = None)` that dynamic adds an property to the instance.
|
||||
|
||||
```python
|
||||
from socketify import App, OpCode
|
||||
|
||||
app = App()
|
||||
|
||||
def extension(request, response, ws):
|
||||
@request.method
|
||||
async def get_user(self):
|
||||
token = self.get_header("token")
|
||||
return { "name": "Test" } if token else { "name", "Anonymous" }
|
||||
|
||||
@response.method
|
||||
def msgpack(self, value: any):
|
||||
self.write_header(b'Content-Type', b'application/msgpack')
|
||||
data = msgpack.packb(value, default=encode_datetime, use_bin_type=True)
|
||||
return self.end(data)
|
||||
|
||||
@ws.method
|
||||
def send_pm(self, to_username: str, message: str):
|
||||
user_data = self.get_user_data()
|
||||
pm_topic = f"pm-{to_username}+{user_data.username}"
|
||||
|
||||
# if topic exists just send the message
|
||||
if app.num_subscribers(pm_topic) > 0:
|
||||
# send private message
|
||||
return self.publish(pm_topic, message, OpCode.TEXT)
|
||||
|
||||
# if the topic not exists create it and signal the user
|
||||
# subscribe to the conversation
|
||||
self.subscribe(pm_topic)
|
||||
# signal user that you want to talk and create an pm room
|
||||
# all users must subscribe to signal-{username}
|
||||
self.publish(f"signal-{to_username}", {
|
||||
"type": "pm",
|
||||
"username": user_data.username,
|
||||
"message": message
|
||||
}, OpCode.TEXT)
|
||||
# this property can be used on extension methods and/or middlewares
|
||||
request.property("cart", [])
|
||||
|
||||
# extensions must be registered before routes
|
||||
app.register(extension)
|
||||
```
|
||||
|
||||
### Next [SSL](ssl.md)
|
|
@ -8,10 +8,14 @@ Hello world app
|
|||
```python
|
||||
from socketify import App
|
||||
|
||||
app = App()
|
||||
app.get("/", lambda res, req: res.end("Hello World socketify from Python!"))
|
||||
app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port))
|
||||
app.run()
|
||||
def make_app(app: App):
|
||||
app.get("/", lambda res, req: res.end("Hello World socketify from Python!"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = App()
|
||||
make_app(app)
|
||||
app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port))
|
||||
app.run()
|
||||
```
|
||||
> This example just show how intuitive is to start an simple hello world app.
|
||||
|
||||
|
@ -19,10 +23,13 @@ SSL version sample
|
|||
``` python
|
||||
from socketify import App, AppOptions
|
||||
|
||||
app = App(AppOptions(key_file_name="./misc/key.pem", cert_file_name="./misc/cert.pem", passphrase="1234"))
|
||||
app.get("/", lambda res, req: res.end("Hello World socketify from Python!"))
|
||||
app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port))
|
||||
app.run()
|
||||
def make_app(app):
|
||||
app.get("/", lambda res, req: res.end("Hello World socketify from Python!"))
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = App(AppOptions(key_file_name="./misc/key.pem", cert_file_name="./misc/cert.pem", passphrase="1234"))
|
||||
app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % config.port))
|
||||
app.run()
|
||||
```
|
||||
|
||||
> We have a lot of SSL options, but this is the most common you can see all the options in the [API Reference](api.md)
|
||||
|
@ -38,20 +45,25 @@ def ws_open(ws):
|
|||
def ws_message(ws, message, opcode):
|
||||
#Ok is false if backpressure was built up, wait for drain
|
||||
ok = ws.send(message, opcode)
|
||||
|
||||
app = App()
|
||||
app.ws("/*", {
|
||||
'compression': CompressOptions.SHARED_COMPRESSOR,
|
||||
'max_payload_length': 16 * 1024 * 1024,
|
||||
'idle_timeout': 12,
|
||||
'open': ws_open,
|
||||
'message': ws_message,
|
||||
'drain': lambda ws: print('WebSocket backpressure: %i' % ws.get_buffered_amount()),
|
||||
'close': lambda ws, code, message: print('WebSocket closed')
|
||||
})
|
||||
app.any("/", lambda res,req: res.end("Nothing to see here!'"))
|
||||
app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % (config.port)))
|
||||
app.run()
|
||||
|
||||
def make_app(app):
|
||||
app.ws("/*", {
|
||||
'compression': CompressOptions.SHARED_COMPRESSOR,
|
||||
'max_payload_length': 16 * 1024 * 1024,
|
||||
'idle_timeout': 12,
|
||||
'open': ws_open,
|
||||
'message': ws_message,
|
||||
'drain': lambda ws: print('WebSocket backpressure: %i' % ws.get_buffered_amount()),
|
||||
'close': lambda ws, code, message: print('WebSocket closed')
|
||||
})
|
||||
app.any("/", lambda res,req: res.end("Nothing to see here!'"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = App()
|
||||
make_app(app)
|
||||
app.listen(3000, lambda config: print("Listening on port http://localhost:%d now\n" % (config.port)))
|
||||
app.run()
|
||||
```
|
||||
|
||||
> We can have multiple routes for WebSockets, but in this example we just get one for anything we need, adding an option of compression using SHARED_COMPRESSOR, max_payload_length of 1mb and an idle timeout of 12s just to show some most commonly used features you can see all these options in the [API Reference](api.md)
|
||||
|
@ -59,4 +71,4 @@ app.run()
|
|||
|
||||
If you just wanna to see some more examples you can go to our [examples folder](https://github.com/cirospaciari/socketify.py/tree/main/examples) for more than 25 quick examples.
|
||||
|
||||
### Next [Corking Concept](corking.md)
|
||||
### Next [Corking Concept](corking.md)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
## GraphiQL Support
|
||||
In /src/examples/helper/graphiql.py we implemented an helper for using graphiQL with strawberry.
|
||||
In [`/src/examples/helper/graphiql.py`](https://github.com/cirospaciari/socketify.py/blob/main/examples/graphiql.py) we implemented an helper for using graphiQL with strawberry.
|
||||
|
||||
### Usage
|
||||
```python
|
||||
|
|
|
@ -57,6 +57,13 @@ auth_router.get("/another", middleware(another_middie, home))
|
|||
other_router = MiddlewareRouter(app, auth, another_middie)
|
||||
other_router.get("/another_way", home)
|
||||
|
||||
# you can also use middlewares when using the decorator router
|
||||
private = app.router("/api", auth, another_middie)
|
||||
|
||||
# will serve in /api/users and use auth_middleware
|
||||
@private.get("/users")
|
||||
def get_users(res, req, user):
|
||||
res.cork_end("Hello private API!")
|
||||
|
||||
app.listen(
|
||||
3000,
|
||||
|
|
|
@ -24,6 +24,34 @@ app.post("/", home)
|
|||
```
|
||||
> Whenever your callback is a coroutine, such as the async/await, automatic corking can only happen in the very first portion of the coroutine (consider await a separator which essentially cuts the coroutine into smaller segments). Only the first "segment" of the coroutine will be called from socketify, the following async segments will be called by the asyncio event loop at a later point in time and will thus not be under our control with default corking enabled, HttpRequest object being stack-allocated and only valid in one single callback invocation so only valid in the first "segment" before the first await. If you just want to preserve headers, url, method, cookies and query string you can use `req.preserve()` to copy all data and keep it in the request object, but will be some performance penalty. Take a look in [Corking](corking.md) for get a more in deph information
|
||||
|
||||
|
||||
You can also use the `Decorator router` as the name suggests this router allows to use of decorators for routing and also comes up with a prefix option, and middleware support.
|
||||
|
||||
```python
|
||||
from socketify import App
|
||||
|
||||
app = App()
|
||||
router = app.router()
|
||||
|
||||
@router.get("/")
|
||||
def home(res, req):
|
||||
res.end("Hello World!")
|
||||
|
||||
api = app.router(prefix="/api")
|
||||
|
||||
# will serve in /api/hello
|
||||
@api.get("/hello")
|
||||
def hello(res, req):
|
||||
res.end("Hello API!")
|
||||
|
||||
private = app.router("/api", auth_middleware)
|
||||
|
||||
# will serve in /api/users and use auth_middleware
|
||||
@private.get("/users")
|
||||
def get_users(res, req, auth):
|
||||
res.end("Hello private API!")
|
||||
```
|
||||
|
||||
## Pattern matching
|
||||
Routes are matched in order of specificity, not by the order you register them:
|
||||
|
||||
|
@ -74,7 +102,7 @@ async def async_xablau(res, req):
|
|||
# this can be async no problems
|
||||
def on_error(error, res, req):
|
||||
# here you can log properly the error and do a pretty response to your clients
|
||||
print("Somethind goes %s" % str(error))
|
||||
print("Something goes %s" % str(error))
|
||||
# response and request can be None if the error is in an async function
|
||||
if res != None:
|
||||
# if response exists try to send something
|
||||
|
@ -136,4 +164,4 @@ app.remove_server_name("*.google.*")
|
|||
|
||||
```
|
||||
|
||||
### Next [Middlewares](middlewares.md)
|
||||
### Next [Middlewares](middlewares.md)
|
||||
|
|
|
@ -31,4 +31,4 @@ class AppOptions:
|
|||
ssl_ciphers: str = None,
|
||||
ssl_prefer_low_memory_usage: int = 0
|
||||
```
|
||||
### Next [CLI Reference](cli.md)
|
||||
### Next [CLI, ASGI and WSGI](cli.md)
|
|
@ -3,6 +3,52 @@ WebSocket "routes" are registered similarly, but not identically.
|
|||
|
||||
Every websocket route has the same pattern and pattern matching as for Http, but instead of one single callback you have a whole set of them, here's an example:
|
||||
|
||||
Configuration details, notes:
|
||||
- *idle_timeout*: number of seconds of inactivity before client is disconnected. If set to 0, no policy is enforced (connections can be stale).
|
||||
- *open*: callback function for websocket connection being open
|
||||
```python
|
||||
def on_open(ws : WebSocket):
|
||||
"""
|
||||
ws: WebSocket - websocket connection
|
||||
"""
|
||||
...
|
||||
```
|
||||
- *close*: callback function for websocket connection closed
|
||||
```python
|
||||
def on_close(ws: WebSocket, code: int, msg: Union[bytes, str]):
|
||||
"""
|
||||
ws: WebSocket
|
||||
websocket connection
|
||||
code: int
|
||||
exit code from client
|
||||
msg: byte, str
|
||||
exit message
|
||||
"""
|
||||
...
|
||||
```
|
||||
- *upgrade*: callback function to upgrade socket connection details
|
||||
```python
|
||||
def on_upgrade(res: Response, req: Request, socket_context):
|
||||
"""
|
||||
res: Response
|
||||
req: Request
|
||||
"""
|
||||
...
|
||||
```
|
||||
- *message*: callback function for websocket message received
|
||||
```python
|
||||
def on_message(ws: WebSocket, msg: Union[bytes, str], opcode: OpCode):
|
||||
"""
|
||||
ws: WebSocket
|
||||
msg: bytes, str
|
||||
opcode: OpCode
|
||||
"""
|
||||
```
|
||||
- *drain*: in the event of backpressure, policy to drain ws buffer
|
||||
```python
|
||||
def on_drain(ws: WebSocket):
|
||||
...
|
||||
```
|
||||
```python
|
||||
app = App()
|
||||
app.ws(
|
||||
|
@ -11,12 +57,12 @@ app.ws(
|
|||
"compression": CompressOptions.SHARED_COMPRESSOR,
|
||||
"max_payload_length": 16 * 1024 * 1024,
|
||||
"idle_timeout": 12,
|
||||
"open": ws_open,
|
||||
"message": ws_message,
|
||||
"drain": lambda ws: print(
|
||||
"WebSocket backpressure: %s", ws.get_buffered_amount()
|
||||
),
|
||||
"close": lambda ws, code, message: print("WebSocket closed"),
|
||||
"open": on_open,
|
||||
"message": on_message,
|
||||
"close": on_close,
|
||||
"upgrade": on_upgrade,
|
||||
'drain': on_drain,
|
||||
"subscription": lambda ws, topic, subscriptions, subscriptions_before: print(f'subscription/unsubscription on topic {topic} {subscriptions} {subscriptions_before}'),
|
||||
},
|
||||
)
|
||||
```
|
||||
|
@ -25,6 +71,57 @@ You should use the provided user data feature to store and attach any per-socket
|
|||
|
||||
If you want to create something more elaborate you could have the user data hold a pointer to some dynamically allocated memory block that keeps a boolean whether the WebSocket is still valid or not. Sky is the limit here.
|
||||
|
||||
In order to do so, use the `upgrade` callback configuration in the `app.ws` settings.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from socketify import App, WebSocket, OpCode
|
||||
app = App()
|
||||
|
||||
ID = 0
|
||||
|
||||
def on_open(ws: WebSocket):
|
||||
user_data = ws.get_user_data()
|
||||
print('ws %s connected' % user_data['user_id'])
|
||||
ws.send('Hello, world!')
|
||||
|
||||
def on_upgrade(res, req, socket_context):
|
||||
global ID
|
||||
ID += 1
|
||||
key = req.get_header("sec-websocket-key")
|
||||
protocol = req.get_header("sec-websocket-protocol")
|
||||
extensions = req.get_header("sec-websocket-extensions")
|
||||
user_data=dict(user_id=ID)
|
||||
res.upgrade(key, protocol, extensions, socket_context, user_data)
|
||||
|
||||
def on_message(ws: WebSocket, msg: str, opcode: OpCode):
|
||||
user_data = ws.get_user_data()
|
||||
print('ws %s: %s' % (user_data['user_id'], msg))
|
||||
|
||||
def on_close(ws, code, msg):
|
||||
user_data = ws.get_user_data()
|
||||
print('ws %s closed' % user_data['user_id'])
|
||||
|
||||
def on_drain(ws: WebSocket):
|
||||
user_data = ws.get_user_data()
|
||||
print('ws %s backpressure: %s' % (user_data['user_id'], ws.get_buffered_amount()))
|
||||
|
||||
app.ws(
|
||||
"/*",
|
||||
{
|
||||
"compression": CompressOptions.SHARED_COMPRESSOR,
|
||||
"max_payload_length": 16 * 1024 * 1024,
|
||||
"idle_timeout": 12,
|
||||
"open": on_open,
|
||||
"message": on_message,
|
||||
"close": on_close,
|
||||
"upgrade": on_upgrade,
|
||||
"drain": on_drain,
|
||||
"subscription": lambda ws, topic, subscriptions, subscriptions_before: print(f'subscription/unsubscription on topic {topic} {subscriptions} {subscriptions_before}'),
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## WebSockets are valid from open to close
|
||||
All given WebSocket are guaranteed to live from open event (where you got your WebSocket) until close event is called.
|
||||
Message events will never emit outside of open/close. Calling ws.close or ws.end will immediately call the close handler.
|
||||
|
@ -50,4 +147,4 @@ You probably want shared compressor if dealing with larger JSON messages, or 4kb
|
|||
|
||||
idle_timeout is roughly the amount of seconds that may pass between messages. Being idle for more than this, and the connection is severed. This means you should make your clients send small ping messages every now and then, to keep the connection alive. The server will automatically send pings in case it needs to.
|
||||
|
||||
### Next [SSL](ssl.md)
|
||||
### Next [Plugins / Extensions](extensions.md)
|
|
@ -8,23 +8,21 @@ def ws_open(ws):
|
|||
|
||||
|
||||
def ws_message(ws, message, opcode):
|
||||
# Ok is false if backpressure was built up, wait for drain
|
||||
ok = ws.send(message, opcode)
|
||||
# Broadcast this message
|
||||
ws.publish("broadcast", message, opcode)
|
||||
|
||||
|
||||
app = App()
|
||||
app.ws(
|
||||
"/*",
|
||||
{
|
||||
"compression": CompressOptions.SHARED_COMPRESSOR,
|
||||
"max_payload_length": 16 * 1024 * 1024,
|
||||
"idle_timeout": 12,
|
||||
"idle_timeout": 60,
|
||||
"open": ws_open,
|
||||
"message": ws_message,
|
||||
# The library guarantees proper unsubscription at close
|
||||
"close": lambda ws, code, message: print("WebSocket closed"),
|
||||
"subscription": lambda ws, topic, subscriptions, subscriptions_before: print(f'subscription/unsubscription on topic {topic} {subscriptions} {subscriptions_before}'),
|
||||
},
|
||||
)
|
||||
app.any("/", lambda res, req: res.end("Nothing to see here!"))
|
||||
|
|
|
@ -110,7 +110,6 @@ def ws_message(ws, message, opcode):
|
|||
history = history[::100]
|
||||
|
||||
#broadcast
|
||||
ws.send(message_data, OpCode.TEXT)
|
||||
ws.publish(room, message_data, OpCode.TEXT)
|
||||
|
||||
except:
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
from socketify import App
|
||||
import ujson
|
||||
|
||||
app = App()
|
||||
|
||||
|
||||
# set json serializer to ujson
|
||||
# json serializer must have dumps and loads functions
|
||||
app.json_serializer(ujson)
|
||||
|
||||
app.get("/", lambda res, req: res.end({"Hello":"World!"}))
|
||||
app.listen(
|
||||
3000,
|
||||
lambda config: print("Listening on port http://localhost:%d now\n" % config.port),
|
||||
)
|
||||
app.run()
|
|
@ -9,11 +9,12 @@ def xablau(res, req):
|
|||
|
||||
|
||||
async def async_xablau(res, req):
|
||||
await asyncio.sleep(1)
|
||||
raise RuntimeError("Async Xablau!")
|
||||
|
||||
|
||||
|
||||
# this can be async no problems
|
||||
@app.on_error
|
||||
def on_error(error, res, req):
|
||||
# here you can log properly the error and do a pretty response to your clients
|
||||
print("Somethind goes %s" % str(error))
|
||||
|
@ -27,7 +28,8 @@ def on_error(error, res, req):
|
|||
app.get("/", xablau)
|
||||
app.get("/async", async_xablau)
|
||||
|
||||
app.set_error_handler(on_error)
|
||||
# you can also use set_error_handler
|
||||
# app.set_error_handler(on_error)
|
||||
|
||||
app.listen(
|
||||
3000,
|
||||
|
|
|
@ -2,7 +2,6 @@ from socketify import App
|
|||
import os
|
||||
import multiprocessing
|
||||
|
||||
|
||||
def run_app():
|
||||
app = App()
|
||||
app.get("/", lambda res, req: res.end("Hello, World!"))
|
||||
|
@ -16,15 +15,19 @@ def run_app():
|
|||
app.run()
|
||||
|
||||
|
||||
def create_fork():
|
||||
n = os.fork()
|
||||
# n greater than 0 means parent process
|
||||
if not n > 0:
|
||||
run_app()
|
||||
|
||||
|
||||
pid_list = []
|
||||
# fork limiting the cpu count - 1
|
||||
for i in range(1, multiprocessing.cpu_count()):
|
||||
create_fork()
|
||||
for _ in range(1, multiprocessing.cpu_count()):
|
||||
pid = os.fork()
|
||||
# n greater than 0 means parent process
|
||||
if not pid > 0:
|
||||
run_app()
|
||||
break
|
||||
pid_list.append(pid)
|
||||
|
||||
run_app() # run app on the main process too :)
|
||||
|
||||
# sigint everything to graceful shutdown
|
||||
import signal
|
||||
for pid in pid_list:
|
||||
os.kill(pid, signal.SIGINT)
|
|
@ -0,0 +1,21 @@
|
|||
from streaming_form_data import StreamingFormDataParser
|
||||
from socketify import Response
|
||||
def get_formdata(res: Response, parser: StreamingFormDataParser):
|
||||
_dataFuture = res.app.loop.create_future()
|
||||
|
||||
def is_aborted(res):
|
||||
res.aborted = True
|
||||
try:
|
||||
if not _dataFuture.done():
|
||||
_dataFuture.set_result(parser)
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_chunks(res, chunk, is_end):
|
||||
parser.data_received(chunk)
|
||||
if is_end:
|
||||
_dataFuture.set_result(parser)
|
||||
|
||||
res.on_aborted(is_aborted)
|
||||
res.on_data(get_chunks)
|
||||
return _dataFuture
|
|
@ -1,6 +1,6 @@
|
|||
import strawberry
|
||||
import strawberry.utils.graphiql
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
def graphiql_from(Query, Mutation=None):
|
||||
if Mutation:
|
||||
|
@ -8,32 +8,45 @@ def graphiql_from(Query, Mutation=None):
|
|||
else:
|
||||
schema = strawberry.Schema(Query)
|
||||
|
||||
async def post(res, req):
|
||||
def post(res, req):
|
||||
# we can pass whatever we want to context, query, headers or params, cookies etc
|
||||
context_value = req.preserve()
|
||||
|
||||
buffer = BytesIO()
|
||||
def on_data(res, chunk, is_end):
|
||||
buffer.write(chunk)
|
||||
if is_end:
|
||||
try:
|
||||
body = res.app._json_serializer.loads(buffer.getvalue().decode("utf-8"))
|
||||
res.run_async(graph_ql(res, body, context_value))
|
||||
except Exception as err:
|
||||
res.app.trigger_error(err, res, None)
|
||||
|
||||
res.grab_aborted_handler()
|
||||
res.on_data(on_data)
|
||||
|
||||
# get all incoming data and parses as json
|
||||
body = await res.get_json()
|
||||
async def graph_ql(res, body, context_value):
|
||||
query = body["query"]
|
||||
|
||||
|
||||
variables = body.get("variables", None)
|
||||
root_value = body.get("rootValue", None)
|
||||
operation_name = body.get("operationName", None)
|
||||
|
||||
data = await schema.execute(
|
||||
query,
|
||||
variables,
|
||||
context_value,
|
||||
root_value,
|
||||
operation_name,
|
||||
)
|
||||
|
||||
query = body["query"]
|
||||
variables = body.get("variables", None)
|
||||
root_value = body.get("root_value", None)
|
||||
operation_name = body.get("operation_name", None)
|
||||
|
||||
data = await schema.execute(
|
||||
query,
|
||||
variables,
|
||||
context_value,
|
||||
root_value,
|
||||
operation_name,
|
||||
)
|
||||
|
||||
res.cork_end(
|
||||
{
|
||||
"data": (data.data),
|
||||
**({"errors": data.errors} if data.errors else {}),
|
||||
**({"extensions": data.extensions} if data.extensions else {}),
|
||||
}
|
||||
)
|
||||
res.cork_send(
|
||||
{
|
||||
"data": (data.data),
|
||||
**({"errors": data.errors} if data.errors else {}),
|
||||
**({"extensions": data.extensions} if data.extensions else {}),
|
||||
}
|
||||
)
|
||||
|
||||
return post
|
||||
|
|
|
@ -9,9 +9,10 @@ app = App(
|
|||
)
|
||||
app.get("/", lambda res, req: res.end("Hello World socketify from Python!"))
|
||||
app.listen(
|
||||
3000,
|
||||
54321,
|
||||
lambda config: print("Listening on port https://localhost:%d now\n" % config.port),
|
||||
)
|
||||
app.run()
|
||||
|
||||
# mkdir misc
|
||||
# openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -passout pass:1234 -keyout ./misc/key.pem -out ./misc/cert.pem
|
||||
|
|
|
@ -6,7 +6,7 @@ from socketify import App
|
|||
def middleware(*functions):
|
||||
def middleware_route(res, req):
|
||||
data = None
|
||||
# cicle to all middlewares
|
||||
# circle to all middlewares
|
||||
for function in functions:
|
||||
# call middlewares
|
||||
data = function(res, req, data)
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
# using oha -c 400 -z 5s http://localhost:3000/
|
||||
|
||||
# nginx - try_files - 77630.15 req/s
|
||||
# pypy3 - socketify static - 15839.22 req/s
|
||||
# python3 - socketify static - 8294.96 req/s
|
||||
# pypy3 - socketify static - 16797.30 req/s
|
||||
# python3 - socketify static - 10140.19 req/s
|
||||
# node.js - @fastify/static - 5437.16 req/s
|
||||
# node.js - express.static - 4077.49 req/s
|
||||
# python3 - socketify static_aiofile - 2390.96 req/s
|
||||
|
|
|
@ -78,6 +78,69 @@ async def upload_multiple(res, req):
|
|||
# We respond when we are done
|
||||
res.cork_end("Thanks for the data!")
|
||||
|
||||
def upload_formdata(res, req):
|
||||
# using streaming_form_data package for parsing
|
||||
from streaming_form_data import StreamingFormDataParser
|
||||
from streaming_form_data.targets import ValueTarget, FileTarget
|
||||
|
||||
print(f"Posted to {req.get_url()}")
|
||||
parser = StreamingFormDataParser(headers=req.get_headers())
|
||||
name = ValueTarget()
|
||||
parser.register('name', name)
|
||||
file = FileTarget('/tmp/file')
|
||||
file2 = FileTarget('/tmp/file2')
|
||||
parser.register('file', file)
|
||||
parser.register('file2', file2)
|
||||
|
||||
|
||||
def on_data(res, chunk, is_end):
|
||||
parser.data_received(chunk)
|
||||
if is_end:
|
||||
res.cork(on_finish)
|
||||
|
||||
|
||||
def on_finish(res):
|
||||
print(name.value)
|
||||
|
||||
print(file.multipart_filename)
|
||||
print(file.multipart_content_type)
|
||||
|
||||
print(file2.multipart_filename)
|
||||
print(file2.multipart_content_type)
|
||||
|
||||
res.end("Thanks for the data!")
|
||||
|
||||
res.on_data(on_data)
|
||||
|
||||
|
||||
async def upload_formhelper(res, req):
|
||||
# using streaming_form_data package for parsing + helper
|
||||
from streaming_form_data import StreamingFormDataParser
|
||||
from streaming_form_data.targets import ValueTarget, FileTarget
|
||||
from helpers.form_data import get_formdata
|
||||
|
||||
|
||||
print(f"Posted to {req.get_url()}")
|
||||
parser = StreamingFormDataParser(headers=req.get_headers())
|
||||
name = ValueTarget()
|
||||
parser.register('name', name)
|
||||
file = FileTarget('/tmp/file')
|
||||
file2 = FileTarget('/tmp/file2')
|
||||
parser.register('file', file)
|
||||
parser.register('file2', file2)
|
||||
|
||||
await get_formdata(res, parser)
|
||||
|
||||
print(name.value)
|
||||
|
||||
print(file.multipart_filename)
|
||||
print(file.multipart_content_type)
|
||||
|
||||
print(file2.multipart_filename)
|
||||
print(file2.multipart_content_type)
|
||||
|
||||
res.cork_end("Thanks for the data!")
|
||||
|
||||
|
||||
app = App()
|
||||
app.post("/", upload)
|
||||
|
@ -86,6 +149,8 @@ app.post("/json", upload_json)
|
|||
app.post("/text", upload_text)
|
||||
app.post("/urlencoded", upload_urlencoded)
|
||||
app.post("/multiple", upload_multiple)
|
||||
app.post("/formdata", upload_formdata)
|
||||
app.post("/formdata2", upload_formhelper)
|
||||
|
||||
app.any("/*", lambda res, _: res.write_status(404).end("Not Found"))
|
||||
app.listen(
|
||||
|
|
|
@ -4,13 +4,14 @@ build-backend = "setuptools.build_meta"
|
|||
|
||||
[project]
|
||||
name = "socketify"
|
||||
version = "0.0.2"
|
||||
version = "0.0.31"
|
||||
dynamic = ["dependencies"]
|
||||
authors = [
|
||||
{ name="Ciro Spaciari", email="ciro.spaciari@gmail.com" },
|
||||
]
|
||||
description = "Bringing WebSockets, Http/Https High Performance servers for PyPy3 and Python3"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.7"
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
|
|
8
setup.py
8
setup.py
|
@ -1,8 +1,8 @@
|
|||
import sys
|
||||
|
||||
vi = sys.version_info
|
||||
if vi < (3, 7):
|
||||
raise RuntimeError("socketify requires Python 3.7 or greater")
|
||||
if vi < (3, 8):
|
||||
raise RuntimeError("socketify requires Python 3.8 or greater")
|
||||
|
||||
# if sys.platform in ('win32', 'cygwin', 'cli'):
|
||||
# raise RuntimeError('socketify does not support Windows at the moment')
|
||||
|
@ -58,7 +58,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
|
|||
|
||||
setuptools.setup(
|
||||
name="socketify",
|
||||
version="0.0.2",
|
||||
version="0.0.31",
|
||||
platforms=["any"],
|
||||
author="Ciro Spaciari",
|
||||
author_email="ciro.spaciari@gmail.com",
|
||||
|
@ -88,7 +88,7 @@ setuptools.setup(
|
|||
"./native/*/*/*",
|
||||
]
|
||||
},
|
||||
python_requires=">=3.7",
|
||||
python_requires=">=3.8",
|
||||
install_requires=["cffi>=1.0", "setuptools>=58.1.0"],
|
||||
has_ext_modules=lambda: True,
|
||||
cmdclass={}, # cmdclass={'sdist': Prepare, 'build_ext': Makefile},
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import asyncio
|
||||
|
||||
from .dataclasses import AppListenOptions, AppOptions
|
||||
from .tasks import TaskFactory, create_task, RequestTask
|
||||
from .socketify import (
|
||||
App,
|
||||
AppOptions,
|
||||
AppListenOptions,
|
||||
OpCode,
|
||||
SendStatus,
|
||||
CompressOptions,
|
||||
Loop
|
||||
Loop,
|
||||
AppExtension,
|
||||
WebSocket,
|
||||
AppRequest as Request,
|
||||
AppResponse as Response
|
||||
)
|
||||
from .asgi import (
|
||||
ASGI
|
||||
|
|
|
@ -1,14 +1,25 @@
|
|||
from socketify import App, OpCode, Loop
|
||||
from socketify import App, OpCode
|
||||
from queue import SimpleQueue
|
||||
from .native import lib, ffi
|
||||
from .tasks import create_task, create_task_with_factory
|
||||
from .tasks import create_task, TaskFactory
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
import sys
|
||||
import logging
|
||||
import uuid
|
||||
import asyncio
|
||||
|
||||
is_pypy = platform.python_implementation() == "PyPy"
|
||||
@ffi.callback("void(uws_res_t*, void*)")
|
||||
def asgi_on_abort_handler(res, user_data):
|
||||
ctx = ffi.from_handle(user_data)
|
||||
ctx.aborted = True
|
||||
ctx.loop.is_idle = False
|
||||
|
||||
if ctx.abort_future is not None:
|
||||
ctx.abort_future.set_result(True)
|
||||
ctx.abort_future = None
|
||||
|
||||
async def task_wrapper(task):
|
||||
try:
|
||||
return await task
|
||||
|
@ -19,8 +30,8 @@ async def task_wrapper(task):
|
|||
finally:
|
||||
return None
|
||||
|
||||
EMPTY_RESPONSE = {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
EMPTY_RESPONSE = {"type": "http.request", "body": b"", "more_body": False}
|
||||
|
||||
@ffi.callback("void(uws_websocket_t*, const char*, size_t, uws_opcode_t, void*)")
|
||||
def ws_message(ws, message, length, opcode, user_data):
|
||||
|
@ -30,7 +41,7 @@ def ws_message(ws, message, length, opcode, user_data):
|
|||
message = message.decode("utf8")
|
||||
|
||||
socket_data.message(ws, message, OpCode(opcode))
|
||||
|
||||
|
||||
|
||||
@ffi.callback("void(uws_websocket_t*, int, const char*, size_t, void*)")
|
||||
def ws_close(ws, code, message, length, user_data):
|
||||
|
@ -46,10 +57,11 @@ def ws_open(ws, user_data):
|
|||
|
||||
|
||||
@ffi.callback(
|
||||
"void(int, uws_res_t*, socketify_asgi_ws_data, uws_socket_context_t* socket, void*, bool*)"
|
||||
"void(int, uws_res_t*, socketify_asgi_ws_data, uws_socket_context_t* socket, void*)"
|
||||
)
|
||||
def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
|
||||
def ws_upgrade(ssl, response, info, socket_context, user_data):
|
||||
app = ffi.from_handle(user_data)
|
||||
app.server.loop.is_idle = False
|
||||
headers = []
|
||||
next_header = info.header_list
|
||||
while next_header != ffi.NULL:
|
||||
|
@ -79,7 +91,8 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
|
|||
extensions = ffi.unpack(info.extensions, info.extensions_size).decode("utf8")
|
||||
compress = app.ws_compression
|
||||
ws = ASGIWebSocket(app.server.loop)
|
||||
|
||||
lib.uws_res_on_aborted(ssl, response, asgi_on_abort_handler, ws._ptr)
|
||||
|
||||
scope = {
|
||||
"type": "websocket",
|
||||
"asgi": {"version": "3.0", "spec_version": "2.3"},
|
||||
|
@ -94,7 +107,7 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
|
|||
"root_path": "",
|
||||
"path": url.decode("utf8"),
|
||||
"raw_path": url,
|
||||
"query_string": ffi.unpack(info.query_string, info.query_string_size),
|
||||
"query_string": ffi.unpack(info.query_string, info.query_string_size)[1:],
|
||||
"headers": headers,
|
||||
"subprotocols": [protocol] if protocol else [],
|
||||
"extensions": {
|
||||
|
@ -105,8 +118,9 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
|
|||
}
|
||||
|
||||
async def send(options):
|
||||
if bool(aborted[0]):
|
||||
if ws.aborted:
|
||||
return False
|
||||
ws.loop.is_idle = False
|
||||
type = options["type"]
|
||||
if type == "websocket.send":
|
||||
data = options.get("bytes", None)
|
||||
|
@ -160,10 +174,12 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
|
|||
else:
|
||||
sec_web_socket_extensions_data = b""
|
||||
_id = uuid.uuid4()
|
||||
|
||||
|
||||
app.server._socket_refs[_id] = ws
|
||||
|
||||
def unregister():
|
||||
app.server._socket_refs.pop(_id, None)
|
||||
|
||||
ws.unregister = unregister
|
||||
lib.uws_res_upgrade(
|
||||
ssl,
|
||||
|
@ -232,6 +248,7 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
|
|||
@ffi.callback("void(uws_res_t*, const char*, size_t, bool, void*)")
|
||||
def asgi_on_data_handler(res, chunk, chunk_length, is_end, user_data):
|
||||
data_response = ffi.from_handle(user_data)
|
||||
data_response.loop.is_idle = False
|
||||
data_response.is_end = bool(is_end)
|
||||
more_body = not data_response.is_end
|
||||
result = {
|
||||
|
@ -253,6 +270,22 @@ class ASGIDataQueue:
|
|||
self.is_end = False
|
||||
self.next_data_future = loop.create_future()
|
||||
|
||||
class ASGIContext:
|
||||
def __init__(self, ssl, response, loop):
|
||||
self._ptr = ffi.new_handle(self)
|
||||
self.aborted = False
|
||||
self.sended_empty = False
|
||||
self.data_queue = None
|
||||
self.ssl = ssl
|
||||
self.response = response
|
||||
self.loop = loop
|
||||
self.abort_future = None
|
||||
|
||||
async def wait_disconnect(self):
|
||||
if not self.aborted:
|
||||
if self.abort_future is None:
|
||||
self.abort_future = self.loop.create_future()
|
||||
await self.abort_future
|
||||
|
||||
class ASGIWebSocket:
|
||||
def __init__(self, loop):
|
||||
|
@ -267,6 +300,14 @@ class ASGIWebSocket:
|
|||
self._message = None
|
||||
self._ptr = ffi.new_handle(self)
|
||||
self.unregister = None
|
||||
self.aborted = False
|
||||
self.abort_future = None
|
||||
|
||||
async def wait_disconnect(self):
|
||||
if not self.aborted:
|
||||
if self.abort_future is None:
|
||||
self.abort_future = self.loop.create_future()
|
||||
await self.abort_future
|
||||
|
||||
def accept(self):
|
||||
self.accept_future = self.loop.create_future()
|
||||
|
@ -374,7 +415,6 @@ def uws_asgi_corked_response_start_handler(res, user_data):
|
|||
lib.socketify_res_write_int_status(ssl, res, int(status))
|
||||
for name, value in headers:
|
||||
write_header(ssl, res, name, value)
|
||||
write_header(ssl, res, b"Server", b"socketify.py")
|
||||
|
||||
|
||||
@ffi.callback("void(uws_res_t*, void*)")
|
||||
|
@ -384,7 +424,6 @@ def uws_asgi_corked_accept_handler(res, user_data):
|
|||
lib.socketify_res_write_int_status(ssl, res, int(status))
|
||||
for name, value in headers:
|
||||
write_header(ssl, res, name, value)
|
||||
write_header(ssl, res, b"Server", b"socketify.py")
|
||||
|
||||
|
||||
@ffi.callback("void(uws_res_t*, void*)")
|
||||
|
@ -392,7 +431,6 @@ def uws_asgi_corked_ws_accept_handler(res, user_data):
|
|||
(ssl, headers) = ffi.from_handle(user_data)
|
||||
for name, value in headers:
|
||||
write_header(ssl, res, name, value)
|
||||
write_header(ssl, res, b"Server", b"socketify.py")
|
||||
|
||||
|
||||
@ffi.callback("void(uws_res_t*, void*)")
|
||||
|
@ -402,10 +440,11 @@ def uws_asgi_corked_403_handler(res, user_data):
|
|||
lib.uws_res_end_without_body(ssl, res, 0)
|
||||
|
||||
|
||||
@ffi.callback("void(int, uws_res_t*, socketify_asgi_data, void*, bool*)")
|
||||
def asgi(ssl, response, info, user_data, aborted):
|
||||
@ffi.callback("void(int, uws_res_t*, socketify_asgi_data, void*)")
|
||||
def asgi(ssl, response, info, user_data):
|
||||
app = ffi.from_handle(user_data)
|
||||
|
||||
app.server.loop.is_idle = False
|
||||
|
||||
headers = []
|
||||
next_header = info.header_list
|
||||
while next_header != ffi.NULL:
|
||||
|
@ -424,7 +463,7 @@ def asgi(ssl, response, info, user_data, aborted):
|
|||
"http_version": "1.1",
|
||||
"server": (app.SERVER_HOST, app.SERVER_PORT),
|
||||
"client": (
|
||||
ffi.unpack(info.remote_address, info.remote_address_size).decode("utf8"),
|
||||
None if info.remote_address == ffi.NULL else ffi.unpack(info.remote_address, info.remote_address_size).decode("utf8"),
|
||||
None,
|
||||
),
|
||||
"scheme": app.SERVER_SCHEME,
|
||||
|
@ -432,18 +471,25 @@ def asgi(ssl, response, info, user_data, aborted):
|
|||
"root_path": "",
|
||||
"path": url.decode("utf8"),
|
||||
"raw_path": url,
|
||||
"query_string": ffi.unpack(info.query_string, info.query_string_size),
|
||||
"query_string": ffi.unpack(info.query_string, info.query_string_size)[1:],
|
||||
"headers": headers,
|
||||
|
||||
}
|
||||
loop = app.server.loop
|
||||
ctx = ASGIContext(ssl, response, loop)
|
||||
if bool(info.has_content):
|
||||
data_queue = ASGIDataQueue(app.server.loop)
|
||||
data_queue = ASGIDataQueue(loop)
|
||||
lib.uws_res_on_data(ssl, response, asgi_on_data_handler, data_queue._ptr)
|
||||
else:
|
||||
data_queue = None
|
||||
ctx.data_queue = data_queue
|
||||
|
||||
lib.uws_res_on_aborted(ssl, response, asgi_on_abort_handler, ctx._ptr)
|
||||
|
||||
async def receive():
|
||||
if bool(aborted[0]):
|
||||
if ctx.aborted:
|
||||
return {"type": "http.disconnect"}
|
||||
|
||||
ctx.loop.is_idle = False
|
||||
data_queue = ctx.data_queue
|
||||
if data_queue:
|
||||
if data_queue.queue.empty():
|
||||
if not data_queue.is_end:
|
||||
|
@ -455,13 +501,24 @@ def asgi(ssl, response, info, user_data, aborted):
|
|||
else:
|
||||
return data_queue.queue.get(False) # consume queue
|
||||
|
||||
# no more body, just empty
|
||||
return EMPTY_RESPONSE
|
||||
# no more body, just EMPTY RESPONSE
|
||||
if not ctx.sended_empty:
|
||||
ctx.sended_empty = True
|
||||
return EMPTY_RESPONSE
|
||||
|
||||
# already sended empty body so wait for aborted request
|
||||
if not ctx.aborted:
|
||||
await ctx.wait_disconnect()
|
||||
return {"type": "http.disconnect"}
|
||||
|
||||
async def send(options):
|
||||
if bool(aborted[0]):
|
||||
if ctx.aborted:
|
||||
return False
|
||||
|
||||
ctx.loop.is_idle = False
|
||||
type = options["type"]
|
||||
ssl = ctx.ssl
|
||||
response = ctx.response
|
||||
if type == "http.response.start":
|
||||
# can also be more native optimized to do it in one GIL call
|
||||
# try socketify_res_write_int_status_with_headers and create and socketify_res_cork_write_int_status_with_headers
|
||||
|
@ -490,20 +547,34 @@ def asgi(ssl, response, info, user_data, aborted):
|
|||
elif isinstance(message, str):
|
||||
data = message.encode("utf-8")
|
||||
lib.socketify_res_cork_end(ssl, response, data, len(data), 0)
|
||||
|
||||
|
||||
if ctx.abort_future is not None:
|
||||
ctx.aborted = True
|
||||
ctx.abort_future.set_result(False)
|
||||
ctx.abort_future = None
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
app._run_task(app.app(scope, receive, send))
|
||||
|
||||
|
||||
|
||||
class _ASGI:
|
||||
def __init__(self, app, options=None, websocket=True, websocket_options=None, task_factory_max_items=100_000, lifespan=True):
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
options=None,
|
||||
websocket=True,
|
||||
websocket_options=None,
|
||||
task_factory_max_items=100_000,
|
||||
lifespan=True,
|
||||
):
|
||||
self.server = App(options, task_factory_max_items=0)
|
||||
self.SERVER_PORT = None
|
||||
self.SERVER_HOST = ""
|
||||
self.SERVER_SCHEME = "https" if self.server.options else "http"
|
||||
self.SERVER_WS_SCHEME = "wss" if self.server.options else "ws"
|
||||
self.SERVER_SCHEME = "https" if options and options.cert_file_name is not None else "http"
|
||||
self.SERVER_WS_SCHEME = "wss" if options and options.cert_file_name is not None else "ws"
|
||||
self.task_factory_max_items = task_factory_max_items
|
||||
self.lifespan = lifespan
|
||||
|
||||
|
@ -512,32 +583,27 @@ class _ASGI:
|
|||
# internally will still use custom task factory for pypy because of Loop
|
||||
if is_pypy:
|
||||
if task_factory_max_items > 0:
|
||||
factory = create_task_with_factory(task_factory_max_items)
|
||||
|
||||
factory = TaskFactory(task_factory_max_items)
|
||||
|
||||
def run_task(task):
|
||||
factory(loop, task_wrapper(task))
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
else:
|
||||
|
||||
def run_task(task):
|
||||
create_task(loop, task_wrapper(task))
|
||||
loop._run_once()
|
||||
future = create_task(loop, task_wrapper(task))
|
||||
future._log_destroy_pending = False
|
||||
|
||||
self._run_task = run_task
|
||||
|
||||
|
||||
else:
|
||||
if sys.version_info >= (3, 8): # name fixed to avoid dynamic name
|
||||
|
||||
def run_task(task):
|
||||
future = loop.create_task(task_wrapper(task), name='socketify.py-request-task')
|
||||
future = create_task(loop, task_wrapper(task))
|
||||
future._log_destroy_pending = False
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
else:
|
||||
def run_task(task):
|
||||
future = loop.create_task(task_wrapper(task))
|
||||
future._log_destroy_pending = False
|
||||
loop._run_once()
|
||||
self._run_task = run_task
|
||||
|
||||
|
||||
self.app = app
|
||||
self.ws_compression = False
|
||||
|
@ -598,6 +664,7 @@ class _ASGI:
|
|||
native_behavior.ping = ffi.NULL
|
||||
native_behavior.pong = ffi.NULL
|
||||
native_behavior.close = ws_close
|
||||
native_behavior.subscription = ffi.NULL
|
||||
|
||||
self.asgi_ws_info = lib.socketify_add_asgi_ws_handler(
|
||||
self.server.SSL, self.server.app, native_behavior, ws_upgrade, self._ptr
|
||||
|
@ -612,102 +679,107 @@ class _ASGI:
|
|||
self.SERVER_HOST = (
|
||||
"0.0.0.0" if isinstance(port_or_options, int) else port_or_options.host
|
||||
)
|
||||
self.server.listen(port_or_options, handler)
|
||||
return self
|
||||
|
||||
def run(self):
|
||||
if not self.lifespan:
|
||||
print("No lifespan!")
|
||||
self.server.run()
|
||||
self.server.listen(port_or_options, handler)
|
||||
return self
|
||||
|
||||
scope = {"type": "lifespan", "asgi": {"version": "3.0", "spec_version": "2.3"}}
|
||||
|
||||
lifespan_loop = Loop(lambda loop, error, response: logging.error("Uncaught Exception: %s" % str(error)))
|
||||
is_starting = True
|
||||
is_stopped = False
|
||||
status = 0 # 0 starting, 1 ok, 2 error, 3 stoping, 4 stopped, 5 stopped with error, 6 no lifespan
|
||||
status_message = ""
|
||||
stop_future = lifespan_loop.create_future()
|
||||
|
||||
asgi_app = self
|
||||
self.is_starting = True
|
||||
self.is_stopped = False
|
||||
self.status = 0 # 0 starting, 1 ok, 2 error, 3 stopping, 4 stopped, 5 stopped with error, 6 no lifespan
|
||||
self.status_message = ""
|
||||
self.stop_future = self.server.loop.create_future()
|
||||
|
||||
async def send(options):
|
||||
nonlocal status, status_message, is_stopped
|
||||
nonlocal asgi_app
|
||||
asgi_app.server.loop.is_idle = False
|
||||
type = options["type"]
|
||||
status_message = options.get("message", "")
|
||||
asgi_app.status_message = options.get("message", "")
|
||||
if type == "lifespan.startup.complete":
|
||||
status = 1
|
||||
asgi_app.status = 1
|
||||
asgi_app.server.listen(port_or_options, handler)
|
||||
elif type == "lifespan.startup.failed":
|
||||
is_stopped = True
|
||||
status = 2
|
||||
asgi_app.is_stopped = True
|
||||
asgi_app.status = 2
|
||||
elif type == "lifespan.shutdown.complete":
|
||||
is_stopped = True
|
||||
status = 4
|
||||
asgi_app.is_stopped = True
|
||||
asgi_app.status = 4
|
||||
elif type == "lifespan.shutdown.failed":
|
||||
is_stopped = True
|
||||
status = 5
|
||||
asgi_app.is_stopped = True
|
||||
asgi_app.status = 5
|
||||
|
||||
async def receive():
|
||||
nonlocal is_starting, is_stopped
|
||||
while not is_stopped:
|
||||
if is_starting:
|
||||
is_starting = False
|
||||
nonlocal asgi_app
|
||||
asgi_app.server.loop.is_idle = False
|
||||
while not asgi_app.is_stopped:
|
||||
if asgi_app.is_starting:
|
||||
asgi_app.is_starting = False
|
||||
return {
|
||||
"type": "lifespan.startup",
|
||||
"asgi": {"version": "3.0", "spec_version": "2.3"},
|
||||
}
|
||||
return await stop_future
|
||||
return await asgi_app.stop_future
|
||||
|
||||
async def task_wrapper(task):
|
||||
nonlocal status
|
||||
nonlocal asgi_app
|
||||
try:
|
||||
return await task
|
||||
except Exception as error:
|
||||
try:
|
||||
# just log in console the error to call attention
|
||||
logging.error("Uncaught Exception: %s" % str(error))
|
||||
status = 6 # no more lifespan
|
||||
if asgi_app.status < 2:
|
||||
asgi_app.status = 6 # no more lifespan
|
||||
asgi_app.server.listen(port_or_options, handler)
|
||||
finally:
|
||||
return None
|
||||
|
||||
self.server.loop.is_idle = False
|
||||
# start lifespan
|
||||
lifespan_loop.ensure_future(task_wrapper(self.app(scope, receive, send)))
|
||||
|
||||
# run until start or fail
|
||||
while status == 0:
|
||||
lifespan_loop.run_once()
|
||||
|
||||
self.server.loop.ensure_future(task_wrapper(self.app(scope, receive, send)))
|
||||
self.server.run()
|
||||
# failed to start
|
||||
if status == 2:
|
||||
logging.error("Startup failed: %s" % str(status_message))
|
||||
if self.status == 2:
|
||||
logging.error("Startup failed: %s" % str(self.status_message))
|
||||
return self
|
||||
return self
|
||||
|
||||
def run(self):
|
||||
if not self.lifespan:
|
||||
self.server.run()
|
||||
return self
|
||||
|
||||
# run app
|
||||
self.server.run()
|
||||
|
||||
self.server.run()
|
||||
# no more lifespan events
|
||||
if status == 6:
|
||||
if self.status == 6:
|
||||
return self
|
||||
|
||||
# signal stop
|
||||
status = 3
|
||||
stop_future.set_result({
|
||||
"type": "lifespan.shutdown",
|
||||
"asgi": {"version": "3.0", "spec_version": "2.3"},
|
||||
})
|
||||
|
||||
# run until end or fail
|
||||
while status == 3:
|
||||
lifespan_loop.run_once()
|
||||
self.status = 3
|
||||
self.stop_future.set_result(
|
||||
{
|
||||
"type": "lifespan.shutdown",
|
||||
"asgi": {"version": "3.0", "spec_version": "2.3"},
|
||||
}
|
||||
)
|
||||
# run until end or fail
|
||||
while self.status == 3:
|
||||
self.server.loop.run_once()
|
||||
|
||||
# failed to stop
|
||||
if status == 5:
|
||||
logging.error("Shutdown failed: %s" % str(status_message))
|
||||
if self.status == 5:
|
||||
logging.error("Shutdown failed: %s" % str(self.status_message))
|
||||
return self
|
||||
|
||||
def __del__(self):
|
||||
if self.asgi_http_info:
|
||||
lib.socketify_destroy_asgi_app_info(self.asgi_http_info)
|
||||
if self.asgi_ws_info:
|
||||
lib.socketify_destroy_asgi_ws_app_info(self.asgi_ws_info)
|
||||
try:
|
||||
if self.asgi_http_info:
|
||||
lib.socketify_destroy_asgi_app_info(self.asgi_http_info)
|
||||
if self.asgi_ws_info:
|
||||
lib.socketify_destroy_asgi_ws_app_info(self.asgi_ws_info)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# "Public" ASGI interface to allow easy forks/workers
|
||||
|
@ -718,8 +790,8 @@ class ASGI:
|
|||
options=None,
|
||||
websocket=True,
|
||||
websocket_options=None,
|
||||
task_factory_max_items=100_000, #default = 100k = +20mib in memory
|
||||
lifespan=True
|
||||
task_factory_max_items=100_000, # default = 100k = +20mib in memory
|
||||
lifespan=True,
|
||||
):
|
||||
self.app = app
|
||||
self.options = options
|
||||
|
@ -728,12 +800,26 @@ class ASGI:
|
|||
self.listen_options = None
|
||||
self.task_factory_max_items = task_factory_max_items
|
||||
self.lifespan = lifespan
|
||||
self.server = None
|
||||
self.pid_list = None
|
||||
|
||||
def listen(self, port_or_options, handler=None):
|
||||
self.listen_options = (port_or_options, handler)
|
||||
return self
|
||||
|
||||
def run(self, workers=1):
|
||||
def close(self):
|
||||
# always wait a sec so forks can start properly if close is called too fast
|
||||
import time
|
||||
time.sleep(1)
|
||||
|
||||
if self.server is not None:
|
||||
self.server.close()
|
||||
if self.pid_list is not None:
|
||||
import signal
|
||||
for pid in self.pid_list:
|
||||
os.kill(pid, signal.SIGINT)
|
||||
|
||||
def run(self, workers=1, block=True):
|
||||
def run_task():
|
||||
server = _ASGI(
|
||||
self.app,
|
||||
|
@ -741,21 +827,32 @@ class ASGI:
|
|||
self.websocket,
|
||||
self.websocket_options,
|
||||
self.task_factory_max_items,
|
||||
self.lifespan,
|
||||
)
|
||||
if self.listen_options:
|
||||
(port_or_options, handler) = self.listen_options
|
||||
server.listen(port_or_options, handler)
|
||||
server.run()
|
||||
|
||||
def create_fork():
|
||||
n = os.fork()
|
||||
# n greater than 0 means parent process
|
||||
if not n > 0:
|
||||
run_task()
|
||||
self.server = server
|
||||
server.run()
|
||||
|
||||
pid_list = []
|
||||
start = 1 if block else 0
|
||||
# fork limiting the cpu count - 1
|
||||
for _ in range(1, workers):
|
||||
create_fork()
|
||||
for _ in range(block, workers):
|
||||
pid = os.fork()
|
||||
# n greater than 0 means parent process
|
||||
if not pid > 0:
|
||||
run_task()
|
||||
break
|
||||
pid_list.append(pid)
|
||||
|
||||
self.pid_list = pid_list
|
||||
|
||||
if block:
|
||||
run_task() # run app on the main process too :)
|
||||
# sigint everything to graceful shutdown
|
||||
import signal
|
||||
for pid in pid_list:
|
||||
os.kill(pid, signal.SIGINT)
|
||||
|
||||
run_task() # run app on the main process too :)
|
||||
return self
|
||||
|
|
|
@ -2,6 +2,7 @@ import inspect
|
|||
import os
|
||||
import logging
|
||||
from . import App, AppOptions, AppListenOptions
|
||||
|
||||
help = """
|
||||
Usage: python -m socketify APP [OPTIONS]
|
||||
python3 -m socketify APP [OPTIONS]
|
||||
|
@ -22,12 +23,12 @@ Options:
|
|||
--ws-idle-timeout INT WebSocket idle timeout [default: 20]
|
||||
--ws-reset-idle-on-send BOOLEAN Reset WebSocket idle timeout on send [default: True]
|
||||
--ws-per-message-deflate BOOLEAN WebSocket per-message-deflate compression [default: False]
|
||||
--ws-max-lifetime INT Websocket maximum socket lifetime in seconds before forced closure, 0 to disable [default: 0]
|
||||
--ws-max-lifetime INT Websocket maximum socket lifetime in minutes before forced closure, 0 to disable [default: 0]
|
||||
--ws-max-backpressure INT WebSocket maximum backpressure in bytes [default: 16777216]
|
||||
--ws-close-on-backpressure-limit BOOLEAN Close connections that hits maximum backpressure [default: False]
|
||||
--lifespan [auto|on|off] Lifespan implementation. [default: auto]
|
||||
--interface [auto|asgi|asgi3|wsgi|ssgi|socketify] Select ASGI (same as ASGI3), ASGI3, WSGI or SSGI as the application interface. [default: auto]
|
||||
--disable-listen-log BOOLEAN Disable log when start listenning [default: False]
|
||||
--disable-listen-log BOOLEAN Disable log when start listening [default: False]
|
||||
--version or -v Display the socketify.py version and exit.
|
||||
--ssl-keyfile TEXT SSL key file
|
||||
--ssl-certfile TEXT SSL certificate file
|
||||
|
@ -43,40 +44,59 @@ Example:
|
|||
|
||||
"""
|
||||
|
||||
# --reload Enable auto-reload. This options also disable --workers or -w option.
|
||||
# --reload-dir PATH Set reload directories explicitly, instead of using the current working directory.
|
||||
# --reload-include TEXT Set extensions to include while watching for files.
|
||||
# Includes '.py,.html,.js,.png,.jpeg,.jpg and .webp' by default;
|
||||
# these defaults can be overridden with `--reload-exclude`.
|
||||
# --reload-exclude TEXT Set extensions to include while watching for files.
|
||||
# --reload-delay INT Milliseconds to delay reload between file changes. [default: 1000]
|
||||
# --reload Enable auto-reload. This options also disable --workers or -w option.
|
||||
# --reload-dir PATH Set reload directories explicitly, instead of using the current working directory.
|
||||
# --reload-include TEXT Set extensions to include while watching for files.
|
||||
# Includes '.py,.html,.js,.png,.jpeg,.jpg and .webp' by default;
|
||||
# these defaults can be overridden with `--reload-exclude`.
|
||||
# --reload-exclude TEXT Set extensions to include while watching for files.
|
||||
# --reload-delay INT Milliseconds to delay reload between file changes. [default: 1000]
|
||||
def is_wsgi(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 2
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 2
|
||||
)
|
||||
|
||||
|
||||
def is_asgi(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 3
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 3
|
||||
)
|
||||
|
||||
|
||||
def is_ssgi(module):
|
||||
return False # no spec yet
|
||||
return False # no spec yet
|
||||
|
||||
|
||||
def is_ssgi(module):
|
||||
return False # no spec yet
|
||||
return False # no spec yet
|
||||
|
||||
|
||||
def is_socketify(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 1
|
||||
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 1
|
||||
)
|
||||
|
||||
|
||||
def is_factory(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 0
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 0
|
||||
)
|
||||
|
||||
|
||||
def str_bool(text):
|
||||
text = str(text).lower()
|
||||
return text == "true"
|
||||
|
||||
|
||||
|
||||
def load_module(file, reload=False):
|
||||
try:
|
||||
[full_module, app] = file.split(':')
|
||||
[full_module, app] = file.split(":")
|
||||
import importlib
|
||||
module =importlib.import_module(full_module)
|
||||
|
||||
module = importlib.import_module(full_module)
|
||||
if reload:
|
||||
importlib.reload(module)
|
||||
|
||||
|
||||
app = getattr(module, app)
|
||||
# if is an factory just auto call
|
||||
if is_factory(module):
|
||||
|
@ -85,6 +105,8 @@ def load_module(file, reload=False):
|
|||
except Exception as error:
|
||||
logging.exception(error)
|
||||
return None
|
||||
|
||||
|
||||
def execute(args):
|
||||
arguments_length = len(args)
|
||||
if arguments_length <= 2:
|
||||
|
@ -93,17 +115,20 @@ def execute(args):
|
|||
if arguments_length == 2 and (args[1] == "--version" or args[1] == "-v"):
|
||||
import pkg_resources # part of setuptools
|
||||
import platform
|
||||
|
||||
version = pkg_resources.require("socketify")[0].version
|
||||
return print(f"Running socketify {version} with {platform.python_implementation()} {platform.python_version()} on {platform.system()}")
|
||||
return print(
|
||||
f"Running socketify {version} with {platform.python_implementation()} {platform.python_version()} on {platform.system()}"
|
||||
)
|
||||
elif arguments_length < 2:
|
||||
return print(help)
|
||||
|
||||
file = (args[1]).lower()
|
||||
file = (args[1]).lower()
|
||||
module = load_module(file)
|
||||
|
||||
if not module:
|
||||
return print(f"Cannot load module {file}")
|
||||
|
||||
|
||||
options_list = args[2:]
|
||||
options = {}
|
||||
selected_option = None
|
||||
|
@ -111,72 +136,84 @@ def execute(args):
|
|||
if selected_option:
|
||||
options[selected_option] = option
|
||||
selected_option = None
|
||||
elif option.startswith('--') or option.startswith('-'):
|
||||
elif option.startswith("--") or option.startswith("-"):
|
||||
if selected_option is None:
|
||||
selected_option = option
|
||||
else: # --factory, --reload etc
|
||||
else: # --factory, --reload etc
|
||||
options[selected_option] = True
|
||||
else:
|
||||
return print(f"Invalid option ${selected_option} see --help")
|
||||
if selected_option: # --factory, --reload etc
|
||||
if selected_option: # --factory, --reload etc
|
||||
options[selected_option] = True
|
||||
|
||||
|
||||
interface = (options.get("--interface", "auto")).lower()
|
||||
|
||||
if interface == "auto":
|
||||
if is_asgi(module):
|
||||
from . import ASGI as Interface
|
||||
|
||||
interface = "asgi"
|
||||
elif is_wsgi(module):
|
||||
from . import WSGI as Interface
|
||||
|
||||
interface = "wsgi"
|
||||
elif is_ssgi(module):
|
||||
from . import SSGI as Interface
|
||||
|
||||
interface = "ssgi"
|
||||
else:
|
||||
interface = "socketify"
|
||||
|
||||
elif interface == "asgi" or interface == "asgi3":
|
||||
from . import ASGI as Interface
|
||||
|
||||
interface = "asgi"
|
||||
# you may use ASGI in SSGI so no checks here
|
||||
if is_wsgi(module):
|
||||
return print("Cannot use WSGI interface as ASGI interface")
|
||||
if not is_asgi(module):
|
||||
return print("ASGI interface must be callable with 3 parameters async def app(scope, receive and send)")
|
||||
return print(
|
||||
"ASGI interface must be callable with 3 parameters async def app(scope, receive and send)"
|
||||
)
|
||||
elif interface == "wsgi":
|
||||
from . import WSGI as Interface
|
||||
|
||||
# you may use WSGI in SSGI so no checks here
|
||||
if is_asgi(module):
|
||||
return print("Cannot use ASGI interface as WSGI interface")
|
||||
if not is_wsgi(module):
|
||||
return print("WSGI interface must be callable with 2 parameters def app(environ, start_response)")
|
||||
return print(
|
||||
"WSGI interface must be callable with 2 parameters def app(environ, start_response)"
|
||||
)
|
||||
|
||||
elif interface == "ssgi":
|
||||
# if not is_ssgi(module):
|
||||
# return print("SSGI is in development yet but is comming soon")
|
||||
# return print("SSGI is in development yet but is coming soon")
|
||||
# from . import SSGI as Interface
|
||||
# interface = "ssgi"
|
||||
return print("SSGI is in development yet but is comming soon")
|
||||
return print("SSGI is in development yet but is coming soon")
|
||||
elif interface != "socketify":
|
||||
return print(f"{interface} interface is not supported yet")
|
||||
|
||||
auto_reload = options.get('--reload', False)
|
||||
workers = int(options.get("--workers", options.get("-w", os.environ.get('WEB_CONCURRENCY', 1))))
|
||||
auto_reload = options.get("--reload", False)
|
||||
workers = int(
|
||||
options.get(
|
||||
"--workers", options.get("-w", os.environ.get("WEB_CONCURRENCY", 1))
|
||||
)
|
||||
)
|
||||
if workers < 1 or auto_reload:
|
||||
workers = 1
|
||||
|
||||
port = int(options.get("--port", options.get("-p", 8000)))
|
||||
host = options.get("--host", options.get("-h", "127.0.0.1"))
|
||||
uds = options.get('--uds', None)
|
||||
lifespan = options.get('--lifespan', "auto")
|
||||
lifespan=False if lifespan == "off" or lifespan is not True else True
|
||||
|
||||
uds = options.get("--uds", None)
|
||||
lifespan = options.get("--lifespan", "auto")
|
||||
lifespan = False if lifespan == "off" else True
|
||||
task_factory_maxitems = int(options.get("--task-factory-maxitems", 100000))
|
||||
|
||||
|
||||
disable_listen_log = options.get("--disable-listen-log", False)
|
||||
websockets = options.get("--ws", "auto")
|
||||
|
||||
|
||||
if websockets == "none":
|
||||
# disable websockets
|
||||
websockets = None
|
||||
|
@ -186,26 +223,25 @@ def execute(args):
|
|||
websockets = True
|
||||
elif is_wsgi(module):
|
||||
# if is WSGI no websockets using auto
|
||||
websockets = False
|
||||
else: # if is socketify websockets must be set in app
|
||||
websockets = False
|
||||
websockets = False
|
||||
else: # if is socketify websockets must be set in app
|
||||
websockets = False
|
||||
else:
|
||||
#websocket dedicated module
|
||||
# websocket dedicated module
|
||||
ws_module = load_module(websockets)
|
||||
if not ws_module:
|
||||
return print(f"Cannot load websocket module {websockets}")
|
||||
websockets = ws_module
|
||||
websockets = ws_module
|
||||
|
||||
key_file_name = options.get("--ssl-keyfile", None)
|
||||
|
||||
|
||||
key_file_name = options.get('--ssl-keyfile', None)
|
||||
|
||||
if key_file_name:
|
||||
ssl_options = AppOptions(
|
||||
key_file_name=options.get('--ssl-keyfile', None),
|
||||
cert_file_name=options.get('--ssl-certfile', None),
|
||||
passphrase=options.get('--ssl-keyfile-password', None),
|
||||
ca_file_name=options.get('--ssl-ca-certs', None),
|
||||
ssl_ciphers=options.get('--ssl-ciphers', None)
|
||||
key_file_name=options.get("--ssl-keyfile", None),
|
||||
cert_file_name=options.get("--ssl-certfile", None),
|
||||
passphrase=options.get("--ssl-keyfile-password", None),
|
||||
ca_file_name=options.get("--ssl-ca-certs", None),
|
||||
ssl_ciphers=options.get("--ssl-ciphers", None),
|
||||
)
|
||||
else:
|
||||
ssl_options = None
|
||||
|
@ -213,24 +249,34 @@ def execute(args):
|
|||
def listen_log(config):
|
||||
if not disable_listen_log:
|
||||
if uds:
|
||||
print(f"Listening on {config.domain} {'https' if ssl_options else 'http'}://localhost now\n")
|
||||
print(
|
||||
f"Listening on {config.domain} {'https' if ssl_options else 'http'}://localhost now\n"
|
||||
)
|
||||
else:
|
||||
print(f"Listening on {'https' if ssl_options else 'http'}://{config.host if config.host and len(config.host) > 1 else '127.0.0.1' }:{config.port} now\n")
|
||||
print(
|
||||
f"Listening on {'https' if ssl_options else 'http'}://{config.host if config.host and len(config.host) > 1 else '127.0.0.1' }:{config.port} now\n"
|
||||
)
|
||||
|
||||
if websockets:
|
||||
websocket_options = {
|
||||
'compression': int(1 if options.get('--ws-per-message-deflate', False) else 0),
|
||||
'max_payload_length': int(options.get('--ws-max-size', 16777216)),
|
||||
'idle_timeout': int(options.get('--ws-idle-timeout', 20)),
|
||||
'send_pings_automatically': str_bool(options.get('--ws-auto-ping', True)),
|
||||
'reset_idle_timeout_on_send': str_bool(options.get('--ws-reset-idle-on-send', True)),
|
||||
'max_lifetime': int(options.get('--ws-max-lifetime', 0)),
|
||||
'max_backpressure': int(options.get('--max-backpressure', 16777216)),
|
||||
'close_on_backpressure_limit': str_bool(options.get('--ws-close-on-backpressure-limit', False))
|
||||
"compression": int(
|
||||
1 if options.get("--ws-per-message-deflate", False) else 0
|
||||
),
|
||||
"max_payload_length": int(options.get("--ws-max-size", 16777216)),
|
||||
"idle_timeout": int(options.get("--ws-idle-timeout", 20)),
|
||||
"send_pings_automatically": str_bool(options.get("--ws-auto-ping", True)),
|
||||
"reset_idle_timeout_on_send": str_bool(
|
||||
options.get("--ws-reset-idle-on-send", True)
|
||||
),
|
||||
"max_lifetime": int(options.get("--ws-max-lifetime", 0)),
|
||||
"max_backpressure": int(options.get("--max-backpressure", 16777216)),
|
||||
"close_on_backpressure_limit": str_bool(
|
||||
options.get("--ws-close-on-backpressure-limit", False)
|
||||
),
|
||||
}
|
||||
else:
|
||||
websocket_options = None
|
||||
|
||||
|
||||
if interface == "socketify":
|
||||
if is_asgi(websockets):
|
||||
return print("Cannot mix ASGI websockets with socketify.py interface yet")
|
||||
|
@ -239,15 +285,24 @@ def execute(args):
|
|||
elif is_wsgi(module):
|
||||
return print("Cannot use WSGI interface as socketify interface")
|
||||
elif not is_socketify(module):
|
||||
return print("socketify interface must be callable with 1 parameter def run(app: App)")
|
||||
return print(
|
||||
"socketify interface must be callable with 1 parameter def run(app: App)"
|
||||
)
|
||||
# run app with the settings desired
|
||||
def run_app():
|
||||
# Add lifespan when lifespan hooks are implemented
|
||||
fork_app = App(ssl_options, int(options.get("--req-res-factory-maxitems", 0)), int(options.get("--ws-factory-maxitems", 0)), task_factory_maxitems)
|
||||
module(fork_app) # call module factory
|
||||
fork_app = App(
|
||||
ssl_options,
|
||||
int(options.get("--req-res-factory-maxitems", 0)),
|
||||
int(options.get("--ws-factory-maxitems", 0)),
|
||||
task_factory_maxitems,
|
||||
lifespan,
|
||||
)
|
||||
module(fork_app) # call module factory
|
||||
|
||||
if websockets: # if socketify websockets are added using --ws in socketify interface we can set here
|
||||
websockets.update(websocket_options) # set websocket options
|
||||
if (
|
||||
websockets
|
||||
): # if socketify websockets are added using --ws in socketify interface we can set here
|
||||
websockets.update(websocket_options) # set websocket options
|
||||
fork_app.ws("/*", websockets)
|
||||
if uds:
|
||||
fork_app.listen(AppListenOptions(domain=uds), listen_log)
|
||||
|
@ -255,26 +310,41 @@ def execute(args):
|
|||
fork_app.listen(AppListenOptions(port=port, host=host), listen_log)
|
||||
fork_app.run()
|
||||
|
||||
# now we can start all over again
|
||||
def create_fork(_):
|
||||
n = os.fork()
|
||||
pid_list = []
|
||||
# fork limiting the cpu count - 1
|
||||
for _ in range(1, workers):
|
||||
pid = os.fork()
|
||||
# n greater than 0 means parent process
|
||||
if not n > 0:
|
||||
if not pid > 0:
|
||||
run_app()
|
||||
return n
|
||||
break
|
||||
pid_list.append(pid)
|
||||
|
||||
# run in all forks
|
||||
pid_list = list(map(create_fork, range(1, workers)))
|
||||
run_app() # run app on the main process too :)
|
||||
|
||||
# run in this process
|
||||
run_app()
|
||||
# sigint everything to gracefull shutdown
|
||||
# sigint everything to graceful shutdown
|
||||
import signal
|
||||
for pid in pid_list:
|
||||
os.kill(pid, signal.SIGINT)
|
||||
else:
|
||||
|
||||
if uds:
|
||||
Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options, task_factory_max_items=task_factory_maxitems, lifespan=lifespan).listen(AppListenOptions(domain=uds), listen_log).run(workers=workers)
|
||||
Interface(
|
||||
module,
|
||||
options=ssl_options,
|
||||
websocket=websockets,
|
||||
websocket_options=websocket_options,
|
||||
task_factory_max_items=task_factory_maxitems,
|
||||
lifespan=lifespan,
|
||||
).listen(AppListenOptions(domain=uds), listen_log).run(workers=workers)
|
||||
else:
|
||||
Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options, task_factory_max_items=task_factory_maxitems, lifespan=lifespan).listen(AppListenOptions(port=port, host=host), listen_log).run(workers=workers)
|
||||
Interface(
|
||||
module,
|
||||
options=ssl_options,
|
||||
websocket=websockets,
|
||||
websocket_options=websocket_options,
|
||||
task_factory_max_items=task_factory_maxitems,
|
||||
lifespan=lifespan,
|
||||
).listen(AppListenOptions(port=port, host=host), listen_log).run(
|
||||
workers=workers
|
||||
)
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class AppListenOptions:
|
||||
port: int = 0
|
||||
host: str = None
|
||||
options: int = 0
|
||||
domain: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.port, int):
|
||||
raise RuntimeError("port must be an int")
|
||||
if not isinstance(self.host, (type(None), str)):
|
||||
raise RuntimeError("host must be a str if specified")
|
||||
if not isinstance(self.domain, (type(None), str)):
|
||||
raise RuntimeError("domain must be a str if specified")
|
||||
if not isinstance(self.options, int):
|
||||
raise RuntimeError("options must be an int")
|
||||
if self.domain and (self.host or self.port != 0):
|
||||
raise RuntimeError(
|
||||
"if domain is specified, host and port will be no effect"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppOptions:
|
||||
key_file_name: str = None
|
||||
cert_file_name: str = None
|
||||
passphrase: str = None
|
||||
dh_params_file_name: str = None
|
||||
ca_file_name: str = None
|
||||
ssl_ciphers: str = None
|
||||
ssl_prefer_low_memory_usage: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
NoneType = type(None)
|
||||
|
||||
if not isinstance(self.key_file_name, (NoneType, str)):
|
||||
raise RuntimeError("key_file_name must be a str if specified")
|
||||
if not isinstance(self.cert_file_name, (NoneType, str)):
|
||||
raise RuntimeError("cert_file_name must be a str if specified")
|
||||
if not isinstance(self.passphrase, (NoneType, str)):
|
||||
raise RuntimeError("passphrase must be a str if specified")
|
||||
if not isinstance(self.dh_params_file_name, (NoneType, str)):
|
||||
raise RuntimeError("dh_params_file_name must be a str if specified")
|
||||
if not isinstance(self.ca_file_name, (NoneType, str)):
|
||||
raise RuntimeError("ca_file_name must be a str if specified")
|
||||
if not isinstance(self.ssl_ciphers, (NoneType, str)):
|
||||
raise RuntimeError("ssl_ciphers must be a str if specified")
|
||||
if not isinstance(self.ssl_prefer_low_memory_usage, int):
|
||||
raise RuntimeError("ssl_prefer_low_memory_usage must be an int")
|
|
@ -9,7 +9,7 @@ mimetypes.init()
|
|||
# We have an version of this using aiofile and aiofiles
|
||||
# This is an sync version without any dependencies is normally much faster in CPython and PyPy3
|
||||
# In production we highly recommend to use CDN like CloudFlare or/and NGINX or similar for static files
|
||||
# TODO: this must be reimplemented pure C++ to avoid GIL
|
||||
# TODO: this must be reimplemented pure C++ to avoid GIL
|
||||
async def sendfile(res, req, filename):
|
||||
# read headers before the first await
|
||||
if_modified_since = req.get_header("if-modified-since")
|
||||
|
@ -39,13 +39,13 @@ async def sendfile(res, req, filename):
|
|||
|
||||
# check if modified since is provided
|
||||
if if_modified_since == last_modified:
|
||||
return res.cork(lambda res: res.write_status(304).end_without_body())
|
||||
return res.cork(lambda res: res.write_status(304).end_without_body())
|
||||
|
||||
# add content type
|
||||
(content_type, encoding) = mimetypes.guess_type(filename, strict=True)
|
||||
if content_type and encoding:
|
||||
content_type = "%s; %s" % (content_type, encoding)
|
||||
|
||||
|
||||
with open(filename, "rb") as fd:
|
||||
# check range and support it
|
||||
if start > 0 or not end == -1:
|
||||
|
@ -55,8 +55,14 @@ async def sendfile(res, req, filename):
|
|||
fd.seek(start)
|
||||
if start > total_size or size > total_size or size < 0 or start < 0:
|
||||
if content_type:
|
||||
return res.cork(lambda res: res.write_header(b"Content-Type", content_type).write_status(416).end_without_body())
|
||||
return res.cork(lambda res: res.write_status(416).end_without_body())
|
||||
return res.cork(
|
||||
lambda res: res.write_header(b"Content-Type", content_type)
|
||||
.write_status(416)
|
||||
.end_without_body()
|
||||
)
|
||||
return res.cork(
|
||||
lambda res: res.write_status(416).end_without_body()
|
||||
)
|
||||
status = 206
|
||||
else:
|
||||
end = size - 1
|
||||
|
@ -64,7 +70,7 @@ async def sendfile(res, req, filename):
|
|||
|
||||
def send_headers(res):
|
||||
res.write_status(status)
|
||||
# tells the broswer the last modified date
|
||||
# tells the browser the last modified date
|
||||
res.write_header(b"Last-Modified", last_modified)
|
||||
|
||||
# tells the browser that we support range
|
||||
|
@ -74,6 +80,7 @@ async def sendfile(res, req, filename):
|
|||
res.write_header(
|
||||
b"Content-Range", "bytes %d-%d/%d" % (start, end, total_size)
|
||||
)
|
||||
|
||||
res.cork(send_headers)
|
||||
pending_size = size
|
||||
# keep sending until abort or done
|
||||
|
@ -104,9 +111,9 @@ def static_route(app, route, directory):
|
|||
def route_handler(res, req):
|
||||
url = req.get_url()
|
||||
res.grab_aborted_handler()
|
||||
url = url[len(route)::]
|
||||
url = url[len(route) : :]
|
||||
if url.endswith("/"):
|
||||
if url.startswith("/"):
|
||||
if url.startswith("/"):
|
||||
url = url[1:-1]
|
||||
else:
|
||||
url = url[:-1]
|
||||
|
@ -125,10 +132,73 @@ def static_route(app, route, directory):
|
|||
|
||||
|
||||
def middleware(*functions):
|
||||
syncs = []
|
||||
asyncs = []
|
||||
for function in functions:
|
||||
# all is async after the first async
|
||||
if inspect.iscoroutinefunction(function) or len(asyncs) > 0:
|
||||
asyncs.append(function)
|
||||
else:
|
||||
syncs.append(function)
|
||||
if len(asyncs) == 0: # pure sync
|
||||
return sync_middleware(*functions)
|
||||
if len(syncs) == 0: # pure async
|
||||
return async_middleware(*functions)
|
||||
|
||||
# we use Optional data=None at the end so you can use and middleware inside a middleware
|
||||
def optimized_middleware_route(res, req, data=None):
|
||||
# circle to all middlewares
|
||||
for function in syncs:
|
||||
# call middlewares
|
||||
data = function(res, req, data)
|
||||
# stops if returns Falsy
|
||||
if not data:
|
||||
return
|
||||
|
||||
async def wrapper(res, req, data):
|
||||
# circle to all middlewares
|
||||
for function in asyncs:
|
||||
# detect if is coroutine or not
|
||||
if inspect.iscoroutinefunction(function):
|
||||
data = await function(res, req, data)
|
||||
else:
|
||||
# call sync middleware
|
||||
data = function(res, req, data)
|
||||
# stops if returns Falsy
|
||||
if not data:
|
||||
break
|
||||
return data
|
||||
|
||||
# in async query string, arguments and headers are only valid until the first await
|
||||
# preserve queries, headers, parameters, url, full_url and method
|
||||
req.preserve()
|
||||
|
||||
# go async
|
||||
res.run_async(wrapper(res, req, data))
|
||||
|
||||
return optimized_middleware_route
|
||||
|
||||
|
||||
def sync_middleware(*functions):
|
||||
# we use Optional data=None at the end so you can use and middleware inside a middleware
|
||||
def middleware_route(res, req, data=None):
|
||||
# circle to all middlewares
|
||||
for function in functions:
|
||||
# call middlewares
|
||||
data = function(res, req, data)
|
||||
# stops if returns Falsy
|
||||
if not data:
|
||||
break
|
||||
return data
|
||||
|
||||
return middleware_route
|
||||
|
||||
|
||||
def async_middleware(*functions):
|
||||
# we use Optional data=None at the end so you can use and middleware inside a middleware
|
||||
async def middleware_route(res, req, data=None):
|
||||
some_async_as_run = False
|
||||
# cicle to all middlewares
|
||||
# circle to all middlewares
|
||||
for function in functions:
|
||||
# detect if is coroutine or not
|
||||
if inspect.iscoroutinefunction(function):
|
||||
|
@ -149,6 +219,144 @@ def middleware(*functions):
|
|||
return middleware_route
|
||||
|
||||
|
||||
class DecoratorRouter:
|
||||
def __init__(self, app, prefix: str = "", *middlewares):
|
||||
self.app = app
|
||||
self.middlewares = list(*middlewares)
|
||||
self.prefix = prefix
|
||||
|
||||
def get(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.get(path, middleware(*middies))
|
||||
else:
|
||||
self.app.get(path, handler)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
def post(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.post(path, middleware(*middies))
|
||||
else:
|
||||
self.app.post(path, handler)
|
||||
|
||||
return decorator
|
||||
|
||||
def options(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.options(path, middleware(*middies))
|
||||
else:
|
||||
self.app.options(path, handler)
|
||||
|
||||
return decorator
|
||||
|
||||
def delete(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.delete(path, middleware(*middies))
|
||||
else:
|
||||
self.app.delete(path, handler)
|
||||
|
||||
return decorator
|
||||
|
||||
def patch(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.patch(path, middleware(*middies))
|
||||
else:
|
||||
self.app.patch(path, handler)
|
||||
|
||||
return decorator
|
||||
|
||||
def put(self, path: str):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.put(path, middleware(*middies))
|
||||
else:
|
||||
self.app.put(path, handler)
|
||||
|
||||
return decorator
|
||||
|
||||
def head(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.head(path, middleware(*middies))
|
||||
else:
|
||||
self.app.head(path, handler)
|
||||
|
||||
return decorator
|
||||
|
||||
def connect(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.connect(path, middleware(*middies))
|
||||
else:
|
||||
self.app.connect(path, handler)
|
||||
|
||||
return decorator
|
||||
|
||||
def trace(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.trace(path, middleware(*middies))
|
||||
else:
|
||||
self.app.trace(path, handler)
|
||||
|
||||
return decorator
|
||||
|
||||
def any(self, path):
|
||||
path = f"{self.prefix}{path}"
|
||||
|
||||
def decorator(handler):
|
||||
if len(self.middlewares) > 0:
|
||||
middies = list(self.middlewares)
|
||||
middies.append(handler)
|
||||
self.app.any(path, middleware(*middies))
|
||||
else:
|
||||
self.app.any(path, handler)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class MiddlewareRouter:
|
||||
def __init__(self, app, *middlewares):
|
||||
self.app = app
|
||||
|
|
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
Plik binarny nie jest wyświetlany.
|
@ -1,12 +1,13 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from .tasks import create_task, create_task_with_factory
|
||||
from .tasks import create_task, TaskFactory
|
||||
from .uv import UVLoop
|
||||
|
||||
import asyncio
|
||||
import platform
|
||||
|
||||
is_pypy = platform.python_implementation() == 'PyPy'
|
||||
is_pypy = platform.python_implementation() == "PyPy"
|
||||
|
||||
async def task_wrapper(exception_handler, loop, response, task):
|
||||
try:
|
||||
return await task
|
||||
|
@ -24,16 +25,18 @@ async def task_wrapper(exception_handler, loop, response, task):
|
|||
|
||||
|
||||
class Loop:
|
||||
def __init__(self, exception_handler=None, task_factory_max_items=0):
|
||||
|
||||
def __init__(self, exception_handler=None, task_factory_max_items=0, idle_relaxation_time=0.01):
|
||||
|
||||
# get the current running loop or create a new one without warnings
|
||||
self.loop = asyncio._get_running_loop()
|
||||
self._idle_count = 0
|
||||
self.is_idle = False
|
||||
self.idle_relaxation_time = idle_relaxation_time
|
||||
if self.loop is None:
|
||||
self.loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self.loop)
|
||||
|
||||
self.uv_loop = UVLoop()
|
||||
|
||||
self.uv_loop = UVLoop()
|
||||
if hasattr(exception_handler, "__call__"):
|
||||
self.exception_handler = exception_handler
|
||||
self.loop.set_exception_handler(
|
||||
|
@ -43,23 +46,30 @@ class Loop:
|
|||
self.exception_handler = None
|
||||
|
||||
self.started = False
|
||||
if is_pypy: # PyPy async Optimizations
|
||||
if task_factory_max_items > 0: # Only available in PyPy for now
|
||||
self._task_factory = create_task_with_factory(task_factory_max_items)
|
||||
if is_pypy: # PyPy async Optimizations
|
||||
if task_factory_max_items > 0: # Only available in PyPy for now
|
||||
self._task_factory = TaskFactory(task_factory_max_items)
|
||||
else:
|
||||
self._task_factory = create_task
|
||||
self.run_async = self._run_async_pypy
|
||||
# custom task factory
|
||||
|
||||
# TODO: check if any framework breaks without current_task(loop) support
|
||||
# custom task factory for other tasks
|
||||
def pypy_task_factory(loop, coro, context=None):
|
||||
return create_task(loop, coro, context=context)
|
||||
|
||||
self.loop.set_task_factory(pypy_task_factory)
|
||||
else:
|
||||
# CPython performs worse using custom create_task, so native create_task is used
|
||||
# but this also did not allow the use of create_task_with_factory :/
|
||||
# native create_task do not allow to change context, callbacks, state etc
|
||||
|
||||
# TODO: check if any framework breaks without current_task(loop) support
|
||||
# custom task factory for other tasks
|
||||
def cpython_task_factory(loop, coro, context=None):
|
||||
return create_task(loop, coro, context=context)
|
||||
|
||||
self.loop.set_task_factory(cpython_task_factory)
|
||||
|
||||
# CPython performs equals or worse using TaskFactory
|
||||
self.run_async = self._run_async_cpython
|
||||
|
||||
|
||||
def set_timeout(self, timeout, callback, user_data):
|
||||
return self.uv_loop.create_timer(timeout, 0, callback, user_data)
|
||||
|
@ -69,24 +79,80 @@ class Loop:
|
|||
|
||||
def _keep_alive(self):
|
||||
if self.started:
|
||||
self.uv_loop.run_once()
|
||||
self.loop.call_soon(self._keep_alive)
|
||||
|
||||
relax = False
|
||||
if not self.is_idle:
|
||||
self._idle_count = 0
|
||||
elif self._idle_count < 10000:
|
||||
self._idle_count += 1
|
||||
else:
|
||||
relax = True
|
||||
|
||||
self.is_idle = True
|
||||
|
||||
if relax:
|
||||
self.uv_loop.run_nowait()
|
||||
|
||||
if self._idle_count < 15000:
|
||||
self._idle_count += 1
|
||||
# we are idle not for long, wait 5s until next relax mode
|
||||
self.loop.call_later(0.001, self._keep_alive)
|
||||
else:
|
||||
# we are really idle now lets use less CPU
|
||||
self.loop.call_later(self.idle_relaxation_time, self._keep_alive)
|
||||
else:
|
||||
self.uv_loop.run_nowait()
|
||||
# be more aggressive when needed
|
||||
self.loop.call_soon(self._keep_alive)
|
||||
|
||||
def create_task(self, *args, **kwargs):
|
||||
# this is not using optimized create_task yet
|
||||
return self.loop.create_task(*args, **kwargs)
|
||||
|
||||
def ensure_future(self, task):
|
||||
return asyncio.ensure_future(task, loop=self.loop)
|
||||
|
||||
def set_event_loop(self, loop):
|
||||
needs_start = False
|
||||
if self.loop.is_running():
|
||||
self.stop()
|
||||
|
||||
self.loop = loop
|
||||
if self.exception_handler is not None:
|
||||
self.loop.set_exception_handler(
|
||||
lambda loop, context: self.exception_handler(loop, context, None)
|
||||
)
|
||||
|
||||
if is_pypy: # PyPy async Optimizations
|
||||
# TODO: check if any framework breaks without current_task(loop) support
|
||||
# custom task factory for other tasks
|
||||
def pypy_task_factory(loop, coro, context=None):
|
||||
return create_task(loop, coro, context=context)
|
||||
|
||||
self.loop.set_task_factory(pypy_task_factory)
|
||||
else:
|
||||
|
||||
# TODO: check if any framework breaks without current_task(loop) support
|
||||
# custom task factory for other tasks
|
||||
def cpython_task_factory(loop, coro, context=None):
|
||||
return create_task(loop, coro, context=context)
|
||||
|
||||
self.loop.set_task_factory(cpython_task_factory)
|
||||
if needs_start:
|
||||
self.run()
|
||||
|
||||
def create_background_task(self, bg_task):
|
||||
def next_tick():
|
||||
self.ensure_future(bg_task())
|
||||
self.loop.call_soon(next_tick)
|
||||
|
||||
def run_until_complete(self, task=None):
|
||||
self.started = True
|
||||
if task is not None:
|
||||
future = self.ensure_future(task)
|
||||
else:
|
||||
future = None
|
||||
future = None
|
||||
self.loop.call_soon(self._keep_alive)
|
||||
self.loop.run_until_complete()
|
||||
self.loop.run_until_complete(future)
|
||||
# clean up uvloop
|
||||
self.uv_loop.stop()
|
||||
return future
|
||||
|
@ -96,7 +162,7 @@ class Loop:
|
|||
if task is not None:
|
||||
future = self.ensure_future(task)
|
||||
else:
|
||||
future = None
|
||||
future = None
|
||||
self.loop.call_soon(self._keep_alive)
|
||||
self.loop.run_forever()
|
||||
# clean up uvloop
|
||||
|
@ -104,13 +170,19 @@ class Loop:
|
|||
return future
|
||||
|
||||
def run_once(self):
|
||||
# run one step of asyncio
|
||||
self.loop._stopping = True
|
||||
self.loop._run_once()
|
||||
# run one step of asyncio
|
||||
# if loop._run_once is not available use loop.run_forever + loop.call_soon(loop.stop)
|
||||
# this is useful when using uvloop or custom loops
|
||||
try:
|
||||
self.loop._stopping = True
|
||||
self.loop._run_once()
|
||||
except Exception:
|
||||
# this can be _StopError with means we should not call run_forever, but we can ignore it
|
||||
self.loop.call_soon(self.loop.stop)
|
||||
self.loop.run_forever()
|
||||
# run one step of libuv
|
||||
self.uv_loop.run_once()
|
||||
|
||||
|
||||
def stop(self):
|
||||
if self.started:
|
||||
# Just mark as started = False and wait
|
||||
|
@ -120,29 +192,29 @@ class Loop:
|
|||
# Exposes native loop for uWS
|
||||
def get_native_loop(self):
|
||||
return self.uv_loop.get_native_loop()
|
||||
|
||||
|
||||
def _run_async_pypy(self, task, response=None):
|
||||
# this garanties error 500 in case of uncaught exceptions, and can trigger the custom error handler
|
||||
# using an coroutine wrapper generates less overhead than using add_done_callback
|
||||
# this is an custom task/future with less overhead
|
||||
future = self._task_factory(self.loop, task_wrapper(self.exception_handler, self.loop, response, task))
|
||||
# force asyncio run once to enable req in async functions before first await
|
||||
self.loop._run_once()
|
||||
return None # this future maybe already done and reused not safe to await
|
||||
# this guarantees error 500 in case of uncaught exceptions, and can trigger the custom error handler
|
||||
# using an coroutine wrapper generates less overhead than using add_done_callback
|
||||
# this is an custom task/future with less overhead and that calls the first step
|
||||
future = self._task_factory(
|
||||
self.loop, task_wrapper(self.exception_handler, self.loop, response, task)
|
||||
)
|
||||
return None # this future maybe already done and reused not safe to await
|
||||
|
||||
def _run_async_cpython(self, task, response=None):
|
||||
# this guarantees error 500 in case of uncaught exceptions, and can trigger the custom error handler
|
||||
# using an coroutine wrapper generates less overhead than using add_done_callback
|
||||
# this is an custom task/future with less overhead and that calls the first step
|
||||
future = create_task(self.loop, task_wrapper(self.exception_handler, self.loop, response, task))
|
||||
return None # this future is safe to await but we return None for compatibility, and in the future will be the same behavior as PyPy
|
||||
|
||||
def _run_async_cpython(self, task, response=None):
|
||||
# this garanties error 500 in case of uncaught exceptions, and can trigger the custom error handler
|
||||
# using an coroutine wrapper generates less overhead than using add_done_callback
|
||||
future = self.loop.create_task(task_wrapper(self.exception_handler, self.loop, response, task))
|
||||
# force asyncio run once to enable req in async functions before first await
|
||||
self.loop._run_once()
|
||||
return None # this future is safe to await but we return None for compatibility, and in the future will be the same behavior as PyPy
|
||||
def dispose(self):
|
||||
if self.uv_loop:
|
||||
self.uv_loop.dispose()
|
||||
self.uv_loop = None
|
||||
|
||||
|
||||
# if sys.version_info >= (3, 11)
|
||||
# with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
|
||||
# runner.run(main())
|
||||
|
|
|
@ -123,6 +123,8 @@ typedef void (*uws_websocket_message_handler)(uws_websocket_t *ws, const char *m
|
|||
typedef void (*uws_websocket_ping_pong_handler)(uws_websocket_t *ws, const char *message, size_t length, void* user_data);
|
||||
typedef void (*uws_websocket_close_handler)(uws_websocket_t *ws, int code, const char *message, size_t length, void* user_data);
|
||||
typedef void (*uws_websocket_upgrade_handler)(uws_res_t *response, uws_req_t *request, uws_socket_context_t *context, void* user_data);
|
||||
typedef void (*uws_websocket_subscription_handler)(uws_websocket_t *ws, const char *topic_name, size_t topic_name_length, int new_number_of_subscriber, int old_number_of_subscriber, void* user_data);
|
||||
|
||||
typedef struct
|
||||
{
|
||||
uws_compress_options_t compression;
|
||||
|
@ -140,6 +142,7 @@ typedef struct
|
|||
uws_websocket_ping_pong_handler ping;
|
||||
uws_websocket_ping_pong_handler pong;
|
||||
uws_websocket_close_handler close;
|
||||
uws_websocket_subscription_handler subscription;
|
||||
} uws_socket_behavior_t;
|
||||
|
||||
typedef struct {
|
||||
|
@ -184,7 +187,7 @@ void uws_add_server_name_with_options(int ssl, uws_app_t *app, const char *hostn
|
|||
void uws_missing_server_name(int ssl, uws_app_t *app, uws_missing_server_handler handler, void *user_data);
|
||||
void uws_filter(int ssl, uws_app_t *app, uws_filter_handler handler, void *user_data);
|
||||
|
||||
|
||||
void uws_res_close(int ssl, uws_res_t *res);
|
||||
void uws_res_end(int ssl, uws_res_t *res, const char *data, size_t length, bool close_connection);
|
||||
void uws_res_pause(int ssl, uws_res_t *res);
|
||||
void uws_res_resume(int ssl, uws_res_t *res);
|
||||
|
@ -212,7 +215,7 @@ size_t uws_res_get_proxied_remote_address_as_text(int ssl, uws_res_t *res, const
|
|||
|
||||
bool uws_req_is_ancient(uws_req_t *res);
|
||||
bool uws_req_get_yield(uws_req_t *res);
|
||||
void uws_req_set_field(uws_req_t *res, bool yield);
|
||||
void uws_req_set_yield(uws_req_t *res, bool yield);
|
||||
size_t uws_req_get_url(uws_req_t *res, const char **dest);
|
||||
size_t uws_req_get_method(uws_req_t *res, const char **dest);
|
||||
size_t uws_req_get_case_sensitive_method(uws_req_t *res, const char **dest);
|
||||
|
@ -349,21 +352,21 @@ typedef struct {
|
|||
socketify_header* header_list;
|
||||
} socketify_asgi_ws_data;
|
||||
|
||||
typedef void (*socketify_asgi_method_handler)(int ssl, uws_res_t *response, socketify_asgi_data request, void *user_data, bool* aborted);
|
||||
typedef void (*socketify_asgi_method_handler)(int ssl, uws_res_t *response, socketify_asgi_data request, void *user_data);
|
||||
typedef struct {
|
||||
int ssl;
|
||||
uws_app_t* app;
|
||||
socketify_asgi_method_handler handler;
|
||||
void * user_data;
|
||||
} socksocketify_asgi_app_info;
|
||||
typedef void (*socketify_asgi_ws_method_handler)(int ssl, uws_res_t *response, socketify_asgi_ws_data request, uws_socket_context_t* socket, void *user_data, bool* aborted);
|
||||
typedef void (*socketify_asgi_ws_method_handler)(int ssl, uws_res_t *response, socketify_asgi_ws_data request, uws_socket_context_t* socket, void *user_data);
|
||||
typedef struct {
|
||||
int ssl;
|
||||
uws_app_t* app;
|
||||
socketify_asgi_ws_method_handler handler;
|
||||
uws_socket_behavior_t behavior;
|
||||
void * user_data;
|
||||
} socksocketify_asgi_ws_app_info;
|
||||
} socketify_asgi_ws_app_info;
|
||||
|
||||
socketify_asgi_data socketify_asgi_request(int ssl, uws_req_t *req, uws_res_t *res);
|
||||
void socketify_destroy_headers(socketify_header* headers);
|
||||
|
@ -373,8 +376,8 @@ socketify_asgi_ws_data socketify_asgi_ws_request(int ssl, uws_req_t *req, uws_re
|
|||
bool socketify_res_write_int_status(int ssl, uws_res_t* res, int code);
|
||||
socksocketify_asgi_app_info* socketify_add_asgi_http_handler(int ssl, uws_app_t* app, socketify_asgi_method_handler handler, void* user_data);
|
||||
void socketify_destroy_asgi_app_info(socksocketify_asgi_app_info* app);
|
||||
socksocketify_asgi_ws_app_info* socketify_add_asgi_ws_handler(int ssl, uws_app_t* app, uws_socket_behavior_t behavior, socketify_asgi_ws_method_handler handler, void* user_data);
|
||||
void socketify_destroy_asgi_ws_app_info(socksocketify_asgi_ws_app_info* app);
|
||||
socketify_asgi_ws_app_info* socketify_add_asgi_ws_handler(int ssl, uws_app_t* app, uws_socket_behavior_t behavior, socketify_asgi_ws_method_handler handler, void* user_data);
|
||||
void socketify_destroy_asgi_ws_app_info(socketify_asgi_ws_app_info* app);
|
||||
|
||||
void socketify_res_cork_write(int ssl, uws_res_t *response, const char* data, size_t length);
|
||||
void socketify_res_cork_end(int ssl, uws_res_t *response, const char* data, size_t length, bool close_connection);
|
||||
|
@ -382,6 +385,12 @@ void socketify_ws_cork_send(int ssl, uws_websocket_t *ws, const char* data, size
|
|||
|
||||
|
||||
void socketify_ws_cork_send_with_options(int ssl, uws_websocket_t *ws, const char* data, size_t length, uws_opcode_t opcode, bool compress, bool close_connection);
|
||||
|
||||
void socketify_res_send_int_code(int ssl, uws_res_t *res, const char* content_data, size_t content_data_size, int code, const char *content_type, size_t content_type_size, bool close_connection);
|
||||
void socketify_res_send(int ssl, uws_res_t *res, const char *content_data, size_t content_data_size, const char *status_code, size_t status_code_size, const char *content_type, size_t content_type_size, bool close_connection);
|
||||
|
||||
void socketify_res_cork_send_int_code(int ssl, uws_res_t *res, const char* content_data, size_t content_data_size, int code, const char *content_type, size_t content_type_size, bool close_connection);
|
||||
void socketify_res_cork_send(int ssl, uws_res_t *res, const char *content_data, size_t content_data_size, const char *status_code, size_t status_code_size, const char *content_type, size_t content_type_size, bool close_connection);
|
||||
"""
|
||||
)
|
||||
|
||||
|
@ -398,5 +407,3 @@ library_path = os.path.join(
|
|||
|
||||
|
||||
lib = ffi.dlopen(library_path)
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
LIBRARY_NAME := libsocketify
|
||||
UWS_LIBRARY_NAME := libuwebsockets
|
||||
CC := clang
|
||||
CXX := clang++
|
||||
CC ?= clang
|
||||
CXX ?= clang++
|
||||
|
||||
ARCH := amd64
|
||||
ifeq ($(PLATFORM), arm64)
|
||||
|
@ -72,17 +72,14 @@ macos:
|
|||
# build boringssl
|
||||
cd ../uWebSockets/uSockets/boringssl && mkdir -p amd64 && cd amd64 && cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_DEPLOYMENT_TARGET=10.14 .. && make crypto ssl
|
||||
|
||||
# build lsquic
|
||||
cd ../uWebSockets/uSockets/lsquic && cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBORINGSSL_DIR=../boringssl -DCMAKE_BUILD_TYPE=Release -DLSQUIC_BIN=Off . && make lsquic
|
||||
|
||||
# build uWebSockets
|
||||
cd ../uWebSockets/uSockets && $(CC) -mmacosx-version-min=10.14 -I src -I lsquic/include -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -DLIBUS_USE_QUIC -pthread -fPIC -std=c11 -O3 -c src/*.c src/eventing/*.c src/crypto/*.c
|
||||
cd ../uWebSockets/uSockets && $(CXX) -stdlib=libc++ -mmacosx-version-min=10.14 -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -DLIBUS_USE_QUIC -pthread -fPIC -std=c++17 -O3 -c src/crypto/*.cpp
|
||||
cd ../uWebSockets/uSockets && $(CC) -mmacosx-version-min=10.14 -I src -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -pthread -fPIC -std=c11 -O3 -c src/*.c src/eventing/*.c src/crypto/*.c
|
||||
cd ../uWebSockets/uSockets && $(CXX) -stdlib=libc++ -mmacosx-version-min=10.14 -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -pthread -fPIC -std=c++17 -O3 -c src/crypto/*.cpp
|
||||
cd ../uWebSockets/uSockets && $(AR) rvs uSockets_darwin_amd64.a *.o
|
||||
|
||||
# build CAPI + libsocketify
|
||||
$(CXX) -stdlib=libc++ -mmacosx-version-min=10.14 -I ./src -I ../uWebSockets/src -I ../uWebSockets/uSockets/src -I ../uWebSockets/capi -I ../uWebSockets/uSockets/lsquic/include -I ../uWebSockets/uSockets/boringssl/include -DUWS_WITH_PROXY -pthread -fPIC -std=c++17 -c -O3 ./src/$(LIBRARY_NAME).cpp
|
||||
$(CXX) -stdlib=libc++ -mmacosx-version-min=10.14 -shared -undefined dynamic_lookup -o ../$(LIBRARY_NAME)_darwin_amd64.so $(LIBRARY_NAME).o ../uWebSockets/uSockets/uSockets_darwin_amd64.a ../uWebSockets/uSockets/boringssl/amd64/ssl/libssl.a ../uWebSockets/uSockets/boringssl/amd64/crypto/libcrypto.a ../uWebSockets/uSockets/lsquic/src/liblsquic/liblsquic.a -flto -fPIC -lz -luv
|
||||
$(CXX) -stdlib=libc++ -mmacosx-version-min=10.14 -I ./src -I ../uWebSockets/src -I ../uWebSockets/uSockets/src -I ../uWebSockets/capi -I ../uWebSockets/uSockets/boringssl/include -DUWS_WITH_PROXY -pthread -fPIC -std=c++17 -c -O3 ./src/$(LIBRARY_NAME).cpp
|
||||
$(CXX) -stdlib=libc++ -mmacosx-version-min=10.14 -shared -undefined dynamic_lookup -o ../$(LIBRARY_NAME)_darwin_amd64.so $(LIBRARY_NAME).o ../uWebSockets/uSockets/uSockets_darwin_amd64.a ../uWebSockets/uSockets/boringssl/amd64/ssl/libssl.a ../uWebSockets/uSockets/boringssl/amd64/crypto/libcrypto.a -flto -fPIC -lz -luv
|
||||
|
||||
linux:
|
||||
$(MAKE) clean
|
||||
|
@ -90,19 +87,26 @@ linux:
|
|||
# build boringssl
|
||||
cd ../uWebSockets/uSockets/boringssl && mkdir -p $(ARCH) && cd $(ARCH) && cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release .. && make crypto ssl
|
||||
|
||||
# build lsquic
|
||||
cd ../uWebSockets/uSockets/lsquic && cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DBORINGSSL_DIR=../boringssl -DCMAKE_BUILD_TYPE=Release -DLSQUIC_BIN=Off . && make lsquic
|
||||
|
||||
# build uWebSockets
|
||||
cd ../uWebSockets/uSockets && $(CC) -I src -I lsquic/include -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -DLIBUS_USE_QUIC -pthread -fPIC -std=c11 -O3 -c src/*.c src/eventing/*.c src/crypto/*.c
|
||||
cd ../uWebSockets/uSockets && $(CXX) -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -DLIBUS_USE_QUIC -pthread -fPIC -std=c++17 -O3 -c src/crypto/*.cpp
|
||||
cd ../uWebSockets/uSockets && $(CC) -I src -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -pthread -fPIC -std=c11 -O3 -c src/*.c src/eventing/*.c src/crypto/*.c
|
||||
cd ../uWebSockets/uSockets && $(CXX) -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -pthread -fPIC -std=c++17 -O3 -c src/crypto/*.cpp
|
||||
cd ../uWebSockets/uSockets && $(AR) rvs uSockets_linux_$(ARCH).a *.o
|
||||
|
||||
# build CAPI + libsocketify
|
||||
$(CXX) -I ./src -I ../uWebSockets/src -I ../uWebSockets/uSockets/src -I ../uWebSockets/capi -I ../uWebSockets/uSockets/lsquic/include -I ../uWebSockets/uSockets/boringssl/include -DUWS_WITH_PROXY -pthread -fPIC -std=c++17 -c -O3 ./src/$(LIBRARY_NAME).cpp
|
||||
$(CXX) -shared -static-libstdc++ -static-libgcc -s -o ../$(LIBRARY_NAME)_linux_$(ARCH).so $(LIBRARY_NAME).o ../uWebSockets/uSockets/uSockets_linux_$(ARCH).a ../uWebSockets/uSockets/boringssl/$(ARCH)/ssl/libssl.a ../uWebSockets/uSockets/boringssl/$(ARCH)/crypto/libcrypto.a ../uWebSockets/uSockets/lsquic/src/liblsquic/liblsquic.a -flto -fPIC -lz -luv
|
||||
$(CXX) -I ./src -I ../uWebSockets/src -I ../uWebSockets/uSockets/src -I ../uWebSockets/capi -I ../uWebSockets/uSockets/boringssl/include -DUWS_WITH_PROXY -pthread -fPIC -std=c++17 -c -O3 ./src/$(LIBRARY_NAME).cpp
|
||||
$(CXX) -shared -static-libstdc++ -static-libgcc -s -o ../$(LIBRARY_NAME)_linux_$(ARCH).so $(LIBRARY_NAME).o ../uWebSockets/uSockets/uSockets_linux_$(ARCH).a ../uWebSockets/uSockets/boringssl/$(ARCH)/ssl/libssl.a ../uWebSockets/uSockets/boringssl/$(ARCH)/crypto/libcrypto.a -flto -fPIC -lz -luv
|
||||
|
||||
linux-uws-socketify:
|
||||
# build uWebSockets
|
||||
cd ../uWebSockets/uSockets && $(CC) -I src -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -pthread -fPIC -std=c11 -O3 -c src/*.c src/eventing/*.c src/crypto/*.c
|
||||
cd ../uWebSockets/uSockets && $(CXX) -I boringssl/include -DUWS_WITH_PROXY -DLIBUS_USE_OPENSSL -DLIBUS_USE_LIBUV -pthread -fPIC -std=c++17 -O3 -c src/crypto/*.cpp
|
||||
cd ../uWebSockets/uSockets && $(AR) rvs uSockets_linux_$(ARCH).a *.o
|
||||
|
||||
# build CAPI + libsocketify
|
||||
$(CXX) -I ./src -I ../uWebSockets/src -I ../uWebSockets/uSockets/src -I ../uWebSockets/capi -I ../uWebSockets/uSockets/boringssl/include -DUWS_WITH_PROXY -pthread -fPIC -std=c++17 -c -O3 ./src/$(LIBRARY_NAME).cpp
|
||||
$(CXX) -shared -static-libstdc++ -static-libgcc -s -o ../$(LIBRARY_NAME)_linux_$(ARCH).so $(LIBRARY_NAME).o ../uWebSockets/uSockets/uSockets_linux_$(ARCH).a ../uWebSockets/uSockets/boringssl/$(ARCH)/ssl/libssl.a ../uWebSockets/uSockets/boringssl/$(ARCH)/crypto/libcrypto.a -flto -fPIC -lz -luv
|
||||
|
||||
linux-socketify-only:
|
||||
# build CAPI + libsocketify
|
||||
$(CXX) -I ./src -I ../uWebSockets/src -I ../uWebSockets/uSockets/src -I ../uWebSockets/capi -I ../uWebSockets/uSockets/lsquic/include -I ../uWebSockets/uSockets/boringssl/include -DUWS_WITH_PROXY -pthread -fPIC -std=c++17 -c -O3 ./src/$(LIBRARY_NAME).cpp
|
||||
$(CXX) -shared -static-libstdc++ -static-libgcc -s -o ../$(LIBRARY_NAME)_linux_$(ARCH).so $(LIBRARY_NAME).o ../uWebSockets/uSockets/uSockets_linux_$(ARCH).a ../uWebSockets/uSockets/boringssl/$(ARCH)/ssl/libssl.a ../uWebSockets/uSockets/boringssl/$(ARCH)/crypto/libcrypto.a ../uWebSockets/uSockets/lsquic/src/liblsquic/liblsquic.a -flto -fPIC -lz -luv
|
||||
$(CXX) -I ./src -I ../uWebSockets/src -I ../uWebSockets/uSockets/src -I ../uWebSockets/capi -I ../uWebSockets/uSockets/boringssl/include -DUWS_WITH_PROXY -pthread -fPIC -std=c++17 -c -O3 ./src/$(LIBRARY_NAME).cpp
|
||||
$(CXX) -shared -static-libstdc++ -static-libgcc -s -o ../$(LIBRARY_NAME)_linux_$(ARCH).so $(LIBRARY_NAME).o ../uWebSockets/uSockets/uSockets_linux_$(ARCH).a ../uWebSockets/uSockets/boringssl/$(ARCH)/ssl/libssl.a ../uWebSockets/uSockets/boringssl/$(ARCH)/crypto/libcrypto.a -flto -fPIC -lz -luv
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -8,136 +8,144 @@
|
|||
extern "C"
|
||||
{
|
||||
#endif
|
||||
DLL_EXPORT typedef void (*socketify_prepare_handler)(void* user_data);
|
||||
DLL_EXPORT typedef void (*socketify_timer_handler)(void* user_data);
|
||||
DLL_EXPORT typedef void (*socketify_prepare_handler)(void *user_data);
|
||||
DLL_EXPORT typedef void (*socketify_timer_handler)(void *user_data);
|
||||
|
||||
DLL_EXPORT typedef enum {
|
||||
SOCKETIFY_RUN_DEFAULT = 0,
|
||||
SOCKETIFY_RUN_ONCE,
|
||||
SOCKETIFY_RUN_NOWAIT
|
||||
} socketify_run_mode;
|
||||
DLL_EXPORT typedef enum {
|
||||
SOCKETIFY_RUN_DEFAULT = 0,
|
||||
SOCKETIFY_RUN_ONCE,
|
||||
SOCKETIFY_RUN_NOWAIT
|
||||
} socketify_run_mode;
|
||||
|
||||
DLL_EXPORT typedef struct {
|
||||
void* uv_prepare_ptr;
|
||||
DLL_EXPORT typedef struct
|
||||
{
|
||||
void *uv_prepare_ptr;
|
||||
socketify_prepare_handler on_prepare_handler;
|
||||
void* on_prepare_data;
|
||||
void* uv_loop;
|
||||
} socketify_loop;
|
||||
void *on_prepare_data;
|
||||
void *uv_loop;
|
||||
} socketify_loop;
|
||||
|
||||
DLL_EXPORT typedef struct{
|
||||
void* uv_timer_ptr;
|
||||
DLL_EXPORT typedef struct
|
||||
{
|
||||
void *uv_timer_ptr;
|
||||
socketify_timer_handler handler;
|
||||
void* user_data;
|
||||
} socketify_timer;
|
||||
void *user_data;
|
||||
} socketify_timer;
|
||||
|
||||
DLL_EXPORT typedef struct
|
||||
{
|
||||
|
||||
DLL_EXPORT typedef struct {
|
||||
const char *name;
|
||||
const char *value;
|
||||
|
||||
const char* name;
|
||||
const char* value;
|
||||
|
||||
size_t name_size;
|
||||
size_t value_size;
|
||||
|
||||
void* next;
|
||||
} socketify_header;
|
||||
size_t name_size;
|
||||
size_t value_size;
|
||||
|
||||
void *next;
|
||||
} socketify_header;
|
||||
|
||||
DLL_EXPORT typedef struct {
|
||||
DLL_EXPORT typedef struct
|
||||
{
|
||||
|
||||
const char* full_url;
|
||||
const char* url;
|
||||
const char* query_string;
|
||||
const char* method;
|
||||
const char* remote_address;
|
||||
const char *full_url;
|
||||
const char *url;
|
||||
const char *query_string;
|
||||
const char *method;
|
||||
const char *remote_address;
|
||||
|
||||
size_t full_url_size;
|
||||
size_t url_size;
|
||||
size_t query_string_size;
|
||||
size_t method_size;
|
||||
size_t remote_address_size;
|
||||
size_t full_url_size;
|
||||
size_t url_size;
|
||||
size_t query_string_size;
|
||||
size_t method_size;
|
||||
size_t remote_address_size;
|
||||
|
||||
socketify_header* header_list;
|
||||
socketify_header *header_list;
|
||||
|
||||
bool has_content;
|
||||
} socketify_asgi_data;
|
||||
bool has_content;
|
||||
} socketify_asgi_data;
|
||||
|
||||
DLL_EXPORT typedef struct {
|
||||
DLL_EXPORT typedef struct
|
||||
{
|
||||
|
||||
const char* full_url;
|
||||
const char* url;
|
||||
const char* query_string;
|
||||
const char* method;
|
||||
const char* remote_address;
|
||||
const char *full_url;
|
||||
const char *url;
|
||||
const char *query_string;
|
||||
const char *method;
|
||||
const char *remote_address;
|
||||
|
||||
size_t full_url_size;
|
||||
size_t url_size;
|
||||
size_t query_string_size;
|
||||
size_t method_size;
|
||||
size_t remote_address_size;
|
||||
size_t full_url_size;
|
||||
size_t url_size;
|
||||
size_t query_string_size;
|
||||
size_t method_size;
|
||||
size_t remote_address_size;
|
||||
|
||||
const char* protocol;
|
||||
const char* key;
|
||||
const char* extensions;
|
||||
size_t protocol_size;
|
||||
size_t key_size;
|
||||
size_t extensions_size;
|
||||
const char *protocol;
|
||||
const char *key;
|
||||
const char *extensions;
|
||||
size_t protocol_size;
|
||||
size_t key_size;
|
||||
size_t extensions_size;
|
||||
|
||||
socketify_header* header_list;
|
||||
} socketify_asgi_ws_data;
|
||||
socketify_header *header_list;
|
||||
} socketify_asgi_ws_data;
|
||||
|
||||
DLL_EXPORT typedef void (*socketify_asgi_method_handler)(int ssl, uws_res_t *response, socketify_asgi_data request, void *user_data, bool* aborted);
|
||||
DLL_EXPORT typedef void (*socketify_asgi_ws_method_handler)(int ssl, uws_res_t *response, socketify_asgi_ws_data request, uws_socket_context_t* socket, void *user_data, bool* aborted);
|
||||
DLL_EXPORT typedef struct {
|
||||
int ssl;
|
||||
uws_app_t* app;
|
||||
socketify_asgi_method_handler handler;
|
||||
void * user_data;
|
||||
} socksocketify_asgi_app_info;
|
||||
DLL_EXPORT typedef void (*socketify_asgi_method_handler)(int ssl, uws_res_t *response, socketify_asgi_data request, void *user_data);
|
||||
DLL_EXPORT typedef void (*socketify_asgi_ws_method_handler)(int ssl, uws_res_t *response, socketify_asgi_ws_data request, uws_socket_context_t *socket, void *user_data);
|
||||
DLL_EXPORT typedef struct
|
||||
{
|
||||
int ssl;
|
||||
uws_app_t *app;
|
||||
socketify_asgi_method_handler handler;
|
||||
void *user_data;
|
||||
} socksocketify_asgi_app_info;
|
||||
|
||||
DLL_EXPORT typedef struct {
|
||||
int ssl;
|
||||
uws_app_t* app;
|
||||
socketify_asgi_ws_method_handler handler;
|
||||
uws_socket_behavior_t behavior;
|
||||
void * user_data;
|
||||
} socksocketify_asgi_ws_app_info;
|
||||
DLL_EXPORT typedef struct
|
||||
{
|
||||
int ssl;
|
||||
uws_app_t *app;
|
||||
socketify_asgi_ws_method_handler handler;
|
||||
uws_socket_behavior_t behavior;
|
||||
void *user_data;
|
||||
} socketify_asgi_ws_app_info;
|
||||
|
||||
DLL_EXPORT socketify_loop *socketify_create_loop();
|
||||
DLL_EXPORT bool socketify_constructor_failed(socketify_loop *loop);
|
||||
DLL_EXPORT bool socketify_on_prepare(socketify_loop *loop, socketify_prepare_handler handler, void *user_data);
|
||||
DLL_EXPORT bool socketify_prepare_unbind(socketify_loop *loop);
|
||||
DLL_EXPORT void socketify_destroy_loop(socketify_loop *loop);
|
||||
DLL_EXPORT void *socketify_get_native_loop(socketify_loop *loop);
|
||||
|
||||
DLL_EXPORT socketify_loop * socketify_create_loop();
|
||||
DLL_EXPORT bool socketify_constructor_failed(socketify_loop* loop);
|
||||
DLL_EXPORT bool socketify_on_prepare(socketify_loop* loop, socketify_prepare_handler handler, void* user_data);
|
||||
DLL_EXPORT bool socketify_prepare_unbind(socketify_loop* loop);
|
||||
DLL_EXPORT void socketify_destroy_loop(socketify_loop* loop);
|
||||
DLL_EXPORT void* socketify_get_native_loop(socketify_loop* loop);
|
||||
DLL_EXPORT int socketify_loop_run(socketify_loop *loop, socketify_run_mode mode);
|
||||
DLL_EXPORT void socketify_loop_stop(socketify_loop *loop);
|
||||
|
||||
DLL_EXPORT int socketify_loop_run(socketify_loop* loop, socketify_run_mode mode);
|
||||
DLL_EXPORT void socketify_loop_stop(socketify_loop* loop);
|
||||
DLL_EXPORT socketify_timer *socketify_create_timer(socketify_loop *loop, uint64_t timeout, uint64_t repeat, socketify_timer_handler handler, void *user_data);
|
||||
DLL_EXPORT void socketify_timer_destroy(socketify_timer *timer);
|
||||
DLL_EXPORT void socketify_timer_set_repeat(socketify_timer *timer, uint64_t repeat);
|
||||
|
||||
DLL_EXPORT socketify_timer* socketify_create_timer(socketify_loop* loop, uint64_t timeout, uint64_t repeat, socketify_timer_handler handler, void* user_data);
|
||||
DLL_EXPORT void socketify_timer_destroy(socketify_timer* timer);
|
||||
DLL_EXPORT void socketify_timer_set_repeat(socketify_timer* timer, uint64_t repeat);
|
||||
DLL_EXPORT socketify_timer *socketify_create_check(socketify_loop *loop, socketify_timer_handler handler, void *user_data);
|
||||
DLL_EXPORT void socketify_check_destroy(socketify_timer *timer);
|
||||
|
||||
DLL_EXPORT socketify_timer* socketify_create_check(socketify_loop* loop, socketify_timer_handler handler, void* user_data);
|
||||
DLL_EXPORT void socketify_check_destroy(socketify_timer* timer);
|
||||
DLL_EXPORT socketify_asgi_data socketify_asgi_request(int ssl, uws_req_t *req, uws_res_t *res);
|
||||
DLL_EXPORT void socketify_destroy_headers(socketify_header *headers);
|
||||
DLL_EXPORT bool socketify_res_write_int_status_with_headers(int ssl, uws_res_t *res, int code, socketify_header *headers);
|
||||
DLL_EXPORT void socketify_res_write_headers(int ssl, uws_res_t *res, socketify_header *headers);
|
||||
DLL_EXPORT bool socketify_res_write_int_status(int ssl, uws_res_t *res, int code);
|
||||
DLL_EXPORT socketify_asgi_ws_data socketify_asgi_ws_request(int ssl, uws_req_t *req, uws_res_t *res);
|
||||
|
||||
DLL_EXPORT socketify_asgi_data socketify_asgi_request(int ssl, uws_req_t *req, uws_res_t *res);
|
||||
DLL_EXPORT void socketify_destroy_headers(socketify_header* headers);
|
||||
DLL_EXPORT bool socketify_res_write_int_status_with_headers(int ssl, uws_res_t* res, int code, socketify_header* headers);
|
||||
DLL_EXPORT void socketify_res_write_headers(int ssl, uws_res_t* res, socketify_header* headers);
|
||||
DLL_EXPORT bool socketify_res_write_int_status(int ssl, uws_res_t* res, int code);
|
||||
DLL_EXPORT socketify_asgi_ws_data socketify_asgi_ws_request(int ssl, uws_req_t *req, uws_res_t *res);
|
||||
DLL_EXPORT socksocketify_asgi_app_info *socketify_add_asgi_http_handler(int ssl, uws_app_t *app, socketify_asgi_method_handler handler, void *user_data);
|
||||
DLL_EXPORT void socketify_destroy_asgi_app_info(socksocketify_asgi_app_info *app);
|
||||
|
||||
DLL_EXPORT socksocketify_asgi_app_info* socketify_add_asgi_http_handler(int ssl, uws_app_t* app, socketify_asgi_method_handler handler, void* user_data);
|
||||
DLL_EXPORT void socketify_destroy_asgi_app_info(socksocketify_asgi_app_info* app);
|
||||
DLL_EXPORT void socketify_res_cork_write(int ssl, uws_res_t *response, const char *data, size_t length);
|
||||
DLL_EXPORT void socketify_res_cork_end(int ssl, uws_res_t *response, const char *data, size_t length, bool close_connection);
|
||||
|
||||
DLL_EXPORT void socketify_res_cork_write(int ssl, uws_res_t *response, const char* data, size_t length);
|
||||
DLL_EXPORT void socketify_res_cork_end(int ssl, uws_res_t *response, const char* data, size_t length, bool close_connection);
|
||||
|
||||
DLL_EXPORT socksocketify_asgi_ws_app_info* socketify_add_asgi_ws_handler(int ssl, uws_app_t* app, uws_socket_behavior_t behavior, socketify_asgi_ws_method_handler handler, void* user_data);
|
||||
DLL_EXPORT void socketify_destroy_asgi_ws_app_info(socksocketify_asgi_ws_app_info* app);
|
||||
DLL_EXPORT void socketify_ws_cork_send(int ssl, uws_websocket_t *ws, const char* data, size_t length, uws_opcode_t opcode);
|
||||
DLL_EXPORT void socketify_ws_cork_send_with_options(int ssl, uws_websocket_t *ws, const char* data, size_t length, uws_opcode_t opcode, bool compress, bool fin);
|
||||
DLL_EXPORT socketify_asgi_ws_app_info *socketify_add_asgi_ws_handler(int ssl, uws_app_t *app, uws_socket_behavior_t behavior, socketify_asgi_ws_method_handler handler, void *user_data);
|
||||
DLL_EXPORT void socketify_destroy_asgi_ws_app_info(socketify_asgi_ws_app_info *app);
|
||||
DLL_EXPORT void socketify_ws_cork_send(int ssl, uws_websocket_t *ws, const char *data, size_t length, uws_opcode_t opcode);
|
||||
DLL_EXPORT void socketify_ws_cork_send_with_options(int ssl, uws_websocket_t *ws, const char *data, size_t length, uws_opcode_t opcode, bool compress, bool fin);
|
||||
DLL_EXPORT void socketify_res_send_int_code(int ssl, uws_res_t *res, const char *content_data, size_t content_data_size, int code, const char *content_type, size_t content_type_size, bool close_connection);
|
||||
DLL_EXPORT void socketify_res_send(int ssl, uws_res_t *res, const char *content_data, size_t content_data_size, const char *status_code, size_t status_code_size, const char *content_type, size_t content_type_size, bool close_connection);
|
||||
DLL_EXPORT void socketify_res_cork_send_int_code(int ssl, uws_res_t *res, const char *content_data, size_t content_data_size, int code, const char *content_type, size_t content_type_size, bool close_connection);
|
||||
DLL_EXPORT void socketify_res_cork_send(int ssl, uws_res_t *res, const char *content_data, size_t content_data_size, const char *status_code, size_t status_code_size, const char *content_type, size_t content_type_size, bool close_connection);
|
||||
#endif
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -25,7 +25,7 @@ class SSGIHttpResponse:
|
|||
# send chunk of data, can be used to perform with less backpressure than using send
|
||||
# total_size is the sum of all lengths in bytes of all chunks to be sended
|
||||
# connection will end when total_size is met
|
||||
# returns tuple(bool, bool) first bool represents if the chunk is succefully sended, the second if the connection has ended
|
||||
# returns tuple(bool, bool) first bool represents if the chunk is successfully sended, the second if the connection has ended
|
||||
def send_chunk(self, chunk: Union[bytes, bytearray, memoryview], total_size: int) -> Awaitable:
|
||||
return self.res.send_chunk(chunk, total_size)
|
||||
|
||||
|
@ -187,8 +187,8 @@ class SSGIWebSocket:
|
|||
self._close_handler = on_close_handler
|
||||
|
||||
class SSGI:
|
||||
def __init__(self, app, options=None, request_response_factory_max_items=0, websocket_factory_max_itens=0):
|
||||
self.server = App(options, request_response_factory_max_items, websocket_factory_max_itens)
|
||||
def __init__(self, app, options=None, request_response_factory_max_items=0, websocket_factory_max_items=0):
|
||||
self.server = App(options, request_response_factory_max_items, websocket_factory_max_items)
|
||||
self.SERVER_PORT = None
|
||||
self.SERVER_HOST = ''
|
||||
self.SERVER_SCHEME = 'https' if self.server.options else 'http'
|
||||
|
|
|
@ -6,6 +6,7 @@ from asyncio import (
|
|||
futures,
|
||||
_register_task,
|
||||
_enter_task,
|
||||
current_task,
|
||||
_leave_task,
|
||||
_unregister_task,
|
||||
)
|
||||
|
@ -36,7 +37,9 @@ class RequestTask:
|
|||
|
||||
Differences:
|
||||
|
||||
- This class is only used by socketify.py loop.run_async
|
||||
- This class do not support current_task
|
||||
|
||||
- This class executes the first step like node.js Promise
|
||||
|
||||
- This class is not thread-safe.
|
||||
|
||||
|
@ -89,7 +92,10 @@ class RequestTask:
|
|||
# status is still pending
|
||||
_log_destroy_pending = True
|
||||
|
||||
def __init__(self, coro, loop, default_done_callback=None, no_register=False, context=None):
|
||||
_parent_task = None
|
||||
def __init__(
|
||||
self, coro, loop, default_done_callback=None, no_register=False, context=None
|
||||
):
|
||||
"""Initialize the future.
|
||||
|
||||
The optional event_loop argument allows explicitly setting the event
|
||||
|
@ -113,8 +119,12 @@ class RequestTask:
|
|||
self._log_destroy_pending = False
|
||||
if self._loop.get_debug():
|
||||
self._source_traceback = format_helpers.extract_stack(sys._getframe(1))
|
||||
self._loop.call_soon(self.__step, context=self._context)
|
||||
_register_task(self)
|
||||
if loop.is_running():
|
||||
self.__step()
|
||||
else:
|
||||
self._loop.call_soon(self.__step, context=self._context)
|
||||
|
||||
|
||||
def _reuse(self, coro, loop, default_done_callback=None):
|
||||
"""Reuse an future that is not pending anymore."""
|
||||
|
@ -145,8 +155,12 @@ class RequestTask:
|
|||
self._fut_waiter = None
|
||||
self._coro = coro
|
||||
|
||||
self._loop.call_soon(self.__step, context=self._context)
|
||||
_register_task(self)
|
||||
# if current_task():
|
||||
# self._loop.call_soon(self.__step, context=self._context)
|
||||
# else:
|
||||
self.__step()
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return base_tasks._task_repr(self)
|
||||
|
@ -485,7 +499,11 @@ class RequestTask:
|
|||
self._must_cancel = False
|
||||
coro = self._coro
|
||||
self._fut_waiter = None
|
||||
|
||||
|
||||
_parent_task = current_task(self._loop)
|
||||
if _parent_task is not None:
|
||||
_leave_task(self._loop, _parent_task)
|
||||
self._parent_task = _parent_task
|
||||
_enter_task(self._loop, self)
|
||||
# Call either coro.throw(exc) or coro.send(None).
|
||||
try:
|
||||
|
@ -557,6 +575,9 @@ class RequestTask:
|
|||
self._loop.call_soon(self.__step, new_exc, context=self._context)
|
||||
finally:
|
||||
_leave_task(self._loop, self)
|
||||
if self._parent_task is not None:
|
||||
_enter_task(self._loop, self._parent_task)
|
||||
self._parent_task = None
|
||||
self = None # Needed to break cycles when an exception occurs.
|
||||
|
||||
def __wakeup(self, future):
|
||||
|
@ -578,27 +599,29 @@ class RequestTask:
|
|||
__iter__ = __await__ # make compatible with 'yield from'.
|
||||
|
||||
|
||||
def create_task_with_factory(task_factory_max_items=100_000):
|
||||
items = []
|
||||
for _ in range(0, task_factory_max_items):
|
||||
task = RequestTask(None, None, None, True)
|
||||
if task._source_traceback:
|
||||
del task._source_traceback[-1]
|
||||
items.append(task)
|
||||
async def factory_task_wrapper(task, dispose):
|
||||
try:
|
||||
await task
|
||||
finally:
|
||||
dispose()
|
||||
|
||||
def factory(loop, coro, default_done_callback=None):
|
||||
if len(items) == 0:
|
||||
return create_task(loop, coro, default_done_callback)
|
||||
task = items.pop()
|
||||
def done(f):
|
||||
if default_done_callback is not None:
|
||||
default_done_callback(f)
|
||||
items.append(f)
|
||||
task._reuse(coro, loop, done)
|
||||
class TaskFactory:
|
||||
def __init__(self, task_factory_max_items=100_000):
|
||||
self.items = []
|
||||
for _ in range(0, task_factory_max_items):
|
||||
task = RequestTask(None, None, None, True)
|
||||
if task._source_traceback:
|
||||
del task._source_traceback[-1]
|
||||
self.items.append(task)
|
||||
|
||||
def __call__(self, loop, coro):
|
||||
if len(self.items) == 0:
|
||||
return create_task(loop, coro)
|
||||
task = self.items.pop()
|
||||
|
||||
task._reuse(factory_task_wrapper(coro, lambda : self.items.append(task)), loop)
|
||||
return task
|
||||
|
||||
return factory
|
||||
|
||||
|
||||
def create_task(loop, coro, default_done_callback=None, context=None):
|
||||
"""Schedule a coroutine object.
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 5773238f673348d560e769b10a424c6dbe598730
|
||||
Subproject commit f33291d10a2051ba0f10c18761c030fb00390fdf
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
from .native import ffi, lib
|
||||
|
||||
|
||||
|
@ -7,6 +8,7 @@ def socketify_generic_handler(data):
|
|||
(handler, user_data) = ffi.from_handle(data)
|
||||
handler(user_data)
|
||||
|
||||
|
||||
class UVCheck:
|
||||
def __init__(self, loop, handler, user_data):
|
||||
self._handler_data = ffi.new_handle((handler, user_data))
|
||||
|
@ -85,11 +87,11 @@ class UVLoop:
|
|||
def run_nowait(self):
|
||||
if self._loop != ffi.NULL:
|
||||
return lib.socketify_loop_run(self._loop, lib.SOCKETIFY_RUN_NOWAIT)
|
||||
|
||||
|
||||
def run(self):
|
||||
if self._loop != ffi.NULL:
|
||||
return lib.socketify_loop_run(self._loop, lib.SOCKETIFY_RUN_DEFAULT)
|
||||
|
||||
|
||||
def run_once(self):
|
||||
if self._loop != ffi.NULL:
|
||||
return lib.socketify_loop_run(self._loop, lib.SOCKETIFY_RUN_ONCE)
|
||||
|
|
|
@ -5,13 +5,18 @@ from .asgi import ws_close, ws_upgrade, ws_open, ws_message
|
|||
from io import BytesIO, BufferedReader
|
||||
from .native import lib, ffi
|
||||
import platform
|
||||
|
||||
is_pypy = platform.python_implementation() == "PyPy"
|
||||
from .tasks import create_task, create_task_with_factory
|
||||
from .tasks import create_task, TaskFactory
|
||||
import sys
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
@ffi.callback("void(uws_res_t*, const char*, size_t, bool, void*)")
|
||||
def wsgi_on_data_handler(res, chunk, chunk_length, is_end, user_data):
|
||||
data_response = ffi.from_handle(user_data)
|
||||
data_response.app.server.loop.is_idle = False
|
||||
|
||||
if chunk != ffi.NULL:
|
||||
data_response.buffer.write(ffi.unpack(chunk, chunk_length))
|
||||
if bool(is_end):
|
||||
|
@ -22,6 +27,88 @@ def wsgi_on_data_handler(res, chunk, chunk_length, is_end, user_data):
|
|||
data_response._ptr,
|
||||
)
|
||||
|
||||
@ffi.callback("void(uws_res_t*, void*)")
|
||||
def wsgi_on_data_ref_abort_handler(res, user_data):
|
||||
data_retry = ffi.from_handle(user_data)
|
||||
data_retry.aborted = True
|
||||
data_retry.server.loop.is_idle = False
|
||||
if data_retry.id is not None:
|
||||
data_retry.app._data_refs.pop(data_retry.id, None)
|
||||
|
||||
|
||||
@ffi.callback("bool(uws_res_t*, uintmax_t, void*)")
|
||||
def wsgi_on_writable_handler(res, offset, user_data):
|
||||
data_retry = ffi.from_handle(user_data)
|
||||
if data_retry.aborted:
|
||||
return False
|
||||
|
||||
chunks = data_retry.chunks
|
||||
last_sended_offset = data_retry.last_offset
|
||||
server = data_retry.app.server
|
||||
ssl = server.SSL
|
||||
server.loop.is_idle = False
|
||||
content_length = data_retry.content_length
|
||||
|
||||
data = chunks[0]
|
||||
data_size = len(data)
|
||||
last_offset = int(lib.uws_res_get_write_offset(ssl, res))
|
||||
if last_sended_offset != last_offset:
|
||||
offset = last_offset - last_sended_offset
|
||||
data = data[offset:data_size]
|
||||
data_size = len(data)
|
||||
if data_size == 0:
|
||||
chunks.pop(0)
|
||||
|
||||
if len(chunks) == 0:
|
||||
logging.error(AssertionError("Content-Length do not match sended content"))
|
||||
lib.uws_res_close(
|
||||
ssl,
|
||||
res
|
||||
)
|
||||
if data_retry.id is not None:
|
||||
data_retry.app._data_refs.pop(data_retry.id, None)
|
||||
|
||||
return True
|
||||
data = chunks[0]
|
||||
|
||||
result = lib.uws_res_try_end(
|
||||
ssl,
|
||||
res,
|
||||
data,
|
||||
data_size,
|
||||
content_length,
|
||||
0,
|
||||
)
|
||||
has_responded = bool(result.has_responded)
|
||||
ok = bool(result.ok)
|
||||
data_retry.last_offset = int(lib.uws_res_get_write_offset(ssl, res))
|
||||
|
||||
if ok:
|
||||
chunks.pop(0)
|
||||
if not has_responded and len(chunks) == 0:
|
||||
logging.error(AssertionError("Content-Length do not match sended content"))
|
||||
lib.uws_res_close(
|
||||
ssl,
|
||||
res
|
||||
)
|
||||
if data_retry.id is not None:
|
||||
data_retry.app._data_refs.pop(data_retry.id, None)
|
||||
elif has_responded and data_retry.id is not None:
|
||||
data_retry.app._data_refs.pop(data_retry.id, None)
|
||||
elif not has_responded and len(chunks) == 0:
|
||||
logging.error(AssertionError("Content-Length do not match sended content"))
|
||||
lib.uws_res_close(
|
||||
ssl,
|
||||
res
|
||||
)
|
||||
if data_retry.id is not None:
|
||||
data_retry.app._data_refs.pop(data_retry.id, None)
|
||||
elif has_responded and data_retry.id is not None:
|
||||
data_retry.app._data_refs.pop(data_retry.id, None)
|
||||
|
||||
|
||||
return ok
|
||||
|
||||
class WSGIBody:
|
||||
def __init__(self, buffer):
|
||||
self.buf = buffer
|
||||
|
@ -105,19 +192,32 @@ class WSGIBody:
|
|||
ret.append(data)
|
||||
data = b""
|
||||
else:
|
||||
line, data = data[:pos + 1], data[pos + 1:]
|
||||
line, data = data[: pos + 1], data[pos + 1 :]
|
||||
ret.append(line)
|
||||
return ret
|
||||
|
||||
|
||||
class WSGIDataResponse:
|
||||
def __init__(self, app, environ, start_response, aborted, buffer, on_data):
|
||||
def __init__(self, app, environ, start_response, buffer, on_data):
|
||||
self.buffer = buffer
|
||||
self.aborted = aborted
|
||||
self._ptr = ffi.new_handle(self)
|
||||
self.on_data = on_data
|
||||
self.environ = environ
|
||||
self.app = app
|
||||
self.start_response = start_response
|
||||
self.id = None
|
||||
self.aborted = False
|
||||
|
||||
class WSGIRetryDataSend:
|
||||
def __init__(self, app, chunks, content_length, last_offset):
|
||||
self.chunks = chunks
|
||||
self._ptr = ffi.new_handle(self)
|
||||
self.app = app
|
||||
self.content_length = content_length
|
||||
self.last_offset = last_offset
|
||||
self.id = None
|
||||
self.aborted = False
|
||||
|
||||
|
||||
@ffi.callback("void(uws_res_t*, void*)")
|
||||
def wsgi_corked_response_start_handler(res, user_data):
|
||||
|
@ -125,9 +225,12 @@ def wsgi_corked_response_start_handler(res, user_data):
|
|||
data_response.on_data(data_response, res)
|
||||
|
||||
|
||||
@ffi.callback("void(int, uws_res_t*, socketify_asgi_data request, void*, bool*)")
|
||||
def wsgi(ssl, response, info, user_data, aborted):
|
||||
|
||||
@ffi.callback("void(int, uws_res_t*, socketify_asgi_data request, void*)")
|
||||
def wsgi(ssl, response, info, user_data):
|
||||
app = ffi.from_handle(user_data)
|
||||
app.server.loop.is_idle = False
|
||||
|
||||
# reusing the dict is slower than cloning because we need to clear HTTP headers
|
||||
environ = dict(app.BASIC_ENVIRON)
|
||||
|
||||
|
@ -135,7 +238,7 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
environ["PATH_INFO"] = ffi.unpack(info.url, info.url_size).decode("utf8")
|
||||
environ["QUERY_STRING"] = ffi.unpack(
|
||||
info.query_string, info.query_string_size
|
||||
).decode("utf8")
|
||||
).decode("utf8")[1:]
|
||||
if info.remote_address != ffi.NULL:
|
||||
environ["REMOTE_ADDR"] = ffi.unpack(
|
||||
info.remote_address, info.remote_address_size
|
||||
|
@ -152,38 +255,62 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
environ[f"HTTP_{name.replace('-', '_').upper()}"] = value
|
||||
next_header = ffi.cast("socketify_header*", next_header.next)
|
||||
|
||||
environ["CONTENT_TYPE"] = environ.get("HTTP_CONTENT_TYPE", None)
|
||||
environ["CONTENT_TYPE"] = environ.get("HTTP_CONTENT_TYPE", "")
|
||||
|
||||
def start_response(status, headers):
|
||||
if isinstance(status, str):
|
||||
data = status.encode("utf-8")
|
||||
headers_set = None
|
||||
headers_written = False
|
||||
status_text = None
|
||||
is_chunked = False
|
||||
content_length = -1
|
||||
def write_headers(headers):
|
||||
nonlocal headers_written, headers_set, status_text, content_length, is_chunked, app
|
||||
if headers_written or not headers_set:
|
||||
return
|
||||
app.server.loop.is_idle = False
|
||||
|
||||
headers_written = True
|
||||
|
||||
if isinstance(status_text, str):
|
||||
data = status_text.encode("utf-8")
|
||||
lib.uws_res_write_status(ssl, response, data, len(data))
|
||||
elif isinstance(status, bytes):
|
||||
lib.uws_res_write_status(ssl, response, status, len(status))
|
||||
|
||||
elif isinstance(status_text, bytes):
|
||||
lib.uws_res_write_status(ssl, response, status_text, len(status_text))
|
||||
|
||||
for (key, value) in headers:
|
||||
if isinstance(key, str):
|
||||
# this is faster than using .lower()
|
||||
# this is faster than using .lower()
|
||||
if (
|
||||
key == "content-length"
|
||||
or key == "Content-Length"
|
||||
or key == "Transfer-Encoding"
|
||||
):
|
||||
content_length = int(value)
|
||||
continue # auto generated by try_end
|
||||
if (
|
||||
key == "Transfer-Encoding"
|
||||
or key == "transfer-encoding"
|
||||
):
|
||||
continue # auto
|
||||
is_chunked = str(value) == "chunked"
|
||||
if is_chunked:
|
||||
continue
|
||||
|
||||
key_data = key.encode("utf-8")
|
||||
elif isinstance(key, bytes):
|
||||
# this is faster than using .lower()
|
||||
if (
|
||||
key == b"content-length"
|
||||
or key == b"Content-Length"
|
||||
or key == b"Transfer-Encoding"
|
||||
):
|
||||
content_length = int(value)
|
||||
continue # auto
|
||||
if (
|
||||
key == b"Transfer-Encoding"
|
||||
or key == b"transfer-encoding"
|
||||
):
|
||||
continue # auto
|
||||
is_chunked = str(value) == "chunked"
|
||||
if is_chunked:
|
||||
continue
|
||||
key_data = key
|
||||
|
||||
|
||||
if isinstance(value, str):
|
||||
value_data = value.encode("utf-8")
|
||||
elif isinstance(value, bytes):
|
||||
|
@ -197,117 +324,284 @@ def wsgi(ssl, response, info, user_data, aborted):
|
|||
ffi.cast("uint64_t", value),
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
lib.uws_res_write_header(
|
||||
ssl, response, key_data, len(key_data), value_data, len(value_data)
|
||||
)
|
||||
lib.uws_res_write_header(
|
||||
ssl, response, b'Server', 6, b'socketify.py', 12
|
||||
)
|
||||
# no content-length
|
||||
if content_length < 0:
|
||||
is_chunked = True
|
||||
content_length = ffi.cast("uintmax_t", content_length)
|
||||
|
||||
def start_response(status, headers, exc_info=None):
|
||||
nonlocal headers_set, status_text, app
|
||||
app.server.loop.is_idle = False
|
||||
if exc_info:
|
||||
try:
|
||||
if headers_written:
|
||||
# Re-raise original exception if headers sent
|
||||
raise exc_info[1].with_traceback(exc_info[2])
|
||||
finally:
|
||||
exc_info = None # avoid dangling circular ref
|
||||
elif headers_set:
|
||||
raise AssertionError("Headers already set!")
|
||||
|
||||
headers_set = headers
|
||||
status_text = status
|
||||
|
||||
def write(data):
|
||||
nonlocal is_chunked, app
|
||||
app.server.loop.is_idle = False
|
||||
if not headers_written:
|
||||
write_headers(headers_set)
|
||||
# will allow older frameworks only with is_chunked
|
||||
is_chunked = True
|
||||
|
||||
if isinstance(data, bytes):
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
elif isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
|
||||
return write
|
||||
|
||||
|
||||
|
||||
|
||||
# check for body
|
||||
if bool(info.has_content):
|
||||
WSGI_INPUT = BytesIO()
|
||||
environ["wsgi.input"] = WSGIBody(WSGI_INPUT)
|
||||
|
||||
def on_data(data_response, response):
|
||||
if bool(data_response.aborted[0]):
|
||||
return
|
||||
last_offset = -1
|
||||
data_retry = None
|
||||
failed_chunks = None
|
||||
|
||||
|
||||
if data_response.aborted:
|
||||
return
|
||||
data_response.app.server.loop.is_idle = False
|
||||
ssl = data_response.app.server.SSL
|
||||
data_response.environ["CONTENT_LENGTH"] = str(data_response.buffer.getbuffer().nbytes)
|
||||
data_response.environ["CONTENT_LENGTH"] = str(
|
||||
data_response.buffer.getbuffer().nbytes
|
||||
)
|
||||
if data_response.id is not None:
|
||||
data_response.app._data_refs.pop(data_response.id, None)
|
||||
|
||||
app_iter = data_response.app.wsgi(
|
||||
data_response.environ, data_response.start_response
|
||||
)
|
||||
|
||||
try:
|
||||
for data in app_iter:
|
||||
if isinstance(data, bytes):
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
elif isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
|
||||
|
||||
if data:
|
||||
if not headers_written:
|
||||
write_headers(headers_set)
|
||||
|
||||
if is_chunked:
|
||||
if isinstance(data, bytes):
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
elif isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
else:
|
||||
if isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
if failed_chunks:
|
||||
failed_chunks.append(data)
|
||||
else:
|
||||
last_offset = int(lib.uws_res_get_write_offset(ssl, response))
|
||||
result = lib.uws_res_try_end(
|
||||
ssl,
|
||||
response,
|
||||
data,
|
||||
len(data),
|
||||
content_length,
|
||||
0,
|
||||
)
|
||||
# this should be very very rare for HTTP
|
||||
if not bool(result.ok):
|
||||
failed_chunks = []
|
||||
# just mark the chunks
|
||||
failed_chunks.append(data)
|
||||
# add on writable handler
|
||||
data_retry = WSGIRetryDataSend(
|
||||
app, failed_chunks, content_length, last_offset
|
||||
)
|
||||
|
||||
except Exception as error:
|
||||
logging.exception(error)
|
||||
finally:
|
||||
if hasattr(app_iter, "close"):
|
||||
app_iter.close()
|
||||
lib.uws_res_end_without_body(ssl, response, 0)
|
||||
|
||||
if not headers_written:
|
||||
write_headers(headers_set)
|
||||
if is_chunked:
|
||||
lib.uws_res_end_without_body(ssl, response, 0)
|
||||
elif data_retry is not None:
|
||||
_id = uuid.uuid4()
|
||||
app._data_refs[_id] = data_retry
|
||||
lib.uws_res_on_aborted(ssl, response, wsgi_on_data_ref_abort_handler, data_retry._ptr)
|
||||
lib.uws_res_on_writable(ssl, response, wsgi_on_writable_handler, data_retry._ptr)
|
||||
elif result is None or (not bool(result.has_responded) and bool(result.ok)): # not reaches Content-Length
|
||||
logging.error(AssertionError("Content-Length do not match sended content"))
|
||||
lib.uws_res_close(
|
||||
ssl,
|
||||
response
|
||||
)
|
||||
data_response = WSGIDataResponse(
|
||||
app, environ, start_response, aborted, WSGI_INPUT, on_data
|
||||
app, environ, start_response, WSGI_INPUT, on_data
|
||||
)
|
||||
|
||||
_id = uuid.uuid4()
|
||||
data_response.id = _id
|
||||
app._data_refs[_id] = data_response
|
||||
lib.uws_res_on_aborted(ssl, response, wsgi_on_data_ref_abort_handler, data_response._ptr)
|
||||
lib.uws_res_on_data(ssl, response, wsgi_on_data_handler, data_response._ptr)
|
||||
else:
|
||||
environ["wsgi.input"] = None
|
||||
failed_chunks = None
|
||||
last_offset = -1
|
||||
data_retry = None
|
||||
# Django do not check for None with is lame
|
||||
# we use the same empty for everyone to avoid extra allocations
|
||||
environ["wsgi.input"] = app.EMPTY_WSGI_BODY
|
||||
# we also set CONTENT_LENGTH to 0 so if Django is lame again its covered
|
||||
environ["CONTENT_LENGTH"] = "0"
|
||||
app_iter = app.wsgi(environ, start_response)
|
||||
result = None
|
||||
try:
|
||||
for data in app_iter:
|
||||
if isinstance(data, bytes):
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
elif isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
if data:
|
||||
if not headers_written:
|
||||
write_headers(headers_set)
|
||||
if is_chunked:
|
||||
if isinstance(data, bytes):
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
elif isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
lib.uws_res_write(ssl, response, data, len(data))
|
||||
else:
|
||||
if isinstance(data, str):
|
||||
data = data.encode("utf-8")
|
||||
if failed_chunks: # if failed once, will fail again later
|
||||
failed_chunks.append(data)
|
||||
else:
|
||||
last_offset = int(lib.uws_res_get_write_offset(ssl, response))
|
||||
result = lib.uws_res_try_end(
|
||||
ssl,
|
||||
response,
|
||||
data,
|
||||
len(data),
|
||||
content_length,
|
||||
0,
|
||||
)
|
||||
# this should be very very rare for HTTP
|
||||
if not bool(result.ok):
|
||||
failed_chunks = []
|
||||
# just mark the chunks
|
||||
failed_chunks.append(data)
|
||||
# add on writable handler
|
||||
data_retry = WSGIRetryDataSend(
|
||||
app, failed_chunks, content_length, last_offset
|
||||
)
|
||||
|
||||
|
||||
except Exception as error:
|
||||
logging.exception(error)
|
||||
finally:
|
||||
if hasattr(app_iter, "close"):
|
||||
app_iter.close()
|
||||
lib.uws_res_end_without_body(ssl, response, 0)
|
||||
|
||||
if not headers_written:
|
||||
write_headers(headers_set)
|
||||
if is_chunked:
|
||||
lib.uws_res_end_without_body(ssl, response, 0)
|
||||
elif data_retry is not None:
|
||||
_id = uuid.uuid4()
|
||||
data_retry.id = _id
|
||||
app._data_refs[_id] = data_retry
|
||||
lib.uws_res_on_aborted(ssl, response, wsgi_on_data_ref_abort_handler, data_retry._ptr)
|
||||
lib.uws_res_on_writable(ssl, response, wsgi_on_writable_handler, data_retry._ptr)
|
||||
elif result is None or (not bool(result.has_responded) and bool(result.ok)): # not reaches Content-Length
|
||||
logging.error(AssertionError("Content-Length do not match sended content"))
|
||||
lib.uws_res_close(
|
||||
ssl,
|
||||
response
|
||||
)
|
||||
|
||||
|
||||
def is_asgi(module):
|
||||
return hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 3
|
||||
return (
|
||||
hasattr(module, "__call__") and len(inspect.signature(module).parameters) == 3
|
||||
)
|
||||
|
||||
|
||||
class _WSGI:
|
||||
def __init__(self, app, options=None, websocket=None, websocket_options=None, task_factory_max_items=100_000):
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
options=None,
|
||||
websocket=None,
|
||||
websocket_options=None,
|
||||
task_factory_max_items=100_000,
|
||||
):
|
||||
self.server = App(options, task_factory_max_items=0)
|
||||
self.SERVER_HOST = None
|
||||
self.SERVER_PORT = None
|
||||
self.SERVER_WS_SCHEME = "wss" if self.server.options else "ws"
|
||||
self.SERVER_WS_SCHEME = "wss" if self.server._options else "ws"
|
||||
self.wsgi = app
|
||||
self.EMPTY_WSGI_BODY = WSGIBody(BytesIO())
|
||||
self.BASIC_ENVIRON = dict(os.environ)
|
||||
self.ws_compression = False
|
||||
|
||||
self._data_refs = {}
|
||||
self._ptr = ffi.new_handle(self)
|
||||
self.asgi_http_info = lib.socketify_add_asgi_http_handler(
|
||||
self.server.SSL, self.server.app, wsgi, self._ptr
|
||||
)
|
||||
self.asgi_ws_info = None
|
||||
|
||||
|
||||
if isinstance(websocket, dict): # serve websocket as socketify.py
|
||||
if websocket_options:
|
||||
websocket.update(websocket_options)
|
||||
|
||||
self.server.ws("/*", websocket)
|
||||
elif is_asgi(websocket):
|
||||
self.app = websocket # set ASGI app
|
||||
self.app = websocket # set ASGI app
|
||||
loop = self.server.loop.loop
|
||||
# ASGI do not use app.run_async to not add any overhead from socketify.py WebFramework
|
||||
# internally will still use custom task factory for pypy because of Loop
|
||||
if is_pypy:
|
||||
if task_factory_max_items > 0:
|
||||
factory = create_task_with_factory(task_factory_max_items)
|
||||
|
||||
factory = TaskFactory(task_factory_max_items)
|
||||
|
||||
def run_task(task):
|
||||
factory(loop, task)
|
||||
loop._run_once()
|
||||
|
||||
self._run_task = run_task
|
||||
else:
|
||||
|
||||
def run_task(task):
|
||||
create_task(loop, task)
|
||||
loop._run_once()
|
||||
future = create_task(loop, task)
|
||||
future._log_destroy_pending = False
|
||||
|
||||
self._run_task = run_task
|
||||
|
||||
|
||||
else:
|
||||
if sys.version_info >= (3, 8): # name fixed to avoid dynamic name
|
||||
if sys.version_info >= (3, 8): # name fixed to avoid dynamic name
|
||||
|
||||
def run_task(task):
|
||||
loop.create_task(task, name='socketify.py-request-task')
|
||||
loop._run_once()
|
||||
future = create_task(loop, task)
|
||||
future._log_destroy_pending = False
|
||||
|
||||
self._run_task = run_task
|
||||
else:
|
||||
|
||||
def run_task(task):
|
||||
loop.create_task(task)
|
||||
loop._run_once()
|
||||
future = create_task(loop, task)
|
||||
future._log_destroy_pending = False
|
||||
|
||||
self._run_task = run_task
|
||||
|
||||
|
||||
# detect ASGI to use as WebSocket as mixed protocol
|
||||
native_options = ffi.new("uws_socket_behavior_t *")
|
||||
native_behavior = native_options[0]
|
||||
|
@ -353,7 +647,7 @@ class _WSGI:
|
|||
native_behavior.ping = ffi.NULL
|
||||
native_behavior.pong = ffi.NULL
|
||||
native_behavior.close = ws_close
|
||||
|
||||
native_behavior.subscription = ffi.NULL
|
||||
|
||||
self.asgi_ws_info = lib.socketify_add_asgi_ws_handler(
|
||||
self.server.SSL, self.server.app, native_behavior, ws_upgrade, self._ptr
|
||||
|
@ -377,7 +671,7 @@ class _WSGI:
|
|||
"wsgi.errors": sys.stderr,
|
||||
"wsgi.version": (1, 0),
|
||||
"wsgi.run_once": False,
|
||||
"wsgi.url_scheme": "https" if self.server.options else "http",
|
||||
"wsgi.url_scheme": "https" if self.server._options and self.server._options.cert_file_name is not None else "http",
|
||||
"wsgi.multithread": False,
|
||||
"wsgi.multiprocess": False,
|
||||
"wsgi.file_wrapper": None, # No file wrapper support for now
|
||||
|
@ -386,7 +680,7 @@ class _WSGI:
|
|||
"REMOTE_HOST": "",
|
||||
"CONTENT_LENGTH": "0",
|
||||
"CONTENT_TYPE": "",
|
||||
'wsgi.input_terminated': True
|
||||
"wsgi.input_terminated": True,
|
||||
}
|
||||
)
|
||||
self.server.listen(port_or_options, handler)
|
||||
|
@ -397,46 +691,86 @@ class _WSGI:
|
|||
return self
|
||||
|
||||
def __del__(self):
|
||||
if self.asgi_http_info:
|
||||
lib.socketify_destroy_asgi_app_info(self.asgi_http_info)
|
||||
if self.asgi_ws_info:
|
||||
lib.socketify_destroy_asgi_ws_app_info(self.asgi_ws_info)
|
||||
try:
|
||||
if self.asgi_http_info:
|
||||
lib.socketify_destroy_asgi_app_info(self.asgi_http_info)
|
||||
if self.asgi_ws_info:
|
||||
lib.socketify_destroy_asgi_ws_app_info(self.asgi_ws_info)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# "Public" WSGI interface to allow easy forks/workers
|
||||
class WSGI:
|
||||
def __init__(self, app, options=None, websocket=None, websocket_options=None, task_factory_max_items=100_000, lifespan=False):
|
||||
def __init__(
|
||||
self,
|
||||
app,
|
||||
options=None,
|
||||
websocket=None,
|
||||
websocket_options=None,
|
||||
task_factory_max_items=100_000,
|
||||
lifespan=False,
|
||||
):
|
||||
self.app = app
|
||||
self.options = options
|
||||
self.websocket = websocket
|
||||
self.websocket_options = websocket_options
|
||||
self.listen_options = None
|
||||
self.task_factory_max_items = task_factory_max_items
|
||||
self.server = None
|
||||
self.pid_list = None
|
||||
# lifespan is not supported in WSGI
|
||||
|
||||
def listen(self, port_or_options, handler=None):
|
||||
self.listen_options = (port_or_options, handler)
|
||||
return self
|
||||
|
||||
def run(self, workers=1):
|
||||
def run_app():
|
||||
def close(self):
|
||||
# always wait a sec so forks can start properly if close is called too fast
|
||||
import time
|
||||
time.sleep(1)
|
||||
|
||||
if self.server is not None:
|
||||
self.server.close()
|
||||
if self.pid_list is not None:
|
||||
import signal
|
||||
for pid in self.pid_list:
|
||||
os.kill(pid, signal.SIGINT)
|
||||
|
||||
def run(self, workers=1, block=True):
|
||||
def run_task():
|
||||
server = _WSGI(
|
||||
self.app, self.options, self.websocket, self.websocket_options, self.task_factory_max_items
|
||||
self.app,
|
||||
self.options,
|
||||
self.websocket,
|
||||
self.websocket_options,
|
||||
self.task_factory_max_items,
|
||||
)
|
||||
if self.listen_options:
|
||||
(port_or_options, handler) = self.listen_options
|
||||
server.listen(port_or_options, handler)
|
||||
self.server = server
|
||||
server.run()
|
||||
|
||||
def create_fork():
|
||||
n = os.fork()
|
||||
# n greater than 0 means parent process
|
||||
if not n > 0:
|
||||
run_app()
|
||||
|
||||
pid_list = []
|
||||
|
||||
start = 1 if block else 0
|
||||
# fork limiting the cpu count - 1
|
||||
for i in range(1, workers):
|
||||
create_fork()
|
||||
for _ in range(start, workers):
|
||||
pid = os.fork()
|
||||
# n greater than 0 means parent process
|
||||
if not pid > 0:
|
||||
run_task()
|
||||
break
|
||||
pid_list.append(pid)
|
||||
|
||||
self.pid_list = pid_list
|
||||
|
||||
if block:
|
||||
run_task() # run app on the main process too :)
|
||||
# sigint everything to graceful shutdown
|
||||
import signal
|
||||
for pid in pid_list:
|
||||
os.kill(pid, signal.SIGINT)
|
||||
|
||||
run_app() # run app on the main process too :)
|
||||
return self
|
||||
|
|
39
src/tests.py
39
src/tests.py
|
@ -1,26 +1,35 @@
|
|||
from socketify import App
|
||||
import asyncio
|
||||
app = App(lifespan=False)
|
||||
router = app.router()
|
||||
|
||||
app = App()
|
||||
@app.on_start
|
||||
async def on_start():
|
||||
print("wait...")
|
||||
await asyncio.sleep(1)
|
||||
print("start!")
|
||||
|
||||
def extension(request, response, ws):
|
||||
@app.on_shutdown
|
||||
async def on_shutdown():
|
||||
print("wait...")
|
||||
await asyncio.sleep(1)
|
||||
print("shutdown!")
|
||||
|
||||
@request.method
|
||||
async def get_user(self):
|
||||
token = self.get_header("token")
|
||||
self.token = token
|
||||
return { "name": "Test" } if token else { "name", "Anonymous" }
|
||||
@router.get("/")
|
||||
def home(res, req, data=None):
|
||||
# print(data)
|
||||
# print("token", req.token)
|
||||
# cart = await req.get_cart()
|
||||
# print("cart", cart)
|
||||
# user = await req.get_user()
|
||||
# print("user", user)
|
||||
# print("token", req.token)
|
||||
res.send({"Hello": "World!"}, headers=(("X-Rate-Limit-Remaining", "10"), (b'Another-Headers', b'Value')))
|
||||
|
||||
@request.method
|
||||
async def get_cart(self):
|
||||
return [{ "quantity": 10, "name": "T-Shirt" }]
|
||||
|
||||
request.property("token", None)
|
||||
|
||||
app.register(extension)
|
||||
|
||||
app.get("/", lambda res, req: res.end("Hello World!"))
|
||||
app.listen(
|
||||
3000,
|
||||
lambda config: print("Listening on port http://localhost:%d now\n" % config.port),
|
||||
)
|
||||
app.run()
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue