more tweaks, custom tasks for PyPy and cli doc

pull/75/head
Ciro 2022-12-18 10:23:32 -03:00
rodzic 5276608608
commit 4745dd29bc
14 zmienionych plików z 696 dodań i 452 usunięć

Wyświetl plik

@ -28,7 +28,7 @@ class SSGIHttpResponse:
pass pass
# ensure async call for the handler, passing any arguments to it # ensure async call for the handler, passing any arguments to it
def run_async(self, handler: Awaitable, *arguments) -> Awaitable: def run_async(self, handler: Awaitable, *arguments):
pass pass
# get an all data # get an all data
@ -67,7 +67,7 @@ class SSGIWebSocket:
pass pass
# ensure async call for the handler, passing any arguments to it # ensure async call for the handler, passing any arguments to it
def run_async(self, handler: Awaitable, *arguments) -> Awaitable: def run_async(self, handler: Awaitable, *arguments):
pass pass
# on receive event, called when the socket disconnect # on receive event, called when the socket disconnect

Wyświetl plik

@ -23,4 +23,4 @@ home = Home()
app.add_route("/", home) app.add_route("/", home)
if __name__ == "__main__": if __name__ == "__main__":
WSGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(workers=2) WSGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(workers=8)

Wyświetl plik

@ -20,4 +20,4 @@ async def app(scope, receive, send):
if __name__ == "__main__": if __name__ == "__main__":
ASGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run() ASGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(8)

Wyświetl plik

@ -5,4 +5,4 @@ def app(environ, start_response):
yield b'Hello, World!\n' yield b'Hello, World!\n'
if __name__ == "__main__": if __name__ == "__main__":
WSGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run() WSGI(app).listen(8000, lambda config: print(f"Listening on port http://localhost:{config.port} now\n")).run(8)

Wyświetl plik

@ -1,9 +1,10 @@
from socketify import App from socketify import App
import os import os
import multiprocessing import multiprocessing
import asyncio
def run_app(): def run_app():
app = App(request_response_factory_max_itens=200_000) app = App(request_response_factory_max_items=200_000)
def home(res, req): async def home(res, req):
res.end("Hello, World!") res.end("Hello, World!")
app.get("/", home) app.get("/", home)

Wyświetl plik

@ -99,6 +99,25 @@ class AppRequest:
def is_ancient(self): def is_ancient(self):
def __del__(self): def __del__(self):
``` ```
## AppListenOptions
```python
class AppListenOptions:
port: int = 0
host: str = None
options: int = 0
domain: str = None
```
## AppOptions
```python
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
```
## WebSockets ## WebSockets
```python ```python
@ -179,6 +198,8 @@ class SendStatus(IntEnum):
SUCCESS = 1 SUCCESS = 1
DROPPED = 2 DROPPED = 2
``` ```
## Helpers ## Helpers
```python ```python
async def sendfile(res, req, filename) async def sendfile(res, req, filename)

Wyświetl plik

@ -1,5 +1,5 @@
## Corking ## Corking
It is very important to understand the corking mechanism, as that is responsible for efficiently formatting, packing and sending data. Without corking your app will still work reliably, but can perform very bad and use excessive networking. In some cases the performance can be dreadful without proper corking. It is very important to understand the corking mechanism, as that is responsible for efficiently formatting, packing and sending data. Corking is packing many sends into one single syscall/SSL block, without corking your app will still work reliably, but can perform very bad and use excessive networking. In some cases the performance can be dreadful without proper corking.
That's why your sockets will be corked by default in most simple cases, including all of the examples provided. However there are cases where default corking cannot happen automatically. That's why your sockets will be corked by default in most simple cases, including all of the examples provided. However there are cases where default corking cannot happen automatically.

Wyświetl plik

@ -1,3 +1,34 @@
Support is already there, docs Coming soon...
### Next [API Reference](api.md) # SSL / HTTPS
Is really easy to pass and key_file_name, cert_file_name and passphrase to get SSL working, you can also pass ssl_ciphers, ca_file_name to it.
```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 https://localhost:%d now\n" % config.port),
)
app.run()
```
```python
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
```
### Next [CLI Reference](cli.md)

Wyświetl plik

@ -1,9 +1,15 @@
from socketify import App, CompressOptions, OpCode from socketify import App, CompressOptions, OpCode
from queue import SimpleQueue from queue import SimpleQueue
from .native import lib, ffi from .native import lib, ffi
from .tasks import create_task, create_task_with_factory
import os import os
import platform
import sys
is_pypy = platform.python_implementation() == "PyPy"
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*)") @ffi.callback("void(uws_websocket_t*, const char*, size_t, uws_opcode_t, void*)")
def ws_message(ws, message, length, opcode, user_data): def ws_message(ws, message, length, opcode, user_data):
@ -14,25 +20,35 @@ def ws_message(ws, message, length, opcode, user_data):
socket_data.message(ws, message, OpCode(opcode)) socket_data.message(ws, message, OpCode(opcode))
@ffi.callback("void(uws_websocket_t*, int, const char*, size_t, void*)") @ffi.callback("void(uws_websocket_t*, int, const char*, size_t, void*)")
def ws_close(ws, code, message, length, user_data): def ws_close(ws, code, message, length, user_data):
socket_data = ffi.from_handle(user_data) socket_data = ffi.from_handle(user_data)
message = None if message == ffi.NULL else ffi.unpack(message, length) message = None if message == ffi.NULL else ffi.unpack(message, length)
socket_data.disconnect(code, message) socket_data.disconnect(code, message)
@ffi.callback("void(uws_websocket_t*, void*)") @ffi.callback("void(uws_websocket_t*, void*)")
def ws_open(ws, user_data): def ws_open(ws, user_data):
socket_data = ffi.from_handle(user_data) socket_data = ffi.from_handle(user_data)
socket_data.open(ws) socket_data.open(ws)
@ffi.callback("void(int, uws_res_t*, socketify_asgi_ws_data, uws_socket_context_t* socket, void*, bool*)")
def ws_upgrade(ssl, response, info, socket_context, user_data, aborted): @ffi.callback(
"void(int, uws_res_t*, socketify_asgi_ws_data, uws_socket_context_t* socket, void*, bool*)"
)
def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
app = ffi.from_handle(user_data) app = ffi.from_handle(user_data)
headers = [] headers = []
next_header = info.header_list next_header = info.header_list
while next_header != ffi.NULL: while next_header != ffi.NULL:
header = next_header[0] header = next_header[0]
headers.append((ffi.unpack(header.name, header.name_size),ffi.unpack(header.value, header.value_size))) headers.append(
(
ffi.unpack(header.name, header.name_size),
ffi.unpack(header.value, header.value_size),
)
)
next_header = ffi.cast("socketify_header*", next_header.next) next_header = ffi.cast("socketify_header*", next_header.next)
url = ffi.unpack(info.url, info.url_size) url = ffi.unpack(info.url, info.url_size)
@ -40,58 +56,76 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
if info.key == ffi.NULL: if info.key == ffi.NULL:
key = None key = None
else: else:
key = ffi.unpack(info.key, info.key_size).decode('utf8') key = ffi.unpack(info.key, info.key_size).decode("utf8")
if info.protocol == ffi.NULL: if info.protocol == ffi.NULL:
protocol = None protocol = None
else: else:
protocol = ffi.unpack(info.protocol, info.protocol_size).decode('utf8') protocol = ffi.unpack(info.protocol, info.protocol_size).decode("utf8")
if info.extensions == ffi.NULL: if info.extensions == ffi.NULL:
extensions = None extensions = None
else: else:
extensions = ffi.unpack(info.extensions, info.extensions_size).decode('utf8') extensions = ffi.unpack(info.extensions, info.extensions_size).decode("utf8")
compress = app.ws_compression compress = app.ws_compression
ws = ASGIWebSocket(app.server.loop) ws = ASGIWebSocket(app.server.loop)
scope = { scope = {
'type': 'websocket', "type": "websocket",
'asgi': { "asgi": {"version": "3.0", "spec_version": "2.3"},
'version': '3.0', "http_version": "1.1",
'spec_version': '2.3' "server": (app.SERVER_HOST, app.SERVER_PORT),
}, "client": (
'http_version': '1.1', ffi.unpack(info.remote_address, info.remote_address_size).decode("utf8"),
'server': (app.SERVER_HOST, app.SERVER_PORT), None,
'client': (ffi.unpack(info.remote_address, info.remote_address_size).decode('utf8'), None), ),
'scheme': app.SERVER_WS_SCHEME, "scheme": app.SERVER_WS_SCHEME,
'method': ffi.unpack(info.method, info.method_size).decode('utf8'), "method": ffi.unpack(info.method, info.method_size).decode("utf8"),
'root_path': '', "root_path": "",
'path': url.decode('utf8'), "path": url.decode("utf8"),
'raw_path': url, "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),
'headers': headers, "headers": headers,
'subprotocols': [protocol] if protocol else [], "subprotocols": [protocol] if protocol else [],
'extensions': { 'websocket.publish': True, 'websocket.subscribe': True, 'websocket.unsubscribe': True } "extensions": {
"websocket.publish": True,
"websocket.subscribe": True,
"websocket.unsubscribe": True,
},
} }
async def send(options): async def send(options):
if bool(aborted[0]): return False if bool(aborted[0]):
type = options['type'] return False
if type == 'websocket.send': type = options["type"]
data = options.get("bytes", None) if type == "websocket.send":
if ws.ws: data = options.get("bytes", None)
if ws.ws:
if data: if data:
lib.socketify_ws_cork_send_with_options(ssl, ws.ws, data, len(data), int(OpCode.BINARY), int(compress), 0) lib.socketify_ws_cork_send_with_options(
ssl,
ws.ws,
data,
len(data),
int(OpCode.BINARY),
int(compress),
0,
)
else: else:
data = options.get('text', '').encode('utf8') data = options.get("text", "").encode("utf8")
lib.socketify_ws_cork_send_with_options(ssl, ws.ws, data, len(data), int(OpCode.TEXT), int(compress), 0) lib.socketify_ws_cork_send_with_options(
ssl, ws.ws, data, len(data), int(OpCode.TEXT), int(compress), 0
)
return True return True
return False return False
if type == 'websocket.accept': # upgrade! if type == "websocket.accept": # upgrade!
res_headers = options.get('headers', None) res_headers = options.get("headers", None)
if res_headers: if res_headers:
cork_data = ffi.new_handle((ssl, res_headers)) cork_data = ffi.new_handle((ssl, res_headers))
lib.uws_res_cork(ssl, response, uws_asgi_corked_ws_accept_handler, cork_data) lib.uws_res_cork(
ssl, response, uws_asgi_corked_ws_accept_handler, cork_data
)
future = ws.accept() future = ws.accept()
upgrade_protocol = options.get('subprotocol', protocol) upgrade_protocol = options.get("subprotocol", protocol)
if isinstance(key, str): if isinstance(key, str):
sec_web_socket_key_data = key.encode("utf-8") sec_web_socket_key_data = key.encode("utf-8")
@ -113,7 +147,7 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
sec_web_socket_extensions_data = extensions sec_web_socket_extensions_data = extensions
else: else:
sec_web_socket_extensions_data = b"" sec_web_socket_extensions_data = b""
lib.uws_res_upgrade( lib.uws_res_upgrade(
ssl, ssl,
response, response,
@ -127,23 +161,25 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
socket_context, socket_context,
) )
return await future return await future
if type == 'websocket.close': # code and reason? if type == "websocket.close": # code and reason?
if ws.ws: if ws.ws:
lib.uws_ws_close(ssl, ws.ws) lib.uws_ws_close(ssl, ws.ws)
else: else:
cork_data = ffi.new_handle(ssl) cork_data = ffi.new_handle(ssl)
lib.uws_res_cork(ssl, response, uws_asgi_corked_403_handler, cork_data) lib.uws_res_cork(ssl, response, uws_asgi_corked_403_handler, cork_data)
return True return True
if type == 'websocket.publish': # publish extension if type == "websocket.publish": # publish extension
data = options.get("bytes", None) data = options.get("bytes", None)
if data: if data:
app.server.publish(options.get('topic'), data, OpCode.BINARY, compress) app.server.publish(options.get("topic"), data, OpCode.BINARY, compress)
else: else:
app.server.publish(options.get('topic'), options.get('text', ''), OpCode.TEXT, compress) app.server.publish(
options.get("topic"), options.get("text", ""), OpCode.TEXT, compress
)
return True return True
if type == 'websocket.subscribe': # subscribe extension if type == "websocket.subscribe": # subscribe extension
if ws.ws: if ws.ws:
topic = options.get('topic') topic = options.get("topic")
if isinstance(topic, str): if isinstance(topic, str):
data = topic.encode("utf-8") data = topic.encode("utf-8")
elif isinstance(topic, bytes): elif isinstance(topic, bytes):
@ -152,13 +188,13 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
return False return False
return bool(lib.uws_ws_subscribe(ssl, ws.ws, data, len(data))) return bool(lib.uws_ws_subscribe(ssl, ws.ws, data, len(data)))
else: else:
cork_data = ffi.new_handle(ssl) cork_data = ffi.new_handle(ssl)
lib.uws_res_cork(ssl, response, uws_asgi_corked_403_handler, cork_data) lib.uws_res_cork(ssl, response, uws_asgi_corked_403_handler, cork_data)
return True return True
if type == 'websocket.unsubscribe': # unsubscribe extension if type == "websocket.unsubscribe": # unsubscribe extension
if ws.ws: if ws.ws:
topic = options.get('topic') topic = options.get("topic")
if isinstance(topic, str): if isinstance(topic, str):
data = topic.encode("utf-8") data = topic.encode("utf-8")
elif isinstance(topic, bytes): elif isinstance(topic, bytes):
@ -167,12 +203,14 @@ def ws_upgrade(ssl, response, info, socket_context, user_data, aborted):
return False return False
return bool(lib.uws_ws_unsubscribe(ssl, ws.ws, data, len(data))) return bool(lib.uws_ws_unsubscribe(ssl, ws.ws, data, len(data)))
else: else:
cork_data = ffi.new_handle(ssl) cork_data = ffi.new_handle(ssl)
lib.uws_res_cork(ssl, response, uws_asgi_corked_403_handler, cork_data) lib.uws_res_cork(ssl, response, uws_asgi_corked_403_handler, cork_data)
return True return True
return False return False
app.server.run_async(app.app(scope, ws.receive, send))
app._run_task(app.app(scope, ws.receive, send))
@ffi.callback("void(uws_res_t*, const char*, size_t, bool, void*)") @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): def asgi_on_data_handler(res, chunk, chunk_length, is_end, user_data):
@ -180,15 +218,15 @@ def asgi_on_data_handler(res, chunk, chunk_length, is_end, user_data):
data_response.is_end = bool(is_end) data_response.is_end = bool(is_end)
more_body = not data_response.is_end more_body = not data_response.is_end
result = { result = {
'type': 'http.request', "type": "http.request",
'body': b'' if chunk == ffi.NULL else ffi.unpack(chunk, chunk_length), "body": b"" if chunk == ffi.NULL else ffi.unpack(chunk, chunk_length),
'more_body': more_body "more_body": more_body,
} }
data_response.queue.put(result, False) data_response.queue.put(result, False)
data_response.next_data_future.set_result(result) data_response.next_data_future.set_result(result)
if more_body: if more_body:
data_response.next_data_future = data_response.loop.create_future() data_response.next_data_future = data_response.loop.create_future()
class ASGIDataQueue: class ASGIDataQueue:
def __init__(self, loop): def __init__(self, loop):
@ -197,7 +235,7 @@ class ASGIDataQueue:
self.loop = loop self.loop = loop
self.is_end = False self.is_end = False
self.next_data_future = loop.create_future() self.next_data_future = loop.create_future()
class ASGIWebSocket: class ASGIWebSocket:
def __init__(self, loop): def __init__(self, loop):
@ -207,9 +245,7 @@ class ASGIWebSocket:
self._disconnected = False self._disconnected = False
self.receive_queue = SimpleQueue() self.receive_queue = SimpleQueue()
self.miss_receive_queue = SimpleQueue() self.miss_receive_queue = SimpleQueue()
self.miss_receive_queue.put({ self.miss_receive_queue.put({"type": "websocket.connect"}, False)
'type': 'websocket.connect'
}, False)
self._code = None self._code = None
self._message = None self._message = None
self._ptr = ffi.new_handle(self) self._ptr = ffi.new_handle(self)
@ -222,24 +258,25 @@ class ASGIWebSocket:
self.ws = ws self.ws = ws
if not self.accept_future.done(): if not self.accept_future.done():
self.accept_future.set_result(True) self.accept_future.set_result(True)
def receive(self): def receive(self):
future = self.loop.create_future() future = self.loop.create_future()
if not self.miss_receive_queue.empty(): if not self.miss_receive_queue.empty():
future.set_result(self.miss_receive_queue.get(False)) future.set_result(self.miss_receive_queue.get(False))
return future return future
if self._disconnected: if self._disconnected:
future.set_result({ future.set_result(
'type': 'websocket.disconnect', {
'code': self._code, "type": "websocket.disconnect",
'message': self._message "code": self._code,
}) "message": self._message,
}
)
return future return future
self.receive_queue.put(future, False) self.receive_queue.put(future, False)
return future return future
def disconnect(self, code, message): def disconnect(self, code, message):
self.ws = None self.ws = None
self._disconnected = True self._disconnected = True
@ -247,55 +284,57 @@ class ASGIWebSocket:
self._message = message self._message = message
if not self.receive_queue.empty(): if not self.receive_queue.empty():
future = self.receive_queue.get(False) future = self.receive_queue.get(False)
future.set_result({ future.set_result(
'type': 'websocket.disconnect', {"type": "websocket.disconnect", "code": code, "message": message}
'code': code, )
'message': message
})
def message(self, ws, value, opcode): def message(self, ws, value, opcode):
self.ws = ws self.ws = ws
if self.receive_queue.empty(): if self.receive_queue.empty():
if opcode == OpCode.TEXT: if opcode == OpCode.TEXT:
self.miss_receive_queue.put({ self.miss_receive_queue.put(
'type': 'websocket.receive', {"type": "websocket.receive", "text": value}, False
'text': value )
}, False) elif opcode == OpCode.BINARY:
elif opcode == OpCode.BINARY: self.miss_receive_queue.put(
self.miss_receive_queue.put({ {"type": "websocket.receive", "bytes": value}, False
'type': 'websocket.receive', )
'bytes': value
}, False)
return True return True
future = self.receive_queue.get(False) future = self.receive_queue.get(False)
if opcode == OpCode.TEXT: if opcode == OpCode.TEXT:
future.set_result({ future.set_result({"type": "websocket.receive", "text": value})
'type': 'websocket.receive', elif opcode == OpCode.BINARY:
'text': value future.set_result({"type": "websocket.receive", "bytes": value})
})
elif opcode == OpCode.BINARY:
future.set_result({
'type': 'websocket.receive',
'bytes': value
})
def write_header(ssl, res, key, value): def write_header(ssl, res, key, value):
if isinstance(key, str): if isinstance(key, bytes):
if key.lower() == "content-length": return #auto # this is faster than using .lower()
if key.lower() == "transfer-encoding": return #auto if (
key_data = key.encode("utf-8") key == b"content-length"
elif isinstance(key, bytes): or key == b"Content-Length"
if key.lower() == b'content-length': return #auto or key == b"Transfer-Encoding"
if key.lower() == b'transfer-encoding': return #auto or key == b"transfer-encoding"
):
return # auto
key_data = key key_data = key
elif isinstance(key, str):
# this is faster than using .lower()
if (
key == "content-length"
or key == "Content-Length"
or key == "Transfer-Encoding"
or key == "transfer-encoding"
):
return # auto
key_data = key.encode("utf-8")
if isinstance(value, int): if isinstance(value, bytes):
value_data = value
elif isinstance(value, str):
value_data = value.encode("utf-8")
elif isinstance(value, int):
lib.uws_res_write_header_int( lib.uws_res_write_header_int(
ssl, ssl,
res, res,
@ -303,188 +342,232 @@ def write_header(ssl, res, key, value):
len(key_data), len(key_data),
ffi.cast("uint64_t", value), ffi.cast("uint64_t", value),
) )
elif isinstance(value, str):
value_data = value.encode("utf-8")
elif isinstance(value, bytes):
value_data = value
lib.uws_res_write_header( lib.uws_res_write_header(
ssl, res, key_data, len(key_data), value_data, len(value_data) ssl, res, key_data, len(key_data), value_data, len(value_data)
) )
@ffi.callback("void(uws_res_t*, void*)") @ffi.callback("void(uws_res_t*, void*)")
def uws_asgi_corked_response_start_handler(res, user_data): def uws_asgi_corked_response_start_handler(res, user_data):
(ssl, status, headers) = ffi.from_handle(user_data) (ssl, status, headers) = ffi.from_handle(user_data)
lib.socketify_res_write_int_status(ssl, res, int(status)) if status != 200:
lib.socketify_res_write_int_status(ssl, res, int(status))
for name, value in headers: for name, value in headers:
write_header(ssl, res, name, value) write_header(ssl, res, name, value)
write_header(ssl, res, b'Server', b'socketify.py') write_header(ssl, res, b"Server", b"socketify.py")
@ffi.callback("void(uws_res_t*, void*)") @ffi.callback("void(uws_res_t*, void*)")
def uws_asgi_corked_accept_handler(res, user_data): def uws_asgi_corked_accept_handler(res, user_data):
(ssl, status, headers) = ffi.from_handle(user_data) (ssl, status, headers) = ffi.from_handle(user_data)
lib.socketify_res_write_int_status(ssl, res, int(status)) if status != 200:
lib.socketify_res_write_int_status(ssl, res, int(status))
for name, value in headers: for name, value in headers:
write_header(ssl, res, name, value) write_header(ssl, res, name, value)
write_header(ssl, res, b'Server', b'socketify.py') write_header(ssl, res, b"Server", b"socketify.py")
@ffi.callback("void(uws_res_t*, void*)") @ffi.callback("void(uws_res_t*, void*)")
def uws_asgi_corked_ws_accept_handler(res, user_data): def uws_asgi_corked_ws_accept_handler(res, user_data):
(ssl, headers) = ffi.from_handle(user_data) (ssl, headers) = ffi.from_handle(user_data)
for name, value in headers: for name, value in headers:
write_header(ssl, res, name, value) write_header(ssl, res, name, value)
write_header(ssl, res, b'Server', b'socketify.py') write_header(ssl, res, b"Server", b"socketify.py")
@ffi.callback("void(uws_res_t*, void*)") @ffi.callback("void(uws_res_t*, void*)")
def uws_asgi_corked_403_handler(res, user_data): def uws_asgi_corked_403_handler(res, user_data):
ssl = ffi.from_handle(user_data) ssl = ffi.from_handle(user_data)
lib.socketify_res_write_int_status(ssl, res, int(403)) lib.socketify_res_write_int_status(ssl, res, int(403))
lib.uws_res_end_without_body(ssl, res, 0) lib.uws_res_end_without_body(ssl, res, 0)
@ffi.callback("void(int, uws_res_t*, socketify_asgi_data, void*, bool*)") @ffi.callback("void(int, uws_res_t*, socketify_asgi_data, void*, bool*)")
def asgi(ssl, response, info, user_data, aborted): def asgi(ssl, response, info, user_data, aborted):
app = ffi.from_handle(user_data) app = ffi.from_handle(user_data)
headers = [] headers = []
next_header = info.header_list next_header = info.header_list
while next_header != ffi.NULL: while next_header != ffi.NULL:
header = next_header[0] header = next_header[0]
headers.append((ffi.unpack(header.name, header.name_size),ffi.unpack(header.value, header.value_size))) headers.append(
(
ffi.unpack(header.name, header.name_size),
ffi.unpack(header.value, header.value_size),
)
)
next_header = ffi.cast("socketify_header*", next_header.next) next_header = ffi.cast("socketify_header*", next_header.next)
url = ffi.unpack(info.url, info.url_size) url = ffi.unpack(info.url, info.url_size)
scope = { scope = {
'type': 'http', "type": "http",
'asgi': { "asgi": {"version": "3.0", "spec_version": "2.3"},
'version': '3.0', "http_version": "1.1",
'spec_version': '2.3' "server": (app.SERVER_HOST, app.SERVER_PORT),
}, "client": (
'http_version': '1.1', ffi.unpack(info.remote_address, info.remote_address_size).decode("utf8"),
'server': (app.SERVER_HOST, app.SERVER_PORT), None,
'client': (ffi.unpack(info.remote_address, info.remote_address_size).decode('utf8'), None), ),
'scheme': app.SERVER_SCHEME, "scheme": app.SERVER_SCHEME,
'method': ffi.unpack(info.method, info.method_size).decode('utf8'), "method": ffi.unpack(info.method, info.method_size).decode("utf8"),
'root_path': '', "root_path": "",
'path': url.decode('utf8'), "path": url.decode("utf8"),
'raw_path': url, "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),
'headers': headers "headers": headers,
} }
if bool(info.has_content): if bool(info.has_content):
data_queue = ASGIDataQueue(app.server.loop) data_queue = ASGIDataQueue(app.server.loop)
lib.uws_res_on_data( lib.uws_res_on_data(ssl, response, asgi_on_data_handler, data_queue._ptr)
ssl, response, asgi_on_data_handler, data_queue._ptr
)
else: else:
data_queue = None data_queue = None
async def receive(): async def receive():
if bool(aborted[0]): if bool(aborted[0]):
return { 'type': 'http.disconnect'} return {"type": "http.disconnect"}
if data_queue: if data_queue:
if data_queue.queue.empty(): if data_queue.queue.empty():
if not data_queue.is_end: if not data_queue.is_end:
#wait for next item # wait for next item
await data_queue.next_data_future await data_queue.next_data_future
return await receive() #consume again because multiple receives maybe called return (
await receive()
) # consume again because multiple receives maybe called
else: else:
return data_queue.queue.get(False) #consume queue return data_queue.queue.get(False) # consume queue
# no more body, just empty # no more body, just empty
return EMPTY_RESPONSE return EMPTY_RESPONSE
async def send(options): async def send(options):
if bool(aborted[0]): if bool(aborted[0]):
return False return False
type = options['type'] type = options["type"]
if type == 'http.response.start': if type == "http.response.start":
#can also be more native optimized to do it in one GIL call # 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 # try socketify_res_write_int_status_with_headers and create and socketify_res_cork_write_int_status_with_headers
status_code = options.get('status', 200) status_code = options.get("status", 200)
headers = options.get('headers', []) headers = options.get("headers", [])
cork_data = ffi.new_handle((ssl, status_code, headers)) cork_data = ffi.new_handle((ssl, status_code, headers))
lib.uws_res_cork(ssl, response, uws_asgi_corked_response_start_handler, cork_data) lib.uws_res_cork(
ssl, response, uws_asgi_corked_response_start_handler, cork_data
)
return True return True
if type == 'http.response.body': if type == "http.response.body":
#native optimized end/send # native optimized end/send
message = options.get('body', b'') message = options.get("body", b"")
if isinstance(message, str): if options.get("more_body", False):
data = message.encode("utf-8") if isinstance(message, bytes):
elif isinstance(message, bytes): lib.socketify_res_cork_write(ssl, response, message, len(message))
data = message elif isinstance(message, str):
data = message.encode("utf-8")
lib.socketify_res_cork_write(ssl, response, data, len(data))
else: else:
data = b'' if isinstance(message, bytes):
if options.get('more_body', False): lib.socketify_res_cork_end(ssl, response, message, len(message), 0)
lib.socketify_res_cork_write(ssl, response, data, len(data)) elif isinstance(message, str):
else: data = message.encode("utf-8")
lib.socketify_res_cork_end(ssl, response, data, len(data), 0) lib.socketify_res_cork_end(ssl, response, data, len(data), 0)
return True return True
return False return False
app.server.loop.run_async(app.app(scope, receive, send)) app._run_task(app.app(scope, receive, send))
class _ASGI: class _ASGI:
def __init__(self, app, options=None, websocket=True, websocket_options=None): def __init__(self, app, options=None, websocket=True, websocket_options=None, task_factory_max_items=0):
self.server = App(options) self.server = App(options)
self.SERVER_PORT = None self.SERVER_PORT = None
self.SERVER_HOST = '' self.SERVER_HOST = ""
self.SERVER_SCHEME = 'https' if self.server.options else 'http' self.SERVER_SCHEME = "https" if self.server.options else "http"
self.SERVER_WS_SCHEME = 'wss' if self.server.options else 'ws' self.SERVER_WS_SCHEME = "wss" if self.server.options else "ws"
self.task_factory_max_items = task_factory_max_items
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)
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()
self._run_task = run_task
else:
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()
self._run_task = run_task
else:
def run_task(task):
loop.create_task(task)
loop._run_once()
self._run_task = run_task
self.app = app self.app = app
self.ws_compression = False self.ws_compression = False
# optimized in native # optimized in native
self._ptr = ffi.new_handle(self) self._ptr = ffi.new_handle(self)
self.asgi_http_info = lib.socketify_add_asgi_http_handler( self.asgi_http_info = lib.socketify_add_asgi_http_handler(
self.server.SSL, self.server.SSL, self.server.app, asgi, self._ptr
self.server.app,
asgi,
self._ptr
) )
self.asgi_ws_info = None self.asgi_ws_info = None
if isinstance(websocket, dict): #serve websocket as socketify.py if isinstance(websocket, dict): # serve websocket as socketify.py
if websocket_options: if websocket_options:
websocket.update(websocket_options) websocket.update(websocket_options)
self.server.ws("/*", websocket) self.server.ws("/*", websocket)
elif websocket: #serve websocket as ASGI elif websocket: # serve websocket as ASGI
native_options = ffi.new("uws_socket_behavior_t *") native_options = ffi.new("uws_socket_behavior_t *")
native_behavior = native_options[0] native_behavior = native_options[0]
if not websocket_options: if not websocket_options:
websocket_options = {} websocket_options = {}
self.ws_compression = bool(websocket_options.get('compression', False)) self.ws_compression = bool(websocket_options.get("compression", False))
native_behavior.maxPayloadLength = ffi.cast( native_behavior.maxPayloadLength = ffi.cast(
"unsigned int", "unsigned int",
int(websocket_options.get('max_payload_length', 16777216)), int(websocket_options.get("max_payload_length", 16777216)),
) )
native_behavior.idleTimeout = ffi.cast( native_behavior.idleTimeout = ffi.cast(
"unsigned short", "unsigned short",
int(websocket_options.get('idle_timeout', 20)), int(websocket_options.get("idle_timeout", 20)),
) )
native_behavior.maxBackpressure = ffi.cast( native_behavior.maxBackpressure = ffi.cast(
"unsigned int", "unsigned int",
int(websocket_options.get('max_backpressure', 16777216)), int(websocket_options.get("max_backpressure", 16777216)),
) )
native_behavior.compression = ffi.cast( native_behavior.compression = ffi.cast(
"uws_compress_options_t", int(self.ws_compression) "uws_compress_options_t", int(self.ws_compression)
) )
native_behavior.maxLifetime = ffi.cast( native_behavior.maxLifetime = ffi.cast(
"unsigned short", int(websocket_options.get('max_lifetime', 0)) "unsigned short", int(websocket_options.get("max_lifetime", 0))
) )
native_behavior.closeOnBackpressureLimit = ffi.cast( native_behavior.closeOnBackpressureLimit = ffi.cast(
"int", int(websocket_options.get('close_on_backpressure_limit', 0)), "int",
int(websocket_options.get("close_on_backpressure_limit", 0)),
) )
native_behavior.resetIdleTimeoutOnSend = ffi.cast( native_behavior.resetIdleTimeoutOnSend = ffi.cast(
"int", bool(websocket_options.get('reset_idle_timeout_on_send', True)) "int", bool(websocket_options.get("reset_idle_timeout_on_send", True))
) )
native_behavior.sendPingsAutomatically = ffi.cast( native_behavior.sendPingsAutomatically = ffi.cast(
"int", bool(websocket_options.get('send_pings_automatically', True)) "int", bool(websocket_options.get("send_pings_automatically", True))
) )
native_behavior.upgrade = ffi.NULL # will be set first on C++ native_behavior.upgrade = ffi.NULL # will be set first on C++
native_behavior.open = ws_open native_behavior.open = ws_open
native_behavior.message = ws_message native_behavior.message = ws_message
@ -493,19 +576,23 @@ class _ASGI:
native_behavior.close = ws_close native_behavior.close = ws_close
self.asgi_ws_info = lib.socketify_add_asgi_ws_handler( self.asgi_ws_info = lib.socketify_add_asgi_ws_handler(
self.server.SSL, self.server.SSL, self.server.app, native_behavior, ws_upgrade, self._ptr
self.server.app,
native_behavior,
ws_upgrade,
self._ptr
) )
def listen(self, port_or_options, handler=None): def listen(self, port_or_options, handler=None):
self.SERVER_PORT = port_or_options if isinstance(port_or_options, int) else port_or_options.port self.SERVER_PORT = (
self.SERVER_HOST = "0.0.0.0" if isinstance(port_or_options, int) else port_or_options.host port_or_options
if isinstance(port_or_options, int)
else port_or_options.port
)
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) self.server.listen(port_or_options, handler)
return self return self
def run(self): def run(self):
# scope = {"type": "lifespan", "asgi": {"version": "3.0", "spec_version": "2.3"}}
self.server.run() # run app on the main process too :) self.server.run() # run app on the main process too :)
return self return self
@ -515,38 +602,51 @@ class _ASGI:
if self.asgi_ws_info: if self.asgi_ws_info:
lib.socketify_destroy_asgi_ws_app_info(self.asgi_ws_info) lib.socketify_destroy_asgi_ws_app_info(self.asgi_ws_info)
# "Public" ASGI interface to allow easy forks/workers # "Public" ASGI interface to allow easy forks/workers
class ASGI: class ASGI:
def __init__(self, app, options=None, websocket=True, websocket_options=None): def __init__(
self,
app,
options=None,
websocket=True,
websocket_options=None,
task_factory_max_items=100_000, #default = 100k = +20mib in memory
):
self.app = app self.app = app
self.options = options self.options = options
self.websocket = websocket self.websocket = websocket
self.websocket_options = websocket_options self.websocket_options = websocket_options
self.listen_options = None self.listen_options = None
self.task_factory_max_items = task_factory_max_items
def listen(self, port_or_options, handler=None): def listen(self, port_or_options, handler=None):
self.listen_options = (port_or_options, handler) self.listen_options = (port_or_options, handler)
return self return self
def run(self, workers=1): def run(self, workers=1):
def run_app(): def run_task():
server = _ASGI(self.app, self.options, self.websocket, self.websocket_options) server = _ASGI(
self.app,
self.options,
self.websocket,
self.websocket_options,
self.task_factory_max_items,
)
if self.listen_options: if self.listen_options:
(port_or_options, handler) = self.listen_options (port_or_options, handler) = self.listen_options
server.listen(port_or_options, handler) server.listen(port_or_options, handler)
server.run() server.run()
def create_fork(): def create_fork():
n = os.fork() n = os.fork()
# n greater than 0 means parent process # n greater than 0 means parent process
if not n > 0: if not n > 0:
run_app() run_task()
# fork limiting the cpu count - 1 # fork limiting the cpu count - 1
for i in range(1, workers): for i in range(1, workers):
create_fork() create_fork()
run_app() # run app on the main process too :) run_task() # run app on the main process too :)
return self return self

Wyświetl plik

@ -12,6 +12,8 @@ Options:
--port or -p INTEGER Bind socket to this port. [default: 8000] --port or -p INTEGER Bind socket to this port. [default: 8000]
--workers or -w INTEGER Number of worker processes. Defaults to the WEB_CONCURRENCY --workers or -w INTEGER Number of worker processes. Defaults to the WEB_CONCURRENCY
environment variable if available, or 1 environment variable if available, or 1
--uds TEXT Bind to a UNIX domain socket, this options disables --host or -h and --port or -p.
--ws [auto|none|module:ws] If informed module:ws will auto detect to use socketify.py or ASGI websockets --ws [auto|none|module:ws] If informed module:ws will auto detect to use socketify.py or ASGI websockets
interface and disabled if informed none [default: auto] interface and disabled if informed none [default: auto]
--ws-max-size INTEGER WebSocket max size message in bytes [default: 16777216] --ws-max-size INTEGER WebSocket max size message in bytes [default: 16777216]
@ -33,21 +35,19 @@ Options:
--ssl-ciphers TEXT Ciphers to use (see stdlib ssl module's) [default: TLSv1] --ssl-ciphers TEXT Ciphers to use (see stdlib ssl module's) [default: TLSv1]
--req-res-factory-maxitems INT Pre allocated instances of Response and Request objects for socketify interface [default: 0] --req-res-factory-maxitems INT Pre allocated instances of Response and Request objects for socketify interface [default: 0]
--ws-factory-maxitems INT Pre allocated instances of WebSockets objects for socketify interface [default: 0] --ws-factory-maxitems INT Pre allocated instances of WebSockets objects for socketify interface [default: 0]
--uds TEXT Bind to a UNIX domain socket, this options disables --host or -h and --port or -p.
--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]
Example: Example:
python3 -m socketify main:app -w 8 -p 8181 python3 -m socketify main:app -w 8 -p 8181
""" """
# --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): 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): def is_asgi(module):
@ -108,9 +108,14 @@ def execute(args):
if selected_option: if selected_option:
options[selected_option] = option options[selected_option] = option
selected_option = None selected_option = None
elif option.startswith('--'):
if selected_option is None:
selected_option = option
else: # --factory, --reload etc
options[selected_option] = True
else: else:
selected_option = option return print(f"Invalid option ${selected_option} see --help")
if selected_option: # --factory if selected_option: # --factory, --reload etc
options[selected_option] = True options[selected_option] = True
interface = (options.get("--interface", "auto")).lower() interface = (options.get("--interface", "auto")).lower()
@ -262,28 +267,7 @@ def execute(args):
os.kill(pid, signal.SIGINT) os.kill(pid, signal.SIGINT)
else: else:
# def on_change(): if uds:
# auto_reload Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options).listen(AppListenOptions(domain=uds), listen_log).run(workers=workers)
def create_app():
#Generic WSGI, ASGI, SSGI Interface
if uds:
app = Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options).listen(AppListenOptions(domain=uds), listen_log)
else:
app = Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options).listen(AppListenOptions(port=port, host=host), listen_log)
return app
if auto_reload:
force_reload = False
app = None
while auto_reload:
app = create_app()
app.run()
if not force_reload:
auto_reload = False
else: else:
app = create_app() Interface(module,options=ssl_options, websocket=websockets, websocket_options=websocket_options).listen(AppListenOptions(port=port, host=host), listen_log).run(workers=workers)
app.run(workers=workers)

Wyświetl plik

@ -1,14 +1,15 @@
import asyncio import asyncio
import logging import logging
import threading from .tasks import create_task, create_task_with_factory
from .uv import UVLoop from .uv import UVLoop
import asyncio import asyncio
import platform
def future_handler(future, loop, exception_handler, response): is_pypy = platform.python_implementation() == 'PyPy'
async def task_wrapper(exception_handler, loop, response, task):
try: try:
future.result() return await task
return None
except Exception as error: except Exception as error:
if hasattr(exception_handler, "__call__"): if hasattr(exception_handler, "__call__"):
exception_handler(loop, error, response) exception_handler(loop, error, response)
@ -20,12 +21,17 @@ def future_handler(future, loop, exception_handler, response):
response.write_status(500).end("Internal Error") response.write_status(500).end("Internal Error")
finally: finally:
return None return None
return None
class Loop: class Loop:
def __init__(self, exception_handler=None): def __init__(self, exception_handler=None, task_factory_max_items=0):
self.loop = asyncio.get_event_loop()
# get the current running loop or create a new one without warnings
self.loop = asyncio._get_running_loop()
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__"): if hasattr(exception_handler, "__call__"):
@ -37,6 +43,23 @@ class Loop:
self.exception_handler = None self.exception_handler = None
self.started = False 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)
else:
self._task_factory = create_task
self.run_async = self._run_async_pypy
# custom task factory
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
self.run_async = self._run_async_cpython
def set_timeout(self, timeout, callback, user_data): def set_timeout(self, timeout, callback, user_data):
return self.uv_loop.create_timer(timeout, 0, callback, user_data) return self.uv_loop.create_timer(timeout, 0, callback, user_data)
@ -49,12 +72,24 @@ class Loop:
self.uv_loop.run_once() self.uv_loop.run_once()
self.loop.call_soon(self._keep_alive) self.loop.call_soon(self._keep_alive)
def run(self): 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 run(self, task=None):
self.started = True self.started = True
if task is not None:
future = self.ensure_future(task)
else:
future = None
self.loop.call_soon(self._keep_alive) self.loop.call_soon(self._keep_alive)
self.loop.run_forever() self.loop.run_forever()
# clean up uvloop # clean up uvloop
self.uv_loop.stop() self.uv_loop.stop()
return future
def run_once(self): def run_once(self):
# run one step of asyncio # run one step of asyncio
@ -75,19 +110,22 @@ class Loop:
return self.uv_loop.get_native_loop() return self.uv_loop.get_native_loop()
def run_async(self, task, response=None): def _run_async_pypy(self, task, response=None):
# with run_once # this garanties error 500 in case of uncaught exceptions, and can trigger the custom error handler
future = asyncio.ensure_future(task, loop=self.loop) # using an coroutine wrapper generates less overhead than using add_done_callback
# this is an custom task/future with less overhead
# with threads future = self._task_factory(self.loop, task_wrapper(self.exception_handler, self.loop, response, task))
future.add_done_callback(
lambda f: future_handler(f, self.loop, self.exception_handler, response)
)
# force asyncio run once to enable req in async functions before first await # force asyncio run once to enable req in async functions before first await
self.loop._run_once() self.loop._run_once()
return None # this future maybe already done and reused not safe to await
return future 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): def dispose(self):
if self.uv_loop: if self.uv_loop:
self.uv_loop.dispose() self.uv_loop.dispose()

Wyświetl plik

@ -52,12 +52,17 @@ def uws_websocket_factory_drain_handler(ws, user_data):
try: try:
handler = handlers.drain handler = handlers.drain
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
future = app.run_async(handler(ws))
if dispose:
def when_finished(_):
app._ws_factory.dispose(instances)
future.add_done_callback(when_finished) if dispose:
async def wrapper(app, instances, handler, ws):
try:
await handler(ws)
finally:
app._ws_factory.dispose(instances)
app.run_async(wrapper(app, instances, handler, ws))
else:
app.run_async(handler(ws))
else: else:
handler(ws) handler(ws)
if dispose: if dispose:
@ -95,12 +100,16 @@ def uws_websocket_factory_open_handler(ws, user_data):
try: try:
handler = handlers.open handler = handlers.open
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
future = app.run_async(handler(ws))
if dispose: if dispose:
def when_finished(_): async def wrapper(app, instances, handler, ws):
app._ws_factory.dispose(instances) try:
await handler(ws)
finally:
app._ws_factory.dispose(instances)
future.add_done_callback(when_finished) app.run_async(wrapper(app, instances, handler, ws))
else:
app.run_async(handler(ws))
else: else:
handler(ws) handler(ws)
if dispose: if dispose:
@ -147,12 +156,16 @@ def uws_websocket_factory_message_handler(ws, message, length, opcode, user_data
handler = handlers.message handler = handlers.message
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
future = app.run_async(handler(ws, data, opcode))
if dispose: if dispose:
def when_finished(_): async def wrapper(app, instances, handler, ws, data):
app._ws_factory.dispose(instances) try:
await handler(ws, data)
finally:
app._ws_factory.dispose(instances)
future.add_done_callback(when_finished) app.run_async(wrapper(app, instances, handler, ws, data))
else:
app.run_async(handler(ws, data))
else: else:
handler(ws, data, opcode) handler(ws, data, opcode)
if dispose: if dispose:
@ -206,12 +219,16 @@ def uws_websocket_factory_pong_handler(ws, message, length, user_data):
handler = handlers.pong handler = handlers.pong
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
future = app.run_async(handler(ws, data))
if dispose: if dispose:
def when_finished(_): async def wrapper(app, instances, handler, ws, data):
app._ws_factory.dispose(instances) try:
await handler(ws, data)
finally:
app._ws_factory.dispose(instances)
future.add_done_callback(when_finished) app.run_async(wrapper(app, instances, handler, ws, data))
else:
app.run_async(handler(ws, data))
else: else:
handler(ws, data) handler(ws, data)
if dispose: if dispose:
@ -260,12 +277,16 @@ def uws_websocket_factory_ping_handler(ws, message, length, user_data):
handler = handlers.ping handler = handlers.ping
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
future = app.run_async(handler(ws, data))
if dispose: if dispose:
def when_finished(_): async def wrapper(app, instances, handler, ws, data):
app._ws_factory.dispose(instances) try:
await handler(ws, data)
finally:
app._ws_factory.dispose(instances)
future.add_done_callback(when_finished) app.run_async(wrapper(app, instances, handler, ws, data))
else:
app.run_async(handler(ws, data))
else: else:
handler(ws, data) handler(ws, data)
if dispose: if dispose:
@ -323,21 +344,22 @@ def uws_websocket_factory_close_handler(ws, code, message, length, user_data):
return return
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
future = app.run_async(handler(ws, int(code), data)) async def wrapper(app, instances, handler, ws, data, code, dispose):
try:
await handler(ws, code, data)
finally:
key = ws.get_user_data_uuid()
if key is not None:
app._socket_refs.pop(key, None)
if dispose:
app._ws_factory.dispose(instances)
def when_finished(_): app.run_async(wrapper(app, instances, handler, ws, data, int(code), dispose))
key = ws.get_user_data_uuid()
if key is not None:
SocketRefs.pop(key, None)
if dispose:
app._ws_factory.dispose(instances)
future.add_done_callback(when_finished)
else: else:
handler(ws, int(code), data) handler(ws, int(code), data)
key = ws.get_user_data_uuid() key = ws.get_user_data_uuid()
if key is not None: if key is not None:
SocketRefs.pop(key, None) app._socket_refs.pop(key, None)
if dispose: if dispose:
app._ws_factory.dispose(instances) app._ws_factory.dispose(instances)
@ -365,19 +387,20 @@ def uws_websocket_close_handler(ws, code, message, length, user_data):
return return
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
future = app.run_async(handler(ws, int(code), data)) async def wrapper(app, handler, ws, data, code, dispose):
try:
def when_finished(_): await handler(ws, code, data)
key = ws.get_user_data_uuid() finally:
if key is not None: key = ws.get_user_data_uuid()
SocketRefs.pop(key, None) if key is not None:
app._socket_refs.pop(key, None)
future.add_done_callback(when_finished)
app.run_async(wrapper(app, handler, ws, data, int(code)))
else: else:
handler(ws, int(code), data) handler(ws, int(code), data)
key = ws.get_user_data_uuid() key = ws.get_user_data_uuid()
if key is not None: if key is not None:
SocketRefs.pop(key, None) app._socket_refs.pop(key, None)
except Exception as err: except Exception as err:
logging.error( logging.error(
@ -394,12 +417,16 @@ def uws_generic_factory_method_handler(res, req, user_data):
try: try:
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
response.grab_aborted_handler() response.grab_aborted_handler()
future = response.run_async(handler(response, request))
if dispose: if dispose:
def when_finished(_): async def wrapper(app, instances, handler, response, request):
app._factory.dispose(instances) try:
await handler(response, request)
finally:
app._factory.dispose(instances)
future.add_done_callback(when_finished) response.run_async(wrapper(app, instances, handler, response, request))
else:
response.run_async(handler(response, request))
else: else:
handler(response, request) handler(response, request)
if dispose: if dispose:
@ -421,12 +448,17 @@ def uws_websocket_factory_upgrade_handler(res, req, context, user_data):
handler = handlers.upgrade handler = handlers.upgrade
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
future = response.run_async(handler(response, request, context)) response.grab_aborted_handler()
if dispose: if dispose:
def when_finished(_): async def wrapper(app, instances, handler, response, request, context):
app._factory.dispose(instances) try:
await handler(response, request, context)
finally:
app._factadd_done_callbackory.dispose(instances)
future.add_done_callback(when_finished) response.run_async(wrapper(app, instances, handler, response, request, context))
else:
response.run_async(handler(response, request, context))
else: else:
handler(response, request, context) handler(response, request, context)
if dispose: if dispose:
@ -490,12 +522,17 @@ def uws_generic_factory_method_handler(res, req, user_data):
try: try:
if inspect.iscoroutinefunction(handler): if inspect.iscoroutinefunction(handler):
response.grab_aborted_handler() response.grab_aborted_handler()
future = response.run_async(handler(response, request)) response.grab_aborted_handler()
if dispose: if dispose:
def when_finished(_): async def wrapper(app, instances, handler, response, request):
app._factory.dispose(instances) try:
await handler(response, request)
finally:
app._factory.dispose(instances)
future.add_done_callback(when_finished) response.run_async(wrapper(app, instances, handler, response, request))
else:
response.run_async(handler(response, request))
else: else:
handler(response, request) handler(response, request)
if dispose: if dispose:
@ -671,10 +708,6 @@ class SendStatus(IntEnum):
DROPPED = 2 DROPPED = 2
# dict to keep socket data alive until closed if needed
SocketRefs = {}
class WebSocket: class WebSocket:
def __init__(self, websocket, ssl, loop): def __init__(self, websocket, ssl, loop):
self.ws = websocket self.ws = websocket
@ -1150,6 +1183,9 @@ class AppRequest:
return self._headers return self._headers
def get_header(self, lower_case_header): def get_header(self, lower_case_header):
if self._headers is not None:
return self._headers.get(lower_case_header, None)
if isinstance(lower_case_header, str): if isinstance(lower_case_header, str):
data = lower_case_header.encode("utf-8") data = lower_case_header.encode("utf-8")
elif isinstance(lower_case_header, bytes): elif isinstance(lower_case_header, bytes):
@ -1763,11 +1799,13 @@ class AppResponse:
class App: class App:
def __init__(self, options=None, request_response_factory_max_items=0, websocket_factory_max_items=0): def __init__(self, options=None, request_response_factory_max_items=0, websocket_factory_max_items=0, task_factory_max_items=100_000):
socket_options_ptr = ffi.new("struct us_socket_context_options_t *") socket_options_ptr = ffi.new("struct us_socket_context_options_t *")
socket_options = socket_options_ptr[0] socket_options = socket_options_ptr[0]
self.options = options self.options = options
self._template = None self._template = None
# keep socket data alive for CFFI
self._socket_refs = {}
if options is not None: if options is not None:
self.is_ssl = True self.is_ssl = True
self.SSL = ffi.cast("int", 1) self.SSL = ffi.cast("int", 1)
@ -1810,7 +1848,8 @@ class App:
self.loop = Loop( self.loop = Loop(
lambda loop, context, response: self.trigger_error(context, response, None) lambda loop, context, response: self.trigger_error(context, response, None),
task_factory_max_items
) )
# set async loop to be the last created (is thread_local), App must be one per thread otherwise will use only the lasted loop # set async loop to be the last created (is thread_local), App must be one per thread otherwise will use only the lasted loop

Wyświetl plik

@ -187,8 +187,8 @@ class SSGIWebSocket:
self._close_handler = on_close_handler self._close_handler = on_close_handler
class SSGI: class SSGI:
def __init__(self, app, options=None, request_response_factory_max_itens=0, websocket_factory_max_itens=0): 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_itens, websocket_factory_max_itens) self.server = App(options, request_response_factory_max_items, websocket_factory_max_itens)
self.SERVER_PORT = None self.SERVER_PORT = None
self.SERVER_HOST = '' self.SERVER_HOST = ''
self.SERVER_SCHEME = 'https' if self.server.options else 'http' self.SERVER_SCHEME = 'https' if self.server.options else 'http'

Wyświetl plik

@ -5,14 +5,20 @@ from .asgi import ws_close, ws_upgrade, ws_open, ws_message
from io import BytesIO from io import BytesIO
from .native import lib, ffi from .native import lib, ffi
@ffi.callback("void(uws_res_t*, const char*, size_t, bool, void*)") @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): def wsgi_on_data_handler(res, chunk, chunk_length, is_end, user_data):
data_response = ffi.from_handle(user_data) data_response = ffi.from_handle(user_data)
if chunk != ffi.NULL: if chunk != ffi.NULL:
data_response.buffer.write(ffi.unpack(chunk, chunk_length)) data_response.buffer.write(ffi.unpack(chunk, chunk_length))
if bool(is_end): if bool(is_end):
lib.uws_res_cork(data_response.app.server.SSL, res, wsgi_corked_response_start_handler, data_response._ptr) lib.uws_res_cork(
data_response.app.server.SSL,
res,
wsgi_corked_response_start_handler,
data_response._ptr,
)
class WSGIDataResponse: class WSGIDataResponse:
def __init__(self, app, environ, start_response, aborted, buffer, on_data): def __init__(self, app, environ, start_response, aborted, buffer, on_data):
@ -24,138 +30,157 @@ class WSGIDataResponse:
self.app = app self.app = app
self.start_response = start_response self.start_response = start_response
def write(ssl, res, message):
if isinstance(message, str):
data = message.encode("utf-8")
elif isinstance(message, bytes):
data = message
else:
data = b''
lib.uws_res_write(ssl, res, data, len(data))
def write_status(ssl, res, status_text):
if isinstance(status_text, str):
data = status_text.encode("utf-8")
elif isinstance(status_text, bytes):
data = status_text
else:
return False
lib.uws_res_write_status(ssl, res, data, len(data))
return True
def write_header(ssl, res, key, value):
if isinstance(key, str):
if key.lower() == "content-length": return #auto
if key.lower() == "transfer-encoding": return #auto
key_data = key.encode("utf-8")
elif isinstance(key, bytes):
if key.lower() == b'content-length': return #auto
if key.lower() == b'transfer-encoding': return #auto
key_data = key
if isinstance(value, int):
lib.uws_res_write_header_int(
ssl,
res,
key_data,
len(key_data),
ffi.cast("uint64_t", value),
)
elif isinstance(value, str):
value_data = value.encode("utf-8")
elif isinstance(value, bytes):
value_data = value
lib.uws_res_write_header(
ssl, res, key_data, len(key_data), value_data, len(value_data)
)
@ffi.callback("void(uws_res_t*, void*)") @ffi.callback("void(uws_res_t*, void*)")
def wsgi_corked_response_start_handler(res, user_data): def wsgi_corked_response_start_handler(res, user_data):
data_response = ffi.from_handle(user_data) data_response = ffi.from_handle(user_data)
data_response.on_data(data_response, res) 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):
app = ffi.from_handle(user_data)
@ffi.callback("void(int, uws_res_t*, socketify_asgi_data request, void*, bool*)")
def wsgi(ssl, response, info, user_data, aborted):
app = ffi.from_handle(user_data)
# reusing the dict is slower than cloning because we need to clear HTTP headers
environ = dict(app.BASIC_ENVIRON) environ = dict(app.BASIC_ENVIRON)
environ['REQUEST_METHOD'] = ffi.unpack(info.method, info.method_size).decode('utf8') environ["REQUEST_METHOD"] = ffi.unpack(info.method, info.method_size).decode("utf8")
environ['PATH_INFO'] = ffi.unpack(info.url, info.url_size).decode('utf8') 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') environ["QUERY_STRING"] = ffi.unpack(
environ['REMOTE_ADDR'] = ffi.unpack(info.remote_address, info.remote_address_size).decode('utf8') info.query_string, info.query_string_size
).decode("utf8")
if info.remote_address != ffi.NULL:
environ["REMOTE_ADDR"] = ffi.unpack(
info.remote_address, info.remote_address_size
).decode("utf8")
else:
environ["REMOTE_ADDR"] = "127.0.0.1"
next_header = info.header_list next_header = info.header_list
while next_header != ffi.NULL: while next_header != ffi.NULL:
header = next_header[0] header = next_header[0]
name = ffi.unpack(header.name, header.name_size).decode('utf8') name = ffi.unpack(header.name, header.name_size).decode("utf8")
value = ffi.unpack(header.value, header.value_size).decode('utf8') value = ffi.unpack(header.value, header.value_size).decode("utf8")
# this conversion should be optimized in future # this conversion should be optimized in future
environ[f"HTTP_{name.replace('-', '_').upper()}"]=value environ[f"HTTP_{name.replace('-', '_').upper()}"] = value
next_header = ffi.cast("socketify_header*", next_header.next) next_header = ffi.cast("socketify_header*", next_header.next)
def start_response(status, headers): def start_response(status, headers):
write_status(ssl, response, status) if isinstance(status, str):
for (name, value) in headers: data = status.encode("utf-8")
write_header(ssl, response, name, value) lib.uws_res_write_status(ssl, response, data, len(data))
write_header(ssl, response, b'Server', b'socketify.py') elif isinstance(status, bytes):
lib.uws_res_write_status(ssl, response, status, len(status))
for (key, value) in headers:
if isinstance(key, str):
# this is faster than using .lower()
if (
key == "content-length"
or key == "Content-Length"
or key == "Transfer-Encoding"
or key == "transfer-encoding"
):
continue # auto
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"
or key == b"transfer-encoding"
):
continue # auto
key_data = key
if isinstance(value, str):
value_data = value.encode("utf-8")
elif isinstance(value, bytes):
value_data = value
elif isinstance(value, int):
lib.uws_res_write_header_int(
ssl,
response,
key_data,
len(key_data),
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
)
# check for body # check for body
if bool(info.has_content): if bool(info.has_content):
WSGI_INPUT = BytesIO() WSGI_INPUT = BytesIO()
environ['wsgi.input'] = WSGI_INPUT environ["wsgi.input"] = WSGI_INPUT
def on_data(data_response, response): def on_data(data_response, response):
if bool(data_response.aborted[0]): if bool(data_response.aborted[0]):
return return
ssl = data_response.app.server.SSL ssl = data_response.app.server.SSL
app_iter = data_response.app.app(data_response.environ, data_response.start_response) app_iter = data_response.app.app(
data_response.environ, data_response.start_response
)
try: try:
for data in app_iter: for data in app_iter:
write(ssl, response, data) 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))
finally: finally:
if hasattr(app_iter, 'close'): if hasattr(app_iter, "close"):
app_iter.close() app_iter.close()
lib.uws_res_end_without_body(ssl, response, 0) lib.uws_res_end_without_body(ssl, response, 0)
data_response = WSGIDataResponse(app, environ, start_response, aborted, WSGI_INPUT, on_data)
lib.uws_res_on_data( data_response = WSGIDataResponse(
ssl, response, wsgi_on_data_handler, data_response._ptr app, environ, start_response, aborted, WSGI_INPUT, on_data
) )
lib.uws_res_on_data(ssl, response, wsgi_on_data_handler, data_response._ptr)
else: else:
environ['wsgi.input'] = None environ["wsgi.input"] = None
app_iter = app.app(environ, start_response) app_iter = app.app(environ, start_response)
try: try:
for data in app_iter: for data in app_iter:
write(ssl, response, data) 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))
finally: finally:
if hasattr(app_iter, 'close'): if hasattr(app_iter, "close"):
app_iter.close() app_iter.close()
lib.uws_res_end_without_body(ssl, response, 0) lib.uws_res_end_without_body(ssl, response, 0)
class _WSGI: class _WSGI:
def __init__(self, app, options=None, websocket=None, websocket_options=None): def __init__(self, app, options=None, websocket=None, websocket_options=None):
self.server = App(options) self.server = App(options)
self.SERVER_HOST = None self.SERVER_HOST = None
self.SERVER_PORT = 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.app = app self.app = app
self.BASIC_ENVIRON = dict(os.environ) self.BASIC_ENVIRON = dict(os.environ)
self.ws_compression = False self.ws_compression = False
self._ptr = ffi.new_handle(self) self._ptr = ffi.new_handle(self)
self.asgi_http_info = lib.socketify_add_asgi_http_handler( self.asgi_http_info = lib.socketify_add_asgi_http_handler(
self.server.SSL, self.server.SSL, self.server.app, wsgi, self._ptr
self.server.app,
wsgi,
self._ptr
) )
self.asgi_ws_info = None self.asgi_ws_info = None
if isinstance(websocket, dict): #serve websocket as socketify.py if isinstance(websocket, dict): # serve websocket as socketify.py
if websocket_options: if websocket_options:
websocket.update(websocket_options) websocket.update(websocket_options)
self.server.ws("/*", websocket) self.server.ws("/*", websocket)
elif inspect.iscoroutine(websocket): elif inspect.iscoroutine(websocket):
# detect ASGI to use as WebSocket as mixed protocol # detect ASGI to use as WebSocket as mixed protocol
@ -165,37 +190,38 @@ class _WSGI:
if not websocket_options: if not websocket_options:
websocket_options = {} websocket_options = {}
self.ws_compression = websocket_options.get('compression', False) self.ws_compression = websocket_options.get("compression", False)
native_behavior.maxPayloadLength = ffi.cast( native_behavior.maxPayloadLength = ffi.cast(
"unsigned int", "unsigned int",
int(websocket_options.get('max_payload_length', 16777216)), int(websocket_options.get("max_payload_length", 16777216)),
) )
native_behavior.idleTimeout = ffi.cast( native_behavior.idleTimeout = ffi.cast(
"unsigned short", "unsigned short",
int(websocket_options.get('idle_timeout', 20)), int(websocket_options.get("idle_timeout", 20)),
) )
native_behavior.maxBackpressure = ffi.cast( native_behavior.maxBackpressure = ffi.cast(
"unsigned int", "unsigned int",
int(websocket_options.get('max_backpressure', 16777216)), int(websocket_options.get("max_backpressure", 16777216)),
) )
native_behavior.compression = ffi.cast( native_behavior.compression = ffi.cast(
"uws_compress_options_t", int(self.ws_compression) "uws_compress_options_t", int(self.ws_compression)
) )
native_behavior.maxLifetime = ffi.cast( native_behavior.maxLifetime = ffi.cast(
"unsigned short", int(websocket_options.get('max_lifetime', 0)) "unsigned short", int(websocket_options.get("max_lifetime", 0))
) )
native_behavior.closeOnBackpressureLimit = ffi.cast( native_behavior.closeOnBackpressureLimit = ffi.cast(
"int", int(websocket_options.get('close_on_backpressure_limit', 0)), "int",
int(websocket_options.get("close_on_backpressure_limit", 0)),
) )
native_behavior.resetIdleTimeoutOnSend = ffi.cast( native_behavior.resetIdleTimeoutOnSend = ffi.cast(
"int", bool(websocket_options.get('reset_idle_timeout_on_send', True)) "int", bool(websocket_options.get("reset_idle_timeout_on_send", True))
) )
native_behavior.sendPingsAutomatically = ffi.cast( native_behavior.sendPingsAutomatically = ffi.cast(
"int", bool(websocket_options.get('send_pings_automatically', True)) "int", bool(websocket_options.get("send_pings_automatically", True))
) )
native_behavior.upgrade = ffi.NULL # will be set first on C++ native_behavior.upgrade = ffi.NULL # will be set first on C++
native_behavior.open = ws_open native_behavior.open = ws_open
native_behavior.message = ws_message native_behavior.message = ws_message
@ -204,35 +230,39 @@ class _WSGI:
native_behavior.close = ws_close native_behavior.close = ws_close
self.asgi_ws_info = lib.socketify_add_asgi_ws_handler( self.asgi_ws_info = lib.socketify_add_asgi_ws_handler(
self.server.SSL, self.server.SSL, self.server.app, native_behavior, ws_upgrade, self._ptr
self.server.app,
native_behavior,
ws_upgrade,
self._ptr
) )
def listen(self, port_or_options, handler=None): def listen(self, port_or_options, handler=None):
self.SERVER_PORT = port_or_options if isinstance(port_or_options, int) else port_or_options.port self.SERVER_PORT = (
self.SERVER_HOST = "0.0.0.0" if isinstance(port_or_options, int) else port_or_options.host port_or_options
self.BASIC_ENVIRON.update({ if isinstance(port_or_options, int)
'GATEWAY_INTERFACE': 'CGI/1.1', else port_or_options.port
'SERVER_PORT': str(self.SERVER_PORT), )
'SERVER_SOFTWARE': 'WSGIServer/0.2', self.SERVER_HOST = (
'wsgi.input': None, "0.0.0.0" if isinstance(port_or_options, int) else port_or_options.host
'wsgi.errors': None, )
'wsgi.version': (1, 0), self.BASIC_ENVIRON.update(
'wsgi.run_once': False, {
'wsgi.url_scheme': 'https' if self.server.options else 'http', "GATEWAY_INTERFACE": "CGI/1.1",
'wsgi.multithread': False, "SERVER_PORT": str(self.SERVER_PORT),
'wsgi.multiprocess': False, "SERVER_SOFTWARE": "WSGIServer/0.2",
'wsgi.file_wrapper': None, # No file wrapper support for now "wsgi.input": None,
'SCRIPT_NAME': '', "wsgi.errors": None,
'SERVER_PROTOCOL': 'HTTP/1.1', "wsgi.version": (1, 0),
'REMOTE_HOST': '', "wsgi.run_once": False,
}) "wsgi.url_scheme": "https" if self.server.options else "http",
"wsgi.multithread": False,
"wsgi.multiprocess": False,
"wsgi.file_wrapper": None, # No file wrapper support for now
"SCRIPT_NAME": "",
"SERVER_PROTOCOL": "HTTP/1.1",
"REMOTE_HOST": "",
}
)
self.server.listen(port_or_options, handler) self.server.listen(port_or_options, handler)
return self return self
def run(self): def run(self):
self.server.run() self.server.run()
return self return self
@ -243,6 +273,7 @@ class _WSGI:
if self.asgi_ws_info: if self.asgi_ws_info:
lib.socketify_destroy_asgi_ws_app_info(self.asgi_ws_info) lib.socketify_destroy_asgi_ws_app_info(self.asgi_ws_info)
# "Public" WSGI interface to allow easy forks/workers # "Public" WSGI interface to allow easy forks/workers
class WSGI: class WSGI:
def __init__(self, app, options=None, websocket=None, websocket_options=None): def __init__(self, app, options=None, websocket=None, websocket_options=None):
@ -251,31 +282,30 @@ class WSGI:
self.websocket = websocket self.websocket = websocket
self.websocket_options = websocket_options self.websocket_options = websocket_options
self.listen_options = None self.listen_options = None
def listen(self, port_or_options, handler=None): def listen(self, port_or_options, handler=None):
self.listen_options = (port_or_options, handler) self.listen_options = (port_or_options, handler)
return self return self
def run(self, workers=1): def run(self, workers=1):
def run_app(): def run_app():
server = _WSGI(self.app, self.options, self.websocket, self.websocket_options) server = _WSGI(
self.app, self.options, self.websocket, self.websocket_options
)
if self.listen_options: if self.listen_options:
(port_or_options, handler) = self.listen_options (port_or_options, handler) = self.listen_options
server.listen(port_or_options, handler) server.listen(port_or_options, handler)
server.run() server.run()
def create_fork(): def create_fork():
n = os.fork() n = os.fork()
# n greater than 0 means parent process # n greater than 0 means parent process
if not n > 0: if not n > 0:
run_app() run_app()
# fork limiting the cpu count - 1 # fork limiting the cpu count - 1
for i in range(1, workers): for i in range(1, workers):
create_fork() create_fork()
run_app() # run app on the main process too :) run_app() # run app on the main process too :)
return self return self